@gregiteen/ssss-cli 0.8.0 → 0.9.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/CHANGELOG.md +55 -4
- package/README.md +42 -84
- package/VERSION +1 -1
- package/conformance/README.md +1 -1
- package/conformance/fixtures.json +4 -48
- package/conformance/reference-bundle.ucw.json +22 -21
- package/docs/help/bundle.md +2 -2
- package/docs/help/overview.md +9 -6
- package/docs/help/semantic.md +5 -4
- package/docs/ssss-spec.md +42 -86
- package/package.json +22 -3
- package/registry/core.json +22 -29
- package/scripts/build-reference-bundle.mjs +0 -20
- package/scripts/cmd-adapter.mjs +29 -0
- package/scripts/cmd-import.mjs +1 -1
- package/scripts/cmd-migrate.mjs +26 -0
- package/scripts/cmd-new.mjs +5 -5
- package/scripts/cmd-primitive.mjs +48 -0
- package/scripts/cmd-registry.mjs +25 -0
- package/scripts/cmd-semantic.mjs +6 -8
- package/scripts/conformance-09.mjs +331 -0
- package/scripts/conformance.mjs +153 -186
- package/scripts/generate-sbom.mjs +96 -0
- package/scripts/phase9-cross-host-corpus.mjs +223 -0
- package/scripts/ssss.mjs +8 -2
- package/skills/ssss/SKILL.md +32 -208
- package/skills/ssss/evals/evals.json +3 -3
- package/skills/ssss/references/contract-reference.md +7 -8
- package/skills/ssss/scripts/validate-schema.mjs +1 -44
- package/src/authorization.mjs +62 -0
- package/src/bundle.mjs +41 -5
- package/src/engine.mjs +101 -503
- package/src/events.mjs +155 -0
- package/src/guard.mjs +32 -0
- package/src/http.mjs +26 -0
- package/src/idempotency.mjs +55 -0
- package/src/kernel.mjs +260 -0
- package/src/leases.mjs +173 -0
- package/src/primitive.mjs +227 -0
- package/src/projections.mjs +133 -0
- package/src/registry.mjs +190 -39
- package/src/semantic.mjs +124 -316
- package/src/ui.mjs +100 -0
- package/src/validator.mjs +87 -0
- package/src/vfs.mjs +203 -0
- package/types/kernel.d.ts +48 -0
- package/docs/help/localization.md +0 -43
- package/scripts/cmd-localize.mjs +0 -44
package/src/registry.mjs
CHANGED
|
@@ -18,7 +18,7 @@ const DEFAULT_REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
|
|
|
18
18
|
|
|
19
19
|
const APPEND_TYPES = new Set(['conversation', 'run']);
|
|
20
20
|
const SAFE_REGISTRY_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
21
|
-
const SAFE_TYPE_NAME = /^[a-z][a-z0-9_-]{0,127}$/;
|
|
21
|
+
const SAFE_TYPE_NAME = /^(?:[a-z][a-z0-9_-]{0,63}:)?[a-z][a-z0-9_-]{0,127}$/;
|
|
22
22
|
const SAFE_FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
23
23
|
|
|
24
24
|
function isRecord(value) {
|
|
@@ -109,58 +109,124 @@ function validatePrimitiveDefinition(name, def, source, portabilityClasses) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/** Return true when a primitive identifier is a safe local or qualified name. */
|
|
113
|
+
export function isPrimitiveTypeName(value) {
|
|
114
|
+
return typeof value === 'string' && SAFE_TYPE_NAME.test(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeExtensionInput(item, index) {
|
|
118
|
+
if (item?.document_primitives) return { source: item.source || `extension[${index}]`, value: item };
|
|
119
|
+
if (item?.registry?.document_primitives) {
|
|
120
|
+
return { source: item.source || `extension[${index}]`, value: item.registry };
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`extension[${index}] must be an extension registry object.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function semverSatisfies(version, range) {
|
|
126
|
+
if (range === '*' || range === undefined) return true;
|
|
127
|
+
if (version === range) return true;
|
|
128
|
+
const current = String(version || '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
129
|
+
const wanted = String(range).match(/^([~^])(\d+)\.(\d+)\.(\d+)$/);
|
|
130
|
+
if (!current || !wanted) return false;
|
|
131
|
+
const [, operator, major, minor, patch] = wanted;
|
|
132
|
+
const [cm, cn, cp] = current.slice(1).map(Number);
|
|
133
|
+
const [wm, wn, wp] = [major, minor, patch].map(Number);
|
|
134
|
+
if (operator === '^') return cm === wm && (cn > wn || (cn === wn && cp >= wp));
|
|
135
|
+
return cm === wm && cn === wn && cp >= wp;
|
|
136
|
+
}
|
|
137
|
+
|
|
112
138
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @returns {{ types: Map<string, object>, contractTypes: Set<string>, portabilityClasses: string[], core: object, extensions: Set<string> }}
|
|
139
|
+
* Compose the package core with extension/repository/workspace registry objects.
|
|
140
|
+
* This is the runtime composition path; hosts do not copy core.json into their repos.
|
|
116
141
|
*/
|
|
117
|
-
export function
|
|
118
|
-
|
|
119
|
-
const core = JSON.parse(fs.readFileSync(corePath, 'utf8'));
|
|
142
|
+
export function composeRegistries({ core, extensions: extensionInputs = [] } = {}) {
|
|
143
|
+
if (!isRecord(core)) throw new Error('core registry must be an object.');
|
|
120
144
|
const portabilityClasses = Object.keys(core.portability?.classes || {});
|
|
121
|
-
if (!portabilityClasses.length) throw new Error('core
|
|
122
|
-
const extensions = new Set();
|
|
145
|
+
if (!portabilityClasses.length) throw new Error('core registry: portability.classes must not be empty.');
|
|
123
146
|
|
|
147
|
+
const extensions = new Set();
|
|
148
|
+
const extensionVersions = new Map();
|
|
149
|
+
const extensionRequirements = [];
|
|
124
150
|
const types = new Map();
|
|
151
|
+
const aliases = new Map();
|
|
152
|
+
|
|
125
153
|
for (const [name, def] of Object.entries(core.document_primitives || {})) {
|
|
126
|
-
validatePrimitiveDefinition(name, def, 'core
|
|
127
|
-
|
|
154
|
+
validatePrimitiveDefinition(name, def, 'core registry', portabilityClasses);
|
|
155
|
+
const value = { ...def, type: name, qualified_type: `ssss:${name}`, registry: 'core' };
|
|
156
|
+
types.set(name, value);
|
|
157
|
+
aliases.set(`ssss:${name}`, name);
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
for (const [index, item] of extensionInputs.entries()) {
|
|
161
|
+
const { source, value: ext } = normalizeExtensionInput(item, index);
|
|
162
|
+
if (!SAFE_REGISTRY_NAME.test(ext.registry || '')) {
|
|
163
|
+
throw new Error(`${source}: registry must be a safe identifier.`);
|
|
164
|
+
}
|
|
165
|
+
if (ext.registry === 'core' || ext.registry === 'ssss' || extensions.has(ext.registry)) {
|
|
166
|
+
throw new Error(`${source}: duplicate or reserved registry name '${ext.registry}'.`);
|
|
167
|
+
}
|
|
168
|
+
if (ext.extends !== undefined && ext.extends !== 'core' && ext.extends !== 'ssss') {
|
|
169
|
+
throw new Error(`${source}: extends must be 'core' or 'ssss'.`);
|
|
170
|
+
}
|
|
171
|
+
if (ext.version !== undefined &&
|
|
172
|
+
(typeof ext.version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(ext.version))) {
|
|
173
|
+
throw new Error(`${source}: version must be a semantic version string.`);
|
|
174
|
+
}
|
|
175
|
+
assertRecord(ext.requires, `${source}: requires`);
|
|
176
|
+
extensions.add(ext.registry);
|
|
177
|
+
extensionVersions.set(ext.registry, ext.version || '0.0.0');
|
|
178
|
+
extensionRequirements.push({ source, registry: ext.registry, requires: ext.requires || {} });
|
|
179
|
+
|
|
180
|
+
for (const [name, def] of Object.entries(ext.document_primitives || {})) {
|
|
181
|
+
validatePrimitiveDefinition(name, def, source, portabilityClasses);
|
|
182
|
+
const qualified = name.includes(':') ? name : `${ext.registry}:${name}`;
|
|
183
|
+
if (name.includes(':') && !name.startsWith(`${ext.registry}:`)) {
|
|
184
|
+
throw new Error(`${source}: primitive '${name}' must use registry namespace '${ext.registry}'.`);
|
|
140
185
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
186
|
+
if (types.has(name) || aliases.has(name) || types.has(qualified) || aliases.has(qualified)) {
|
|
187
|
+
const existing = types.get(name) || types.get(aliases.get(name)) ||
|
|
188
|
+
types.get(qualified) || types.get(aliases.get(qualified));
|
|
189
|
+
throw new Error(
|
|
190
|
+
`${source}: primitive '${name}' collides with registry '${existing?.registry || 'unknown'}'.`
|
|
191
|
+
);
|
|
143
192
|
}
|
|
144
|
-
|
|
145
|
-
|
|
193
|
+
const value = { ...def, type: name, qualified_type: qualified, registry: ext.registry };
|
|
194
|
+
types.set(name, value);
|
|
195
|
+
if (qualified !== name) aliases.set(qualified, name);
|
|
196
|
+
for (const alias of def.aliases || []) {
|
|
197
|
+
if (!isPrimitiveTypeName(alias)) throw new Error(`${source}: primitive '${name}' has unsafe alias '${alias}'.`);
|
|
198
|
+
if (types.has(alias) || aliases.has(alias)) throw new Error(`${source}: primitive alias '${alias}' collides with an existing primitive.`);
|
|
199
|
+
aliases.set(alias, name);
|
|
146
200
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
types.set(name, { ...def, type: name, registry: ext.registry });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const requirement of extensionRequirements) {
|
|
205
|
+
for (const [dependency, range] of Object.entries(requirement.requires)) {
|
|
206
|
+
if (!extensionVersions.has(dependency)) throw new Error(`${requirement.source}: missing required extension '${dependency}'.`);
|
|
207
|
+
if (typeof range !== 'string' || !semverSatisfies(extensionVersions.get(dependency), range)) {
|
|
208
|
+
throw new Error(`${requirement.source}: extension '${dependency}' version '${extensionVersions.get(dependency)}' does not satisfy '${range}'.`);
|
|
156
209
|
}
|
|
157
210
|
}
|
|
158
211
|
}
|
|
159
212
|
|
|
213
|
+
const registrySet = {
|
|
214
|
+
types,
|
|
215
|
+
aliases,
|
|
216
|
+
contractTypes: new Set(Object.keys(core.contract_primitives || {})),
|
|
217
|
+
portabilityClasses,
|
|
218
|
+
core,
|
|
219
|
+
extensions,
|
|
220
|
+
extensionVersions,
|
|
221
|
+
};
|
|
222
|
+
// Backward-compatible Map surface used by the existing engine/bundle APIs.
|
|
223
|
+
// The alias table lets shared validators resolve qualified 0.9 identifiers.
|
|
224
|
+
types.aliases = aliases;
|
|
225
|
+
|
|
160
226
|
for (const def of types.values()) {
|
|
161
227
|
for (const [field, constraint] of Object.entries(def.references || {})) {
|
|
162
228
|
for (const target of [...(constraint.allowed_types || []), ...(constraint.disallowed_types || [])]) {
|
|
163
|
-
if (!
|
|
229
|
+
if (!isPrimitiveTypeName(target) || !resolvePrimitiveDefinition(registrySet, target)) {
|
|
164
230
|
throw new Error(
|
|
165
231
|
`registry '${def.registry}' primitive '${def.type}' reference '${field}' names unknown type '${target}'.`
|
|
166
232
|
);
|
|
@@ -169,8 +235,91 @@ export function loadRegistries(registryDir = DEFAULT_REGISTRY_DIR) {
|
|
|
169
235
|
}
|
|
170
236
|
}
|
|
171
237
|
|
|
172
|
-
|
|
173
|
-
|
|
238
|
+
return registrySet;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Compose named ownership layers without giving lower scopes override semantics. */
|
|
242
|
+
export function composeRegistryLayers({ core, installed = [], repository = [], workspace = [], user = [], policyFloors = {} } = {}) {
|
|
243
|
+
const extensions = [
|
|
244
|
+
...installed.map((registry) => ({ source: 'installed', registry })),
|
|
245
|
+
...repository.map((registry) => ({ source: 'repository', registry })),
|
|
246
|
+
...workspace.map((registry) => ({ source: 'workspace', registry })),
|
|
247
|
+
...user.map((registry) => ({ source: 'user', registry })),
|
|
248
|
+
];
|
|
249
|
+
const registrySet = composeRegistries({ core, extensions });
|
|
250
|
+
for (const [type, actions] of Object.entries(policyFloors)) {
|
|
251
|
+
const definition = resolvePrimitiveDefinition(registrySet, type);
|
|
252
|
+
if (!definition) throw new Error(`Policy floor names unknown primitive '${type}'.`);
|
|
253
|
+
for (const [action, required] of Object.entries(actions || {})) {
|
|
254
|
+
assertStringArray(required, `policy floor '${type}.${action}'`);
|
|
255
|
+
const declared = new Set(definition.capabilities?.[action] || []);
|
|
256
|
+
const missing = required.filter((capability) => !declared.has(capability));
|
|
257
|
+
if (missing.length) throw new Error(`Primitive '${type}' weakens policy floor for '${action}': ${missing.join(', ')}.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return registrySet;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function stableObject(value) {
|
|
264
|
+
if (Array.isArray(value)) return value.map(stableObject);
|
|
265
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableObject(value[key])]));
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createRegistryLock(registrySet) {
|
|
270
|
+
const primitives = [...registrySet.types.values()]
|
|
271
|
+
.map((definition) => ({ ...definition }))
|
|
272
|
+
.sort((a, b) => a.qualified_type.localeCompare(b.qualified_type));
|
|
273
|
+
const payload = {
|
|
274
|
+
version: 1,
|
|
275
|
+
core_spec_version: registrySet.core.spec_version,
|
|
276
|
+
extensions: Object.fromEntries([...registrySet.extensionVersions.entries()].sort()),
|
|
277
|
+
primitives,
|
|
278
|
+
};
|
|
279
|
+
return { ...payload, integrity: `sha256:${crypto.createHash('sha256').update(JSON.stringify(stableObject(payload))).digest('hex')}` };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function verifyRegistryLock(registrySet, lock) {
|
|
283
|
+
const expected = createRegistryLock(registrySet);
|
|
284
|
+
return {
|
|
285
|
+
valid: !!lock && lock.integrity === expected.integrity,
|
|
286
|
+
expected_integrity: expected.integrity,
|
|
287
|
+
actual_integrity: lock?.integrity || null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Resolve local, qualified, or explicit migration-alias primitive identifiers. */
|
|
292
|
+
export function resolvePrimitiveDefinition(registrySet, type) {
|
|
293
|
+
if (!registrySet || typeof type !== 'string') return null;
|
|
294
|
+
const direct = registrySet.types?.get(type);
|
|
295
|
+
if (direct) return direct;
|
|
296
|
+
const local = registrySet.aliases?.get(type);
|
|
297
|
+
return local ? registrySet.types.get(local) || null : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Load the core registry and merge extension registries.
|
|
302
|
+
* @param {string} [registryDir] - directory containing core.json + extensions/
|
|
303
|
+
* @returns {{ types: Map<string, object>, contractTypes: Set<string>, portabilityClasses: string[], core: object, extensions: Set<string> }}
|
|
304
|
+
*/
|
|
305
|
+
export function loadRegistries(registryDir = DEFAULT_REGISTRY_DIR) {
|
|
306
|
+
const corePath = path.join(registryDir, 'core.json');
|
|
307
|
+
const core = JSON.parse(fs.readFileSync(corePath, 'utf8'));
|
|
308
|
+
const extensionInputs = [];
|
|
309
|
+
const extDir = path.join(registryDir, 'extensions');
|
|
310
|
+
if (fs.existsSync(extDir)) {
|
|
311
|
+
for (const file of fs.readdirSync(extDir).filter((f) => f.endsWith('.json')).sort()) {
|
|
312
|
+
const extPath = path.join(extDir, file);
|
|
313
|
+
if (fs.lstatSync(extPath).isSymbolicLink()) {
|
|
314
|
+
throw new Error(`extensions/${file}: symlinked registries are not allowed.`);
|
|
315
|
+
}
|
|
316
|
+
extensionInputs.push({
|
|
317
|
+
source: `extensions/${file}`,
|
|
318
|
+
registry: JSON.parse(fs.readFileSync(extPath, 'utf8')),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return composeRegistries({ core, extensions: extensionInputs });
|
|
174
323
|
}
|
|
175
324
|
|
|
176
325
|
/** Is this document type append-only (events appended, never rewritten)? */
|
|
@@ -258,6 +407,8 @@ export function validateDataConstraints(typeDef, data, universalRequired = []) {
|
|
|
258
407
|
*/
|
|
259
408
|
export function validateReferenceConstraints(typeDef, data, resolveReference, types) {
|
|
260
409
|
const issues = [];
|
|
410
|
+
const lookupType = (name) => types.get(name) ||
|
|
411
|
+
(types.aliases?.get(name) ? types.get(types.aliases.get(name)) : null);
|
|
261
412
|
for (const [field, constraint] of Object.entries(typeDef?.references || {})) {
|
|
262
413
|
const referencePath = data[field];
|
|
263
414
|
if (referencePath === undefined || referencePath === null || referencePath === '') continue;
|
|
@@ -278,7 +429,7 @@ export function validateReferenceConstraints(typeDef, data, resolveReference, ty
|
|
|
278
429
|
continue;
|
|
279
430
|
}
|
|
280
431
|
const referencedType = referenced.data?.type;
|
|
281
|
-
if (!
|
|
432
|
+
if (!lookupType(referencedType)) {
|
|
282
433
|
issues.push({ field, issue: `Referenced document declares unknown type '${referencedType || '(missing)'}'.` });
|
|
283
434
|
continue;
|
|
284
435
|
}
|
|
@@ -288,7 +439,7 @@ export function validateReferenceConstraints(typeDef, data, resolveReference, ty
|
|
|
288
439
|
if (constraint.disallowed_types?.includes(referencedType)) {
|
|
289
440
|
issues.push({ field, issue: `Referenced type '${referencedType}' is forbidden.` });
|
|
290
441
|
}
|
|
291
|
-
const referencedPortability = resolvePortability(
|
|
442
|
+
const referencedPortability = resolvePortability(lookupType(referencedType), referenced.data);
|
|
292
443
|
if (constraint.allowed_portability && !constraint.allowed_portability.includes(referencedPortability)) {
|
|
293
444
|
issues.push({
|
|
294
445
|
field,
|