Files
ruvnet__ruflo/scripts/check-fingerprint-schema.mjs
ruv b0ac5998ea test(metaharness): fingerprint-schema compat tripwire (iter 81)
Third compat tripwire in the family. Protects _similarity.mjs from
silent upstream schema drift in `metaharness score` / `metaharness genome`.

THE BUG-CLASS THIS PROTECTS AGAINST

iter 47 discovered the harness/metaharness binary schema mismatch.
The fix routed score+genome through runMetaharness. But:

   Pre-iter-81: nothing asserted that the metaharness schema STAYS
   correct. If upstream renames `harnessFit` to `score` or
   `harness_fit`:
      - projectToVec reads `s.harnessFit` → undefined
      - All 5 score-derived numerics collapse to 0
      - cosine signal degrades to flat-zero
      - similarity returns only categorical+jaccard agreement
      - Drift detection silently breaks (similar verdict on ANY input)

   And no test would fail — the iter-72 unit tests use hand-built
   fixtures that hard-code the correct field names. The iter-47/68
   roundtrip uses real CLI output, but if the CLI changed AND the
   unit fixtures kept using the old names, the roundtrip would
   produce similarity=0.0 → fail Stage 3 (overall===1) BUT only on
   live CI, not on the unit-test layer.

   iter-81 makes the schema invariant explicit at CI-time, BEFORE
   the roundtrip — pinpoints the breakage instead of cascading.

WHAT SHIPPED

1. scripts/check-fingerprint-schema.mjs — runs `metaharness score`
   and `metaharness genome` against ruflo, asserts each of the 14
   fields _similarity.mjs reads is present in the output:

      score:  harnessFit, compileConfidence, taskCoverage, toolSafety,
              memoryUsefulness, estCostPerRunUsd, recommendedMode,
              archetype, template
      genome: repo_type, agent_topology, risk_score, test_confidence,
              publish_readiness

   Tests `value !== undefined` (key exists, regardless of value type)
   so the tripwire fires on RENAMES but tolerates value-shape changes
   (numbers becoming strings, arrays becoming nested objects, etc.).

   Exit 0 — all 14 fields present OR metaharness absent (graceful skip)
   Exit 1 — at least one field missing
   Exit 2 — unexpected error

2. .github/workflows/metaharness-ci.yml — new step in metaharness-
   real-data job runs the tripwire alongside the iter-80 mcp-scan-
   format check. Both run BEFORE the roundtrip so format/schema
   regressions surface FIRST.

3. Smoke step 17z44 anchors all 14 field literals (anti-shrink: a
   future iter that adds a 15th projectToVec input must also add
   the field to this script AND its smoke marker).

COMPAT TRIPWIRE FAMILY (post-iter-81)

   check-metaharness-compat.mjs       (iter 12)
      → @metaharness/router public API
   check-mcp-scan-format.mjs          (iter 80)
      → harness mcp-scan text-output format
   check-fingerprint-schema.mjs       (THIS ITER)
      → metaharness score / genome JSON field names

Each tripwire protects a distinct upstream-dependency surface that
ruflo's code reads. Combined, they form an "upstream contract"
inspection layer — if any upstream surface drifts, ruflo CI fails
with a SPECIFIC tripwire error before cascading to downstream
symptoms.

GATES THIS ITER
   smoke:    83 → 84 (step 17z44 green)
   ci:       new pre-roundtrip step in metaharness-real-data job
   runtime:  14/14 fields verified against metaharness@0.1.14
   tsc:      no TS source changes

DEFENSE-IN-DEPTH OBSERVATION

Each compat tripwire adds:
   - A specific upstream contract under explicit version control
   - A clear failure message that points at WHICH upstream surface drifted
   - A pre-roundtrip CI gate that catches the regression before
     downstream symptoms cascade

Cost: ~5s per tripwire run. Payoff: catching upstream drift at the
specific PR that introduces it instead of weeks later when production
behavior degrades silently.

Co-Authored-By: RuFlo <ruv@ruv.net>
2026-06-16 22:42:19 -04:00

