/** * Regenerate `TODO.md` — the index over `todo/`. * * The index is the one file everybody reads, so it is the one most able to lie. It is generated rather * than maintained: `bun run todo:index` after any change to `todo/`, and it cannot drift from the tree * it describes. */ import { readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = new URL('..', import.meta.url).pathname; const OUT = join(ROOT, 'todo'); export type Item = { id: string; title: string; theme: string; status: string; created: string; order: number; file: string; }; /** Minimal frontmatter reader — `key: value`, values optionally JSON-quoted. No YAML dependency. */ export function frontmatter(text: string): Record { const end = text.indexOf('\n---', 4); if (!text.startsWith('---') || end === -1) return {}; const out: Record = {}; for (const line of text.slice(4, end).split('\n')) { const m = line.match(/^([a-z_]+):\s*(.*)$/); if (!m || !m[1]) continue; const raw = (m[2] ?? '').trim(); out[m[1]] = raw.startsWith('"') ? (JSON.parse(raw) as string) : raw; } return out; } export function readTree(): { themes: { theme: string; heading: string; items: Item[] }[] } { const themes = readdirSync(OUT) .filter((d) => statSync(join(OUT, d)).isDirectory()) .sort() .map((theme) => { const files = readdirSync(join(OUT, theme)).filter((f) => f.endsWith('.md')); const meta = frontmatter(readFileSync(join(OUT, theme, '_index.md'), 'utf8')); const items = files .filter((f) => f !== '_index.md') .map((f) => { const fm = frontmatter(readFileSync(join(OUT, theme, f), 'utf8')); return { id: fm.id ?? `${theme}/${f.replace(/\.md$/, '')}`, title: fm.title ?? f, theme, status: fm.status ?? 'open', created: fm.created ?? '', order: Number(fm.order ?? 0), file: `todo/${theme}/${f}`, }; }) .sort((a, b) => a.order - b.order); return { theme, heading: meta.heading ?? theme, items }; }); return { themes }; } const HEADER = `# TODO — the index Deferred work. **The items live in \`todo/\`, one file each**, grouped by theme; this file is the map over them and is GENERATED — run \`bun run todo:index\` after any change rather than editing it here. Format and the two-way-sync contract: \`todo/README.md\`. **Context, corrected 2026-08-07.** This list used to open by saying Officer was "collapsing from multi-tenant / open-source-ready to a **single-user platform**", and told you to treat multi-tenant indirection as accidental complexity. **That direction was reversed.** The permission model shipped on 2026-08-07 to serve a real goal — deploy to the company server, onboard people, give each one their own Gitea account through the platform. Per-user scoping is now a requirement, and the items that proposed deleting it have been removed rather than left to mislead the next reader. What did NOT reverse: \`execution\` permissions (terminal, chat, tasks, files, desktop, browser) run as the owner's OS user and can never be granted. Indirection there really is accidental complexity. `; function render(): string { const { themes } = readTree(); const open = themes.reduce((n, t) => n + t.items.filter((i) => i.status !== 'done').length, 0); const done = themes.reduce((n, t) => n + t.items.filter((i) => i.status === 'done').length, 0); const rows = themes .filter((t) => t.items.length) .map((t) => { const o = t.items.filter((i) => i.status !== 'done').length; const d = t.items.length - o; return `| [${t.heading}](todo/${t.theme}/) | ${o} | ${d} |`; }); const recent = themes .flatMap((t) => t.items) .filter((i) => i.status !== 'done') .sort((a, b) => b.created.localeCompare(a.created)) .slice(0, 10) .map((i) => `- \`${i.created}\` **${i.title}** — [${i.id}](${i.file})`); // A theme holding only prose has no row above, and an entry nobody can see is the failure mode this // whole split risks. List them explicitly rather than letting them age out of view. const notes = themes.filter((t) => !t.items.length).map((t) => `- [${t.heading}](todo/${t.theme}/_index.md)`); return [ HEADER, `## Themes\n`, `**${open} open, ${done} closed**, across ${rows.length} themes.\n`, `| Theme | Open | Closed |`, `| --- | --- | --- |`, ...rows, ``, `## Records — no items, kept for the reasoning\n`, ...notes, ``, `## Newest ten\n`, ...recent, ``, ].join('\n'); } if (import.meta.main) { writeFileSync(join(ROOT, 'TODO.md'), render()); console.log('TODO.md regenerated'); }