@zhin.js/runtime 1.0.19 → 1.1.1

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
@@ -22,6 +22,10 @@ await runtime.start();
22
22
  - child/schema 变化替换最浅受影响 subtree。
23
23
  - manifest topology transaction 支持 child/Feature 新增、删除和移动。
24
24
  - Root/package ABI、lockfile 和未知 importer 变化升级为 process restart。
25
+ - 外部 Root 配置按实际投影分类:子 Plugin 配置只替换对应 subtree,Host 配置请求
26
+ process restart。
27
+ - `.env` / `.env.<environment>` 由 `EnvironmentLayersPort` 重读;Runtime 重新展开配置引用,
28
+ Host 投影变化时重启进程,否则提交新的 Plugin generation。
25
29
  - 候选校验或 handoff 失败不提交 generation;旧 snapshot 保持可用。
26
30
  - 最后一个旧 lease 释放后,旧资源才按 children-first 顺序回收。
27
31
 
@@ -1,9 +1,9 @@
1
+ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
1
2
  import type { PluginId } from '@zhin.js/plugin-runtime';
2
3
  import type { ProjectGraph } from './project-graph.js';
3
4
  /** Host configuration keys consumed by Runtime composition and Host installers. */
4
5
  export declare const HOST_CONFIG_KEYS: readonly string[];
5
6
  export type JsonSchema = Readonly<Record<string, unknown>>;
6
- export type RuntimeConfigDocument = Readonly<Record<string, unknown>>;
7
7
  export interface ComposedConfig {
8
8
  readonly effectiveSchema: JsonSchema;
9
9
  readonly document: RuntimeConfigDocument;
@@ -199,12 +199,25 @@ async function readOwnSchema(node) {
199
199
  || schema.allOf !== undefined || schema.$ref !== undefined)) {
200
200
  throw new TypeError(`${file} root schema must declare properties; composition keywords (anyOf/oneOf/allOf/$ref) are not supported`);
201
201
  }
202
+ const properties = schemaProperties(schema);
203
+ if (Object.hasOwn(properties, 'commandNamespace')) {
204
+ throw new ConfigSchemaCollisionError(node.id, 'commandNamespace');
205
+ }
202
206
  return Object.freeze({
203
207
  $id: `urn:zhin:plugin-config:${encodeURIComponent(String(node.id))}`,
204
208
  type: 'object',
205
209
  additionalProperties: false,
206
210
  ...schema,
207
- properties: schemaProperties(schema),
211
+ properties: {
212
+ commandNamespace: {
213
+ type: 'string',
214
+ minLength: 1,
215
+ pattern: '^\\S+(?: \\S+)*$',
216
+ description: 'Explicit command namespace prepended after the Endpoint commandPrefix.',
217
+ 'x-descriptionZh': '显式指令命名空间;在 Endpoint commandPrefix 之后参与路由。',
218
+ },
219
+ ...properties,
220
+ },
208
221
  });
209
222
  }
