@ornexus/neocortex 4.60.18 → 4.60.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sbom.cdx.json +6504 -6410
- package/docs/install/installer-diagnostics.md +52 -0
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +27 -4
- package/packages/client/dist/agent/refresh-stubs.d.ts +7 -0
- package/packages/client/dist/agent/refresh-stubs.js +2 -2
- package/packages/client/dist/cache/encrypted-cache.d.ts +3 -0
- package/packages/client/dist/cache/encrypted-cache.js +1 -1
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -2
- package/packages/client/dist/cli.js +3 -3
- package/packages/client/dist/commands/activate.js +7 -7
- package/packages/client/dist/commands/config.js +1 -1
- package/packages/client/dist/commands/invoke.d.ts +64 -0
- package/packages/client/dist/commands/invoke.js +54 -47
- package/packages/client/dist/config/secure-config.d.ts +9 -3
- package/packages/client/dist/config/secure-config.js +8 -8
- package/packages/client/dist/config.d.ts +1 -1
- package/packages/client/dist/config.js +1 -1
- package/packages/client/dist/continuity/continuity-client-state-store.d.ts +1 -0
- package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1
- package/packages/client/dist/continuity/invoke-hooks.d.ts +2 -1
- package/packages/client/dist/continuity/invoke-hooks.js +1 -1
- package/packages/client/dist/continuity/sqlite-store.d.ts +21 -0
- package/packages/client/dist/continuity/sqlite-store.js +49 -49
- package/packages/client/dist/effects/invoke-effect-gateways.d.ts +22 -0
- package/packages/client/dist/effects/invoke-effect-gateways.js +1 -0
- package/packages/client/dist/errors/frontmatter-diagnostics.d.ts +20 -0
- package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -0
- package/packages/client/dist/generated/command-registry.generated.d.ts +2836 -0
- package/packages/client/dist/generated/command-registry.generated.js +1 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.d.ts +2 -0
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -1
- package/packages/client/dist/index.d.ts +1 -0
- package/packages/client/dist/index.js +1 -1
- package/packages/client/dist/license/license-client.d.ts +3 -0
- package/packages/client/dist/license/license-client.js +1 -1
- package/packages/client/dist/machine/fingerprint.d.ts +3 -1
- package/packages/client/dist/machine/fingerprint.js +2 -2
- package/packages/client/dist/memory/project-memory-writer.js +19 -15
- package/packages/client/dist/{subagent-approval-policy.d.ts → policy/subagent-approval-policy.d.ts} +8 -3
- package/packages/client/dist/policy/subagent-approval-policy.js +1 -0
- package/packages/client/dist/resolvers/local-resolver.d.ts +3 -0
- package/packages/client/dist/resolvers/local-resolver.js +4 -4
- package/packages/client/dist/runner/cli.d.ts +1 -0
- package/packages/client/dist/runner/cli.js +1 -1
- package/packages/client/dist/runner/headless/prepared-story-bundle.d.ts +20 -0
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -0
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +2 -0
- package/packages/client/dist/runner/headless/run-yoloop.js +1 -1
- package/packages/client/dist/runner/portal-sync-worker.js +1 -1
- package/packages/client/dist/runner/scheduler.d.ts +3 -0
- package/packages/client/dist/runner/scheduler.js +7 -7
- package/packages/client/dist/state/project-state-snapshot.js +1 -1
- package/packages/client/dist/tier/tier-aware-client.js +1 -1
- package/packages/client/dist/version.d.ts +3 -0
- package/packages/client/dist/version.js +1 -0
- package/packages/client/dist/yoloop/discovery-hook.js +2 -2
- package/packages/client/dist/yoloop/invoke-hooks.js +4 -4
- package/packages/client/dist/yoloop/shared-discover-epics.d.ts +9 -3
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1
- package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +24 -0
- package/packages/client/dist/yoloop/target-normalization.d.ts +24 -0
- package/packages/client/dist/yoloop/target-normalization.js +1 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +4 -0
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1
- package/packages/shared/dist/command-registry-contract.js +382 -0
- package/packages/shared/dist/generated/command-registry.generated.js +4216 -0
- package/packages/shared/dist/generated/invocation-schema.generated.js +1850 -0
- package/packages/shared/dist/invocation/index.d.ts +175 -0
- package/packages/shared/dist/invocation/index.js +587 -0
- package/packages/shared/dist/nx-invoke-signature.js +233 -0
- package/packages/shared/dist/strict-readonly.js +381 -0
- package/packages/shared/dist/yoloop/wave-decision-receipt.js +77 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +5 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +7 -2
- package/targets-stubs/codex/AGENTS.md +4 -1
- package/targets-stubs/codex/neocortex.toml +4 -0
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +4 -1
- package/targets-stubs/opencode/neocortex.md +3 -0
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
- package/packages/client/dist/subagent-approval-policy.js +0 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/** P205.06 public data-only command-registry admission contract. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
export const COMMAND_REGISTRY_ERROR_CODES = [
|
|
4
|
+
'REGISTRY_UTF8_INVALID', 'REGISTRY_JSON_INVALID', 'REGISTRY_DUPLICATE_MEMBER',
|
|
5
|
+
'REGISTRY_SCHEMA_INVALID', 'REGISTRY_VERSION_UNSUPPORTED', 'REGISTRY_COLLISION',
|
|
6
|
+
'REGISTRY_PROJECTION_DRIFT', 'REGISTRY_HANDLER_ORPHAN', 'REGISTRY_UNAVAILABLE',
|
|
7
|
+
];
|
|
8
|
+
export class CommandRegistryContractError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code) {
|
|
11
|
+
super(code);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = 'CommandRegistryContractError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
17
|
+
const compareBytes = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
18
|
+
const fail = (code) => { throw new CommandRegistryContractError(code); };
|
|
19
|
+
export function normalizeCommandTokenExact(input) {
|
|
20
|
+
if (typeof input !== 'string' || /[^\x20-\x7e]/.test(input))
|
|
21
|
+
return '';
|
|
22
|
+
const trimmed = input.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, '');
|
|
23
|
+
if (!trimmed || trimmed.startsWith('**'))
|
|
24
|
+
return '';
|
|
25
|
+
const unstarred = trimmed.startsWith('*') ? trimmed.slice(1) : trimmed;
|
|
26
|
+
const normalized = unstarred.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
27
|
+
return ID.test(normalized) || normalized === '?' ? normalized : '';
|
|
28
|
+
}
|
|
29
|
+
function assertUnicode(value) {
|
|
30
|
+
for (let i = 0; i < value.length; i++) {
|
|
31
|
+
const current = value.charCodeAt(i);
|
|
32
|
+
if (current >= 0xd800 && current <= 0xdbff) {
|
|
33
|
+
const next = value.charCodeAt(++i);
|
|
34
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
35
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
36
|
+
}
|
|
37
|
+
else if (current >= 0xdc00 && current <= 0xdfff)
|
|
38
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function canonicalizeCommandRegistryJcs(value) {
|
|
42
|
+
if (value === null)
|
|
43
|
+
return 'null';
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
assertUnicode(value);
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'boolean')
|
|
49
|
+
return value ? 'true' : 'false';
|
|
50
|
+
if (typeof value === 'number') {
|
|
51
|
+
if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value)))
|
|
52
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return `[${value.map(canonicalizeCommandRegistryJcs).join(',')}]`;
|
|
57
|
+
if (!value || typeof value !== 'object')
|
|
58
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
59
|
+
const record = value;
|
|
60
|
+
return `{${Object.keys(record).sort(compareBytes).map((key) => `${JSON.stringify(key)}:${canonicalizeCommandRegistryJcs(record[key])}`).join(',')}}`;
|
|
61
|
+
}
|
|
62
|
+
/** Detect duplicate object members before JSON.parse can silently last-win. */
|
|
63
|
+
function rejectDuplicateMembers(text) {
|
|
64
|
+
let index = 0;
|
|
65
|
+
const ws = () => { while (/\s/.test(text[index] ?? ''))
|
|
66
|
+
index++; };
|
|
67
|
+
const string = () => {
|
|
68
|
+
if (text[index++] !== '"')
|
|
69
|
+
fail('REGISTRY_JSON_INVALID');
|
|
70
|
+
const start = index - 1;
|
|
71
|
+
let escaped = false;
|
|
72
|
+
while (index < text.length) {
|
|
73
|
+
const char = text[index++];
|
|
74
|
+
if (!escaped && char === '"') {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text.slice(start, index));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
fail('REGISTRY_JSON_INVALID');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
escaped = !escaped && char === '\\';
|
|
83
|
+
if (char !== '\\')
|
|
84
|
+
escaped = false;
|
|
85
|
+
}
|
|
86
|
+
return fail('REGISTRY_JSON_INVALID');
|
|
87
|
+
};
|
|
88
|
+
const value = () => {
|
|
89
|
+
ws();
|
|
90
|
+
const char = text[index];
|
|
91
|
+
if (char === '{')
|
|
92
|
+
object();
|
|
93
|
+
else if (char === '[')
|
|
94
|
+
array();
|
|
95
|
+
else if (char === '"') {
|
|
96
|
+
string();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const match = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(index));
|
|
100
|
+
if (!match)
|
|
101
|
+
return fail('REGISTRY_JSON_INVALID');
|
|
102
|
+
index += match[0].length;
|
|
103
|
+
}
|
|
104
|
+
ws();
|
|
105
|
+
};
|
|
106
|
+
const array = () => {
|
|
107
|
+
index++;
|
|
108
|
+
ws();
|
|
109
|
+
if (text[index] === ']') {
|
|
110
|
+
index++;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
while (true) {
|
|
114
|
+
value();
|
|
115
|
+
if (text[index] === ']') {
|
|
116
|
+
index++;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (text[index++] !== ',')
|
|
120
|
+
fail('REGISTRY_JSON_INVALID');
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const object = () => {
|
|
124
|
+
index++;
|
|
125
|
+
ws();
|
|
126
|
+
const keys = new Set();
|
|
127
|
+
if (text[index] === '}') {
|
|
128
|
+
index++;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
while (true) {
|
|
132
|
+
ws();
|
|
133
|
+
const key = string();
|
|
134
|
+
if (keys.has(key))
|
|
135
|
+
fail('REGISTRY_DUPLICATE_MEMBER');
|
|
136
|
+
keys.add(key);
|
|
137
|
+
ws();
|
|
138
|
+
if (text[index++] !== ':')
|
|
139
|
+
fail('REGISTRY_JSON_INVALID');
|
|
140
|
+
value();
|
|
141
|
+
if (text[index] === '}') {
|
|
142
|
+
index++;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (text[index++] !== ',')
|
|
146
|
+
fail('REGISTRY_JSON_INVALID');
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
value();
|
|
150
|
+
if (index !== text.length)
|
|
151
|
+
fail('REGISTRY_JSON_INVALID');
|
|
152
|
+
}
|
|
153
|
+
const ROOT_KEYS = ['commands', 'registryVersion', 'schemaVersion'];
|
|
154
|
+
const COMMAND_KEYS = ['aliases', 'category', 'compatibility', 'description', 'examples', 'handlerClass', 'handlerRef', 'id', 'inputShape', 'invocation', 'mode', 'name', 'number', 'policy', 'projections', 'stepId', 'subcommands', 'visibility'];
|
|
155
|
+
const OPERATION_KEYS = ['aliases', 'compatibility', 'handlerClass', 'handlerRef', 'id', 'policy', 'projections', 'visibility'];
|
|
156
|
+
const INVOCATION_KEYS = ['operation', 'options', 'positionals', 'terminator'];
|
|
157
|
+
const INVOCATION_OPERATION_KEYS = ['required', 'unknownAsPositional'];
|
|
158
|
+
const INVOCATION_OPTION_KEYS = ['aliases', 'cardinality', 'default', 'id', 'kind', 'required', 'syntax', 'valuePattern'];
|
|
159
|
+
const INVOCATION_POSITIONAL_KEYS = ['cardinality', 'id', 'kind', 'order', 'required'];
|
|
160
|
+
const POLICY_KEYS = ['billingClass', 'idempotencyClass', 'minimumTier', 'mutationClass', 'privilege', 'quotaClass', 'replayClass', 'strictReadOnly'];
|
|
161
|
+
const COMPATIBILITY_KEYS = ['deprecatedIn', 'introducedIn', 'removedIn', 'replacement'];
|
|
162
|
+
const DESCRIPTION_KEYS = ['en', 'es', 'pt-BR'];
|
|
163
|
+
const PROJECTIONS = ['admin', 'client', 'docs', 'metrics', 'server', 'shared'];
|
|
164
|
+
function exactKeys(value, expected) {
|
|
165
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
166
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
167
|
+
const actual = Object.keys(value).sort(compareBytes);
|
|
168
|
+
const keys = [...expected].sort(compareBytes);
|
|
169
|
+
if (actual.length !== keys.length || actual.some((key, index) => key !== keys[index]))
|
|
170
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
171
|
+
}
|
|
172
|
+
function stringArray(value, allowQuestion = false) {
|
|
173
|
+
if (!Array.isArray(value))
|
|
174
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
175
|
+
const result = value.map((item) => {
|
|
176
|
+
if (typeof item !== 'string')
|
|
177
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
178
|
+
const text = item;
|
|
179
|
+
if (allowQuestion && text === '?')
|
|
180
|
+
return text;
|
|
181
|
+
if (normalizeCommandTokenExact(text) !== text)
|
|
182
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
183
|
+
return text;
|
|
184
|
+
});
|
|
185
|
+
if (new Set(result).size !== result.length)
|
|
186
|
+
fail('REGISTRY_COLLISION');
|
|
187
|
+
return [...result].sort(compareBytes);
|
|
188
|
+
}
|
|
189
|
+
function validateCompatibility(value) {
|
|
190
|
+
exactKeys(value, COMPATIBILITY_KEYS);
|
|
191
|
+
const record = value;
|
|
192
|
+
for (const key of COMPATIBILITY_KEYS) {
|
|
193
|
+
if (record[key] !== null && typeof record[key] !== 'string')
|
|
194
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
195
|
+
}
|
|
196
|
+
return record;
|
|
197
|
+
}
|
|
198
|
+
function validatePolicy(value) {
|
|
199
|
+
exactKeys(value, POLICY_KEYS);
|
|
200
|
+
const record = value;
|
|
201
|
+
const allowed = {
|
|
202
|
+
privilege: ['authenticated', 'admin'], minimumTier: ['free', 'pro', 'enterprise'],
|
|
203
|
+
billingClass: ['billable', 'non-billable'], quotaClass: ['none', 'invocation', 'workflow', 'planning'],
|
|
204
|
+
mutationClass: ['read-only', 'stateful', 'external'], strictReadOnly: ['allow', 'deny'],
|
|
205
|
+
replayClass: ['signed-request', 'none'], idempotencyClass: ['none', 'logical-supported', 'logical-required'],
|
|
206
|
+
};
|
|
207
|
+
for (const [key, values] of Object.entries(allowed))
|
|
208
|
+
if (!values.includes(record[key]))
|
|
209
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
210
|
+
return record;
|
|
211
|
+
}
|
|
212
|
+
function validateInvocation(value) {
|
|
213
|
+
exactKeys(value, INVOCATION_KEYS);
|
|
214
|
+
const invocation = value;
|
|
215
|
+
exactKeys(invocation.operation, INVOCATION_OPERATION_KEYS);
|
|
216
|
+
if (typeof invocation.operation.required !== 'boolean'
|
|
217
|
+
|| typeof invocation.operation.unknownAsPositional !== 'boolean'
|
|
218
|
+
|| invocation.terminator !== true)
|
|
219
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
220
|
+
if (!Array.isArray(invocation.options) || !Array.isArray(invocation.positionals))
|
|
221
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
222
|
+
const optionIds = new Set();
|
|
223
|
+
const optionAliases = new Set();
|
|
224
|
+
const options = invocation.options.map((raw) => {
|
|
225
|
+
exactKeys(raw, INVOCATION_OPTION_KEYS);
|
|
226
|
+
const option = raw;
|
|
227
|
+
if (typeof option.id !== 'string' || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(option.id)
|
|
228
|
+
|| optionIds.has(option.id) || optionAliases.has(option.id))
|
|
229
|
+
fail('REGISTRY_COLLISION');
|
|
230
|
+
const optionId = option.id;
|
|
231
|
+
optionIds.add(optionId);
|
|
232
|
+
const aliases = stringArray(option.aliases);
|
|
233
|
+
for (const alias of aliases) {
|
|
234
|
+
if (optionIds.has(alias) || optionAliases.has(alias))
|
|
235
|
+
fail('REGISTRY_COLLISION');
|
|
236
|
+
optionAliases.add(alias);
|
|
237
|
+
}
|
|
238
|
+
if (!['boolean', 'string', 'string-list', 'url', 'path'].includes(option.kind)
|
|
239
|
+
|| !['one', 'many'].includes(option.cardinality) || typeof option.required !== 'boolean')
|
|
240
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
241
|
+
if (option.default !== null && typeof option.default !== 'boolean' && typeof option.default !== 'string')
|
|
242
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
243
|
+
if (!Array.isArray(option.syntax) || !option.syntax.length
|
|
244
|
+
|| option.syntax.some((syntax) => !['bare', 'split', 'inline'].includes(syntax))
|
|
245
|
+
|| new Set(option.syntax).size !== option.syntax.length)
|
|
246
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
247
|
+
if (option.kind === 'boolean' && (option.default !== false || JSON.stringify(option.syntax) !== '["bare"]'))
|
|
248
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
249
|
+
if (option.kind !== 'boolean' && (option.syntax.includes('bare') || option.default !== null))
|
|
250
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
251
|
+
if (option.valuePattern !== null && (typeof option.valuePattern !== 'string'
|
|
252
|
+
|| option.valuePattern.length > 256 || !option.valuePattern.startsWith('^') || !option.valuePattern.endsWith('$')))
|
|
253
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
254
|
+
if (option.kind === 'boolean' && option.valuePattern !== null)
|
|
255
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
256
|
+
return { ...option, id: optionId, aliases };
|
|
257
|
+
}).sort((a, b) => compareBytes(a.id, b.id));
|
|
258
|
+
const positionalIds = new Set();
|
|
259
|
+
const positionals = invocation.positionals.map((raw, index) => {
|
|
260
|
+
exactKeys(raw, INVOCATION_POSITIONAL_KEYS);
|
|
261
|
+
const positional = raw;
|
|
262
|
+
if (typeof positional.id !== 'string' || !/^[a-z][A-Za-z0-9]*$/.test(positional.id) || positionalIds.has(positional.id)
|
|
263
|
+
|| !['string', 'story-ref', 'epic-ref', 'target-ref', 'uiux-part'].includes(positional.kind)
|
|
264
|
+
|| !['one', 'many'].includes(positional.cardinality) || typeof positional.required !== 'boolean'
|
|
265
|
+
|| positional.order !== index)
|
|
266
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
267
|
+
const positionalId = positional.id;
|
|
268
|
+
positionalIds.add(positionalId);
|
|
269
|
+
return { ...positional, id: positionalId };
|
|
270
|
+
});
|
|
271
|
+
if (positionals.some((positional, index) => positional.cardinality === 'many' && index !== positionals.length - 1))
|
|
272
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
273
|
+
return { operation: invocation.operation, options, positionals, terminator: true };
|
|
274
|
+
}
|
|
275
|
+
export function normalizeCommandRegistryDocument(root) {
|
|
276
|
+
exactKeys(root, ROOT_KEYS);
|
|
277
|
+
if (root.schemaVersion !== 1 || typeof root.registryVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(root.registryVersion) || !Array.isArray(root.commands)) {
|
|
278
|
+
fail('REGISTRY_VERSION_UNSUPPORTED');
|
|
279
|
+
}
|
|
280
|
+
const tokens = new Map();
|
|
281
|
+
const numbers = new Set();
|
|
282
|
+
const handlerRefs = new Set();
|
|
283
|
+
const commands = root.commands.map((raw) => {
|
|
284
|
+
exactKeys(raw, COMMAND_KEYS);
|
|
285
|
+
const command = raw;
|
|
286
|
+
if (typeof command.id !== 'string' || normalizeCommandTokenExact(command.id) !== command.id)
|
|
287
|
+
fail('REGISTRY_COLLISION');
|
|
288
|
+
if (command.visibility !== 'public' && command.visibility !== 'internal')
|
|
289
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
290
|
+
if (command.visibility === 'public') {
|
|
291
|
+
if (!Number.isSafeInteger(command.number) || command.number <= 0 || numbers.has(command.number))
|
|
292
|
+
fail('REGISTRY_COLLISION');
|
|
293
|
+
numbers.add(command.number);
|
|
294
|
+
}
|
|
295
|
+
else if (command.number !== null)
|
|
296
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
297
|
+
const aliases = stringArray(command.aliases, true);
|
|
298
|
+
for (const token of [command.id, ...aliases]) {
|
|
299
|
+
const normalized = normalizeCommandTokenExact(token);
|
|
300
|
+
if (!normalized || tokens.has(normalized))
|
|
301
|
+
fail('REGISTRY_COLLISION');
|
|
302
|
+
tokens.set(normalized, command.id);
|
|
303
|
+
}
|
|
304
|
+
if (typeof command.handlerRef !== 'string' || !/^handler\.[a-z0-9.-]+\.v\d+$/.test(command.handlerRef) || handlerRefs.has(command.handlerRef))
|
|
305
|
+
fail('REGISTRY_HANDLER_ORPHAN');
|
|
306
|
+
handlerRefs.add(command.handlerRef);
|
|
307
|
+
if (command.handlerClass !== 'step' && command.handlerClass !== 'specialized')
|
|
308
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
309
|
+
if (typeof command.category !== 'string' || typeof command.inputShape !== 'string' || typeof command.mode !== 'string' || typeof command.name !== 'string' || typeof command.stepId !== 'string')
|
|
310
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
311
|
+
exactKeys(command.description, DESCRIPTION_KEYS);
|
|
312
|
+
for (const key of DESCRIPTION_KEYS)
|
|
313
|
+
if (typeof command.description[key] !== 'string')
|
|
314
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
315
|
+
if (!Array.isArray(command.examples) || command.examples.some((entry) => typeof entry !== 'string'))
|
|
316
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
317
|
+
const projections = stringArray(command.projections).filter((entry) => PROJECTIONS.includes(entry));
|
|
318
|
+
if (projections.length !== command.projections.length)
|
|
319
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
320
|
+
if (!Array.isArray(command.subcommands))
|
|
321
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
322
|
+
const local = new Set();
|
|
323
|
+
const subcommands = command.subcommands.map((rawOperation) => {
|
|
324
|
+
exactKeys(rawOperation, OPERATION_KEYS);
|
|
325
|
+
const operation = rawOperation;
|
|
326
|
+
if (typeof operation.id !== 'string' || normalizeCommandTokenExact(operation.id) !== operation.id || local.has(operation.id))
|
|
327
|
+
fail('REGISTRY_COLLISION');
|
|
328
|
+
local.add(operation.id);
|
|
329
|
+
const operationAliases = stringArray(operation.aliases);
|
|
330
|
+
for (const alias of operationAliases) {
|
|
331
|
+
if (local.has(alias))
|
|
332
|
+
fail('REGISTRY_COLLISION');
|
|
333
|
+
local.add(alias);
|
|
334
|
+
}
|
|
335
|
+
if (operation.visibility !== command.visibility)
|
|
336
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
337
|
+
if (typeof operation.handlerRef !== 'string' || handlerRefs.has(operation.handlerRef))
|
|
338
|
+
fail('REGISTRY_HANDLER_ORPHAN');
|
|
339
|
+
handlerRefs.add(operation.handlerRef);
|
|
340
|
+
if (operation.handlerClass !== 'step' && operation.handlerClass !== 'specialized')
|
|
341
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
342
|
+
const operationProjections = stringArray(operation.projections);
|
|
343
|
+
if (operationProjections.some((entry) => !PROJECTIONS.includes(entry)))
|
|
344
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
345
|
+
return { ...operation, aliases: operationAliases, projections: operationProjections, policy: validatePolicy(operation.policy), compatibility: validateCompatibility(operation.compatibility) };
|
|
346
|
+
}).sort((left, right) => compareBytes(left.id, right.id));
|
|
347
|
+
return {
|
|
348
|
+
...command,
|
|
349
|
+
aliases,
|
|
350
|
+
projections,
|
|
351
|
+
policy: validatePolicy(command.policy),
|
|
352
|
+
compatibility: validateCompatibility(command.compatibility),
|
|
353
|
+
invocation: validateInvocation(command.invocation),
|
|
354
|
+
subcommands,
|
|
355
|
+
};
|
|
356
|
+
}).sort((left, right) => compareBytes(left.id, right.id));
|
|
357
|
+
return { schemaVersion: 1, registryVersion: root.registryVersion, commands };
|
|
358
|
+
}
|
|
359
|
+
export function parseCommandRegistryBytes(bytes) {
|
|
360
|
+
let text = '';
|
|
361
|
+
try {
|
|
362
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
fail('REGISTRY_UTF8_INVALID');
|
|
366
|
+
}
|
|
367
|
+
const trimmed = text.trim();
|
|
368
|
+
rejectDuplicateMembers(trimmed);
|
|
369
|
+
let parsed;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(trimmed);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
fail('REGISTRY_JSON_INVALID');
|
|
375
|
+
}
|
|
376
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
377
|
+
fail('REGISTRY_SCHEMA_INVALID');
|
|
378
|
+
return normalizeCommandRegistryDocument(parsed);
|
|
379
|
+
}
|
|
380
|
+
export function commandRegistryChecksum(document) {
|
|
381
|
+
return createHash('sha256').update(canonicalizeCommandRegistryJcs(document)).digest('hex');
|
|
382
|
+
}
|