Skip to content
SlashClone
Made by@opsdesk

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.

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