|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * ADR-0076 D7 trigger metric (#2462): cross-package commit ratio of the |
| 4 | + * ObjectQL engine core (`engine.ts` / `registry.ts`). |
| 5 | + * |
| 6 | + * The engine repo-split (D7) is trigger-gated: it may only happen once the |
| 7 | + * share of engine-core commits that ALSO touch files outside |
| 8 | + * `packages/objectql/` falls to a low, stable level (it was ~88% when the |
| 9 | + * ADR was written — i.e. the engine still co-evolves with the rest of the |
| 10 | + * monorepo and is NOT separable). This script computes that ratio from git |
| 11 | + * history so CI can track it over time. |
| 12 | + * |
| 13 | + * Run: node scripts/check-engine-split-ratio.mjs [--days N] [--threshold PCT] |
| 14 | + * |
| 15 | + * --days N Look-back window in days (default 90). |
| 16 | + * --threshold PCT Optional gate: exit 1 if the ratio is ABOVE the given |
| 17 | + * percentage. By default the script is REPORT-ONLY (always |
| 18 | + * exits 0) — the split threshold is an open question |
| 19 | + * (ADR-0076 OQ#5) and is set deliberately, not by default. |
| 20 | + * |
| 21 | + * Output: a human-readable summary on stdout; when $GITHUB_STEP_SUMMARY is |
| 22 | + * set, a markdown section is appended for the Actions run summary. |
| 23 | + * |
| 24 | + * Requires full git history (in CI: actions/checkout with fetch-depth: 0). |
| 25 | + * Zero third-party dependencies. |
| 26 | + */ |
| 27 | + |
| 28 | +import { execFileSync } from 'node:child_process'; |
| 29 | +import { appendFileSync } from 'node:fs'; |
| 30 | +import { dirname, resolve } from 'node:path'; |
| 31 | +import { fileURLToPath } from 'node:url'; |
| 32 | + |
| 33 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 34 | +const repoRoot = resolve(__dirname, '..'); |
| 35 | + |
| 36 | +const ENGINE_CORE = ['packages/objectql/src/engine.ts', 'packages/objectql/src/registry.ts']; |
| 37 | +const ENGINE_PACKAGE_PREFIX = 'packages/objectql/'; |
| 38 | + |
| 39 | +function arg(name, fallback) { |
| 40 | + const i = process.argv.indexOf(`--${name}`); |
| 41 | + if (i !== -1 && process.argv[i + 1] !== undefined) return process.argv[i + 1]; |
| 42 | + return fallback; |
| 43 | +} |
| 44 | + |
| 45 | +const days = Number(arg('days', '90')); |
| 46 | +const thresholdRaw = arg('threshold', ''); |
| 47 | +const threshold = thresholdRaw === '' ? null : Number(thresholdRaw); |
| 48 | +if (!Number.isFinite(days) || days <= 0) { |
| 49 | + console.error(`Invalid --days value: ${arg('days', '90')}`); |
| 50 | + process.exit(2); |
| 51 | +} |
| 52 | +if (thresholdRaw !== '' && (!Number.isFinite(threshold) || threshold < 0 || threshold > 100)) { |
| 53 | + console.error(`Invalid --threshold value: ${thresholdRaw} (expected 0-100)`); |
| 54 | + process.exit(2); |
| 55 | +} |
| 56 | + |
| 57 | +function git(...args) { |
| 58 | + return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); |
| 59 | +} |
| 60 | + |
| 61 | +// Commits in the window that touched the engine core. |
| 62 | +const shas = git( |
| 63 | + 'log', `--since=${days} days ago`, '--format=%H', '--', ...ENGINE_CORE, |
| 64 | +).split('\n').filter(Boolean); |
| 65 | + |
| 66 | +let crossPackage = 0; |
| 67 | +for (const sha of shas) { |
| 68 | + const files = git('show', '--name-only', '--format=', sha).split('\n').filter(Boolean); |
| 69 | + if (files.some((f) => !f.startsWith(ENGINE_PACKAGE_PREFIX))) crossPackage += 1; |
| 70 | +} |
| 71 | + |
| 72 | +const total = shas.length; |
| 73 | +const ratio = total === 0 ? 0 : (crossPackage / total) * 100; |
| 74 | +const ratioStr = ratio.toFixed(1); |
| 75 | + |
| 76 | +const lines = [ |
| 77 | + `ADR-0076 D7 trigger metric — engine cross-package commit ratio`, |
| 78 | + ` window: last ${days} days`, |
| 79 | + ` engine-core commits: ${total} (${ENGINE_CORE.join(', ')})`, |
| 80 | + ` also cross-package: ${crossPackage}`, |
| 81 | + ` ratio: ${total === 0 ? 'n/a (no engine-core commits in window)' : `${ratioStr}%`}`, |
| 82 | + ``, |
| 83 | + `Reference: ~88% at ADR time (2026-06) — the engine repo-split (D7) stays`, |
| 84 | + `deferred until this ratio is low and stable (threshold TBD, ADR-0076 OQ#5).`, |
| 85 | +]; |
| 86 | +console.log(lines.join('\n')); |
| 87 | + |
| 88 | +if (process.env.GITHUB_STEP_SUMMARY) { |
| 89 | + const md = [ |
| 90 | + `### ADR-0076 D7 trigger metric — engine cross-package commit ratio`, |
| 91 | + ``, |
| 92 | + `| Window | Engine-core commits | Cross-package | Ratio |`, |
| 93 | + `|---|---|---|---|`, |
| 94 | + `| last ${days} days | ${total} | ${crossPackage} | ${total === 0 ? 'n/a' : `${ratioStr}%`} |`, |
| 95 | + ``, |
| 96 | + `Reference: ~88% at ADR time. D7 (engine repo-split) stays deferred until this is low and stable (OQ#5).`, |
| 97 | + ``, |
| 98 | + ].join('\n'); |
| 99 | + appendFileSync(process.env.GITHUB_STEP_SUMMARY, md); |
| 100 | +} |
| 101 | + |
| 102 | +if (threshold !== null && total > 0 && ratio > threshold) { |
| 103 | + console.error(`\nRatio ${ratioStr}% exceeds the configured threshold of ${threshold}% — engine is not separable.`); |
| 104 | + process.exit(1); |
| 105 | +} |
0 commit comments