@gregiteen/ssss-cli 0.7.1 → 0.9.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 +19 -4
- package/VERSION +1 -1
- package/conformance/README.md +11 -7
- package/conformance/fixtures.json +24 -2
- package/conformance/reference-bundle.ucw.json +2 -2
- package/docs/help/bundle.md +6 -2
- package/docs/help/import.md +2 -0
- package/docs/help/overview.md +8 -4
- package/docs/help/semantic.md +38 -0
- package/docs/ssss-spec.md +67 -7
- package/package.json +27 -3
- package/registry/core.json +36 -3
- package/scripts/audit-registry-field-usage.mjs +10 -1
- package/scripts/build-reference-bundle.mjs +4 -1
- package/scripts/cmd-adapter.mjs +29 -0
- package/scripts/cmd-export.mjs +3 -3
- package/scripts/cmd-import.mjs +1 -1
- package/scripts/cmd-migrate.mjs +26 -0
- package/scripts/cmd-new.mjs +5 -5
- package/scripts/cmd-primitive.mjs +48 -0
- package/scripts/cmd-registry.mjs +25 -0
- package/scripts/cmd-semantic.mjs +86 -0
- package/scripts/conformance-09.mjs +199 -0
- package/scripts/conformance.mjs +320 -28
- package/scripts/ssss.mjs +10 -0
- package/skills/ssss/SKILL.md +221 -0
- package/skills/ssss/evals/evals.json +37 -0
- package/skills/ssss/references/contract-reference.md +95 -0
- package/skills/ssss/scripts/validate-schema.mjs +215 -0
- package/skills/ssss/subagents/audit-ssss-contract.md +22 -0
- package/src/authorization.mjs +61 -0
- package/src/bundle.mjs +171 -35
- package/src/engine.mjs +101 -429
- package/src/events.mjs +90 -0
- package/src/guard.mjs +32 -0
- package/src/http.mjs +26 -0
- package/src/idempotency.mjs +55 -0
- package/src/kernel.mjs +260 -0
- package/src/leases.mjs +125 -0
- package/src/primitive.mjs +227 -0
- package/src/projections.mjs +62 -0
- package/src/registry.mjs +408 -17
- package/src/semantic.mjs +210 -0
- package/src/ui.mjs +100 -0
- package/src/validator.mjs +87 -0
- package/src/vfs.mjs +203 -0
- package/types/kernel.d.ts +48 -0
package/scripts/conformance.mjs
CHANGED
|
@@ -18,16 +18,33 @@ 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
|
+
contentHash,
|
|
24
|
+
exportBundle,
|
|
25
|
+
validateBundle,
|
|
26
|
+
provisionBundle,
|
|
27
|
+
importBundle,
|
|
28
|
+
} from '../src/bundle.mjs';
|
|
22
29
|
import { parseDocument } from '../src/frontmatter.mjs';
|
|
30
|
+
import { loadRegistries } from '../src/registry.mjs';
|
|
23
31
|
import { createRunEnvelope, planWorkflowTrigger } from '../src/runtime.mjs';
|
|
32
|
+
import {
|
|
33
|
+
buildSemanticIndex,
|
|
34
|
+
enrichSemanticIndex,
|
|
35
|
+
renderSemanticRecord,
|
|
36
|
+
searchSemanticIndex,
|
|
37
|
+
searchSemanticIndexWithAdapter,
|
|
38
|
+
} from '../src/semantic.mjs';
|
|
24
39
|
import { auditRegistryFieldUsage } from './audit-registry-field-usage.mjs';
|
|
25
40
|
import { validateSkills } from './validate-skills.mjs';
|
|
41
|
+
import { runKernel09Conformance } from './conformance-09.mjs';
|
|
26
42
|
|
|
27
43
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
44
|
const FIXTURES = path.resolve(__dirname, '..', 'conformance', 'fixtures.json');
|
|
29
45
|
const REFERENCE_BUNDLE = path.resolve(__dirname, '..', 'conformance', 'reference-bundle.ucw.json');
|
|
30
46
|
const REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
|
|
47
|
+
const PACKAGE = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
31
48
|
const PORTABILITY_CLASSES = ['structural', 'tenant_private', 'resource_bound'];
|
|
32
49
|
|
|
33
50
|
function parseArgs(argv) {
|
|
@@ -146,14 +163,14 @@ function validateRegistry() {
|
|
|
146
163
|
* (spec §12) without needing a live host. Fixtures run in array order so that
|
|
147
164
|
* create → patch → delete dependencies resolve.
|
|
148
165
|
*/
|
|
149
|
-
function runAgainstEngine(doc) {
|
|
166
|
+
async function runAgainstEngine(doc) {
|
|
150
167
|
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-vault-'));
|
|
151
168
|
const engine = createEngine();
|
|
152
169
|
let pass = 0, fail = 0;
|
|
153
170
|
try {
|
|
154
171
|
for (const f of doc.fixtures) {
|
|
155
172
|
const want = f.expected_response || {};
|
|
156
|
-
const res = engine.processOperation(f.request, vault);
|
|
173
|
+
const res = await engine.processOperation(f.request, vault);
|
|
157
174
|
const okSuccess = want.success == null || res.success === want.success;
|
|
158
175
|
const wantType = want.validation && want.validation.type;
|
|
159
176
|
const okType = wantType == null || (res.validation && res.validation.type === wantType);
|
|
@@ -183,7 +200,7 @@ function runAgainstEngine(doc) {
|
|
|
183
200
|
* - no `tenant_private` primitive ever lands (the §5.5 keystone).
|
|
184
201
|
* Returns true on full pass.
|
|
185
202
|
*/
|
|
186
|
-
function runBundleConformance() {
|
|
203
|
+
async function runBundleConformance() {
|
|
187
204
|
if (!fs.existsSync(REFERENCE_BUNDLE)) {
|
|
188
205
|
console.log(' ⚠ No reference bundle; run `node scripts/build-reference-bundle.mjs` first.');
|
|
189
206
|
return false;
|
|
@@ -194,6 +211,32 @@ function runBundleConformance() {
|
|
|
194
211
|
|
|
195
212
|
const { valid, errors } = validateBundle(bundle, { registryDir: REGISTRY_DIR });
|
|
196
213
|
check('reference bundle is schema- and hash-valid', valid, errors.join('; '));
|
|
214
|
+
check('reference bundle provenance matches the published package identity',
|
|
215
|
+
bundle.manifest.provenance.exporter === `${PACKAGE.name}@${PACKAGE.version}`,
|
|
216
|
+
`got=${bundle.manifest.provenance.exporter}`);
|
|
217
|
+
|
|
218
|
+
const defaultExportRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-exporter-'));
|
|
219
|
+
try {
|
|
220
|
+
fs.mkdirSync(path.join(defaultExportRoot, 'rules'), { recursive: true });
|
|
221
|
+
fs.writeFileSync(path.join(defaultExportRoot, 'rules', 'identity.md'), [
|
|
222
|
+
'---',
|
|
223
|
+
'type: rule',
|
|
224
|
+
'title: Exporter Identity',
|
|
225
|
+
'description: Proves the default exporter uses the published package name.',
|
|
226
|
+
'timestamp: 2026-07-10T00:00:00Z',
|
|
227
|
+
'name: Exporter Identity',
|
|
228
|
+
'---',
|
|
229
|
+
'',
|
|
230
|
+
'Identity fixture.',
|
|
231
|
+
'',
|
|
232
|
+
].join('\n'));
|
|
233
|
+
const defaultExport = exportBundle(defaultExportRoot, { registryDir: REGISTRY_DIR });
|
|
234
|
+
check('default bundle exporter matches the published package name',
|
|
235
|
+
defaultExport.manifest.provenance.exporter === DEFAULT_EXPORTER,
|
|
236
|
+
`got=${defaultExport.manifest.provenance.exporter}`);
|
|
237
|
+
} finally {
|
|
238
|
+
fs.rmSync(defaultExportRoot, { recursive: true, force: true });
|
|
239
|
+
}
|
|
197
240
|
check('sale profile carries no tenant_private file',
|
|
198
241
|
bundle.files.every((f) => parseDocument(f.content).data.type !== 'task' && parseDocument(f.content).data.type !== 'conversation'));
|
|
199
242
|
check('sale profile reduces resource_bound fields to requirement declarations',
|
|
@@ -210,6 +253,14 @@ function runBundleConformance() {
|
|
|
210
253
|
!missingFilesValidation.valid && missingFilesValidation.errors.some((e) => e.includes('bundle.files must be an array')),
|
|
211
254
|
missingFilesValidation.errors.join('; '));
|
|
212
255
|
|
|
256
|
+
const malformedFileBundle = JSON.parse(JSON.stringify(bundle));
|
|
257
|
+
malformedFileBundle.files[0] = null;
|
|
258
|
+
const malformedFileValidation = validateBundle(malformedFileBundle, { registryDir: REGISTRY_DIR });
|
|
259
|
+
check('bundle validation returns structured errors for malformed file entries',
|
|
260
|
+
!malformedFileValidation.valid &&
|
|
261
|
+
malformedFileValidation.errors.some((error) => error.includes('file entries must be objects')),
|
|
262
|
+
malformedFileValidation.errors.join('; '));
|
|
263
|
+
|
|
213
264
|
const unknownExtensionBundle = JSON.parse(JSON.stringify(bundle));
|
|
214
265
|
unknownExtensionBundle.manifest.required_extensions = ['missing-extension'];
|
|
215
266
|
const unknownExtensionValidation = validateBundle(unknownExtensionBundle, { registryDir: REGISTRY_DIR });
|
|
@@ -217,6 +268,24 @@ function runBundleConformance() {
|
|
|
217
268
|
!unknownExtensionValidation.valid && unknownExtensionValidation.errors.some((e) => e.includes("required extension 'missing-extension'")),
|
|
218
269
|
unknownExtensionValidation.errors.join('; '));
|
|
219
270
|
|
|
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 });
|
|
284
|
+
check('bundle validation enforces registry patterns before import',
|
|
285
|
+
!invalidPatternValidation.valid &&
|
|
286
|
+
invalidPatternValidation.errors.some((error) => error.includes('registry pattern')),
|
|
287
|
+
invalidPatternValidation.errors.join('; '));
|
|
288
|
+
|
|
220
289
|
const prov = provisionBundle(bundle, { workspaceId: 'ws-conf', parameters: { business_name: 'Demo', domain: 'demo.example' } });
|
|
221
290
|
check('provision resolves params + link integrity (no dangling [[links]])', prov.ok,
|
|
222
291
|
`unresolved=${JSON.stringify(prov.unresolved)} dangling=${JSON.stringify(prov.danglingLinks)}`);
|
|
@@ -224,11 +293,11 @@ function runBundleConformance() {
|
|
|
224
293
|
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-'));
|
|
225
294
|
try {
|
|
226
295
|
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
227
|
-
const first = importBundle(prov.plan, vault, engine);
|
|
296
|
+
const first = await importBundle(prov.plan, vault, engine);
|
|
228
297
|
check('import commits every file', first.ok && first.committed === bundle.files.length,
|
|
229
298
|
`ok=${first.ok} committed=${first.committed}/${bundle.files.length}`);
|
|
230
299
|
|
|
231
|
-
const second = importBundle(prov.plan, vault, engine);
|
|
300
|
+
const second = await importBundle(prov.plan, vault, engine);
|
|
232
301
|
check('re-import is idempotent (0 new commits)', second.ok && second.committed === 0,
|
|
233
302
|
`committed=${second.committed}`);
|
|
234
303
|
|
|
@@ -238,6 +307,39 @@ function runBundleConformance() {
|
|
|
238
307
|
fs.rmSync(vault, { recursive: true, force: true });
|
|
239
308
|
}
|
|
240
309
|
|
|
310
|
+
const atomicVault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-bundle-atomic-'));
|
|
311
|
+
try {
|
|
312
|
+
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
313
|
+
const sourceContent = [
|
|
314
|
+
'---', 'type: rule', 'title: Atomic Source', 'description: Preflight source.',
|
|
315
|
+
'timestamp: 2026-07-10T00:00:00Z', 'name: Atomic Source', '---', '', 'Body.', '',
|
|
316
|
+
].join('\n');
|
|
317
|
+
const badPlan = [
|
|
318
|
+
{
|
|
319
|
+
type: 'operation', idempotency_key: 'atomic-source', workspace_id: 'ws-atomic',
|
|
320
|
+
path: 'rules/atomic.md', content: sourceContent, actor: { role: 'system' },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
type: 'operation', idempotency_key: 'atomic-invalid', workspace_id: 'ws-atomic',
|
|
324
|
+
path: 'primitives/invalid.md', actor: { role: 'system' },
|
|
325
|
+
content: [
|
|
326
|
+
'---', 'type: primitive', 'title: Invalid Primitive',
|
|
327
|
+
'description: Must fail before the source commits.', 'timestamp: 2026-07-10T00:00:00Z',
|
|
328
|
+
'primitive_id: invalid!:thing', 'namespace: invalid!', 'version: 1', 'name: Invalid',
|
|
329
|
+
'mutation: replace', 'portability: structural', 'scopes: [workspace]',
|
|
330
|
+
'fields: [placeholder]', '---', '', 'Invalid.', '',
|
|
331
|
+
].join('\n'),
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
const atomicResult = await importBundle(badPlan, atomicVault, engine);
|
|
335
|
+
check('two-phase import prevents partial commits on a late invalid envelope',
|
|
336
|
+
atomicResult.ok === false && atomicResult.committed === 0 &&
|
|
337
|
+
!fs.existsSync(path.join(atomicVault, 'rules', 'atomic.md')),
|
|
338
|
+
JSON.stringify(atomicResult.results.map((result) => result.validation?.errors || [])));
|
|
339
|
+
} finally {
|
|
340
|
+
fs.rmSync(atomicVault, { recursive: true, force: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
241
343
|
let pass = 0;
|
|
242
344
|
for (const c of checks) {
|
|
243
345
|
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
@@ -247,7 +349,7 @@ function runBundleConformance() {
|
|
|
247
349
|
return pass === checks.length;
|
|
248
350
|
}
|
|
249
351
|
|
|
250
|
-
function runOperationContractRegressionConformance() {
|
|
352
|
+
async function runOperationContractRegressionConformance() {
|
|
251
353
|
const checks = [];
|
|
252
354
|
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
253
355
|
|
|
@@ -256,7 +358,7 @@ function runOperationContractRegressionConformance() {
|
|
|
256
358
|
try {
|
|
257
359
|
const engine = createEngine({ registryDir: REGISTRY_DIR, leaseStore });
|
|
258
360
|
const workflowPath = 'workflows/nested.md';
|
|
259
|
-
const createRes = engine.processOperation({
|
|
361
|
+
const createRes = await engine.processOperation({
|
|
260
362
|
type: 'operation',
|
|
261
363
|
idempotency_key: 'nested-create',
|
|
262
364
|
workspace_id: 'ws-reg',
|
|
@@ -282,7 +384,7 @@ function runOperationContractRegressionConformance() {
|
|
|
282
384
|
}, vault);
|
|
283
385
|
check('nested frontmatter fixture commits before patch', createRes.success === true, JSON.stringify(createRes.validation?.errors || []));
|
|
284
386
|
|
|
285
|
-
const patchRes = engine.processOperation({
|
|
387
|
+
const patchRes = await engine.processOperation({
|
|
286
388
|
type: 'patch',
|
|
287
389
|
idempotency_key: 'nested-patch',
|
|
288
390
|
workspace_id: 'ws-reg',
|
|
@@ -312,13 +414,13 @@ function runOperationContractRegressionConformance() {
|
|
|
312
414
|
actor: { role: 'system' },
|
|
313
415
|
content: '---\ntype: rule\ntitle: Leased\ndescription: Lease test.\ntimestamp: 2026-07-02T00:00:00Z\nname: Leased\n---\nBody.\n',
|
|
314
416
|
};
|
|
315
|
-
const missingLease = engine.processOperation(leasedEnvelope, vault);
|
|
417
|
+
const missingLease = await engine.processOperation(leasedEnvelope, vault);
|
|
316
418
|
check('lease conflict rejects missing lease_id', missingLease.success === false && missingLease.validation.errors.some((e) => e.includes('leased')));
|
|
317
419
|
|
|
318
|
-
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);
|
|
319
421
|
check('lease conflict rejects mismatched lease_id', mismatchLease.success === false && mismatchLease.validation.errors.some((e) => e.includes('Lease mismatch')));
|
|
320
422
|
|
|
321
|
-
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);
|
|
322
424
|
check('lease allows matching lease_id', matchedLease.success === true, JSON.stringify(matchedLease.validation?.errors || []));
|
|
323
425
|
|
|
324
426
|
const expiredTarget = 'rules/expired.md';
|
|
@@ -327,13 +429,13 @@ function runOperationContractRegressionConformance() {
|
|
|
327
429
|
lease_id: 'expired',
|
|
328
430
|
expires_at: '2000-01-01T00:00:00.000Z',
|
|
329
431
|
}));
|
|
330
|
-
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);
|
|
331
433
|
check('expired lease does not block writes', expiredLease.success === true, JSON.stringify(expiredLease.validation?.errors || []));
|
|
332
434
|
|
|
333
435
|
const unreadableTarget = 'rules/unreadable.md';
|
|
334
436
|
const unreadablePathKey = crypto.createHash('sha256').update(unreadableTarget).digest('hex');
|
|
335
437
|
fs.writeFileSync(path.join(leaseDir, `${unreadablePathKey}.lease.json`), '{not json');
|
|
336
|
-
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);
|
|
337
439
|
check('unreadable lease state fails closed', unreadableLease.success === false && unreadableLease.validation.errors.some((e) => e.includes('unreadable')));
|
|
338
440
|
} finally {
|
|
339
441
|
fs.rmSync(vault, { recursive: true, force: true });
|
|
@@ -349,7 +451,177 @@ function runOperationContractRegressionConformance() {
|
|
|
349
451
|
return pass === checks.length;
|
|
350
452
|
}
|
|
351
453
|
|
|
352
|
-
function
|
|
454
|
+
async function runSemanticLocalizationConformance() {
|
|
455
|
+
const checks = [];
|
|
456
|
+
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
457
|
+
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-semantic-'));
|
|
458
|
+
try {
|
|
459
|
+
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
460
|
+
const sourcePath = 'rules/refund-policy.md';
|
|
461
|
+
const sourceContent = [
|
|
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.', '',
|
|
468
|
+
].join('\n');
|
|
469
|
+
const sourceWrite = await engine.processOperation({
|
|
470
|
+
type: 'operation', idempotency_key: 'sem-source', workspace_id: 'ws-sem',
|
|
471
|
+
path: sourcePath, content: sourceContent, actor: { role: 'system' },
|
|
472
|
+
}, vault);
|
|
473
|
+
check('non-English structural source commits', sourceWrite.success === true, JSON.stringify(sourceWrite.validation?.errors || []));
|
|
474
|
+
|
|
475
|
+
const privateContent = [
|
|
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.', '',
|
|
479
|
+
].join('\n');
|
|
480
|
+
const privateWrite = await engine.processOperation({
|
|
481
|
+
type: 'operation', idempotency_key: 'sem-private', workspace_id: 'ws-sem',
|
|
482
|
+
path: 'tasks/private-refund.md', content: privateContent, actor: { role: 'system' },
|
|
483
|
+
}, vault);
|
|
484
|
+
check('tenant-private semantic source commits', privateWrite.success === true, JSON.stringify(privateWrite.validation?.errors || []));
|
|
485
|
+
|
|
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);
|
|
489
|
+
check('safe semantic default excludes tenant-private documents',
|
|
490
|
+
first.documents.every((document) => document.portability === 'structural') &&
|
|
491
|
+
!first.documents.some((document) => document.path.startsWith('tasks/')));
|
|
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);
|
|
514
|
+
|
|
515
|
+
const privateIndex = buildSemanticIndex(vault, { includePrivate: true, registryDir: REGISTRY_DIR });
|
|
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');
|
|
519
|
+
let invalidRawRejected = false;
|
|
520
|
+
try { buildSemanticIndex(vault, { registryDir: REGISTRY_DIR }); }
|
|
521
|
+
catch (error) { invalidRawRejected = error.message.includes("Missing required field 'title'"); }
|
|
522
|
+
check('semantic projection rejects raw documents that violate the registry', invalidRawRejected);
|
|
523
|
+
} finally {
|
|
524
|
+
fs.rmSync(vault, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let pass = 0;
|
|
528
|
+
for (const c of checks) {
|
|
529
|
+
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
530
|
+
else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
531
|
+
}
|
|
532
|
+
console.log(`\n ${pass}/${checks.length} semantic/multilingual rendering checks passed (§11.9)`);
|
|
533
|
+
return pass === checks.length;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function runRegistryExtensionConformance() {
|
|
537
|
+
const checks = [];
|
|
538
|
+
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
539
|
+
const registry = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-registry-'));
|
|
540
|
+
const extensions = path.join(registry, 'extensions');
|
|
541
|
+
fs.mkdirSync(extensions, { recursive: true });
|
|
542
|
+
fs.copyFileSync(path.join(REGISTRY_DIR, 'core.json'), path.join(registry, 'core.json'));
|
|
543
|
+
const primitive = {
|
|
544
|
+
family: 'extension', append_only: false, portability: 'structural',
|
|
545
|
+
required_fields: ['type', 'name'],
|
|
546
|
+
};
|
|
547
|
+
const writeExtension = (name, value) =>
|
|
548
|
+
fs.writeFileSync(path.join(extensions, `${name}.json`), JSON.stringify(value, null, 2));
|
|
549
|
+
try {
|
|
550
|
+
writeExtension('valid', {
|
|
551
|
+
registry: 'valid-ext', extends: 'core', document_primitives: { custom_thing: primitive },
|
|
552
|
+
});
|
|
553
|
+
let validLoaded = false;
|
|
554
|
+
try { validLoaded = loadRegistries(registry).types.has('custom_thing'); } catch {}
|
|
555
|
+
check('valid extension registry composes with core', validLoaded);
|
|
556
|
+
|
|
557
|
+
writeExtension('collision', {
|
|
558
|
+
registry: 'collision', extends: 'core', document_primitives: { rule: primitive },
|
|
559
|
+
});
|
|
560
|
+
let coreCollision = false;
|
|
561
|
+
try { loadRegistries(registry); } catch (error) { coreCollision = error.message.includes("primitive 'rule' collides"); }
|
|
562
|
+
check('extension cannot redefine a core primitive', coreCollision);
|
|
563
|
+
fs.rmSync(path.join(extensions, 'collision.json'));
|
|
564
|
+
|
|
565
|
+
writeExtension('duplicate', {
|
|
566
|
+
registry: 'duplicate', extends: 'core', document_primitives: { custom_thing: primitive },
|
|
567
|
+
});
|
|
568
|
+
let siblingCollision = false;
|
|
569
|
+
try { loadRegistries(registry); } catch (error) { siblingCollision = error.message.includes("primitive 'custom_thing' collides"); }
|
|
570
|
+
check('sibling extensions cannot redefine each other', siblingCollision);
|
|
571
|
+
fs.rmSync(path.join(extensions, 'duplicate.json'));
|
|
572
|
+
|
|
573
|
+
writeExtension('pattern', {
|
|
574
|
+
registry: 'bad-pattern', extends: 'core',
|
|
575
|
+
document_primitives: { patterned: { ...primitive, patterns: { name: '[' } } },
|
|
576
|
+
});
|
|
577
|
+
let badPattern = false;
|
|
578
|
+
try { loadRegistries(registry); } catch (error) { badPattern = error.message.includes('invalid pattern'); }
|
|
579
|
+
check('invalid extension regex fails during registry load', badPattern);
|
|
580
|
+
fs.rmSync(path.join(extensions, 'pattern.json'));
|
|
581
|
+
|
|
582
|
+
writeExtension('shape', {
|
|
583
|
+
registry: 'bad-shape', extends: 'core',
|
|
584
|
+
document_primitives: { shaped: { ...primitive, required_when: [] } },
|
|
585
|
+
});
|
|
586
|
+
let badShape = false;
|
|
587
|
+
try { loadRegistries(registry); } catch (error) { badShape = error.message.includes('required_when must be an object'); }
|
|
588
|
+
check('malformed extension constraint shapes fail during registry load', badShape);
|
|
589
|
+
fs.rmSync(path.join(extensions, 'shape.json'));
|
|
590
|
+
|
|
591
|
+
writeExtension('unknown-reference', {
|
|
592
|
+
registry: 'unknown-reference', extends: 'core',
|
|
593
|
+
document_primitives: {
|
|
594
|
+
linked_thing: {
|
|
595
|
+
...primitive,
|
|
596
|
+
references: { target_path: { allowed_types: ['missing_type'] } },
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
let unknownReference = false;
|
|
601
|
+
try { loadRegistries(registry); } catch (error) { unknownReference = error.message.includes("unknown type 'missing_type'"); }
|
|
602
|
+
check('extension references cannot name unknown primitive types', unknownReference);
|
|
603
|
+
fs.rmSync(path.join(extensions, 'unknown-reference.json'));
|
|
604
|
+
|
|
605
|
+
const symlinkTarget = path.join(registry, 'external.json');
|
|
606
|
+
fs.writeFileSync(symlinkTarget, JSON.stringify({ registry: 'linked', extends: 'core', document_primitives: {} }));
|
|
607
|
+
fs.symlinkSync(symlinkTarget, path.join(extensions, 'linked.json'));
|
|
608
|
+
let symlinkRejected = false;
|
|
609
|
+
try { loadRegistries(registry); } catch (error) { symlinkRejected = error.message.includes('symlinked registries'); }
|
|
610
|
+
check('symlinked extension registry is rejected', symlinkRejected);
|
|
611
|
+
} finally {
|
|
612
|
+
fs.rmSync(registry, { recursive: true, force: true });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let pass = 0;
|
|
616
|
+
for (const c of checks) {
|
|
617
|
+
if (c.cond) { pass++; console.log(` ✅ ${c.name}`); }
|
|
618
|
+
else console.log(` ❌ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
|
|
619
|
+
}
|
|
620
|
+
console.log(`\n ${pass}/${checks.length} extension-registry checks passed`);
|
|
621
|
+
return pass === checks.length;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function runCliSmokeConformance() {
|
|
353
625
|
const checks = [];
|
|
354
626
|
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
355
627
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-cli-smoke-'));
|
|
@@ -369,7 +641,7 @@ function runCliSmokeConformance() {
|
|
|
369
641
|
const failures = [];
|
|
370
642
|
for (const abs of files) {
|
|
371
643
|
const rel = path.relative(vault, abs);
|
|
372
|
-
const res = engine.processOperation({
|
|
644
|
+
const res = await engine.processOperation({
|
|
373
645
|
type: 'operation',
|
|
374
646
|
idempotency_key: `scaffold-${rel.replace(/[^A-Za-z0-9._-]/g, '_')}`,
|
|
375
647
|
workspace_id: 'ws-scaffold',
|
|
@@ -382,6 +654,17 @@ function runCliSmokeConformance() {
|
|
|
382
654
|
}
|
|
383
655
|
check('ssss new starter vault files validate against the registry', files.length > 0 && failures.length === 0, JSON.stringify(failures));
|
|
384
656
|
|
|
657
|
+
const semanticOutput = JSON.parse(execFileSync('node', [
|
|
658
|
+
path.join(__dirname, 'ssss.mjs'),
|
|
659
|
+
'semantic',
|
|
660
|
+
vault,
|
|
661
|
+
'--query',
|
|
662
|
+
'welcome rule',
|
|
663
|
+
], { encoding: 'utf8' }));
|
|
664
|
+
check('ssss semantic returns ranked structural results',
|
|
665
|
+
semanticOutput.results?.some((result) => result.document.path === 'rules/welcome.md'),
|
|
666
|
+
JSON.stringify(semanticOutput.results || []));
|
|
667
|
+
|
|
385
668
|
const dryRunVault = path.join(tmp, 'dry-run-vault');
|
|
386
669
|
const dryRunOut = execFileSync('node', [
|
|
387
670
|
path.join(__dirname, 'ssss.mjs'),
|
|
@@ -426,7 +709,7 @@ function runCliSmokeConformance() {
|
|
|
426
709
|
* workflow frontmatter as the canonical schedule source, then derive trigger,
|
|
427
710
|
* task, and run envelopes without introducing a scheduler database of record.
|
|
428
711
|
*/
|
|
429
|
-
function runRuntimeConformance() {
|
|
712
|
+
async function runRuntimeConformance() {
|
|
430
713
|
const checks = [];
|
|
431
714
|
const check = (name, cond, detail = '') => { checks.push({ name, cond, detail }); };
|
|
432
715
|
|
|
@@ -489,15 +772,15 @@ function runRuntimeConformance() {
|
|
|
489
772
|
const vault = fs.mkdtempSync(path.join(os.tmpdir(), 'ssss-runtime-'));
|
|
490
773
|
try {
|
|
491
774
|
const engine = createEngine({ registryDir: REGISTRY_DIR });
|
|
492
|
-
const eventRes = engine.processOperation(firstPlan.envelopes[0], vault);
|
|
493
|
-
const taskRes = engine.processOperation(firstPlan.envelopes[1], vault);
|
|
775
|
+
const eventRes = await engine.processOperation(firstPlan.envelopes[0], vault);
|
|
776
|
+
const taskRes = await engine.processOperation(firstPlan.envelopes[1], vault);
|
|
494
777
|
check('trigger event commits through Operation Contract', eventRes.success === true,
|
|
495
778
|
JSON.stringify(eventRes.validation?.errors || []));
|
|
496
779
|
check('instantiated task commits through Operation Contract', taskRes.success === true,
|
|
497
780
|
JSON.stringify(taskRes.validation?.errors || []));
|
|
498
781
|
|
|
499
|
-
const replayEvent = engine.processOperation(secondPlan.envelopes[0], vault);
|
|
500
|
-
const replayTask = engine.processOperation(secondPlan.envelopes[1], vault);
|
|
782
|
+
const replayEvent = await engine.processOperation(secondPlan.envelopes[0], vault);
|
|
783
|
+
const replayTask = await engine.processOperation(secondPlan.envelopes[1], vault);
|
|
501
784
|
check('duplicate daemon tick replays trigger event idempotently', replayEvent.replay != null);
|
|
502
785
|
check('duplicate daemon tick replays task write idempotently', replayTask.replay != null);
|
|
503
786
|
|
|
@@ -508,7 +791,7 @@ function runRuntimeConformance() {
|
|
|
508
791
|
status: 'queued',
|
|
509
792
|
startedAt: scheduledFor,
|
|
510
793
|
});
|
|
511
|
-
const runRes = engine.processOperation(runEnvelope, vault);
|
|
794
|
+
const runRes = await engine.processOperation(runEnvelope, vault);
|
|
512
795
|
check('worker-created run commits through Operation Contract', runRes.success === true,
|
|
513
796
|
JSON.stringify(runRes.validation?.errors || []));
|
|
514
797
|
} finally {
|
|
@@ -588,16 +871,25 @@ async function main() {
|
|
|
588
871
|
process.exit(ok ? 0 : 1);
|
|
589
872
|
} else if (opts.engine) {
|
|
590
873
|
console.log('\nRunning fixtures through the reference engine (src/engine.mjs) ...');
|
|
591
|
-
const engineOk = runAgainstEngine(doc);
|
|
874
|
+
const engineOk = await runAgainstEngine(doc);
|
|
592
875
|
console.log('\nRunning workflow runtime conformance (src/runtime.mjs, §11.8) ...');
|
|
593
|
-
const runtimeOk = runRuntimeConformance();
|
|
876
|
+
const runtimeOk = await runRuntimeConformance();
|
|
594
877
|
console.log('\nRunning operation regression conformance (src/engine.mjs, §6/§7) ...');
|
|
595
|
-
const operationRegressionOk = runOperationContractRegressionConformance();
|
|
878
|
+
const operationRegressionOk = await runOperationContractRegressionConformance();
|
|
879
|
+
console.log('\nRunning extension-registry conformance (src/registry.mjs) ...');
|
|
880
|
+
const extensionRegistryOk = runRegistryExtensionConformance();
|
|
881
|
+
console.log('\nRunning semantic/multilingual rendering conformance (src/semantic.mjs, §11.9) ...');
|
|
882
|
+
const semanticOk = await runSemanticLocalizationConformance();
|
|
883
|
+
console.log('\nRunning SSSS 0.9 kernel/adapter/UI conformance ...');
|
|
884
|
+
const kernel09Ok = await runKernel09Conformance();
|
|
596
885
|
console.log('\nRunning bundle/provisioning conformance (src/bundle.mjs, §16/§17) ...');
|
|
597
|
-
const bundleOk = runBundleConformance();
|
|
886
|
+
const bundleOk = await runBundleConformance();
|
|
598
887
|
console.log('\nRunning CLI smoke conformance (scripts/ssss.mjs) ...');
|
|
599
|
-
const cliSmokeOk = runCliSmokeConformance();
|
|
600
|
-
process.exit(
|
|
888
|
+
const cliSmokeOk = await runCliSmokeConformance();
|
|
889
|
+
process.exit(
|
|
890
|
+
engineOk && runtimeOk && operationRegressionOk && extensionRegistryOk &&
|
|
891
|
+
semanticOk && kernel09Ok && bundleOk && cliSmokeOk ? 0 : 1
|
|
892
|
+
);
|
|
601
893
|
} else {
|
|
602
894
|
console.log('\nℹ No --endpoint/--engine given; ran structural + registry validation only.');
|
|
603
895
|
console.log(' Reference engine: ssss conformance --engine');
|
package/scripts/ssss.mjs
CHANGED
|
@@ -36,7 +36,12 @@ Vault → bundle → tenant lifecycle:
|
|
|
36
36
|
import <bundle> Replay a bundle/plan into a target vault (idempotent).
|
|
37
37
|
|
|
38
38
|
Authoring & conformance:
|
|
39
|
+
primitive <verb> Create, validate, inspect, migrate, or emit types.
|
|
40
|
+
registry <verb> Compose, lock, or verify an effective registry.
|
|
41
|
+
adapter conformance Run machine-readable adapter contract suites.
|
|
42
|
+
migrate 0.8-to-0.9 Diagnose and plan a non-destructive 0.9 migration.
|
|
39
43
|
autolink [dir] Generate OKF wiki links across a vault.
|
|
44
|
+
semantic <vault> Build/query a deterministic semantic projection (§11.9).
|
|
40
45
|
conformance Run the canonical conformance suite (spec §12).
|
|
41
46
|
--engine | --endpoint <url> [--token <pat>]
|
|
42
47
|
|
|
@@ -60,6 +65,11 @@ const MODULES = {
|
|
|
60
65
|
inspect: './cmd-inspect.mjs',
|
|
61
66
|
provision: './cmd-provision.mjs',
|
|
62
67
|
import: './cmd-import.mjs',
|
|
68
|
+
semantic: './cmd-semantic.mjs',
|
|
69
|
+
primitive: './cmd-primitive.mjs',
|
|
70
|
+
registry: './cmd-registry.mjs',
|
|
71
|
+
adapter: './cmd-adapter.mjs',
|
|
72
|
+
migrate: './cmd-migrate.mjs',
|
|
63
73
|
help: './cmd-help.mjs',
|
|
64
74
|
};
|
|
65
75
|
|