@gregiteen/ssss-cli 0.14.0 → 0.14.2

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.
@@ -0,0 +1,386 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://ssss.dev/schemas/core-registry.schema.json",
4
+ "title": "SSSS Core Registry Schema",
5
+ "description": "JSON Schema for SSSS core and extension registries (spec §5).",
6
+ "type": "object",
7
+ "required": [
8
+ "registry",
9
+ "spec_version",
10
+ "description",
11
+ "document_primitives"
12
+ ],
13
+ "properties": {
14
+ "$schema": {
15
+ "type": "string"
16
+ },
17
+ "registry": {
18
+ "type": "string"
19
+ },
20
+ "spec_version": {
21
+ "type": "string"
22
+ },
23
+ "description": {
24
+ "type": "string"
25
+ },
26
+ "conformance": {
27
+ "type": "string"
28
+ },
29
+ "document_primitives": {
30
+ "type": "object",
31
+ "additionalProperties": {
32
+ "$ref": "#/$defs/primitiveDefinition"
33
+ }
34
+ },
35
+ "contract_primitives": {
36
+ "type": "object",
37
+ "additionalProperties": {
38
+ "$ref": "#/$defs/contractPrimitive"
39
+ }
40
+ },
41
+ "universal_frontmatter": {
42
+ "type": "object",
43
+ "required": ["required"],
44
+ "properties": {
45
+ "required": {
46
+ "type": "array",
47
+ "items": { "type": "string" }
48
+ },
49
+ "recommended_okf": {
50
+ "type": "array",
51
+ "items": { "type": "string" }
52
+ },
53
+ "recommended_semantic": {
54
+ "type": "array",
55
+ "items": { "type": "string" }
56
+ },
57
+ "spec_ref": {
58
+ "type": "string"
59
+ }
60
+ }
61
+ },
62
+ "portability": {
63
+ "type": "object",
64
+ "required": ["classes", "export_profiles"],
65
+ "properties": {
66
+ "description": { "type": "string" },
67
+ "classes": {
68
+ "type": "object",
69
+ "additionalProperties": { "type": "string" }
70
+ },
71
+ "export_profiles": {
72
+ "type": "object",
73
+ "additionalProperties": {
74
+ "type": "object",
75
+ "required": ["includes"],
76
+ "properties": {
77
+ "includes": {
78
+ "type": "array",
79
+ "items": { "type": "string" }
80
+ },
81
+ "excludes": {
82
+ "type": "array",
83
+ "items": { "type": "string" }
84
+ },
85
+ "resource_bound": { "type": "string" },
86
+ "note": { "type": "string" }
87
+ }
88
+ }
89
+ },
90
+ "conformance": { "type": "string" },
91
+ "spec_ref": { "type": "string" }
92
+ }
93
+ },
94
+ "bundle": {
95
+ "type": "object",
96
+ "required": [
97
+ "container",
98
+ "structure",
99
+ "manifest_required_fields",
100
+ "provenance_required_fields"
101
+ ],
102
+ "properties": {
103
+ "description": { "type": "string" },
104
+ "container": { "type": "string" },
105
+ "structure": {
106
+ "type": "array",
107
+ "items": { "type": "string" }
108
+ },
109
+ "file_sort": { "type": "string" },
110
+ "manifest_required_fields": {
111
+ "type": "array",
112
+ "items": { "type": "string" }
113
+ },
114
+ "manifest_optional_fields": {
115
+ "type": "array",
116
+ "items": { "type": "string" }
117
+ },
118
+ "provenance_required_fields": {
119
+ "type": "array",
120
+ "items": { "type": "string" }
121
+ },
122
+ "provenance_optional_fields": {
123
+ "type": "array",
124
+ "items": { "type": "string" }
125
+ },
126
+ "content_hash": { "type": "string" },
127
+ "parameter_fields": {
128
+ "type": "object",
129
+ "required": ["required"],
130
+ "properties": {
131
+ "required": {
132
+ "type": "array",
133
+ "items": { "type": "string" }
134
+ },
135
+ "optional": {
136
+ "type": "array",
137
+ "items": { "type": "string" }
138
+ },
139
+ "source_enum": {
140
+ "type": "array",
141
+ "items": { "type": "string" }
142
+ },
143
+ "note": { "type": "string" }
144
+ }
145
+ },
146
+ "conformance": { "type": "string" },
147
+ "spec_ref": { "type": "string" }
148
+ }
149
+ },
150
+ "provisioning": {
151
+ "type": "object",
152
+ "required": [
153
+ "verbs",
154
+ "step_required_fields",
155
+ "system_enum",
156
+ "mode_enum"
157
+ ],
158
+ "properties": {
159
+ "description": { "type": "string" },
160
+ "verbs": {
161
+ "type": "object",
162
+ "additionalProperties": { "type": "string" }
163
+ },
164
+ "step_required_fields": {
165
+ "type": "array",
166
+ "items": { "type": "string" }
167
+ },
168
+ "step_optional_fields": {
169
+ "type": "array",
170
+ "items": { "type": "string" }
171
+ },
172
+ "system_enum": {
173
+ "type": "array",
174
+ "items": { "type": "string" }
175
+ },
176
+ "mode_enum": {
177
+ "type": "array",
178
+ "items": { "type": "string" }
179
+ },
180
+ "edge_relations": {
181
+ "type": "array",
182
+ "items": { "type": "string" }
183
+ },
184
+ "install_modes": {
185
+ "type": "array",
186
+ "items": { "type": "string" }
187
+ },
188
+ "id_remap": { "type": "string" },
189
+ "upgrade": { "type": "string" },
190
+ "conformance": { "type": "string" },
191
+ "spec_ref": { "type": "string" }
192
+ }
193
+ },
194
+ "runtime": {
195
+ "type": "object",
196
+ "required": [
197
+ "trigger_types",
198
+ "misfire_policies",
199
+ "concurrency_policies",
200
+ "task_instantiation_fields",
201
+ "run_statuses",
202
+ "idempotency_basis"
203
+ ],
204
+ "properties": {
205
+ "description": { "type": "string" },
206
+ "trigger_types": {
207
+ "type": "array",
208
+ "items": { "type": "string" }
209
+ },
210
+ "misfire_policies": {
211
+ "type": "array",
212
+ "items": { "type": "string" }
213
+ },
214
+ "concurrency_policies": {
215
+ "type": "array",
216
+ "items": { "type": "string" }
217
+ },
218
+ "task_instantiation_fields": {
219
+ "type": "array",
220
+ "items": { "type": "string" }
221
+ },
222
+ "run_statuses": {
223
+ "type": "array",
224
+ "items": { "type": "string" }
225
+ },
226
+ "idempotency_basis": { "type": "string" },
227
+ "spec_ref": { "type": "string" }
228
+ }
229
+ },
230
+ "semantic": {
231
+ "type": "object",
232
+ "required": [
233
+ "version",
234
+ "identity_fields",
235
+ "surface_fields",
236
+ "relation_fields",
237
+ "renderable_fields",
238
+ "language_pattern",
239
+ "safe_default"
240
+ ],
241
+ "properties": {
242
+ "version": { "type": "integer" },
243
+ "identity_fields": {
244
+ "type": "array",
245
+ "items": { "type": "string" }
246
+ },
247
+ "surface_fields": {
248
+ "type": "array",
249
+ "items": { "type": "string" }
250
+ },
251
+ "relation_fields": {
252
+ "type": "array",
253
+ "items": { "type": "string" }
254
+ },
255
+ "renderable_fields": {
256
+ "type": "array",
257
+ "items": { "type": "string" }
258
+ },
259
+ "language_pattern": { "type": "string" },
260
+ "safe_default": { "type": "string" },
261
+ "spec_ref": { "type": "string" }
262
+ }
263
+ },
264
+ "reserved_keys": {
265
+ "type": "array",
266
+ "items": { "type": "string" }
267
+ },
268
+ "host_field_prefix": { "type": "string" },
269
+ "spec_ref_reserved": { "type": "string" }
270
+ },
271
+ "$defs": {
272
+ "primitiveDefinition": {
273
+ "type": "object",
274
+ "required": [
275
+ "family",
276
+ "append_only",
277
+ "portability",
278
+ "required_fields"
279
+ ],
280
+ "properties": {
281
+ "family": {
282
+ "type": "string"
283
+ },
284
+ "canonical_filename": {
285
+ "type": ["string", "null"]
286
+ },
287
+ "filename_convention": {
288
+ "type": "string"
289
+ },
290
+ "append_only": {
291
+ "type": "boolean"
292
+ },
293
+ "portability": {
294
+ "type": "string",
295
+ "enum": ["structural", "tenant_private", "resource_bound"]
296
+ },
297
+ "required_fields": {
298
+ "type": "array",
299
+ "items": { "type": "string" }
300
+ },
301
+ "required_when": {
302
+ "type": "object",
303
+ "additionalProperties": {
304
+ "type": "array",
305
+ "items": { "type": "string" }
306
+ }
307
+ },
308
+ "enums": {
309
+ "type": "object",
310
+ "additionalProperties": {
311
+ "type": "array",
312
+ "items": { "type": "string" }
313
+ }
314
+ },
315
+ "patterns": {
316
+ "type": "object",
317
+ "additionalProperties": {
318
+ "type": "string"
319
+ }
320
+ },
321
+ "immutable_fields": {
322
+ "type": "array",
323
+ "items": { "type": "string" }
324
+ },
325
+ "references": {
326
+ "type": "object",
327
+ "additionalProperties": {
328
+ "type": "object",
329
+ "properties": {
330
+ "allowed_types": {
331
+ "type": "array",
332
+ "items": { "type": "string" }
333
+ },
334
+ "disallowed_types": {
335
+ "type": "array",
336
+ "items": { "type": "string" }
337
+ },
338
+ "allowed_portability": {
339
+ "type": "array",
340
+ "items": { "type": "string" }
341
+ },
342
+ "must_exist": {
343
+ "type": "boolean"
344
+ },
345
+ "hash_field": {
346
+ "type": "string"
347
+ }
348
+ }
349
+ }
350
+ },
351
+ "aliases": {
352
+ "type": "array",
353
+ "items": { "type": "string" }
354
+ },
355
+ "deprecated": {
356
+ "type": "boolean"
357
+ },
358
+ "deprecation_notice": {
359
+ "type": "string"
360
+ },
361
+ "resource": {
362
+ "type": "object",
363
+ "properties": {
364
+ "kind": { "type": "string" },
365
+ "provision_relation": { "type": "string" }
366
+ }
367
+ },
368
+ "scopes": {
369
+ "type": "array",
370
+ "items": { "type": "string" }
371
+ },
372
+ "spec_ref": {
373
+ "type": "string"
374
+ }
375
+ }
376
+ },
377
+ "contractPrimitive": {
378
+ "type": "object",
379
+ "required": ["purpose", "spec_ref"],
380
+ "properties": {
381
+ "purpose": { "type": "string" },
382
+ "spec_ref": { "type": "string" }
383
+ }
384
+ }
385
+ }
386
+ }
@@ -45,6 +45,7 @@ const ENGINE_ENFORCED_KEYS = [
45
45
  'immutable_fields',
46
46
  'references',
47
47
  'append_only',
48
+ 'aliases',
48
49
  ];
