@karmaniverous/jeeves-meta 0.15.10 → 0.15.12

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.
@@ -4,6 +4,17 @@
4
4
  * @module bootstrap
5
5
  */
6
6
  import { type ServiceConfig } from './schema/config.js';
7
+ /**
8
+ * Compute per-cycle token total from a completed meta.
9
+ *
10
+ * Exported for testing.
11
+ *
12
+ * Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
13
+ * a value of 1 means architect ran this cycle (was 0 pre-increment),
14
+ * so all three phase token fields are summed. A value \> 1 means architect
15
+ * was skipped (cached brief reused), so only builder + critic are summed.
16
+ */
17
+ export declare function computeCycleTokens(meta: Record<string, unknown>): number;
7
18
  /**
8
19
  * Bootstrap the service: create logger, build server, start listening,
9
20
  * wire scheduler, queue processing, rule registration, config hot-reload,
@@ -2337,7 +2337,7 @@ function buildPhaseCandidates(entries, architectEvery) {
2337
2337
  if (entry.meta._phaseState) {
2338
2338
  const needsArchitect = !entry.meta._builder ||
2339
2339
  (entry.meta._synthesisCount ?? 0) >= architectEvery;
2340
- if (needsArchitect && ps.architect === 'fresh') {
2340
+ if (needsArchitect && isFullyFresh(ps)) {
2341
2341
  ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
2342
2342
  }
2343
2343
  }
@@ -2573,6 +2573,9 @@ async function persistPhaseState(base, phaseState, updates) {
2573
2573
  _phaseState: phaseState,
2574
2574
  _structureHash: base.structureHash,
2575
2575
  };
2576
+ if (!merged._id) {
2577
+ merged._id = randomUUID();
2578
+ }
2576
2579
  // Clean undefined
2577
2580
  if (merged._error === undefined)
2578
2581
  delete merged._error;
@@ -2678,7 +2681,33 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2678
2681
  timeout: currentMeta._builderTimeout ?? config.builderTimeout,
2679
2682
  label: 'meta-builder',
2680
2683
  });
2681
- const builderOutput = parseBuilderOutput(result.output);
2684
+ const rawOutput = result.output;
2685
+ // Exact match only — ANNOUNCE_SKIP as the entire output means "no update."
2686
+ // A trailing sentinel on valid output (e.g. JSON + ANNOUNCE_SKIP) is handled
2687
+ // by stripSentinel() inside parseBuilderOutput and is NOT a skip.
2688
+ const isSkip = rawOutput.trim() === 'ANNOUNCE_SKIP';
2689
+ if (isSkip) {
2690
+ // ANNOUNCE_SKIP: preserve existing _content, bump _generatedAt only
2691
+ ps = builderSuccess(ps);
2692
+ const skipUpdates = {
2693
+ _builderTokens: result.tokens,
2694
+ _generatedAt: new Date().toISOString(),
2695
+ _error: undefined,
2696
+ };
2697
+ const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
2698
+ if (ancestorHash)
2699
+ skipUpdates._ancestorBuilderHash = ancestorHash;
2700
+ const updatedMeta = await persistPhaseState(base, ps, skipUpdates);
2701
+ await onProgress?.({
2702
+ type: 'phase_complete',
2703
+ path: node.ownerPath,
2704
+ phase: 'builder',
2705
+ tokens: result.tokens,
2706
+ durationMs: Date.now() - builderStart,
2707
+ });
2708
+ return { executed: true, phaseState: ps, updatedMeta };
2709
+ }
2710
+ const builderOutput = parseBuilderOutput(rawOutput);
2682
2711
  const builderTokens = result.tokens;
2683
2712
  // Builder success: builder → fresh, critic → pending
2684
2713
  ps = builderSuccess(ps);
@@ -2798,6 +2827,28 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
2798
2827
  *
2799
2828
  * @module orchestrator/orchestratePhase
2800
2829
  */
