09598dd5bd
Производство двух печатей (артефакт-решение + план-шаги), чтобы стене М2 было
что матчить — код-предусловие флипа. Inline TDD, спека/план одобрены владельцем.
- C1 artifact-from-spec.mjs: спека markdown -> {sections, source_sha} по якорям {#id} (P2-2).
- C2 plan-steps-parse.mjs: план -> [{op,object,ref}], fail-CLOSE, reject op:Task (VA-4),
канон object = repo-relative POSIX (SE-5; pathNormalize только на матче в стене, не на парсе).
- C3/C4 plan-lock.mjs: judge_mode в ПОДПИСАННОЙ базе freezePlan (VA-2) + атомарный persist
temp->rename для обоих save (SE-4/VA-3, артефакт ДО плана).
- C6 seal-orchestration.mjs: sealableArtifact/sealablePlan + judgedHashOf (SD-1) +
sealArtifact/sealPlan на РЕАЛЬНОМ GO (SE-3 wired===true), штамп artifact_id из текущего
артефакта (SD-3), judge_mode впрыснут в печать ПОСЛЕ хеш-сверки sealOnApproval (фикс TOCTOU).
- C5 enforce-judge-gate.mjs: SPEC_PATH_RE + sealOnWiredGo (печать на wired GO, инъекция в main,
юнит-тесты hermetic) + judged_hash в вердикте runJudgeGate. extractGate2Product не тронут
(Гейт-2 = планы; Гейт-1 spec-judging — отдельный заход перед флипом).
- Интеграция seal-to-wall: печать -> decideMode стены М2 (allow / non-match block / closed-door).
Тесты: full tools-only регрессия 3427 passed | 2 skipped, 0 регрессий (+29 новых кейсов).
Печать в рантайме НЕ производится до флипа (стена/судья не зарегистрированы) — сборка
готовит код-предусловие. Спека docs/superpowers/specs/2026-06-09-sealed-plan-production-design.md.
40 lines
1.6 KiB
JavaScript
40 lines
1.6 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { extractStepsBlock, parsePlanSteps } from './plan-steps-parse.mjs';
|
|
|
|
const good = [
|
|
'# Plan', '```steps-json',
|
|
'[{"op":"Edit","object":"app/Foo.php","ref":"dec-a"},',
|
|
' {"op":"Bash","object":"npm run build","ref":"dec-b"}]',
|
|
'```',
|
|
].join('\n');
|
|
|
|
describe('plan-steps-parse', () => {
|
|
it('extracts and parses valid steps', () => {
|
|
const steps = parsePlanSteps(good);
|
|
expect(steps).toHaveLength(2);
|
|
expect(steps[0]).toMatchObject({ op: 'Edit', ref: 'dec-a' });
|
|
});
|
|
it('extractStepsBlock returns block body or null', () => {
|
|
expect(extractStepsBlock(good)).toContain('app/Foo.php');
|
|
expect(extractStepsBlock('# no block')).toBe(null);
|
|
});
|
|
it('canonicalizes file object to repo-relative POSIX (SE-5)', () => {
|
|
const md = '```steps-json\n[{"op":"Write","object":"app\\\\Bar.php","ref":"r"}]\n```';
|
|
expect(parsePlanSteps(md)[0].object).toBe('app/Bar.php');
|
|
});
|
|
it('rejects step without ref (SE-1) → throws', () => {
|
|
const md = '```steps-json\n[{"op":"Edit","object":"x"}]\n```';
|
|
expect(() => parsePlanSteps(md)).toThrow(/ref/);
|
|
});
|
|
it('rejects op:Task (VA-4, субагенты запрещены) → throws', () => {
|
|
const md = '```steps-json\n[{"op":"Task","object":"coder","ref":"r"}]\n```';
|
|
expect(() => parsePlanSteps(md)).toThrow(/Task/);
|
|
});
|
|
it('missing block → throws (fail-CLOSE)', () => {
|
|
expect(() => parsePlanSteps('# Plan no block')).toThrow();
|
|
});
|
|
it('non-JSON block → throws', () => {
|
|
expect(() => parsePlanSteps('```steps-json\nnot json\n```')).toThrow();
|
|
});
|
|
});
|