Elt weekly brief
@opsdesk/elt-weekly-brief
Turn each team's weekly update into one ELT brief: asks that need a decision first, risks ranked by severity, metrics by team, wins, and a list of who has not reported yet.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1#!/usr/bin/env node
- 2// Compile one markdown update per team into a single ELT weekly brief.
- 3// Usage: node scripts/compile-brief.mjs <updates-dir> <out.md> [--week "Week of 2026-11-02"] [--expect Sales,Marketing,...]
- 4// Each update file: "# Team name", then any of "## Metrics", "## Wins", "## Risks", "## Asks" with "- " bullets.
- 5// Only Node built-ins. Reads the folder, writes one markdown file, prints a summary.
- 7import { readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
- 8import { join, basename, dirname } from "node:path";
- 10const SECTIONS = ["metrics", "wins", "risks", "asks"];
- 11const ALIASES = {
- 12 metrics: "metrics",
- 13 kpis: "metrics",
- 14 numbers: "metrics",
- 15 wins: "wins",
- 16 highlights: "wins",
- 17 risks: "risks",
- 18 blockers: "risks",
- 19 lowlights: "risks",
- 20 issues: "risks",
- 21 asks: "asks",
- 22 "asks of the elt": "asks",
- 23 decisions: "asks",
- 24 "decisions needed": "asks",
- 25 help: "asks",
- 26};
- 28function fail(message) {
- 29 console.error(`compile-brief: ${message}`);
- 30 console.error(
- 31 'Usage: node scripts/compile-brief.mjs <updates-dir> <out.md> [--week "Week of 2026-11-02"] [--expect Sales,Marketing]',
- 32 );
- 33 process.exit(1);
- 34}
- 36const args = process.argv.slice(2);
- 37const positional = [];
- 38let week = "";
- 39let expect = [];
- 40for (let i = 0; i < args.length; i++) {
- 41 const a = args[i];
- 42 if (a === "--week") week = args[++i] ?? "";
- 43 else if (a === "--expect")
- 44 expect = (args[++i] ?? "")
- 45 .split(",")
- 46 .map((s) => s.trim())
- 47 .filter(Boolean);
- 48 else if (a.startsWith("--")) fail(`unknown option ${a}`);
- 49 else positional.push(a);
- 50}
- 51const [dir, outPath] = positional;
- 52if (!dir || !outPath) fail("need an updates folder and an output path.");
- 54let files;
- 55try {
- 56 if (!statSync(dir).isDirectory()) fail(`${dir} is not a folder.`);
- 57 files = readdirSync(dir)
- 58 .filter((f) => /\.(md|markdown|txt)$/i.test(f))
- 59 .sort();
- 60} catch (e) {
- 61 fail(`cannot read ${dir}: ${e.message}`);
- 62}
- 63if (files.length === 0) fail(`no .md or .txt files in ${dir}. Put one update per team there.`);
- 65function parse(text, fallbackName) {
- 66 const team = { name: fallbackName, metrics: [], wins: [], risks: [], asks: [], unsorted: [] };
- 67 let current = "unsorted";
- 68 for (const raw of text.split(/\r?\n/)) {
- 69 const line = raw.trimEnd();
- 70 const h1 = /^#\s+(.+)$/.exec(line);
- 71 if (h1) {
- 72 team.name = h1[1].trim();
- 73 continue;
- 74 }
- 75 const h2 = /^#{2,3}\s+(.+)$/.exec(line);
- 76 if (h2) {
- 77 const key = h2[1].trim().toLowerCase().replace(/[:.]$/, "");
- 78 current = ALIASES[key] ?? "unsorted";
- 79 continue;
- 80 }
- 81 const bullet = /^\s*(?:[-*+]|\d+[.)])\s+(.+)$/.exec(line);
- 82 if (bullet) team[current].push(bullet[1].trim());
- 83 }
- 84 return team;
- 85}
- 87const teams = files.map((f) => parse(readFileSync(join(dir, f), "utf8"), basename(f).replace(/\.[^.]+$/, "")));
- 89const severity = (s) => (/\[(high|red|p0|p1)\]/i.test(s) ? 0 : /\[(med|medium|amber|yellow|p2)\]/i.test(s) ? 1 : 2);
- 90const allRisks = teams
- 91 .flatMap((t) => t.risks.map((r) => ({ team: t.name, text: r })))
- 92 .sort((a, b) => severity(a.text) - severity(b.text));
- 93const allAsks = teams.flatMap((t) => t.asks.map((a) => ({ team: t.name, text: a })));
- 94const reported = new Set(teams.map((t) => t.name.toLowerCase()));
- 95const missing = expect.filter((e) => !reported.has(e.toLowerCase()));
- 96const gaps = teams
- 97 .map((t) => ({ team: t.name, empty: SECTIONS.filter((s) => t[s].length === 0) }))
- 98 .filter((g) => g.empty.length > 0);
- 100const out = [];
- 101out.push(`# ELT weekly brief${week ? `: ${week}` : ""}`, "");
- 102out.push(
- 103 `Teams reporting: ${teams.length} | Asks: ${allAsks.length} | Risks: ${allRisks.length} | Missing updates: ${missing.length ? missing.join(", ") : "none"}`,
- 104 "",
- 105);
- 106out.push("## Asks (need a decision or help this week)", "");
- 107if (allAsks.length === 0) out.push("- None this week.");
- 108allAsks.forEach((a, i) => out.push(`${i + 1}. **${a.team}:** ${a.text}`));
- 109out.push("", "## Risks (highest first)", "");
- 110if (allRisks.length === 0) out.push("- None reported.");
- 111for (const r of allRisks) out.push(`- **${r.team}:** ${r.text}`);
- 112out.push("", "## Metrics", "");
- 113for (const t of teams) {
- 114 out.push(`### ${t.name}`, "");
- 115 if (t.metrics.length === 0) out.push("- No metrics reported.");
- 116 for (const m of t.metrics) out.push(`- ${m}`);
- 117 out.push("");
- 118}
- 119out.push("## Wins", "");
- 120const wins = teams.flatMap((t) => t.wins.map((w) => `- **${t.name}:** ${w}`));
- 121out.push(...(wins.length ? wins : ["- None reported."]));
- 122const unsorted = teams.flatMap((t) => t.unsorted.map((u) => `- **${t.name}:** ${u}`));
- 123if (unsorted.length) out.push("", "## Other notes (outside the four sections)", "", ...unsorted);
- 124if (gaps.length || missing.length) {
- 125 out.push("", "## Gaps to chase", "");
- 126 for (const m of missing) out.push(`- ${m}: no update received.`);
- 127 for (const g of gaps) out.push(`- ${g.team}: no ${g.empty.join(", ")}.`);
- 128}
- 129out.push("");
- 131try {
- 132 mkdirSync(dirname(outPath), { recursive: true });
- 133 writeFileSync(outPath, out.join("\n"));
- 134} catch (e) {
- 135 fail(`cannot write ${outPath}: ${e.message}`);
- 136}
- 137console.log(
- 138 `Wrote ${outPath}: ${teams.length} teams, ${allAsks.length} asks, ${allRisks.length} risks, missing: ${missing.length ? missing.join(", ") : "none"}.`,
- 139);