2830
+ /**
2831
+ * Check whether a meta has an empty scope — no source files, no children,
2832
+ * no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
2833
+ */
2834
+ function isEmptyScope(scopeFiles, node, meta) {
2835
+ return (scopeFiles.length === 0 &&
2836
+ node.children.length === 0 &&
2837
+ (!meta._crossRefs || meta._crossRefs.length === 0) &&
2838
+ !meta._content);
2839
+ }
2840
+ /**
2841
+ * Handle an empty-scope meta: set all phases fresh, bump _generatedAt.
2842
+ * Prevents perpetual staleness without wasting tokens on synthesis.
2843
+ */
2844
+ async function handleEmptyScope(node, currentMeta, config, structureHash, logger) {
2845
+ await persistPhaseState({
2846
+ metaPath: node.metaPath,
2847
+ current: currentMeta,
2848
+ structureHash,
2849
+ }, freshPhaseState(), { _generatedAt: new Date().toISOString() });
2850
+ logger?.info({ path: node.ownerPath }, 'Empty scope — set all phases fresh, bumped _generatedAt');
2851
+ }
2801
2852
  /** Phase runner dispatch map — avoids repeating the same switch/case. */
2802
2853
  const phaseRunners = {
2803
2854
  architect: runArchitect,
@@ -2853,6 +2904,11 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
2853
2904
  // Compute structure hash for the phase
2854
2905
  const { scopeFiles } = await getScopeFiles(winner.node, watcher);
2855
2906
  const structureHash = computeStructureHash(scopeFiles);
2907
+ // Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
2908
+ if (isEmptyScope(scopeFiles, winner.node, currentMeta)) {
2909
+ await handleEmptyScope(winner.node, currentMeta, config, structureHash, logger);
2910
+ return { executed: false };
2911
+ }
2856
2912
  // skipUnchanged: bump _generatedAt without altering _phaseState
2857
2913
  if (config.skipUnchanged && currentMeta._generatedAt) {
2858
2914
  const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
@@ -2894,6 +2950,11 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
2894
2950
  // Compute structure hash
2895
2951
  const { scopeFiles } = await getScopeFiles(node, watcher);
2896
2952
  const structureHash = computeStructureHash(scopeFiles);
2953
+ // Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
2954
+ if (isEmptyScope(scopeFiles, node, currentMeta)) {
2955
+ await handleEmptyScope(node, currentMeta, config, structureHash, logger);
2956
+ return { executed: false, metaPath: normalizedTarget };
2957
+ }
2897
2958
  return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
2898
2959
  }
2899
2960
  finally {
@@ -4056,6 +4117,12 @@ function registerConfigApplyRoute(app, configPath) {
4056
4117
  .status(500)
4057
4118
  .send({ error: `Failed to write config: ${message}` });
4058
4119
  }
4120
+ // Compute whether any restart-required fields actually changed
4121
+ // (compares validated output against previous config, catching both
4122
+ // explicit patch keys and implicit changes via replace + schema defaults)
4123
+ const validatedRecord = validatedConfig;
4124
+ const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
4125
+ JSON.stringify(validatedRecord[field]));
4059
4126
  // Apply hot-reload callback
4060
4127
  try {
4061
4128
  applyHotReloadedConfig(validatedConfig);
@@ -4065,11 +4132,12 @@ function registerConfigApplyRoute(app, configPath) {
4065
4132
  return reply.status(200).send({
4066
4133
  applied: true,
4067
4134
  warning: `Config written but callback failed: ${message}`,
4068
- restartRequired: RESTART_REQUIRED_FIELDS,
4135
+ restartRequired,
4069
4136
  });
4070
4137
  }
4071
4138
  return reply.status(200).send({
4072
4139
  applied: true,
4140
+ restartRequired,
4073
4141
  });
4074
4142
  });
4075
4143
  }
@@ -4330,6 +4398,7 @@ function registerMetasUpdateRoute(app, deps) {
4330
4398
  }
4331
4399
  Object.assign(updated, toSet);
4332
4400
  await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
4401
+ deps.cache.invalidate();
4333
4402
  // Project the response — exclude the same large fields as the detail route.
4334
4403
  const projected = {};
