@zhin.js/runtime 1.1.7 → 1.1.9
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 +20 -4
- package/lib/application-plugin-declaration.d.ts +17 -0
- package/lib/application-plugin-declaration.js +69 -0
- package/lib/config-composer.d.ts +2408 -167
- package/lib/config-composer.js +52 -20
- package/lib/config-patch-planner.d.ts +3 -2
- package/lib/config-patch-planner.js +7 -5
- package/lib/host-config-schema.json +3845 -142
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/manifest.d.ts +4 -0
- package/lib/manifest.js +14 -0
- package/lib/plugin-scope-assembler.d.ts +3 -3
- package/lib/plugin-scope-assembler.js +18 -8
- package/lib/primary-config.d.ts +6 -2
- package/lib/primary-config.js +9 -1
- package/lib/project-graph.d.ts +2 -2
- package/lib/project-graph.js +18 -17
- package/lib/root-runtime.d.ts +2 -0
- package/lib/root-runtime.js +138 -44
- package/lib/runtime-input-planner.d.ts +3 -1
- package/lib/runtime-input-planner.js +7 -4
- package/lib/slot-generation-preparer.d.ts +3 -1
- package/lib/slot-generation-preparer.js +7 -2
- package/lib/topology-generation-preparer.js +25 -3
- package/lib/topology-transaction.js +3 -1
- package/package.json +15 -15
package/lib/config-composer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
2
2
|
import hostConfigSchema from './host-config-schema.json' with { type: 'json' };
|
|
3
3
|
import { readPluginConfigSchema } from './plugin-config-schema.js';
|
|
4
|
+
import { readApplicationPluginDeclaration } from './application-plugin-declaration.js';
|
|
4
5
|
function deepFreeze(value) {
|
|
5
6
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
6
7
|
for (const nested of Object.values(value))
|
|
@@ -12,6 +13,7 @@ function deepFreeze(value) {
|
|
|
12
13
|
export const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
|
|
13
14
|
/** Host configuration keys consumed by Runtime composition and Host installers. */
|
|
14
15
|
export const HOST_CONFIG_KEYS = Object.freeze(Object.keys(HOST_CONFIG_SCHEMA.properties));
|
|
16
|
+
const ROOT_FEATURE_CONFIG_KEYS = ['menuKeyword', 'menuStyle'];
|
|
15
17
|
export class ConfigSchemaCollisionError extends Error {
|
|
16
18
|
plugin;
|
|
17
19
|
instanceKey;
|
|
@@ -37,22 +39,36 @@ export class ConfigValidationError extends Error {
|
|
|
37
39
|
export class ConfigComposer {
|
|
38
40
|
inactivePluginInstanceKeys;
|
|
39
41
|
modules;
|
|
40
|
-
|
|
42
|
+
validatedSchemas;
|
|
43
|
+
constructor(inactivePluginInstanceKeys = [], modules,
|
|
44
|
+
/** Previously validated generation declarations; never import retired owners just to compare inputs. */
|
|
45
|
+
validatedSchemas) {
|
|
41
46
|
this.inactivePluginInstanceKeys = inactivePluginInstanceKeys;
|
|
42
47
|
this.modules = modules;
|
|
48
|
+
this.validatedSchemas = validatedSchemas;
|
|
43
49
|
}
|
|
44
50
|
async compose(graph, input = {},
|
|
45
51
|
/** Source config file name, annotated on ConfigValidationError. */
|
|
46
52
|
source) {
|
|
53
|
+
const declaration = readApplicationPluginDeclaration(input, source);
|
|
54
|
+
if (this.inactivePluginInstanceKeys.length > 0) {
|
|
55
|
+
throw new TypeError('Legacy disabled Plugin state must move to disable_plugins; see docs/migrations/plugin-instance-declarations.md');
|
|
56
|
+
}
|
|
57
|
+
const enabledKeys = new Set(declaration.instances.filter(instance => instance.enabled).map(instance => instance.instanceKey));
|
|
58
|
+
if (enabledKeys.size !== graph.root.children.length
|
|
59
|
+
|| graph.root.children.some(child => !enabledKeys.has(child.instanceKey))) {
|
|
60
|
+
throw new TypeError('Compose configuration against its candidate application graph before loading Plugin schemas');
|
|
61
|
+
}
|
|
47
62
|
// Effective schemas include child namespaces for whole-tree validation;
|
|
48
63
|
// ownSchemas retain each package's private configuration contract.
|
|
49
64
|
const ownSchemas = new Map();
|
|
50
|
-
const rootOwn = await readOwnSchema(graph.root, this.modules);
|
|
65
|
+
const rootOwn = await readOwnSchema(graph.root, this.modules, this.validatedSchemas);
|
|
51
66
|
ownSchemas.set(graph.root.id, rootOwn);
|
|
52
67
|
const childSchemas = await Promise.all(graph.root.children.map(async (child) => [
|
|
53
68
|
child.instanceKey,
|
|
54
|
-
await composeNode(child, ownSchemas, this.modules),
|
|
69
|
+
await composeNode(child, ownSchemas, this.modules, this.validatedSchemas),
|
|
55
70
|
]));
|
|
71
|
+
const childSchemaByKey = new Map(childSchemas);
|
|
56
72
|
// Host-level keys (`http`, `database`, `ai`, `mcp`, `a2a`, `speech`,
|
|
57
73
|
// `htmlRenderer`, `assistant`, `log_level`) are consumed by CLI Root installers /
|
|
58
74
|
// start-command, not Plugin ConfigViews.
|
|
@@ -63,15 +79,15 @@ export class ConfigComposer {
|
|
|
63
79
|
properties: {
|
|
64
80
|
...HOST_CONFIG_SCHEMA.properties,
|
|
65
81
|
plugin: withDefault(rootOwn),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
]
|
|
74
|
-
},
|
|
82
|
+
configVersion: { const: 2 },
|
|
83
|
+
plugins: { type: 'object', additionalProperties: { type: 'string' }, default: {} },
|
|
84
|
+
disable_plugins: { type: 'array', items: { type: 'string' }, default: [] },
|
|
85
|
+
...Object.fromEntries(declaration.instances.map(instance => {
|
|
86
|
+
const schema = childSchemaByKey.get(instance.instanceKey);
|
|
87
|
+
if (instance.enabled && !schema)
|
|
88
|
+
throw new TypeError(`Candidate graph is missing enabled Plugin ${instance.instanceKey}`);
|
|
89
|
+
return [instance.instanceKey, instance.enabled && schema ? withDefault(schema) : { type: 'object' }];
|
|
90
|
+
})),
|
|
75
91
|
},
|
|
76
92
|
});
|
|
77
93
|
const document = structuredClone(input);
|
|
@@ -83,8 +99,17 @@ export class ConfigComposer {
|
|
|
83
99
|
const views = new Map();
|
|
84
100
|
// A Plugin never receives its effective node object because that object
|
|
85
101
|
// also contains descendants. Re-pick fields from the owner's own schema.
|
|
86
|
-
views.set(graph.root.id,
|
|
87
|
-
|
|
102
|
+
views.set(graph.root.id,
|
|
103
|
+
// Built-in menu settings belong to the application, but Command Feature
|
|
104
|
+
// consumes the root's generation view. Project only these reserved fields;
|
|
105
|
+
// Host credentials and other instances never enter a Plugin ConfigView.
|
|
106
|
+
Object.freeze({
|
|
107
|
+
...pickOwnFields(document.plugin, requireOwnSchema(ownSchemas, graph.root.id)),
|
|
108
|
+
...Object.fromEntries(ROOT_FEATURE_CONFIG_KEYS
|
|
109
|
+
.filter(key => Object.hasOwn(document, key))
|
|
110
|
+
.map(key => [key, deepFreeze(structuredClone(document[key]))])),
|
|
111
|
+
}));
|
|
112
|
+
collectChildViews(graph.root, document, ownSchemas, views);
|
|
88
113
|
return Object.freeze({
|
|
89
114
|
effectiveSchema,
|
|
90
115
|
document: Object.freeze(document),
|
|
@@ -145,8 +170,8 @@ function injectFrameworkRoleSchema(properties) {
|
|
|
145
170
|
}),
|
|
146
171
|
});
|
|
147
172
|
}
|
|
148
|
-
async function composeNode(node, ownSchemas, modules) {
|
|
149
|
-
const own = await readOwnSchema(node, modules);
|
|
173
|
+
async function composeNode(node, ownSchemas, modules, validatedSchemas) {
|
|
174
|
+
const own = await readOwnSchema(node, modules, validatedSchemas);
|
|
150
175
|
ownSchemas.set(node.id, own);
|
|
151
176
|
const properties = { ...schemaProperties(own) };
|
|
152
177
|
// Adapter plugins (schemas declaring array-typed `endpoints`) get
|
|
@@ -159,11 +184,17 @@ async function composeNode(node, ownSchemas, modules) {
|
|
|
159
184
|
if (Object.hasOwn(properties, child.instanceKey)) {
|
|
160
185
|
throw new ConfigSchemaCollisionError(node.id, child.instanceKey);
|
|
161
186
|
}
|
|
162
|
-
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas, modules));
|
|
187
|
+
properties[child.instanceKey] = withDefault(await composeNode(child, ownSchemas, modules, validatedSchemas));
|
|
163
188
|
}
|
|
164
189
|
return Object.freeze({ ...own, properties });
|
|
165
190
|
}
|
|
166
|
-
async function readOwnSchema(node, modules) {
|
|
191
|
+
async function readOwnSchema(node, modules, validatedSchemas) {
|
|
192
|
+
if (validatedSchemas) {
|
|
193
|
+
const schema = validatedSchemas.get(node.id);
|
|
194
|
+
if (!schema)
|
|
195
|
+
throw new TypeError(`Missing committed configuration schema for ${node.id}`);
|
|
196
|
+
return schema;
|
|
197
|
+
}
|
|
167
198
|
const file = `${node.package.name} config`;
|
|
168
199
|
const value = await readPluginConfigSchema(node.package, modules)
|
|
169
200
|
?? { type: 'object', additionalProperties: false, properties: {} };
|
|
@@ -183,8 +214,9 @@ async function readOwnSchema(node, modules) {
|
|
|
183
214
|
throw new TypeError(`${file} root schema must declare properties; composition keywords (anyOf/oneOf/allOf/$ref) are not supported`);
|
|
184
215
|
}
|
|
185
216
|
const properties = schemaProperties(schema);
|
|
186
|
-
|
|
187
|
-
|
|
217
|
+
for (const key of ['commandNamespace', ...(!node.parent ? ROOT_FEATURE_CONFIG_KEYS : [])]) {
|
|
218
|
+
if (Object.hasOwn(properties, key))
|
|
219
|
+
throw new ConfigSchemaCollisionError(node.id, key);
|
|
188
220
|
}
|
|
189
221
|
return deepFreeze({
|
|
190
222
|
$id: `urn:zhin:plugin-config:${encodeURIComponent(String(node.id))}`,
|
|
@@ -9,6 +9,7 @@ export interface ConfigPatchPlan extends ComposedConfig {
|
|
|
9
9
|
/** Validates a candidate document before deriving its minimal replacement forest. */
|
|
10
10
|
export declare class ConfigPatchPlanner {
|
|
11
11
|
private readonly composer;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
private readonly previousComposer;
|
|
13
|
+
constructor(composer?: ConfigComposer, previousComposer?: ConfigComposer);
|
|
14
|
+
plan(graph: ProjectGraph, current: RuntimeConfigDocument, patches: readonly ConfigPatch[], candidateGraph?: ProjectGraph, previousDocument?: RuntimeConfigDocument): Promise<ConfigPatchPlan>;
|
|
14
15
|
}
|
|
@@ -4,14 +4,16 @@ import { ConfigComposer } from './config-composer.js';
|
|
|
4
4
|
/** Validates a candidate document before deriving its minimal replacement forest. */
|
|
5
5
|
export class ConfigPatchPlanner {
|
|
6
6
|
composer;
|
|
7
|
-
|
|
7
|
+
previousComposer;
|
|
8
|
+
constructor(composer = new ConfigComposer(), previousComposer = composer) {
|
|
8
9
|
this.composer = composer;
|
|
10
|
+
this.previousComposer = previousComposer;
|
|
9
11
|
}
|
|
10
|
-
async plan(graph, current, patches) {
|
|
11
|
-
const previous = await this.
|
|
12
|
+
async plan(graph, current, patches, candidateGraph = graph, previousDocument = current) {
|
|
13
|
+
const previous = await this.previousComposer.compose(graph, previousDocument);
|
|
12
14
|
const candidate = applyConfigPatches(current, patches);
|
|
13
|
-
const next = await this.composer.compose(
|
|
14
|
-
const changed = indexGraph(
|
|
15
|
+
const next = await this.composer.compose(candidateGraph, candidate);
|
|
16
|
+
const changed = indexGraph(candidateGraph)
|
|
15
17
|
.filter((node) => !isDeepStrictEqual(previous.views.get(node.id), next.views.get(node.id)))
|
|
16
18
|
.map((node) => node.id);
|
|
17
19
|
return Object.freeze({
|