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.
37 lines
1.5 KiB
JavaScript
37 lines
1.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* artifact-from-spec (C1, §5) — спека markdown → {sections, source_sha}.
|
|
* ref = ЯВНЫЙ якорь `## ... {#id}` (P2-2, НЕ слаг заголовка). Пустой раздел отброшен
|
|
* (fail-CLOSE). source_sha — sha256 исходного текста (входит в sealable-объект, SD-1).
|
|
*/
|
|
import { createHash } from 'node:crypto';
|
|
|
|
const ANCHOR_RE = /^#{1,6}\s+.*\{#([a-zA-Z0-9._-]+)\}\s*$/;
|
|
|
|
/** markdown → { sections: { <anchor>: <содержимое раздела> } }. */
|
|
export function parseAnchoredSections(md) {
|
|
const lines = String(md == null ? '' : md).split(/\r?\n/);
|
|
const sections = {};
|
|
let curKey = null; let buf = [];
|
|
const flush = () => {
|
|
if (curKey) { const body = buf.join('\n').trim(); if (body) sections[curKey] = body; }
|
|
buf = [];
|
|
};
|
|
for (const line of lines) {
|
|
const m = line.match(ANCHOR_RE);
|
|
const isHeading = /^#{1,6}\s+/.test(line);
|
|
if (m) { flush(); curKey = m[1]; }
|
|
else if (isHeading) { flush(); curKey = null; } // заголовок без якоря закрывает раздел
|
|
else if (curKey) buf.push(line);
|
|
}
|
|
flush();
|
|
return { sections };
|
|
}
|
|
|
|
/** sealable-объект артефакта: { sections, source_sha }. */
|
|
export function buildArtifact(md) {
|
|
const { sections } = parseAnchoredSections(md);
|
|
const source_sha = createHash('sha256').update(String(md == null ? '' : md)).digest('hex');
|
|
return { sections, source_sha };
|
|
}
|