build-digest.mjs
Builds the Monday Teams digest as an Adaptive Card — runs in the redline-metrics repo.
What this is
Builds the Monday stakeholder digest — open/stale PR counts, seed recall, dashboard link — as a Teams Adaptive Card from already-collected telemetry.
How to onboard it
Nothing to onboard, and nothing an onboarded repository ever runs. This is maintainer tooling: it ships in this repository and runs where it already has an environment.
- Runs in: The redline-metrics repo — never this repo.
- Trigger: workflows/weekly-digest.yml, step "Build Adaptive Card" — every Monday 07:00 UTC plus workflow_dispatch. A separate step then POSTs the card to a Power Automate Teams webhook.
To run it yourself you need a checkout of this repository and Node 22 or newer. There are no runtime dependencies to install — every script uses only Node builtins — so a clone and the environment below is the whole setup.
How to use it
$ npx redlinegate metrics digest --org acme --out digest.jsonEnvironment:
DATA_DIR=data, DAYS=7, ORG=org, OPEN_PRS, STALE_PRS, CAPPED, DASHBOARD_URL, SEED_SCORES=data/seed-scores.jsonl
Nothing, normally — it runs itself every Monday. Run it locally to preview digest.json before changing what the card reports.
Expected output
Writes the Adaptive Card JSON to the file named by --out (stdout otherwise). Produces nothing on its own — delivery to Teams is a separate workflow step and needs TEAMS_WEBHOOK_URL.
How to edit it
- Edit the .mjs file directlyNothing generates these — scripts/ is hand-written maintainer tooling with no runtime dependencies. Keep it that way: package.json declares none, and these run in CI with only Node's builtins available.
- node --check scripts/<file>.mjsCI's lint job parses every script in scripts/. A syntax error there fails the build without running anything.
- Run it locally with the same env CI gives itEach script is env-configured with no argument parsing, so a local run is the CI run. The environment variables it needs are listed above.
The full file
#!/usr/bin/env node
// Builds the weekly stakeholder digest from collected telemetry and writes an Adaptive
// Card payload to stdout (or --out <file>).
//
// Microsoft retired Office 365 connectors, so the old `{"text": "..."}` webhook shape no
// longer delivers. The target is a Power Automate "When a Teams webhook request is
// received" flow, which expects an Adaptive Card.
//
// Env: DATA_DIR (default data), DAYS (default 7), ORG, [OPEN_PRS], [STALE_PRS], [CAPPED],
// [DASHBOARD_URL], [SEED_SCORES=data/seed-scores.jsonl]
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
import { loadRecords, aggregate, noisiestRules, pct } from './lib/metrics.mjs';
const {
DATA_DIR = 'data',
DAYS = '7',
ORG = 'org',
OPEN_PRS = '',
STALE_PRS = '',
CAPPED = '',
DASHBOARD_URL = '',
SEED_SCORES = 'data/seed-scores.jsonl',
} = process.env;
const outIndex = process.argv.indexOf('--out');
const outFile = outIndex === -1 ? null : process.argv[outIndex + 1];
const since = new Date(Date.now() - Number(DAYS) * 86400000).toISOString();
const { records, problems } = loadRecords(DATA_DIR, since);
const agg = aggregate(records);
const noisy = noisiestRules(agg);
// Latest seed score per repo, so the digest reports whether review quality is verified
// rather than assumed.
const seedScores = [];
if (existsSync(SEED_SCORES)) {
const latest = new Map();
for (const line of readFileSync(SEED_SCORES, 'utf8').split('\n')) {
if (!line.trim()) continue;
try {
const score = JSON.parse(line);
latest.set(score.repo, score);
} catch {
problems.push('seed-scores.jsonl has an unparseable line');
}
}
seedScores.push(...latest.values());
}
const facts = [
{ title: 'PRs merged (instrumented)', value: `${agg.prs} across ${agg.repos} repo(s)` },
{ title: 'PRs with findings', value: `${agg.prsWithFindings} (${pct(agg.prsWithFindings, agg.prs)})` },
{ title: 'Findings', value: `${agg.findings} — ${agg.blocker} BLOCKER, ${agg.high} HIGH` },
{ title: 'Acted on (thread resolved)', value: `${agg.resolved} (${pct(agg.resolved, agg.findings)})` },
{ title: 'Ignored (stale + outdated)', value: `${agg.stale} (${pct(agg.stale, agg.findings)})` },
{ title: 'Gate exemptions used', value: `${agg.exempted} (${pct(agg.exempted, agg.prs)})` },
];
if (OPEN_PRS) {
facts.push({
title: 'Open PRs',
value: OPEN_PRS + (STALE_PRS ? ` — ${STALE_PRS} idle over 7 days` : ''),
});
}
if (seedScores.length) {
const worst = seedScores.reduce((a, b) =>
a.totals.blocker_recall <= b.totals.blocker_recall ? a : b
);
facts.push({
title: 'Seed validation (worst repo)',
value: `${worst.repo}: BLOCKER recall ${Math.round(worst.totals.blocker_recall * 100)}%, ${worst.totals.false_positives_on_clean} false positive(s)`,
});
}
const topRepos = agg.byRepo.slice(0, 5);
const warnings = [];
if (agg.prs === 0) warnings.push('No telemetry for this window. The collector may be failing — check redline-collect.');
if (agg.withoutRuleId > 0) {
warnings.push(
`${agg.withoutRuleId} finding(s) carried no rule id — per-rule tuning is blind for those repos until the current standards land.`
);
}
if (agg.unknownRuleIds > 0) warnings.push(`${agg.unknownRuleIds} finding(s) cited a rule id that is not in the catalogue.`);
if (agg.untagged > 0) warnings.push(`${agg.untagged} finding(s) ignored the output contract entirely — severity is a guess for those.`);
if (agg.findings > 0 && agg.stale / agg.findings > 0.3) {
warnings.push('Over 30% of findings are being ignored. Tune the noisiest rules below before adding new ones.');
}
if (seedScores.some((s) => s.totals.blocker_recall < 1)) {
warnings.push('At least one pilot repo is below 100% BLOCKER recall on the seed corpus. Do not widen the rollout.');
}
if (CAPPED) warnings.push('Search result cap was hit during collection — counts are a lower bound.');
warnings.push(...problems);
const bullets = (rows) => rows.join('\n');
const card = {
type: 'message',
attachments: [
{
contentType: 'application/vnd.microsoft.card.adaptive',
contentUrl: null,
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
type: 'AdaptiveCard',
version: '1.4',
body: [
{ type: 'TextBlock', size: 'Large', weight: 'Bolder', text: `Redline weekly — ${ORG}` },
{
type: 'TextBlock',
isSubtle: true,
spacing: 'None',
text: `Last ${DAYS} days · generated ${new Date().toISOString().slice(0, 16)}Z`,
},
{ type: 'FactSet', facts },
...(noisy.length
? [
{ type: 'TextBlock', weight: 'Bolder', text: 'Noisiest rules (tuning queue)', spacing: 'Medium' },
{
type: 'TextBlock',
wrap: true,
text: bullets(
noisy.map(
(r) =>
`- \`${r.id}\` — fired ${r.fired}× across ${r.repos} repo(s), ${Math.round(r.ignoredRate * 100)}% ignored`
)
),
},
]
: []),
...(topRepos.length
? [
{ type: 'TextBlock', weight: 'Bolder', text: 'Most findings', spacing: 'Medium' },
{
type: 'TextBlock',
wrap: true,
text: bullets(
topRepos.map((r) => `- ${r.repo}: ${r.findings} (${r.blocker} BLOCKER, ${r.stale} ignored)`)
),
},
]
: []),
...(warnings.length
? [
{ type: 'TextBlock', weight: 'Bolder', color: 'Attention', text: 'Needs attention', spacing: 'Medium' },
{ type: 'TextBlock', wrap: true, color: 'Attention', text: bullets(warnings.map((w) => `- ${w}`)) },
]
: []),
],
...(DASHBOARD_URL
? { actions: [{ type: 'Action.OpenUrl', title: 'Open dashboard', url: DASHBOARD_URL }] }
: {}),
},
},
],
};
const payload = JSON.stringify(card);
if (outFile) {
writeFileSync(outFile, payload);
console.error(`digest written to ${outFile} (${agg.prs} PRs, ${agg.findings} findings, ${warnings.length} warning(s))`);
} else {
process.stdout.write(payload);
}