Files
brain/tools/skill-contract-registry.mjs
T

68 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* skill-contract-registry — сборка набора контрактов скилов. Чистый buildRegistry
* (над записями {contract, currentContent?}) + loadRegistry (диск, fs инъектируется).
* Валидирует каждый, для external сверяет дрейф G4. Вход для машины охвата 3-C.
*/
import fsDefault from 'node:fs';
import { validateContract, normalizeContract, checkContractDrift } from './skill-contract.mjs';
import { unknownTokens } from './capability-vocabulary.mjs';
/** Чистая сборка: валидирует, ловит дубли, помечает дрейф external.
* vocabTokens (Set токенов словаря) — опциональный замок §3: передан → неизвестный
* токен в needs/produces валит контракт в errors; null = замок выключен (стадийная
* раскатка §8, пока ~144 контракта в прозе). */
export function buildRegistry(entries, { vocabTokens = null } = {}) {
const contracts = [], errors = [], driftFlags = [], seen = new Set();
for (const e of entries || []) {
const c = normalizeContract(e.contract);
const v = validateContract(c);
if (!v.ok) { errors.push({ skill: c.skill || '(?)', errors: v.errors }); continue; }
if (seen.has(c.skill)) { errors.push({ skill: c.skill, errors: ['duplicate skill contract'] }); continue; }
if (vocabTokens) {
const unknown = unknownTokens(c, vocabTokens);
if (unknown.length) {
errors.push({ skill: c.skill, errors: unknown.map((u) => `${u.field}: неизвестный токен "${u.token}" (нет в capability-vocabulary)`) });
continue;
}
}
seen.add(c.skill);
if (c.kind === 'external') {
const d = checkContractDrift({ contract: c, currentContent: e.currentContent });
if (d.drifted) driftFlags.push({ skill: c.skill, reason: d.reason, fallback: d.fallback });
}
contracts.push(c);
}
return { contracts, errors, driftFlags };
}
/**
* G3 — детерминированная диспетчеризация по id скила поверх собранного реестра:
* скил в driftFlags (устарел/дрейф G4) ИЛИ нет адаптера → mode 'soft-reasoning'
* (молча не доверять, откат на мягкое рассуждение судьи); есть+свежий → mode
* 'exact' + сам контракт. Чистая функция над выходом buildRegistry/loadRegistry.
*/
export function dispatchContract(registry, skill) {
const reg = registry || {};
const drift = (reg.driftFlags || []).find((d) => d.skill === skill);
if (drift) return { mode: 'soft-reasoning', skill, reason: drift.reason };
const c = (reg.contracts || []).find((x) => x.skill === skill);
if (!c) return { mode: 'soft-reasoning', skill, reason: 'нет адаптера для скила — мягкое рассуждение' };
return { mode: 'exact', skill, contract: c };
}
/** Загрузка с диска: dir/*.contract.json. Для external читает source.path
* (актуальный SKILL.md) для дрейф-сверки G4, если путь задан и доступен. */
export function loadRegistry({ dir, fsImpl = fsDefault, vocabTokens = null }) {
const files = fsImpl.readdirSync(dir).filter((f) => f.endsWith('.contract.json'));
const entries = files.map((f) => {
const raw = JSON.parse(fsImpl.readFileSync(`${dir}/${f}`, 'utf8'));
let currentContent;
if (raw && raw.kind === 'external' && raw.source && raw.source.path) {
try { currentContent = fsImpl.readFileSync(raw.source.path, 'utf8'); } catch { currentContent = undefined; }
}
return { contract: raw, currentContent };
});
return buildRegistry(entries, { vocabTokens });
}