@gregiteen/ssss-cli 0.7.1 → 0.8.0

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.
@@ -18,9 +18,17 @@ import crypto from 'node:crypto';
18
18
  import { execFileSync } from 'node:child_process';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { createEngine } from '../src/engine.mjs';
21
- import { validateBundle, provisionBundle, importBundle } from '../src/bundle.mjs';
21
+ import {
22
+ DEFAULT_EXPORTER,
23
+ exportBundle,
24
+ validateBundle,
25
+ provisionBundle,
26
+ importBundle,
27
+ } from '../src/bundle.mjs';
22
28
  import { parseDocument } from '../src/frontmatter.mjs';
29
+ import { documentHash, loadRegistries } from '../src/registry.mjs';
23
30
  import { createRunEnvelope, planWorkflowTrigger } from '../src/runtime.mjs';
31
+ import { buildSemanticIndex, materializeLocale, searchSemanticIndex } from '../src/semantic.mjs';
24
32
  import { auditRegistryFieldUsage } from './audit-registry-field-usage.mjs';
25
33
  import { validateSkills } from './validate-skills.mjs';
26
34
 
@@ -28,6 +36,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
36
  const FIXTURES = path.resolve(__dirname, '..', 'conformance', 'fixtures.json');
29
37
  const REFERENCE_BUNDLE = path.resolve(__dirname, '..', 'conformance', 'reference-bundle.ucw.json');
30
38
  const REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
39
+ const PACKAGE = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
31
40
  const PORTABILITY_CLASSES = ['structural', 'tenant_private', 'resource_bound'];
32
41
 
