@gregiteen/ssss-cli 0.8.0 → 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.
Files changed (45) hide show
  1. package/README.md +11 -12
  2. package/VERSION +1 -1
  3. package/conformance/README.md +1 -1
  4. package/conformance/fixtures.json +4 -48
  5. package/conformance/reference-bundle.ucw.json +4 -21
  6. package/docs/help/bundle.md +2 -2
  7. package/docs/help/overview.md +4 -4
  8. package/docs/help/semantic.md +5 -4
  9. package/docs/ssss-spec.md +32 -77
  10. package/package.json +22 -3
  11. package/registry/core.json +21 -28
  12. package/scripts/build-reference-bundle.mjs +0 -20
  13. package/scripts/cmd-adapter.mjs +29 -0
  14. package/scripts/cmd-import.mjs +1 -1
  15. package/scripts/cmd-migrate.mjs +26 -0
  16. package/scripts/cmd-new.mjs +5 -5
  17. package/scripts/cmd-primitive.mjs +48 -0
  18. package/scripts/cmd-registry.mjs +25 -0
  19. package/scripts/cmd-semantic.mjs +6 -8
  20. package/scripts/conformance-09.mjs +199 -0
  21. package/scripts/conformance.mjs +106 -186
  22. package/scripts/ssss.mjs +8 -2
  23. package/skills/ssss/SKILL.md +11 -15
  24. package/skills/ssss/evals/evals.json +3 -3
  25. package/skills/ssss/references/contract-reference.md +7 -8
  26. package/skills/ssss/scripts/validate-schema.mjs +1 -44
  27. package/src/authorization.mjs +61 -0
  28. package/src/bundle.mjs +4 -4
  29. package/src/engine.mjs +101 -503
  30. package/src/events.mjs +90 -0
  31. package/src/guard.mjs +32 -0
  32. package/src/http.mjs +26 -0
  33. package/src/idempotency.mjs +55 -0
  34. package/src/kernel.mjs +260 -0
  35. package/src/leases.mjs +125 -0
  36. package/src/primitive.mjs +227 -0
  37. package/src/projections.mjs +62 -0
  38. package/src/registry.mjs +190 -39
  39. package/src/semantic.mjs +124 -316
  40. package/src/ui.mjs +100 -0
  41. package/src/validator.mjs +87 -0
  42. package/src/vfs.mjs +203 -0
  43. package/types/kernel.d.ts +48 -0
  44. package/docs/help/localization.md +0 -43
  45. package/scripts/cmd-localize.mjs +0 -44
@@ -11,7 +11,7 @@ This is an operational map. The normative source is the current
11
11
  | `registry/extensions/*.json` | Host/application types that cannot redefine core primitives. |
12
12
  | `src/engine.mjs` | Operation Contract, validation, authorization, leases, idempotency, atomic commit, audit. |
13
13
  | `src/runtime.mjs` | Workflow trigger planning and deterministic event/task/run envelopes. |
14
- | `src/semantic.mjs` | Deterministic semantic records/edges, localized search, and safe materialization. |
14
+ | `src/semantic.mjs` | Deterministic records/edges, multilingual embeddings, hybrid retrieval, and invariant rendering. |
15
15
  | `src/bundle.mjs` | Export, validate, provision, import, portability filtering, link/hash integrity. |
16
16
  | `src/registry.mjs` | Registry loading and type/portability resolution. |
17
17
  | `src/frontmatter.mjs` | Dependency-free parse/serialize with unknown-field preservation. |
@@ -35,8 +35,7 @@ document types.
35
35
  Current core document families include knowledge (`memory`), capability
36
36
  (`skill`, `page`), governance (`rule`, `security_role`), work (`task`,
37
37
  `workflow`), actors/catalog (`assistant`, `model`), transcripts (`conversation`,
38
- `run`), localization (`translation`), and lifecycle/meta (`conflict`, `migration`,
39
- `release`).
38
+ `run`), and lifecycle/meta (`conflict`, `migration`, `release`, `primitive`).
40
39
 
41
40
  The contract primitives are `operation`, `patch`, `event`, `delete`, and
42
41
  `lease`.
@@ -77,15 +76,15 @@ For a bundle:
77
76
  7. Internal links resolve after remapping.
78
77
  8. Re-import replays without new commits.
79
78
 
80
- For a semantic/localization projection:
79
+ For a multilingual semantic projection:
81
80
 
82
81
  1. Records and index hashes are deterministic for identical inputs.
83
82
  2. Stable identity, tokens, content hashes, portability, and graph edges are retained.
84
83
  3. Private and resource-bound documents are excluded unless explicitly authorized.
85
- 4. A translation targets one structural, non-translation source by safe relative path.
86
- 5. The source hash is exact and only natural-language surface fields change.
87
- 6. Duplicate, stale, traversing, symlinked, or recursive overlays fail closed.
88
- 7. Materialization occurs outside the source vault and remains rebuildable.
84
+ 4. Embedding records retain model identity and vector dimensions.
85
+ 5. Lexical and semantic evidence remain separately inspectable.
86
+ 6. Runtime rendering preserves all symbolic control fields.
87
+ 7. Language selection never widens authorization or portability scope.
89
88
 
