Files
portal/tools/artifact-from-spec.mjs
T
Дмитрий 09598dd5bd feat(seal): sealed-plan production pipeline (M7 Фаза 8 code-precondition)
Производство двух печатей (артефакт-решение + план-шаги), чтобы стене М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.
2026-06-09 17:50:25 +03:00

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 };
}