#!/usr/bin/env node // scripts/yardstick.mjs // The yardstick - Voyage's three numbers, each with a denominator read from its SOURCE: // // use new work that went through Voyage (the order queue joined to trekbrief-stats.jsonl // by order id. New work = an order whose // `` marker // says so, dated on or after the latest of the // cutoff, MARKER_SINCE and USE_COUNTING_SINCE. // YES = a /trekbrief record names the order's // id (its file stem) under one slug that names // no other order. NO = the order's own // `` line. No record, // or a disputed one, is not measured, never a // no. USE_COUNTING_SINCE unset = counting has // not started: not countable) // trust reviews in a row with nothing that (trekreview-stats.jsonl, read as JSON and // stops a release ordered by `ts` - file order is not // chronological; value = the current ALLOW streak) // interrupts operator interrupts per delivery (every *-stats.jsonl: `user_input` records // that carry a slug or run id) // // plus a fourth row that asks whether the end-state gate can itself be fooled: // // mutants signature-preserving mutants the (M9: every export of the module a behaviour // gate fells probe binds keeps its name and returns // {status:'PASSED'}; every named test only // checks `typeof`. The gate fells it when it // refuses to close the probe on that tree.) // // What `use` can and cannot see (PM 04.10): the "no" side is only what orders // declare, so undeclared non-Voyage work is not measured and the value covers the // measured share only. Known false-positive class: an `arbeidsform: voyage` // order claimed while an unrelated /trekbrief runs would be named by that // record; PM chose no repo-mailbox change (the record must itself name the id) - // a strong link would need the claim to name the claiming session. // // A number the source cannot give is NOT COUNTABLE - never 0. The verdict over the first five // deliveries is not judged by anything yet, and says so. Exit 0 = every number countable, every // mutant felled and the verdict judged and met; 1 = anything short of that; 2 = usage error. // It only READS: the queue, the stats and the repo. The mutant is built in a temp directory. // // Usage: node scripts/yardstick.mjs [--json] [--root ] [--coord ] [--data ] // [--cutoff YYYY-MM-DD] import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, mkdtempSync, realpathSync, rmSync, } from 'node:fs'; import { homedir, tmpdir } from 'node:os'; import { join, resolve, dirname, relative, sep, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { evaluateCheck, loadRegistry, TEST_TIMEOUT_MS } from './end-state-gate.mjs'; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); export const DEFAULT_CUTOFF = '2026-09-21'; // The day pre-order-loesningsvalg.sh began refusing an order without the marker (PM 02.10). export const MARKER_SINCE = '2026-09-28'; // The day `use` starts counting. Unset until the operator declares the work done // (operator 04.10): until then `use` is NOT COUNTABLE and every order not measured. export const USE_COUNTING_SINCE = null; export const DEFAULT_COORD = join(homedir(), '.claude', 'coord'); export const DEFAULT_DATA = process.env.CLAUDE_PLUGIN_DATA || join(homedir(), '.claude', 'plugins', 'data', 'voyage-ktg-plugin-marketplace'); const REVIEW_FILE = 'trekreview-stats.jsonl'; const BRIEF_FILE = 'trekbrief-stats.jsonl'; const toPosix = (p) => p.split(sep).join('/'); function walk(dir) { const out = []; for (const name of readdirSync(dir)) { const p = join(dir, name); if (statSync(p).isDirectory()) out.push(...walk(p)); else out.push(p); } return out; } // --- the stats files ----------------------------------------------------------- /** * Every `*-stats.jsonl` in the data directory, parsed line by line. A blank line * is not a line; a line that is not JSON is a line, and is reported by number. */ export function loadStats(dataDir) { if (!existsSync(dataDir) || !statSync(dataDir).isDirectory()) { return { dir: dataDir, missing: true, files: [] }; } const files = readdirSync(dataDir) .filter((n) => n.endsWith('-stats.jsonl')) .sort() .map((name) => { const path = join(dataDir, name); const records = []; const invalid = []; let lines = 0; readFileSync(path, 'utf8').split('\n').forEach((line, i) => { if (line.trim() === '') return; lines++; try { records.push(JSON.parse(line)); } catch { invalid.push(i + 1); } }); return { name, path, lines, records, invalid }; }); return { dir: dataDir, missing: false, files }; } /** * The orders /trekbrief runs named: order id -> its slugs, and slug -> its order * ids. Only trekbrief-stats.jsonl - the one record that carries order_id. */ export function orderRuns(stats) { const byOrder = new Map(); const bySlug = new Map(); const file = stats.files.find((f) => f.name === BRIEF_FILE); for (const r of file ? file.records : []) { if (!r || typeof r.order_id !== 'string' || r.order_id === '') continue; const slug = typeof r.slug === 'string' ? r.slug : ''; if (!byOrder.has(r.order_id)) byOrder.set(r.order_id, new Set()); byOrder.get(r.order_id).add(slug); if (!bySlug.has(slug)) bySlug.set(slug, new Set()); bySlug.get(slug).add(r.order_id); } return { byOrder, bySlug }; } const notCountable = (detail, extra = {}) => ({ countable: false, value: null, detail, ...extra }); // --- 2 · TRUST ------------------------------------------------------------------- export function measureTrust(stats) { const base = { id: 'trust', label: 'reviews in a row with nothing that stops a release' }; const file = stats.files.find((f) => f.name === REVIEW_FILE); if (!file) { return { ...base, source: join(stats.dir, REVIEW_FILE), denominator: 0, facts: {}, ...notCountable(stats.missing ? `data directory ${stats.dir} not found` : `${REVIEW_FILE} not found`) }; } const reviews = file.records.filter((r) => r && typeof r.verdict === 'string'); const undated = reviews.filter((r) => Number.isNaN(Date.parse(r.ts))).length; // Stable sort on ts: the file is appended by whoever ran a review, in no fixed order. const ordered = reviews .map((r, i) => ({ r, i, t: Date.parse(r.ts) })) .filter((x) => !Number.isNaN(x.t)) .sort((a, b) => a.t - b.t || a.i - b.i) .map((x) => x.r); const verdicts = {}; for (const r of reviews) verdicts[r.verdict] = (verdicts[r.verdict] ?? 0) + 1; let longest = 0; let run = 0; for (const r of ordered) { run = r.verdict === 'ALLOW' ? run + 1 : 0; longest = Math.max(longest, run); } const last = ordered.at(-1) ?? null; const facts = { verdicts, longestStreak: longest, invalid: file.invalid.length, undated, last: last ? { ts: last.ts, slug: last.slug, verdict: last.verdict } : null, }; const notes = []; if (file.invalid.length) notes.push(`${file.invalid.length} invalid line(s) left out (line ${file.invalid.join(', ')})`); if (undated) notes.push(`${undated} review(s) without a readable ts left out of the streak`); if (reviews.length === 0) { return { ...base, source: file.path, denominator: 0, facts, ...notCountable(['no review rows', ...notes].join('; ')) }; } const counts = Object.entries(verdicts).map(([k, v]) => `${k} ${v}`).join(' · '); const detail = [ `${counts}; longest ALLOW streak ${longest}; last review ${last ? `${last.ts} ${last.verdict} (${last.slug})` : 'n/a'}`, 'ordered by ts, read as JSON', ...notes, ].join('; '); return { ...base, source: file.path, countable: true, value: run, denominator: reviews.length, facts, detail }; } // --- 1 · USE --------------------------------------------------------------------- // A minimal frontmatter reader: `key: value` lines between the first two `---`. function frontmatter(text) { const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!m) return null; const fm = {}; for (const line of m[1].split(/\r?\n/)) { const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*?)\s*$/); if (kv) fm[kv[1]] = kv[2]; } return fm; } // The solution-choice marker dispatch writes on its own line in the order body. A mention // inside prose (mid-line, in backticks) is not a marker. const MARKER = /^