90
89
  ## Drift checklist
91
90
 
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import crypto from 'node:crypto';
5
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
5
 
7
6
  const FALLBACK_UNIVERSAL = ['type', 'title', 'description', 'timestamp'];
@@ -20,7 +19,7 @@ const FALLBACK_REQUIRED = {
20
19
  page: ['slug', 'name', 'sandbox_entry'],
21
20
  migration: ['migration_id', 'from_version', 'to_version', 'status'],
22
21
  release: ['release_id', 'version', 'schema_version', 'summary', 'released_at'],
23
- translation: ['translation_id', 'source_path', 'source_hash', 'locale', 'status', 'translated_fields'],
22
+ primitive: ['primitive_id', 'namespace', 'version', 'name', 'mutation', 'portability', 'scopes', 'fields'],
24
23
  };
25
24
  const CONTRACT_TYPES = new Set(['operation', 'patch', 'event', 'delete', 'lease']);
26
25
  const APPEND_TYPES = new Set(['conversation', 'run']);
@@ -120,46 +119,6 @@ function isSafeReferencePath(value) {
120
119
  value.split('/').every((segment) => segment && segment !== '.' && segment !== '..');
121
120
  }
122
121
 
123
- function inferVaultRoot(target) {
124
- let cursor = path.dirname(target);
125
- while (path.dirname(cursor) !== cursor) {
126
- if (path.basename(cursor) === 'translations') return path.dirname(cursor);
127
- cursor = path.dirname(cursor);
128
- }
129
- return null;
130
- }
131
-
132
- async function validateTranslationReference(target, data, registry, kernelRoot, errors, warnings) {
133
- if (data.type !== 'translation' || isEmpty(data.source_path)) return;
134
- if (!isSafeReferencePath(data.source_path)) {
135
- errors.push(`Field 'source_path' must be a safe vault-relative Markdown path.`);
136
- return;
137
- }
138
- const vaultRoot = inferVaultRoot(target);
139
- if (!vaultRoot) {
140
- warnings.push("Cannot resolve translation source outside the canonical translations/<locale>/ layout.");
141
- return;
142
- }
143
- const source = path.resolve(vaultRoot, data.source_path);
144
- const relative = path.relative(vaultRoot, source);
145
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
146
- errors.push("Translation source_path escapes the vault.");
147
- return;
148
- }
149
- if (!fs.existsSync(source) || !fs.statSync(source).isFile() || fs.lstatSync(source).isSymbolicLink()) {
150
- errors.push(`Translation source does not exist as a regular non-symlinked file: ${data.source_path}.`);
151
- return;
152
- }
153
- const raw = fs.readFileSync(source, 'utf8');
154
- const parsed = await parseDocument(raw, kernelRoot);
155
- if (parsed.data.type === 'translation') errors.push('Translations may not target another translation.');
156
- const sourceDefinition = registry?.document_primitives?.[parsed.data.type];
157
- const portability = parsed.data.x_portability || sourceDefinition?.portability;
158
- if (portability !== 'structural') errors.push('Translations may target structural documents only.');
159
- const hash = `sha256:${crypto.createHash('sha256').update(raw).digest('hex')}`;
160
- if (data.source_hash !== hash) errors.push(`Translation source_hash is stale; expected '${hash}'.`);
161
- }
162
-
163
122
  export async function validateNode(filePath) {
164
123
  const target = path.resolve(filePath);
165
124
  const kernelRoot = findKernelRoot(path.dirname(target)) || findKernelRoot(process.cwd());
@@ -210,8 +169,6 @@ export async function validateNode(filePath) {
210
169
  }
211
170
  }
212
171
 
213
- await validateTranslationReference(target, data, registry, kernelRoot, errors, warnings);
214
-
215
172
  if (APPEND_TYPES.has(data.type) && body.trim() === '') {
216
173
  warnings.push(`Append-only primitive '${data.type}' has an empty body.`);
217
174
  }
