@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.
- package/CHANGELOG.md +42 -4
- package/README.md +20 -4
- package/VERSION +1 -1
- package/conformance/README.md +11 -7
- package/conformance/fixtures.json +68 -2
- package/conformance/reference-bundle.ucw.json +21 -4
- package/docs/help/bundle.md +6 -2
- package/docs/help/import.md +2 -0
- package/docs/help/localization.md +43 -0
- package/docs/help/overview.md +8 -4
- package/docs/help/semantic.md +37 -0
- package/docs/ssss-spec.md +112 -7
- package/package.json +8 -3
- package/registry/core.json +43 -3
- package/scripts/audit-registry-field-usage.mjs +10 -1
- package/scripts/build-reference-bundle.mjs +24 -1
- package/scripts/cmd-export.mjs +3 -3
- package/scripts/cmd-localize.mjs +44 -0
- package/scripts/cmd-semantic.mjs +88 -0
- package/scripts/conformance.mjs +374 -2
- package/scripts/ssss.mjs +4 -0
- package/skills/ssss/SKILL.md +225 -0
- package/skills/ssss/evals/evals.json +37 -0
- package/skills/ssss/references/contract-reference.md +96 -0
- package/skills/ssss/scripts/validate-schema.mjs +258 -0
- package/skills/ssss/subagents/audit-ssss-contract.md +22 -0
- package/src/bundle.mjs +170 -34
- package/src/engine.mjs +78 -4
- package/src/registry.mjs +246 -6
- package/src/semantic.mjs +402 -0
package/scripts/conformance.mjs
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
|