@gregiteen/ssss-cli 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +1 -1
- package/docs/ssss-spec.md +121 -16
- package/package.json +1 -1
- package/registry/core.json +14 -0
- package/scripts/audit-registry-field-usage.mjs +3 -0
- package/scripts/cmd-adapter.mjs +2 -0
- package/scripts/cmd-bridge.mjs +113 -0
- package/scripts/cmd-import.mjs +21 -1
- package/scripts/cmd-provision.mjs +33 -2
- package/scripts/cmd-serve.mjs +155 -0
- package/scripts/cmd-upgrade.mjs +110 -0
- package/scripts/conformance-09.mjs +139 -0
- package/scripts/conformance.mjs +473 -1
- package/scripts/ssss.mjs +7 -1
- package/skills/ssss/SKILL.md +2 -2
- package/skills/ssss/core/SKILL.md +3 -3
- package/skills/ssss/core/references/core-registry.json +14 -0
- package/skills/ssss/core/references/ssss-spec.md +121 -16
- package/src/authorization.mjs +58 -7
- package/src/bundle.mjs +469 -27
- package/src/engine.mjs +11 -1
- package/src/kernel.mjs +30 -5
- package/src/projections.mjs +93 -0
- package/src/registry.mjs +82 -3
package/src/projections.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Replayable, cursor-aware SSSS projection coordinator and host adapter examples. */
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { MemoryEventStore, createCanonicalEvent } from './events.mjs';
|
|
3
4
|
|
|
4
5
|
function stable(value) {
|
|
5
6
|
if (Array.isArray(value)) return value.map(stable);
|
|
@@ -131,3 +132,95 @@ export class ProjectionCoordinator {
|
|
|
131
132
|
return { drift: actualHash !== expectedHash, expected_hash: expectedHash, actual_hash: actualHash };
|
|
132
133
|
}
|
|
133
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Conformance runner for projection adapters (spec §10, §12).
|
|
138
|
+
* Verifies:
|
|
139
|
+
* 1. Rebuild from event log produces the correct state.
|
|
140
|
+
* 2. Incremental apply equals a full rebuild.
|
|
141
|
+
* 3. Tampered state is reported as drift.
|
|
142
|
+
* 4. Replay is idempotent.
|
|
143
|
+
*/
|
|
144
|
+
export async function runProjectionContract(createAdapter = createSqlProjectionAdapter) {
|
|
145
|
+
const checks = [];
|
|
146
|
+
const record = (name, passed) => checks.push({ name, passed: !!passed });
|
|
147
|
+
|
|
148
|
+
const eventStore = new MemoryEventStore();
|
|
149
|
+
const testWorkspace = 'ws-projection-test';
|
|
150
|
+
const principal = { id: 'agent-1', kind: 'agent' };
|
|
151
|
+
|
|
152
|
+
const events = [
|
|
153
|
+
createCanonicalEvent({
|
|
154
|
+
workspace_id: testWorkspace,
|
|
155
|
+
action: 'create',
|
|
156
|
+
subject: 'tasks/task-1.md',
|
|
157
|
+
operation_id: 'op-1',
|
|
158
|
+
idempotency_key: 'key-1',
|
|
159
|
+
principal,
|
|
160
|
+
after_hash: 'sha256:1111111111111111111111111111111111111111111111111111111111111111',
|
|
161
|
+
}),
|
|
162
|
+
createCanonicalEvent({
|
|
163
|
+
workspace_id: testWorkspace,
|
|
164
|
+
action: 'patch',
|
|
165
|
+
subject: 'tasks/task-1.md',
|
|
166
|
+
operation_id: 'op-2',
|
|
167
|
+
idempotency_key: 'key-2',
|
|
168
|
+
principal,
|
|
169
|
+
before_hash: 'sha256:1111111111111111111111111111111111111111111111111111111111111111',
|
|
170
|
+
after_hash: 'sha256:2222222222222222222222222222222222222222222222222222222222222222',
|
|
171
|
+
}),
|
|
172
|
+
createCanonicalEvent({
|
|
173
|
+
workspace_id: testWorkspace,
|
|
174
|
+
action: 'create',
|
|
175
|
+
subject: 'tasks/task-2.md',
|
|
176
|
+
operation_id: 'op-3',
|
|
177
|
+
idempotency_key: 'key-3',
|
|
178
|
+
principal,
|
|
179
|
+
after_hash: 'sha256:3333333333333333333333333333333333333333333333333333333333333333',
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const ev of events) {
|
|
184
|
+
await eventStore.append(ev);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 1. Rebuild from event log
|
|
188
|
+
const rebuildAdapter = typeof createAdapter === 'function' ? createAdapter({ table: 'rebuild_test' }) : createAdapter;
|
|
189
|
+
const coordinatorRebuild = new ProjectionCoordinator({ eventStore });
|
|
190
|
+
coordinatorRebuild.register(rebuildAdapter);
|
|
191
|
+
|
|
192
|
+
const rebuildRes = await coordinatorRebuild.replay(rebuildAdapter.id, { rebuild: true, workspaceId: testWorkspace });
|
|
193
|
+
record('rebuild from event log applies all events', rebuildRes.applied === 3);
|
|
194
|
+
|
|
195
|
+
const rebuildSnapshot = await rebuildAdapter.snapshot();
|
|
196
|
+
record('rebuild snapshot contains all projected rows', rebuildSnapshot && Object.keys(rebuildSnapshot).length === 3);
|
|
197
|
+
const rebuildHash = projectionHash(rebuildSnapshot);
|
|
198
|
+
|
|
199
|
+
// 2. Incremental apply equals full rebuild
|
|
200
|
+
const incrementalAdapter = typeof createAdapter === 'function' ? createAdapter({ table: 'incremental_test' }) : createSqlProjectionAdapter({ table: 'incremental_test' });
|
|
201
|
+
const coordinatorIncremental = new ProjectionCoordinator({ eventStore });
|
|
202
|
+
coordinatorIncremental.register(incrementalAdapter);
|
|
203
|
+
|
|
204
|
+
for (const ev of events) {
|
|
205
|
+
await coordinatorIncremental.dispatch(ev);
|
|
206
|
+
}
|
|
207
|
+
const incrementalSnapshot = await incrementalAdapter.snapshot();
|
|
208
|
+
const incrementalHash = projectionHash(incrementalSnapshot);
|
|
209
|
+
record('incremental apply equals a full rebuild', rebuildHash === incrementalHash);
|
|
210
|
+
|
|
211
|
+
// 3. Drift detection
|
|
212
|
+
const cleanDrift = await coordinatorRebuild.detectDrift(rebuildAdapter.id, rebuildHash);
|
|
213
|
+
record('matching state reports no drift', cleanDrift.drift === false);
|
|
214
|
+
|
|
215
|
+
const tamperedDrift = await coordinatorRebuild.detectDrift(rebuildAdapter.id, 'sha256:0000000000000000000000000000000000000000000000000000000000000000');
|
|
216
|
+
record('tampered state is reported as drift', tamperedDrift.drift === true);
|
|
217
|
+
|
|
218
|
+
// 4. Replay idempotency
|
|
219
|
+
const replayAtEnd = await coordinatorRebuild.replay(rebuildAdapter.id, { workspaceId: testWorkspace });
|
|
220
|
+
record('replay at cursor end applies 0 events', replayAtEnd.applied === 0);
|
|
221
|
+
|
|
222
|
+
const snapshotAfterReplay = await rebuildAdapter.snapshot();
|
|
223
|
+
record('replay at cursor end preserves projection state and hash', projectionHash(snapshotAfterReplay) === rebuildHash);
|
|
224
|
+
|
|
225
|
+
return { passed: checks.every((c) => c.passed), checks };
|
|
226
|
+
}
|
package/src/registry.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const DEFAULT_REGISTRY_DIR = path.resolve(__dirname, '..', 'registry');
|
|
18
18
|
|
|
19
|
-
const APPEND_TYPES = new Set(['conversation', 'run']);
|
|
19
|
+
const APPEND_TYPES = new Set(['conversation', 'run', 'thread']);
|
|
20
20
|
const SAFE_REGISTRY_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
21
21
|
const SAFE_TYPE_NAME = /^(?:[a-z][a-z0-9_-]{0,63}:)?[a-z][a-z0-9_-]{0,127}$/;
|
|
22
22
|
const SAFE_FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -52,6 +52,13 @@ function validatePrimitiveDefinition(name, def, source, portabilityClasses) {
|
|
|
52
52
|
assertStringArray(def.required_fields, `${source}: primitive '${name}' required_fields`);
|
|
53
53
|
assertStringArray(def.optional_fields, `${source}: primitive '${name}' optional_fields`);
|
|
54
54
|
assertStringArray(def.immutable_fields, `${source}: primitive '${name}' immutable_fields`);
|
|
55
|
+
assertStringArray(def.aliases, `${source}: primitive '${name}' aliases`);
|
|
56
|
+
if (def.deprecated !== undefined && typeof def.deprecated !== 'boolean') {
|
|
57
|
+
throw new Error(`${source}: primitive '${name}' deprecated must be a boolean.`);
|
|
58
|
+
}
|
|
59
|
+
if (def.deprecation_notice !== undefined && typeof def.deprecation_notice !== 'string') {
|
|
60
|
+
throw new Error(`${source}: primitive '${name}' deprecation_notice must be a string.`);
|
|
61
|
+
}
|
|
55
62
|
|
|
56
63
|
assertRecord(def.required_when, `${source}: primitive '${name}' required_when`);
|
|
57
64
|
for (const [condition, fields] of Object.entries(def.required_when || {})) {
|
|
@@ -157,6 +164,33 @@ export function composeRegistries({ core, extensions: extensionInputs = [] } = {
|
|
|
157
164
|
aliases.set(`ssss:${name}`, name);
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
for (const [name, def] of Object.entries(core.document_primitives || {})) {
|
|
168
|
+
for (const alias of def.aliases || []) {
|
|
169
|
+
if (!isPrimitiveTypeName(alias)) {
|
|
170
|
+
throw new Error(`core registry: primitive '${name}' has unsafe alias '${alias}'.`);
|
|
171
|
+
}
|
|
172
|
+
const bareAlias = alias.startsWith('ssss:') ? alias.slice('ssss:'.length) : alias;
|
|
173
|
+
const qualifiedAlias = alias.includes(':') ? alias : `ssss:${alias}`;
|
|
174
|
+
if (bareAlias === name) {
|
|
175
|
+
throw new Error(`core registry: primitive '${name}' cannot alias itself.`);
|
|
176
|
+
}
|
|
177
|
+
if (types.has(bareAlias)) {
|
|
178
|
+
const peer = types.get(bareAlias);
|
|
179
|
+
const peerAliases = (peer.aliases || []).map((a) => (a.startsWith('ssss:') ? a.slice('ssss:'.length) : a));
|
|
180
|
+
if (!peerAliases.includes(name)) {
|
|
181
|
+
throw new Error(`core registry: primitive alias '${alias}' on '${name}' collides with existing primitive '${bareAlias}'.`);
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if ((aliases.has(bareAlias) && aliases.get(bareAlias) !== name) ||
|
|
186
|
+
(aliases.has(qualifiedAlias) && aliases.get(qualifiedAlias) !== name)) {
|
|
187
|
+
throw new Error(`core registry: primitive alias '${alias}' collides with an existing primitive.`);
|
|
188
|
+
}
|
|
189
|
+
aliases.set(bareAlias, name);
|
|
190
|
+
aliases.set(qualifiedAlias, name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
160
194
|
for (const [index, item] of extensionInputs.entries()) {
|
|
161
195
|
const { source, value: ext } = normalizeExtensionInput(item, index);
|
|
162
196
|
if (!SAFE_REGISTRY_NAME.test(ext.registry || '')) {
|
|
@@ -232,6 +266,14 @@ export function composeRegistries({ core, extensions: extensionInputs = [] } = {
|
|
|
232
266
|
);
|
|
233
267
|
}
|
|
234
268
|
}
|
|
269
|
+
const aliasOverlap = (constraint.allowed_types || []).filter((allowed) =>
|
|
270
|
+
(constraint.disallowed_types || []).some((disallowed) => isSamePrimitiveType(registrySet, allowed, disallowed))
|
|
271
|
+
);
|
|
272
|
+
if (aliasOverlap.length) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`registry '${def.registry}' primitive '${def.type}' reference '${field}' both allows and forbids (via alias): ${aliasOverlap.join(', ')}.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
235
277
|
}
|
|
236
278
|
}
|
|
237
279
|
|
|
@@ -297,6 +339,42 @@ export function resolvePrimitiveDefinition(registrySet, type) {
|
|
|
297
339
|
return local ? registrySet.types.get(local) || null : null;
|
|
298
340
|
}
|
|
299
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Return all equivalent primitive definitions for a type or definition
|
|
344
|
+
* (the definition itself plus any definitions linked via `aliases`).
|
|
345
|
+
*/
|
|
346
|
+
export function resolveEquivalentDefinitions(registrySet, typeOrDef) {
|
|
347
|
+
const base = typeof typeOrDef === 'string'
|
|
348
|
+
? resolvePrimitiveDefinition(registrySet, typeOrDef)
|
|
349
|
+
: typeOrDef;
|
|
350
|
+
if (!base) return [];
|
|
351
|
+
const seen = new Map([[base.qualified_type || base.type, base]]);
|
|
352
|
+
for (const alias of base.aliases || []) {
|
|
353
|
+
const target = resolvePrimitiveDefinition(registrySet, alias);
|
|
354
|
+
if (target && !seen.has(target.qualified_type || target.type)) {
|
|
355
|
+
seen.set(target.qualified_type || target.type, target);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return [...seen.values()];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Return true if two primitive type identifiers resolve to the same canonical
|
|
363
|
+
* primitive definition (either exact match, qualified match, or mutual alias).
|
|
364
|
+
*/
|
|
365
|
+
export function isSamePrimitiveType(registrySet, typeA, typeB) {
|
|
366
|
+
if (typeA === typeB) return true;
|
|
367
|
+
if (!typeA || !typeB) return false;
|
|
368
|
+
const defA = resolvePrimitiveDefinition(registrySet, typeA);
|
|
369
|
+
const defB = resolvePrimitiveDefinition(registrySet, typeB);
|
|
370
|
+
if (!defA || !defB) return false;
|
|
371
|
+
if (defA.qualified_type === defB.qualified_type) return true;
|
|
372
|
+
const equivalentsA = resolveEquivalentDefinitions(registrySet, defA);
|
|
373
|
+
if (equivalentsA.some((d) => d.qualified_type === defB.qualified_type)) return true;
|
|
374
|
+
const equivalentsB = resolveEquivalentDefinitions(registrySet, defB);
|
|
375
|
+
return equivalentsB.some((d) => d.qualified_type === defA.qualified_type);
|
|
376
|
+
}
|
|
377
|
+
|
|
300
378
|
/**
|
|
301
379
|
* Names of the extensions loaded when a caller does not scope the load.
|
|
302
380
|
*
|
|
@@ -472,14 +550,15 @@ export function validateReferenceConstraints(typeDef, data, resolveReference, ty
|
|
|
472
550
|
continue;
|
|
473
551
|
}
|
|
474
552
|
const referencedType = referenced.data?.type;
|
|
553
|
+
const registrySurface = { types, aliases: types.aliases };
|
|
475
554
|
if (!lookupType(referencedType)) {
|
|
476
555
|
issues.push({ field, issue: `Referenced document declares unknown type '${referencedType || '(missing)'}'.` });
|
|
477
556
|
continue;
|
|
478
557
|
}
|
|
479
|
-
if (constraint.allowed_types && !constraint.allowed_types.
|
|
558
|
+
if (constraint.allowed_types && !constraint.allowed_types.some((allowed) => isSamePrimitiveType(registrySurface, allowed, referencedType))) {
|
|
480
559
|
issues.push({ field, issue: `Referenced type '${referencedType}' is not allowed.` });
|
|
481
560
|
}
|
|
482
|
-
if (constraint.disallowed_types?.
|
|
561
|
+
if (constraint.disallowed_types?.some((disallowed) => isSamePrimitiveType(registrySurface, disallowed, referencedType))) {
|
|
483
562
|
issues.push({ field, issue: `Referenced type '${referencedType}' is forbidden.` });
|
|
484
563
|
}
|
|
485
564
|
const referencedPortability = resolvePortability(lookupType(referencedType), referenced.data);
|