@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +55 -4
  2. package/README.md +42 -84
  3. package/VERSION +1 -1
  4. package/conformance/README.md +1 -1
  5. package/conformance/fixtures.json +4 -48
  6. package/conformance/reference-bundle.ucw.json +22 -21
  7. package/docs/help/bundle.md +2 -2
  8. package/docs/help/overview.md +9 -6
  9. package/docs/help/semantic.md +5 -4
  10. package/docs/ssss-spec.md +42 -86
  11. package/package.json +22 -3
  12. package/registry/core.json +22 -29
  13. package/scripts/build-reference-bundle.mjs +0 -20
  14. package/scripts/cmd-adapter.mjs +29 -0
  15. package/scripts/cmd-import.mjs +1 -1
  16. package/scripts/cmd-migrate.mjs +26 -0
  17. package/scripts/cmd-new.mjs +5 -5
  18. package/scripts/cmd-primitive.mjs +48 -0
  19. package/scripts/cmd-registry.mjs +25 -0
  20. package/scripts/cmd-semantic.mjs +6 -8
  21. package/scripts/conformance-09.mjs +331 -0
  22. package/scripts/conformance.mjs +153 -186
  23. package/scripts/generate-sbom.mjs +96 -0
  24. package/scripts/phase9-cross-host-corpus.mjs +223 -0
  25. package/scripts/ssss.mjs +8 -2
  26. package/skills/ssss/SKILL.md +32 -208
  27. package/skills/ssss/evals/evals.json +3 -3
  28. package/skills/ssss/references/contract-reference.md +7 -8
  29. package/skills/ssss/scripts/validate-schema.mjs +1 -44
  30. package/src/authorization.mjs +62 -0
  31. package/src/bundle.mjs +41 -5
  32. package/src/engine.mjs +101 -503
  33. package/src/events.mjs +155 -0
  34. package/src/guard.mjs +32 -0
  35. package/src/http.mjs +26 -0
  36. package/src/idempotency.mjs +55 -0
  37. package/src/kernel.mjs +260 -0
  38. package/src/leases.mjs +173 -0
  39. package/src/primitive.mjs +227 -0
  40. package/src/projections.mjs +133 -0
  41. package/src/registry.mjs +190 -39
  42. package/src/semantic.mjs +124 -316
  43. package/src/ui.mjs +100 -0
  44. package/src/validator.mjs +87 -0
  45. package/src/vfs.mjs +203 -0
  46. package/types/kernel.d.ts +48 -0
  47. package/docs/help/localization.md +0 -43
  48. package/scripts/cmd-localize.mjs +0 -44
@@ -20,17 +20,25 @@ import { fileURLToPath } from 'node:url';
20
20
  import { createEngine } from '../src/engine.mjs';
21
21
  import {
22
22
  DEFAULT_EXPORTER,
23
+ contentHash,
23
24
  exportBundle,
24
25
  validateBundle,
25
26
  provisionBundle,
26
27
  importBundle,
27
28
  } from '../src/bundle.mjs';
28
29
  import { parseDocument } from '../src/frontmatter.mjs';
29
- import { documentHash, loadRegistries } from '../src/registry.mjs';
30
+ import { loadRegistries } from '../src/registry.mjs';
30
31
  import { createRunEnvelope, planWorkflowTrigger } from '../src/runtime.mjs';
31
- import { buildSemanticIndex, materializeLocale, searchSemanticIndex } from '../src/semantic.mjs';
32
+ import {
33
+ buildSemanticIndex,
34
+ enrichSemanticIndex,
35
+ renderSemanticRecord,
36
+ searchSemanticIndex,
37
+ searchSemanticIndexWithAdapter,
38
+ } from '../src/semantic.mjs';
32
39
  import { auditRegistryFieldUsage } from './audit-registry-field-usage.mjs';
33
40
  import { validateSkills } from './validate-skills.mjs';
41
+ import { runKernel09Conformance } from './conformance-09.mjs';
34
42
 
35
43
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
44
  const FIXTURES = path.resolve(__dirname, '..', 'conformance', 'fixtures.json');