4335
4404
  for (const [k, v] of Object.entries(updated)) {
@@ -4367,10 +4436,10 @@ function registerPreviewRoute(app, deps) {
4367
4436
  const normalized = normalizePath(query.path);
4368
4437
  targetNode = findNode(result.tree, normalized);
4369
4438
  if (!targetNode) {
4370
- return {
4439
+ return reply.status(404).send({
4371
4440
  error: 'NOT_FOUND',
4372
4441
  message: 'Meta path not found: ' + query.path,
4373
- };
4442
+ });
4374
4443
  }
4375
4444
  }
4376
4445
  else {
@@ -4940,7 +5009,7 @@ function registerRoutes(app, deps) {
4940
5009
  });
4941
5010
  registerStatusRoute(app, deps);
4942
5011
  registerMetasRoutes(app, deps);
4943
- registerMetasUpdateRoute(app);
5012
+ registerMetasUpdateRoute(app, deps);
4944
5013
  registerSynthesizeRoute(app, deps);
4945
5014
  registerPreviewRoute(app, deps);
4946
5015
  registerSeedRoute(app, deps);
@@ -5027,6 +5096,18 @@ function registerShutdownHandlers(deps) {
5027
5096
  deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
5028
5097
  }
5029
5098
  }
5099
+ // Release lock for in-progress override synthesis (only when it
5100
+ // differs from the legacy current item to avoid double-release)
5101
+ const currentPhase = deps.queue.currentPhase;
5102
+ if (currentPhase && currentPhase.path !== current?.path) {
5103
+ try {
5104
+ releaseLock(resolveMetaDir(currentPhase.path));
5105
+ deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress override synthesis');
5106
+ }
5107
+ catch {
5108
+ deps.logger.warn({ path: currentPhase.path }, 'Failed to release override lock during shutdown');
5109
+ }
5110
+ }
5030
5111
  // 3. Close server