49
50
 
50
51
  // Keys that are real and load-bearing but are intentionally consumed
@@ -58,6 +59,8 @@ const KNOWN_NON_ENGINE_KEYS = new Set([
58
59
  'portability', // consumed by src/registry.mjs resolvePortability() + src/bundle.mjs export filtering, not write-time validation
59
60
  'resource', // extension-defined resource_bound metadata, consumed by src/bundle.mjs provisioning
60
61
  'description', // human documentation on a primitive definition, same role as `notes`; never enforced at write time
62
+ 'deprecated', // documentation / catalog deprecation status; consumed by tool displays and spec generation, not write-time validation
63
+ 'deprecation_notice', // human guidance on migration path for deprecated primitives; consumed by documentation
61
64
  // Product-side projection metadata. The SSSS engine never reads these: they are
62
65
  // consumed by the host application's own projector to decide which target tables
63
66
  // a document feeds and how (see festech.live SsssGenericProjector.ts). Kept in the
@@ -306,6 +306,7 @@ async function checkRecoveryTriggers(root) {
306
306
  const untouched = read(live, 'rules/live.md').includes('in flight');
307
307
  holder.child.kill('SIGKILL');
308
308
  const signal = await holder.exited;
309
+ await new Promise((resolve) => setTimeout(resolve, 50));
309
310
  const afterKill = await fileKernel(live).recover();
310
311
  check('recovery skips an intent whose writer is alive',
311
312
  whileAlive.length === 1 && whileAlive[0].outcome === 'in_flight' && untouched, JSON.stringify(whileAlive));
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Schema export and inspection commands for SSSS.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { parseArgs, die, wantsHelp } from './lib/cli.mjs';
8
+
9
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
10
+ const ROOT = path.resolve(HERE, '..');
11
+ const SCHEMAS_DIR = path.resolve(ROOT, 'schemas');
12
+
13
+ const HELP = `Usage: ssss schema core|bundle|export [options]
14
+
15
+ Commands:
16
+ core [--out <file>] Output the Core Registry JSON Schema.
17
+ bundle [--out <file>] Output the .ucw Bundle JSON Schema.
18
+ export [--out <dir>] Export all canonical schemas to a directory (default: ./schemas).
19
+ `;
20
+
21
+ function emit(value, output) {
22
+ const text = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
23
+ if (output) {
24
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
25
+ fs.writeFileSync(path.resolve(output), text, { encoding: 'utf8' });
26
+ } else {
27
+ process.stdout.write(text);
28
+ }
29
+ }
30
+
31
+ export async function run(argv) {
32
+ if (wantsHelp(argv)) { console.log(HELP); return; }
33
+ const verb = argv[0];
34
+ const { flags } = parseArgs(argv.slice(1));
35
+
36
+ try {
37
+ const coreSchemaPath = path.join(SCHEMAS_DIR, 'core-registry.schema.json');
38
+ const bundleSchemaPath = path.join(SCHEMAS_DIR, 'bundle.schema.json');
39
+
40
+ if (verb === 'core') {
41
+ const content = fs.readFileSync(coreSchemaPath, 'utf8');
42
+ emit(content, flags.out);
43
+ return;
44
+ }
45
+ if (verb === 'bundle') {
46
+ const content = fs.readFileSync(bundleSchemaPath, 'utf8');
47
+ emit(content, flags.out);
48
+ return;
49
+ }
50
+ if (verb === 'export') {
51
+ const targetDir = path.resolve(flags.out || './schemas');
52
+ fs.mkdirSync(targetDir, { recursive: true });
53
+ fs.copyFileSync(coreSchemaPath, path.join(targetDir, 'core-registry.schema.json'));
54
+ fs.copyFileSync(bundleSchemaPath, path.join(targetDir, 'bundle.schema.json'));
55
+ console.log(`Exported canonical schemas to ${targetDir}`);
56
+ return;
57
+ }
58
+ die(`Unknown schema verb '${verb || ''}'.\n${HELP}`);
59
+ } catch (error) {
60
+ die(error.message);
61
+ }
62
+ }
@@ -33,6 +33,7 @@ import { MemoryIdempotencyStore, FileIdempotencyStore, runIdempotencyContract }
33
33
  import { capabilityCovers, createCapabilityAuthorizer } from '../src/authorization.mjs';
34
34
  import { createEngine } from '../src/engine.mjs';
35
35
  import { validateVerifiedPrincipal } from '../src/authorization.mjs';
36
+ import { validateJsonSchema } from '../src/schema-validator.mjs';
36
37
 
37
38
  async function rejects(promise, pattern) {
38
39
  try { await promise; return false; } catch (error) { return pattern.test(error.message); }
@@ -319,11 +320,109 @@ async function checkCapabilities(root, check) {
319
320
  await write('lr-1', 'roles/editor/ROLE.md', role('editor', ['"write:rule"']), 'system');
320
321
  await write('lr-2', 'roles/owner/ROLE.md', role('owner', ['"write:*"']), 'system');
321
322
  await write('lr-3', 'roles/typed/ROLE.md', role('typed', ['"*:assistant"']), 'system');
323
+ await write('lr-rw', 'roles/rolewriter/ROLE.md', role('rolewriter', ['"write:role"']), 'system');
322
324
  check('legacy roles: write:<type> grants writes to that type', (await write('lr-4', 'rules/a.md', doc('rule', 'A'), 'editor')).success);
323
325
  check('legacy roles: write:<type> denies other types', (await write('lr-5', 'assistants/b/ASSISTANT.md', doc('assistant', 'B'), 'editor')).error?.code === 'forbidden');
324
326
  check('legacy roles: *:<type> grants writes to that type', (await write('lr-6', 'assistants/c/ASSISTANT.md', doc('assistant', 'C'), 'typed')).success);
325
327
  check('legacy roles: write:* grants writes to every type', (await write('lr-7', 'workflows/d/WORKFLOW.md', doc('workflow', 'D'), 'owner')).success);
326
328
  check('legacy roles: an unknown role is forbidden, not promoted', (await write('lr-8', 'rules/e.md', doc('rule', 'E'), 'ghost')).error?.code === 'forbidden');
329
+ check('legacy roles: write:role grants writes to security_role (alias unification)',
330
+ (await write('lr-aliased-1', 'roles/sec/ROLE.md', role('sec', ['"write:rule"']), 'rolewriter')).success);
331
+
332
+ const roleDoc = (name, permissions) => `---\ntype: role\ntitle: "${name}"\ndescription: "Canonical role."\ntimestamp: "2026-09-24T00:00:00Z"\nname: "${name}"\npermissions: [${permissions.join(', ')}]\n---\n`;
333
+ const replaceRes = await write('lr-aliased-replace', 'roles/sec/ROLE.md', roleDoc('sec', ['"write:rule"', '"write:task"']), 'rolewriter');
334
+ check('kernel mutation: replace security_role with role succeeds (alias unification)', replaceRes.success);
335
+
336
+ const illegalTypeChange = await write('lr-illegal-type', 'roles/sec/ROLE.md', doc('assistant', 'SecAsst'), 'owner');
337
+ check('kernel mutation: replace role with assistant fails closed',
338
+ illegalTypeChange.error?.code === 'validation_failed' &&
339
+ illegalTypeChange.error?.message?.includes('Operation may not change an existing document type.'));
340
+
341
+ const convDoc = `---\ntype: conversation\ntitle: "Thread"\ndescription: "Chat transcript."\ntimestamp: "2026-09-24T00:00:00Z"\nthread_id: "t-1"\n---\n\nInitial message.\n`;
342
+ await engine.processOperation({ type: 'operation', workspace_id: 'w', idempotency_key: 'conv-init', path: 'threads/t1/CONVERSATION.md', content: convDoc, actor: { role: 'system' } }, vault);
343
+ const patchRes = await engine.processOperation({
344
+ type: 'patch', workspace_id: 'w', idempotency_key: 'conv-patch-alias', path: 'threads/t1/CONVERSATION.md',
345
+ patches: { type: 'thread', __body__: 'New reply.' },
346
+ actor: { role: 'system' },
347
+ }, vault);
348
+ check('kernel mutation: patch conversation with type thread succeeds (alias unification)', patchRes.success);
349
+
350
+ const regTypes = engine._registry.types;
351
+ check('core registry: release, model, page, conflict declare deprecated: true',
352
+ regTypes.get('release')?.deprecated === true &&
353
+ regTypes.get('model')?.deprecated === true &&
354
+ regTypes.get('page')?.deprecated === true &&
355
+ regTypes.get('conflict')?.deprecated === true);
356
+
357
+ let selfAliasRejected = false;
358
+ try {
359
+ composeRegistries({
360
+ core: {
361
+ registry: 'ssss',
362
+ version: '0.14.0',
363
+ universal_fields: ['type'],
364
+ portability: { classes: { structural: {} } },
365
+ document_primitives: {
366
+ rule: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'], aliases: ['rule'] },
367
+ },
368
+ },
369
+ extensions: [],
370
+ });
371
+ } catch (err) {
372
+ selfAliasRejected = err.message.includes('cannot alias itself');
373
+ }
374
+ check('core registry: self-alias on a primitive is rejected', selfAliasRejected);
375
+
376
+ let asymmetricAliasRejected = false;
377
+ try {
378
+ composeRegistries({
379
+ core: {
380
+ registry: 'ssss',
381
+ version: '0.14.0',
382
+ universal_fields: ['type'],
383
+ portability: { classes: { structural: {} } },
384
+ document_primitives: {
385
+ rule: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'], aliases: ['assistant'] },
386
+ assistant: { family: 'core', append_only: false, portability: 'structural', required_fields: ['type'] },
387
+ },
388
+ },
389
+ extensions: [],
390
+ });
391
+ } catch (err) {
392
+ asymmetricAliasRejected = err.message.includes('collides with existing primitive');
393
+ }
394
+ check('core registry: non-mutual core alias collision is rejected', asymmetricAliasRejected);
395
+
396
+ const floorAuthorizer = createCapabilityAuthorizer({
397
+ policyFloors: { 'ssss:role': { create: ['governance:elevated'] } },
398
+ });
399
+ const secRoleDef = regTypes.get('security_role');
400
+ const floorBlocked = await floorAuthorizer({
401
+ principal: {
402
+ id: 'u1',
403
+ kind: 'agent',
404
+ workspaceIds: ['w'],
405
+ capabilities: ['ssss:security_role:create'],
406
+ authentication: { provider: 'token', assurance: 'verified' },
407
+ },
408
+ workspaceId: 'w',
409
+ definition: secRoleDef,
410
+ action: 'create',
411
+ });
412
+ const floorAllowed = await floorAuthorizer({
413
+ principal: {
414
+ id: 'u1',
415
+ kind: 'agent',
416
+ workspaceIds: ['w'],
417
+ capabilities: ['ssss:security_role:create', 'governance:elevated'],
418
+ authentication: { provider: 'token', assurance: 'verified' },
419
+ },
420
+ workspaceId: 'w',
421
+ definition: secRoleDef,
422
+ action: 'create',
423
+ });
424
+ check('authorization: policyFloors on ssss:role apply when mutating security_role alias',
425
+ floorBlocked.allowed === false && floorBlocked.missing?.includes('governance:elevated') && floorAllowed.allowed === true);
327
426
 
328
427
  const pathPrincipal = {
329
428
  id: 'scoped-user',
@@ -340,6 +439,32 @@ async function checkCapabilities(root, check) {
340
439
  (await writeScoped('ps-2', 'rules/engineering/lint.md', doc('rule', 'Lint'))).error?.code === 'forbidden');
341
440
  }
342
441
 
442
+ function checkJsonSchemas(check) {
443
+ const coreRegistry = JSON.parse(fs.readFileSync(path.resolve('registry/core.json'), 'utf8'));
444
+ const coreSchema = JSON.parse(fs.readFileSync(path.resolve('schemas/core-registry.schema.json'), 'utf8'));
445
+ const coreResult = validateJsonSchema(coreRegistry, coreSchema);
446
+ check('schema: registry/core.json validates against schemas/core-registry.schema.json', coreResult.valid, coreResult.errors?.[0]?.message);
447
+
448
+ const bundle = JSON.parse(fs.readFileSync(path.resolve('conformance/reference-bundle.ucw.json'), 'utf8'));
449
+ const bundleSchema = JSON.parse(fs.readFileSync(path.resolve('schemas/bundle.schema.json'), 'utf8'));
450
+ const bundleResult = validateJsonSchema(bundle, bundleSchema);
451
+ check('schema: reference-bundle.ucw.json validates against schemas/bundle.schema.json', bundleResult.valid, bundleResult.errors?.[0]?.message);
452
+
453
+ const badCore = { ...coreRegistry };
454
+ delete badCore.spec_version;
455
+ const badCoreResult = validateJsonSchema(badCore, coreSchema);
456
+ check('schema: core registry schema rejects missing spec_version', !badCoreResult.valid && badCoreResult.errors.some((e) => e.path.includes('spec_version')));
457
+
458
+ const badPortability = JSON.parse(JSON.stringify(coreRegistry));
459
+ badPortability.document_primitives.memory.portability = 'illegal_class';
460
+ const badPortResult = validateJsonSchema(badPortability, coreSchema);
461
+ check('schema: core registry schema rejects invalid portability enum', !badPortResult.valid && badPortResult.errors.some((e) => e.path.includes('portability')));
462
+
463
+ const badBundle = { files: [] };
464
+ const badBundleResult = validateJsonSchema(badBundle, bundleSchema);
465
+ check('schema: bundle schema rejects missing manifest', !badBundleResult.valid && badBundleResult.errors.some((e) => e.path.includes('manifest')));
466
+ }
467
+
343
468
  // Resource hooks (spec §6.3): every prepare ends in exactly one finalize or
344
469
  // reconcile, and nothing after the event append may undo the commit.
345
470
  async function checkResourceLifecycle(check) {
@@ -432,6 +557,27 @@ async function checkResourceLifecycle(check) {
432
557
  check('references: a reference to a disallowed type is rejected', wrongType.error?.code === 'validation_failed');
433
558
  check('references: a patch cannot repoint a reference at a missing document', repointed.error?.code === 'validation_failed');
434
559
 
560
+ const withRoleRefs = createValidator({ extensions: [{
561
+ registry: 'acme', extends: 'core',
562
+ document_primitives: {
563
+ grant_doc: {
564
+ family: 'extension', append_only: false, portability: 'structural', required_fields: ['type', 'name', 'role_ref'],
565
+ references: { role_ref: { allowed_types: ['role'] } },
566
+ },
567
+ deny_role_doc: {
568
+ family: 'extension', append_only: false, portability: 'structural', required_fields: ['type', 'name', 'target_ref'],
569
+ references: { target_ref: { disallowed_types: ['role'] } },
570
+ },
571
+ },
572
+ }] });
573
+ const roleRefKernel = createKernel({ vfs: new MemoryVfs(), eventStore: new MemoryEventStore(), validator: withRoleRefs });
574
+ const writeRef = (key, pathName, content) => roleRefKernel.execute({ type: 'operation', workspace_id: 'w', idempotency_key: key, path: pathName, content }, { principal });
575
+ await writeRef('rr-1', 'roles/admin.md', `---\ntype: security_role\ntitle: "Admin"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nrole_id: "admin"\nname: "Admin"\npermissions: ["*:*"]\n---\n`);
576
+ const allowedViaAlias = await writeRef('rr-2', 'grants/g1.md', `---\ntype: grant_doc\ntitle: "G"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nname: "G"\nrole_ref: "roles/admin.md"\n---\n`);
577
+ const disallowedViaAlias = await writeRef('rr-3', 'denies/d1.md', `---\ntype: deny_role_doc\ntitle: "D"\ndescription: "D"\ntimestamp: "2026-09-24T00:00:00Z"\nname: "D"\ntarget_ref: "roles/admin.md"\n---\n`);
578
+ check('references: allowed_types role accepts aliased security_role target', allowedViaAlias.success);
579
+ check('references: disallowed_types role rejects aliased security_role target', disallowedViaAlias.error?.code === 'validation_failed');
580
+
435
581
  // Every failure carries a symbolic code that maps to the §6.5 status table.
436
582
  const plain = createKernel({ vfs: new MemoryVfs(), eventStore: new MemoryEventStore(), leaseStore: new MemoryLeaseStore() });
437
583
  const missing = await plain.execute({ type: 'patch', workspace_id: 'w', idempotency_key: 'ec-404', path: 'rules/none.md', patches: { name: 'x' } }, { principal });
@@ -666,6 +812,7 @@ export async function runKernel09Conformance() {
666
812
  checkFrontmatterSubset(check);
667
813
  checkRequiredPresence(check);
668
814
  await checkCapabilities(temp, check);
815
+ checkJsonSchemas(check);
669
816
  } finally {
670
817
  fs.rmSync(temp, { recursive: true, force: true });
671
818
  }
package/scripts/ssss.mjs CHANGED
@@ -41,6 +41,7 @@ Authoring, wire protocol & conformance:
41
41
  bridge [options] Execute operations over stdin/stdout JSON (spec §6.8).
42
42
  primitive <verb> Create, validate, inspect, migrate, or emit types.
43
43
  registry <verb> Compose, lock, or verify an effective registry.
44
+ schema <verb> Output or export canonical JSON Schemas (core, bundle).
44
45
  adapter conformance Run machine-readable adapter contract suites.
45
46
  migrate 0.8-to-0.9 Diagnose and plan a non-destructive 0.9 migration.
46
47
  autolink [dir] Generate OKF wiki links across a vault.
@@ -72,6 +73,7 @@ const MODULES = {
72
73
  semantic: './cmd-semantic.mjs',
73
74
  primitive: './cmd-primitive.mjs',
74
75
  registry: './cmd-registry.mjs',
76
+ schema: './cmd-schema.mjs',
75
77
  adapter: './cmd-adapter.mjs',
76
78
  migrate: './cmd-migrate.mjs',
77
79
  serve: './cmd-serve.mjs',