4713a65b63
extractPlanGoal обрывал многострочную цель на первой строке (multiline $) и при пустой строке под заголовком падал в фолбэк, хватавший skills-json-блок или Kind-маркер как цель → роутер классифицировал план по мусору, пининг садился на мусор. Регекс теперь берёт тело секции целиком до следующего заголовка / fenced-блока / конца текста; фолбэк исключает заголовки и fenced-блоки; при нескольких секциях берётся первая. Хвост спеки роутера §4 (вход роутера), эпик роутер-реестр этап 3, item 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43 lines
2.9 KiB
JavaScript
43 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Парсер объявленных в плане скилов (мерж роутер↔наставник). Зеркало parseVerifiedContext:
|
|
* ищет fenced-блок ```skills-json со списком строк. Нет/битый → []. */
|
|
export function parsePlanSkills(content) {
|
|
const m = String(content ?? '').match(/```skills-json\s*\n([\s\S]*?)\n```/i);
|
|
if (!m) return [];
|
|
let arr;
|
|
try { arr = JSON.parse(m[1]); } catch { return []; }
|
|
if (!Array.isArray(arr)) return [];
|
|
return arr.filter((s) => typeof s === 'string' && s.trim());
|
|
}
|
|
|
|
/** Цель плана для classify(): ТЕЛО секции «## Цель» / «## Goal» целиком — от строки под
|
|
* заголовком до первой границы: следующий заголовок `\n## `, начало fenced-блока (` \n``` `)
|
|
* или истинный конец текста (`$(?![\s\S])`). Многострочное тело возвращается полностью (раньше
|
|
* multiline-`$` обрывал на первой строке). При нескольких секциях «## Цель» берётся ПЕРВАЯ
|
|
* (`String.match` = первое совпадение). Фолбэк (секции цели нет): «осмысленный абзац» — первый
|
|
* непустой абзац, НЕ заголовок (`#`) и НЕ fenced-блок (` ``` `); fenced-блоки (skills/steps/
|
|
* verified-context) и маркеры шапки телом цели не являются. Зеркало extractGoal судьи. */
|
|
export function extractPlanGoal(content) {
|
|
const text = String(content ?? '');
|
|
const m = text.match(/^##\s*(?:Цель|Goal)[^\n]*\n([\s\S]*?)(?=\n##\s|\n```|$(?![\s\S]))/im);
|
|
if (m && m[1].trim()) return m[1].trim();
|
|
const para = text.split(/\n\s*\n/).map((s) => s.trim())
|
|
.find((s) => s && !s.startsWith('#') && !s.startsWith('```'));
|
|
return para || '';
|
|
}
|
|
|
|
/** Пометка поставки плана: `**Delivery:** internal|user-result`. По умолчанию/мусор → 'internal'
|
|
* (fail-safe: владельца не дёргаем без явной пометки результата). Зеркало parsePlanSkills. */
|
|
export function parsePlanDelivery(content) {
|
|
const m = String(content ?? '').match(/(^|\n)\*\*Delivery:\*\*\s*(internal|user-result)\b/i);
|
|
return m ? m[2].toLowerCase() : 'internal';
|
|
}
|
|
|
|
/** D1: пометка типа плана: `**Kind:** deploy`. По умолчанию/мусор → 'normal' (fail-safe:
|
|
* благословлённый ops-runbook-канал enforce-floor применяется ТОЛЬКО к явному deploy-плану).
|
|
* Зеркало parsePlanDelivery. */
|
|
export function parsePlanKind(content) {
|
|
const m = String(content ?? '').match(/(^|\n)\*\*Kind:\*\*\s*(deploy|normal)\b/i);
|
|
return m ? m[2].toLowerCase() : 'normal';
|
|
}
|