@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
package/src/semantic.mjs
CHANGED
|
@@ -1,33 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Semantic projection, multilingual embedding, retrieval, and runtime rendering.
|
|
3
|
+
* Canonical documents remain authored once; translations are never stored here.
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
|
-
import { parseDocument
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
isSafeDocumentPath,
|
|
12
|
-
loadRegistries,
|
|
13
|
-
resolvePortability,
|
|
14
|
-
validateDataConstraints,
|
|
15
|
-
validateReferenceConstraints,
|
|
16
|
-
} from './registry.mjs';
|
|
8
|
+
import { parseDocument } from './frontmatter.mjs';
|
|
9
|
+
import { createValidator } from './validator.mjs';
|
|
10
|
+
import { isSafeDocumentPath, resolvePortability, resolvePrimitiveDefinition, documentHash } from './registry.mjs';
|
|
17
11
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const APPROVED_STATUSES = new Set(['approved']);
|
|
12
|
+
const LANGUAGE_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/u;
|
|
13
|
+
const RENDER_FIELDS = new Set(['title', 'description', 'body']);
|
|
21
14
|
|
|
22
15
|
function asStrings(value) {
|
|
23
16
|
if (Array.isArray(value)) return value.filter((item) => typeof item === 'string' && item);
|
|
24
17
|
return typeof value === 'string' && value ? [value] : [];
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
export function
|
|
28
|
-
if (typeof value !== 'string' || !
|
|
29
|
-
throw new Error(`Invalid locale '${value}'. Use a BCP47-style tag such as en, es-MX, or zh-Hant.`);
|
|
30
|
-
}
|
|
20
|
+
export function normalizeLanguage(value) {
|
|
21
|
+
if (typeof value !== 'string' || !LANGUAGE_RE.test(value)) throw new Error(`Invalid language tag '${value}'.`);
|
|
31
22
|
return value.split('-').map((part, index) => {
|
|
32
23
|
if (index === 0) return part.toLowerCase();
|
|
33
24
|
if (part.length === 2 || /^\d{3}$/.test(part)) return part.toUpperCase();
|
|
@@ -36,153 +27,40 @@ export function normalizeLocale(value, pattern = DEFAULT_LOCALE_PATTERN) {
|
|
|
36
27
|
}).join('-');
|
|
37
28
|
}
|
|
38
29
|
|
|
30
|
+
function tokenize(value) {
|
|
31
|
+
return [...new Set(String(value || '').normalize('NFKC').toLocaleLowerCase().match(/[\p{L}\p{N}]+/gu) || [])].sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
39
34
|
function walkDocuments(vaultRoot) {
|
|
40
|
-
const root = path.resolve(vaultRoot);
|
|
41
|
-
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
42
|
-
throw new Error(`Vault directory does not exist: ${root}`);
|
|
43
|
-
}
|
|
35
|
+
const root = fs.realpathSync(path.resolve(vaultRoot));
|
|
44
36
|
const documents = [];
|
|
45
37
|
const stack = [root];
|
|
46
38
|
while (stack.length) {
|
|
47
39
|
const dir = stack.pop();
|
|
48
|
-
const
|
|
49
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
50
|
-
for (const entry of entries) {
|
|
40
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
51
41
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
52
42
|
const absolute = path.join(dir, entry.name);
|
|
53
43
|
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
|
54
|
-
if (entry.isSymbolicLink()) {
|
|
55
|
-
throw new Error(`Refusing symlinked vault entry: ${relative}`);
|
|
56
|
-
}
|
|
44
|
+
if (entry.isSymbolicLink()) throw new Error(`Refusing symlinked vault entry: ${relative}`);
|
|
57
45
|
if (entry.isDirectory()) stack.push(absolute);
|
|
58
|
-
else if (entry.isFile() && entry.name.endsWith('.md') &&
|
|
59
|
-
entry.name !== 'index.md' && entry.name !== 'log.md') {
|
|
46
|
+
else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'index.md' && entry.name !== 'log.md') {
|
|
60
47
|
if (!isSafeDocumentPath(relative)) throw new Error(`Unsafe vault path: ${relative}`);
|
|
61
48
|
const content = fs.readFileSync(absolute, 'utf8');
|
|
62
49
|
const parsed = parseDocument(content);
|
|
63
|
-
if (
|
|
64
|
-
documents.push({ path: relative, absolute, content, ...parsed });
|
|
50
|
+
if (parsed.data.type) documents.push({ path: relative, content, ...parsed });
|
|
65
51
|
}
|
|
66
52
|
}
|
|
67
53
|
}
|
|
68
54
|
return documents.sort((a, b) => a.path.localeCompare(b.path));
|
|
69
55
|
}
|
|
70
56
|
|
|
71
|
-
function documentMap(documents) {
|
|
72
|
-
return new Map(documents.map((document) => [document.path, document]));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function validateReferences(document, documents, types) {
|
|
76
|
-
const definition = types.get(document.data.type);
|
|
77
|
-
return validateReferenceConstraints(
|
|
78
|
-
definition,
|
|
79
|
-
document.data,
|
|
80
|
-
(referencePath) => documents.get(referencePath) || null,
|
|
81
|
-
types
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function translationMap(documents, locale, types, { includeDrafts = false } = {}) {
|
|
86
|
-
const wanted = normalizeLocale(locale);
|
|
87
|
-
const bySource = new Map();
|
|
88
|
-
for (const document of documents.values()) {
|
|
89
|
-
if (document.data.type !== 'translation') continue;
|
|
90
|
-
const normalized = normalizeLocale(document.data.locale);
|
|
91
|
-
if (normalized !== wanted) continue;
|
|
92
|
-
if (!includeDrafts && !APPROVED_STATUSES.has(document.data.status)) continue;
|
|
93
|
-
const key = document.data.source_path;
|
|
94
|
-
if (bySource.has(key)) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Duplicate translation overlays for '${key}' and locale '${wanted}': ` +
|
|
97
|
-
`${bySource.get(key).path}, ${document.path}`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
const issues = validateReferences(document, documents, types);
|
|
101
|
-
if (issues.length) {
|
|
102
|
-
throw new Error(`${document.path}: ${issues.map((issue) => `${issue.field}: ${issue.issue}`).join('; ')}`);
|
|
103
|
-
}
|
|
104
|
-
bySource.set(key, document);
|
|
105
|
-
}
|
|
106
|
-
return bySource;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function applyTranslationOverlay(sourceContent, translationContent, options = {}) {
|
|
110
|
-
const { types, core } = loadRegistries(options.registryDir);
|
|
111
|
-
const source = parseDocument(sourceContent);
|
|
112
|
-
const overlay = parseDocument(translationContent);
|
|
113
|
-
if (overlay.data.type !== 'translation') throw new Error('Overlay must declare type: translation.');
|
|
114
|
-
if (source.data.type === 'translation') throw new Error('Translations may not target another translation.');
|
|
115
|
-
const sourcePath = options.sourcePath || overlay.data.source_path;
|
|
116
|
-
if (!isSafeDocumentPath(sourcePath) || overlay.data.source_path !== sourcePath) {
|
|
117
|
-
throw new Error(`Translation source_path '${overlay.data.source_path}' does not match '${sourcePath}'.`);
|
|
118
|
-
}
|
|
119
|
-
if (resolvePortability(types.get(source.data.type), source.data) !== 'structural') {
|
|
120
|
-
throw new Error('Translation overlays may target structural documents only.');
|
|
121
|
-
}
|
|
122
|
-
const expectedHash = documentHash(sourceContent);
|
|
123
|
-
if (overlay.data.source_hash !== expectedHash) {
|
|
124
|
-
throw new Error(`Stale translation for '${sourcePath}'; expected source_hash '${expectedHash}'.`);
|
|
125
|
-
}
|
|
126
|
-
const localePattern = new RegExp(core.semantic?.locale_pattern || DEFAULT_LOCALE_PATTERN.source, 'u');
|
|
127
|
-
const locale = normalizeLocale(overlay.data.locale, localePattern);
|
|
128
|
-
const translatedFields = [...new Set(asStrings(overlay.data.translated_fields))];
|
|
129
|
-
if (!translatedFields.length) throw new Error('translated_fields must not be empty.');
|
|
130
|
-
for (const field of translatedFields) {
|
|
131
|
-
if (!TRANSLATABLE_FIELDS.has(field)) {
|
|
132
|
-
throw new Error(`Field '${field}' is a symbolic control field and may not be translated.`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const data = { ...source.data };
|
|
137
|
-
let body = source.body;
|
|
138
|
-
if (translatedFields.includes('title')) {
|
|
139
|
-
if (!overlay.data.translated_title) throw new Error('translated_title is required when translating title.');
|
|
140
|
-
data.title = overlay.data.translated_title;
|
|
141
|
-
}
|
|
142
|
-
if (translatedFields.includes('description')) {
|
|
143
|
-
if (!overlay.data.translated_description) {
|
|
144
|
-
throw new Error('translated_description is required when translating description.');
|
|
145
|
-
}
|
|
146
|
-
data.description = overlay.data.translated_description;
|
|
147
|
-
}
|
|
148
|
-
if (translatedFields.includes('body')) {
|
|
149
|
-
if (!overlay.body.trim()) throw new Error('Translation body must not be empty when translating body.');
|
|
150
|
-
body = overlay.body;
|
|
151
|
-
}
|
|
152
|
-
data.locale = locale;
|
|
153
|
-
data.translation_of = sourcePath;
|
|
154
|
-
data.translation_source_hash = expectedHash;
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
content: serializeDocument(data, body),
|
|
158
|
-
data,
|
|
159
|
-
body,
|
|
160
|
-
locale,
|
|
161
|
-
status: overlay.data.status,
|
|
162
|
-
translated_fields: translatedFields,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function tokenize(value) {
|
|
167
|
-
const normalized = String(value || '').normalize('NFKC').toLocaleLowerCase();
|
|
168
|
-
return [...new Set(normalized.match(/[\p{L}\p{N}]+/gu) || [])].sort();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
57
|
function extractEdges(documentPath, data, body) {
|
|
172
58
|
const edges = [];
|
|
173
|
-
for (const match of body.matchAll(/\[\[([^\]]+)\]\]/g)) {
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
for (const match of body.matchAll(/\[[^\]]*\]\((\/[^)]+\.md)\)/g)) {
|
|
177
|
-
edges.push({ source: documentPath, relation: 'links_to', target: match[1].slice(1) });
|
|
178
|
-
}
|
|
59
|
+
for (const match of body.matchAll(/\[\[([^\]]+)\]\]/g)) edges.push({ source: documentPath, relation: 'mentions', target: match[1].trim() });
|
|
60
|
+
for (const match of body.matchAll(/\[[^\]]*\]\((\/[^)]+\.md)\)/g)) edges.push({ source: documentPath, relation: 'links_to', target: match[1].slice(1) });
|
|
179
61
|
for (const relation of Array.isArray(data.relations) ? data.relations : []) {
|
|
180
62
|
if (relation && typeof relation === 'object' && relation.target) {
|
|
181
|
-
edges.push({
|
|
182
|
-
source: documentPath,
|
|
183
|
-
relation: String(relation.relation || relation.type || 'related_to'),
|
|
184
|
-
target: String(relation.target),
|
|
185
|
-
});
|
|
63
|
+
edges.push({ source: documentPath, relation: String(relation.relation || relation.type || 'related_to'), target: String(relation.target) });
|
|
186
64
|
}
|
|
187
65
|
}
|
|
188
66
|
return edges;
|
|
@@ -192,211 +70,141 @@ function canonicalHash(value) {
|
|
|
192
70
|
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
|
|
193
71
|
}
|
|
194
72
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
const all = walkDocuments(vaultRoot);
|
|
198
|
-
const documents = documentMap(all);
|
|
199
|
-
for (const document of all) {
|
|
200
|
-
const definition = types.get(document.data.type);
|
|
201
|
-
if (!definition) throw new Error(`${document.path}: unknown type '${document.data.type}'.`);
|
|
202
|
-
const issues = [
|
|
203
|
-
...validateDataConstraints(
|
|
204
|
-
definition,
|
|
205
|
-
document.data,
|
|
206
|
-
core.universal_frontmatter?.required || ['type']
|
|
207
|
-
),
|
|
208
|
-
...(document.data.type === 'translation' ? [] : validateReferences(document, documents, types)),
|
|
209
|
-
];
|
|
210
|
-
if (issues.length) {
|
|
211
|
-
throw new Error(`${document.path}: ${issues.map((issue) => `${issue.field}: ${issue.issue}`).join('; ')}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const overlays = options.locale
|
|
215
|
-
? translationMap(documents, options.locale, types, options)
|
|
216
|
-
: new Map();
|
|
217
|
-
const output = [];
|
|
218
|
-
let excluded = 0;
|
|
219
|
-
|
|
220
|
-
for (const source of all) {
|
|
221
|
-
if (source.data.type === 'translation') continue;
|
|
222
|
-
const portability = resolvePortability(types.get(source.data.type), source.data);
|
|
223
|
-
if (!options.includePrivate && portability !== 'structural') {
|
|
224
|
-
excluded++;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const overlay = overlays.get(source.path);
|
|
228
|
-
const localized = overlay
|
|
229
|
-
? applyTranslationOverlay(source.content, overlay.content, {
|
|
230
|
-
sourcePath: source.path,
|
|
231
|
-
registryDir: options.registryDir,
|
|
232
|
-
})
|
|
233
|
-
: { content: source.content, data: source.data, body: source.body, locale: source.data.locale || null };
|
|
234
|
-
output.push({
|
|
235
|
-
path: source.path,
|
|
236
|
-
source,
|
|
237
|
-
overlay: overlay || null,
|
|
238
|
-
portability,
|
|
239
|
-
...localized,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
return { documents: output, excluded };
|
|
73
|
+
function semanticText(record) {
|
|
74
|
+
return [record.title, record.description, record.surface?.body, record.aliases?.join(' '), record.tags?.join(' ')].filter(Boolean).join('\n');
|
|
243
75
|
}
|
|
244
76
|
|
|
245
77
|
export function buildSemanticIndex(vaultRoot, options = {}) {
|
|
246
|
-
const
|
|
247
|
-
const localized = localizedDocuments(vaultRoot, { ...options, locale });
|
|
78
|
+
const validator = options.validator || createValidator(options);
|
|
248
79
|
const records = [];
|
|
249
80
|
const edges = [];
|
|
250
|
-
|
|
251
|
-
for (const document of
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
81
|
+
let excluded = 0;
|
|
82
|
+
for (const document of walkDocuments(vaultRoot)) {
|
|
83
|
+
const validation = validator.validateDocument(document.content);
|
|
84
|
+
if (!validation.valid) throw new Error(`${document.path}: ${validation.errors.join('; ')}`);
|
|
85
|
+
const definition = resolvePrimitiveDefinition(validator.registry, validation.declared_type);
|
|
86
|
+
const portability = resolvePortability(definition, document.data);
|
|
87
|
+
if (!options.includePrivate && portability !== 'structural') { excluded++; continue; }
|
|
88
|
+
const identity = document.data.semantic_id || document.data.resource || `${definition.qualified_type}:${document.path}`;
|
|
89
|
+
const labels = [document.data.title, document.data.description, document.data.name, document.data.slug, ...asStrings(document.data.aliases)];
|
|
90
|
+
const tags = asStrings(document.data.tags);
|
|
256
91
|
const record = {
|
|
257
92
|
semantic_id: identity,
|
|
258
93
|
path: document.path,
|
|
259
|
-
type:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
94
|
+
type: definition.qualified_type,
|
|
95
|
+
primitive_version: definition.primitive_version || 1,
|
|
96
|
+
portability,
|
|
97
|
+
authored_language: document.data.language || document.data.locale || null,
|
|
98
|
+
title: document.data.title || null,
|
|
99
|
+
description: document.data.description || null,
|
|
100
|
+
resource: document.data.resource || null,
|
|
101
|
+
aliases: asStrings(document.data.aliases),
|
|
266
102
|
tags,
|
|
103
|
+
relations: Array.isArray(document.data.relations) ? document.data.relations : [],
|
|
267
104
|
content_hash: documentHash(document.content),
|
|
268
|
-
|
|
269
|
-
path: document.overlay.path,
|
|
270
|
-
status: document.overlay.data.status,
|
|
271
|
-
source_hash: document.overlay.data.source_hash,
|
|
272
|
-
} : null,
|
|
105
|
+
surface: { title: document.data.title || null, description: document.data.description || null, body: document.body || '' },
|
|
273
106
|
search: {
|
|
274
|
-
identity: tokenize([identity, data.resource,
|
|
107
|
+
identity: tokenize([identity, document.data.resource, definition.qualified_type, document.path].filter(Boolean).join(' ')),
|
|
275
108
|
labels: tokenize(labels.filter(Boolean).join(' ')),
|
|
276
109
|
tags: tokenize(tags.join(' ')),
|
|
277
|
-
text: tokenize(`${data.description || ''}\n${document.body || ''}`),
|
|
110
|
+
text: tokenize(`${document.data.description || ''}\n${document.body || ''}`),
|
|
278
111
|
},
|
|
279
112
|
};
|
|
280
113
|
records.push(record);
|
|
281
|
-
edges.push(...extractEdges(document.path, data, document.body));
|
|
114
|
+
edges.push(...extractEdges(document.path, document.data, document.body));
|
|
282
115
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
116
|
+
edges.sort((a, b) => a.source.localeCompare(b.source) || a.relation.localeCompare(b.relation) || a.target.localeCompare(b.target));
|
|
117
|
+
const payload = { version: 2, include_private: !!options.includePrivate, excluded_documents: excluded, embedding: null, documents: records, edges };
|
|
118
|
+
return { ...payload, index_hash: canonicalHash(payload) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateVectors(vectors, expectedLength) {
|
|
122
|
+
if (!Array.isArray(vectors) || vectors.length !== expectedLength) throw new Error(`Embedding adapter returned ${vectors?.length || 0} vectors; expected ${expectedLength}.`);
|
|
123
|
+
const dimension = vectors[0]?.length;
|
|
124
|
+
if (!Number.isInteger(dimension) || dimension < 1) throw new Error('Embedding vectors must be non-empty arrays.');
|
|
125
|
+
for (const vector of vectors) {
|
|
126
|
+
if (!Array.isArray(vector) || vector.length !== dimension || vector.some((value) => typeof value !== 'number' || !Number.isFinite(value))) {
|
|
127
|
+
throw new Error('Embedding vectors must have one finite numeric dimension.');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return dimension;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function enrichSemanticIndex(index, { embed, model }) {
|
|
134
|
+
if (typeof embed !== 'function') throw new Error('A multilingual embed(texts, context) adapter is required.');
|
|
135
|
+
if (typeof model !== 'string' || !model) throw new Error('Embedding model identity is required.');
|
|
136
|
+
const vectors = await embed(index.documents.map(semanticText), { model, purpose: 'document' });
|
|
137
|
+
const dimension = validateVectors(vectors, index.documents.length);
|
|
138
|
+
const documents = index.documents.map((record, indexNumber) => ({ ...record, embedding: vectors[indexNumber] }));
|
|
139
|
+
const payload = { ...index, embedding: { model, dimension, multilingual: true }, documents };
|
|
140
|
+
delete payload.index_hash;
|
|
295
141
|
return { ...payload, index_hash: canonicalHash(payload) };
|
|
296
142
|
}
|
|
297
143
|
|
|
144
|
+
function cosine(a, b) {
|
|
145
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return 0;
|
|
146
|
+
let dot = 0; let aa = 0; let bb = 0;
|
|
147
|
+
for (let index = 0; index < a.length; index++) { dot += a[index] * b[index]; aa += a[index] ** 2; bb += b[index] ** 2; }
|
|
148
|
+
return aa && bb ? dot / (Math.sqrt(aa) * Math.sqrt(bb)) : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
298
151
|
export function searchSemanticIndex(index, query, options = {}) {
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
301
|
-
const allowedTypes = options.types ? new Set(options.types) : null;
|
|
152
|
+
const tokens = tokenize(query);
|
|
153
|
+
if (!tokens.length) return [];
|
|
302
154
|
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 10;
|
|
155
|
+
const allowedTypes = options.types ? new Set(options.types) : null;
|
|
303
156
|
const results = [];
|
|
304
157
|
for (const document of index.documents || []) {
|
|
305
158
|
if (allowedTypes && !allowedTypes.has(document.type)) continue;
|
|
306
|
-
const buckets = [
|
|
307
|
-
|
|
308
|
-
[document.search?.labels || [], 7],
|
|
309
|
-
[document.search?.tags || [], 5],
|
|
310
|
-
[document.search?.text || [], 1],
|
|
311
|
-
];
|
|
312
|
-
let score = 0;
|
|
159
|
+
const buckets = [[document.search?.identity || [], 12], [document.search?.labels || [], 7], [document.search?.tags || [], 5], [document.search?.text || [], 1]];
|
|
160
|
+
let lexical = 0;
|
|
313
161
|
const matched = new Set();
|
|
314
|
-
for (const token of
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
matched.add(token);
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
162
|
+
for (const token of tokens) for (const [values, weight] of buckets) if (values.includes(token)) { lexical += weight; matched.add(token); break; }
|
|
163
|
+
const semantic = options.queryVector && document.embedding ? cosine(options.queryVector, document.embedding) : 0;
|
|
164
|
+
if (lexical > 0 || semantic > 0) {
|
|
165
|
+
results.push({ score: lexical + Math.max(0, semantic) * 20, evidence: { lexical, semantic, matched: [...matched].sort() }, document });
|
|
322
166
|
}
|
|
323
|
-
if (score > 0) results.push({ score, matched: [...matched].sort(), document });
|
|
324
167
|
}
|
|
325
|
-
return results
|
|
326
|
-
.sort((a, b) => b.score - a.score || a.document.path.localeCompare(b.document.path))
|
|
327
|
-
.slice(0, limit);
|
|
168
|
+
return results.sort((a, b) => b.score - a.score || a.document.path.localeCompare(b.document.path)).slice(0, limit);
|
|
328
169
|
}
|
|
329
170
|
|
|
330
|
-
function
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
171
|
+
export async function searchSemanticIndexWithAdapter(index, query, { embed, model = index.embedding?.model, ...options }) {
|
|
172
|
+
if (!index.embedding) throw new Error('Semantic index has no embeddings; enrich it first.');
|
|
173
|
+
if (model !== index.embedding.model) throw new Error(`Embedding model mismatch: index uses '${index.embedding.model}', query requested '${model}'.`);
|
|
174
|
+
const vectors = await embed([query], { model, purpose: 'query' });
|
|
175
|
+
validateVectors(vectors, 1);
|
|
176
|
+
if (vectors[0].length !== index.embedding.dimension) throw new Error('Query embedding dimension does not match the index.');
|
|
177
|
+
return searchSemanticIndex(index, query, { ...options, queryVector: vectors[0] });
|
|
335
178
|
}
|
|
336
179
|
|
|
337
|
-
function
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
180
|
+
export function prepareRenderRequest(record, options = {}) {
|
|
181
|
+
const language = normalizeLanguage(options.language);
|
|
182
|
+
return {
|
|
183
|
+
target_language: language,
|
|
184
|
+
instruction: 'Render only the natural-language presentation fields in target_language. Never alter invariant controls or invent authority.',
|
|
185
|
+
invariant: {
|
|
186
|
+
semantic_id: record.semantic_id,
|
|
187
|
+
path: record.path,
|
|
188
|
+
type: record.type,
|
|
189
|
+
primitive_version: record.primitive_version,
|
|
190
|
+
resource: record.resource,
|
|
191
|
+
portability: record.portability,
|
|
192
|
+
tags: record.tags,
|
|
193
|
+
relations: record.relations,
|
|
194
|
+
content_hash: record.content_hash,
|
|
195
|
+
available_actions: options.availableActions || [],
|
|
196
|
+
},
|
|
197
|
+
surface: { ...record.surface },
|
|
198
|
+
context: options.context || null,
|
|
199
|
+
};
|
|
349
200
|
}
|
|
350
201
|
|
|
351
|
-
export function
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const targetStat = fs.lstatSync(targetRoot);
|
|
360
|
-
if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
|
|
361
|
-
throw new Error(`Localization output must be a real directory: ${targetRoot}`);
|
|
362
|
-
}
|
|
363
|
-
if (fs.readdirSync(targetRoot).length > 0) {
|
|
364
|
-
throw new Error(`Localization output directory must be empty: ${targetRoot}`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const localized = localizedDocuments(sourceRoot, { ...options, locale: normalizedLocale });
|
|
368
|
-
const parent = path.dirname(targetRoot);
|
|
369
|
-
fs.mkdirSync(parent, { recursive: true });
|
|
370
|
-
const stageRoot = path.join(parent, `.${path.basename(targetRoot)}.${crypto.randomUUID()}.stage`);
|
|
371
|
-
const inventory = [];
|
|
372
|
-
let targetWasEmpty = fs.existsSync(targetRoot);
|
|
373
|
-
try {
|
|
374
|
-
fs.mkdirSync(stageRoot);
|
|
375
|
-
for (const document of localized.documents) {
|
|
376
|
-
if (!isSafeDocumentPath(document.path)) throw new Error(`Unsafe projection path: ${document.path}`);
|
|
377
|
-
const target = path.resolve(stageRoot, document.path);
|
|
378
|
-
if (!target.startsWith(stageRoot + path.sep)) throw new Error(`Projection path escaped output: ${document.path}`);
|
|
379
|
-
atomicWrite(target, document.content);
|
|
380
|
-
inventory.push({ path: document.path, content_hash: documentHash(document.content) });
|
|
381
|
-
}
|
|
382
|
-
inventory.sort((a, b) => a.path.localeCompare(b.path));
|
|
383
|
-
const manifest = {
|
|
384
|
-
type: 'ssss_localization_projection',
|
|
385
|
-
version: 1,
|
|
386
|
-
locale: normalizedLocale,
|
|
387
|
-
include_private: !!options.includePrivate,
|
|
388
|
-
document_count: inventory.length,
|
|
389
|
-
excluded_documents: localized.excluded,
|
|
390
|
-
source_hash: canonicalHash(inventory),
|
|
391
|
-
documents: inventory,
|
|
392
|
-
};
|
|
393
|
-
atomicWrite(path.join(stageRoot, '.ssss-projection.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
394
|
-
if (targetWasEmpty) fs.rmdirSync(targetRoot);
|
|
395
|
-
fs.renameSync(stageRoot, targetRoot);
|
|
396
|
-
return manifest;
|
|
397
|
-
} catch (error) {
|
|
398
|
-
fs.rmSync(stageRoot, { recursive: true, force: true });
|
|
399
|
-
if (targetWasEmpty && !fs.existsSync(targetRoot)) fs.mkdirSync(targetRoot);
|
|
400
|
-
throw error;
|
|
401
|
-
}
|
|
202
|
+
export async function renderSemanticRecord(record, { language, render, ...options }) {
|
|
203
|
+
if (typeof render !== 'function') throw new Error('An LLM render(request) adapter is required.');
|
|
204
|
+
const request = prepareRenderRequest(record, { ...options, language });
|
|
205
|
+
const rendered = await render(request);
|
|
206
|
+
if (!rendered || typeof rendered !== 'object' || Array.isArray(rendered)) throw new Error('Render adapter must return an object.');
|
|
207
|
+
for (const key of Object.keys(rendered)) if (!RENDER_FIELDS.has(key)) throw new Error(`Render adapter returned forbidden field '${key}'.`);
|
|
208
|
+
for (const [key, value] of Object.entries(rendered)) if (typeof value !== 'string') throw new Error(`Rendered field '${key}' must be a string.`);
|
|
209
|
+
return { record, presentation: { language: request.target_language, ...rendered }, invariant: request.invariant };
|
|
402
210
|
}
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Typed, capability-bound generative UI projection contract. */
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const SAFE_ID = /^[a-z][a-z0-9_.-]{0,127}$/;
|
|
5
|
+
const LAYOUTS = new Set(['form', 'table', 'detail', 'detail-with-actions', 'dashboard', 'wizard']);
|
|
6
|
+
|
|
7
|
+
function isRecord(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
8
|
+
|
|
9
|
+
export class UiRegistry {
|
|
10
|
+
constructor() { this.components = new Map(); this.actions = new Map(); }
|
|
11
|
+
registerComponent(definition) {
|
|
12
|
+
if (!SAFE_ID.test(definition?.id || '') || typeof definition.validateProps !== 'function') throw new Error('Component id and validateProps are required.');
|
|
13
|
+
if (this.components.has(definition.id)) throw new Error(`Duplicate UI component '${definition.id}'.`);
|
|
14
|
+
this.components.set(definition.id, { ...definition });
|
|
15
|
+
}
|
|
16
|
+
registerAction(definition) {
|
|
17
|
+
if (!SAFE_ID.test(definition?.id || '') || !definition.capability || !definition.command) throw new Error('Action id, capability, and command are required.');
|
|
18
|
+
if (this.actions.has(definition.id)) throw new Error(`Duplicate UI action '${definition.id}'.`);
|
|
19
|
+
this.actions.set(definition.id, { ...definition });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateUiManifest(manifest, { registry, visibleFields = [], grantedCapabilities = [], maxComponents = 100 } = {}) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (!isRecord(manifest)) return { valid: false, errors: [{ field: '(root)', issue: 'UI manifest must be an object.' }] };
|
|
26
|
+
if (manifest.type !== 'ssss:ui_projection') errors.push({ field: 'type', issue: 'Expected ssss:ui_projection.' });
|
|
27
|
+
if (!LAYOUTS.has(manifest.layout)) errors.push({ field: 'layout', issue: `Use one of: ${[...LAYOUTS].join(', ')}.` });
|
|
28
|
+
if (!Array.isArray(manifest.components)) errors.push({ field: 'components', issue: 'Components must be an array.' });
|
|
29
|
+
else if (manifest.components.length > maxComponents) errors.push({ field: 'components', issue: `At most ${maxComponents} components are allowed.` });
|
|
30
|
+
const allowedFields = new Set(visibleFields);
|
|
31
|
+
const capabilities = new Set(grantedCapabilities);
|
|
32
|
+
for (const [index, component] of (manifest.components || []).entries()) {
|
|
33
|
+
const prefix = `components[${index}]`;
|
|
34
|
+
if (!isRecord(component)) { errors.push({ field: prefix, issue: 'Component must be an object.' }); continue; }
|
|
35
|
+
const definition = registry?.components.get(component.component);
|
|
36
|
+
if (!definition) { errors.push({ field: `${prefix}.component`, issue: `Unregistered component '${component.component}'.` }); continue; }
|
|
37
|
+
if (component.bind && !allowedFields.has(component.bind)) errors.push({ field: `${prefix}.bind`, issue: `Field '${component.bind}' is hidden or unknown.` });
|
|
38
|
+
if (!isRecord(component.props) || typeof component.props.label !== 'string' || !component.props.label.trim()) {
|
|
39
|
+
errors.push({ field: `${prefix}.props.label`, issue: 'An accessible label is required.' });
|
|
40
|
+
}
|
|
41
|
+
if (component.action) {
|
|
42
|
+
const action = registry.actions.get(component.action);
|
|
43
|
+
if (!action) errors.push({ field: `${prefix}.action`, issue: `Unregistered action '${component.action}'.` });
|
|
44
|
+
else if (!capabilities.has(action.capability) && !capabilities.has('*:*')) errors.push({ field: `${prefix}.action`, issue: `Missing capability '${action.capability}'.` });
|
|
45
|
+
}
|
|
46
|
+
const propIssues = definition.validateProps(component.props || {});
|
|
47
|
+
for (const issue of propIssues || []) errors.push({ field: `${prefix}.props.${issue.field || '(root)'}`, issue: issue.issue || String(issue) });
|
|
48
|
+
}
|
|
49
|
+
return { valid: errors.length === 0, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createDeterministicUi(definition, options = {}) {
|
|
53
|
+
const fields = (options.visibleFields || definition.fields || []).map((field) => typeof field === 'string' ? { id: field, name: field, kind: 'string' } : field);
|
|
54
|
+
return {
|
|
55
|
+
type: 'ssss:ui_projection',
|
|
56
|
+
projection_id: `ui_${crypto.createHash('sha256').update(`${definition.primitive_id || definition.qualified_type}\0${options.mode || 'form'}`).digest('hex').slice(0, 16)}`,
|
|
57
|
+
primitive: definition.primitive_id || definition.qualified_type,
|
|
58
|
+
layout: options.mode || 'form',
|
|
59
|
+
title: definition.name || definition.qualified_type,
|
|
60
|
+
components: fields.map((field) => ({
|
|
61
|
+
component: field.kind === 'enum' ? 'select-field' : field.kind === 'boolean' ? 'boolean-field' : 'data-field',
|
|
62
|
+
bind: field.id,
|
|
63
|
+
props: { label: field.name || field.id, required: !!field.required, values: field.values || undefined },
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function planUi({ definition, data, principal, language, planner, registry, visibleFields, grantedCapabilities }) {
|
|
69
|
+
const fallback = createDeterministicUi(definition, { visibleFields });
|
|
70
|
+
if (typeof planner !== 'function') return { manifest: fallback, generated: false, fallback_reason: 'planner unavailable' };
|
|
71
|
+
const safeData = Object.fromEntries(Object.entries(data || {}).filter(([key]) => visibleFields.includes(key)));
|
|
72
|
+
let candidate;
|
|
73
|
+
try {
|
|
74
|
+
candidate = await planner({
|
|
75
|
+
instruction: 'Compose a UI manifest using only registered components, visible fields, and actions. Do not emit executable code.',
|
|
76
|
+
primitive: {
|
|
77
|
+
primitive_id: definition.primitive_id,
|
|
78
|
+
qualified_type: definition.qualified_type,
|
|
79
|
+
name: definition.name,
|
|
80
|
+
fields: (definition.fields || []).filter((field) => visibleFields.includes(field.id)),
|
|
81
|
+
},
|
|
82
|
+
data: safeData,
|
|
83
|
+
principal: { id: principal.id, kind: principal.kind },
|
|
84
|
+
language,
|
|
85
|
+
components: [...registry.components.keys()],
|
|
86
|
+
actions: [...registry.actions.values()].filter((action) => grantedCapabilities.includes(action.capability)).map((action) => action.id),
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { manifest: fallback, generated: false, fallback_reason: error.message };
|
|
90
|
+
}
|
|
91
|
+
const validation = validateUiManifest(candidate, { registry, visibleFields, grantedCapabilities });
|
|
92
|
+
if (!validation.valid) return { manifest: fallback, generated: false, fallback_reason: 'generated manifest rejected', errors: validation.errors };
|
|
93
|
+
return { manifest: candidate, generated: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function actionToEnvelope(manifestComponent, registry, input = {}) {
|
|
97
|
+
const action = registry.actions.get(manifestComponent.action);
|
|
98
|
+
if (!action) throw new Error(`Unknown UI action '${manifestComponent.action}'.`);
|
|
99
|
+
return typeof action.command === 'function' ? action.command(input) : structuredClone(action.command);
|
|
100
|
+
}
|