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

57 lines
3.1 KiB
JavaScript
Raw Normal View History

#!/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';
/** Чистая сборка: валидирует, ловит дубли, помечает дрейф external. */
export function buildRegistry(entries) {
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; }
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 }) {
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);
}