From f96610cd7b806d230ec91657bcbe25a22deb53b3 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: Wed, 17 Jun 2026 03:42:22 +0300 Subject: [PATCH] feat: round-memory ownerSealActionForContent per-stage hash helper SP3-c2a Co-Authored-By: Claude Opus 4.8 --- tools/seal-orchestration.mjs | 15 ++++++++++++++- tools/seal-orchestration.test.mjs | 12 +++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tools/seal-orchestration.mjs b/tools/seal-orchestration.mjs index c92b847..490ae6f 100644 --- a/tools/seal-orchestration.mjs +++ b/tools/seal-orchestration.mjs @@ -14,7 +14,7 @@ import { buildArtifact } from './artifact-from-spec.mjs'; import { parsePlanSteps } from './plan-steps-parse.mjs'; import { parsePlanSkills } from './plan-skills.mjs'; import { contentHash, sealOnApproval, requiresOwnerSeal } from './judge-seal-channel.mjs'; -import { freezeArtifact, freezePlan } from './plan-lock.mjs'; +import { freezeArtifact, freezePlan, planId } from './plan-lock.mjs'; export function sealableArtifact(md) { return buildArtifact(md); } // {sections, source_sha} export function sealablePlan(md) { return { steps: parsePlanSteps(md), skills: parsePlanSkills(md) }; } // {steps,skills} @@ -90,3 +90,16 @@ export function decideSeal({ verdict, ownerSealOpen = false, ownerSealRequired = if (isRealGo(verdict) && ownerSealRequired) return { seal: false, via: null, ownerRequired: true }; return { seal: false, via: null }; } + +/** SP3-c: owner-seal-метка для карточки арбитража по содержимому. ТОТ ЖЕ хеш, что считает + * sealTurnProd → подпись владельца на этой метке сматчится: план (есть steps) → planId(steps); + * иначе спека → judgedHashOf(sealableArtifact). Тотально — null при сбое (карточка тогда без + * точной escape-строки, владелец берёт хеш из логов). */ +export function ownerSealActionForContent(content) { + const c = String(content == null ? '' : content); + try { + const steps = sealablePlan(c).steps; + if (Array.isArray(steps) && steps.length) return ownerSealAction(planId(steps)); + } catch { /* не план — пробуем как спеку */ } + try { return ownerSealAction(judgedHashOf(sealableArtifact(c))); } catch { return null; } +} diff --git a/tools/seal-orchestration.test.mjs b/tools/seal-orchestration.test.mjs index f89aea8..ae0f75b 100644 --- a/tools/seal-orchestration.test.mjs +++ b/tools/seal-orchestration.test.mjs @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan, ownerSealAction, decideSeal } from './seal-orchestration.mjs'; +import { sealableArtifact, sealablePlan, judgedHashOf, sealArtifact, sealPlan, ownerSealAction, decideSeal, ownerSealActionForContent } from './seal-orchestration.mjs'; import { contentHash } from './judge-seal-channel.mjs'; +import { planId } from './plan-lock.mjs'; const specMd = '## Реш {#dec-a}\nтекст'; const planMd = '```steps-json\n[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"}]\n```'; @@ -102,3 +103,12 @@ describe('owner-seal в seal-функциях (SP3-b3)', () => { expect(r.ownerRequired).toBe(true); }); }); + +describe('ownerSealActionForContent (SP3-c)', () => { + it('план (есть steps-json) → owner-seal:planId(steps) (как в sealTurnProd)', () => { + expect(ownerSealActionForContent(planMd)).toBe(ownerSealAction(planId(sealablePlan(planMd).steps))); + }); + it('спека (нет steps) → owner-seal:judgedHashOf(sealableArtifact) (как в sealTurnProd)', () => { + expect(ownerSealActionForContent(specMd)).toBe(ownerSealAction(judgedHashOf(sealableArtifact(specMd)))); + }); +});