@@ -155,14 +163,14 @@ function validateRegistry() {
155
163
  * (spec §12) without needing a live host. Fixtures run in array order so that
156
164
  * create → patch → delete dependencies resolve.
157
165
  */
158
- function runAgainstEngine(doc) {
166
+ async function runAgainstEngine(doc) {
159
167
  const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-vault-'));
160
168
  const engine = createEngine();
161
169
  let pass = 0, fail = 0;
162
170
  try {
163
171
  for (const f of doc.fixtures) {
164
172
  const want = f.expected_response || {};
165
- const res = engine.processOperation(f.request, vault);
173
+ const res = await engine.processOperation(f.request, vault);
166
174
  const okSuccess = want.success == null || res.success === want.success;
167
175
  const wantType = want.validation && want.validation.type;
168
176
  const okType = wantType == null || (res.validation && res.validation.type === wantType);
@@ -192,7 +200,7 @@ function runAgainstEngine(doc) {
192
200
  * - no `tenant_private` primitive ever lands (the §5.5 keystone).
193
201
  * Returns true on full pass.
194
202
  */
195
- function runBundleConformance() {
203
+ async function runBundleConformance() {
196
204
  if (!fs.existsSync(REFERENCE_BUNDLE)) {
197
205
  console.log(' ⚠ No reference bundle; run `node scripts/build-reference-bundle.mjs` first.');
198
206
  return false;
@@ -260,15 +268,23 @@ function runBundleConformance() {
260
268
  !unknownExtensionValidation.valid && unknownExtensionValidation.errors.some((e) => e.includes("required extension 'missing-extension'")),
261
269
  unknownExtensionValidation.errors.join('; '));
262
270
 
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 });
271
+ const invalidPatternBundle = JSON.parse(JSON.stringify(bundle));
272
+ const invalidPattern = invalidPatternBundle.files.find((file) => parseDocument(file.content).data.type === 'rule');
273
+ invalidPattern.path = 'primitives/invalid.md';
274
+ invalidPattern.content = [
275
+ '---', 'type: primitive', 'title: Invalid Primitive', 'description: Invalid namespace pattern.',
276
+ 'timestamp: 2026-07-10T00:00:00Z', 'primitive_id: invalid!:thing', 'namespace: invalid!',
277
+ 'version: 1', 'name: Invalid', 'mutation: replace', 'portability: structural',
278
+ 'scopes: [workspace]', 'fields: [placeholder]', '---', '', 'Invalid.', '',
279
+ ].join('\n');
280
+ invalidPatternBundle.manifest.primitive_inventory.rule--;
281
+ invalidPatternBundle.manifest.primitive_inventory.primitive = 1;
282
+ invalidPatternBundle.manifest.provenance.content_hash = contentHash(invalidPatternBundle.files);
283
+ const invalidPatternValidation = validateBundle(invalidPatternBundle, { registryDir: REGISTRY_DIR });
268
284
  check('bundle validation enforces registry patterns before import',
269
- !invalidTranslationValidation.valid &&
270
- invalidTranslationValidation.errors.some((error) => error.includes('registry pattern')),
271
- invalidTranslationValidation.errors.join('; '));
285
+ !invalidPatternValidation.valid &&
286
+ invalidPatternValidation.errors.some((error) => error.includes('registry pattern')),
287
+ invalidPatternValidation.errors.join('; '));
272
288
 
273
289
  const prov = provisionBundle(bundle, { workspaceId: 'ws-conf', parameters: { business_name: 'Demo', domain: 'demo.example' } });
274
290
  check('provision resolves params + link integrity (no dangling [[links]])', prov.ok,
@@ -277,11 +293,11 @@ function runBundleConformance() {
277
293
  const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-'));
278
294
  try {
279
295
  const engine = createEngine({ registryDir: REGISTRY_DIR });
280
- const first = importBundle(prov.plan, vault, engine);
296
+ const first = await importBundle(prov.plan, vault, engine);
281
297
  check('import commits every file', first.ok && first.committed === bundle.files.length,
282
298
  `ok=${first.ok} committed=${first.committed}/${bundle.files.length}`);
283
299
 
284
- const second = importBundle(prov.plan, vault, engine);
300
+ const second = await importBundle(prov.plan, vault, engine);
285
301
  check('re-import is idempotent (0 new commits)', second.ok && second.committed === 0,
286
302
  `committed=${second.committed}`);
287
303
 
@@ -305,17 +321,17 @@ function runBundleConformance() {
305
321
  },
306
322
  {
307
323
  type: 'operation', idempotency_key: 'atomic-invalid', workspace_id: 'ws-atomic',
308
- path: 'translations/bad/rules/atomic.md', actor: { role: 'system' },
324
+ path: 'primitives/invalid.md', actor: { role: 'system' },
309
325
  content: [
310
- '---', 'type: translation', 'title: Invalid Translation',
326
+ '---', 'type: primitive', 'title: Invalid Primitive',
311
327
  '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.', '',
328
+ 'primitive_id: invalid!:thing', 'namespace: invalid!', 'version: 1', 'name: Invalid',
329
+ 'mutation: replace', 'portability: structural', 'scopes: [workspace]',
330
+ 'fields: [placeholder]', '---', '', 'Invalid.', '',
315
331
  ].join('\n'),
316
332
  },
317
333
  ];
318
- const atomicResult = importBundle(badPlan, atomicVault, engine);
334
+ const atomicResult = await importBundle(badPlan, atomicVault, engine);
319
335
  check('two-phase import prevents partial commits on a late invalid envelope',
320
336
  atomicResult.ok === false && atomicResult.committed === 0 &&
321
337
  !fs.existsSync(path.join(atomicVault, 'rules', 'atomic.md')),
@@ -333,7 +349,7 @@ function runBundleConformance() {
333
349
  return pass === checks.length;
334
350
  }
335
351
 
336
- function runOperationContractRegressionConformance() {
352
+ async function runOperationContractRegressionConformance() {
337
353
  const checks = [];
338
354
  const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
339
355
 
@@ -342,7 +358,7 @@ function runOperationContractRegressionConformance() {
342
358
  try {
343
359
  const engine = createEngine({ registryDir: REGISTRY_DIR, leaseStore });
344
360
  const workflowPath = 'workflows/nested.md';
345
- const createRes = engine.processOperation({
361
+ const createRes = await engine.processOperation({
346
362
  type: 'operation',
347
363
  idempotency_key: 'nested-create',
348
364
  workspace_id: 'ws-reg',
@@ -368,7 +384,7 @@ function runOperationContractRegressionConformance() {
368
384
  }, vault);
369
385
  check('nested frontmatter fixture commits before patch', createRes.success === true, JSON.stringify(createRes.validation?.errors || []));
370
386
 
371
- const patchRes = engine.processOperation({
387
+ const patchRes = await engine.processOperation({
372
388
  type: 'patch',
373
389
  idempotency_key: 'nested-patch',
374
390
  workspace_id: 'ws-reg',
@@ -398,13 +414,13 @@ function runOperationContractRegressionConformance() {
398
414
  actor: { role: 'system' },
399
415
  content: '---\ntype: rule\ntitle: Leased\ndescription: Lease test.\ntimestamp: 2026-07-02T00:00:00Z\nname: Leased\n---\nBody.\n',
400
416
  };
401
- const missingLease = engine.processOperation(leasedEnvelope, vault);
417
+ const missingLease = await engine.processOperation(leasedEnvelope, vault);
402
418
  check('lease conflict rejects missing lease_id', missingLease.success === false && missingLease.validation.errors.some((e) => e.includes('leased')));
403
419
 
404
- const mismatchLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-mismatch', lease_id: 'wrong' }, vault);
420
+ const mismatchLease = await engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-mismatch', lease_id: 'wrong' }, vault);
405
421
  check('lease conflict rejects mismatched lease_id', mismatchLease.success === false && mismatchLease.validation.errors.some((e) => e.includes('Lease mismatch')));
406
422
 
407
- const matchedLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-match', lease_id: 'lease-ok' }, vault);
423
+ const matchedLease = await engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-match', lease_id: 'lease-ok' }, vault);
408
424
  check('lease allows matching lease_id', matchedLease.success === true, JSON.stringify(matchedLease.validation?.errors || []));
409
425
 
410
426
  const expiredTarget = 'rules/expired.md';
@@ -413,13 +429,13 @@ function runOperationContractRegressionConformance() {
413
429
  lease_id: 'expired',
414
430
  expires_at: '2000-01-01T00:00:00.000Z',
415
431
  }));
416
- const expiredLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-expired', path: expiredTarget }, vault);
432
+ const expiredLease = await engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-expired', path: expiredTarget }, vault);
417
433
  check('expired lease does not block writes', expiredLease.success === true, JSON.stringify(expiredLease.validation?.errors || []));
418
434
 
419
435
  const unreadableTarget = 'rules/unreadable.md';
420
436
  const unreadablePathKey = crypto.createHash('sha256').update(unreadableTarget).digest('hex');
421
437
  fs.writeFileSync(path.join(leaseDir, `${unreadablePathKey}.lease.json`), '{not json');
422
- const unreadableLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-unreadable', path: unreadableTarget }, vault);
438
+ const unreadableLease = await engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-unreadable', path: unreadableTarget }, vault);
423
439
  check('unreadable lease state fails closed', unreadableLease.success === false && unreadableLease.validation.errors.some((e) => e.includes('unreadable')));
424
440
  } finally {
425
441
  fs.rmSync(vault, { recursive: true, force: true });
@@ -435,160 +451,77 @@ function runOperationContractRegressionConformance() {
435
451
  return pass === checks.length;
436
452
  }
437
453
 
438
- function runSemanticLocalizationConformance() {
454
+ async function runSemanticLocalizationConformance() {
439
455
  const checks = [];
440
456
  const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
441
457
  const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-semantic-'));
442
- const projection = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-locale-'));
443
458
  try {
444
459
  const engine = createEngine({ registryDir: REGISTRY_DIR });
445
460
  const sourcePath = 'rules/refund-policy.md';
446
461
  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
+ '---', 'type: rule', 'title: Política de reembolsos',
463
+ 'description: Explica cuándo están disponibles los reembolsos de boletos.',
464
+ 'timestamp: 2026-07-10T00:00:00Z', 'name: Refund Policy', 'language: es',
465
+ 'aliases: [términos de reembolso, política de cancelación]', 'tags: [boletos, finanzas]',
466
+ 'relations:', ' - relation: applies_to', ' target: workflows/box-office.md',
467
+ '---', '', 'Los reembolsos están disponibles hasta catorce días antes del evento.', '',
462
468
  ].join('\n');
463
- const sourceWrite = engine.processOperation({
469
+ const sourceWrite = await engine.processOperation({
464
470
  type: 'operation', idempotency_key: 'sem-source', workspace_id: 'ws-sem',
465
471
  path: sourcePath, content: sourceContent, actor: { role: 'system' },
466
472
  }, vault);
467
- check('structural semantic source commits', sourceWrite.success === true,
468
- JSON.stringify(sourceWrite.validation?.errors || []));
473
+ check('non-English structural source commits', sourceWrite.success === true, JSON.stringify(sourceWrite.validation?.errors || []));
469
474
 
470
475
  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
- '',
476
+ '---', 'type: task', 'title: Solicitud privada', 'description: Datos privados del cliente.',
477
+ 'timestamp: 2026-07-10T00:00:00Z', 'priority: high', 'category: support', 'status: pending',
478
+ '---', '', 'Cuenta y detalles de pago.', '',
483
479
  ].join('\n');
484
- const privateWrite = engine.processOperation({
480
+ const privateWrite = await engine.processOperation({
485
481
  type: 'operation', idempotency_key: 'sem-private', workspace_id: 'ws-sem',
486
482
  path: 'tasks/private-refund.md', content: privateContent, actor: { role: 'system' },
487
483
  }, 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 || []));
484
+ check('tenant-private semantic source commits', privateWrite.success === true, JSON.stringify(privateWrite.validation?.errors || []));
538
485
 
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');
486
+ const first = buildSemanticIndex(vault, { registryDir: REGISTRY_DIR });
487
+ const second = buildSemanticIndex(vault, { registryDir: REGISTRY_DIR });
488
+ check('semantic index is deterministic', first.index_hash === second.index_hash);
551
489
  check('safe semantic default excludes tenant-private documents',
552
490
  first.documents.every((document) => document.portability === 'structural') &&
553
491
  !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 }))));
492
+ check('semantic graph captures explicit relations', first.edges.some((edge) => edge.source === sourcePath && edge.relation === 'applies_to'));
493
+ const lexical = searchSemanticIndex(first, 'reembolsos boletos', { limit: 3 });
494
+ check('lexical evidence ranks same-language content', lexical[0]?.document.path === sourcePath && lexical[0].evidence.lexical > 0);
495
+
496
+ const embed = async (texts) => texts.map((text) => /refund|reembols/i.test(text) ? [1, 0] : [0, 1]);
497
+ const enriched = await enrichSemanticIndex(first, { embed, model: 'conformance-multilingual-v1' });
498
+ check('embedding provenance records model and dimension', enriched.embedding.model === 'conformance-multilingual-v1' && enriched.embedding.dimension === 2);
499
+ const crossLanguage = await searchSemanticIndexWithAdapter(enriched, 'refund policy', { embed, limit: 3 });
500
+ check('cross-language semantic evidence ranks the authored source',
501
+ crossLanguage[0]?.document.path === sourcePath && crossLanguage[0].evidence.semantic > 0,
502
+ JSON.stringify(crossLanguage.map((match) => ({ path: match.document.path, evidence: match.evidence }))));
503
+
504
+ const rendered = await renderSemanticRecord(first.documents[0], {
505
+ language: 'ja', render: async () => ({ title: '返金ポリシー', description: 'チケット返金の条件。', body: 'イベントの14日前まで返金できます。' }),
506
+ });
507
+ check('runtime rendering changes presentation without changing controls',
508
+ rendered.presentation.language === 'ja' && rendered.presentation.title === '返金ポリシー' &&
509
+ rendered.invariant.type === first.documents[0].type && rendered.invariant.path === sourcePath);
510
+ let forbiddenRejected = false;
511
+ try { await renderSemanticRecord(first.documents[0], { language: 'fr', render: async () => ({ type: 'assistant', title: 'Politique' }) }); }
512
+ catch (error) { forbiddenRejected = error.message.includes('forbidden field'); }
513
+ check('renderer cannot change symbolic controls', forbiddenRejected);
560
514
 
561
515
  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');
516
+ check('private semantic indexing requires explicit opt-in', privateIndex.documents.some((document) => document.path === 'tasks/private-refund.md'));
517
+
518
+ fs.writeFileSync(path.join(vault, 'rules', 'invalid-raw.md'), '---\ntype: rule\nname: Invalid\n---\nBody.\n');
585
519
  let invalidRawRejected = false;
586
520
  try { buildSemanticIndex(vault, { registryDir: REGISTRY_DIR }); }
587
521
  catch (error) { invalidRawRejected = error.message.includes("Missing required field 'title'"); }
588
522
  check('semantic projection rejects raw documents that violate the registry', invalidRawRejected);
589
523
  } finally {
590
524
  fs.rmSync(vault, { recursive: true, force: true });
591
- fs.rmSync(projection, { recursive: true, force: true });
592
525
  }
593
526
 
594
527
  let pass = 0;
@@ -596,7 +529,7 @@ function runSemanticLocalizationConformance() {
596
529
  if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
597
530
  else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
598
531
  }
599
- console.log(`\n ${pass}/${checks.length} semantic/localization checks passed (§11.9)`);
532
+ console.log(`\n ${pass}/${checks.length} semantic/multilingual rendering checks passed (§11.9)`);
600
533
  return pass === checks.length;
601
534
  }
602
535
 
@@ -688,7 +621,7 @@ function runRegistryExtensionConformance() {
688
621
  return pass === checks.length;
689
622
  }
690
623
 
691
- function runCliSmokeConformance() {
624
+ async function runCliSmokeConformance() {
692
625
  const checks = [];
693
626
  const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
694
627
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-cli-smoke-'));
@@ -708,7 +641,7 @@ function runCliSmokeConformance() {
708
641
  const failures = [];
709
642
  for (const abs of files) {
710
643
  const rel = path.relative(vault, abs);
711
- const res = engine.processOperation({
644
+ const res = await engine.processOperation({
712
645
  type: 'operation',
713
646
  idempotency_key: `scaffold-${rel.replace(/[^A-Za-z0-9._-]/g, '_')}`,
714
647
  workspace_id: 'ws-scaffold',
@@ -732,21 +665,6 @@ function runCliSmokeConformance() {
732
665
  semanticOutput.results?.some((result) => result.document.path === 'rules/welcome.md'),
733
666
  JSON.stringify(semanticOutput.results || []));
734
667
 
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
-
750
668
  const dryRunVault = path.join(tmp, 'dry-run-vault');
751
669
  const dryRunOut = execFileSync('node', [
752
670
  path.join(__dirname, 'ssss.mjs'),
@@ -773,6 +691,53 @@ function runCliSmokeConformance() {
773
691
  check('ssss import --dry-run reports would-commit counts and leaves target empty',
774
692
  dryRunOut.includes('would commit') && dryRunFiles.length === 0,
775
693
  dryRunOut.trim());
694
+
695
+ const primitiveOut = JSON.parse(execFileSync('node', [
696
+ path.join(__dirname, 'ssss.mjs'),
697
+ 'primitive', 'create',
698
+ '--namespace', 'acme',
699
+ '--name', '顧客予約',
700
+ '--language', 'ja',
701
+ '--fields', JSON.stringify([{ id: 'status', name: '状態', kind: 'enum', values: ['pending', 'confirmed'], required: true }]),
702
+ ], { encoding: 'utf8' }));
703
+ check('ssss primitive create emits a stable qualified identity',
704
+ /^acme:p_[a-f0-9]{20}$/.test(primitiveOut.primitive_id),
705
+ JSON.stringify(primitiveOut));
706
+
707
+ const defPath = path.join(tmp, 'definition.json');
708
+ fs.writeFileSync(defPath, `${JSON.stringify(primitiveOut)}\n`);
709
+ const inspectOut = JSON.parse(execFileSync('node', [
710
+ path.join(__dirname, 'ssss.mjs'),
711
+ 'primitive', 'inspect',
712
+ defPath,
713
+ ], { encoding: 'utf8' }));
714
+ const extensionPath = path.join(tmp, 'extension.json');
715
+ fs.writeFileSync(extensionPath, `${JSON.stringify(inspectOut.registry)}\n`);
716
+ const lockOut = JSON.parse(execFileSync('node', [
717
+ path.join(__dirname, 'ssss.mjs'),
718
+ 'registry', 'lock',
719
+ '--extension', extensionPath,
720
+ ], { encoding: 'utf8' }));
721
+ check('ssss registry lock emits integrity metadata',
722
+ typeof lockOut.integrity === 'string' && lockOut.integrity.startsWith('sha256:'),
723
+ JSON.stringify(lockOut));
724
+
725
+ const adapterOut = JSON.parse(execFileSync('node', [
726
+ path.join(__dirname, 'ssss.mjs'),
727
+ 'adapter', 'conformance',
728
+ ], { encoding: 'utf8' }));
729
+ check('ssss adapter conformance report is machine-readable and green',
730
+ adapterOut.passed === true && adapterOut.suites?.vfs_memory?.passed === true,
731
+ JSON.stringify(adapterOut));
732
+
733
+ const migrateOut = JSON.parse(execFileSync('node', [
734
+ path.join(__dirname, 'ssss.mjs'),
735
+ 'migrate', '0.8-to-0.9',
736
+ vault,
737
+ ], { encoding: 'utf8' }));
738
+ check('ssss migrate 0.8-to-0.9 dry-run emits backup manifest without writes',
739
+ migrateOut.dry_run === true && migrateOut.writes_performed === 0 && Array.isArray(migrateOut.backup_manifest),
740
+ JSON.stringify(migrateOut));
776
741
  } finally {
777
742
  fs.rmSync(tmp, { recursive: true, force: true });
778
743
  }
@@ -791,7 +756,7 @@ function runCliSmokeConformance() {
791
756
  * workflow frontmatter as the canonical schedule source, then derive trigger,
792
757
  * task, and run envelopes without introducing a scheduler database of record.
793
758
  */
794
- function runRuntimeConformance() {
759
+ async function runRuntimeConformance() {
795
760
  const checks = [];
796
761
  const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
797
762
 
@@ -854,15 +819,15 @@ function runRuntimeConformance() {
854
819
  const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-runtime-'));
855
820
  try {
856
821
  const engine = createEngine({ registryDir: REGISTRY_DIR });
857
- const eventRes = engine.processOperation(firstPlan.envelopes[0], vault);
858
- const taskRes = engine.processOperation(firstPlan.envelopes[1], vault);
822
+ const eventRes = await engine.processOperation(firstPlan.envelopes[0], vault);
823
+ const taskRes = await engine.processOperation(firstPlan.envelopes[1], vault);
859
824
  check('trigger event commits through Operation Contract', eventRes.success === true,
860
825
  JSON.stringify(eventRes.validation?.errors || []));
861
826
  check('instantiated task commits through Operation Contract', taskRes.success === true,
862
827
  JSON.stringify(taskRes.validation?.errors || []));
863
828
 
864
- const replayEvent = engine.processOperation(secondPlan.envelopes[0], vault);
865
- const replayTask = engine.processOperation(secondPlan.envelopes[1], vault);
829
+ const replayEvent = await engine.processOperation(secondPlan.envelopes[0], vault);
830
+ const replayTask = await engine.processOperation(secondPlan.envelopes[1], vault);
866
831
  check('duplicate daemon tick replays trigger event idempotently', replayEvent.replay != null);
867
832
  check('duplicate daemon tick replays task write idempotently', replayTask.replay != null);
868
833
 
@@ -873,7 +838,7 @@ function runRuntimeConformance() {
873
838
  status: 'queued',
874
839
  startedAt: scheduledFor,
875
840
  });
876
- const runRes = engine.processOperation(runEnvelope, vault);
841
+ const runRes = await engine.processOperation(runEnvelope, vault);
877
842
  check('worker-created run commits through Operation Contract', runRes.success === true,
878
843
  JSON.stringify(runRes.validation?.errors || []));
879
844
  } finally {
@@ -953,22 +918,24 @@ async function main() {
953
918
  process.exit(ok ? 0 : 1);
954
919
  } else if (opts.engine) {
955
920
  console.log('\nRunning fixtures through the reference engine (src/engine.mjs) ...');
956
- const engineOk = runAgainstEngine(doc);
921
+ const engineOk = await runAgainstEngine(doc);
957
922
  console.log('\nRunning workflow runtime conformance (src/runtime.mjs, §11.8) ...');
958
- const runtimeOk = runRuntimeConformance();
923
+ const runtimeOk = await runRuntimeConformance();
959
924
  console.log('\nRunning operation regression conformance (src/engine.mjs, §6/§7) ...');
960
- const operationRegressionOk = runOperationContractRegressionConformance();
925
+ const operationRegressionOk = await runOperationContractRegressionConformance();
961
926
  console.log('\nRunning extension-registry conformance (src/registry.mjs) ...');
962
927
  const extensionRegistryOk = runRegistryExtensionConformance();
963
- console.log('\nRunning semantic/localization conformance (src/semantic.mjs, §11.9) ...');
964
- const semanticOk = runSemanticLocalizationConformance();
928
+ console.log('\nRunning semantic/multilingual rendering conformance (src/semantic.mjs, §11.9) ...');
929
+ const semanticOk = await runSemanticLocalizationConformance();
930
+ console.log('\nRunning SSSS 0.9 kernel/adapter/UI conformance ...');
931
+ const kernel09Ok = await runKernel09Conformance();
965
932
  console.log('\nRunning bundle/provisioning conformance (src/bundle.mjs, §16/§17) ...');
966
- const bundleOk = runBundleConformance();
933
+ const bundleOk = await runBundleConformance();
967
934
  console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
968
- const cliSmokeOk = runCliSmokeConformance();
935
+ const cliSmokeOk = await runCliSmokeConformance();
969
936
  process.exit(
970
937
  engineOk && runtimeOk && operationRegressionOk && extensionRegistryOk &&
971
- semanticOk && bundleOk && cliSmokeOk ? 0 : 1
938
+ semanticOk && kernel09Ok && bundleOk && cliSmokeOk ? 0 : 1
972
939
  );
973
940
  } else {
974
941
  console.log('\nℹ No --endpoint/--engine given; ran structural + registry validation only.');
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate a minimal SPDX-lite SBOM for @gregiteen/ssss-cli publish artifacts.
4
+ * Dependency-free: packages only what is declared in package.json (zero runtime deps).
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import crypto from 'node:crypto';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
13
+
14
+ function walk(dir, out = []) {
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
16
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
17
+ const abs = path.join(dir, entry.name);
18
+ if (entry.isDirectory()) walk(abs, out);
19
+ else if (entry.isFile()) out.push(abs);
20
+ }
21
+ return out;
22
+ }
23
+
24
+ const files = walk(root).filter(
25
+ (f) =>
26
+ !f.includes(`${path.sep}artifacts${path.sep}`) &&
27
+ !f.endsWith('package-lock.json'),
28
+ );
29
+
30
+ const packages = [
31
+ {
32
+ name: pkg.name,
33
+ version: pkg.version,
34
+ downloadLocation: `https://registry.npmjs.org/${pkg.name}/-/${pkg.name.split('/').pop()}-${pkg.version}.tgz`,
35
+ filesAnalyzed: true,
36
+ licenseConcluded: pkg.license || 'NOASSERTION',
37
+ },
38
+ ];
39
+
40
+ // Zero runtime dependencies by design — still list optional peer/dev if present
41
+ for (const [section, deps] of Object.entries({
42
+ dependencies: pkg.dependencies || {},
43
+ optionalDependencies: pkg.optionalDependencies || {},
44
+ })) {
45
+ for (const [name, version] of Object.entries(deps)) {
46
+ packages.push({
47
+ name,
48
+ version: String(version).replace(/^[\^~]/, ''),
49
+ relationship: section,
50
+ });
51
+ }
52
+ }
53
+
54
+ const fileEntries = files.map((abs) => {
55
+ const rel = path.relative(root, abs).split(path.sep).join('/');
56
+ const buf = fs.readFileSync(abs);
57
+ return {
58
+ fileName: rel,
59
+ checksums: [
60
+ {
61
+ algorithm: 'SHA256',
62
+ checksumValue: crypto.createHash('sha256').update(buf).digest('hex'),
63
+ },
64
+ ],
65
+ };
66
+ });
67
+
68
+ const sbom = {
69
+ spdxVersion: 'SPDX-2.3',
70
+ dataLicense: 'CC0-1.0',
71
+ SPDXID: 'SPDXRef-DOCUMENT',
72
+ name: `${pkg.name}-${pkg.version}`,
73
+ documentNamespace: `https://github.com/gregiteen/ssss/sbom/${pkg.version}`,
74
+ creationInfo: {
75
+ created: new Date().toISOString(),
76
+ creators: ['Tool: ssss-generate-sbom'],
77
+ },
78
+ packages,
79
+ files: fileEntries,
80
+ relationships: [
81
+ {
82
+ spdxElementId: 'SPDXRef-DOCUMENT',
83
+ relationshipType: 'DESCRIBES',
84
+ relatedSpdxElement: 'SPDXRef-Package',
85
+ },
86
+ ],
87
+ comment:
88
+ 'SSSS reference package is intentionally dependency-free at runtime. Host apps supply adapters.',
89
+ };
90
+
91
+ const outDir = path.join(root, 'artifacts');
92
+ fs.mkdirSync(outDir, { recursive: true });
93
+ const outFile = path.join(outDir, `sbom-${pkg.version}.spdx.json`);
94
+ fs.writeFileSync(outFile, `${JSON.stringify(sbom, null, 2)}\n`);
95
+ console.log(`SBOM written: ${outFile}`);
96
+ console.log(`Packages: ${packages.length}, files hashed: ${fileEntries.length}`);