#!/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); }