@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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSSS primitive-definition API (spec 0.9).
|
|
3
|
+
*
|
|
4
|
+
* Human-facing names and descriptions may be authored in any language. Stable
|
|
5
|
+
* primitive/field/action/capability identifiers are symbolic controls and are
|
|
6
|
+
* generated or validated here rather than translated by presentation layers.
|
|
7
|
+
*/
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import { isPrimitiveTypeName } from './registry.mjs';
|
|
10
|
+
|
|
11
|
+
export const PRIMITIVE_MUTATIONS = ['replace', 'append'];
|
|
12
|
+
export const PRIMITIVE_PORTABILITY = ['structural', 'tenant_private', 'resource_bound'];
|
|
13
|
+
export const PRIMITIVE_SCOPES = ['system', 'account', 'workspace', 'user'];
|
|
14
|
+
export const FIELD_KINDS = [
|
|
15
|
+
'string', 'text', 'number', 'integer', 'boolean', 'datetime', 'date',
|
|
16
|
+
'enum', 'object', 'array', 'reference', 'secret_reference', 'resource_reference',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const NAMESPACE_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
20
|
+
const SYMBOL_RE = /^[a-z][a-z0-9_-]{0,127}$/;
|
|
21
|
+
const LANGUAGE_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
22
|
+
const CAPABILITY_RE = /^[a-z][a-z0-9_.-]{0,63}:[a-z][a-z0-9_.-]{0,127}$/;
|
|
23
|
+
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function issue(field, message) {
|
|
29
|
+
return { field, issue: message };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function uniqueStrings(value) {
|
|
33
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string' && item) &&
|
|
34
|
+
new Set(value).size === value.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Generate a stable opaque local ID from an authored name in any language. */
|
|
38
|
+
export function generatePrimitiveId({ namespace, name, salt = '' }) {
|
|
39
|
+
if (!NAMESPACE_RE.test(namespace || '')) throw new Error('namespace must be a safe lowercase identifier.');
|
|
40
|
+
if (typeof name !== 'string' || !name.trim()) throw new Error('name must be non-empty.');
|
|
41
|
+
const digest = crypto.createHash('sha256')
|
|
42
|
+
.update(`${namespace}\0${name.normalize('NFKC')}\0${salt}`)
|
|
43
|
+
.digest('hex')
|
|
44
|
+
.slice(0, 20);
|
|
45
|
+
return `${namespace}:p_${digest}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validatePrimitiveDefinition(definition) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
if (!isRecord(definition)) return { valid: false, errors: [issue('(root)', 'Primitive definition must be an object.')] };
|
|
51
|
+
const required = ['primitive_id', 'namespace', 'version', 'name', 'mutation', 'portability', 'scopes', 'fields'];
|
|
52
|
+
for (const field of required) {
|
|
53
|
+
const value = definition[field];
|
|
54
|
+
if (value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)) {
|
|
55
|
+
errors.push(issue(field, `Missing required field '${field}'.`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!NAMESPACE_RE.test(definition.namespace || '')) {
|
|
59
|
+
errors.push(issue('namespace', 'Use a lowercase namespace containing letters, numbers, underscore, or hyphen.'));
|
|
60
|
+
}
|
|
61
|
+
if (!isPrimitiveTypeName(definition.primitive_id) ||
|
|
62
|
+
!definition.primitive_id?.startsWith(`${definition.namespace}:`)) {
|
|
63
|
+
errors.push(issue('primitive_id', 'Use a qualified identifier whose prefix matches namespace.'));
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isInteger(definition.version) || definition.version < 1) {
|
|
66
|
+
errors.push(issue('version', 'Version must be a positive integer.'));
|
|
67
|
+
}
|
|
68
|
+
if (definition.revision !== undefined && (!Number.isInteger(definition.revision) || definition.revision < 1)) {
|
|
69
|
+
errors.push(issue('revision', 'Revision must be a positive integer.'));
|
|
70
|
+
}
|
|
71
|
+
if (typeof definition.name !== 'string' || !definition.name.trim()) {
|
|
72
|
+
errors.push(issue('name', 'Name may use any language but must not be empty.'));
|
|
73
|
+
}
|
|
74
|
+
if (definition.language !== undefined && !LANGUAGE_RE.test(definition.language)) {
|
|
75
|
+
errors.push(issue('language', 'Use a BCP-47-style language tag.'));
|
|
76
|
+
}
|
|
77
|
+
if (!PRIMITIVE_MUTATIONS.includes(definition.mutation)) {
|
|
78
|
+
errors.push(issue('mutation', `Use one of: ${PRIMITIVE_MUTATIONS.join(', ')}.`));
|
|
79
|
+
}
|
|
80
|
+
if (!PRIMITIVE_PORTABILITY.includes(definition.portability)) {
|
|
81
|
+
errors.push(issue('portability', `Use one of: ${PRIMITIVE_PORTABILITY.join(', ')}.`));
|
|
82
|
+
}
|
|
83
|
+
if (!uniqueStrings(definition.scopes) ||
|
|
84
|
+
definition.scopes.some((scope) => !PRIMITIVE_SCOPES.includes(scope))) {
|
|
85
|
+
errors.push(issue('scopes', `Provide a unique non-empty subset of: ${PRIMITIVE_SCOPES.join(', ')}.`));
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(definition.fields)) {
|
|
88
|
+
errors.push(issue('fields', 'Fields must be an array.'));
|
|
89
|
+
} else {
|
|
90
|
+
const ids = new Set();
|
|
91
|
+
for (const [index, field] of definition.fields.entries()) {
|
|
92
|
+
const prefix = `fields[${index}]`;
|
|
93
|
+
if (!isRecord(field)) {
|
|
94
|
+
errors.push(issue(prefix, 'Field definition must be an object.'));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!SYMBOL_RE.test(field.id || '')) errors.push(issue(`${prefix}.id`, 'Use a stable lowercase symbolic field ID.'));
|
|
98
|
+
else if (ids.has(field.id)) errors.push(issue(`${prefix}.id`, `Duplicate field ID '${field.id}'.`));
|
|
99
|
+
else ids.add(field.id);
|
|
100
|
+
if (typeof field.name !== 'string' || !field.name.trim()) {
|
|
101
|
+
errors.push(issue(`${prefix}.name`, 'Field name may use any language but must not be empty.'));
|
|
102
|
+
}
|
|
103
|
+
if (!FIELD_KINDS.includes(field.kind)) {
|
|
104
|
+
errors.push(issue(`${prefix}.kind`, `Use one of: ${FIELD_KINDS.join(', ')}.`));
|
|
105
|
+
}
|
|
106
|
+
if (field.kind === 'enum' && !uniqueStrings(field.values)) {
|
|
107
|
+
errors.push(issue(`${prefix}.values`, 'Enum fields require unique non-empty symbolic values.'));
|
|
108
|
+
}
|
|
109
|
+
if (field.pattern !== undefined) {
|
|
110
|
+
try { new RegExp(field.pattern, 'u'); }
|
|
111
|
+
catch (error) { errors.push(issue(`${prefix}.pattern`, `Invalid regular expression: ${error.message}`)); }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (definition.capabilities !== undefined) {
|
|
116
|
+
if (!isRecord(definition.capabilities)) errors.push(issue('capabilities', 'Capabilities must be an action-to-list object.'));
|
|
117
|
+
else for (const [action, values] of Object.entries(definition.capabilities)) {
|
|
118
|
+
if (!SYMBOL_RE.test(action) || !uniqueStrings(values) || values.some((value) => !CAPABILITY_RE.test(value))) {
|
|
119
|
+
errors.push(issue(`capabilities.${action}`, 'Use unique qualified capability IDs.'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (definition.projections !== undefined && !Array.isArray(definition.projections)) {
|
|
124
|
+
errors.push(issue('projections', 'Projections must be an array.'));
|
|
125
|
+
}
|
|
126
|
+
if (definition.aliases !== undefined && (!uniqueStrings(definition.aliases) ||
|
|
127
|
+
definition.aliases.some((alias) => !isPrimitiveTypeName(alias)))) {
|
|
128
|
+
errors.push(issue('aliases', 'Aliases must be unique safe primitive identifiers.'));
|
|
129
|
+
}
|
|
130
|
+
if (definition.migrations !== undefined && !Array.isArray(definition.migrations)) {
|
|
131
|
+
errors.push(issue('migrations', 'Migrations must be an array.'));
|
|
132
|
+
}
|
|
133
|
+
return { valid: errors.length === 0, errors };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Normalize a user/repo-authored definition and generate missing symbolic IDs.
|
|
138
|
+
*/
|
|
139
|
+
export function definePrimitive(input, options = {}) {
|
|
140
|
+
if (!isRecord(input)) throw new Error('Primitive input must be an object.');
|
|
141
|
+
const namespace = input.namespace || options.namespace;
|
|
142
|
+
const primitive_id = input.primitive_id || generatePrimitiveId({ namespace, name: input.name, salt: options.salt });
|
|
143
|
+
const fields = (input.fields || []).map((field, index) => ({
|
|
144
|
+
...field,
|
|
145
|
+
id: field.id || `field_${crypto.createHash('sha256')
|
|
146
|
+
.update(`${primitive_id}\0${field.name || index}`)
|
|
147
|
+
.digest('hex').slice(0, 12)}`,
|
|
148
|
+
}));
|
|
149
|
+
const definition = {
|
|
150
|
+
type: 'ssss:primitive',
|
|
151
|
+
version: 1,
|
|
152
|
+
revision: 1,
|
|
153
|
+
mutation: 'replace',
|
|
154
|
+
portability: 'structural',
|
|
155
|
+
scopes: ['workspace'],
|
|
156
|
+
...input,
|
|
157
|
+
namespace,
|
|
158
|
+
primitive_id,
|
|
159
|
+
fields,
|
|
160
|
+
};
|
|
161
|
+
const result = validatePrimitiveDefinition(definition);
|
|
162
|
+
if (!result.valid) {
|
|
163
|
+
const error = new Error(result.errors.map((entry) => `${entry.field}: ${entry.issue}`).join('; '));
|
|
164
|
+
error.field_errors = result.errors;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
return definition;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Convert a rich primitive definition into the executable registry schema. */
|
|
171
|
+
export function primitiveDefinitionToRegistryEntry(definition) {
|
|
172
|
+
const result = validatePrimitiveDefinition(definition);
|
|
173
|
+
if (!result.valid) throw new Error(result.errors.map((entry) => `${entry.field}: ${entry.issue}`).join('; '));
|
|
174
|
+
const required_fields = ['type'];
|
|
175
|
+
const optional_fields = [];
|
|
176
|
+
const enums = {};
|
|
177
|
+
const patterns = {};
|
|
178
|
+
const references = {};
|
|
179
|
+
for (const field of definition.fields) {
|
|
180
|
+
(field.required ? required_fields : optional_fields).push(field.id);
|
|
181
|
+
if (field.kind === 'enum') enums[field.id] = field.values;
|
|
182
|
+
if (field.pattern) patterns[field.id] = field.pattern;
|
|
183
|
+
if (field.kind === 'reference') references[field.id] = {
|
|
184
|
+
must_exist: field.must_exist !== false,
|
|
185
|
+
...(field.allowed_types ? { allowed_types: field.allowed_types } : {}),
|
|
186
|
+
...(field.allowed_portability ? { allowed_portability: field.allowed_portability } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
family: definition.family || 'extension',
|
|
191
|
+
canonical_path: definition.path_template || null,
|
|
192
|
+
append_only: definition.mutation === 'append',
|
|
193
|
+
portability: definition.portability,
|
|
194
|
+
required_fields,
|
|
195
|
+
optional_fields,
|
|
196
|
+
...(Object.keys(enums).length ? { enums } : {}),
|
|
197
|
+
...(Object.keys(patterns).length ? { patterns } : {}),
|
|
198
|
+
...(Object.keys(references).length ? { references } : {}),
|
|
199
|
+
scopes: definition.scopes,
|
|
200
|
+
capabilities: definition.capabilities || {},
|
|
201
|
+
events: definition.events || [],
|
|
202
|
+
projections: definition.projections || [],
|
|
203
|
+
ui: definition.ui || {},
|
|
204
|
+
aliases: definition.aliases || [],
|
|
205
|
+
migrations: definition.migrations || [],
|
|
206
|
+
primitive_version: definition.version,
|
|
207
|
+
primitive_revision: definition.revision || 1,
|
|
208
|
+
authored_language: definition.language || null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Package one or more definitions as an extension registry object. */
|
|
213
|
+
export function definitionsToExtensionRegistry(namespace, definitions, options = {}) {
|
|
214
|
+
if (!NAMESPACE_RE.test(namespace || '')) throw new Error('Invalid namespace.');
|
|
215
|
+
const document_primitives = {};
|
|
216
|
+
for (const definition of definitions) {
|
|
217
|
+
if (definition.namespace !== namespace) throw new Error(`Definition '${definition.primitive_id}' belongs to another namespace.`);
|
|
218
|
+
document_primitives[definition.primitive_id] = primitiveDefinitionToRegistryEntry(definition);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
registry: namespace,
|
|
222
|
+
extends: 'ssss',
|
|
223
|
+
version: options.version || '1.0.0',
|
|
224
|
+
requires: options.requires || {},
|
|
225
|
+
document_primitives,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Replayable, cursor-aware SSSS projection coordinator. */
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
function stable(value) {
|
|
5
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
6
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function projectionHash(value) {
|
|
11
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex')}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ProjectionCoordinator {
|
|
15
|
+
constructor({ eventStore, cursorStore = new Map() } = {}) {
|
|
16
|
+
if (!eventStore) throw new Error('ProjectionCoordinator requires an eventStore.');
|
|
17
|
+
this.eventStore = eventStore;
|
|
18
|
+
this.cursorStore = cursorStore;
|
|
19
|
+
this.projections = new Map();
|
|
20
|
+
}
|
|
21
|
+
register(definition) {
|
|
22
|
+
if (!definition?.id || typeof definition.apply !== 'function') throw new Error('Projection id and apply(event, context) are required.');
|
|
23
|
+
if (this.projections.has(definition.id)) throw new Error(`Duplicate projection '${definition.id}'.`);
|
|
24
|
+
this.projections.set(definition.id, definition);
|
|
25
|
+
}
|
|
26
|
+
async replay(id, options = {}) {
|
|
27
|
+
const projection = this.projections.get(id);
|
|
28
|
+
if (!projection) throw new Error(`Unknown projection '${id}'.`);
|
|
29
|
+
let cursor = options.rebuild ? 0 : (this.cursorStore.get(id) || 0);
|
|
30
|
+
if (options.rebuild && projection.reset) await projection.reset();
|
|
31
|
+
let applied = 0;
|
|
32
|
+
for await (const item of this.eventStore.replay({ cursor, workspaceId: options.workspaceId })) {
|
|
33
|
+
if (!projection.filter || projection.filter(item.event)) {
|
|
34
|
+
await projection.apply(item.event, { replay: true, cursor: item.cursor });
|
|
35
|
+
applied++;
|
|
36
|
+
}
|
|
37
|
+
cursor = item.cursor;
|
|
38
|
+
this.cursorStore.set(id, cursor);
|
|
39
|
+
}
|
|
40
|
+
const state = projection.snapshot ? await projection.snapshot() : null;
|
|
41
|
+
return { id, cursor, applied, output_hash: state === null ? null : projectionHash(state) };
|
|
42
|
+
}
|
|
43
|
+
async dispatch(event) {
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const projection of this.projections.values()) {
|
|
46
|
+
if (projection.filter && !projection.filter(event)) continue;
|
|
47
|
+
try {
|
|
48
|
+
await projection.apply(event, { replay: false });
|
|
49
|
+
results.push({ id: projection.id, success: true });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
results.push({ id: projection.id, success: false, error: error.message });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
async detectDrift(id, expectedHash) {
|
|
57
|
+
const projection = this.projections.get(id);
|
|
58
|
+
if (!projection?.snapshot) throw new Error(`Projection '${id}' does not expose snapshot().`);
|
|
59
|
+
const actualHash = projectionHash(await projection.snapshot());
|
|
60
|
+
return { drift: actualHash !== expectedHash, expected_hash: expectedHash, actual_hash: actualHash };
|
|
61
|
+
}
|
|
62
|
+
}
|