From 171f7fb44a6b61aa6ce9ad801a9fd29df7cabdc6 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: Tue, 16 Jun 2026 18:41:29 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20round-memory=20owner-seal=20ownerSealAc?= =?UTF-8?q?tion=20decideSeal=20SP3-a=20=D1=8F=D0=B4=D1=80=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- tools/seal-orchestration.mjs | 19 +++++++++++++++++++ tools/seal-orchestration.test.mjs | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tools/seal-orchestration.mjs b/tools/seal-orchestration.mjs index 691fe92..41bd3ad 100644 --- a/tools/seal-orchestration.mjs +++ b/tools/seal-orchestration.mjs @@ -43,3 +43,22 @@ export function sealPlan({ md, currentArtifact, verdict, key, judgeMode, nowMs, const seal = freezeImpl({ steps: planObj.steps, skills: planObj.skills, artifactId: currentArtifact.artifact_id, judgeMode, key, nowMs }); return { sealed: true, seal }; } + +// ── owner-seal (SP3 / Фикс 3) ── +// Владелец на арбитраже опечатывает артефакт/план как есть, перевешивая NO-GO судьи И +// наставника. Каноническая строка действия = `owner-seal:` — её подписывает escape-грант +// владельца (router-mentor-receipts); контроллер подделать не может (protected runtime). + +/** Каноническая метка owner-seal для escape-гранта. hash = judged_hash артефакта/плана. */ +export function ownerSealAction(hash) { + return `owner-seal:${String(hash == null ? '' : hash)}`; +} + +/** Решение печати с учётом owner-seal. Реальный GO (wired && GO) → печать обычным путём. + * Иначе ownerSealOpen (владелец подписал owner-seal на этот hash) → печать несмотря на + * NO-GO/degraded. Ни того, ни другого → нет печати. */ +export function decideSeal({ verdict, ownerSealOpen = false } = {}) { + if (isRealGo(verdict)) return { seal: true, via: 'wired-go' }; + if (ownerSealOpen) return { seal: true, via: 'owner-seal' }; + return { seal: false, via: null }; +} diff --git a/tools/seal-orchestration.test.mjs b/tools/seal-orchestration.test.mjs index bb55550..1585260 100644 --- a/tools/seal-orchestration.test.mjs +++ b/tools/seal-orchestration.test.mjs @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan } from './seal-orchestration.mjs'; +import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan, ownerSealAction, decideSeal } from './seal-orchestration.mjs'; import { contentHash } from './judge-seal-channel.mjs'; const specMd = '## Реш {#dec-a}\nтекст'; @@ -41,3 +41,24 @@ describe('seal-orchestration', () => { expect(r.sealed).toBe(false); }); }); + +describe('owner-seal (SP3)', () => { + it('ownerSealAction → каноническая метка owner-seal:', () => { + expect(ownerSealAction('abc123')).toBe('owner-seal:abc123'); + }); + it('ownerSealAction нулевой hash → owner-seal:', () => { + expect(ownerSealAction(null)).toBe('owner-seal:'); + }); + it('decideSeal: реальный GO → печать via wired-go', () => { + expect(decideSeal({ verdict: { wired: true, decision: 'GO' } })).toEqual({ seal: true, via: 'wired-go' }); + }); + it('decideSeal: NO-GO + ownerSealOpen → печать via owner-seal (перевешивает)', () => { + expect(decideSeal({ verdict: { wired: true, decision: 'NO-GO' }, ownerSealOpen: true })).toEqual({ seal: true, via: 'owner-seal' }); + }); + it('decideSeal: NO-GO без owner-seal → нет печати', () => { + expect(decideSeal({ verdict: { wired: true, decision: 'NO-GO' }, ownerSealOpen: false })).toEqual({ seal: false, via: null }); + }); + it('decideSeal: degraded (wired:false) + ownerSealOpen → печать via owner-seal', () => { + expect(decideSeal({ verdict: { wired: false, decision: 'GO' }, ownerSealOpen: true })).toEqual({ seal: true, via: 'owner-seal' }); + }); +});