From b447287b187da16476ef64b95c69c668675b9417 Mon Sep 17 00:00:00 2001 From: propcgamer20-png Date: Wed, 9 Sep 2026 20:49:41 +0530 Subject: [PATCH] fix: web profiler export records resolved params, not sparse currentOpts profiler-ui.js and profiler-compare.js built provenance.params as Object.assign({}, currentOpts). currentOpts starts {} on every upload and only gains a key when the user edits a threshold input, so for the common case (no threshold touched) the exported params was {} - not the 7 resolved defaults SPEC.md section 10 requires, and which the Python CLI/MCP path already records. Added E.publicParams(opts) to the engine, mirroring faircode.provenance.public_params(faircode.profiler._resolve_opts(opts)): resolveOpts merge, drop the opaque `reference` structure, key-sorted. Both export sites now use it. Closes #490 --- assets/profiler-compare.js | 2 +- assets/profiler-engine.js | 22 ++++++++++++++++++++++ assets/profiler-ui.js | 2 +- tests/test_js_parity.py | 25 +++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/assets/profiler-compare.js b/assets/profiler-compare.js index 52c52d0..7df9bea 100644 --- a/assets/profiler-compare.js +++ b/assets/profiler-compare.js @@ -653,7 +653,7 @@ engine: 'js', dataset_hash_a: hashA.digest, dataset_hash_b: hashB.digest, - params: Object.assign({}, currentOpts), + params: E.publicParams(currentOpts), overrides: Object.assign({}, currentOverrides) }; if (hashA.note !== null) provenance.dataset_hash_a_note = hashA.note; diff --git a/assets/profiler-engine.js b/assets/profiler-engine.js index 5545454..438d4e0 100644 --- a/assets/profiler-engine.js +++ b/assets/profiler-engine.js @@ -69,6 +69,25 @@ validateOpts(o); return o; } + + // Parsed data structures that carry their own provenance field elsewhere - + // echoing them into params would be noise. Mirrors + // faircode.provenance._OPAQUE_PARAMS. + var OPAQUE_PARAMS = { reference: 1 }; + + // The resolved knobs as actually applied (defaults included), minus the + // opaque parsed structures, key-sorted. Mirrors + // faircode.provenance.public_params(faircode.profiler._resolve_opts(opts)), + // so a web-profiler export's provenance.params matches the CLI/MCP path + // even when the user never touched a threshold input (#490). + function publicParams(opts) { + var resolved = resolveOpts(opts); + var out = {}; + Object.keys(resolved).sort().forEach(function (k) { + if (!OPAQUE_PARAMS[k]) out[k] = resolved[k]; + }); + return out; + } // Comparison / drift (SPEC section 8) var PSI_EPSILON = 0.0001; var MISSING_DRIFT_FLAG = 0.05; @@ -1088,6 +1107,9 @@ sniffDelimiter: sniffDelimiter, profile: profile, compare: compare, parseReference: parseReference, + // publicParams: resolved knobs for an export's + // provenance.params, matching the Python path (#490). + publicParams: publicParams, // Exposed so the Profile/Compare threshold-input // placeholders (issue #377) can be sourced from // this single source of truth instead of a diff --git a/assets/profiler-ui.js b/assets/profiler-ui.js index 0952c5d..1d67687 100644 --- a/assets/profiler-ui.js +++ b/assets/profiler-ui.js @@ -752,7 +752,7 @@ faircode_version: FAIRCODE_VERSION, engine: 'js', dataset_hash: hash.digest, - params: Object.assign({}, currentOpts), + params: E.publicParams(currentOpts), overrides: Object.assign({}, currentOverrides) }; diff --git a/tests/test_js_parity.py b/tests/test_js_parity.py index 1c6ad9c..847e88e 100644 --- a/tests/test_js_parity.py +++ b/tests/test_js_parity.py @@ -248,6 +248,31 @@ def test_python_js_na_token_parity_on_literal_na_and_none(tmp_path): assert status["missing_pct"] == 0.2 +def test_python_js_public_params_parity_for_a_defaulted_run(): + """A web-profiler export with no threshold ever touched must still record + the 7 resolved defaults in provenance.params, matching the CLI/MCP path - + E.publicParams({}) mirrors provenance.public_params(_resolve_opts(None)) (#490).""" + from faircode.profiler import _resolve_opts + from faircode.provenance import public_params + + expected = public_params(_resolve_opts(None)) + + script = ( + "require(process.argv[1]);" + "process.stdout.write(JSON.stringify(globalThis.FairCodeProfiler.publicParams({})));" + ) + completed = subprocess.run( + ["node", "-e", script, str(REPO_ROOT / "assets" / "profiler-engine.js")], + capture_output=True, text=True, encoding="utf-8", check=True, + ) + assert json.loads(completed.stdout) == expected + assert set(expected) == { + "cross", "imbalance_flag", "intersection_floor", "min_group_size", + "min_share", "missing_flag", "reference_flag", + } + assert "reference" not in expected + + def test_python_js_profiler_parity_with_overrides_cross_and_thresholds(tmp_path): """Non-default options - --map/--cross/--reference/thresholds - only ever had cross-engine parity coverage for their default-off path (issue #376).