mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
* docs: move superpowers/research artifacts to isolated _tasks repo + docs tree cleanup
- Move docs/superpowers/{plans,specs} and docs/research/* into the gitignored,
separately-versioned _tasks/ repo; untrack the two tracked research design docs.
- Add CLAUDE.md "Planning & Research Artifacts" section overriding the superpowers
default save paths (docs/... -> _tasks/...); align REPOSITORY_MAP and
DOCUMENTATION_OVERHAUL_PLAN with the new convention.
- Drop 4 now-obsolete /api/discovery/* entries from check-docs-symbols allowlist
(stale-enforcement) and refresh code/spec path comments to _tasks/...
- Sweeps in concurrent docs-tree restructuring (root-level provider/guide docs,
compression spec cleanup, .mcp.json.example removal).
* docs: reorganize docs/ tree + fix stale facts across ~26 docs
Phase A — reorganization:
- Move 7 orphan root docs into subfolders (providers/ created; TIERS+USAGE_QUOTA→guides/;
plugins+PLUGIN_SDK→frameworks/); delete 8 obsolete/redundant docs (SUBMIT_PR superseded
by CONTRIBUTING; DOCUMENTATION_OVERHAUL_PLAN; INCIDENT_RESPONSE/PERF_BUDGETS/THREAT_MODEL;
3 ops snapshots). Rebuild README index (was missing ~40 files) + per-folder meta.json nav.
- Clean 14 dangling doc-path references in bin/ ops scripts, scripts/, workflow, tests;
fix the dockerignore-docs-coverage required-docs path (PROVIDERS→providers/CLAUDE_WEB).
Phase B — content accuracy (verified against code, not the audit summary):
- Functional: ENVIRONMENT flag defaults (INPUT_SANITIZER/MCP_ENFORCE_SCOPES=true,
COMPRESS_DESCRIPTIONS=false, dynamic heap); MCP-SERVER notion tool names (omniroute_*→
notion_*) + counts 87→94; coverage gate 75/70→60/60/60/60 (RELEASE_CHECKLIST, COVERAGE_PLAN,
ERROR_SANITIZATION, CONTRIBUTING); pre-push hook description; regenerate PROVIDER_REFERENCE (237).
- Count drift: providers 237, executors 70, migrations 106, db modules 94, oauth 19,
strategies 17, MCP 94, flags 38, TS 6.0, open-sse ~900/services 294 across architecture/
frameworks/ops docs; AUTO-COMBO 9→12 factors w/ correct DEFAULT_WEIGHTS; REASONING +2
patterns; STEALTH UA defaults; AGENT_PROTOCOLS +cursor-cloud/list-capabilities;
LANGUAGE_PACKS +id pack.
- Kept Node 20 (runtime guard accepts 20.20.2+; only engines is stricter) and MCP scopes=13
(mcpScopes.ts) — both were correct in the docs; corrected only the attribution.
* docs: finish content refresh — compression engines, CLI_TOKEN merge, metadata sweep
- Compression: document the additional built-in engines (CCR, headroom, ionizer,
session-dedup) in COMPRESSION_ENGINES; clarify LLMLingua-2 is the ultra-mode SLM
backend + cross-ref the extra engines in EXTENDING_COMPRESSION; add the id
(Indonesian) language pack to LANGUAGE_PACKS.
- AUTO-COMBO: replace the orphan 'How tiers fit' weight table (stale weights) with a
pointer to the canonical 12-factor DEFAULT_WEIGHTS table.
- Security: merge CLI_TOKEN_AUTH.md (legacy 32-char SHA-256 format) into CLI_TOKEN.md
as a 'Legacy format — still accepted' section (server accepts both HMAC + legacy),
delete CLI_TOKEN_AUTH.md, drop it from the index + security nav.
- Metadata: bump stale frontmatter (version/lastUpdated) to 3.8.40/2026-06-28 across the
doc set audited this pass, and normalize the in-body 'Last updated' header lines to match.
* fix(runtime): drop Node 20 from supported range + align all docs/diagrams/counts
- Node minimum is now 22 (aligned with package.json engines). SUPPORTED_NODE_RANGE in
src/shared/utils/nodeRuntimeSupport.ts (and the bin/ mirror) drops the 20.x line →
'>=22.22.2 <23 || >=24.0.0 <27'; getNodeRuntimeSupport now rejects Node 20 as
unsupported-major. Test updated (TDD): node-runtime-support.test.ts asserts Node 20
rejected. Docs aligned (TROUBLESHOOTING ×2, TERMUX, RELEASE_CHECKLIST, CODEBASE,
CLI-TOOLS, README, llm.txt + 42 i18n llm.txt mirrors, skills/cli-serve).
- Diagrams regenerated: mcp-tools-87 -> mcp-tools-94 (34 base + pool 6 = 94) and
auto-combo-9factor -> auto-combo-12factor (correct DEFAULT_WEIGHTS); SVGs re-rendered
via mermaid-cli; doc refs + diagrams/README updated; fixed a pre-existing broken
resilience-3layers image path.
- CLAUDE.md + AGENTS.md aligned to real counts (237 providers, 94 MCP tools / 34 base,
106 migrations, 94 db modules, 12-factor auto-combo, 17 strategies); README provider
count 231 -> 237; executor count corrected to 68 (provider executors, excl base/index)
and OAuth to 18 across architecture docs. check:docs-all now passes (0 strict drift,
0 broken links); removed dead .mcp.json.example doc link.
* fix(services): update installer Node hint to >=22.22.2 (aligned with dropped Node 20)
* docs: realign counts to current release tip after rebase
The release tip advanced while this work was in flight (Gemini CLI provider/executor
removed by #5246, plus other PRs). Re-counted against the current code and updated:
providers 237->236, executors 68->67, OAuth modules 18->17, open-sse services 294->298;
regenerated PROVIDER_REFERENCE.md (236). check:docs-all passes (0 strict drift).
* docs(changelog) + i18n: record Node 20 drop + fix nodeIncompatibleHint
- CHANGELOG: add [3.8.40] entries for the Node 20.x removal (runtime) and the docs
reorganization/accuracy audit.
- i18n: nodeIncompatibleHint across all 42 locales no longer lists Node 20.x as
supported (ASCII + CJK full-width variants), aligned with the dropped Node 20.
* fix(docs): repair CI breakages from the doc moves
- test: cli-plugin-system asserted docs/dev/plugins.md exists; the file moved to
docs/frameworks/PLUGINS.md — point the test at the new path (Unit fast-path 2/2 fix).
- frontmatter: PLUGINS.md and PLUGIN_SDK.md moved into the fumadocs-indexed
docs/frameworks/ which requires a 'title' frontmatter; the missing frontmatter
failed the Next.js MDX build (dast-smoke 'invalid frontmatter'). Added frontmatter
to both, plus the providers/ docs (consistency; that folder is not indexed).
293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Mutation radiography (Quality Gate v2 / Fase 9 T5 — Onda 2, Task 1).
|
||
*
|
||
* Classifies every COVERING test file by its mutation-kill contribution, using the
|
||
* `killedBy` attribution that the Stryker tap-runner emits per mutant
|
||
* (`coverageAnalysis: perTest`, validated by the Task 12 spike):
|
||
*
|
||
* 🔴 empty — the test file never appears in any `killedBy` (kills no mutant
|
||
* of the mutated modules). Prime R1-prune candidate (Task 2).
|
||
* 🟠 redundant — every mutant it kills is ALSO killed by ≥1 other test file
|
||
* (zero unique kills).
|
||
* 🟡 overlapping — kills ≥1 unique mutant, but the MAJORITY of its kills are shared.
|
||
* 🟢 unique — kills ≥1 mutant that NO other test file kills (and unique kills
|
||
* are not outnumbered by shared kills).
|
||
*
|
||
* CAVEAT — bail-on-first-kill: Stryker bails after the first test kills a mutant
|
||
* (we do NOT set `disableBail`), so `killedBy` lists the FIRST killer, not every
|
||
* killer. Consequence: 🔴 empty is RELIABLE (a sole killer is always recorded, so a
|
||
* file that never appears in killedBy is never the sole killer of any mutant → safe
|
||
* R1-prune candidate w.r.t. mutationScore), but 🟢/🟠/🟡 are OPTIMISTIC — "unique" is
|
||
* overstated and "redundant" understated, because a non-first coverer that WOULD also
|
||
* kill is never recorded. Use 🟢/🟠/🟡 as advisory only; an accurate redundancy split
|
||
* (for R2) needs a `disableBail: true` run. R1 (Task 2) acts on 🔴 alone + a line-
|
||
* coverage cross-check + human review, so bail-on-first is sufficient there.
|
||
*
|
||
* IMPORTANT — multi-batch merge: the nightly splits `mutate` across parallel batches
|
||
* (one mutation.json per batch). Stryker assigns numeric test ids PER RUN, so id "12"
|
||
* in batch c is unrelated to id "12" in batch d. Each report is therefore resolved
|
||
* (id -> file name, via its own `testFiles` section) and classified independently;
|
||
* `aggregateRadiography` then sums the per-FILE kill counts across batches and
|
||
* reclassifies. A file empty in one batch but unique in another is unique overall.
|
||
*
|
||
* Usage:
|
||
* node scripts/quality/mutation-radiography.mjs <mutation-c.json> [<mutation-d.json> ...]
|
||
* The universe of test files (so 🔴 empty files are detectable) defaults to
|
||
* `stryker.conf.json:tap.testFiles`; pass --no-conf-universe to use only the union
|
||
* of the reports' own `testFiles` sections instead.
|
||
*/
|
||
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||
const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
|
||
|
||
export function loadMutationReport(reportPath) {
|
||
return JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||
}
|
||
|
||
/**
|
||
* Threshold rules shared by single-report and aggregated classification.
|
||
* @param {number} uniqueKills mutants this file kills ALONE
|
||
* @param {number} sharedKills mutants this file kills together with others
|
||
*/
|
||
export function classifyFromCounts(uniqueKills, sharedKills) {
|
||
if (uniqueKills === 0 && sharedKills === 0) return "empty";
|
||
if (uniqueKills === 0) return "redundant";
|
||
if (sharedKills > uniqueKills) return "overlapping";
|
||
return "unique";
|
||
}
|
||
|
||
// Map each numeric test id to its file name via the report's `testFiles` section.
|
||
// Real tap-runner reports key killedBy by id; the synthetic test fixtures key it by
|
||
// file name directly (no testFiles section) — those pass through unchanged.
|
||
function buildIdToFile(report) {
|
||
const map = new Map();
|
||
for (const [file, data] of Object.entries(report.testFiles || {})) {
|
||
for (const t of data.tests || []) {
|
||
map.set(String(t.id), t.name || file);
|
||
}
|
||
}
|
||
return map;
|
||
}
|
||
|
||
// Raw per-file kill counts for ONE report (no universe, no classification).
|
||
function countKills(report) {
|
||
const idToFile = buildIdToFile(report);
|
||
const counts = new Map();
|
||
const bump = (file, key) => {
|
||
const c = counts.get(file) || { uniqueKills: 0, sharedKills: 0 };
|
||
c[key] += 1;
|
||
counts.set(file, c);
|
||
};
|
||
for (const data of Object.values(report.files || {})) {
|
||
for (const m of data.mutants || []) {
|
||
if (m.status !== "Killed") continue;
|
||
const killers = [...new Set((m.killedBy || []).map((id) => idToFile.get(String(id)) ?? id))];
|
||
if (killers.length === 0) continue;
|
||
if (killers.length === 1) bump(killers[0], "uniqueKills");
|
||
else for (const k of killers) bump(k, "sharedKills");
|
||
}
|
||
}
|
||
return counts;
|
||
}
|
||
|
||
function materialize(counts, universe) {
|
||
const files = new Set(universe || []);
|
||
for (const f of counts.keys()) files.add(f);
|
||
const out = {};
|
||
for (const file of files) {
|
||
const { uniqueKills = 0, sharedKills = 0 } = counts.get(file) || {};
|
||
out[file] = { class: classifyFromCounts(uniqueKills, sharedKills), uniqueKills, sharedKills };
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Classify the test files of a SINGLE mutation report.
|
||
* @param {object} report parsed mutation.json
|
||
* @param {string[]} [allTestFiles] universe; defaults to the report's testFiles keys
|
||
*/
|
||
export function classifyTestFiles(report, allTestFiles) {
|
||
const universe = allTestFiles || Object.keys(report.testFiles || {});
|
||
return materialize(countKills(report), universe);
|
||
}
|
||
|
||
/**
|
||
* Merge several per-batch reports at the file level, then classify.
|
||
* @param {object[]} reports parsed mutation.json objects (one per batch)
|
||
* @param {string[]} [allTestFiles] universe; defaults to the union of testFiles keys
|
||
*/
|
||
export function aggregateRadiography(reports, allTestFiles) {
|
||
const total = new Map();
|
||
const universe = new Set(allTestFiles || []);
|
||
for (const report of reports) {
|
||
if (!allTestFiles) for (const f of Object.keys(report.testFiles || {})) universe.add(f);
|
||
for (const [file, c] of countKills(report)) {
|
||
const acc = total.get(file) || { uniqueKills: 0, sharedKills: 0 };
|
||
acc.uniqueKills += c.uniqueKills;
|
||
acc.sharedKills += c.sharedKills;
|
||
total.set(file, acc);
|
||
}
|
||
}
|
||
return materialize(total, [...universe]);
|
||
}
|
||
|
||
/**
|
||
* R1 prune-candidate list: the test files with ZERO unique kills — 🔴 empty (kills no
|
||
* mutant) ∪ 🟠 redundant (every mutant it kills is also killed by ≥1 other file). Files
|
||
* with ≥1 unique kill (🟢 unique / 🟡 overlapping) are NEVER candidates — removing one
|
||
* would drop a mutant's only killer and lower the mutation score.
|
||
*
|
||
* IMPORTANT: 🟠 redundant is only ACCURATE when the reports come from a `disableBail:true`
|
||
* run (killedBy lists EVERY killer). Under the bail-on-first nightly, redundant is
|
||
* understated — see the module caveat. Pass disableBail reports here (mutation-redundancy.yml).
|
||
*
|
||
* @param {object[]} reports parsed mutation.json objects (one per batch)
|
||
* @param {string[]} [allTestFiles] universe; defaults to the union of testFiles keys
|
||
* @returns {{ classification: object, empty: string[], redundant: string[], candidates: string[] }}
|
||
*/
|
||
export function redundancyCandidates(reports, allTestFiles) {
|
||
const classification = aggregateRadiography(reports, allTestFiles);
|
||
const empty = [];
|
||
const redundant = [];
|
||
for (const [file, info] of Object.entries(classification)) {
|
||
if (info.class === "empty") empty.push(file);
|
||
else if (info.class === "redundant") redundant.push(file);
|
||
}
|
||
empty.sort((a, b) => a.localeCompare(b));
|
||
redundant.sort((a, b) => a.localeCompare(b));
|
||
const candidates = [...empty, ...redundant].sort((a, b) => a.localeCompare(b));
|
||
return { classification, empty, redundant, candidates };
|
||
}
|
||
|
||
// ── CLI ──────────────────────────────────────────────────────────────────────
|
||
|
||
function tapTestFilesUniverse() {
|
||
try {
|
||
const conf = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "stryker.conf.json"), "utf8"));
|
||
return conf?.tap?.testFiles || null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const CLASS_LABEL = {
|
||
empty: "🔴 empty",
|
||
redundant: "🟠 redundant",
|
||
overlapping: "🟡 overlapping",
|
||
unique: "🟢 unique",
|
||
};
|
||
const CLASS_ORDER = ["empty", "redundant", "overlapping", "unique"];
|
||
|
||
function renderMarkdown(classification) {
|
||
const byClass = { empty: [], redundant: [], overlapping: [], unique: [] };
|
||
for (const [file, info] of Object.entries(classification))
|
||
byClass[info.class].push({ file, ...info });
|
||
for (const k of CLASS_ORDER) byClass[k].sort((a, b) => a.file.localeCompare(b.file));
|
||
|
||
const total = Object.keys(classification).length;
|
||
const lines = [];
|
||
lines.push("# Mutation Radiography");
|
||
lines.push("");
|
||
lines.push(
|
||
`Test files classified by mutation-kill contribution (\`killedBy\`). Total: **${total}**.`
|
||
);
|
||
lines.push("");
|
||
lines.push("| Class | Count | Meaning |");
|
||
lines.push("| --- | --- | --- |");
|
||
lines.push(
|
||
`| 🔴 empty | ${byClass.empty.length} | kills no mutant of the mutated modules (R1-prune candidate) |`
|
||
);
|
||
lines.push(
|
||
`| 🟠 redundant | ${byClass.redundant.length} | every kill is shared with another file |`
|
||
);
|
||
lines.push(
|
||
`| 🟡 overlapping | ${byClass.overlapping.length} | kills ≥1 unique but mostly shared |`
|
||
);
|
||
lines.push(`| 🟢 unique | ${byClass.unique.length} | kills ≥1 mutant no other file kills |`);
|
||
lines.push("");
|
||
lines.push(
|
||
"> **Bail caveat:** Stryker bails on the first kill (no `disableBail`), so `killedBy` is the " +
|
||
"FIRST killer only. 🔴 empty is reliable (safe R1-prune candidate w.r.t. mutationScore); " +
|
||
"🟢/🟠/🟡 are optimistic (unique overstated, redundant understated) — advisory until a " +
|
||
"`disableBail` run. R1 prunes 🔴 only, with a line-coverage cross-check + human review."
|
||
);
|
||
lines.push("");
|
||
for (const k of CLASS_ORDER) {
|
||
const rows = byClass[k];
|
||
lines.push(`## ${CLASS_LABEL[k]} (${rows.length})`);
|
||
lines.push("");
|
||
if (rows.length === 0) {
|
||
lines.push("_none_");
|
||
} else {
|
||
lines.push("| Test file | unique | shared |");
|
||
lines.push("| --- | --- | --- |");
|
||
for (const r of rows) lines.push(`| ${r.file} | ${r.uniqueKills} | ${r.sharedKills} |`);
|
||
}
|
||
lines.push("");
|
||
}
|
||
return lines.join("\n");
|
||
}
|
||
|
||
const FLAGS = new Set(["--no-conf-universe", "--candidates"]);
|
||
|
||
function renderCandidates({ empty, redundant, candidates }) {
|
||
const lines = [];
|
||
lines.push("# R1 — Test-redundancy prune candidates (disableBail)");
|
||
lines.push("");
|
||
lines.push(
|
||
`Test files with ZERO unique kills: **${candidates.length}** ` +
|
||
`(🔴 empty ${empty.length} + 🟠 redundant ${redundant.length}).`
|
||
);
|
||
lines.push("");
|
||
lines.push(
|
||
"> Accurate ONLY for a `disableBail:true` run (killedBy lists every killer). " +
|
||
"These are CANDIDATES, not deletions: exclude security/contract/repro tests " +
|
||
"(routeGuard, OAuth, error-sanitization, *-repro*/*-regression*/issue-linked) and " +
|
||
"require human review before removing any (R1 human gate)."
|
||
);
|
||
lines.push("");
|
||
lines.push(`## 🔴 empty — kills no mutant (${empty.length})`);
|
||
lines.push("");
|
||
if (empty.length === 0) lines.push("_none_");
|
||
else for (const f of empty) lines.push(`- ${f}`);
|
||
lines.push("");
|
||
lines.push(`## 🟠 redundant — every kill shared with another file (${redundant.length})`);
|
||
lines.push("");
|
||
if (redundant.length === 0) lines.push("_none_");
|
||
else for (const f of redundant) lines.push(`- ${f}`);
|
||
lines.push("");
|
||
return lines.join("\n");
|
||
}
|
||
|
||
function main(argv) {
|
||
const wantCandidates = argv.includes("--candidates");
|
||
const useConfUniverse = !argv.includes("--no-conf-universe");
|
||
const paths = argv.slice(2).filter((a) => !FLAGS.has(a));
|
||
if (paths.length === 0) {
|
||
process.stderr.write(
|
||
"usage: mutation-radiography.mjs <mutation-1.json> [<mutation-2.json> ...] " +
|
||
"[--candidates] [--no-conf-universe]\n"
|
||
);
|
||
process.exit(2);
|
||
}
|
||
const reports = paths.map(loadMutationReport);
|
||
const universe = useConfUniverse ? tapTestFilesUniverse() : null;
|
||
if (wantCandidates) {
|
||
process.stdout.write(
|
||
renderCandidates(redundancyCandidates(reports, universe || undefined)) + "\n"
|
||
);
|
||
return;
|
||
}
|
||
const classification = aggregateRadiography(reports, universe || undefined);
|
||
process.stdout.write(renderMarkdown(classification) + "\n");
|
||
}
|
||
|
||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
main(process.argv);
|
||
}
|