diff --git a/tools/skill-contract.mjs b/tools/skill-contract.mjs index 03c0eadd..e94e9a7d 100644 --- a/tools/skill-contract.mjs +++ b/tools/skill-contract.mjs @@ -104,6 +104,12 @@ export function contractHash(content) { export function checkContractDrift({ contract, currentContent }) { if (!contract || contract.kind !== 'external') return { ok: true, drifted: false, reason: 'own/нет внешнего источника — дрейф не сторожится' }; + // G-E: нет локального источника (path пуст) И нечего сравнивать (content не прочитан) → + // сторожить нечего, G4 инертен. Это ровно прод-случай зеро-хеша (Р5 MCP/marketplace): + // loadRegistry при пустом path не читает content → currentContent === undefined. + // Прямой вызов с поданным currentContent (тест/иной потребитель) → drift сверяется как обычно. + if (!contract.source?.path && currentContent == null) + return { ok: true, drifted: false, reason: 'external без локального source.path и без содержания — дрейф не сторожится (G4 инертен)' }; const stored = contract.source?.hash; const actual = contractHash(currentContent); if (!stored) return { ok: false, drifted: true, reason: 'нет сохранённого отпечатка внешнего скила', fallback: 'soft-reasoning' }; diff --git a/tools/skill-contract.test.mjs b/tools/skill-contract.test.mjs index 319672d8..0d4345c8 100644 --- a/tools/skill-contract.test.mjs +++ b/tools/skill-contract.test.mjs @@ -105,6 +105,12 @@ describe('checkContractDrift (G4)', () => { const r = checkContractDrift({ contract: { skill: 's', kind: 'external', source: { version: '1' } }, currentContent: 'x' }); expect(r.drifted).toBe(true); }); + it('G-E: external с пустым source.path → дрейф не сторожится (инертен)', () => { + const c = { skill: 'x', kind: 'external', source: { version: '1', hash: '0'.repeat(64), path: '' } }; + const r = checkContractDrift({ contract: c, currentContent: undefined }); + expect(r.drifted).toBe(false); + expect(r.ok).toBe(true); + }); }); describe('inherent (#1 присущее по природе + гард R4 rationale)', () => {