@@ -0,0 +1,61 @@
1
+ /** Verified identity and fail-closed capability authorization contracts. */
2
+ const PRINCIPAL_KINDS = new Set(['human', 'agent', 'service', 'system']);
3
+ const ACTIONS = new Set(['create', 'replace', 'patch', 'append', 'event', 'delete', 'define', 'migrate']);
4
+ const ASSURANCE = { unverified: 0, verified: 1, elevated: 2, hardware: 3 };
5
+
6
+ export function validateVerifiedPrincipal(principal) {
7
+ const errors = [];
8
+ if (!principal || typeof principal !== 'object') errors.push('Verified principal is required.');
9
+ else {
10
+ if (typeof principal.id !== 'string' || !principal.id) errors.push('Principal id is required.');
11
+ if (!PRINCIPAL_KINDS.has(principal.kind)) errors.push(`Principal kind must be one of: ${[...PRINCIPAL_KINDS].join(', ')}.`);
12
+ if (!Array.isArray(principal.workspaceIds)) errors.push('Principal workspaceIds must be an array.');
13
+ if (!principal.authentication || typeof principal.authentication.provider !== 'string') {
14
+ errors.push('Authentication provenance is required.');
15
+ }
16
+ }
17
+ return { valid: errors.length === 0, errors };
18
+ }
19
+
20
+ export function requiredCapabilities(definition, action) {
21
+ if (!ACTIONS.has(action)) throw new Error(`Unknown authorization action '${action}'.`);
22
+ const declared = definition?.capabilities?.[action];
23
+ if (Array.isArray(declared) && declared.length) return declared;
24
+ const primitive = definition?.qualified_type || definition?.type || 'unknown';
25
+ return [`${primitive}:${action}`];
26
+ }
27
+
28
+ export function createCapabilityAuthorizer(options = {}) {
29
+ const policyFloors = options.policyFloors || {};
30
+ return async function authorize({ principal, workspaceId, definition, action, context = {} }) {
31
+ const principalValidation = validateVerifiedPrincipal(principal);
32
+ if (!principalValidation.valid) return { allowed: false, reason: principalValidation.errors.join(' ') };
33
+ if (principal.kind !== 'system' && !principal.workspaceIds.includes(workspaceId)) {
34
+ return { allowed: false, reason: `Principal '${principal.id}' is not scoped to workspace '${workspaceId}'.` };
35
+ }
36
+ const required = new Set([
37
+ ...requiredCapabilities(definition, action),
38
+ ...(policyFloors[definition?.qualified_type || definition?.type]?.[action] || []),
39
+ ...(context.policyFloor || []),
40
+ ]);
41
+ const granted = new Set(principal.capabilities || []);
42
+ if (principal.kind === 'system' && options.systemBypass !== false) return { allowed: true, required: [...required] };
43
+ const missing = [...required].filter((capability) =>
44
+ !granted.has(capability) && !granted.has('*:*') && !granted.has(`${capability.split(':')[0]}:*`));
45
+ if (missing.length) return { allowed: false, required: [...required], missing, reason: `Missing capabilities: ${missing.join(', ')}.` };
46
+ if (context.requiresHumanConfirmation && principal.kind !== 'human') {
47
+ return { allowed: false, required: [...required], reason: 'A verified human confirmation is required.' };
48
+ }
49
+ if (context.requiresHumanConfirmation && context.humanConfirmation !== true) {
50
+ return { allowed: false, required: [...required], reason: 'Explicit human confirmation is required.' };
51
+ }
52
+ if (context.requiredAssurance) {
53
+ const actual = ASSURANCE[principal.authentication.assurance] ?? -1;
54
+ const requiredAssurance = ASSURANCE[context.requiredAssurance] ?? Number.POSITIVE_INFINITY;
55
+ if (actual < requiredAssurance) {
56
+ return { allowed: false, required: [...required], reason: `Authentication step-up to '${context.requiredAssurance}' is required.` };
57
+ }
58
+ }
59
+ return { allowed: true, required: [...required] };
60
+ };
61
+ }
package/src/bundle.mjs CHANGED
@@ -429,7 +429,7 @@ export function provisionBundle(bundle, opts = {}) {
429
429
  * import(plan, target) → replay (§17.1). Runs each envelope through the engine
430
430
  * (§6.3). Idempotent: each envelope's idempotency_key makes a re-run a no-op.
431
431
  */
432
- export function importBundle(plan, vaultRoot, engine) {
432
+ export async function importBundle(plan, vaultRoot, engine) {
433
433
  const dryRunFlags = new Set(plan.map((envelope) => !!envelope.dry_run));
434
434
  if (dryRunFlags.size > 1) {
435
435
  return {
@@ -457,7 +457,7 @@ export function importBundle(plan, vaultRoot, engine) {
457
457
  // Two-phase import: validate the complete plan with in-plan references first.
458
458
  // A single invalid document prevents every commit, eliminating partial bundle
459
459
  // installation and allowing dry-run to validate cross-file constraints.
460
- const preflight = plan.map((envelope) => {
460
+ const preflight = await Promise.all(plan.map((envelope) => {
461
461
  const preflightKey = 'preflight-' + crypto.createHash('sha256')
462
462
  .update(`${envelope.workspace_id}:${envelope.idempotency_key}:${envelope.path}`)
463
463
  .digest('hex')
@@ -467,7 +467,7 @@ export function importBundle(plan, vaultRoot, engine) {
467
467
  vaultRoot,
468
468
  operationOptions
469
469
  );
470
- });
470
+ }));
471
471
  if (!preflight.every((result) => result.success)) {
472
472
  return { ok: false, committed: 0, wouldCommit: 0, results: preflight };
473
473
  }
@@ -480,7 +480,7 @@ export function importBundle(plan, vaultRoot, engine) {
480
480
  };
481
481
  }
482
482
  const results = [];
483
- for (const env of plan) results.push(engine.processOperation(env, vaultRoot, operationOptions));
483
+ for (const env of plan) results.push(await engine.processOperation(env, vaultRoot, operationOptions));
484
484
  const committed = results.filter((r) => r.success && !r.replay && !r.dry_run).length;
485
485
  return { ok: results.every((r) => r.success), committed, wouldCommit: 0, results };
486
486
  }