33
42
  function parseArgs(argv) {
@@ -194,6 +203,32 @@ function runBundleConformance() {
194
203
 
195
204
  const { valid, errors } = validateBundle(bundle, { registryDir: REGISTRY_DIR });
196
205
  check('reference bundle is schema- and hash-valid', valid, errors.join('; '));
206
+ check('reference bundle provenance matches the published package identity',
207
+ bundle.manifest.provenance.exporter === `${PACKAGE.name}@${PACKAGE.version}`,
208
+ `got=${bundle.manifest.provenance.exporter}`);
209
+
210
+ const defaultExportRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-exporter-'));
211
+ try {
212
+ fs.mkdirSync(path.join(defaultExportRoot, 'rules'), { recursive: true });
213
+ fs.writeFileSync(path.join(defaultExportRoot, 'rules', 'identity.md'), [
214
+ '---',
215
+ 'type: rule',
216
+ 'title: Exporter Identity',
217
+ 'description: Proves the default exporter uses the published package name.',
218
+ 'timestamp: 2026-07-10T00:00:00Z',
219
+ 'name: Exporter Identity',
220
+ '---',
221
+ '',
222
+ 'Identity fixture.',
223
+ '',
224
+ ].join('\n'));
225
+ const defaultExport = exportBundle(defaultExportRoot, { registryDir: REGISTRY_DIR });
226
+ check('default bundle exporter matches the published package name',
227
+ defaultExport.manifest.provenance.exporter === DEFAULT_EXPORTER,
228
+ `got=${defaultExport.manifest.provenance.exporter}`);
229
+ } finally {
230
+ fs.rmSync(defaultExportRoot, { recursive: true, force: true });
231
+ }
197
232
  check('sale profile carries no tenant_private file',
198
233
  bundle.files.every((f) => parseDocument(f.content).data.type !== 'task' && parseDocument(f.content).data.type !== 'conversation'));
199
234
  check('sale profile reduces resource_bound fields to requirement declarations',
@@ -210,6 +245,14 @@ function runBundleConformance() {
210
245
  !missingFilesValidation.valid && missingFilesValidation.errors.some((e) => e.includes('bundle.files must be an array')),
211
246
  missingFilesValidation.errors.join('; '));
212
247
 
248
+ const malformedFileBundle = JSON.parse(JSON.stringify(bundle));
249
+ malformedFileBundle.files[0] = null;
250
+ const malformedFileValidation = validateBundle(malformedFileBundle, { registryDir: REGISTRY_DIR });
251
+ check('bundle validation returns structured errors for malformed file entries',
252
+ !malformedFileValidation.valid &&
253
+ malformedFileValidation.errors.some((error) => error.includes('file entries must be objects')),
254
+ malformedFileValidation.errors.join('; '));
255
+
213
256
  const unknownExtensionBundle = JSON.parse(JSON.stringify(bundle));
214
257
  unknownExtensionBundle.manifest.required_extensions = ['missing-extension'];
215
258
  const unknownExtensionValidation = validateBundle(unknownExtensionBundle, { registryDir: REGISTRY_DIR });
@@ -217,6 +260,16 @@ function runBundleConformance() {
217
260
  !unknownExtensionValidation.valid && unknownExtensionValidation.errors.some((e) => e.includes("required extension 'missing-extension'")),
218
261
  unknownExtensionValidation.errors.join('; '));
219
262
 
263
+ const invalidTranslationBundle = JSON.parse(JSON.stringify(bundle));
264
+ const invalidTranslation = invalidTranslationBundle.files.find((file) =>
265
+ parseDocument(file.content).data.type === 'translation');
266
+ invalidTranslation.content = invalidTranslation.content.replace('locale: es', 'locale: invalid_locale!');
267
+ const invalidTranslationValidation = validateBundle(invalidTranslationBundle, { registryDir: REGISTRY_DIR });
268
+ check('bundle validation enforces registry patterns before import',
269
+ !invalidTranslationValidation.valid &&
270
+ invalidTranslationValidation.errors.some((error) => error.includes('registry pattern')),
271
+ invalidTranslationValidation.errors.join('; '));
272
+
220
273
  const prov = provisionBundle(bundle, { workspaceId: 'ws-conf', parameters: { business_name: 'Demo', domain: 'demo.example' } });
221
274
  check('provision resolves params + link integrity (no dangling [[links]])', prov.ok,
222
275
  `unresolved=${JSON.stringify(prov.unresolved)} dangling=${JSON.stringify(prov.danglingLinks)}`);
@@ -238,6 +291,39 @@ function runBundleConformance() {
238
291
  fs.rmSync(vault, { recursive: true, force: true });
239
292
  }
240
293
 
294
+ const atomicVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-atomic-'));
295
+ try {
296
+ const engine = createEngine({ registryDir: REGISTRY_DIR });
297
+ const sourceContent = [
298
+ '---', 'type: rule', 'title: Atomic Source', 'description: Preflight source.',
299
+ 'timestamp: 2026-07-10T00:00:00Z', 'name: Atomic Source', '---', '', 'Body.', '',
300
+ ].join('\n');
301
+ const badPlan = [
302
+ {
303
+ type: 'operation', idempotency_key: 'atomic-source', workspace_id: 'ws-atomic',
304
+ path: 'rules/atomic.md', content: sourceContent, actor: { role: 'system' },
305
+ },
306
+ {
307
+ type: 'operation', idempotency_key: 'atomic-invalid', workspace_id: 'ws-atomic',
308
+ path: 'translations/bad/rules/atomic.md', actor: { role: 'system' },
309
+ content: [
310
+ '---', 'type: translation', 'title: Invalid Translation',
311
+ 'description: Must fail before the source commits.', 'timestamp: 2026-07-10T00:00:00Z',
312
+ 'translation_id: invalid-atomic', 'source_path: rules/atomic.md',
313
+ `source_hash: ${documentHash(sourceContent)}`, 'locale: invalid_locale!',
314
+ 'status: approved', 'translated_fields: [body]', '---', '', 'Translated.', '',
315
+ ].join('\n'),
316
+ },
317
+ ];
318
+ const atomicResult = importBundle(badPlan, atomicVault, engine);
319
+ check('two-phase import prevents partial commits on a late invalid envelope',
320
+ atomicResult.ok === false && atomicResult.committed === 0 &&
321
+ !fs.existsSync(path.join(atomicVault, 'rules', 'atomic.md')),
322
+ JSON.stringify(atomicResult.results.map((result) => result.validation?.errors || [])));
323
+ } finally {
324
+ fs.rmSync(atomicVault, { recursive: true, force: true });
325
+ }
326
+
241
327
  let pass = 0;
242
328
  for (const c of checks) {
243
329
  if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
@@ -349,6 +435,259 @@ function runOperationContractRegressionConformance() {
349
435
  return pass === checks.length;
350
436
  }
351
437
 
438
+ function runSemanticLocalizationConformance() {
439
+ const checks = [];
440
+ const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
441
+ const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-semantic-'));
442
+ const projection = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-locale-'));
443
+ try {
444
+ const engine = createEngine({ registryDir: REGISTRY_DIR });
445
+ const sourcePath = 'rules/refund-policy.md';
446
+ const sourceContent = [
447
+ '---',
448
+ 'type: rule',
449
+ 'title: Refund Policy',
450
+ 'description: Explains when festival ticket refunds are available.',
451
+ 'timestamp: 2026-07-10T00:00:00Z',
452
+ 'name: Refund Policy',
453
+ 'aliases: [refund terms, cancellation policy]',
454
+ 'tags: [tickets, finance]',
455
+ 'relations:',
456
+ ' - relation: applies_to',
457
+ ' target: workflows/box-office.md',
458
+ '---',
459
+ '',
460
+ 'Refunds are available until fourteen days before the event.',
461
+ '',
462
+ ].join('\n');
463
+ const sourceWrite = engine.processOperation({
464
+ type: 'operation', idempotency_key: 'sem-source', workspace_id: 'ws-sem',
465
+ path: sourcePath, content: sourceContent, actor: { role: 'system' },
466
+ }, vault);
467
+ check('structural semantic source commits', sourceWrite.success === true,
468
+ JSON.stringify(sourceWrite.validation?.errors || []));
469
+
470
+ const privateContent = [
471
+ '---',
472
+ 'type: task',
473
+ 'title: Private Refund Request',
474
+ 'description: Private customer refund task.',
475
+ 'timestamp: 2026-07-10T00:00:00Z',
476
+ 'priority: high',
477
+ 'category: support',
478
+ 'status: pending',
479
+ '---',
480
+ '',
481
+ 'Customer account and payment details.',
482
+ '',
483
+ ].join('\n');
484
+ const privateWrite = engine.processOperation({
485
+ type: 'operation', idempotency_key: 'sem-private', workspace_id: 'ws-sem',
486
+ path: 'tasks/private-refund.md', content: privateContent, actor: { role: 'system' },
487
+ }, vault);
488
+ check('tenant-private semantic source commits', privateWrite.success === true,
489
+ JSON.stringify(privateWrite.validation?.errors || []));
490
+
491
+ const overlayPath = 'translations/es-MX/rules/refund-policy.md';
492
+ const overlayContent = [
493
+ '---',
494
+ 'type: translation',
495
+ 'title: Spanish Refund Policy Translation',
496
+ 'description: Approved Spanish presentation overlay for the refund policy.',
497
+ 'timestamp: 2026-07-10T00:00:00Z',
498
+ 'translation_id: refund-policy-es-mx',
499
+ `source_path: ${sourcePath}`,
500
+ `source_hash: ${documentHash(sourceContent)}`,
501
+ 'locale: es-MX',
502
+ 'status: approved',
503
+ 'translated_fields: [title, description, body]',
504
+ 'translated_title: Política de reembolsos',
505
+ 'translated_description: Explica cuándo están disponibles los reembolsos de boletos.',
506
+ '---',
507
+ '',
508
+ 'Los reembolsos están disponibles hasta catorce días antes del evento.',
509
+ '',
510
+ ].join('\n');
511
+ const overlayWrite = engine.processOperation({
512
+ type: 'operation', idempotency_key: 'sem-overlay', workspace_id: 'ws-sem',
513
+ path: overlayPath, content: overlayContent, actor: { role: 'system' },
514
+ }, vault);
515
+ check('hash-bound translation overlay commits', overlayWrite.success === true,
516
+ JSON.stringify(overlayWrite.validation?.errors || []));
517
+
518
+ const staleOverlay = overlayContent.replace(documentHash(sourceContent), `sha256:${'0'.repeat(64)}`)
519
+ .replace('translation_id: refund-policy-es-mx', 'translation_id: stale-es-mx');
520
+ const staleWrite = engine.processOperation({
521
+ type: 'operation', idempotency_key: 'sem-stale', workspace_id: 'ws-sem',
522
+ path: 'translations/es-MX/rules/stale.md', content: staleOverlay, actor: { role: 'system' },
523
+ }, vault);
524
+ check('stale translation source hash is rejected', staleWrite.success === false &&
525
+ staleWrite.validation.errors.some((error) => error.includes('Source hash mismatch')),
526
+ JSON.stringify(staleWrite.validation?.errors || []));
527
+
528
+ const traversalOverlay = overlayContent
529
+ .replace('translation_id: refund-policy-es-mx', 'translation_id: traversal-es-mx')
530
+ .replace(`source_path: ${sourcePath}`, 'source_path: ../secrets.md');
531
+ const traversalWrite = engine.processOperation({
532
+ type: 'operation', idempotency_key: 'sem-traversal', workspace_id: 'ws-sem',
533
+ path: 'translations/es-MX/rules/traversal.md', content: traversalOverlay, actor: { role: 'system' },
534
+ }, vault);
535
+ check('translation reference traversal is rejected', traversalWrite.success === false &&
536
+ traversalWrite.validation.errors.some((error) => error.includes('safe vault-relative path')),
537
+ JSON.stringify(traversalWrite.validation?.errors || []));
538
+
539
+ const immutablePatch = engine.processOperation({
540
+ type: 'patch', idempotency_key: 'sem-immutable', workspace_id: 'ws-sem',
541
+ path: overlayPath, patches: { locale: 'fr' }, actor: { role: 'system' },
542
+ }, vault);
543
+ check('translation identity fields are immutable', immutablePatch.success === false &&
544
+ immutablePatch.validation.errors.some((error) => error.includes("immutable field 'locale'")),
545
+ JSON.stringify(immutablePatch.validation?.errors || []));
546
+
547
+ const first = buildSemanticIndex(vault, { locale: 'es-MX', registryDir: REGISTRY_DIR });
548
+ const second = buildSemanticIndex(vault, { locale: 'es-mx', registryDir: REGISTRY_DIR });
549
+ check('semantic index is deterministic and locale-normalized',
550
+ first.index_hash === second.index_hash && first.locale === 'es-MX');
551
+ check('safe semantic default excludes tenant-private documents',
552
+ first.documents.every((document) => document.portability === 'structural') &&
553
+ !first.documents.some((document) => document.path.startsWith('tasks/')));
554
+ check('semantic graph captures explicit relations',
555
+ first.edges.some((edge) => edge.source === sourcePath && edge.relation === 'applies_to'));
556
+ const matches = searchSemanticIndex(first, 'reembolsos boletos', { limit: 3 });
557
+ check('localized semantic search ranks the translated source',
558
+ matches[0]?.document.path === sourcePath && matches[0].document.title === 'Política de reembolsos',
559
+ JSON.stringify(matches.map((match) => ({ path: match.document.path, score: match.score }))));
560
+
561
+ const privateIndex = buildSemanticIndex(vault, { includePrivate: true, registryDir: REGISTRY_DIR });
562
+ check('private semantic indexing requires explicit opt-in',
563
+ privateIndex.documents.some((document) => document.path === 'tasks/private-refund.md'));
564
+
565
+ const manifest = materializeLocale(vault, 'es-mx', projection, { registryDir: REGISTRY_DIR });
566
+ const localizedContent = fs.readFileSync(path.join(projection, sourcePath), 'utf8');
567
+ const localizedDocument = parseDocument(localizedContent);
568
+ check('localization projection preserves symbolic control fields',
569
+ localizedDocument.data.type === 'rule' && localizedDocument.data.name === 'Refund Policy' &&
570
+ localizedDocument.data.title === 'Política de reembolsos' &&
571
+ localizedDocument.data.locale === 'es-MX');
572
+ check('localization projection excludes private documents by default',
573
+ manifest.document_count >= 1 && !fs.existsSync(path.join(projection, 'tasks', 'private-refund.md')));
574
+ check('localization publishes a complete projection without staging residue',
575
+ fs.existsSync(path.join(projection, '.ssss-projection.json')) &&
576
+ !fs.readdirSync(path.dirname(projection)).some((name) =>
577
+ name.startsWith(`.${path.basename(projection)}.`) && name.endsWith('.stage')));
578
+ let insideRejected = false;
579
+ try { materializeLocale(vault, 'es-MX', path.join(vault, 'derived', 'es'), { registryDir: REGISTRY_DIR }); }
580
+ catch (error) { insideRejected = error.message.includes('outside the source vault'); }
581
+ check('localization output inside the source vault is rejected', insideRejected);
582
+
583
+ const invalidRawPath = path.join(vault, 'rules', 'invalid-raw.md');
584
+ fs.writeFileSync(invalidRawPath, '---\ntype: rule\nname: Invalid\n---\nBody.\n');
585
+ let invalidRawRejected = false;
586
+ try { buildSemanticIndex(vault, { registryDir: REGISTRY_DIR }); }
587
+ catch (error) { invalidRawRejected = error.message.includes("Missing required field 'title'"); }
588
+ check('semantic projection rejects raw documents that violate the registry', invalidRawRejected);
589
+ } finally {
590
+ fs.rmSync(vault, { recursive: true, force: true });
591
+ fs.rmSync(projection, { recursive: true, force: true });
592
+ }
593
+
594
+ let pass = 0;
595
+ for (const c of checks) {
596
+ if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
597
+ else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
598
+ }
599
+ console.log(`\n ${pass}/${checks.length} semantic/localization checks passed (§11.9)`);
600
+ return pass === checks.length;
601
+ }
602
+
603
+ function runRegistryExtensionConformance() {
604
+ const checks = [];
605
+ const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
606
+ const registry = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-registry-'));
607
+ const extensions = path.join(registry, 'extensions');
608
+ fs.mkdirSync(extensions, { recursive: true });
609
+ fs.copyFileSync(path.join(REGISTRY_DIR, 'core.json'), path.join(registry, 'core.json'));
610
+ const primitive = {
611
+ family: 'extension', append_only: false, portability: 'structural',
612
+ required_fields: ['type', 'name'],
613
+ };
614
+ const writeExtension = (name, value) =>
615
+ fs.writeFileSync(path.join(extensions, `${name}.json`), JSON.stringify(value, null, 2));
616
+ try {
617
+ writeExtension('valid', {
618
+ registry: 'valid-ext', extends: 'core', document_primitives: { custom_thing: primitive },
619
+ });
620
+ let validLoaded = false;
621
+ try { validLoaded = loadRegistries(registry).types.has('custom_thing'); } catch {}
622
+ check('valid extension registry composes with core', validLoaded);
623
+
624
+ writeExtension('collision', {
625
+ registry: 'collision', extends: 'core', document_primitives: { rule: primitive },
626
+ });
627
+ let coreCollision = false;
628
+ try { loadRegistries(registry); } catch (error) { coreCollision = error.message.includes("primitive 'rule' collides"); }
629
+ check('extension cannot redefine a core primitive', coreCollision);
630
+ fs.rmSync(path.join(extensions, 'collision.json'));
631
+
632
+ writeExtension('duplicate', {
633
+ registry: 'duplicate', extends: 'core', document_primitives: { custom_thing: primitive },
634
+ });
635
+ let siblingCollision = false;
636
+ try { loadRegistries(registry); } catch (error) { siblingCollision = error.message.includes("primitive 'custom_thing' collides"); }
637
+ check('sibling extensions cannot redefine each other', siblingCollision);
638
+ fs.rmSync(path.join(extensions, 'duplicate.json'));
639
+
640
+ writeExtension('pattern', {
641
+ registry: 'bad-pattern', extends: 'core',
642
+ document_primitives: { patterned: { ...primitive, patterns: { name: '[' } } },
643
+ });
644
+ let badPattern = false;
645
+ try { loadRegistries(registry); } catch (error) { badPattern = error.message.includes('invalid pattern'); }
646
+ check('invalid extension regex fails during registry load', badPattern);
647
+ fs.rmSync(path.join(extensions, 'pattern.json'));
648
+
649
+ writeExtension('shape', {
650
+ registry: 'bad-shape', extends: 'core',
651
+ document_primitives: { shaped: { ...primitive, required_when: [] } },
652
+ });
653
+ let badShape = false;
654
+ try { loadRegistries(registry); } catch (error) { badShape = error.message.includes('required_when must be an object'); }
655
+ check('malformed extension constraint shapes fail during registry load', badShape);
656
+ fs.rmSync(path.join(extensions, 'shape.json'));
657
+
658
+ writeExtension('unknown-reference', {
659
+ registry: 'unknown-reference', extends: 'core',
660
+ document_primitives: {
661
+ linked_thing: {
662
+ ...primitive,
663
+ references: { target_path: { allowed_types: ['missing_type'] } },
664
+ },
665
+ },
666
+ });
667
+ let unknownReference = false;
668
+ try { loadRegistries(registry); } catch (error) { unknownReference = error.message.includes("unknown type 'missing_type'"); }
669
+ check('extension references cannot name unknown primitive types', unknownReference);
670
+ fs.rmSync(path.join(extensions, 'unknown-reference.json'));
671
+
672
+ const symlinkTarget = path.join(registry, 'external.json');
673
+ fs.writeFileSync(symlinkTarget, JSON.stringify({ registry: 'linked', extends: 'core', document_primitives: {} }));
674
+ fs.symlinkSync(symlinkTarget, path.join(extensions, 'linked.json'));
675
+ let symlinkRejected = false;
676
+ try { loadRegistries(registry); } catch (error) { symlinkRejected = error.message.includes('symlinked registries'); }
677
+ check('symlinked extension registry is rejected', symlinkRejected);
678
+ } finally {
679
+ fs.rmSync(registry, { recursive: true, force: true });
680
+ }
681
+
682
+ let pass = 0;
683
+ for (const c of checks) {
684
+ if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
685
+ else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
686
+ }
687
+ console.log(`\n ${pass}/${checks.length} extension-registry checks passed`);
688
+ return pass === checks.length;
689
+ }
690
+
352
691
  function runCliSmokeConformance() {
353
692
  const checks = [];
354
693
  const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
@@ -382,6 +721,32 @@ function runCliSmokeConformance() {
382
721
  }
383
722
  check('ssss new starter vault files validate against the registry', files.length > 0 && failures.length === 0, JSON.stringify(failures));
384
723
 
724
+ const semanticOutput = JSON.parse(execFileSync('node', [
725
+ path.join(__dirname, 'ssss.mjs'),
726
+ 'semantic',
727
+ vault,
728
+ '--query',
729
+ 'welcome rule',
730
+ ], { encoding: 'utf8' }));
731
+ check('ssss semantic returns ranked structural results',
732
+ semanticOutput.results?.some((result) => result.document.path === 'rules/welcome.md'),
733
+ JSON.stringify(semanticOutput.results || []));
734
+
735
+ const localizedVault = path.join(tmp, 'localized-es');
736
+ const localizationOutput = JSON.parse(execFileSync('node', [
737
+ path.join(__dirname, 'ssss.mjs'),
738
+ 'localize',
739
+ vault,
740
+ '--locale',
741
+ 'es',
742
+ '--out',
743
+ localizedVault,
744
+ ], { encoding: 'utf8' }));
745
+ check('ssss localize materializes a safe structural projection',
746
+ localizationOutput.locale === 'es' &&
747
+ fs.existsSync(path.join(localizedVault, 'rules', 'welcome.md')) &&
748
+ !fs.existsSync(path.join(localizedVault, 'tasks', 'first-task.md')));
749
+
385
750
  const dryRunVault = path.join(tmp, 'dry-run-vault');
386
751
  const dryRunOut = execFileSync('node', [
387
752
  path.join(__dirname, 'ssss.mjs'),
@@ -593,11 +958,18 @@ async function main() {
593
958
  const runtimeOk = runRuntimeConformance();
594
959
  console.log('\nRunning operation regression conformance (src/engine.mjs, §6/§7) ...');
595
960
  const operationRegressionOk = runOperationContractRegressionConformance();
961
+ console.log('\nRunning extension-registry conformance (src/registry.mjs) ...');
962
+ const extensionRegistryOk = runRegistryExtensionConformance();
963
+ console.log('\nRunning semantic/localization conformance (src/semantic.mjs, §11.9) ...');
964
+ const semanticOk = runSemanticLocalizationConformance();
596
965
  console.log('\nRunning bundle/provisioning conformance (src/bundle.mjs, §16/§17) ...');
597
966
  const bundleOk = runBundleConformance();
598
967
  console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
599
968
  const cliSmokeOk = runCliSmokeConformance();
600
- process.exit(engineOk && runtimeOk && operationRegressionOk && bundleOk && cliSmokeOk ? 0 : 1);
969
+ process.exit(
970
+ engineOk && runtimeOk && operationRegressionOk && extensionRegistryOk &&
971
+ semanticOk && bundleOk && cliSmokeOk ? 0 : 1
972
+ );
601
973
  } else {
602
974
  console.log('\nℹ No --endpoint/--engine given; ran structural + registry validation only.');
603
975
  console.log(' Reference engine: ssss conformance --engine');
package/scripts/ssss.mjs CHANGED
@@ -37,6 +37,8 @@ Vault → bundle → tenant lifecycle:
37
37
 
38
38
  Authoring & conformance:
39
39
  autolink [dir] Generate OKF wiki links across a vault.
40
+ semantic <vault> Build/query a deterministic semantic projection (§11.9).
41
+ localize <vault> Materialize approved locale overlays outside the vault.
40
42
  conformance Run the canonical conformance suite (spec §12).
41
43
  --engine | --endpoint <url> [--token <pat>]
42
44
 
@@ -60,6 +62,8 @@ const MODULES = {
60
62
  inspect: './cmd-inspect.mjs',
61
63
  provision: './cmd-provision.mjs',
62
64
  import: './cmd-import.mjs',
65
+ semantic: './cmd-semantic.mjs',
66
+ localize: './cmd-localize.mjs',
63
67
  help: './cmd-help.mjs',
64
68
  };
65
69