/
deka
/
buffer
Обзор
Документация
Войти
/
deka
/
buffer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
tools/build-lab.mjs
189 строк
11 KB
Maksim Ratnikov
feat: доска в Markdown — таблицы объектов
07 авг 2026, 12:01
07 авг 2026, 12:01
3f61bda
Код
Авторство
О чём код?
import { readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve, relative } from "node:path"; import { transformModule } from "./strip-module.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT = resolve(__dirname, ".."); const MODULE_PATHS = [ "src/util/format.js", "src/converters/wiki-table.js", "src/converters/layout-renderer.js", "src/converters/goal-to-layout.js", "src/converters/goal-diff.js", "src/converters/markdown-to-wiki.js", "src/converters/html-to-markdown.js", "src/converters/board-text.js", "src/converters/board-payload.js", "src/converters/board-layout.js", "src/converters/board-to-layout.js", "src/converters/board-to-markdown.js" ]; const PROMPT_PATH = "prompts/wiki-layout-full.md"; // Промпт доски склеивается из двух частей прямо в инжектируемую константу. // Третьего файла на диске нет намеренно: сборщик запускается из freshness-теста // во временный каталог, и побочная запись в репозиторий сделала бы проверку // мутирующей. Заодно не появляется третий расходящийся контракт. // Порядок важен: сначала задача и инварианты, потом контракт. Иначе модель // доходит до собственно задания к 60% текста. const BOARD_PROMPT_PATHS = ["prompts/text-to-board-layout.md", "prompts/board-layout-reference.md"]; const MEET_PROTOCOL_PROMPT_PATH = "prompts/meet-protocol.md"; const MEETING_NARRATIVE_PROMPT_PATH = "prompts/meeting-narrative.md"; const KB_UPDATE_PROMPT_PATH = "prompts/knowledge-base-update.md"; const GOAL_EXTRACTION_PROMPT_PATH = "prompts/goal-extraction.system.md"; const KNOWLEDGE_BASE_LAYOUT_PATH = "inputs/knowledge-base.layout.json"; const PROTOCOL_EXAMPLE_MARKDOWN_PATH = "Протокол пример.txt"; const args = process.argv.slice(2); const outIdx = args.indexOf("--out"); const OUT_PATH = outIdx >= 0 ? resolve(args[outIdx + 1]) : resolve(ROOT, "lab/wiki-clipboard-lab.html"); const TEMPLATE_PATH = resolve(ROOT, "lab/wiki-clipboard-lab.template.html"); const CONVERTERS_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_CONVERTERS__[ \t]*\r?\n/m; const PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_LLM_PROMPT__[ \t]*\r?\n/m; const BOARD_PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_BOARD_PROMPT__[ \t]*\r?\n/m; const MEET_PROTOCOL_PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_MEET_PROTOCOL_PROMPT__[ \t]*\r?\n/m; const MEETING_NARRATIVE_PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_MEETING_NARRATIVE_PROMPT__[ \t]*\r?\n/m; const KB_UPDATE_PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_KB_UPDATE_PROMPT__[ \t]*\r?\n/m; const GOAL_EXTRACTION_PROMPT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_GOAL_EXTRACTION_PROMPT__[ \t]*\r?\n/m; const KNOWLEDGE_BASE_LAYOUT_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_KNOWLEDGE_BASE_LAYOUT__[ \t]*\r?\n/m; const PROTOCOL_EXAMPLE_MARKDOWN_MARKER_RE = /^([ \t]*)\/\/\s*__INJECT_PROTOCOL_EXAMPLE_MARKDOWN__[ \t]*\r?\n/m; const FONTS_MARKER_RE = /^([ \t]*)\/\*\s*__INJECT_FONTS__\s*\*\/[ \t]*\r?\n/m; const FONTS_DIR = resolve(ROOT, "lab/fonts"); function readInlineFontFaces() { // lab/fonts/manifest.json (written by tools/fetch-fonts.mjs) describes every // WOFF2 file: family, weight (single value or a variable-font range like // "100 700") and the unicode-range of its subset. Per-subset unicode-range is // what keeps Cyrillic and Latin rendering with the same family instead of one // script silently falling back to a system font. let manifest; try { manifest = JSON.parse(readFileSync(resolve(FONTS_DIR, "manifest.json"), "utf8")); } catch (err) { if (err.code !== "ENOENT") throw err; return ""; } const rules = []; for (const entry of manifest) { const { file, family, weight, range } = entry; if (!file || !family || !weight || !range) { console.error(`build-lab: lab/fonts/manifest.json entry is missing file/family/weight/range: ${JSON.stringify(entry)}`); process.exit(1); } const b64 = readFileSync(resolve(FONTS_DIR, file)).toString("base64"); rules.push( `@font-face{font-family:'${family}';font-style:normal;font-weight:${weight};font-display:swap;src:url(data:font/woff2;base64,${b64}) format('woff2');unicode-range:${range};}` ); } return rules.join("\n"); } function assertNoLeftoverEsm(relPath, transformed) { const lines = transformed.split("\n"); for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; if (/^\s*(import|export)\s/.test(line)) { console.error(`build-lab: ${relPath}:${i + 1} has unsupported ESM syntax that wasn't stripped:`); console.error(` ${line}`); console.error(`build-lab supports only:`); console.error(` - single-line: import { x } from "./y.js";`); console.error(` - prefix: export function|const|let|class NAME ...`); process.exit(1); } } } function assertSingleMarker(template, regex, label) { const matches = [...template.matchAll(new RegExp(regex.source, "gm"))]; if (matches.length !== 1) { console.error(`build-lab: expected exactly one ${label} marker, found ${matches.length}`); process.exit(1); } return matches[0]; } function replaceLiteral(source, regex, value) { return source.replace(regex, () => value); } function main() { const template = readFileSync(TEMPLATE_PATH, "utf8"); const fontsMatch = assertSingleMarker(template, FONTS_MARKER_RE, "__INJECT_FONTS__"); const promptMatch = assertSingleMarker(template, PROMPT_MARKER_RE, "__INJECT_LLM_PROMPT__"); const boardPromptMatch = assertSingleMarker(template, BOARD_PROMPT_MARKER_RE, "__INJECT_BOARD_PROMPT__"); const meetProtocolPromptMatch = assertSingleMarker(template, MEET_PROTOCOL_PROMPT_MARKER_RE, "__INJECT_MEET_PROTOCOL_PROMPT__"); const meetingNarrativePromptMatch = assertSingleMarker(template, MEETING_NARRATIVE_PROMPT_MARKER_RE, "__INJECT_MEETING_NARRATIVE_PROMPT__"); const kbUpdatePromptMatch = assertSingleMarker(template, KB_UPDATE_PROMPT_MARKER_RE, "__INJECT_KB_UPDATE_PROMPT__"); const goalExtractionPromptMatch = assertSingleMarker(template, GOAL_EXTRACTION_PROMPT_MARKER_RE, "__INJECT_GOAL_EXTRACTION_PROMPT__"); const knowledgeBaseLayoutMatch = assertSingleMarker(template, KNOWLEDGE_BASE_LAYOUT_MARKER_RE, "__INJECT_KNOWLEDGE_BASE_LAYOUT__"); const protocolExampleMarkdownMatch = assertSingleMarker(template, PROTOCOL_EXAMPLE_MARKDOWN_MARKER_RE, "__INJECT_PROTOCOL_EXAMPLE_MARKDOWN__"); const convertersMatch = assertSingleMarker(template, CONVERTERS_MARKER_RE, "__INJECT_CONVERTERS__"); const fontsIndent = fontsMatch[1]; const fontsRules = readInlineFontFaces(); const fontsLine = fontsRules ? fontsRules.split("\n").map((rule) => `${fontsIndent}${rule}`).join("\n") + "\n" : `${fontsIndent}/* no fonts inlined */\n`; const promptIndent = promptMatch[1]; const boardPromptIndent = boardPromptMatch[1]; const meetProtocolPromptIndent = meetProtocolPromptMatch[1]; const meetingNarrativePromptIndent = meetingNarrativePromptMatch[1]; const kbUpdatePromptIndent = kbUpdatePromptMatch[1]; const goalExtractionPromptIndent = goalExtractionPromptMatch[1]; const knowledgeBaseLayoutIndent = knowledgeBaseLayoutMatch[1]; const protocolExampleMarkdownIndent = protocolExampleMarkdownMatch[1]; const convertersIndent = convertersMatch[1]; const promptText = readFileSync(resolve(ROOT, PROMPT_PATH), "utf8"); const meetProtocolPromptText = readFileSync(resolve(ROOT, MEET_PROTOCOL_PROMPT_PATH), "utf8"); const meetingNarrativePromptText = readFileSync(resolve(ROOT, MEETING_NARRATIVE_PROMPT_PATH), "utf8"); const kbUpdatePromptText = readFileSync(resolve(ROOT, KB_UPDATE_PROMPT_PATH), "utf8"); const goalExtractionPromptText = readFileSync(resolve(ROOT, GOAL_EXTRACTION_PROMPT_PATH), "utf8"); const knowledgeBaseLayoutText = readFileSync(resolve(ROOT, KNOWLEDGE_BASE_LAYOUT_PATH), "utf8"); const protocolExampleMarkdownText = readFileSync(resolve(ROOT, PROTOCOL_EXAMPLE_MARKDOWN_PATH), "utf8"); const boardPromptText = BOARD_PROMPT_PATHS .map((relPath) => readFileSync(resolve(ROOT, relPath), "utf8").replace(/\s+$/, "")) .join("\n\n---\n\n") + "\n"; const promptLine = `${promptIndent}const LLM_PROMPT_TEXT = ${JSON.stringify(promptText)};\n`; const boardPromptLine = `${boardPromptIndent}const BOARD_PROMPT_TEXT = ${JSON.stringify(boardPromptText)};\n`; const meetProtocolPromptLine = `${meetProtocolPromptIndent}const MEET_PROTOCOL_PROMPT_TEXT = ${JSON.stringify(meetProtocolPromptText)};\n`; const meetingNarrativePromptLine = `${meetingNarrativePromptIndent}const MEETING_NARRATIVE_PROMPT_TEXT = ${JSON.stringify(meetingNarrativePromptText)};\n`; const kbUpdatePromptLine = `${kbUpdatePromptIndent}const KB_UPDATE_PROMPT_TEXT = ${JSON.stringify(kbUpdatePromptText)};\n`; const goalExtractionPromptLine = `${goalExtractionPromptIndent}const GOAL_EXTRACTION_PROMPT_TEXT = ${JSON.stringify(goalExtractionPromptText)};\n`; const knowledgeBaseLayoutLine = `${knowledgeBaseLayoutIndent}const KNOWLEDGE_BASE_LAYOUT_TEXT = ${JSON.stringify(knowledgeBaseLayoutText)};\n`; const protocolExampleMarkdownLine = `${protocolExampleMarkdownIndent}const PROTOCOL_EXAMPLE_MARKDOWN_TEXT = ${JSON.stringify(protocolExampleMarkdownText)};\n`; const blocks = MODULE_PATHS.map((relPath) => { const source = readFileSync(resolve(ROOT, relPath), "utf8"); const transformed = transformModule(source).replace(/\s+$/, ""); assertNoLeftoverEsm(relPath, transformed); return `// ===== FILE: ${relPath} =====\n\n${transformed}`; }); const joined = blocks.join("\n\n"); const indentedConverters = joined .split("\n") .map((line) => (line.length ? `${convertersIndent}${line}` : "")) .join("\n"); let output = replaceLiteral(template, FONTS_MARKER_RE, fontsLine); output = replaceLiteral(output, PROMPT_MARKER_RE, promptLine); output = replaceLiteral(output, BOARD_PROMPT_MARKER_RE, boardPromptLine); output = replaceLiteral(output, MEET_PROTOCOL_PROMPT_MARKER_RE, meetProtocolPromptLine); output = replaceLiteral(output, MEETING_NARRATIVE_PROMPT_MARKER_RE, meetingNarrativePromptLine); output = replaceLiteral(output, KB_UPDATE_PROMPT_MARKER_RE, kbUpdatePromptLine); output = replaceLiteral(output, GOAL_EXTRACTION_PROMPT_MARKER_RE, goalExtractionPromptLine); output = replaceLiteral(output, KNOWLEDGE_BASE_LAYOUT_MARKER_RE, knowledgeBaseLayoutLine); output = replaceLiteral(output, PROTOCOL_EXAMPLE_MARKDOWN_MARKER_RE, protocolExampleMarkdownLine); output = replaceLiteral(output, CONVERTERS_MARKER_RE, `${indentedConverters}\n`); writeFileSync(OUT_PATH, output); console.log(`build-lab: wrote ${relative(ROOT, OUT_PATH)} (${output.length} bytes)`); } main();