Files
admin/scripts/migrate-tasks-v2-numbers.mjs

130 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Number all date-only .tasks files (yyyy-mm-dd-<slug>.md) → full v2 (yyyy-mm-dd-NNNNN-<slug>.md).
// Directive 2026-08-23 (operator via workshop): numbers for ALL leftover files + restore blocks
// for the 9 candidate tasks (sched r6-* ×5, snolla admin-api-batch-info-wave, penrose
// reverse-engineer-schematics, books reprice-cli + shelf-sale). Counter 830 → 830+count.
// Numbers assigned in creation-date order (spec: number encodes creation order), tiebreak repo/slug.
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const ROOT = "C:/Users/vitya/projects";
const APPLY = process.argv.includes("--apply");
const SKIP = new Set(["meeting-room", "projects-meta-mcp", "yt-tools", "projects-wiki", "alpha"]);
const NEXT = 831; // counter was 830 (kreknin took #830)
// (repo, slug) → {status, desc} for restored blocks
const RESTORE = {
"sched/r6-daemon-storage-option": { st: "⚪", desc: "r6 D1: createDaemon({ storage }) — daemon принимает storage-адаптер (дока 07.storage обещает, createDaemon — нет)" },
"sched/r6-docs-batch": { st: "⚪", desc: "Доки по репорту r6 (F1/F4/F6/F7/F8/F9 — не код, правки текстов)" },
"sched/r6-lastrun-status": { st: "⚪", desc: "r6 F10: в Schedules/Tasks нет статуса последнего рана (GET /api/schedules)" },
"sched/r6-manual-trigger-lastrun": { st: "⚪", desc: "r6 F5: POST /tasks/:name/run (triggerTask) не трогает task.lastRunId/lastRunAt" },
"sched/r6-ssh-fingerprint-error": { st: "⚪", desc: "r6: SSH fingerprint error при подключении воркеров" },
"snolla/admin-api-batch-info-wave": { st: "⚪", desc: "POST /:resource/info для ВСЕХ шаблонизируемых сущностей (0.8.0; паттерн ozon-seller-api, решение vitya)" },
"penrose-quantizer/reverse-engineer-schematics": { st: "⚪", desc: "Разобрать схемы Penrose Quantizer (оригинал + BugBrand), pin mapping + отличия в вики" },
"books/ozon-promotion-reprice-cli": { st: "⚪", desc: "Штатный CLI смены discount_percent у активной доменной акции (сейчас reprice ручной двухшаговый)" },
"books/slovo-shelf-sale-25pct": { st: "🔴", desc: "Slovo долгопрод 25% на 2 месяца (канал id_sales_channel=2), аналог domain-promotions" },
};
function git(repoDir, args) {
try {
return execSync(`git -C "${repoDir}" ${args}`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
} catch { return ""; }
}
function collect() {
const dirs = new Set(fs.readdirSync(ROOT).filter(d => { try { return fs.statSync(path.join(ROOT, d)).isDirectory(); } catch { return false; } }));
[".common", ".wiki", ".workshop", ".factory", ".admin"].forEach(d => dirs.add(d));
const files = [];
for (const repo of dirs) {
if (SKIP.has(repo)) continue;
const td = path.join(ROOT, repo, ".tasks");
if (!fs.existsSync(path.join(td, "STATUS.md"))) continue;
for (const sub of ["", "done"]) {
const dir = sub ? path.join(td, "done") : td;
if (!fs.existsSync(dir)) continue;
for (const f of fs.readdirSync(dir)) {
// date-only name: yyyy-mm-dd-<slug>.md (slug starts with a letter — numbered files have 5 digits after date)
if (!/^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*\.md$/.test(f)) continue;
const m = f.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
files.push({ repo, dir: sub, date: m[1], slug: m[2], old: f });
}
}
}
files.sort((a, b) =>
a.date < b.date ? -1 : a.date > b.date ? 1
: a.repo < b.repo ? -1 : a.repo > b.repo ? 1
: a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
files.forEach((f, i) => (f.number = NEXT + i));
return files;
}
function makeBlock(r, num) {
const spec = RESTORE[`${r.repo}/${r.slug}`];
const st = spec ? spec.st : "⚪";
const desc = spec ? spec.desc : r.slug;
const file = path.join(ROOT, r.repo, ".tasks", r.dir, r.old);
let where = "";
try {
const content = fs.readFileSync(file, "utf8");
const lines = content.split(/\r?\n/).filter(Boolean).slice(0, 14);
const goal = lines.find(l => /^## (Goal|Цель|Проблема)/.test(l));
if (goal) {
const idx = lines.indexOf(goal);
const next = lines[idx + 1];
if (next && !next.startsWith("#")) where = next.slice(0, 220);
}
} catch {}
return `## ${st} [#${num} ${r.slug}] — ${desc}
**Status:** ${st === "🔴" ? "active" : "ready"}
**Created:** ${r.date}
**Where I stopped:** ${where || "Файл-спека без блока; восстановлен при миграции v2 (см. per-task файл)."}
**Next action:** См. per-task файл ${r.date}-${String(num).padStart(5, "0")}-${r.slug}.md (Goal/Open questions).
**Branch:** n/a
`;
}
function main() {
const files = collect();
const byRepo = {};
for (const f of files) byRepo[f.repo] = (byRepo[f.repo] || 0) + 1;
const restored = files.filter(f => RESTORE[`${f.repo}/${f.slug}`]);
console.log(`файлов к нумерации: ${files.length} (номера ${NEXT}..${NEXT + files.length - 1}), восстановление блоков: ${restored.length}`);
for (const [r, n] of Object.entries(byRepo).sort((a, b) => b[1] - a[1])) console.log(` ${r.padEnd(20)} ${n}`);
console.log("восстановить:", restored.map(r => `#${r.number} ${r.repo}/${r.slug}`).join("; "));
if (!APPLY) { console.log("\ndry-run (--apply для выполнения)"); return; }
const byRepoFiles = {};
for (const f of files) (byRepoFiles[f.repo] = byRepoFiles[f.repo] || []).push(f);
for (const [repo, list] of Object.entries(byRepoFiles)) {
const repoDir = path.join(ROOT, repo);
const conflicted = git(repoDir, "status --porcelain").split("\n").some(l => /^(UU|AA|DD|AU|UA|DU|UD)/.test(l));
if (conflicted) { console.log(`skip ${repo}: conflicted`); continue; }
let renamed = 0;
for (const f of list) {
const dir = f.dir ? path.join(repoDir, ".tasks", "done") : path.join(repoDir, ".tasks");
const src = path.join(dir, f.old);
const dst = path.join(dir, `${f.date}-${String(f.number).padStart(5, "0")}-${f.slug}.md`);
if (fs.existsSync(src)) { fs.renameSync(src, dst); renamed++; }
}
// insert restored blocks (append at board end)
const td = path.join(repoDir, ".tasks");
const statusPath = path.join(td, "STATUS.md");
let status = fs.readFileSync(statusPath, "utf8").replace(/\r?\n$/, "");
let blocksAdded = 0;
for (const f of list.filter(x => RESTORE[`${x.repo}/${x.slug}`])) {
status += "\n---\n" + makeBlock(f, f.number);
blocksAdded++;
}
if (blocksAdded) fs.writeFileSync(statusPath, status);
if (renamed || blocksAdded) {
git(repoDir, "add -- .tasks/");
git(repoDir, `commit -m "meta(tasks): number ${renamed} legacy files to v2 + restore ${blocksAdded} blocks" -q`);
console.log(`done: ${repo} (renamed ${renamed}, blocks ${blocksAdded})`);
}
}
console.log(`\nИтог: counter должен стать ${NEXT + files.length - 1} (записать в agenda).`);
}
main();