@gregiteen/ssss-cli 0.7.0 → 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 +63 -4
- package/README.md +29 -13
- package/VERSION +1 -1
- package/conformance/README.md +17 -11
- package/conformance/fixtures.json +68 -2
- package/conformance/reference-bundle.ucw.json +39 -5
- package/docs/help/bundle.md +6 -2
- package/docs/help/import.md +6 -2
- package/docs/help/leases.md +31 -0
- package/docs/help/localization.md +43 -0
- package/docs/help/overview.md +9 -5
- package/docs/help/portability.md +11 -3
- package/docs/help/runtime.md +3 -3
- package/docs/help/scaffold.md +2 -2
- 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 +25 -2
- package/scripts/cmd-export.mjs +3 -3
- package/scripts/cmd-import.mjs +5 -1
- package/scripts/cmd-localize.mjs +44 -0
- package/scripts/cmd-new.mjs +11 -11
- package/scripts/cmd-semantic.mjs +88 -0
- package/scripts/conformance.mjs +574 -2
- package/scripts/ssss.mjs +6 -2
- 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 +266 -41
- package/src/engine.mjs +78 -4
- package/src/frontmatter.mjs +141 -38
- package/src/registry.mjs +249 -7
- package/src/semantic.mjs +402 -0
package/scripts/conformance.mjs
CHANGED
|
@@ -14,11 +14,21 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
17
19
|
import { fileURLToPath } from 'node:url';
|
|
18
20
|
import { createEngine } from '../src/engine.mjs';
|
|
19
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_EXPORTER,
|
|
23
|
+
exportBundle,
|
|
24
|
+
validateBundle,
|
|
25
|
+
provisionBundle,
|
|
26
|
+
importBundle,
|
|
27
|
+
} from '../src/bundle.mjs';
|
|
20
28
|
import { parseDocument } from '../src/frontmatter.mjs';
|
|
29
|
+
import { documentHash, loadRegistries } from '../src/registry.mjs';
|
|
21
30
|
import { createRunEnvelope, planWorkflowTrigger } from '../src/runtime.mjs';
|
|
31
|
+
import { buildSemanticIndex, materializeLocale, searchSemanticIndex } from '../src/semantic.mjs';
|
|
22
32
|
import { auditRegistryFieldUsage } from './audit-registry-field-usage.mjs';
|
|
23
33
|
import { validateSkills } from './validate-skills.mjs';
|
|
24
34
|
|
|
@@ -26,6 +36,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
26
36
|
const FIXTURES = path.resolve(__dirname, '..', 'conformance', 'fixtures.json');
|
|
27
37
|
const REFERENCE_BUNDLE = path.resolve(__dirname, '..', 'conformance', 'reference-bundle.ucw.json');
|
|
28
38
|
const REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
|
|
39
|
+
const PACKAGE = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
29
40
|
const PORTABILITY_CLASSES = ['structural', 'tenant_private', 'resource_bound'];
|
|
30
41
|
|
|
31
42
|
function parseArgs(argv) {
|
|
@@ -192,8 +203,72 @@ function runBundleConformance() {
|
|
|
192
203
|
|
|
193
204
|
const { valid, errors } = validateBundle(bundle, { registryDir: REGISTRY_DIR });
|
|
194
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
|
+
}
|
|
195
232
|
check('sale profile carries no tenant_private file',
|
|
196
233
|
bundle.files.every((f) => parseDocument(f.content).data.type !== 'task' && parseDocument(f.content).data.type !== 'conversation'));
|
|
234
|
+
check('sale profile reduces resource_bound fields to requirement declarations',
|
|
235
|
+
bundle.files.every((f) => {
|
|
236
|
+
const data = parseDocument(f.content).data;
|
|
237
|
+
if (data.type !== 'domain') return true;
|
|
238
|
+
return data.domain_name === 'REQUIREMENT' && data.registrar === 'REQUIREMENT';
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
const missingFilesBundle = JSON.parse(JSON.stringify(bundle));
|
|
242
|
+
delete missingFilesBundle.files;
|
|
243
|
+
const missingFilesValidation = validateBundle(missingFilesBundle, { registryDir: REGISTRY_DIR });
|
|
244
|
+
check('bundle validation rejects missing files array',
|
|
245
|
+
!missingFilesValidation.valid && missingFilesValidation.errors.some((e) => e.includes('bundle.files must be an array')),
|
|
246
|
+
missingFilesValidation.errors.join('; '));
|
|
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
|
+
|
|
256
|
+
const unknownExtensionBundle = JSON.parse(JSON.stringify(bundle));
|
|
257
|
+
unknownExtensionBundle.manifest.required_extensions = ['missing-extension'];
|
|
258
|
+
const unknownExtensionValidation = validateBundle(unknownExtensionBundle, { registryDir: REGISTRY_DIR });
|
|
259
|
+
check('bundle validation rejects unknown required_extensions entries',
|
|
260
|
+
!unknownExtensionValidation.valid && unknownExtensionValidation.errors.some((e) => e.includes("required extension 'missing-extension'")),
|
|
261
|
+
unknownExtensionValidation.errors.join('; '));
|
|
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('; '));
|
|
197
272
|
|
|
198
273
|
const prov = provisionBundle(bundle, { workspaceId: 'ws-conf', parameters: { business_name: 'Demo', domain: 'demo.example' } });
|
|
199
274
|
check('provision resolves params + link integrity (no dangling [[links]])', prov.ok,
|
|
@@ -216,6 +291,39 @@ function runBundleConformance() {
|
|
|
216
291
|
fs.rmSync(vault, { recursive: true, force: true });
|
|
217
292
|
}
|
|
218
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
|
+
|
|
219
327
|
let pass = 0;
|
|
220
328
|
for (const c of checks) {
|
|
221
329
|
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
@@ -225,6 +333,459 @@ function runBundleConformance() {
|
|
|
225
333
|
return pass === checks.length;
|
|
226
334
|
}
|
|
227
335
|
|
|
336
|
+
function runOperationContractRegressionConformance() {
|
|
337
|
+
const checks = [];
|
|
338
|
+
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
339
|
+
|
|
340
|
+
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-op-reg-'));
|
|
341
|
+
const leaseStore = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-lease-'));
|
|
342
|
+
try {
|
|
343
|
+
const engine = createEngine({ registryDir: REGISTRY_DIR, leaseStore });
|
|
344
|
+
const workflowPath = 'workflows/nested.md';
|
|
345
|
+
const createRes = engine.processOperation({
|
|
346
|
+
type: 'operation',
|
|
347
|
+
idempotency_key: 'nested-create',
|
|
348
|
+
workspace_id: 'ws-reg',
|
|
349
|
+
path: workflowPath,
|
|
350
|
+
actor: { role: 'system' },
|
|
351
|
+
content: [
|
|
352
|
+
'---',
|
|
353
|
+
'type: workflow',
|
|
354
|
+
'title: Nested Workflow',
|
|
355
|
+
'description: Exercises nested frontmatter patch serialization.',
|
|
356
|
+
'timestamp: 2026-07-02T00:00:00Z',
|
|
357
|
+
'name: Nested Workflow',
|
|
358
|
+
'triggers:',
|
|
359
|
+
' - type: cron',
|
|
360
|
+
' id: morning',
|
|
361
|
+
' cron: "0 8 * * *"',
|
|
362
|
+
' timezone: "America/Denver"',
|
|
363
|
+
'---',
|
|
364
|
+
'',
|
|
365
|
+
'Body.',
|
|
366
|
+
'',
|
|
367
|
+
].join('\n'),
|
|
368
|
+
}, vault);
|
|
369
|
+
check('nested frontmatter fixture commits before patch', createRes.success === true, JSON.stringify(createRes.validation?.errors || []));
|
|
370
|
+
|
|
371
|
+
const patchRes = engine.processOperation({
|
|
372
|
+
type: 'patch',
|
|
373
|
+
idempotency_key: 'nested-patch',
|
|
374
|
+
workspace_id: 'ws-reg',
|
|
375
|
+
path: workflowPath,
|
|
376
|
+
actor: { role: 'system' },
|
|
377
|
+
patches: { description: 'Patched without dropping triggers.' },
|
|
378
|
+
}, vault);
|
|
379
|
+
const patched = fs.readFileSync(path.join(vault, workflowPath), 'utf8');
|
|
380
|
+
const patchedData = parseDocument(patched).data;
|
|
381
|
+
check('patch preserves nested frontmatter arrays of maps',
|
|
382
|
+
patchRes.success === true && Array.isArray(patchedData.triggers) && patchedData.triggers[0]?.cron === '0 8 * * *',
|
|
383
|
+
patched);
|
|
384
|
+
|
|
385
|
+
const leaseTarget = 'rules/leased.md';
|
|
386
|
+
const leasePathKey = crypto.createHash('sha256').update(leaseTarget).digest('hex');
|
|
387
|
+
const leaseDir = path.join(leaseStore, 'ws-reg');
|
|
388
|
+
fs.mkdirSync(leaseDir, { recursive: true });
|
|
389
|
+
fs.writeFileSync(path.join(leaseDir, `${leasePathKey}.lease.json`), JSON.stringify({
|
|
390
|
+
lease_id: 'lease-ok',
|
|
391
|
+
expires_at: '2999-01-01T00:00:00.000Z',
|
|
392
|
+
}));
|
|
393
|
+
const leasedEnvelope = {
|
|
394
|
+
type: 'operation',
|
|
395
|
+
idempotency_key: 'lease-missing',
|
|
396
|
+
workspace_id: 'ws-reg',
|
|
397
|
+
path: leaseTarget,
|
|
398
|
+
actor: { role: 'system' },
|
|
399
|
+
content: '---\ntype: rule\ntitle: Leased\ndescription: Lease test.\ntimestamp: 2026-07-02T00:00:00Z\nname: Leased\n---\nBody.\n',
|
|
400
|
+
};
|
|
401
|
+
const missingLease = engine.processOperation(leasedEnvelope, vault);
|
|
402
|
+
check('lease conflict rejects missing lease_id', missingLease.success === false && missingLease.validation.errors.some((e) => e.includes('leased')));
|
|
403
|
+
|
|
404
|
+
const mismatchLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-mismatch', lease_id: 'wrong' }, vault);
|
|
405
|
+
check('lease conflict rejects mismatched lease_id', mismatchLease.success === false && mismatchLease.validation.errors.some((e) => e.includes('Lease mismatch')));
|
|
406
|
+
|
|
407
|
+
const matchedLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-match', lease_id: 'lease-ok' }, vault);
|
|
408
|
+
check('lease allows matching lease_id', matchedLease.success === true, JSON.stringify(matchedLease.validation?.errors || []));
|
|
409
|
+
|
|
410
|
+
const expiredTarget = 'rules/expired.md';
|
|
411
|
+
const expiredPathKey = crypto.createHash('sha256').update(expiredTarget).digest('hex');
|
|
412
|
+
fs.writeFileSync(path.join(leaseDir, `${expiredPathKey}.lease.json`), JSON.stringify({
|
|
413
|
+
lease_id: 'expired',
|
|
414
|
+
expires_at: '2000-01-01T00:00:00.000Z',
|
|
415
|
+
}));
|
|
416
|
+
const expiredLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-expired', path: expiredTarget }, vault);
|
|
417
|
+
check('expired lease does not block writes', expiredLease.success === true, JSON.stringify(expiredLease.validation?.errors || []));
|
|
418
|
+
|
|
419
|
+
const unreadableTarget = 'rules/unreadable.md';
|
|
420
|
+
const unreadablePathKey = crypto.createHash('sha256').update(unreadableTarget).digest('hex');
|
|
421
|
+
fs.writeFileSync(path.join(leaseDir, `${unreadablePathKey}.lease.json`), '{not json');
|
|
422
|
+
const unreadableLease = engine.processOperation({ ...leasedEnvelope, idempotency_key: 'lease-unreadable', path: unreadableTarget }, vault);
|
|
423
|
+
check('unreadable lease state fails closed', unreadableLease.success === false && unreadableLease.validation.errors.some((e) => e.includes('unreadable')));
|
|
424
|
+
} finally {
|
|
425
|
+
fs.rmSync(vault, { recursive: true, force: true });
|
|
426
|
+
fs.rmSync(leaseStore, { recursive: true, force: true });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let pass = 0;
|
|
430
|
+
for (const c of checks) {
|
|
431
|
+
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
432
|
+
else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
433
|
+
}
|
|
434
|
+
console.log(`\n ${pass}/${checks.length} operation regression checks passed (§6/§7)`);
|
|
435
|
+
return pass === checks.length;
|
|
436
|
+
}
|
|
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
|
+
|
|
691
|
+
function runCliSmokeConformance() {
|
|
692
|
+
const checks = [];
|
|
693
|
+
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
694
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-cli-smoke-'));
|
|
695
|
+
try {
|
|
696
|
+
const starter = path.join(tmp, 'starter');
|
|
697
|
+
execFileSync('node', [path.join(__dirname, 'ssss.mjs'), 'new', starter, '--no-git'], { stdio: 'ignore' });
|
|
698
|
+
const vault = path.join(starter, 'vault');
|
|
699
|
+
const files = [];
|
|
700
|
+
(function walk(dir) {
|
|
701
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
702
|
+
const abs = path.join(dir, entry.name);
|
|
703
|
+
if (entry.isDirectory()) walk(abs);
|
|
704
|
+
else if (entry.name.endsWith('.md')) files.push(abs);
|
|
705
|
+
}
|
|
706
|
+
})(vault);
|
|
707
|
+
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
708
|
+
const failures = [];
|
|
709
|
+
for (const abs of files) {
|
|
710
|
+
const rel = path.relative(vault, abs);
|
|
711
|
+
const res = engine.processOperation({
|
|
712
|
+
type: 'operation',
|
|
713
|
+
idempotency_key: `scaffold-${rel.replace(/[^A-Za-z0-9._-]/g, '_')}`,
|
|
714
|
+
workspace_id: 'ws-scaffold',
|
|
715
|
+
path: rel,
|
|
716
|
+
content: fs.readFileSync(abs, 'utf8'),
|
|
717
|
+
dry_run: true,
|
|
718
|
+
actor: { role: 'system' },
|
|
719
|
+
}, vault);
|
|
720
|
+
if (!res.success) failures.push({ rel, errors: res.validation?.errors || [] });
|
|
721
|
+
}
|
|
722
|
+
check('ssss new starter vault files validate against the registry', files.length > 0 && failures.length === 0, JSON.stringify(failures));
|
|
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
|
+
|
|
750
|
+
const dryRunVault = path.join(tmp, 'dry-run-vault');
|
|
751
|
+
const dryRunOut = execFileSync('node', [
|
|
752
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
753
|
+
'import',
|
|
754
|
+
REFERENCE_BUNDLE,
|
|
755
|
+
'--vault',
|
|
756
|
+
dryRunVault,
|
|
757
|
+
'--param',
|
|
758
|
+
'business_name=Demo',
|
|
759
|
+
'--param',
|
|
760
|
+
'domain=demo.example',
|
|
761
|
+
'--dry-run',
|
|
762
|
+
], { encoding: 'utf8' });
|
|
763
|
+
const dryRunFiles = [];
|
|
764
|
+
if (fs.existsSync(dryRunVault)) {
|
|
765
|
+
(function walkDryRun(dir) {
|
|
766
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
767
|
+
const abs = path.join(dir, entry.name);
|
|
768
|
+
if (entry.isDirectory()) walkDryRun(abs);
|
|
769
|
+
else if (entry.isFile()) dryRunFiles.push(abs);
|
|
770
|
+
}
|
|
771
|
+
})(dryRunVault);
|
|
772
|
+
}
|
|
773
|
+
check('ssss import --dry-run reports would-commit counts and leaves target empty',
|
|
774
|
+
dryRunOut.includes('would commit') && dryRunFiles.length === 0,
|
|
775
|
+
dryRunOut.trim());
|
|
776
|
+
} finally {
|
|
777
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
let pass = 0;
|
|
781
|
+
for (const c of checks) {
|
|
782
|
+
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
783
|
+
else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
784
|
+
}
|
|
785
|
+
console.log(`\n ${pass}/${checks.length} CLI smoke checks passed`);
|
|
786
|
+
return pass === checks.length;
|
|
787
|
+
}
|
|
788
|
+
|
|
228
789
|
/**
|
|
229
790
|
* Runtime conformance (spec §11.8). Proves that a conformant daemon can treat
|
|
230
791
|
* workflow frontmatter as the canonical schedule source, then derive trigger,
|
|
@@ -395,9 +956,20 @@ async function main() {
|
|
|
395
956
|
const engineOk = runAgainstEngine(doc);
|
|
396
957
|
console.log('\nRunning workflow runtime conformance (src/runtime.mjs, §11.8) ...');
|
|
397
958
|
const runtimeOk = runRuntimeConformance();
|
|
959
|
+
console.log('\nRunning operation regression conformance (src/engine.mjs, §6/§7) ...');
|
|
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();
|
|
398
965
|
console.log('\nRunning bundle/provisioning conformance (src/bundle.mjs, §16/§17) ...');
|
|
399
966
|
const bundleOk = runBundleConformance();
|
|
400
|
-
|
|
967
|
+
console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
|
|
968
|
+
const cliSmokeOk = runCliSmokeConformance();
|
|
969
|
+
process.exit(
|
|
970
|
+
engineOk && runtimeOk && operationRegressionOk && extensionRegistryOk &&
|
|
971
|
+
semanticOk && bundleOk && cliSmokeOk ? 0 : 1
|
|
972
|
+
);
|
|
401
973
|
} else {
|
|
402
974
|
console.log('\nℹ No --endpoint/--engine given; ran structural + registry validation only.');
|
|
403
975
|
console.log(' Reference engine: ssss conformance --engine');
|
package/scripts/ssss.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @ssss
|
|
3
|
+
* @gregiteen/ssss-cli dispatcher — the `ssss` command.
|
|
4
4
|
*
|
|
5
5
|
* Routes the first positional argument to a subcommand. The bundle/provisioning
|
|
6
6
|
* commands (export/validate/inspect/provision/import/help) export a `run(argv)`
|
|
@@ -21,7 +21,7 @@ const VERSION = (() => {
|
|
|
21
21
|
catch { return '0.0.0'; }
|
|
22
22
|
})();
|
|
23
23
|
|
|
24
|
-
const TOP_LEVEL_HELP = `@ssss
|
|
24
|
+
const TOP_LEVEL_HELP = `@gregiteen/ssss-cli v${VERSION} — Structured Semantic Syntax System
|
|
25
25
|
|
|
26
26
|
Usage: ssss <command> [options]
|
|
27
27
|
|
|
@@ -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
|
|