From d1a767867aec3a7e8d2eda75c5a16bf6fc4cd253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 8 Jun 2026 18:33:05 +0300 Subject: [PATCH] =?UTF-8?q?fix(skill-contract):=20G-E=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=B6=20=D0=B4=D1=80=D0=B5=D0=B9=D1=84=D0=B0?= =?UTF-8?q?=20=D0=B8=D0=BD=D0=B5=D1=80=D1=82=D0=B5=D0=BD=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=BC=20source.path=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80=D0=B6=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkContractDrift: external без локального source.path И без поданного currentContent (== null) → не сторожим (G4 инертен). Это прод-случай зеро-хеша (Р5 MCP/marketplace): loadRegistry при пустом path не читает content. Прямой вызов с поданным currentContent — drift сверяется как раньше. TDD: RED→GREEN, 48/48 skill-contract + registry. Дисциплина doubt→drift на реальных источниках не понижена. coverage: skill:test-driven-development --- tools/skill-contract.mjs | 6 ++++++ tools/skill-contract.test.mjs | 6 ++++++ 2 files changed, 12 insertions(+) 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)', () => {