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.
46 lines
2.6 KiB
JavaScript
46 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* plan-steps-parse (C2, §5) — план markdown → [{op,object,ref}]. fail-CLOSE на любой брак.
|
|
* Reject op:'Task' (VA-4). Канон object: файлы → repo-relative POSIX (SE-5). ref обязателен (SE-1).
|
|
*/
|
|
const BLOCK_RE = /```steps-json\s*\n([\s\S]*?)\n```/;
|
|
const FILE_OPS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
|
|
/** Достать содержимое блока ```steps-json``` или null. */
|
|
export function extractStepsBlock(md) {
|
|
const m = String(md == null ? '' : md).match(BLOCK_RE);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
/**
|
|
* Канон формы object для файловых op — repo-relative POSIX (SE-5): только backslash→slash,
|
|
* БЕЗ абсолютизации. Стабильная, cwd-независимая форма для хранения в печати; реальная
|
|
* нормализация для матча — pathNormalize в actionMatchesStep (plan-lock.mjs) на ОБЕ стороны
|
|
* (шаг И событие) во время матча. Класть сюда pathNormalize нельзя: он cwd-зависим/абсолютен,
|
|
* печать ставится в cwd хука-судьи, а матч — в cwd стены (рассинхрон).
|
|
*/
|
|
function canonObject(op, object) {
|
|
const s = String(object ?? '');
|
|
return FILE_OPS.has(op) ? s.replace(/\\/g, '/') : s;
|
|
}
|
|
|
|
/** Распарсить + провалидировать шаги. Бросает на брак (fail-CLOSE). */
|
|
export function parsePlanSteps(md) {
|
|
const raw = extractStepsBlock(md);
|
|
if (raw == null) throw new Error('plan-steps-parse: нет блока ```steps-json``` (fail-CLOSE)');
|
|
let arr;
|
|
try { arr = JSON.parse(raw); } catch { throw new Error('plan-steps-parse: блок не валидный JSON (fail-CLOSE)'); }
|
|
if (!Array.isArray(arr) || arr.length === 0) throw new Error('plan-steps-parse: ожидался непустой массив');
|
|
return arr.map((s, i) => {
|
|
if (!s || typeof s !== 'object') throw new Error(`plan-steps-parse: шаг ${i} не объект`);
|
|
const op = String(s.op || '');
|
|
const object = String(s.object ?? '');
|
|
const ref = String(s.ref ?? '');
|
|
if (!op) throw new Error(`plan-steps-parse: шаг ${i} без op`);
|
|
if (op === 'Task') throw new Error('plan-steps-parse: op:Task запрещён (субагенты, VA-4)');
|
|
if (!object.trim()) throw new Error(`plan-steps-parse: шаг ${i} без object`);
|
|
if (!ref.trim()) throw new Error(`plan-steps-parse: шаг ${i} без ref (closed-door, SE-1)`);
|
|
return { op, object: canonObject(op, object), ref };
|
|
});
|
|
}
|