5031
5112
  try {
5032
5113
  await deps.server.close();
@@ -5124,6 +5205,25 @@ class HttpWatcherClient {
5124
5205
  *
5125
5206
  * @module bootstrap
5126
5207
  */
5208
+ /**
5209
+ * Compute per-cycle token total from a completed meta.
5210
+ *
5211
+ * Exported for testing.
5212
+ *
5213
+ * Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
5214
+ * a value of 1 means architect ran this cycle (was 0 pre-increment),
5215
+ * so all three phase token fields are summed. A value \> 1 means architect
5216
+ * was skipped (cached brief reused), so only builder + critic are summed.
5217
+ */
5218
+ function computeCycleTokens(meta) {
5219
+ const builderTokens = meta._builderTokens ?? 0;
5220
+ const criticTokens = meta._criticTokens ?? 0;
5221
+ const architectRan = (meta._synthesisCount ?? 1) === 1;
5222
+ const architectTokens = architectRan
5223
+ ? (meta._architectTokens ?? 0)
5224
+ : 0;
5225
+ return architectTokens + builderTokens + criticTokens;
5226
+ }
5127
5227
  /**
5128
5228
  * Bootstrap the service: create logger, build server, start listening,
5129
5229
  * wire scheduler, queue processing, rule registration, config hot-reload,
@@ -5250,10 +5350,15 @@ async function startService(config, configPath) {
5250
5350
  }
5251
5351
  // Emit synthesis_complete only on full-cycle completion
5252
5352
  if (result.cycleComplete) {
5353
+ const updatedMeta = result.phaseResult?.updatedMeta;
5354
+ const tokens = updatedMeta
5355
+ ? computeCycleTokens(updatedMeta)
5356
+ : undefined;
5253
5357
  await progress.report({
5254
5358
  type: 'synthesis_complete',
5255
5359
  path: ownerPath,
5256
5360
  durationMs,
5361
+ tokens,
5257
5362
  });
5258
5363
  }
5259
5364
  }
package/dist/index.js CHANGED
@@ -2595,7 +2595,7 @@ function buildPhaseCandidates(entries, architectEvery) {
2595
2595
  if (entry.meta._phaseState) {
2596
2596
  const needsArchitect = !entry.meta._builder ||
2597
2597
  (entry.meta._synthesisCount ?? 0) >= architectEvery;
2598
- if (needsArchitect && ps.architect === 'fresh') {
2598
+ if (needsArchitect && isFullyFresh(ps)) {
2599
2599
  ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
2600
2600
  }
2601
2601
  }
@@ -2831,6 +2831,9 @@ async function persistPhaseState(base, phaseState, updates) {
2831
2831
  _phaseState: phaseState,
2832
2832
  _structureHash: base.structureHash,
2833
2833
  };
2834
+ if (!merged._id) {
2835
+ merged._id = randomUUID();
2836
+ }
2834
2837
  // Clean undefined
2835
2838
  if (merged._error === undefined)
2836
2839
  delete merged._error;
@@ -2936,7 +2939,33 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
2936
2939
  timeout: currentMeta._builderTimeout ?? config.builderTimeout,
2937
2940
  label: 'meta-builder',
2938
2941
  });
2939
- const builderOutput = parseBuilderOutput(result.output);
2942
+ const rawOutput = result.output;
2943
+ // Exact match only — ANNOUNCE_SKIP as the entire output means "no update."
2944
+ // A trailing sentinel on valid output (e.g. JSON + ANNOUNCE_SKIP) is handled
2945
+ // by stripSentinel() inside parseBuilderOutput and is NOT a skip.
2946
+ const isSkip = rawOutput.trim() === 'ANNOUNCE_SKIP';
2947
+ if (isSkip) {
2948
+ // ANNOUNCE_SKIP: preserve existing _content, bump _generatedAt only
2949
+ ps = builderSuccess(ps);
2950
+ const skipUpdates = {
2951
+ _builderTokens: result.tokens,
2952
+ _generatedAt: new Date().toISOString(),
2953
+ _error: undefined,
2954
+ };
2955
+ const ancestorHash = hashAncestorBuilder(ctx.ancestorBuilder);
2956
+ if (ancestorHash)
2957
+ skipUpdates._ancestorBuilderHash = ancestorHash;
2958
+ const updatedMeta = await persistPhaseState(base, ps, skipUpdates);
2959
+ await onProgress?.({
2960
+ type: 'phase_complete',
2961
+ path: node.ownerPath,
2962
+ phase: 'builder',
2963
+ tokens: result.tokens,
2964
+ durationMs: Date.now() - builderStart,
2965
+ });
2966
+ return { executed: true, phaseState: ps, updatedMeta };
2967
+ }
2968
+ const builderOutput = parseBuilderOutput(rawOutput);
2940
2969
  const builderTokens = result.tokens;
2941
2970
  // Builder success: builder → fresh, critic → pending
2942
2971
  ps = builderSuccess(ps);
@@ -3056,6 +3085,28 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
3056
3085
  *
3057
3086
  * @module orchestrator/orchestratePhase
3058
3087
  */
3088
+ /**
3089
+ * Check whether a meta has an empty scope — no source files, no children,
3090
+ * no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
3091
+ */
3092
+ function isEmptyScope(scopeFiles, node, meta) {
3093
+ return (scopeFiles.length === 0 &&
3094
+ node.children.length === 0 &&
3095
+ (!meta._crossRefs || meta._crossRefs.length === 0) &&
3096
+ !meta._content);
3097
+ }
3098
+ /**
3099
+ * Handle an empty-scope meta: set all phases fresh, bump _generatedAt.
3100
+ * Prevents perpetual staleness without wasting tokens on synthesis.
3101
+ */
3102
+ async function handleEmptyScope(node, currentMeta, config, structureHash, logger) {
3103
+ await persistPhaseState({
3104
+ metaPath: node.metaPath,
3105
+ current: currentMeta,
3106
+ structureHash,
3107
+ }, freshPhaseState(), { _generatedAt: new Date().toISOString() });
3108
+ logger?.info({ path: node.ownerPath }, 'Empty scope — set all phases fresh, bumped _generatedAt');
3109
+ }
3059
3110
  /** Phase runner dispatch map — avoids repeating the same switch/case. */
3060
3111
  const phaseRunners = {
3061
3112
  architect: runArchitect,
@@ -3111,6 +3162,11 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
3111
3162
  // Compute structure hash for the phase
3112
3163
  const { scopeFiles } = await getScopeFiles(winner.node, watcher);
3113
3164
  const structureHash = computeStructureHash(scopeFiles);
3165
+ // Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
3166
+ if (isEmptyScope(scopeFiles, winner.node, currentMeta)) {
3167
+ await handleEmptyScope(winner.node, currentMeta, config, structureHash, logger);
3168
+ return { executed: false };
3169
+ }
3114
3170
  // skipUnchanged: bump _generatedAt without altering _phaseState
3115
3171
  if (config.skipUnchanged && currentMeta._generatedAt) {
3116
3172
  const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
@@ -3152,6 +3208,11 @@ async function orchestrateTargeted(config, executor, watcher, targetPath, onProg
3152
3208
  // Compute structure hash
3153
3209
  const { scopeFiles } = await getScopeFiles(node, watcher);
3154
3210
  const structureHash = computeStructureHash(scopeFiles);
3211
+ // Empty-scope guard (§3.9, #177): skip synthesis when nothing to synthesize
3212
+ if (isEmptyScope(scopeFiles, node, currentMeta)) {
3213
+ await handleEmptyScope(node, currentMeta, config, structureHash, logger);
3214
+ return { executed: false, metaPath: normalizedTarget };
3215
+ }
3155
3216
  return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
3156
3217
  }
3157
3218
  finally {
@@ -4314,6 +4375,12 @@ function registerConfigApplyRoute(app, configPath) {
4314
4375
  .status(500)
4315
4376
  .send({ error: `Failed to write config: ${message}` });
4316
4377
  }
4378
+ // Compute whether any restart-required fields actually changed
4379
+ // (compares validated output against previous config, catching both
4380
+ // explicit patch keys and implicit changes via replace + schema defaults)
4381
+ const validatedRecord = validatedConfig;
4382
+ const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
4383
+ JSON.stringify(validatedRecord[field]));
4317
4384
  // Apply hot-reload callback
4318
4385
  try {
4319
4386
  applyHotReloadedConfig(validatedConfig);
@@ -4323,11 +4390,12 @@ function registerConfigApplyRoute(app, configPath) {
4323
4390
  return reply.status(200).send({
4324
4391
  applied: true,
4325
4392
  warning: `Config written but callback failed: ${message}`,
4326
- restartRequired: RESTART_REQUIRED_FIELDS,
4393
+ restartRequired,
4327
4394
  });
4328
4395
  }
4329
4396
  return reply.status(200).send({
4330
4397
  applied: true,
4398
+ restartRequired,
4331
4399
  });
4332
4400
  });
4333
4401
  }
@@ -4588,6 +4656,7 @@ function registerMetasUpdateRoute(app, deps) {
4588
4656
  }
4589
4657
  Object.assign(updated, toSet);
4590
4658
  await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
4659
+ deps.cache.invalidate();
4591
4660
  // Project the response — exclude the same large fields as the detail route.
4592
4661
  const projected = {};
4593
4662
  for (const [k, v] of Object.entries(updated)) {
@@ -4625,10 +4694,10 @@ function registerPreviewRoute(app, deps) {
4625
4694
  const normalized = normalizePath(query.path);
4626
4695
  targetNode = findNode(result.tree, normalized);
4627
4696
  if (!targetNode) {
4628
- return {
4697
+ return reply.status(404).send({
4629
4698
  error: 'NOT_FOUND',
4630
4699
  message: 'Meta path not found: ' + query.path,
4631
- };
4700
+ });
4632
4701
  }
4633
4702
  }
4634
4703
  else {
@@ -5170,7 +5239,7 @@ function registerRoutes(app, deps) {
5170
5239
  });
5171
5240
  registerStatusRoute(app, deps);
5172
5241
  registerMetasRoutes(app, deps);
5173
- registerMetasUpdateRoute(app);
5242
+ registerMetasUpdateRoute(app, deps);
5174
5243
  registerSynthesizeRoute(app, deps);
5175
5244
  registerPreviewRoute(app, deps);
5176
5245
  registerSeedRoute(app, deps);
@@ -5257,6 +5326,18 @@ function registerShutdownHandlers(deps) {
5257
5326
  deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
5258
5327
  }
5259
5328
  }
5329
+ // Release lock for in-progress override synthesis (only when it
5330
+ // differs from the legacy current item to avoid double-release)
5331
+ const currentPhase = deps.queue.currentPhase;
5332
+ if (currentPhase && currentPhase.path !== current?.path) {
5333
+ try {
5334
+ releaseLock(resolveMetaDir(currentPhase.path));
5335
+ deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress override synthesis');
5336
+ }
5337
+ catch {
5338
+ deps.logger.warn({ path: currentPhase.path }, 'Failed to release override lock during shutdown');
5339
+ }
5340
+ }
5260
5341
  // 3. Close server
5261
5342
  try {
5262
5343
  await deps.server.close();
@@ -5354,6 +5435,25 @@ class HttpWatcherClient {
5354
5435
  *
5355
5436
  * @module bootstrap
5356
5437
  */
5438
+ /**
5439
+ * Compute per-cycle token total from a completed meta.
5440
+ *
5441
+ * Exported for testing.
5442
+ *
5443
+ * Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
5444
+ * a value of 1 means architect ran this cycle (was 0 pre-increment),
5445
+ * so all three phase token fields are summed. A value \> 1 means architect
5446
+ * was skipped (cached brief reused), so only builder + critic are summed.
5447
+ */
5448
+ function computeCycleTokens(meta) {
5449
+ const builderTokens = meta._builderTokens ?? 0;
5450
+ const criticTokens = meta._criticTokens ?? 0;
5451
+ const architectRan = (meta._synthesisCount ?? 1) === 1;
5452
+ const architectTokens = architectRan
5453
+ ? (meta._architectTokens ?? 0)
5454
+ : 0;
5455
+ return architectTokens + builderTokens + criticTokens;
5456
+ }
5357
5457
  /**
5358
5458
  * Bootstrap the service: create logger, build server, start listening,
5359
5459
  * wire scheduler, queue processing, rule registration, config hot-reload,
@@ -5480,10 +5580,15 @@ async function startService(config, configPath) {
5480
5580
  }
5481
5581
  // Emit synthesis_complete only on full-cycle completion
5482
5582
  if (result.cycleComplete) {
5583
+ const updatedMeta = result.phaseResult?.updatedMeta;
5584
+ const tokens = updatedMeta
5585
+ ? computeCycleTokens(updatedMeta)
5586
+ : undefined;
5483
5587
  await progress.report({
5484
5588
  type: 'synthesis_complete',
5485
5589
  path: ownerPath,
5486
5590
  durationMs,
5591
+ tokens,
5487
5592
  });
5488
5593
  }
5489
5594
  }
@@ -8,11 +8,17 @@
8
8
  *
9
9
  * @module orchestrator/orchestratePhase
10
10
  */
11
+ import type { MetaNode } from '../discovery/types.js';
11
12
  import type { MetaExecutor, WatcherClient } from '../interfaces/index.js';
12
13
  import type { MinimalLogger } from '../logger/index.js';
13
14
  import type { ProgressEvent } from '../progress/index.js';
14
- import type { MetaConfig, PhaseName } from '../schema/index.js';
15
+ import type { MetaConfig, MetaJson, PhaseName } from '../schema/index.js';
15
16
  import { type PhaseResult } from './runPhase.js';
17
+ /**
18
+ * Check whether a meta has an empty scope — no source files, no children,
19
+ * no cross-refs, and no prior content. Matches §3.9 empty-scope criteria.
20
+ */
21
+ export declare function isEmptyScope(scopeFiles: string[], node: MetaNode, meta: MetaJson): boolean;
16
22
  /** Callback for synthesis progress events. */
17
23
  export type PhaseProgressCallback = (event: ProgressEvent) => void | Promise<void>;
18
24
  /** Result of a single phase-aware orchestration tick. */
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/karmaniverous/jeeves-meta/issues"
8
8
  },
9
9
  "dependencies": {
10
- "@karmaniverous/jeeves": "^0.5.11",
10
+ "@karmaniverous/jeeves": "^0.5.12",
11
11
  "@karmaniverous/jeeves-meta-core": "^0.1.3",
12
12
  "commander": "^14",
13
13
  "croner": "^10",
@@ -110,5 +110,5 @@
110
110
  },
111
111
  "type": "module",
112
112
  "types": "dist/index.d.ts",
113
- "version": "0.15.10"
113
+ "version": "0.15.12"
114
114
  }