@gregiteen/ssss-cli 0.14.1 → 0.14.3

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.
@@ -28,6 +28,7 @@ const HELP = usage({
28
28
  usage: 'ssss new <dir> [options]',
29
29
  options: [
30
30
  ['--name <s>', 'Package name (default: the target directory name).'],
31
+ ['--extension <s>', 'Scaffold a custom app extension registry under registry/extensions/<name>.json.'],
31
32
  ['--with-total-recall', 'Also wire the Total Recall memory OS (total-recall-brain).'],
32
33
  ['--install', 'Run `npm install` after scaffolding (and Total Recall init if requested).'],
33
34
  ['--no-git', 'Do not run `git init`.'],
@@ -36,6 +37,7 @@ const HELP = usage({
36
37
  ],
37
38
  examples: [
38
39
  'ssss new my-app',
40
+ 'ssss new my-app --extension myapp',
39
41
  'ssss new my-app --with-total-recall --install',
40
42
  ],
41
43
  seeAlso: ['ssss help scaffold', 'ssss export', 'ssss conformance'],
@@ -65,6 +67,15 @@ export async function run(argv) {
65
67
  const name = flags.name || path.basename(root);
66
68
  const ref = flags.ref || `v${VERSION}`;
67
69
  const withTR = !!flags['with-total-recall'];
70
+ const extensionName = flags.extension === undefined ? null : String(flags.extension);
71
+ if (extensionName !== null) {
72
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(extensionName)) {
73
+ die(`--extension '${extensionName}' is not a valid registry name (lowercase letter first, then a-z, 0-9, _ or -; max 64).`);
74
+ }
75
+ if (extensionName === 'core' || extensionName === 'ssss') {
76
+ die(`--extension '${extensionName}' is reserved for the SSSS core registry.`);
77
+ }
78
+ }
68
79
 
69
80
  if (fs.existsSync(root) && fs.readdirSync(root).length && !flags.force) {
70
81
  die(`${root} is not empty. Use --force to scaffold anyway.`);
@@ -79,7 +90,9 @@ export async function run(argv) {
79
90
  ssss: 'ssss',
80
91
  test: 'node --test',
81
92
  export: 'ssss export vault --profile sale --out dist/bundle.ucw.json',
82
- validate: 'ssss conformance --engine',
93
+ validate: extensionName
94
+ ? `ssss schema validate registry/extensions/${extensionName}.json && ssss conformance --engine`
95
+ : 'ssss conformance --engine',
83
96
  },
84
97
  dependencies: { '@gregiteen/ssss-cli': `github:gregiteen/ssss#${ref}` },
85
98
  ...(withTR ? { devDependencies: { 'total-recall-brain': 'latest' } } : {}),
@@ -96,14 +109,42 @@ export async function run(argv) {
96
109
  W(root, 'vault/tasks/first-task.md',
97
110
  '---\ntype: task\ntitle: First Task\ndescription: Starter private setup task that demonstrates tenant_private filtering.\ntimestamp: 2026-07-02T00:00:00Z\npriority: normal\ncategory: setup\nstatus: pending\n---\n\n# First task\n\nThis is `tenant_private` (§5.5) — it is **dropped** from `template`/`sale` exports.\n');
98
111
 
112
+ // ── optional app extension registry ─────────────────────────────────────
113
+ if (extensionName) {
114
+ const extRegistry = {
115
+ $schema: 'https://ssss.dev/schemas/core-registry.schema.json',
116
+ registry: extensionName,
117
+ extends: 'core',
118
+ version: '0.1.0',
119
+ description: `Custom SSSS primitive extensions for ${name}.`,
120
+ document_primitives: {
121
+ [`${extensionName}_item`]: {
122
+ family: 'content',
123
+ canonical_filename: null,
124
+ append_only: false,
125
+ portability: 'structural',
126
+ required_fields: ['type', 'name', 'title'],
127
+ optional_fields: ['description', 'status'],
128
+ enums: {
129
+ status: ['active', 'archived', 'draft'],
130
+ },
131
+ notes: `Custom primitive for ${name} application.`,
132
+ },
133
+ },
134
+ };
135
+ W(root, `registry/extensions/${extensionName}.json`, JSON.stringify(extRegistry, null, 2) + '\n');
136
+ W(root, `vault/${extensionName}_items/first-${extensionName}-item.md`,
137
+ `---\ntype: ${extensionName}_item\ntitle: Starter Item\ndescription: Starter ${extensionName} item.\ntimestamp: 2026-07-02T00:00:00Z\nname: Starter Item\nstatus: active\n---\n\n# Starter Item\n\nCustom primitive defined in \`registry/extensions/${extensionName}.json\`.\n`);
138
+ }
139
+
99
140
  // ── conformance test (zero-dependency: node --test) ─────────────────────
100
- W(root, 'test/ssss-conformance.test.mjs', CONFORMANCE_TEST);
141
+ W(root, 'test/ssss-conformance.test.mjs', conformanceTest(extensionName));
101
142
 
102
143
  // ── repo meta ───────────────────────────────────────────────────────────
103
144
  W(root, '.gitignore', 'node_modules/\ndist/\n*.ucw.json\n.DS_Store\n');
104
- W(root, 'README.md', readme(name, withTR));
105
- W(root, 'CLAUDE.md', claudeMd(name, withTR));
106
- W(root, 'docs/design.md', designMd(name));
145
+ W(root, 'README.md', readme(name, withTR, extensionName));
146
+ W(root, 'CLAUDE.md', claudeMd(name, withTR, extensionName));
147
+ W(root, 'docs/design.md', designMd(name, extensionName));
107
148
 
108
149
  // ── git + install (best-effort, flag-gated, no shell) ───────────────────
109
150
  const log = (m) => console.error(m);
@@ -145,7 +186,8 @@ export async function run(argv) {
145
186
  console.log(' npx ssss help # explore the standard');
146
187
  }
147
188
 
148
- const CONFORMANCE_TEST = `/**
189
+ function conformanceTest(extensionName) {
190
+ return `/**
149
191
  * SSSS conformance — proves this repo's toolchain matches the published standard.
150
192
  * Zero test dependencies: run with \`node --test\` (Node 18+).
151
193
  */
@@ -161,7 +203,18 @@ import { exportBundle, validateBundle, provisionBundle, importBundle } from '@gr
161
203
 
162
204
  const require = createRequire(import.meta.url);
163
205
  const { fixtures } = require('@gregiteen/ssss-cli/conformance/fixtures.json');
164
- const VAULT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'vault');
206
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
207
+ const VAULT = path.join(ROOT, 'vault');
208
+ ${extensionName
209
+ ? `// App-specific primitives live in this repo's registry/extensions/; the core comes from the package.
210
+ const REGISTRY = path.join(ROOT, 'registry');
211
+ const EXTENSIONS = [${JSON.stringify(extensionName)}];
212
+ const EXPORT_OPTIONS = { registryDir: REGISTRY, requiredExtensions: EXTENSIONS };
213
+ const REGISTRY_OPTIONS = { registryDir: REGISTRY };
214
+ const ENGINE_OPTIONS = { registryDir: REGISTRY, registryNames: EXTENSIONS };`
215
+ : `const EXPORT_OPTIONS = {};
216
+ const REGISTRY_OPTIONS = {};
217
+ const ENGINE_OPTIONS = {};`}
165
218
 
166
219
  test('engine implements the canonical Operation Contract (spec §6)', async () => {
167
220
  const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-conf-'));
@@ -179,17 +232,17 @@ test('engine implements the canonical Operation Contract (spec §6)', async () =
179
232
  });
180
233
 
181
234
  test('this vault exports, validates, and round-trips as a sale bundle (spec §16/§17)', async () => {
182
- const bundle = exportBundle(VAULT, { profile: 'sale', name: 'starter' });
183
- const { valid, errors } = validateBundle(bundle);
235
+ const bundle = exportBundle(VAULT, { profile: 'sale', name: 'starter', ...EXPORT_OPTIONS });
236
+ const { valid, errors } = validateBundle(bundle, REGISTRY_OPTIONS);
184
237
  assert.ok(valid, 'bundle invalid: ' + errors.join('; '));
185
238
  assert.ok(!bundle.files.some((f) => f.path.startsWith('tasks/')), 'tenant_private leaked into sale export');
186
239
 
187
- const plan = provisionBundle(bundle, { workspaceId: 'ws-test' });
188
- assert.ok(plan.ok, 'provision failed: ' + JSON.stringify(plan.unresolved) + ' ' + JSON.stringify(plan.danglingLinks));
240
+ const plan = provisionBundle(bundle, { workspaceId: 'ws-test', ...REGISTRY_OPTIONS });
241
+ assert.ok(plan.ok, 'provision failed: ' + JSON.stringify(plan.errors) + ' ' + JSON.stringify(plan.unresolved) + ' ' + JSON.stringify(plan.danglingLinks));
189
242
 
190
243
  const target = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-import-'));
191
244
  try {
192
- const engine = createEngine();
245
+ const engine = createEngine(ENGINE_OPTIONS);
193
246
  const r1 = await importBundle(plan.plan, target, engine);
194
247
  assert.ok(r1.ok && r1.committed === bundle.files.length, 'import did not commit every file');
195
248
  const r2 = await importBundle(plan.plan, target, engine);
@@ -197,8 +250,9 @@ test('this vault exports, validates, and round-trips as a sale bundle (spec §16
197
250
  } finally { fs.rmSync(target, { recursive: true, force: true }); }
198
251
  });
199
252
  `;
253
+ }
200
254
 
201
- function readme(name, withTR) {
255
+ function readme(name, withTR, extensionName) {
202
256
  return `# ${name}
203
257
 
204
258
  An [SSSS](https://github.com/gregiteen/ssss)-native project: a Markdown **vault** is
@@ -208,7 +262,7 @@ the source of truth, validated and packaged by the dependency-free \`@gregiteen/
208
262
 
209
263
  \`\`\`
210
264
  vault/ Your SSSS documents (structural + tenant_private primitives).
211
- test/ Conformance test — replays the canonical fixtures + round-trips the vault.
265
+ ${extensionName ? `registry/ Custom primitive extension schema (registry/extensions/${extensionName}.json).\n` : ''}test/ Conformance test — replays the canonical fixtures + round-trips the vault.
212
266
  \`\`\`
213
267
 
214
268
  ## Use
@@ -236,18 +290,23 @@ npx total-recall recall "..." # semantic recall
236
290
  ` : ''}`;
237
291
  }
238
292
 
239
- function claudeMd(name, withTR) {
293
+ function claudeMd(name, withTR, extensionName) {
240
294
  return `# ${name} — agent guide
241
295
 
242
296
  This repo uses **SSSS** (Structured Semantic Syntax System) as its state contract.
243
297
 
244
298
  ## Source of truth
245
299
  - \`vault/\` is the only source of truth — typed Markdown documents with YAML
246
- frontmatter. Every file declares a \`type\` defined in the \`@gregiteen/ssss-cli\` core registry.
300
+ frontmatter. Every file declares a \`type\` defined in the \`@gregiteen/ssss-cli\` core registry${extensionName ? ` or local \`registry/extensions/${extensionName}.json\`` : ''}.
247
301
  - Mutate the vault through the standard, not by hand-writing files in bulk:
248
302
  \`npx ssss\` for bundle/tenant tooling, or the \`@gregiteen/ssss-cli/engine\` Operation Contract
249
303
  in code.
250
304
 
305
+ ## Extension & Registry Isolation Invariant
306
+ - App-specific primitives belong exclusively in \`registry/extensions/\` inside this application repository.
307
+ - Never modify or fork \`@gregiteen/ssss-cli\` to add app-specific schemas.
308
+ - Validate local extension schemas with: \`npx ssss schema validate registry/extensions/<name>.json\`.
309
+
251
310
  ## Portability (§5.5) — the rule that matters
252
311
  - \`structural\` documents (rules, workflows, pages, assistants) are the sellable model.
253
312
  - \`tenant_private\` documents (tasks, customer data) are **never** shipped in a
@@ -264,7 +323,7 @@ ${withTR ? `
264
323
  ` : ''}`;
265
324
  }
266
325
 
267
- function designMd(name) {
326
+ function designMd(name, extensionName) {
268
327
  return `# ${name} — System Architecture & Design
269
328
 
270
329
  > Technical design and architecture specification for ${name}.
@@ -278,7 +337,7 @@ function designMd(name) {
278
337
  - **Vault Structure**: Typed Markdown documents under \`vault/\` serving as the source of truth.
279
338
  - **Workflows & Execution**: Automated procedures defined in \`vault/workflows/\` triggered by operational events.
280
339
  - **Capabilities & Resources**: Tools and external service integrations required for execution.
281
-
340
+ ${extensionName ? `- **Extension Registry**: Custom primitive definitions located in \`registry/extensions/${extensionName}.json\` extending core SSSS.\n` : ''}
282
341
  ## Data & Schema Contracts
283
342
 
284
343
  <Core entities, external dependencies, and integration interfaces.>
@@ -287,5 +346,7 @@ function designMd(name) {
287
346
 
288
347
  - SSSS VFS is the single source of truth for application state.
289
348
  - Structural business logic is strictly decoupled from private tenant operational data.
349
+ - App-specific primitives reside in \`registry/extensions/\` within this repository and extend \`core\`.
350
+ - Core SSSS library (\`@gregiteen/ssss-cli\`) remains vendor-neutral and dependency-free.
290
351
  `;
291
352
  }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * `ssss schema` — inspect, export, and validate against the canonical Draft 2020-12
3
+ * JSON Schemas for SSSS registries (`schemas/core-registry.schema.json`) and
4
+ * `.ucw` transport bundles (`schemas/bundle.schema.json`).
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import {
10
+ getCoreRegistrySchema,
11
+ getBundleSchema,
12
+ validateJsonSchema,
13
+ } from '../src/schema-validator.mjs';
14
+ import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
17
+ const ROOT = path.resolve(HERE, '..');
18
+ const SCHEMAS_DIR = path.resolve(ROOT, 'schemas');
19
+
20
+ const HELP = usage({
21
+ summary: 'Output, export, or validate against the canonical SSSS Draft 2020-12 JSON Schemas.',
22
+ usage: 'ssss schema <core|bundle|export|validate> [target] [options]',
23
+ options: [
24
+ ['--out <path>', 'Destination file (core/bundle) or directory (export; default: ./schemas).'],
25
+ ['--type <core|bundle>', 'Target schema kind for `validate` (auto-detected when omitted).'],
26
+ ['--schema <file>', 'Custom JSON Schema file path for `validate`.'],
27
+ ['--json', 'Emit machine-readable JSON output for `export` or `validate`.'],
28
+ ['--quiet', 'Suppress human-readable output on valid `validate` check.'],
29
+ ],
30
+ examples: [
31
+ 'ssss schema core',
32
+ 'ssss schema bundle --out ./bundle.schema.json',
33
+ 'ssss schema export --out ./schemas',
34
+ 'ssss schema validate registry/core.json',
35
+ 'ssss schema validate conformance/reference-bundle.ucw.json --type bundle',
36
+ ],
37
+ seeAlso: ['ssss registry', 'ssss validate', 'ssss help schema'],
38
+ });
39
+
40
+ function emit(value, output) {
41
+ const text = typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`;
42
+ if (output) {
43
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
44
+ fs.writeFileSync(path.resolve(output), text, { encoding: 'utf8' });
45
+ } else {
46
+ process.stdout.write(text);
47
+ }
48
+ }
49
+
50
+ function detectSchemaType(data, explicitType) {
51
+ if (explicitType === 'core' || explicitType === 'registry') return 'core';
52
+ if (explicitType === 'bundle') return 'bundle';
53
+ if (explicitType) {
54
+ die(`Unknown --type '${explicitType}'. Use 'core' or 'bundle'.`);
55
+ }
56
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
57
+ if (typeof data.$schema === 'string') {
58
+ if (data.$schema.includes('bundle.schema.json')) return 'bundle';
59
+ if (data.$schema.includes('core-registry.schema.json') || data.$schema.includes('registry')) return 'core';
60
+ }
61
+ if ('manifest' in data || 'files' in data) return 'bundle';
62
+ if ('registry' in data || 'document_primitives' in data) return 'core';
63
+ }
64
+ return 'core';
65
+ }
66
+
67
+ export async function run(argv) {
68
+ if (wantsHelp(argv)) { console.log(HELP); return; }
69
+ const verb = argv[0];
70
+ const { positionals, flags } = parseArgs(argv.slice(1), {
71
+ booleans: ['json', 'quiet'],
72
+ alias: { j: 'json', q: 'quiet' },
73
+ });
74
+
75
+ try {
76
+ const coreSchemaPath = path.join(SCHEMAS_DIR, 'core-registry.schema.json');
77
+ const bundleSchemaPath = path.join(SCHEMAS_DIR, 'bundle.schema.json');
78
+
79
+ if (verb === 'core') {
80
+ const content = fs.readFileSync(coreSchemaPath, 'utf8');
81
+ emit(content, flags.out);
82
+ return;
83
+ }
84
+ if (verb === 'bundle') {
85
+ const content = fs.readFileSync(bundleSchemaPath, 'utf8');
86
+ emit(content, flags.out);
87
+ return;
88
+ }
89
+ if (verb === 'export') {
90
+ const targetDir = path.resolve(flags.out || './schemas');
91
+ fs.mkdirSync(targetDir, { recursive: true });
92
+ const written = [
93
+ path.join(targetDir, 'core-registry.schema.json'),
94
+ path.join(targetDir, 'bundle.schema.json'),
95
+ ];
96
+ fs.copyFileSync(coreSchemaPath, written[0]);
97
+ fs.copyFileSync(bundleSchemaPath, written[1]);
98
+ if (flags.json) {
99
+ console.log(JSON.stringify({ exported: written, directory: targetDir }, null, 2));
100
+ } else {
101
+ console.log(`Exported canonical schemas to ${targetDir}`);
102
+ }
103
+ return;
104
+ }
105
+ if (verb === 'validate') {
106
+ const file = positionals[0];
107
+ if (!file) die(`validate requires a target JSON file.\n\n${HELP}`);
108
+ const resolvedFile = path.resolve(file);
109
+ if (!fs.existsSync(resolvedFile)) die(`no such file: ${file}`);
110
+ let data;
111
+ try {
112
+ data = JSON.parse(fs.readFileSync(resolvedFile, 'utf8'));
113
+ } catch (err) {
114
+ die(`not valid JSON (${file}): ${err.message}`);
115
+ }
116
+
117
+ let schema;
118
+ let schemaLabel;
119
+ if (flags.schema) {
120
+ const customSchemaPath = path.resolve(flags.schema);
121
+ if (!fs.existsSync(customSchemaPath)) die(`no such schema file: ${flags.schema}`);
122
+ schema = JSON.parse(fs.readFileSync(customSchemaPath, 'utf8'));
123
+ schemaLabel = flags.schema;
124
+ } else {
125
+ const kind = detectSchemaType(data, flags.type);
126
+ schema = kind === 'bundle' ? getBundleSchema() : getCoreRegistrySchema();
127
+ schemaLabel = kind;
128
+ }
129
+
130
+ const { valid, errors } = validateJsonSchema(data, schema);
131
+ if (flags.json) {
132
+ console.log(JSON.stringify({ valid, schema: schemaLabel, file, errors }, null, 2));
133
+ if (!valid) process.exit(1);
134
+ return;
135
+ }
136
+ if (valid) {
137
+ if (!flags.quiet) {
138
+ console.log(`✓ valid — ${file} (${schemaLabel} schema)`);
139
+ }
140
+ return;
141
+ }
142
+ console.error(`✗ invalid — ${errors.length} error(s) in ${file}:`);
143
+ for (const e of errors) {
144
+ console.error(` - ${e.path ? `${e.path}: ` : ''}${e.message}`);
145
+ }
146
+ process.exit(1);
147
+ }
148
+ die(`Unknown schema verb '${verb || ''}'.\n\n${HELP}`);
149
+ } catch (error) {
150
+ die(error.message);
151
+ }
152
+ }
@@ -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); }
@@ -438,6 +439,45 @@ async function checkCapabilities(root, check) {
438
439
  (await writeScoped('ps-2', 'rules/engineering/lint.md', doc('rule', 'Lint'))).error?.code === 'forbidden');
439
440
  }
440
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
+ // Extension registries need no spec_version/description (those are core-only, A-009).
468
+ const extDef = definePrimitive({ namespace: 'acme', name: 'Widget', fields: [{ id: 'title', name: 'Title', kind: 'string', required: true }] });
469
+ const extResult = validateJsonSchema(definitionsToExtensionRegistry('acme', [extDef]), coreSchema);
470
+ check('schema: definitionsToExtensionRegistry() output validates against the registry schema', extResult.valid, extResult.errors?.[0] && `${extResult.errors[0].path}: ${extResult.errors[0].message}`);
471
+
472
+ const strayKey = validateJsonSchema({ ...coreRegistry, unexpected_root_key: true }, coreSchema);
473
+ check('schema: registry schema rejects unknown root keys (fail-closed)', !strayKey.valid && strayKey.errors.some((e) => e.path === 'unexpected_root_key'));
474
+
475
+ const dupFields = JSON.parse(JSON.stringify(coreRegistry));
476
+ dupFields.document_primitives.rule.required_fields = ['type', 'type'];
477
+ const dupResult = validateJsonSchema(dupFields, coreSchema);
478
+ check('schema: registry schema rejects duplicate required_fields (uniqueItems)', !dupResult.valid && dupResult.errors.some((e) => e.message.includes('uniqueItems')));
479
+ }
480
+
441
481
  // Resource hooks (spec §6.3): every prepare ends in exactly one finalize or
442
482
  // reconcile, and nothing after the event append may undo the commit.
443
483
  async function checkResourceLifecycle(check) {
@@ -785,6 +825,7 @@ export async function runKernel09Conformance() {
785
825
  checkFrontmatterSubset(check);
786
826
  checkRequiredPresence(check);
787
827
  await checkCapabilities(temp, check);
828
+ checkJsonSchemas(check);
788
829
  } finally {
789
830
  fs.rmSync(temp, { recursive: true, force: true });
790
831
  }
@@ -430,13 +430,13 @@ async function runBundleConformance() {
430
430
  // D4: Resource bindings emitted in result
431
431
  check('provisionBundle emits resource bindings with relations and modes (D4)',
432
432
  provAlpha.bindings.length > 0 &&
433
- provAlpha.bindings.some((b) => b.type === 'domain' && b.value === 'alpha.example' && b.relation === 'owns_domain'));
433
+ provAlpha.bindings.some((b) => (b.type === 'resource' || b.type === 'domain') && b.value === 'alpha.example'));
434
434
 
435
435
  // D4: Unbound required resource fails when not dryRun
436
436
  const unboundBundle = JSON.parse(JSON.stringify(bundle));
437
437
  const provUnbound = provisionBundle(unboundBundle, {
438
438
  workspaceId: 'ws-unbound',
439
- parameters: { business_name: 'Unbound Fest' }, // domain omitted!
439
+ parameters: { business_name: 'Unbound Fest' }, // domain / resource binding omitted!
440
440
  dryRun: false,
441
441
  });
442
442
  check('unbound required resource fails provisioning when dryRun is false (D4)',
@@ -1135,6 +1135,44 @@ async function runCliSmokeConformance() {
1135
1135
  }
1136
1136
  check('ssss new starter vault files validate against the registry', files.length > 0 && failures.length === 0, JSON.stringify(failures));
1137
1137
 
1138
+ // A-008: an app-local extension scaffold must pass its own generated test.
1139
+ const extApp = path.join(tmp, 'ext-app');
1140
+ execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', extApp, '--extension', 'acme', '--no-git'], { stdio: 'ignore' });
1141
+ fs.mkdirSync(path.join(extApp, 'node_modules', '@gregiteen'), { recursive: true });
1142
+ fs.symlinkSync(path.resolve(__dirname, '..'), path.join(extApp, 'node_modules', '@gregiteen', 'ssss-cli'), 'dir');
1143
+ let extTestOk = true;
1144
+ let extTestDetail = '';
1145
+ try { execFileSync('node', ['--test'], { cwd: extApp, stdio: 'pipe' }); }
1146
+ catch (err) { extTestOk = false; extTestDetail = String(err.stdout || err.message).slice(-600); }
1147
+ check('ssss new --extension scaffolds an app whose own node --test passes', extTestOk, extTestDetail);
1148
+
1149
+ const badExt = (() => {
1150
+ try { execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', path.join(tmp, 'bad-ext'), '--extension', 'core', '--no-git'], { stdio: 'pipe' }); return false; }
1151
+ catch { return !fs.existsSync(path.join(tmp, 'bad-ext')); }
1152
+ })();
1153
+ check('ssss new --extension rejects reserved registry names before writing', badExt);
1154
+
1155
+ // ssss schema validate: core, scaffolded extension, bundle, and a failing input.
1156
+ const schemaValidate = (target, extra = []) => {
1157
+ try {
1158
+ return { code: 0, out: JSON.parse(execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'schema', 'validate', target, '--json', ...extra], { encoding: 'utf8' })) };
1159
+ } catch (err) {
1160
+ return { code: err.status, out: JSON.parse(err.stdout || '{}') };
1161
+ }
1162
+ };
1163
+ const coreValid = schemaValidate(path.join(REGISTRY_DIR, 'core.json'));
1164
+ const extValid = schemaValidate(path.join(extApp, 'registry', 'extensions', 'acme.json'));
1165
+ const bundleValid = schemaValidate(path.resolve(__dirname, '..', 'conformance', 'reference-bundle.ucw.json'));
1166
+ check('ssss schema validate accepts core, extension, and bundle documents',
1167
+ coreValid.code === 0 && coreValid.out.valid && extValid.code === 0 && extValid.out.valid &&
1168
+ bundleValid.code === 0 && bundleValid.out.valid && bundleValid.out.schema === 'bundle',
1169
+ JSON.stringify({ coreValid, extValid, bundleValid }).slice(0, 600));
1170
+ const badRegistryPath = path.join(tmp, 'bad-registry.json');
1171
+ fs.writeFileSync(badRegistryPath, JSON.stringify({ registry: 'acme', document_primitives: {}, typo_key: 1 }));
1172
+ const badValid = schemaValidate(badRegistryPath, ['--type', 'core']);
1173
+ check('ssss schema validate exits 1 with structured errors on an invalid registry',
1174
+ badValid.code === 1 && badValid.out.valid === false && badValid.out.errors.some((e) => e.path === 'typo_key'));
1175
+
1138
1176
  const semanticOutput = JSON.parse(execFileSync('node', [
1139
1177
  path.join(__dirname, 'ssss.mjs'),
1140
1178
  'semantic',
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',
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Automatically synchronize and verify documentation after OpenWiki updates:
4
+ * 1. Updates overview.md command lists, topics, and primitive counts.
5
+ * 2. Ingests updated docs into OpenWiki knowledge graph.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ const SCRIPTS_DIR = path.join(ROOT, 'scripts');
13
+ const HELP_DIR = path.join(ROOT, 'docs', 'help');
14
+ const OVERVIEW_FILE = path.join(HELP_DIR, 'overview.md');
15
+ const CORE_REGISTRY = path.join(ROOT, 'registry', 'core.json');
16
+
17
+ const commands = fs.readdirSync(SCRIPTS_DIR)
18
+ .filter((f) => f.startsWith('cmd-') && f.endsWith('.mjs'))
19
+ .map((f) => f.replace(/^cmd-/, '').replace(/\.mjs$/, ''))
20
+ .sort();
21
+
22
+ const helpTopics = fs.existsSync(HELP_DIR)
23
+ ? fs.readdirSync(HELP_DIR)
24
+ .filter((f) => f.endsWith('.md'))
25
+ .map((f) => f.replace(/\.md$/, ''))
26
+ .sort()
27
+ : [];
28
+
29
+ let corePrimitivesCount = 20;
30
+ let contractPrimitivesCount = 5;
31
+ if (fs.existsSync(CORE_REGISTRY)) {
32
+ const core = JSON.parse(fs.readFileSync(CORE_REGISTRY, 'utf8'));
33
+ corePrimitivesCount = Object.keys(core.document_primitives || {}).length;
34
+ contractPrimitivesCount = Object.keys(core.contract_primitives || {}).length;
35
+ }
36
+
37
+ if (fs.existsSync(OVERVIEW_FILE)) {
38
+ let content = fs.readFileSync(OVERVIEW_FILE, 'utf8');
39
+
40
+ // Update primitive counts
41
+ const regPattern = /\|\s*Core registry\s*\|\s*`registry\/core\.json`\s*\|\s*[^|]+\|/;
42
+ const regReplacement = `| Core registry | \`registry/core.json\` | ${corePrimitivesCount} document primitives + ${contractPrimitivesCount} contract primitives; semantic (§11.9), bundle (§16), and provisioning (§17) rules. |`;
43
+ content = content.replace(regPattern, regReplacement);
44
+
45
+ // Update help topic list
46
+ const helpSection = `Run \`ssss help <topic>\` for any of:\n\`${helpTopics.join('`, `')}\`.`;
47
+ content = content.replace(/Run `ssss help <topic>` for any of:\n[\s\S]*?(?=\n\nRun `ssss <command>)/, helpSection);
48
+
49
+ // Update command list
50
+ const cmdSection = `Run \`ssss <command> --help\` for: \`${commands.join('`, `')}\`.`;
51
+ content = content.replace(/Run `ssss <command> --help` for:[\s\S]*?(?=\n*$)/, cmdSection);
52
+
53
+ fs.writeFileSync(OVERVIEW_FILE, content.trim() + '\n', 'utf8');
54
+ }
55
+
56
+ console.log(`✓ Synchronized docs/help/overview.md: ${commands.length} commands, ${helpTopics.length} help topics, ${corePrimitivesCount} primitives.`);
@@ -4,7 +4,7 @@ title: SSSS Protocol Reference
4
4
  name: ssss
5
5
  description: Validate or change SSSS documents, operations, registries, workflows and portable host adapters.
6
6
  timestamp: 2026-11-02T19:00:00Z
7
- version: 0.14.1
7
+ version: 0.14.2
8
8
  repo_scoped: false
9
9
  ---
10
10
 
@@ -1,4 +1,4 @@
1
- # SSSS portable core — 0.14.1
1
+ # SSSS portable core — 0.14.2
2
2
 
3
3
  Verified against the SSSS owner package, registry and spec on 2026-11-02. Establish ground truth from the installed validator, composed registry plus lock, and actual host mutation path. Schema support does not prove a configured runner or provider integration.
4
4
 
@@ -15,4 +15,4 @@ Preserve portability classes: structural definitions, tenant-private data and re
15
15
  ## Verification
16
16
  Run relevant schema/registry/lock and operation conformance, with valid and invalid examples. Cover duplicate/conflicting idempotency (including retries answered from the event log), unsafe paths, unauthorized/expired actors, leases, append semantics, rollback and commit-intent crash recovery where changed. Use `ssss --help`, then operation-specific help; commands differ by installed version. Report exact package version, registry/lock identity and actual results. Keep secrets and private runtime data out of skill content, bundles and logs.
17
17
 
18
- Normative sources are bundled below from the owner release tag v0.14.1 (`docs/ssss-spec.md` and `registry/core.json` at that tag). Retain authored local wrappers/references; this managed core is the portable update boundary.
18
+ Normative sources are bundled below from the owner release tag v0.14.2 (`docs/ssss-spec.md` and `registry/core.json` at that tag). Retain authored local wrappers/references; this managed core is the portable update boundary.
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://ssss.dev/registry/v1",
2
+ "$schema": "https://ssss.dev/schemas/core-registry.schema.json",
3
3
  "registry": "core",
4
4
  "spec_version": "0.9",
5
5
  "description": "The canonical SSSS core registry and semantic application-kernel contract. Core primitives are qualified as ssss:<name>; applications define namespaced primitives through layered extension registries without copying or redefining core behavior.",
@@ -269,7 +269,7 @@
269
269
  "classes": {
270
270
  "structural": "The reusable business model — sellable and templatable. Carries no operator- or customer-specific data. Examples: workflow, assistant, page, skill, model, rule, memory, migration, release.",
271
271
  "tenant_private": "The operator's private operational data — customers, transcripts, runtime work. NEVER leaves the workspace in a template or sale bundle; included only in an encrypted backup. Examples: conversation, run, task, conflict, and (via extension) profile / CRM contact / deal.",
272
- "resource_bound": "Requires a real external resource bound at provision time (a domain, phone number, mailbox, API connection). NEVER ships the seller's resource value; ships only a requirement declaration the buyer satisfies. Defined by extension registries (e.g. festech domain / phone_number / integration_connection)."
272
+ "resource_bound": "Requires a real external resource bound at provision time (a domain, phone number, mailbox, API connection). NEVER ships the seller's resource value; ships only a requirement declaration the buyer satisfies. Defined by extension registries (e.g. a domain, phone_number, or integration_connection primitive)."
273
273
  },
274
274
  "export_profiles": {
275
275
  "backup": { "includes": ["structural", "tenant_private", "resource_bound"], "note": "Complete fidelity copy. tenant_private MUST be encrypted at rest." },
@@ -261,8 +261,9 @@ primitive — it implements the subset its product requires — but any primitiv
261
261
  | `primitive` | meta | structural | A governed, versioned definition for a namespaced SSSS primitive. |
262
262
 
263
263
  The **Portability** column is the primitive's default portability class (§5.5); a host
264
- resolves it from `registry/core.json`. Extension registries assign their own primitives a
265
- class — notably `resource_bound` for things like domains and phone numbers.
264
+ resolves it from `registry/core.json`. The canonical machine-readable registry conforms to the
265
+ JSON Schema (Draft 2020-12) published in `schemas/core-registry.schema.json`. Extension registries
266
+ assign their own primitives a class — notably `resource_bound` for things like domains and phone numbers.
266
267
 
267
268
  ### 5.2 Contract Primitives
268
269
 
@@ -1869,7 +1870,9 @@ A bundle is a JSON object with three top-level members:
1869
1870
  ```
1870
1871
 
1871
1872
  `files` MUST be sorted by `path` (§16.3 order) so that two exports of the same vault state
1872
- are identical and content-hashable (§16.3, `provenance.content_hash`).
1873
+ are identical and content-hashable (§16.3, `provenance.content_hash`). The normative JSON Schema
1874
+ (Draft 2020-12) for `.ucw` bundle containers is published in `schemas/bundle.schema.json` and
1875
+ exportable via the CLI command `ssss schema bundle`.
1873
1876
 
1874
1877
  ### 16.2 The Manifest
1875
1878
 
package/src/bundle.mjs CHANGED
@@ -63,6 +63,7 @@ function requirementParameterKey(type, field) {
63
63
  function resourceFieldParameterAliases(type, field) {
64
64
  const aliases = [requirementParameterKey(type, field), field];
65
65
  if (type === 'domain' && field === 'domain_name') aliases.push('domain');
66
+ if (type === 'resource' && (field === 'binds' || field === 'name')) aliases.push('domain', 'resource_domain');
66
67
  if (type === 'phone_number' && field === 'phone_number') aliases.push('phone');
67
68
  return aliases;
68
69
  }