@gregiteen/ssss-cli 0.8.0 → 0.9.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 +55 -4
- package/README.md +42 -84
- package/VERSION +1 -1
- package/conformance/README.md +1 -1
- package/conformance/fixtures.json +4 -48
- package/conformance/reference-bundle.ucw.json +22 -21
- package/docs/help/bundle.md +2 -2
- package/docs/help/overview.md +9 -6
- package/docs/help/semantic.md +5 -4
- package/docs/ssss-spec.md +42 -86
- package/package.json +22 -3
- package/registry/core.json +22 -29
- package/scripts/build-reference-bundle.mjs +0 -20
- package/scripts/cmd-adapter.mjs +29 -0
- 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 +6 -8
- package/scripts/conformance-09.mjs +331 -0
- package/scripts/conformance.mjs +153 -186
- package/scripts/generate-sbom.mjs +96 -0
- package/scripts/phase9-cross-host-corpus.mjs +223 -0
- package/scripts/ssss.mjs +8 -2
- package/skills/ssss/SKILL.md +32 -208
- package/skills/ssss/evals/evals.json +3 -3
- package/skills/ssss/references/contract-reference.md +7 -8
- package/skills/ssss/scripts/validate-schema.mjs +1 -44
- package/src/authorization.mjs +62 -0
- package/src/bundle.mjs +41 -5
- package/src/engine.mjs +101 -503
- package/src/events.mjs +155 -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 +173 -0
- package/src/primitive.mjs +227 -0
- package/src/projections.mjs +133 -0
- package/src/registry.mjs +190 -39
- package/src/semantic.mjs +124 -316
- 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/docs/help/localization.md +0 -43
- package/scripts/cmd-localize.mjs +0 -44
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Canonical registry-driven validation shared by every SSSS host. */
|
|
2
|
+
import { parseDocument } from './frontmatter.mjs';
|
|
3
|
+
import {
|
|
4
|
+
composeRegistries,
|
|
5
|
+
loadRegistries,
|
|
6
|
+
resolvePrimitiveDefinition,
|
|
7
|
+
validateDataConstraints,
|
|
8
|
+
validateReferenceConstraints,
|
|
9
|
+
} from './registry.mjs';
|
|
10
|
+
import { validatePrimitiveDefinition } from './primitive.mjs';
|
|
11
|
+
|
|
12
|
+
function registryFor(options = {}) {
|
|
13
|
+
if (options.registrySet) return options.registrySet;
|
|
14
|
+
const base = loadRegistries(options.registryDir);
|
|
15
|
+
if (!options.extensions?.length) return base;
|
|
16
|
+
return composeRegistries({ core: base.core, extensions: options.extensions });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function result(declaredType, definition, issues, warnings = []) {
|
|
20
|
+
return {
|
|
21
|
+
valid: issues.length === 0,
|
|
22
|
+
type: definition?.qualified_type || declaredType || 'unknown',
|
|
23
|
+
declared_type: declaredType || null,
|
|
24
|
+
registry: definition?.registry || null,
|
|
25
|
+
primitive_version: definition?.primitive_version || 1,
|
|
26
|
+
errors: issues.map((entry) => `${entry.field}: ${entry.issue}`),
|
|
27
|
+
warnings,
|
|
28
|
+
field_errors: issues,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateData(data, options = {}) {
|
|
33
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
34
|
+
return result(null, null, [{ field: 'frontmatter', issue: 'Document frontmatter must be an object.' }]);
|
|
35
|
+
}
|
|
36
|
+
const declaredType = typeof data.type === 'string' ? data.type : null;
|
|
37
|
+
if (!declaredType) return result(null, null, [{ field: 'type', issue: "Missing required field 'type'." }]);
|
|
38
|
+
const registrySet = registryFor(options);
|
|
39
|
+
const definition = resolvePrimitiveDefinition(registrySet, declaredType);
|
|
40
|
+
if (!definition) return result(declaredType, null, [{ field: 'type', issue: `Unknown SSSS primitive '${declaredType}'.` }]);
|
|
41
|
+
const universal = options.enforceUniversalFields === false
|
|
42
|
+
? ['type']
|
|
43
|
+
: (registrySet.core.universal_frontmatter?.required || ['type']);
|
|
44
|
+
const issues = validateDataConstraints(definition, data, universal);
|
|
45
|
+
|
|
46
|
+
if (definition.qualified_type === 'ssss:primitive' || definition.type === 'primitive') {
|
|
47
|
+
issues.push(...validatePrimitiveDefinition(data).errors);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (definition.references && options.resolveReference) {
|
|
51
|
+
issues.push(...validateReferenceConstraints(
|
|
52
|
+
definition,
|
|
53
|
+
data,
|
|
54
|
+
options.resolveReference,
|
|
55
|
+
registrySet.types
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
if (options.validateExtension) {
|
|
59
|
+
const extra = options.validateExtension({ data, definition, registrySet }) || [];
|
|
60
|
+
issues.push(...extra);
|
|
61
|
+
}
|
|
62
|
+
return result(declaredType, definition, issues);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateDocument(content, options = {}) {
|
|
66
|
+
if (typeof content !== 'string') {
|
|
67
|
+
return result(null, null, [{ field: 'content', issue: 'Document content must be a string.' }]);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const parsed = parseDocument(content);
|
|
71
|
+
const validation = validateData(parsed.data, options);
|
|
72
|
+
return { ...validation, data: parsed.data, body: parsed.body };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return result(null, null, [{ field: 'frontmatter', issue: `Invalid frontmatter: ${error.message}` }]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createValidator(options = {}) {
|
|
79
|
+
const registrySet = registryFor(options);
|
|
80
|
+
const fixed = { ...options, registrySet, extensions: undefined, registryDir: undefined };
|
|
81
|
+
return {
|
|
82
|
+
registry: registrySet,
|
|
83
|
+
validateData: (data, callOptions = {}) => validateData(data, { ...fixed, ...callOptions, registrySet }),
|
|
84
|
+
validateDocument: (content, callOptions = {}) => validateDocument(content, { ...fixed, ...callOptions, registrySet }),
|
|
85
|
+
resolve: (type) => resolvePrimitiveDefinition(registrySet, type),
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/vfs.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/** Standard SSSS VFS adapter contract and reference implementations. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { isSafeDocumentPath } from './registry.mjs';
|
|
6
|
+
|
|
7
|
+
export class VfsConflictError extends Error {
|
|
8
|
+
constructor(message) { super(message); this.name = 'VfsConflictError'; this.code = 'SSSS_VFS_CONFLICT'; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class VfsPathError extends Error {
|
|
12
|
+
constructor(message) { super(message); this.name = 'VfsPathError'; this.code = 'SSSS_VFS_PATH'; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function bytesHash(bytes) {
|
|
16
|
+
return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function asBuffer(value) {
|
|
20
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function checkPrecondition(current, precondition = {}) {
|
|
24
|
+
if (precondition.ifAbsent && current) throw new VfsConflictError('Target already exists.');
|
|
25
|
+
if (precondition.ifPresent && !current) throw new VfsConflictError('Target does not exist.');
|
|
26
|
+
if (precondition.version !== undefined && current?.version !== precondition.version) {
|
|
27
|
+
throw new VfsConflictError(`Version mismatch; expected '${precondition.version}', found '${current?.version || '(missing)'}'.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class MemoryVfs {
|
|
32
|
+
#entries = new Map();
|
|
33
|
+
|
|
34
|
+
async read(vfsPath) {
|
|
35
|
+
if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
|
|
36
|
+
const entry = this.#entries.get(vfsPath);
|
|
37
|
+
return entry ? { bytes: Buffer.from(entry.bytes), version: entry.version, hash: entry.hash } : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async stat(vfsPath) {
|
|
41
|
+
const entry = await this.read(vfsPath);
|
|
42
|
+
return entry ? { path: vfsPath, kind: 'file', size: entry.bytes.length, version: entry.version, hash: entry.hash } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async *list(prefix = '') {
|
|
46
|
+
if (prefix && !isSafeDocumentPath(prefix)) throw new VfsPathError(`Unsafe VFS prefix '${prefix}'.`);
|
|
47
|
+
for (const key of [...this.#entries.keys()].sort()) {
|
|
48
|
+
if (!prefix || key === prefix || key.startsWith(`${prefix}/`)) yield await this.stat(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async writeAtomic(vfsPath, bytes, precondition = {}) {
|
|
53
|
+
if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
|
|
54
|
+
const current = await this.read(vfsPath);
|
|
55
|
+
checkPrecondition(current, precondition);
|
|
56
|
+
const nextBytes = asBuffer(bytes);
|
|
57
|
+
const hash = bytesHash(nextBytes);
|
|
58
|
+
const version = hash;
|
|
59
|
+
this.#entries.set(vfsPath, { bytes: Buffer.from(nextBytes), hash, version });
|
|
60
|
+
return { path: vfsPath, hash, version, size: nextBytes.length };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async append(vfsPath, bytes, precondition = {}) {
|
|
64
|
+
const current = await this.read(vfsPath);
|
|
65
|
+
checkPrecondition(current, precondition);
|
|
66
|
+
const combined = Buffer.concat([current?.bytes || Buffer.alloc(0), asBuffer(bytes)]);
|
|
67
|
+
return this.writeAtomic(vfsPath, combined, current ? { version: current.version } : { ifAbsent: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async remove(vfsPath, precondition = {}) {
|
|
71
|
+
const current = await this.read(vfsPath);
|
|
72
|
+
checkPrecondition(current, precondition);
|
|
73
|
+
if (!current) return { path: vfsPath, removed: false };
|
|
74
|
+
this.#entries.delete(vfsPath);
|
|
75
|
+
return { path: vfsPath, removed: true, previous_hash: current.hash };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class FileSystemVfs {
|
|
80
|
+
constructor(root) {
|
|
81
|
+
if (!root) throw new Error('FileSystemVfs requires a root directory.');
|
|
82
|
+
fs.mkdirSync(root, { recursive: true });
|
|
83
|
+
this.root = fs.realpathSync(root);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#resolve(vfsPath, { allowMissing = true } = {}) {
|
|
87
|
+
if (!isSafeDocumentPath(vfsPath)) throw new VfsPathError(`Unsafe VFS path '${vfsPath}'.`);
|
|
88
|
+
const target = path.resolve(this.root, ...vfsPath.split('/'));
|
|
89
|
+
if (target === this.root || !target.startsWith(`${this.root}${path.sep}`)) {
|
|
90
|
+
throw new VfsPathError(`VFS path '${vfsPath}' escapes the root.`);
|
|
91
|
+
}
|
|
92
|
+
let cursor = target;
|
|
93
|
+
while (!fs.existsSync(cursor)) {
|
|
94
|
+
const parent = path.dirname(cursor);
|
|
95
|
+
if (parent === cursor) break;
|
|
96
|
+
cursor = parent;
|
|
97
|
+
}
|
|
98
|
+
if (fs.existsSync(cursor)) {
|
|
99
|
+
const stat = fs.lstatSync(cursor);
|
|
100
|
+
if (stat.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS path is forbidden: '${vfsPath}'.`);
|
|
101
|
+
const real = fs.realpathSync(cursor);
|
|
102
|
+
if (real !== this.root && !real.startsWith(`${this.root}${path.sep}`)) {
|
|
103
|
+
throw new VfsPathError(`VFS path '${vfsPath}' escapes through a filesystem alias.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!allowMissing && !fs.existsSync(target)) return null;
|
|
107
|
+
return target;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async read(vfsPath) {
|
|
111
|
+
const target = this.#resolve(vfsPath, { allowMissing: false });
|
|
112
|
+
if (!target) return null;
|
|
113
|
+
const stat = fs.lstatSync(target);
|
|
114
|
+
if (stat.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS path is forbidden: '${vfsPath}'.`);
|
|
115
|
+
if (!stat.isFile()) throw new VfsPathError(`VFS path '${vfsPath}' is not a file.`);
|
|
116
|
+
const bytes = fs.readFileSync(target);
|
|
117
|
+
const hash = bytesHash(bytes);
|
|
118
|
+
return { bytes, hash, version: hash };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async stat(vfsPath) {
|
|
122
|
+
const entry = await this.read(vfsPath);
|
|
123
|
+
return entry ? { path: vfsPath, kind: 'file', size: entry.bytes.length, hash: entry.hash, version: entry.version } : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async *list(prefix = '') {
|
|
127
|
+
if (prefix && !isSafeDocumentPath(prefix)) throw new VfsPathError(`Unsafe VFS prefix '${prefix}'.`);
|
|
128
|
+
const start = prefix ? this.#resolve(prefix) : this.root;
|
|
129
|
+
if (!fs.existsSync(start)) return;
|
|
130
|
+
const stack = [start];
|
|
131
|
+
while (stack.length) {
|
|
132
|
+
const dir = stack.pop();
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
134
|
+
const absolute = path.join(dir, entry.name);
|
|
135
|
+
const relative = path.relative(this.root, absolute).split(path.sep).join('/');
|
|
136
|
+
if (entry.isSymbolicLink()) throw new VfsPathError(`Symlinked VFS entry is forbidden: '${relative}'.`);
|
|
137
|
+
if (entry.isDirectory()) stack.push(absolute);
|
|
138
|
+
else if (entry.isFile() && (!prefix || relative === prefix || relative.startsWith(`${prefix}/`))) {
|
|
139
|
+
yield await this.stat(relative);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async writeAtomic(vfsPath, bytes, precondition = {}) {
|
|
146
|
+
const target = this.#resolve(vfsPath);
|
|
147
|
+
const current = await this.read(vfsPath);
|
|
148
|
+
checkPrecondition(current, precondition);
|
|
149
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
150
|
+
const parentReal = fs.realpathSync(path.dirname(target));
|
|
151
|
+
if (parentReal !== this.root && !parentReal.startsWith(`${this.root}${path.sep}`)) {
|
|
152
|
+
throw new VfsPathError(`VFS parent for '${vfsPath}' escapes the root.`);
|
|
153
|
+
}
|
|
154
|
+
const nextBytes = asBuffer(bytes);
|
|
155
|
+
const temp = path.join(path.dirname(target), `.${path.basename(target)}.${crypto.randomUUID()}.tmp`);
|
|
156
|
+
try {
|
|
157
|
+
fs.writeFileSync(temp, nextBytes, { flag: 'wx', mode: 0o600 });
|
|
158
|
+
fs.renameSync(temp, target);
|
|
159
|
+
} finally {
|
|
160
|
+
fs.rmSync(temp, { force: true });
|
|
161
|
+
}
|
|
162
|
+
const hash = bytesHash(nextBytes);
|
|
163
|
+
return { path: vfsPath, hash, version: hash, size: nextBytes.length };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async append(vfsPath, bytes, precondition = {}) {
|
|
167
|
+
const current = await this.read(vfsPath);
|
|
168
|
+
checkPrecondition(current, precondition);
|
|
169
|
+
const combined = Buffer.concat([current?.bytes || Buffer.alloc(0), asBuffer(bytes)]);
|
|
170
|
+
return this.writeAtomic(vfsPath, combined, current ? { version: current.version } : { ifAbsent: true });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async remove(vfsPath, precondition = {}) {
|
|
174
|
+
const target = this.#resolve(vfsPath, { allowMissing: false });
|
|
175
|
+
const current = await this.read(vfsPath);
|
|
176
|
+
checkPrecondition(current, precondition);
|
|
177
|
+
if (!target || !current) return { path: vfsPath, removed: false };
|
|
178
|
+
fs.rmSync(target);
|
|
179
|
+
return { path: vfsPath, removed: true, previous_hash: current.hash };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function runVfsContract(vfs) {
|
|
184
|
+
const pathName = `contract/${crypto.randomUUID()}.md`;
|
|
185
|
+
const checks = [];
|
|
186
|
+
const record = (name, condition) => checks.push({ name, passed: !!condition });
|
|
187
|
+
record('missing read returns null', await vfs.read(pathName) === null);
|
|
188
|
+
const first = await vfs.writeAtomic(pathName, 'one', { ifAbsent: true });
|
|
189
|
+
record('atomic create returns version', typeof first.version === 'string');
|
|
190
|
+
const read = await vfs.read(pathName);
|
|
191
|
+
record('read preserves bytes', read?.bytes.toString() === 'one');
|
|
192
|
+
let conflict = false;
|
|
193
|
+
try { await vfs.writeAtomic(pathName, 'two', { version: 'wrong' }); } catch (error) { conflict = error instanceof VfsConflictError; }
|
|
194
|
+
record('compare-and-swap rejects stale version', conflict);
|
|
195
|
+
const appended = await vfs.append(pathName, '\ntwo', { version: first.version });
|
|
196
|
+
record('append preserves existing bytes', (await vfs.read(pathName)).bytes.toString() === 'one\ntwo');
|
|
197
|
+
await vfs.remove(pathName, { version: appended.version });
|
|
198
|
+
record('remove deletes target', await vfs.read(pathName) === null);
|
|
199
|
+
let traversal = false;
|
|
200
|
+
try { await vfs.read('../escape.md'); } catch (error) { traversal = error instanceof VfsPathError; }
|
|
201
|
+
record('traversal is rejected', traversal);
|
|
202
|
+
return { passed: checks.every((check) => check.passed), checks };
|
|
203
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type SsssCommandType = 'operation' | 'patch' | 'event' | 'delete';
|
|
2
|
+
|
|
3
|
+
export interface VerifiedPrincipal {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: 'human' | 'agent' | 'service' | 'system';
|
|
6
|
+
workspaceIds: string[];
|
|
7
|
+
capabilities?: string[];
|
|
8
|
+
authentication: { provider: string; assurance: string; [key: string]: unknown };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SsssCommand {
|
|
12
|
+
type: SsssCommandType;
|
|
13
|
+
workspace_id: string;
|
|
14
|
+
idempotency_key: string;
|
|
15
|
+
path: string;
|
|
16
|
+
operation_id?: string;
|
|
17
|
+
primitive_type?: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
patches?: Record<string, unknown> & { __body__?: string };
|
|
20
|
+
lease_id?: string;
|
|
21
|
+
dry_run?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SsssResponse {
|
|
25
|
+
success: boolean;
|
|
26
|
+
type: SsssCommandType | null;
|
|
27
|
+
operation_id: string;
|
|
28
|
+
path: string;
|
|
29
|
+
committed_at: string | null;
|
|
30
|
+
dry_run: boolean;
|
|
31
|
+
replay?: boolean;
|
|
32
|
+
event_id?: string;
|
|
33
|
+
validation: { valid: boolean; type: string | null; errors: string[]; warnings: string[] };
|
|
34
|
+
repair?: { field_errors: Array<{ field: string; issue: string }> };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SsssKernel {
|
|
38
|
+
execute(command: SsssCommand, context: { principal: VerifiedPrincipal; [key: string]: unknown }): Promise<SsssResponse>;
|
|
39
|
+
registry: unknown;
|
|
40
|
+
validator: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function canonicalRequestHash(command: SsssCommand, principal: VerifiedPrincipal): string;
|
|
44
|
+
export class MemoryIdempotencyStore {
|
|
45
|
+
get(workspaceId: string, idempotencyKey: string): Promise<unknown>;
|
|
46
|
+
put(workspaceId: string, idempotencyKey: string, value: unknown): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export function createKernel(options: Record<string, unknown>): SsssKernel;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# Localization and translation overlays (spec §5.4, §11.9)
|
|
2
|
-
|
|
3
|
-
A `translation` is a structural overlay for one exact source document. It never
|
|
4
|
-
forks the source or translates symbolic control fields.
|
|
5
|
-
|
|
6
|
-
```yaml
|
|
7
|
-
---
|
|
8
|
-
type: translation
|
|
9
|
-
title: "Política de reembolsos — español"
|
|
10
|
-
description: "Traducción al español."
|
|
11
|
-
timestamp: 2026-07-10T00:00:00Z
|
|
12
|
-
translation_id: refund-policy-es
|
|
13
|
-
source_path: rules/refund-policy.md
|
|
14
|
-
source_hash: sha256:<hash-of-current-source-bytes>
|
|
15
|
-
locale: es
|
|
16
|
-
status: approved
|
|
17
|
-
translated_fields: [title, description, body]
|
|
18
|
-
translated_title: "Política de reembolsos"
|
|
19
|
-
translated_description: "Reglas de reembolso para compradores."
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
Contenido traducido.
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Only `title`, `description`, and body may change. `type`, ids, status values,
|
|
26
|
-
permissions, relations, portability, and every other control field remain canonical.
|
|
27
|
-
The engine rejects stale source hashes, missing or private sources, traversal,
|
|
28
|
-
symlinks, recursive translation, and changes to immutable overlay identity.
|
|
29
|
-
|
|
30
|
-
Draft and reviewed overlays are retained for workflow, but normal localization uses
|
|
31
|
-
approved overlays only. Use `--include-drafts` only for an explicit preview.
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
ssss localize ./vault --locale es --out ./derived/es
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The output must be a new or empty, non-symlinked directory outside the source vault.
|
|
38
|
-
It preserves source-relative paths, excludes private/resource-bound documents by
|
|
39
|
-
default, and writes `.ssss-projection.json` so the derived result is auditable and
|
|
40
|
-
rebuildable.
|
|
41
|
-
|
|
42
|
-
Use `ssss semantic ./vault --locale es` to query localized surfaces without
|
|
43
|
-
materializing files. See also: `ssss help semantic`.
|
package/scripts/cmd-localize.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/** `ssss localize` — materialize a non-destructive locale projection. */
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { materializeLocale } from '../src/semantic.mjs';
|
|
4
|
-
import { parseArgs, usage, die, wantsHelp } from './lib/cli.mjs';
|
|
5
|
-
|
|
6
|
-
const HELP = usage({
|
|
7
|
-
summary: 'Materialize approved translation overlays as a derived vault projection (spec §11.9).',
|
|
8
|
-
usage: 'ssss localize <vault-dir> --locale <tag> --out <empty-dir> [options]',
|
|
9
|
-
options: [
|
|
10
|
-
['--locale <tag>', 'Required BCP47-style target locale.'],
|
|
11
|
-
['--out <dir>', 'Required empty directory outside the source vault.'],
|
|
12
|
-
['--include-private', 'Explicitly include private/resource-bound source documents.'],
|
|
13
|
-
['--include-drafts', 'Allow non-approved translation overlays.'],
|
|
14
|
-
['--registry <dir>', 'Registry directory (default: package registry).'],
|
|
15
|
-
],
|
|
16
|
-
examples: [
|
|
17
|
-
'ssss localize ./vault --locale es --out ./derived/es',
|
|
18
|
-
],
|
|
19
|
-
seeAlso: ['ssss semantic', 'ssss help localization'],
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export async function run(argv) {
|
|
23
|
-
if (wantsHelp(argv)) { console.log(HELP); return; }
|
|
24
|
-
const { positionals, flags } = parseArgs(argv, {
|
|
25
|
-
booleans: ['include-private', 'include-drafts'],
|
|
26
|
-
});
|
|
27
|
-
const vault = positionals[0];
|
|
28
|
-
if (!vault || !flags.locale || !flags.out) die('localize requires a vault, --locale, and --out.\n\n' + HELP);
|
|
29
|
-
try {
|
|
30
|
-
const manifest = materializeLocale(
|
|
31
|
-
path.resolve(vault),
|
|
32
|
-
flags.locale,
|
|
33
|
-
path.resolve(flags.out),
|
|
34
|
-
{
|
|
35
|
-
includePrivate: !!flags['include-private'],
|
|
36
|
-
includeDrafts: !!flags['include-drafts'],
|
|
37
|
-
registryDir: flags.registry,
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
|
|
41
|
-
} catch (error) {
|
|
42
|
-
die(error.message);
|
|
43
|
-
}
|
|
44
|
-
}
|