210
223
  function schemaProperties(schema) {
@@ -1,22 +1,11 @@
1
- import { type PluginId } from '@zhin.js/plugin-runtime';
2
- import { ConfigComposer, type ComposedConfig, type RuntimeConfigDocument } from './config-composer.js';
1
+ import { type ConfigPatch, type PluginId, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
2
+ import { ConfigComposer, type ComposedConfig } from './config-composer.js';
3
3
  import type { ProjectGraph } from './project-graph.js';
4
- export type ConfigPatch = {
5
- readonly op: 'set';
6
- readonly path: readonly string[];
7
- readonly value: unknown;
8
- } | {
9
- readonly op: 'remove';
10
- readonly path: readonly string[];
11
- };
12
4
  export interface ConfigPatchPlan extends ComposedConfig {
13
5
  readonly candidate: RuntimeConfigDocument;
14
6
  readonly documentChanged: boolean;
15
7
  readonly roots: readonly PluginId[];
16
8
  }
17
- export declare class ConfigPatchPathError extends Error {
18
- constructor(message: string);
19
- }
20
9
  /** Validates a candidate document before deriving its minimal replacement forest. */
21
10
  export declare class ConfigPatchPlanner {
22
11
  private readonly composer;
@@ -1,12 +1,6 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
- import { rootPluginId } from '@zhin.js/plugin-runtime';
3
- import { ConfigComposer, } from './config-composer.js';
4
- export class ConfigPatchPathError extends Error {
5
- constructor(message) {
6
- super(message);
7
- this.name = 'ConfigPatchPathError';
8
- }
9
- }
2
+ import { applyConfigPatches, rootPluginId, } from '@zhin.js/plugin-runtime';
3
+ import { ConfigComposer } from './config-composer.js';
10
4
  /** Validates a candidate document before deriving its minimal replacement forest. */
11
5
  export class ConfigPatchPlanner {
12
6
  composer;
@@ -15,123 +9,19 @@ export class ConfigPatchPlanner {
15
9
  }
16
10
  async plan(graph, current, patches) {
17
11
  const previous = await this.composer.compose(graph, current);
18
- let candidate = structuredClone(current);
19
- for (const patch of patches)
20
- candidate = applyPatch(candidate, patch);
12
+ const candidate = applyConfigPatches(current, patches);
21
13
  const next = await this.composer.compose(graph, candidate);
22
14
  const changed = indexGraph(graph)
23
15
  .filter((node) => !isDeepStrictEqual(previous.views.get(node.id), next.views.get(node.id)))
24
16
  .map((node) => node.id);
25
17
  return Object.freeze({
26
18
  ...next,
27
- candidate: Object.freeze(candidate),
19
+ candidate,
28
20
  documentChanged: !isDeepStrictEqual(current, candidate),
29
21
  roots: Object.freeze(collapseRoots(changed)),
30
22
  });
31
23
  }
32
24
  }
33
- function applyPatch(document, patch) {
34
- assertPath(patch.path);
35
- if (patch.path.length === 0) {
36
- if (patch.op === 'remove') {
37
- throw new ConfigPatchPathError('The config document root cannot be removed');
38
- }
39
- return cloneDocument(patch.value);
40
- }
41
- if (patch.op === 'set')
42
- setValue(document, patch.path, structuredClone(patch.value));
43
- else
44
- removeValue(document, patch.path);
45
- return document;
46
- }
47
- function setValue(document, path, value) {
48
- let target = document;
49
- for (const [index, segment] of path.slice(0, -1).entries()) {
50
- const existing = readChild(target, segment, path.slice(0, index + 1));
51
- if (existing === undefined) {
52
- // Missing intermediates are always created as records; arrays only ever
53
- // come from the existing document (numeric segments index into them).
54
- const created = {};
55
- target[segment] = created;
56
- target = created;
57
- }
58
- else {
59
- target = requireContainer(existing, path.slice(0, index + 1));
60
- }
61
- }
62
- writeChild(target, lastSegment(path), value, path);
63
- }
64
- function removeValue(document, path) {
65
- let target = document;
66
- for (const [index, segment] of path.slice(0, -1).entries()) {
67
- const existing = readChild(target, segment, path.slice(0, index + 1));
68
- if (existing === undefined)
69
- return;
70
- target = requireContainer(existing, path.slice(0, index + 1));
71
- }
72
- const segment = lastSegment(path);
73
- if (Array.isArray(target)) {
74
- target.splice(arrayIndex(segment, target, path), 1);
75
- return;
76
- }
77
- delete target[segment];
78
- }
79
- function cloneDocument(value) {
80
- return requireRecord(structuredClone(value), []);
81
- }
82
- function readChild(container, segment, path) {
83
- if (Array.isArray(container))
84
- return container[arrayIndex(segment, container, path)];
85
- return container[segment];
86
- }
87
- function writeChild(container, segment, value, path) {
88
- if (Array.isArray(container)) {
89
- container[arrayIndex(segment, container, path)] = value;
90
- return;
91
- }
92
- container[segment] = value;
93
- }
94
- /** Resolves a numeric path segment against an array, rejecting non-numeric segments and out-of-bounds indexes. */
95
- function arrayIndex(segment, container, path) {
96
- if (!/^(0|[1-9]\d*)$/.test(segment)) {
97
- throw new ConfigPatchPathError(`Config path ${pointer(path)} requires an array index, got "${segment}"`);
98
- }
99
- const index = Number(segment);
100
- if (index >= container.length) {
101
- throw new ConfigPatchPathError(`Config path ${pointer(path)} is out of bounds (array length ${container.length})`);
102
- }
103
- return index;
104
- }
105
- function requireContainer(value, path) {
106
- if (!value || typeof value !== 'object') {
107
- throw new ConfigPatchPathError(`Config path ${pointer(path)} is not an object`);
108
- }
109
- return value;
110
- }
111
- function requireRecord(value, path) {
112
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
113
- throw new ConfigPatchPathError(`Config path ${pointer(path)} is not an object`);
114
- }
115
- return value;
116
- }
117
- function assertPath(path) {
118
- for (const segment of path) {
119
- if (!segment || segment === '__proto__' || segment === 'prototype' || segment === 'constructor') {
120
- throw new ConfigPatchPathError(`Unsafe config path segment: ${segment || '<empty>'}`);
121
- }
122
- }
123
- }
124
- function lastSegment(path) {
125
- const segment = path[path.length - 1];
126
- if (!segment)
127
- throw new ConfigPatchPathError('Config patch path is empty');
128
- return segment;
129
- }
130
- function pointer(path) {
131
- if (path.length === 0)
132
- return '/';
133
- return `/${path.map((segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1')).join('/')}`;
134
- }
135
25
  function indexGraph(graph) {
136
26
  const result = [];
137
27
  const visit = (node) => {
@@ -28,7 +28,8 @@ export class ConventionCapabilityDeltaResolver {
28
28
  for (const root of roots) {
29
29
  const discovered = await this.#discover(cache, provider, root);
30
30
  for (const candidate of discovered) {
31
- if (candidate.source !== fallback.source)
31
+ if (candidate.source !== fallback.source
32
+ && !candidate.relatedSources.includes(fallback.source))
32
33
  continue;
33
34
  const ids = capabilities.get(feature) ?? new Set();
34
35
  ids.add(candidate.id);
@@ -82,6 +83,7 @@ export class ConventionCapabilityDeltaResolver {
82
83
  for await (const source of convention.discover(context)) {
83
84
  discovered.push(Object.freeze({
84
85
  source: resolve(source.source),
86
+ relatedSources: Object.freeze((source.relatedSources ?? []).map((item) => resolve(item))),
85
87
  id: capabilityId(root.owner, provider.id, source.localName),
86
88
  }));
87
89
  }
@@ -6,6 +6,11 @@ export interface EnvironmentLayers {
6
6
  readonly environments?: Readonly<Record<string, EnvironmentSource>>;
7
7
  readonly plugins?: Readonly<Record<string, EnvironmentSource>>;
8
8
  }
9
+ /** Reloadable authority for environment layers and their watched files. */
10
+ export interface EnvironmentLayersPort {
11
+ readonly sources: readonly string[];
12
+ read(): Promise<EnvironmentLayers>;
13
+ }
9
14
  export interface EnvSchema<T> {
10
15
  readonly secretKeys?: readonly string[];
11
16
  parse(source: EnvironmentSource): T;
@@ -3,6 +3,7 @@ import { type GenerationInvalidationPlan, type InvalidationPlan, type ProcessInv
3
3
  import type { ModuleRuntime } from './module-runtime.js';
4
4
  import type { SourceOwnershipIndex } from './source-ownership.js';
5
5
  export interface HmrReloadPort {
6
+ handlesSource?(source: string): boolean;
6
7
  reload(plan: GenerationInvalidationPlan): Promise<ProcessInvalidationPlan | void>;
7
8
  }
8
9
  export interface HmrCoordinatorOptions {
@@ -68,7 +68,8 @@ export class HmrCoordinator {
68
68
  while (this.#pending.size > 0) {
69
69
  const changed = [...this.#pending];
70
70
  this.#pending.clear();
71
- const forcedRestart = changed.filter((source) => this.options.modules.requiresProcessRestart?.(source));
71
+ const forcedRestart = changed.filter((source) => (!this.options.runtime.handlesSource?.(source)
72
+ && this.options.modules.requiresProcessRestart?.(source)));
72
73
  if (forcedRestart.length > 0) {
73
74
  this.#restartRequired = true;
74
75
  this.#pending.clear();
@@ -123,10 +123,10 @@
123
123
  "description": "Command allowlist preset used outside full mode.",
124
124
  "x-descriptionZh": "非 full 模式使用的命令白名单预设。"
125
125
  },
126
- "execApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for main Agent commands.", "x-descriptionZh": "主 Agent 命令的审批策略。" },
127
- "subagentExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for sub-Agent commands.", "x-descriptionZh": "子 Agent 命令的审批策略。" },
128
- "workerExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for worker commands.", "x-descriptionZh": "Worker 命令的审批策略。" },
129
- "taskExecApprovalMode": { "type": "string", "enum": ["ask", "allow", "deny"], "description": "Approval policy for task commands.", "x-descriptionZh": "Task 命令的审批策略。" },
126
+ "execApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for main Agent commands.", "x-descriptionZh": "主 Agent 命令的审批策略。" },
127
+ "subagentExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for sub-Agent commands.", "x-descriptionZh": "子 Agent 命令的审批策略。" },
128
+ "workerExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for worker commands.", "x-descriptionZh": "Worker 命令的审批策略。" },
129
+ "taskExecApprovalMode": { "type": "string", "enum": ["ask", "bypass", "auto"], "description": "Approval policy for task commands.", "x-descriptionZh": "Task 命令的审批策略。" },
130
130
  "toolExecution": {
131
131
  "type": "string",
132
132
  "enum": ["parallel", "sequential", "tiered"],
package/lib/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './config-composer.js';
2
- export * from './config-document.js';
3
2
  export * from './config-patch-planner.js';
4
- export * from './compatibility.js';
3
+ export * from './package-contract.js';
5
4
  export * from './environment.js';
6
5
  export * from './environment-store.js';
7
6
  export * from './hmr-coordinator.js';
package/lib/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export * from './config-composer.js';
2
- export * from './config-document.js';
3
2
  export * from './config-patch-planner.js';
4
- export * from './compatibility.js';
3
+ export * from './package-contract.js';
5
4
  export * from './environment.js';
6
5
  export * from './environment-store.js';
7
6
  export * from './hmr-coordinator.js';
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, statSync, watch as watchDirectory, } from 'node:fs';
2
- import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
2
+ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  const ignoredDirectories = new Set([
5
5
  '.git', '.zhin', 'coverage', 'data', 'dist', 'lib', 'node_modules',
@@ -8,7 +8,7 @@ const watchedExtensions = new Set([
8
8
  '.cjs', '.js', '.json', '.md', '.mjs', '.ts', '.tsx', '.yaml', '.yml',
9
9
  ]);
10
10
  const capabilityRoots = new Set([
11
- 'adapters', 'agents', 'commands', 'components', 'handlers', 'mcp', 'middlewares', 'pages', 'skills', 'tools',
11
+ 'adapters', 'agents', 'commands', 'components', 'handlers', 'hooks', 'mcps', 'middlewares', 'pages', 'prompt-sections', 'schedules', 'skills', 'tools',
12
12
  ]);
13
13
  /**
14
14
  * Uses Node's native ESM/TypeScript loader and adds only cache busting and watch.
@@ -50,19 +50,36 @@ export class NativeDevelopmentModuleRuntime {
50
50
  // The HMR coordinator turns this into a visible process restart reason.
51
51
  if (!packageRoot || isNodeModulesSource(packageRoot, normalized))
52
52
  return true;
53
+ if (packageRoot === this.#projectRoot && basename(normalized).startsWith('.env'))
54
+ return true;
53
55
  const parts = relative(packageRoot, normalized).split(sep);
54
56
  const capability = parts.findIndex((part) => capabilityRoots.has(part));
55
57
  if (capability < 0)
56
58
  return isExecutableSource(normalized);
57
59
  const root = parts[capability];
58
- if (root === 'pages')
59
- return false;
60
- if (root === 'skills' || root === 'agents')
60
+ if (root === 'agents' || root === 'skills') {
61
+ const local = parts.slice(capability + 1);
62
+ if (isNestedDirectoryEntry(local))
63
+ return false;
61
64
  return extname(normalized) !== '.md';
62
- if (root === 'tools' || root === 'mcp')
63
- return parts.length !== capability + 2;
64
- if (isCapabilityEntry(parts.slice(capability + 1)))
65
- return false;
65
+ }
66
+ if (root === 'commands') {
67
+ const local = parts.slice(capability + 1);
68
+ if (!isExecutableSource(normalized))
69
+ return extname(normalized) === '.json';
70
+ return local.length < 2
71
+ || !local.slice(0, -1).every(isCommandDirectorySegment);
72
+ }
73
+ if (root === 'adapters' || root === 'components' || root === 'handlers'
74
+ || root === 'hooks' || root === 'mcps' || root === 'middlewares'
75
+ || root === 'pages' || root === 'prompt-sections' || root === 'schedules'
76
+ || root === 'tools') {
77
+ const local = parts.slice(capability + 1);
78
+ if (!isExecutableSource(normalized))
79
+ return extname(normalized) === '.json';
80
+ return local.length < 2
81
+ || !isNamedCapabilityDirectory(local[0] ?? '', root === 'tools');
82
+ }
66
83
  // Support files inside capability directories (e.g. commands/_utils.ts)
67
84
  // are not discovery entries: reloading the entry URL only bumps that
68
85
  // entry's zhin-generation, so the importer closure keeps the old code.
@@ -104,6 +121,27 @@ export class NativeDevelopmentModuleRuntime {
104
121
  throw new Error('NativeDevelopmentModuleRuntime is closed');
105
122
  }
106
123
  }
124
+ function isCommandDirectorySegment(value) {
125
+ return /^[a-z0-9][a-z0-9-]*$/u.test(value)
126
+ || (!/[A-Z]/u.test(value) && /[^\x00-\x7F]/u.test(value) && !/[\s/\\]/u.test(value))
127
+ || /^\[(?:\[)?(?:\.\.\.)?[a-zA-Z][a-zA-Z0-9]*\](?:\])?$/u.test(value);
128
+ }
129
+ function isNamedCapabilityDirectory(value, allowSnake) {
130
+ return allowSnake
131
+ ? /^[a-z0-9][a-z0-9_-]*$/u.test(value)
132
+ : /^[a-z0-9][a-z0-9-]*$/u.test(value);
133
+ }
134
+ function isDirectoryCapabilityEntry(parts) {
135
+ return parts.length === 2
136
+ && parts[1] === `index${extname(parts[1] ?? '')}`
137
+ && ['.cjs', '.js', '.mjs', '.ts'].includes(extname(parts[1] ?? ''));
138
+ }
139
+ function isNestedDirectoryEntry(parts) {
140
+ const index = parts.indexOf('tools');
141
+ const hookIndex = parts.indexOf('hooks');
142
+ const capability = index >= 0 ? index : hookIndex;
143
+ return capability >= 1 && isDirectoryCapabilityEntry(parts.slice(capability + 1));
144
+ }
107
145
  export function supportsNativeTypeScript(version = process.versions.node, execArguments = process.execArgv, nodeOptions = process.env.NODE_OPTIONS ?? '') {
108
146
  if (execArguments.includes('--experimental-strip-types')
109
147
  || /(?:^|\s)--experimental-strip-types(?:\s|$)/u.test(nodeOptions))
@@ -238,21 +276,6 @@ function isWatchedSource(source) {
238
276
  function isIgnoredSource(root, source) {
239
277
  return relative(root, source).split(sep).some((segment) => ignoredDirectories.has(segment));
240
278
  }
241
- /**
242
- * Discovery entries follow the typeScriptModules convention
243
- * (feature-kit typescript-convention.ts): lowercase segment directories and
244
- * lowercase .ts/.tsx module names only.
245
- */
246
- function isCapabilityEntry(segments) {
247
- const file = segments[segments.length - 1] ?? '';
248
- return segments.slice(0, -1).every(isCapabilitySegment) && isCapabilityModule(file);
249
- }
250
- function isCapabilitySegment(value) {
251
- return /^[a-z0-9][a-z0-9-]*$/u.test(value);
252
- }
253
- function isCapabilityModule(value) {
254
- return /^[a-z0-9][a-z0-9-]*\.tsx?$/u.test(value);
255
- }
256
279
  function isExecutableSource(source) {
257
280
  return ['.cjs', '.js', '.mjs', '.ts', '.tsx'].includes(extname(source));
258
281
  }
@@ -1,7 +1,7 @@
1
1
  import type { PackageReference } from './manifest.js';
2
2
  import type { ResolvedPackage } from './package-resolver.js';
3
3
  export declare const runtimeEngineVersion = "1.0.0";
4
- export declare class PackageCompatibilityError extends Error {
4
+ export declare class PackageContractError extends Error {
5
5
  readonly packageName: string;
6
6
  readonly contract: 'engine' | 'feature-api';
7
7
  constructor(packageName: string, contract: 'engine' | 'feature-api', message: string);
@@ -2,14 +2,14 @@ import { createRequire } from 'node:module';
2
2
  const require = createRequire(import.meta.url);
3
3
  const semver = require('semver');
4
4
  export const runtimeEngineVersion = '1.0.0';
5
- export class PackageCompatibilityError extends Error {
5
+ export class PackageContractError extends Error {
6
6
  packageName;
7
7
  contract;
8
8
  constructor(packageName, contract, message) {
9
- super(`Incompatible ${contract} for ${packageName}: ${message}`);
9
+ super(`Invalid ${contract} contract for ${packageName}: ${message}`);
10
10
  this.packageName = packageName;
11
11
  this.contract = contract;
12
- this.name = 'PackageCompatibilityError';
12
+ this.name = 'PackageContractError';
13
13
  }
14
14
  }
15
15
  export function assertPackageEngine(pkg, engineVersion = runtimeEngineVersion) {
@@ -18,7 +18,7 @@ export function assertPackageEngine(pkg, engineVersion = runtimeEngineVersion) {
18
18
  return;
19
19
  assertRange(pkg.name, 'engine', range);
20
20
  if (!semver.satisfies(engineVersion, range, { includePrerelease: true })) {
21
- throw new PackageCompatibilityError(pkg.name, 'engine', `requires ${range}, Runtime provides ${engineVersion}`);
21
+ throw new PackageContractError(pkg.name, 'engine', `requires ${range}, Runtime provides ${engineVersion}`);
22
22
  }
23
23
  }
24
24
  export function assertFeatureApi(owner, reference, feature) {
@@ -26,20 +26,20 @@ export function assertFeatureApi(owner, reference, feature) {
26
26
  ? feature.packageJson.zhin.featureApi
27
27
  : undefined;
28
28
  if (actual && !semver.valid(actual)) {
29
- throw new PackageCompatibilityError(feature.name, 'feature-api', `declares invalid featureApi version ${actual}`);
29
+ throw new PackageContractError(feature.name, 'feature-api', `declares invalid featureApi version ${actual}`);
30
30
  }
31
31
  if (!reference.api)
32
32
  return;
33
33
  assertRange(owner.name, 'feature-api', reference.api);
34
34
  if (!actual) {
35
- throw new PackageCompatibilityError(feature.name, 'feature-api', `must declare a valid featureApi version for ${owner.name}'s ${reference.api} requirement`);
35
+ throw new PackageContractError(feature.name, 'feature-api', `must declare a valid featureApi version for ${owner.name}'s ${reference.api} requirement`);
36
36
  }
37
37
  if (!semver.satisfies(actual, reference.api, { includePrerelease: true })) {
38
- throw new PackageCompatibilityError(feature.name, 'feature-api', `${owner.name} requires ${reference.api}, provider declares ${actual}`);
38
+ throw new PackageContractError(feature.name, 'feature-api', `${owner.name} requires ${reference.api}, provider declares ${actual}`);
39
39
  }
40
40
  }
41
41
  function assertRange(packageName, contract, range) {
42
42
  if (!semver.validRange(range)) {
43
- throw new PackageCompatibilityError(packageName, contract, `invalid semver range ${range}`);
43
+ throw new PackageContractError(packageName, contract, `invalid semver range ${range}`);
44
44
  }
45
45
  }
@@ -5,7 +5,7 @@ import type { ModuleRuntime } from './module-runtime.js';
5
5
  import type { PluginGraphNode } from './project-graph.js';
6
6
  import type { IsolatedPluginRuntimePort } from './isolation.js';
7
7
  import { type PrimaryConfig } from './primary-config.js';
8
- import type { RuntimeConfigDocument } from './config-composer.js';
8
+ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
9
9
  export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
10
10
  export interface RootResourceContext {
11
11
  /** Exact shadow generation being assembled; never inferred from a latest snapshot. */
@@ -53,10 +53,6 @@ export declare class PluginScopeAssembler {
53
53
  createdScopeDisposers(): readonly (readonly [PluginId, Dispose])[];
54
54
  generationHandoff(): GenerationHandoff | undefined;
55
55
  setupCapabilities(): readonly Readonly<SetupCapabilityRegistration>[];
56
- /**
57
- * Root resources are process-owned. A Plugin Scope receives only a facade
58
- * that translates logical table/job names to its owner namespace. The root
59
- * owner deliberately keeps bare names for backward-compatible projects.
60
- */
56
+ /** Root resources are process-owned; every Plugin Scope receives an owner-bound facade. */
61
57
  private installOwnerScopedHosts;
62
58
  }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, createPluginDatabaseHost, createPluginScheduleHost, databaseHostToken, databaseRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, unwrapPluginDatabaseHost, unwrapPluginScheduleHost, } from '@zhin.js/plugin-runtime';
2
+ import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, PluginDatabaseHost, PluginScheduleHost, databaseHostToken, databaseRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, } from '@zhin.js/plugin-runtime';
3
3
  import { runtimeEnvironmentToken } from './environment.js';
4
4
  import { EnvStoreFactory, envStoreToken, } from './environment-store.js';
5
5
  import { createPrimaryConfig, primaryConfigToken, } from './primary-config.js';
@@ -212,27 +212,19 @@ export class PluginScopeAssembler {
212
212
  setupCapabilities() {
213
213
  return Object.freeze([...this.#setupCapabilities.values()]);
214
214
  }
215
- /**
216
- * Root resources are process-owned. A Plugin Scope receives only a facade
217
- * that translates logical table/job names to its owner namespace. The root
218
- * owner deliberately keeps bare names for backward-compatible projects.
219
- */
215
+ /** Root resources are process-owned; every Plugin Scope receives an owner-bound facade. */
220
216
  installOwnerScopedHosts(scope, owner) {
221
217
  const database = scope.has(databaseRootHostToken)
222
218
  ? scope.use(databaseRootHostToken)
223
- : scope.has(databaseHostToken)
224
- ? unwrapPluginDatabaseHost(scope.use(databaseHostToken))
225
- : undefined;
219
+ : undefined;
226
220
  if (database) {
227
- scope.provide(databaseHostToken, createPluginDatabaseHost(owner, database));
221
+ scope.provide(databaseHostToken, new PluginDatabaseHost(owner, database));
228
222
  }
229
223
  const schedule = scope.has(scheduleRootHostToken)
230
224
  ? scope.use(scheduleRootHostToken)
231
- : scope.has(scheduleHostToken)
232
- ? unwrapPluginScheduleHost(scope.use(scheduleHostToken))
233
- : undefined;
225
+ : undefined;
234
226
  if (schedule) {
235
- scope.provide(scheduleHostToken, createPluginScheduleHost(owner, schedule));
227
+ scope.provide(scheduleHostToken, new PluginScheduleHost(owner, schedule));
236
228
  }
237
229
  }
238
230
  }
@@ -1,5 +1,5 @@
1
1
  import type { EnvStore } from './environment-store.js';
2
- import type { RuntimeConfigDocument } from './config-composer.js';
2
+ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
3
3
  /**
4
4
  * Generation-owned, validated Root configuration.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  import { childPluginId, rootPluginId } from '@zhin.js/plugin-runtime';
2
- import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './compatibility.js';
2
+ import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './package-contract.js';
3
3
  import { PackageResolutionError, } from './package-resolver.js';
4
4
  import { PLATFORM_FEATURE_CARRIER, PLATFORM_FEATURE_FACADE, declaredPackageDependency, mergeChildPluginReferences, mergeFeatureReferences, } from './platform-features.js';
5
5
  export class ProjectGraphError extends Error {
@@ -1,9 +1,6 @@
1
- import { type ControlErrorHandler, type GenerationCommitListener, type PluginId, type RuntimeSnapshot, type SnapshotReader } from '@zhin.js/plugin-runtime';
2
- import { type RuntimeConfigDocument } from './config-composer.js';
3
- import { type ConfigDocumentPort } from './config-document.js';
4
- import { type ConfigPatch } from './config-patch-planner.js';
1
+ import { type ControlErrorHandler, type ConfigDocumentPort, type ConfigPatch, type GenerationCommitListener, type PluginId, type RuntimeConfigDocument, type RuntimeSnapshot, type SnapshotReader } from '@zhin.js/plugin-runtime';
5
2
  import { type RuntimeEnvironment } from './environment.js';
6
- import { type EnvironmentLayers } from './environment-store.js';
3
+ import { type EnvironmentLayers, type EnvironmentLayersPort } from './environment-store.js';
7
4
  import type { IsolatedPluginRuntimePort } from './isolation.js';
8
5
  import type { ModuleRuntime } from './module-runtime.js';
9
6
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
@@ -17,6 +14,7 @@ export interface RootRuntimeOptions {
17
14
  readonly modules: ModuleRuntime;
18
15
  readonly environment: RuntimeEnvironment;
19
16
  readonly environmentVariables?: EnvironmentLayers;
17
+ readonly environmentSource?: EnvironmentLayersPort;
20
18
  readonly config?: PluginConfigResolver | RuntimeConfigDocument | ConfigDocumentPort;
21
19
  readonly installResources?: RootResourceInstaller;
22
20
  readonly isolation?: IsolatedPluginRuntimePort;
@@ -1,10 +1,9 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { isDeepStrictEqual } from 'node:util';
3
- import { DisposeStack, GenerationHandoffStack, RootController, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
3
+ import { DisposeStack, GenerationHandoffStack, RootController, ConfigDocumentDivergenceError, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
4
4
  import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
5
5
  import { ConfigComposer } from './config-composer.js';
6
- import { ConfigDocumentDivergenceError, } from './config-document.js';
7
- import { ConfigPatchPlanner, } from './config-patch-planner.js';
6
+ import { ConfigPatchPlanner } from './config-patch-planner.js';
8
7
  import { defineRuntimeEnvironment } from './environment.js';
9
8
  import { createEnvStore, defineEnvironmentLayers, } from './environment-store.js';
10
9
  import { FeatureProjector, composeGenerationHandoffs, } from './feature-projector.js';
@@ -23,11 +22,14 @@ import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from
23
22
  import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtree-generation-preparer.js';
24
23
  import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
25
24
  import { RestartBoundaryPlanner } from './restart-boundary.js';
25
+ import { RuntimeInputPlanner } from './runtime-input-planner.js';
26
26
  export class RootRuntime {
27
27
  #projectRoot;
28
28
  #modules;
29
29
  #environment;
30
30
  #environmentLayers;
31
+ #environmentSource;
32
+ #runtimeInputSources;
31
33
  #configResolver;
32
34
  #configPort;
33
35
  #configSnapshot;
@@ -43,12 +45,17 @@ export class RootRuntime {
43
45
  this.#modules = options.modules;
44
46
  this.#environment = defineRuntimeEnvironment(options.environment);
45
47
  this.#environmentLayers = defineEnvironmentLayers(options.environmentVariables);
48
+ this.#environmentSource = options.environmentSource;
46
49
  if (typeof options.config === 'function')
47
50
  this.#configResolver = options.config;
48
51
  else if (isConfigDocumentPort(options.config))
49
52
  this.#configPort = options.config;
50
53
  else
51
54
  this.#configDocument = structuredClone(options.config ?? {});
55
+ this.#runtimeInputSources = new Set([
56
+ ...(this.#configPort?.sources ?? []),
57
+ ...(this.#environmentSource?.sources ?? []),
58
+ ].map((source) => resolve(source)));
52
59
  this.#installResources = options.installResources;
53
60
  this.#isolation = options.isolation;
54
61
  this.#disabledPluginInstanceKeys = Object.freeze([...(options.disabledPluginInstanceKeys ?? [])]);
@@ -67,6 +74,9 @@ export class RootRuntime {
67
74
  return this.#controller.committed.state.ownership;
68
75
  }
69
76
  async start() {
77
+ if (this.#environmentSource) {
78
+ this.#environmentLayers = defineEnvironmentLayers(await this.#environmentSource.read());
79
+ }
70
80
  if (this.#configPort) {
71
81
  const snapshot = await this.#configPort.read();
72
82
  this.#configSnapshot = snapshot;
@@ -95,6 +105,7 @@ export class RootRuntime {
95
105
  modules: this.#modules,
96
106
  ownership: () => this.sourceOwnership,
97
107
  runtime: {
108
+ handlesSource: (source) => this.#runtimeInputSources.has(resolve(source)),
98
109
  reload: async (plan) => {
99
110
  const result = await this.#reloadPlan(plan);
100
111
  return isProcessPlan(result) ? result : undefined;
@@ -120,40 +131,115 @@ export class RootRuntime {
120
131
  return result;
121
132
  }
122
133
  async #reloadPlan(plan) {
134
+ const inputs = await this.#planRuntimeInputs(plan.changed);
135
+ if (inputs && 'kind' in inputs)
136
+ return inputs;
137
+ const previousInputs = inputs ? this.#runtimeInputState() : undefined;
138
+ const effectiveInputPlan = inputs
139
+ ? Object.freeze({
140
+ ...plan,
141
+ subtrees: collapseInvalidationSubtrees([...plan.subtrees, ...inputs.roots]),
142
+ fallbacks: Object.freeze(plan.fallbacks.filter((fallback) => !inputs.inputSources.has(resolve(fallback.source)))),
143
+ })
144
+ : plan;
123
145
  let restart;
124
- const snapshot = await this.#controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current, signal) => {
146
+ const snapshot = await this.#controller.reload(effectiveInputPlan.subtrees[0] ?? effectiveInputPlan.slots[0] ?? rootPluginId(), async (current, signal) => {
125
147
  let prepared;
126
- const resolved = await this.#resolveCapabilityDelta(current, plan);
148
+ const resolved = await this.#resolveCapabilityDelta(current, effectiveInputPlan);
127
149
  const effective = resolved.plan;
150
+ if (inputs
151
+ && effective.manifestSources.length === 0
152
+ && effective.subtrees.length === 0
153
+ && effective.slots.length === 0
154
+ && resolved.capabilities.size === 0)
155
+ return undefined;
128
156
  if (this.#model && effective.manifestSources.length > 0) {
129
- const inspected = await this.#inspectProject();
157
+ const inspected = await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
130
158
  restart = new RestartBoundaryPlanner().plan(this.#model.graph, inspected.graph, effective.changed);
131
159
  if (restart)
132
160
  return undefined;
133
161
  prepared = effective.subtrees.includes(rootPluginId())
134
162
  ? await this.#prepareInspected(current, inspected, signal)
135
- : await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, signal, { ...effective, capabilities: resolved.capabilities });
163
+ : await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, inspected.environmentLayers, this.#isolation).prepare(current, signal, { ...effective, capabilities: resolved.capabilities });
136
164
  }
137
165
  else if (effective.subtrees.length === 0 && resolved.capabilities.size > 0 && this.#model) {
138
166
  prepared = await new SlotGenerationPreparer(this.#modules, this.#model)
139
167
  .prepare(current, resolved.capabilities, signal);
140
168
  }
141
169
  else if (this.#model && this.#canPrepareSubtrees(effective)) {
142
- const inspected = await this.#inspectProject();
170
+ const inspected = await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
143
171
  prepared = await this.#prepareSubtrees(current, inspected, effective.subtrees, signal);
144
172
  }
145
173
  else {
146
- prepared = await this.#prepare(current, signal);
174
+ prepared = await this.#prepare(current, signal, inputs?.document, inputs?.environmentLayers);
147
175
  }
148
- return prepared?.generation;
176
+ if (!prepared)
177
+ return undefined;
178
+ return inputs && previousInputs
179
+ ? withRuntimeInputHandoff(prepared.generation, () => this.#adoptRuntimeInputs(inputs), () => this.#restoreRuntimeInputs(previousInputs))
180
+ : prepared.generation;
149
181
  });
150
182
  if (restart)
151
183
  return restart;
184
+ if (inputs)
185
+ this.#adoptRuntimeInputs(inputs);
152
186
  return snapshot;
153
187
  }
154
188
  get #model() {
155
189
  return this.#controller.committed.state.model;
156
190
  }
191
+ async #planRuntimeInputs(changed) {
192
+ const inputSources = new Set(changed.map((source) => resolve(source)).filter((source) => this.#runtimeInputSources.has(source)));
193
+ if (inputSources.size === 0 || !this.#model)
194
+ return undefined;
195
+ const configChanged = this.#configPort?.sources?.some((source) => inputSources.has(resolve(source))) ?? false;
196
+ const environmentChanged = this.#environmentSource?.sources.some((source) => inputSources.has(resolve(source))) ?? false;
197
+ const configSnapshot = configChanged ? await this.#configPort?.read() : undefined;
198
+ const document = configSnapshot?.document ?? requireConfigDocument(this.#configDocument);
199
+ const environmentLayers = environmentChanged && this.#environmentSource
200
+ ? defineEnvironmentLayers(await this.#environmentSource.read())
201
+ : this.#environmentLayers;
202
+ const inputPlan = await new RuntimeInputPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys)).plan({
203
+ graph: this.#model.graph,
204
+ environment: this.#environment,
205
+ previousDocument: requireConfigDocument(this.#configDocument),
206
+ nextDocument: document,
207
+ previousEnvironment: this.#environmentLayers,
208
+ nextEnvironment: environmentLayers,
209
+ });
210
+ if (inputPlan.hostKeys.length > 0) {
211
+ return Object.freeze({
212
+ kind: 'process',
213
+ changed: Object.freeze([...changed].map((source) => resolve(source))),
214
+ reasons: Object.freeze(inputPlan.hostKeys.map((key) => `Host configuration changed: ${key}`)),
215
+ });
216
+ }
217
+ return Object.freeze({
218
+ document,
219
+ configSnapshot,
220
+ environmentLayers,
221
+ inputSources,
222
+ roots: inputPlan.roots,
223
+ });
224
+ }
225
+ #adoptRuntimeInputs(candidate) {
226
+ this.#configDocument = structuredClone(candidate.document);
227
+ if (candidate.configSnapshot)
228
+ this.#configSnapshot = candidate.configSnapshot;
229
+ this.#environmentLayers = candidate.environmentLayers;
230
+ }
231
+ #runtimeInputState() {
232
+ return Object.freeze({
233
+ document: this.#configDocument,
234
+ configSnapshot: this.#configSnapshot,
235
+ environmentLayers: this.#environmentLayers,
236
+ });
237
+ }
238
+ #restoreRuntimeInputs(state) {
239
+ this.#configDocument = state.document;
240
+ this.#configSnapshot = state.configSnapshot;
241
+ this.#environmentLayers = state.environmentLayers;
242
+ }
157
243
  async #resolveCapabilityDelta(current, plan) {
158
244
  const known = capabilityDeltaFromSlots(current, plan.slots);
159
245
  if (!this.#model || (plan.fallbacks.length === 0 && plan.slots.length === 0)) {
@@ -177,35 +263,41 @@ export class RootRuntime {
177
263
  if (plan.subtrees.length === 0 || plan.subtrees.includes(rootPluginId()))
178
264
  return false;
179
265
  return plan.changed.every((source) => {
266
+ if (this.#runtimeInputSources.has(resolve(source)))
267
+ return true;
180
268
  const records = this.sourceOwnership.recordsFor(source);
181
269
  return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
182
270
  });
183
271
  }
184
- async #prepare(current, signal) {
272
+ async #prepare(current, signal, document, environmentLayers) {
185
273
  signal.throwIfAborted();
186
- const inspected = await this.#inspectProject();
274
+ const inspected = await this.#inspectProject(document, environmentLayers);
187
275
  signal.throwIfAborted();
188
276
  return this.#prepareInspected(current, inspected, signal);
189
277
  }
190
- async #inspectProject() {
278
+ async #inspectProject(document, environmentLayers = this.#environmentLayers) {
191
279
  const resolver = await NodePackageResolver.create(this.#projectRoot);
192
280
  const graph = await new ProjectGraphService(resolver, {
193
281
  disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
194
282
  }).inspect(this.#projectRoot);
195
- await this.#refreshConfigDocument();
283
+ if (document === undefined)
284
+ await this.#refreshConfigDocument();
285
+ const activeDocument = document ?? this.#configDocument;
196
286
  if (this.#configResolver) {
197
287
  return {
198
288
  graph,
199
289
  configResolver: this.#configResolver,
200
290
  primaryConfigDocument: Object.freeze({}),
291
+ environmentLayers,
201
292
  };
202
293
  }
203
294
  const composed = await new ConfigComposer(this.#disabledPluginInstanceKeys)
204
- .compose(graph, this.#configDocument);
295
+ .compose(graph, activeDocument);
205
296
  return {
206
297
  graph,
207
- configResolver: this.#configViewResolver(composed.views),
298
+ configResolver: this.#configViewResolver(composed.views, environmentLayers),
208
299
  primaryConfigDocument: composed.document,
300
+ environmentLayers,
209
301
  };
210
302
  }
211
303
  /**
@@ -224,13 +316,13 @@ export class RootRuntime {
224
316
  this.#configSnapshot = snapshot;
225
317
  this.#configDocument = structuredClone(snapshot.document);
226
318
  }
227
- #configViewResolver(views) {
228
- const env = createEnvStore(rootPluginId(), this.#environment, this.#environmentLayers);
319
+ #configViewResolver(views, environmentLayers = this.#environmentLayers) {
229
320
  return (node) => {
230
321
  const view = views.get(node.id);
231
322
  if (view === undefined)
232
323
  return undefined;
233
- return env.expandMissingAsEmpty(view);
324
+ return createEnvStore(node.id, this.#environment, environmentLayers)
325
+ .expandMissingAsEmpty(view);
234
326
  };
235
327
  }
236
328
  async #applyConfigPatches(patches) {
@@ -278,11 +370,12 @@ export class RootRuntime {
278
370
  graph,
279
371
  configResolver: this.#configViewResolver(planned.views),
280
372
  primaryConfigDocument: planned.document,
373
+ environmentLayers: this.#environmentLayers,
281
374
  };
282
375
  // Root resources may consume any Primary Config section. Reinstall them
283
376
  // when present so a committed patch cannot leave Host services on the
284
377
  // previous generation's document.
285
- if (!this.#installResources && this.#model && !planned.roots.includes(rootPluginId())) {
378
+ if (this.#model && !planned.roots.includes(rootPluginId()) && planned.roots.length > 0) {
286
379
  prepared = await this.#prepareSubtrees(current, inspected, planned.roots, signal);
287
380
  }
288
381
  else {
@@ -308,14 +401,14 @@ export class RootRuntime {
308
401
  return snapshot;
309
402
  }
310
403
  #prepareInspected(current, inspected, signal) {
311
- const assembler = new GenerationAssembler(inspected.graph, this.#modules, inspected.configResolver, inspected.primaryConfigDocument, current.generation + 1, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation);
404
+ const assembler = new GenerationAssembler(inspected.graph, this.#modules, inspected.configResolver, inspected.primaryConfigDocument, current.generation + 1, this.#environment, this.#installResources, inspected.environmentLayers, this.#isolation);
312
405
  return assembler.prepare(signal);
313
406
  }
314
407
  async #prepareSubtrees(current, inspected, roots, signal) {
315
408
  if (!this.#model)
316
409
  return this.#prepareInspected(current, inspected, signal);
317
410
  try {
318
- return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, roots, signal);
411
+ return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, inspected.environmentLayers, this.#isolation).prepare(current, roots, signal);
319
412
  }
320
413
  catch (error) {
321
414
  if (!(error instanceof SubtreeTopologyChangedError))
@@ -347,6 +440,13 @@ function withConfigDocumentHandoff(generation, document, committed) {
347
440
  });
348
441
  return { ...generation, handoff: handoffs.seal() };
349
442
  }
443
+ function withRuntimeInputHandoff(generation, activate, deactivate) {
444
+ const handoffs = new GenerationHandoffStack();
445
+ if (generation.handoff)
446
+ handoffs.add(generation.handoff);
447
+ handoffs.add({ activateNext: activate, deactivateNext: deactivate });
448
+ return { ...generation, handoff: handoffs.seal() };
449
+ }
350
450
  function isConfigDocumentPort(value) {
351
451
  if (!value || typeof value !== 'object')
352
452
  return false;
@@ -0,0 +1,24 @@
1
+ import { type PluginId, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
2
+ import { ConfigComposer, type ComposedConfig } from './config-composer.js';
3
+ import { type EnvironmentLayers } from './environment-store.js';
4
+ import type { RuntimeEnvironment } from './environment.js';
5
+ import type { ProjectGraph } from './project-graph.js';
6
+ export interface RuntimeInputPlan {
7
+ readonly composed: ComposedConfig;
8
+ readonly roots: readonly PluginId[];
9
+ readonly hostKeys: readonly string[];
10
+ readonly environmentChanged: boolean;
11
+ }
12
+ /** Classifies config/environment changes by their actual expanded runtime projection. */
13
+ export declare class RuntimeInputPlanner {
14
+ private readonly composer;
15
+ constructor(composer?: ConfigComposer);
16
+ plan(options: {
17
+ readonly graph: ProjectGraph;
18
+ readonly environment: RuntimeEnvironment;
19
+ readonly previousDocument: RuntimeConfigDocument;
20
+ readonly nextDocument: RuntimeConfigDocument;
21
+ readonly previousEnvironment: EnvironmentLayers;
22
+ readonly nextEnvironment: EnvironmentLayers;
23
+ }): Promise<RuntimeInputPlan>;
24
+ }
@@ -0,0 +1,65 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import { rootPluginId, } from '@zhin.js/plugin-runtime';
3
+ import { ConfigComposer, HOST_CONFIG_KEYS } from './config-composer.js';
4
+ import { createEnvStore, } from './environment-store.js';
5
+ /** Classifies config/environment changes by their actual expanded runtime projection. */
6
+ export class RuntimeInputPlanner {
7
+ composer;
8
+ constructor(composer = new ConfigComposer()) {
9
+ this.composer = composer;
10
+ }
11
+ async plan(options) {
12
+ const previous = await this.composer.compose(options.graph, options.previousDocument);
13
+ const next = await this.composer.compose(options.graph, options.nextDocument);
14
+ const environmentChanged = !isDeepStrictEqual(options.previousEnvironment, options.nextEnvironment);
15
+ const hostKeys = HOST_CONFIG_KEYS.filter((key) => !isDeepStrictEqual(expandHostValue(previous.document[key], options.environment, options.previousEnvironment), expandHostValue(next.document[key], options.environment, options.nextEnvironment)));
16
+ const changed = indexGraph(options.graph)
17
+ .filter((node) => !isDeepStrictEqual(expandPluginView(node.id, previous.views.get(node.id), options.environment, options.previousEnvironment), expandPluginView(node.id, next.views.get(node.id), options.environment, options.nextEnvironment)))
18
+ .map((node) => node.id);
19
+ // EnvStore is itself a public Plugin resource. A changed global layer can
20
+ // affect direct env.get()/parse() consumers even when no ${VAR} occurs in
21
+ // config, so conservatively rebuild the Root generation when no narrower
22
+ // config-derived owner proves the impact.
23
+ const roots = environmentChanged ? [rootPluginId()] : collapseRoots(changed);
24
+ return Object.freeze({
25
+ composed: next,
26
+ roots: Object.freeze(roots),
27
+ hostKeys: Object.freeze(hostKeys),
28
+ environmentChanged,
29
+ });
30
+ }
31
+ }
32
+ function expandHostValue(value, environment, layers) {
33
+ return createEnvStore(rootPluginId(), environment, layers).expandMissingAsEmpty(value);
34
+ }
35
+ function expandPluginView(owner, value, environment, layers) {
36
+ return createEnvStore(owner, environment, layers).expandMissingAsEmpty(value);
37
+ }
38
+ function indexGraph(graph) {
39
+ const result = [];
40
+ const visit = (node) => {
41
+ result.push(node);
42
+ for (const child of node.children)
43
+ visit(child);
44
+ };
45
+ visit(graph.root);
46
+ return result;
47
+ }
48
+ function collapseRoots(plugins) {
49
+ if (plugins.includes(rootPluginId()))
50
+ return [rootPluginId()];
51
+ const selected = new Set(plugins);
52
+ return plugins.filter((plugin) => {
53
+ let parent = parentPlugin(plugin);
54
+ while (parent) {
55
+ if (selected.has(parent))
56
+ return false;
57
+ parent = parentPlugin(parent);
58
+ }
59
+ return true;
60
+ });
61
+ }
62
+ function parentPlugin(plugin) {
63
+ const separator = plugin.lastIndexOf('/');
64
+ return separator < 0 ? undefined : plugin.slice(0, separator);
65
+ }
@@ -1,7 +1,7 @@
1
1
  import { type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { RuntimeEnvironment } from './environment.js';
3
3
  import type { EnvironmentLayers } from './environment-store.js';
4
- import type { RuntimeConfigDocument } from './config-composer.js';
4
+ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
5
5
  import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
@@ -1,7 +1,7 @@
1
1
  import { type CapabilityId, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { RuntimeEnvironment } from './environment.js';
3
3
  import type { EnvironmentLayers } from './environment-store.js';
4
- import type { RuntimeConfigDocument } from './config-composer.js';
4
+ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
5
5
  import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.0.19",
3
+ "version": "1.1.1",
4
4
  "description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,23 +18,23 @@
18
18
  "dependencies": {
19
19
  "ajv": "8.18.0",
20
20
  "semver": "7.8.5",
21
- "@zhin.js/feature-kit": "1.0.13",
22
- "@zhin.js/plugin-runtime": "1.1.8"
21
+ "@zhin.js/feature-kit": "1.1.1",
22
+ "@zhin.js/plugin-runtime": "1.1.10"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^26.1.2",
26
26
  "typescript": "^6.0.3",
27
- "@zhin.js/adapter": "1.2.2",
28
- "@zhin.js/agent-feature": "1.0.13",
29
- "@zhin.js/command": "1.0.16",
30
- "@zhin.js/component": "1.0.13",
31
- "@zhin.js/layout": "1.0.13",
32
- "@zhin.js/mcp-feature": "1.0.13",
33
- "@zhin.js/middleware": "1.0.13",
34
- "@zhin.js/page": "1.0.13",
35
- "@zhin.js/prompt-section": "0.0.1",
36
- "@zhin.js/skill": "1.0.13",
37
- "@zhin.js/tool": "1.0.13"
27
+ "@zhin.js/adapter": "1.1.14",
28
+ "@zhin.js/agent-feature": "1.1.1",
29
+ "@zhin.js/command": "1.1.1",
30
+ "@zhin.js/component": "1.1.1",
31
+ "@zhin.js/layout": "1.1.1",
32
+ "@zhin.js/mcp-feature": "1.1.1",
33
+ "@zhin.js/middleware": "1.1.1",
34
+ "@zhin.js/page": "1.1.1",
35
+ "@zhin.js/prompt-section": "1.1.1",
36
+ "@zhin.js/skill": "1.1.1",
37
+ "@zhin.js/tool": "1.1.1"
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",
@@ -1,19 +0,0 @@
1
- import type { RuntimeConfigDocument } from './config-composer.js';
2
- import type { ConfigPatch } from './config-patch-planner.js';
3
- export interface ConfigDocumentSnapshot {
4
- readonly document: RuntimeConfigDocument;
5
- readonly revision: string;
6
- }
7
- /** A prepared write is inert until the generation handoff activates it. */
8
- export interface PreparedConfigDocument {
9
- readonly document: RuntimeConfigDocument;
10
- commit(): Promise<ConfigDocumentSnapshot>;
11
- rollback(): Promise<void>;
12
- }
13
- export interface ConfigDocumentPort {
14
- read(): Promise<ConfigDocumentSnapshot>;
15
- prepare(current: ConfigDocumentSnapshot, patches: readonly ConfigPatch[]): Promise<PreparedConfigDocument>;
16
- }
17
- export declare class ConfigDocumentDivergenceError extends Error {
18
- constructor();
19
- }
@@ -1,6 +0,0 @@
1
- export class ConfigDocumentDivergenceError extends Error {
2
- constructor() {
3
- super('ConfigDocument adapter candidate differs from the validated Runtime candidate');
4
- this.name = 'ConfigDocumentDivergenceError';
5
- }
6
- }