48 lines
2.6 KiB
JavaScript
48 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* capability-vocabulary — контролируемый словарь capability-токенов (спека v2 §3,
|
||
* OPEN-1). Единственный источник допустимых токенов для needs/produces контрактов.
|
||
* Чистые функции (без LLM): валидация формы словаря + сверка токенов контракта.
|
||
*/
|
||
import fsDefault from 'node:fs';
|
||
|
||
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||
|
||
/** Валидация формы словаря → {ok, tokens:Set, errors[]}. */
|
||
export function validateVocabulary(raw) {
|
||
if (!raw || typeof raw !== 'object' || !Array.isArray(raw.tokens))
|
||
return { ok: false, tokens: new Set(), errors: ['vocabulary: объект с массивом tokens обязателен'] };
|
||
const errors = [];
|
||
const tokens = new Set();
|
||
raw.tokens.forEach((t, i) => {
|
||
if (!t || typeof t !== 'object' || typeof t.token !== 'string' || !t.token.trim()) {
|
||
errors.push(`tokens[${i}].token: непустая строка обязательна`);
|
||
return;
|
||
}
|
||
const tok = t.token.trim();
|
||
if (!KEBAB.test(tok)) errors.push(`tokens[${i}].token "${tok}": требуется kebab-case`);
|
||
if (typeof t.label !== 'string' || !t.label.trim()) errors.push(`tokens[${i}] (${tok}).label: непустая строка обязательна`);
|
||
if (typeof t.description !== 'string' || !t.description.trim()) errors.push(`tokens[${i}] (${tok}).description: непустая строка обязательна`);
|
||
if (tokens.has(tok)) { errors.push(`tokens[${i}].token "${tok}": duplicate`); return; }
|
||
tokens.add(tok);
|
||
});
|
||
return { ok: errors.length === 0, tokens, errors };
|
||
}
|
||
|
||
/** Загрузка словаря с диска (fs инъектируется). Бросает на битом JSON. */
|
||
export function loadVocabulary({ path, fsImpl = fsDefault }) {
|
||
const raw = JSON.parse(fsImpl.readFileSync(path, 'utf8'));
|
||
return validateVocabulary(raw);
|
||
}
|
||
|
||
/** Неизвестные токены контракта в needs/produces (отсутствуют в словаре). [{field, token}].
|
||
* Сверка по String(tok).trim(); в выдаче — исходное значение token (не нормализованное). */
|
||
export function unknownTokens(contract, tokenSet) {
|
||
const set = tokenSet instanceof Set ? tokenSet : new Set(tokenSet || []);
|
||
const out = [];
|
||
for (const field of ['needs', 'produces'])
|
||
for (const tok of contract?.[field] || [])
|
||
if (!set.has(String(tok).trim())) out.push({ field, token: tok });
|
||
return out;
|
||
}
|