mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
b0ac5998ea
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>
148 lines
5.0 KiB
JavaScript
Executable File
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);
|
|
});
|