@gregiteen/ssss-cli 0.13.0 → 0.14.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 +28 -0
- package/VERSION +1 -1
- package/conformance/reference-bundle.ucw.json +1 -1
- package/docs/ssss-spec.md +112 -7
- package/package.json +1 -1
- 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 +20 -0
- package/scripts/conformance.mjs +473 -1
- package/scripts/ssss.mjs +7 -1
- package/src/authorization.mjs +28 -5
- package/src/bundle.mjs +469 -27
- package/src/kernel.mjs +28 -3
- package/src/projections.mjs +93 -0
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
|
+
}
|