148 lines
5.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
// check-fingerprint-schema — protect _similarity.mjs from silent upstream
// schema drift in `metaharness score` / `metaharness genome`.
//
// iter 47 discovered that `harness score` and `metaharness score` have
// different output schemas — using the wrong one made similarity() return
// 0.8125 on self-roundtrip (should be 1.0). The fix routed score+genome
// through `runMetaharness` to get the correct shape.
//
// But there's still no test that ASSERTS the correct shape stays correct.
// If upstream renames `harnessFit` to `harness_fit` (or anything),
// _similarity.mjs::projectToVec reads `s.harnessFit` → undefined → 0 →
// all numerics collapse to 0 → similarity always returns the categorical
// + jaccard agreement, never the cosine. Drift detection silently breaks.
//
// This script runs the two `metaharness` subcommands and asserts the
// fields _similarity.mjs::projectToVec actually reads.
//
// USAGE
// node scripts/check-fingerprint-schema.mjs # exits 0 if intact
// node scripts/check-fingerprint-schema.mjs --format json # CI-consumable
//
// EXIT CODES
// 0 fingerprint schemas intact (or metaharness not installed)
// 1 at least one expected field is missing
// 2 unexpected error
import { spawnSync } from 'node:child_process';
const ARGS = (() => {
const a = { format: 'table' };
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i] === '--format') a.format = process.argv[++i];
}
return a;
})();
// _similarity.mjs::projectToVec reads these EXACT field names. If
// upstream renames any of them, projectToVec returns 0 for that index
// and the cosine signal degrades silently.
const SCORE_FIELDS = [
'harnessFit', 'compileConfidence', 'taskCoverage', 'toolSafety',
'memoryUsefulness', 'estCostPerRunUsd', 'recommendedMode',
'archetype', 'template',
];
const GENOME_FIELDS = [
'repo_type', 'agent_topology', 'risk_score',
'test_confidence', 'publish_readiness',
];
function emit(payload, code) {
if (ARGS.format === 'json') console.log(JSON.stringify(payload, null, 2));
else {
console.log(`# check-fingerprint-schema\n`);
if (payload.skipped) console.log(`⊘ ${payload.reason}`);
else if (payload.ok) console.log(`✓ All ${payload.results.length} fields intact.`);
else {
console.log(`✗ ${payload.failedCount}/${payload.results.length} fields missing:`);
for (const r of payload.results) {
if (!r.ok) console.log(` - ${r.subcommand}.${r.field} (${r.detail})`);
}
}
}
process.exit(code);
}
function runMeta(subcmd, path) {
const r = spawnSync('npx', [
'-y', 'metaharness@latest', subcmd, path, '--json',
], { encoding: 'utf-8', timeout: 90_000 });
if (r.status === null
|| /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND/i.test(r.stderr || '')) {
return { degraded: true };
}
const m = /\{[\s\S]*\}/.exec(r.stdout || '');
if (!m) return { degraded: false, json: null, raw: r.stdout?.slice(0, 200) };
try { return { degraded: false, json: JSON.parse(m[0]) }; }
catch (e) { return { degraded: false, json: null, error: e.message }; }
}
async function main() {
const path = '.';
// 1. metaharness score
const scoreR = runMeta('score', path);
if (scoreR.degraded) {
emit({
skipped: true,
reason: 'metaharness-not-available — install with `npm i --include=optional` to verify schema',
generatedAt: new Date().toISOString(),
}, 0);
}
if (!scoreR.json) {
emit({
ok: false,
results: [{ subcommand: 'score', field: '*', ok: false, detail: 'no JSON in output' }],
failedCount: 1,
generatedAt: new Date().toISOString(),
}, 1);
}
// 2. metaharness genome
const genomeR = runMeta('genome', path);
if (!genomeR.json) {
emit({
ok: false,
results: [{ subcommand: 'genome', field: '*', ok: false, detail: 'no JSON in output' }],
failedCount: 1,
generatedAt: new Date().toISOString(),
}, 1);
}
// 3. Assert each expected field is present (i.e., key exists, even
// if value is null — we just care upstream didn't rename it).
const results = [];
for (const field of SCORE_FIELDS) {
const present = scoreR.json[field] !== undefined;
results.push({
subcommand: 'score',
field,
ok: present,
detail: present ? `value=${typeof scoreR.json[field]}` : 'MISSING — projectToVec defaults to 0',
});
}
for (const field of GENOME_FIELDS) {
const present = genomeR.json[field] !== undefined;
results.push({
subcommand: 'genome',
field,
ok: present,
detail: present ? `value=${typeof genomeR.json[field]}` : 'MISSING — projectToVec defaults to 0',
});
}
const failed = results.filter((r) => !r.ok);
emit({
ok: failed.length === 0,
results,
failedCount: failed.length,
generatedAt: new Date().toISOString(),
}, failed.length === 0 ? 0 : 1);
}
main().catch((e) => {
console.error('check-fingerprint-schema crashed:', e?.message ?? e);
process.exit(2);
});