From 56e5ae1a6dc3516be5c129a157be6213e4abc0ca Mon Sep 17 00:00:00 2001 From: rUv Date: Mon, 12 Oct 2026 16:50:20 -0700 Subject: [PATCH] Explore RuField and WorldGraph with bounded CSI context experiments Retain the stronger ACF model after an 18-configuration temporal ablation regresses on development data. Add a pinned actual-library integrity probe, strict bounded replay inspection, measured synthetic overhead, and CI tests. Preserve source hashes and separate software integrity from sensing accuracy. --- .github/workflows/csi-encoder-training.yml | 22 + ...420-public-amplitude-model-optimization.md | 22 + docs/espfi-field-context-research.md | 186 ++++++ docs/espfi-model-optimization.md | 15 + docs/validation/espfi-context-20261012.json | 221 +++++++ scripts/csi_training/espfi_context.py | 211 +++++++ scripts/field-context-probe/.gitignore | 1 + scripts/field-context-probe/Cargo.lock | 547 ++++++++++++++++++ scripts/field-context-probe/Cargo.toml | 16 + scripts/field-context-probe/README.md | 94 +++ .../field-context-probe/example-result.json | 56 ++ scripts/field-context-probe/src/main.rs | 378 ++++++++++++ .../verification-receipt.json | 66 +++ scripts/tests/test_espfi_context.py | 130 +++++ 14 files changed, 1965 insertions(+) create mode 100644 docs/espfi-field-context-research.md create mode 100644 docs/validation/espfi-context-20261012.json create mode 100644 scripts/csi_training/espfi_context.py create mode 100644 scripts/field-context-probe/.gitignore create mode 100644 scripts/field-context-probe/Cargo.lock create mode 100644 scripts/field-context-probe/Cargo.toml create mode 100644 scripts/field-context-probe/README.md create mode 100644 scripts/field-context-probe/example-result.json create mode 100644 scripts/field-context-probe/src/main.rs create mode 100644 scripts/field-context-probe/verification-receipt.json create mode 100644 scripts/tests/test_espfi_context.py diff --git a/.github/workflows/csi-encoder-training.yml b/.github/workflows/csi-encoder-training.yml index 5b0b698c9..d4b07f795 100644 --- a/.github/workflows/csi-encoder-training.yml +++ b/.github/workflows/csi-encoder-training.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'scripts/csi_training/**' + - 'scripts/field-context-probe/**' - 'scripts/train-csi-encoder.py' - 'scripts/train-espfi.py' - 'scripts/improve-espfi.py' @@ -39,10 +40,31 @@ jobs: - name: Validate real training code using temporary synthetic data env: PYTHONPATH: scripts + OPENBLAS_NUM_THREADS: '1' + OMP_NUM_THREADS: '1' run: python -m unittest discover -s scripts/tests -p 'test_csi_training*.py' -v - name: Validate public amplitude intake and optimization with synthetic fixtures env: PYTHONPATH: scripts + OPENBLAS_NUM_THREADS: '1' + OMP_NUM_THREADS: '1' run: python -m unittest discover -s scripts/tests -p 'test_espfi*.py' -v - name: Enforce capture data policy run: bash scripts/csi-data-policy-check.sh --tracked + + field-context-contract: + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: scripts/field-context-probe + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - name: Install fixed Rust toolchain + run: rustup toolchain install 1.90.0 --profile minimal + - name: Verify actual field and graph library contracts + run: cargo +1.90.0 test --locked + - name: Exercise synthetic evidence pipeline + run: cargo +1.90.0 run --locked --quiet --bin ruview-field-context-probe diff --git a/docs/adr/ADR-420-public-amplitude-model-optimization.md b/docs/adr/ADR-420-public-amplitude-model-optimization.md index 2395e6b52..2a4a90175 100644 --- a/docs/adr/ADR-420-public-amplitude-model-optimization.md +++ b/docs/adr/ADR-420-public-amplitude-model-optimization.md @@ -76,3 +76,25 @@ original frozen bundle and label this development evidence, not a new test. Only a fresh independent confirmation dataset can authorize a stronger generalization claim. Feature extraction, model latency and artifact integrity must be measured together for the new artifact family. + +## Field context research boundary + +The RuField and WorldGraph exploration uses two explicitly separate paths. +The Python amplitude experiment may add temporal descriptors within a trial; +it cannot invent continuous chronology, verified empty room calibration, +positions or room topology. The standalone Rust integrity probe calls pinned +actual RuField and WorldGraph crates using authored synthetic events. Public +unlabeled replay is inspected for format only and never promoted to labeled +activity evidence or graph beliefs. + +Root sensor evidence and derived graph beliefs remain distinct. The probe +bridge rejects derived input as new independent roots, duplicate event hashes, +stale observations and expired or untraceable beliefs. The bridge is not a +production graph cycle detector or an enrollment authority. Native WorldGraph +permits cycles, and native RuField simulation trust does not enforce wall clock +freshness. Neither behavior proves a production defect when used as documented. + +The frozen temporal extension improved internal training folds but regressed +development accuracy and macro F1. Reject it and preserve the retained motion +ACF model. Evidence, executable commands and future field acceptance are in +[`espfi-field-context-research.md`](../espfi-field-context-research.md). diff --git a/docs/espfi-field-context-research.md b/docs/espfi-field-context-research.md new file mode 100644 index 000000000..55a03db49 --- /dev/null +++ b/docs/espfi-field-context-research.md @@ -0,0 +1,186 @@ +# RuField and WorldGraph sample data experiment + +Date: 2026-10-12. This is a bounded research result, not model promotion. +The existing motion ACF candidate remains the retained model. + +## Decision and evidence boundary + +RuField supplies typed sensor events, trust verification and temporal fusion. +WorldGraph supplies a typed graph of environmental beliefs and their evidence. +They do not supply missing labels, independent room calibration or physical +track positions for an amplitude activity dataset. There is no measured +classification improvement from attaching their names or APIs to a pipeline. + +We therefore separate two executable questions: + +1. Can ordered summaries inside each existing recording improve the frozen + activity model? Measure on the authorized public ESP-Fi bundle. +2. Can actual RuField and WorldGraph libraries maintain source ancestry and + reject duplicate, stale or circular evidence? Test deterministic synthetic + events and characterize the real replay sample boundary separately. + +The first question is a temporal representation ablation inspired by the +context discussion. It does not use graph features or call the Rust libraries. +The second question calls actual pinned libraries, but cannot establish field +accuracy. These outputs must never be combined into an accuracy claim. + +## Sample audit + +ESP-Fi contains labeled individual trials. The prepared input is 52 amplitude +channels over 190 pooled samples. It has no validated wall clock sampling rate, +continuous sequence across trials, independently annotated empty room interval, +sensor position or room geometry. Numeric activity names remain unresolved. +The existing 837 training and 70 development recordings are reused; final test +arrays are never opened. Sorting trial filenames is not evidence of chronology. + +At [RuField revision 99556728](https://github.com/ruvnet/rufield/tree/995567283606b86594439d6594387ff551a21ede), +the public replay fixtures have the following structural properties, verified +without exporting signal values: + +| Fixture | Complete JSON frames | Time span | Structural status | +| --- | ---: | ---: | --- | +| `real_csi_small.jsonl` | 3 | approximately 0.201 s | increasing timestamps | +| `real_csi_medium.jsonl` | 199 | approximately 13.415 s | increasing complete frames, truncated JSON at line 200 | + +Git blob hashes are `fb2b2aa1ccd65306e4eccf5b30309c071b6da3ea` and +`81b8097a54940319ff4b6df290c34628d4418772`, respectively. The source path is +`crates/rufield-adapters/tests/fixtures/`. The [adapter](https://github.com/ruvnet/rufield/blob/995567283606b86594439d6594387ff551a21ede/crates/rufield-adapters/src/csi_replay.rs) +and [replay tests](https://github.com/ruvnet/rufield/blob/995567283606b86594439d6594387ff551a21ede/crates/rufield-adapters/tests/csi_replay_fusion.rs) +describe unlabeled replay. Its first frames are a statistical reference, not +independently verified emptiness. The code license is MIT; no separate dataset +permission or consent record was found in the inspected sources. These samples +are not imported into model training or committed here. + +The [RuView BatVu fixture test](https://github.com/ruvnet/RuView/blob/5cfbb8d98d561bddb7c8918ae431407267517016/v2/crates/wifi-densepose-rufield/tests/ultrasonic_gates.rs) +identifies its 72 pings as simulator output. It cannot provide an independent +physical sensing confirmation set. The inspected WorldGraph sources likewise +did not supply independently annotated continuous sensing data for this task. + +## Frozen model experiment + +The current ACF representation is invariant under reversing a recording. This +is an exact feature property, not proof that direction loss explains a class +failure. Test three families: unchanged ACF with 468 features; ACF plus three +ordered per-carrier means with 624; and those features plus a signed linear +trend with 676. The three temporal bins have 63, 64 and 63 samples. Reversal +swaps the first and last bin and negates the trend. All log amplitude centering +and global RMS arithmetic remain float64. + +Each family uses gamma multipliers 0.1 and 1, ridge penalties 0.01, 0.1 and 1, +128 RFF dimensions and seed 42. The 18 candidates were fixed before the run. +Selection maximizes the minimum of the two original training fold macro F1 +scores, then mean score, then smaller state, with declaration order for ties. +Scalers and models fit only their respective training rows. No room label is an +input feature. The complete selection is saved before development extraction. + +The unchanged ACF control must reproduce every learned tensor exactly. The +retention gate requires development macro F1 at least 0.03 above the retained +ACF candidate, no accuracy regression, no newly zero class F1, and measured p95 +under 1 ms. Repeatedly viewed development data remains adaptive research data. + +MEASURED: `ordered_trend`, gamma 0.1, alpha 0.1 wins the training selection. + +| Metric | Retained ACF | Ordered candidate | +| --- | ---: | ---: | +| Training fold 1 macro F1 | 0.332115 | 0.347404 | +| Training fold 2 macro F1 | 0.335944 | 0.364028 | +| Development correct | 39/70 | 34/70 | +| Development accuracy | 55.71% | 48.57% | +| Development macro F1 | 0.499871 | 0.437458 | +| Explicit float64 artifact bytes | 496488 | 712816 | + +The candidate is rejected. Activity 3 still has zero F1; its confusion row is +unchanged. Activity 1 improves only from one to two correct predictions, while +other classes regress. Better internal fold scores did not transfer to this +development room and participant. No second accuracy search follows this result. + +Exact baseline reconstruction and exact exported reload pass. The paired +1000-call run observed candidate median/p95 0.782435/4.814087 ms versus +0.512245/0.861634 ms for the ACF control. An independent Rust compilation was +running on the shared machine, so this timing is recorded as contended execution, +not a stable runtime regression estimate. The accuracy failures alone reject +the candidate. Acquisition, pooling and I/O are excluded from these timings. + +The [aggregate receipt](validation/espfi-context-20261012.json) binds source, +model, baseline and manifest hashes. Full experiment records remain with the +executor; no individual input recordings or model weights enter the repository. + +```sh +OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 PYTHONPATH=scripts \ +python -m csi_training.espfi_context \ + --manifest /private/espfi/prepared/manifest.json \ + --baseline /private/espfi/motion-002-worst \ + --output /private/espfi/context-001 +``` + +## Actual library integration + +[`scripts/field-context-probe`](../scripts/field-context-probe/README.md) is a +standalone Rust executable with exact Git dependency revisions and a lockfile. +It creates signed synthetic RuField events, runs actual RuField trust and +fusion, creates actual WorldGraph semantic beliefs with `DerivedFrom` edges, +and serializes and restores the graph. + +The additional bridge admits only fresh unique root evidence. It refuses derived +events as new roots and refuses missing source handles, expired beliefs and +duplicate belief publication. These are deliberately conservative application +rules; they are not native WorldGraph cycle detection. WorldGraph permits a +self edge, which is demonstrated in a separate boundary test. RuField simulation +trust intentionally skips wall clock freshness, whereas its production policy +rejects synthetic evidence. No synthetic key is enrolled for production use. + +The bridge never turns a model score into a calibrated probability. No person +track, physical position or room adjacency is fabricated. In this closed +synthetic probe, every declared derived input is rejected as independent root +evidence. The caller supplies that ancestry declaration; it is not a complete +cryptographic proof of independence. A production bridge would need verified +lineage, including correlation between observations from the same original +signal, before claiming to prevent all circular confidence amplification. + +The optional replay inspector uses a bounded regular UTF-8 file, a 1 MiB input +limit and at most 10,000 nonempty lines. It validates every required field, +numeric conversion, increasing timestamp and consistent channel width before +calling the native adapter. The parsed frame count must match the admitted line +count. It does not silently recover truncated or schema-invalid tails. This is +a stricter research intake policy than the native replay parser's documented +tail recovery behavior. No input capture is calibrated or inserted into a graph. + +The official small sample is accepted as three frames. The medium sample is +rejected at malformed line 200 with exit status 2; the native adapter alone +accepts its complete prefix. Synthetic regressions also cover a valid JSON but +invalid final schema and numeric overflow. These checks establish completeness +and bounded parsing, not labels or physical truth. + +The release benchmark compares actual RuField alone against the bridge with +actual WorldGraph in 1,000 alternating pairs after 100 warmups. Both paths create +a fresh isolated engine, parse the rule, verify a signature, ingest and infer. +The bridge adds its guards, graph creation and ancestry publication. The exact +timings and source hashes are in the probe's `example-result.json` and +`verification-receipt.json`. Final added p95 is 0.038340 ms on synthetic +fixtures. They exclude signing, acquisition, calibration, +classification and persistent streaming costs, and do not establish target +device performance. Benchmark improvement is not model improvement. + +All 44 public pipeline tests and 28 original encoder tests pass on ruOS. The +first isolated original-suite attempt lacked an unchanged collector dependency; +copying that exact existing script restored all 28 passes. Python source review +found no blocking issue. Rust review exposed tail completeness and numeric +conversion gaps in the new inspector; explicit validation and regression tests +address those gaps. All six Rust tests and the release build pass; an independent +review found no remaining blocker. The primary agent reran the six tests and +verified the tested source hashes against the committed receipt. CI includes both +Python suites and the standalone locked Rust probe. + +## Next acceptance + +Keep the current ACF model. A useful deployment experiment needs independently +annotated continuous captures with timestamps, permitted data use, verified +empty intervals, sensor placement, room connectivity and activity boundaries. +Compare the same streams with classifier alone, RuField validation, and RuField +plus WorldGraph context. Proposed acceptance is 20% fewer false alerts at the +same recall, plus less than 0.1 ms incremental p95 processing overhead on the +target device. Measure event detection delay separately from compute time. + +Those are future acceptance targets, not results of the isolated clips or the +synthetic integrity probe. No production model, published Hugging Face weights, +live sensor configuration or canonical learned memory is promoted by this work. diff --git a/docs/espfi-model-optimization.md b/docs/espfi-model-optimization.md index fc8c33cf3..5c152bb8c 100644 --- a/docs/espfi-model-optimization.md +++ b/docs/espfi-model-optimization.md @@ -464,3 +464,18 @@ explicit float64 artifact is larger. One class remains at zero F1 and two classes regress. The optimized compact incumbent remains available. The audit contains source revisions, all retention checks, full limitations and commands to reproduce both policies without opening final test arrays. + +## RuField and WorldGraph context experiment + +The [field context research report](espfi-field-context-research.md) separates +an actual Rust library integrity probe from a Python temporal model ablation. +The frozen 18-candidate experiment adds ordered means and signed trends within +individual recordings. Its selected candidate improves both training folds but +regresses development accuracy to 34/70 and macro F1 to 0.437458. It is rejected; +the retained motion ACF model stays at 39/70 and 0.499871 on these reused data. + +Actual RuField and WorldGraph tests verify source ancestry and conservative +rejection policies. Public unlabeled replay is inspected for format only, with +no assumed empty room, activity labels or fabricated positions. Software +integrity and synthetic timing are not predictive accuracy evidence. The report +and aggregate receipts preserve that distinction and the negative model result. diff --git a/docs/validation/espfi-context-20261012.json b/docs/validation/espfi-context-20261012.json new file mode 100644 index 000000000..06620e665 --- /dev/null +++ b/docs/validation/espfi-context-20261012.json @@ -0,0 +1,221 @@ +{ + "status": "complete", + "promotionAuthorized": false, + "task": "public_context_ablation", + "evidence": "MEASURED", + "development": { + "accuracy": 0.4857142857142857, + "macro_f1": 0.4374577417173766, + "per_class_f1": [ + 0.3333333333333333, + 0.6896551724137931, + 0, + 0.6666666666666666, + 0.7058823529411765, + 0.5128205128205128, + 0.15384615384615385 + ], + "confusion_matrix": [ + [ + 2, + 8, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 10, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 1, + 0, + 0, + 1, + 6, + 2 + ], + [ + 0, + 0, + 2, + 5, + 0, + 3, + 0 + ], + [ + 0, + 0, + 0, + 0, + 6, + 4, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 10, + 0 + ], + [ + 0, + 0, + 3, + 0, + 0, + 6, + 1 + ] + ], + "samples": 70 + }, + "incumbent_development": { + "accuracy": 0.5571428571428572, + "macro_f1": 0.4998706443528175, + "per_class_f1": [ + 0.16666666666666666, + 0.6451612903225806, + 0, + 0.8421052631578947, + 0.5333333333333333, + 0.6451612903225806, + 0.6666666666666666 + ], + "confusion_matrix": [ + [ + 1, + 9, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 10, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 1, + 0, + 0, + 1, + 6, + 2 + ], + [ + 0, + 1, + 1, + 8, + 0, + 0, + 0 + ], + [ + 0, + 0, + 2, + 0, + 4, + 4, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 10, + 0 + ], + [ + 1, + 0, + 1, + 1, + 0, + 1, + 6 + ] + ], + "samples": 70 + }, + "baseline_state_exact": true, + "reload_max_abs_logit_error": 0, + "retention_checks": { + "macro_f1_gain": false, + "accuracy_nonregression": false, + "protect_each_previously_nonzero_class": true, + "p95_under_1ms": false + }, + "retained_as_research_candidate": false, + "latency": { + "candidate": { + "p50_ms": 0.782435, + "p95_ms": 4.814087499999998 + }, + "incumbent": { + "p50_ms": 0.5122450000000001, + "p95_ms": 0.8616344999999994 + } + }, + "model_bytes": 712816, + "model_sha256": "589a04de997594a7e83dbb487e40509f038b4df21c76c81151c974fc5af8d317", + "baseline_model_sha256": "8d3c8b144663808159159f7af9d14b09f54ee78dc1f852c643fbd001a23d2fc5", + "baseline_receipt_sha256": "f8a565cb031d3ce9b488432dfb97408a58cee8f511c0ddc89aa74edb90388b88", + "source_sha256": { + "espfi_context.py": "736f3e68ef23298cbcd14f41343557f96698221596eeeda3909bf5ac870a3f5c", + "espfi_motion.py": "c6a8529ccf9d9f834383a2bdf5b14626764ec7ca2ade7ad5d4c1c14797d76054", + "espfi_generalize.py": "956b3a5e918f7c263a2b9183b4a6a70c8f2157604262a4ec6cee2c324a69224f", + "espfi_kernel.py": "ae837cb2e3fac7f052b4bd371cd496e9636e5d5dbaf3a341526a4fc5aef23478" + }, + "elapsed_seconds": 6.865275681000185, + "scope": "18 configurations selected by original training folds; adaptive research on reused development data. 1000 paired batch1 calls include feature extraction/checks/classifier, exclude acquisition/pooling/IO. No cross-recording chronology, empty-room calibration, actual graph feature, final test or field accuracy.", + "selected": { + "candidate": { + "family": "ordered_trend", + "kind": "rff", + "dimensions": 128, + "gamma_multiplier": 0.1, + "alpha": 0.1 + }, + "minimum_cv_macro_f1": 0.3474042415498739, + "mean_cv_macro_f1": 0.35571616304408127, + "scalar_count": 89039 + }, + "fold_f1": [ + 0.3474042415498739, + 0.3640280845382886 + ], + "manifest_sha256": "74e0fe039acbed1b287bd11ee6b779becbb8f9b66c3c249d260ed66f5583f02a", + "full_receipt_sha256": "d84edfd5273edd22d23aead1d2f0500015c1e1e9b744d6923c81d216001cf50b", + "validation": { + "public_tests": 44, + "original_tests": 28, + "independent_review": "NO BLOCK", + "original_suite_setup": "First isolated run lacked unchanged collector script; exact dependency restored and 28 tests rerun successfully", + "final_test_opened": false + }, + "latency_caveat": "Shared CPU timing overlaps independent Rust dependency compilation; not a stable runtime regression estimate. Accuracy and macro F1 alone reject this candidate." +} diff --git a/scripts/csi_training/espfi_context.py b/scripts/csi_training/espfi_context.py new file mode 100644 index 000000000..743b2bfe0 --- /dev/null +++ b/scripts/csi_training/espfi_context.py @@ -0,0 +1,211 @@ +"""Bounded within-recording order ablation; not inferred room context. + +Only source training folds select the candidate. Each recording is independent: +no artificial timeline, empty-room baseline, graph belief or target label enters +the features. The separate Rust probe exercises actual field/graph integration. +""" +import argparse +import hashlib +import json +from pathlib import Path +import time + +import numpy as np +from safetensors.numpy import load, save_file + +from .espfi_generalize import amplitudes +from .espfi_kernel import fit, predict_features +from .espfi_motion import MotionBundle, _read, _validate, motion_features +from .espfi_train import digest, scores, write_json + +WIDTHS = {'motion_acf': 468, 'ordered_mean': 624, 'ordered_trend': 676} +GRID = [dict(family=family, kind='rff', dimensions=128, gamma_multiplier=gamma, alpha=alpha) + for family in WIDTHS for gamma in (.1, 1.) for alpha in (.01, .1, 1.)] +BASELINE = dict(family='motion_acf', kind='rff', dimensions=128, gamma_multiplier=.1, alpha=.1) +CONTRACT = { + 'seed': 42, 'grid': GRID, 'input': 'nonnegative_amplitude_N52T190', + 'selection': 'minimum training-fold macro F1, then mean, then fewer scalars, then declaration order', + 'ordered_bins': [[0, 63], [63, 127], [127, 190]], + 'trend': 'OLS slope on linspace(-1,1,190) of per-carrier centered log amplitude divided by global RMS', + 'log_floor': 1e-6, 'rms_floor': 1e-6, + 'context': 'within one recording only; no chronology, physical direction or room calibration inferred', + 'retention_gate': {'development_macro_f1_absolute_gain': .03, + 'accuracy_nonregression': True, + 'protect_each_previously_nonzero_class': True, + 'paired_p95_limit_ms': 1.0}, + 'promotionAuthorized': False, +} + + +def context_features(values, family): + if family not in WIDTHS: + raise ValueError('Unknown context feature family') + x = amplitudes(values) + original = motion_features(x, 'motion_acf') + if family == 'motion_acf': + return original + logged = np.log(np.maximum(x, CONTRACT['log_floor'])) + relative = logged - logged.mean(-1, keepdims=True) + relative /= np.maximum(np.sqrt(np.mean(relative**2, axis=(1, 2), keepdims=True)), + CONTRACT['rms_floor']) + ordered = np.stack([relative[..., a:b].mean(-1) for a, b in CONTRACT['ordered_bins']], -1) + chunks = [original, ordered.reshape(len(x), -1)] + if family == 'ordered_trend': + time_axis = np.linspace(-1., 1., 190) + chunks.append((relative * time_axis).sum(-1) / np.dot(time_axis, time_axis)) + result = np.ascontiguousarray(np.concatenate(chunks, axis=1)) + if result.shape != (len(x), WIDTHS[family]) or not np.isfinite(result).all(): + raise ValueError('Invalid context feature values') + return result + + +class ContextBundle: + def __init__(self, directory): + root = Path(directory) + status = json.loads(_read(root / 'status.json', 65536)) + report = json.loads(_read(root / 'metrics.json', 16 * 1024**2)) + if (not isinstance(status, dict) or not isinstance(report, dict) + or status.get('status') != 'complete' or report.get('status') != 'complete' + or status.get('promotionAuthorized') is not False + or report.get('promotionAuthorized') is not False + or report.get('contract') != CONTRACT + or report.get('task') != 'public_context_ablation'): + raise ValueError('Invalid completed context artifact') + self.candidate = report['selected']['candidate'] + if self.candidate not in GRID: + raise ValueError('Unknown context candidate') + data = _read(root / 'model.safetensors', 1024**2) + if hashlib.sha256(data).hexdigest() != report.get('model_sha256'): + raise ValueError('Model hash mismatch') + self.state = load(data) + width = WIDTHS[self.candidate['family']] + shapes = {'feature_mean': (width,), 'feature_scale': (width,), + 'projection': (width, 128), 'phase': (128,), 'output_mean': (128,), + 'weights': (128, 7), 'bias': (7,)} + if (set(self.state) != set(shapes) or any(self.state[k].shape != shape + or self.state[k].dtype != np.float64 or not np.isfinite(self.state[k]).all() + for k, shape in shapes.items()) or not (self.state['feature_scale'] > 0).all()): + raise ValueError('Invalid context model state') + + def predict(self, values): + return predict_features(self.state, context_features(values, self.candidate['family'])) + + +def search(parts, provenance, baseline_directory, output, max_seconds=600): + if not 1 <= max_seconds <= 3600: + raise ValueError('Invalid time budget') + fold_ids = _validate(parts) + baseline = MotionBundle(baseline_directory) + prior_bytes = _read(Path(baseline_directory) / 'metrics.json', 16 * 1024**2) + prior = json.loads(prior_bytes) + if (baseline.candidate != BASELINE or not provenance.get('manifest_sha256') + or prior.get('provenance', {}).get('manifest_sha256') != provenance['manifest_sha256']): + raise ValueError('Baseline model or data provenance mismatch') + root = Path(output) + root.mkdir(exist_ok=False, parents=True) + write_json(root / 'status.json', {'status': 'running', 'promotionAuthorized': False}) + write_json(root / 'search_contract.json', CONTRACT) + start = time.monotonic() + try: + train, dev = parts['train'], parts['validation'] + cache = {family: context_features(train['x'], family) for family in WIDTHS} + results, selected, best = [], None, None + for candidate in GRID: + evaluations = [] + for fit_ids, eval_ids in fold_ids: + if time.monotonic() - start > max_seconds: + raise TimeoutError('Context search budget exceeded') + state = fit(cache[candidate['family']][fit_ids], train['y'][fit_ids], candidate) + result = predict_features(state, cache[candidate['family']][eval_ids]) + evaluations.append(scores(train['y'][eval_ids], result.argmax(1), 7)) + row = {'candidate': candidate, 'folds': evaluations, + 'minimum_cv_macro_f1': min(v['macro_f1'] for v in evaluations), + 'mean_cv_macro_f1': float(np.mean([v['macro_f1'] for v in evaluations])), + 'scalar_count': sum(v.size for v in state.values())} + results.append(row) + rank = (row['minimum_cv_macro_f1'], row['mean_cv_macro_f1'], -row['scalar_count']) + if best is None or rank > best: + best, selected = rank, row + # Persist the complete train-only choice before any development feature extraction. + write_json(root / 'selection.json', {'selected': selected, 'candidates': results, 'contract': CONTRACT}) + chosen = selected['candidate'] + state = fit(cache[chosen['family']], train['y'], chosen) + # Require the unchanged control to reproduce the exact existing learned state. + control = fit(cache['motion_acf'], train['y'], BASELINE) + if any(not np.array_equal(control[k], baseline.state[k]) for k in control): + raise ValueError('Unchanged baseline state did not reproduce') + save_file(state, str(root / 'model.safetensors')) + actual = predict_features(state, context_features(dev['x'], chosen['family'])) + incumbent = scores(dev['y'], baseline.predict(dev['x']).argmax(1), 7) + measured = scores(dev['y'], actual.argmax(1), 7) + replay = load(_read(root / 'model.safetensors', 1024**2)) + if not np.array_equal(actual, predict_features(replay, context_features(dev['x'], chosen['family']))): + raise ValueError('Reload changes logits') + def candidate_predict(x): + return predict_features(state, context_features(x, chosen['family'])) + for i in range(20): + x = dev['x'][i % len(dev['x']):i % len(dev['x']) + 1] + candidate_predict(x) + baseline.predict(x) + timings = {'candidate': [], 'incumbent': []} + for i in range(1000): + x = dev['x'][i % len(dev['x']):i % len(dev['x']) + 1] + calls = [('candidate', candidate_predict), ('incumbent', baseline.predict)] + if i % 2: + calls.reverse() + for name, fn in calls: + before = time.perf_counter_ns() + fn(x) + timings[name].append((time.perf_counter_ns() - before) / 1e6) + timing = {k: {'p50_ms': float(np.percentile(v, 50)), 'p95_ms': float(np.percentile(v, 95))} + for k, v in timings.items()} + gate = CONTRACT['retention_gate'] + checks = { + 'macro_f1_gain': measured['macro_f1'] >= incumbent['macro_f1'] + gate['development_macro_f1_absolute_gain'], + 'accuracy_nonregression': measured['accuracy'] >= incumbent['accuracy'], + 'protect_each_previously_nonzero_class': all(b == 0 or a > 0 for a, b in + zip(measured['per_class_f1'], incumbent['per_class_f1'])), + 'p95_under_1ms': timing['candidate']['p95_ms'] < gate['paired_p95_limit_ms'], + } + report = { + 'status': 'complete', 'promotionAuthorized': False, 'task': 'public_context_ablation', + 'contract': CONTRACT, 'selected': selected, 'candidates': results, + 'baseline_control': next(r for r in results if r['candidate'] == BASELINE), + 'baseline_state_exact': True, 'reload_max_abs_logit_error': 0.0, + 'evidence': 'SYNTHETIC' if provenance.get('evidence') == 'SYNTHETIC' else 'MEASURED', + 'provenance': provenance, 'development': measured, 'incumbent_development': incumbent, + 'retention_checks': checks, 'retained_as_research_candidate': all(checks.values()), + 'latency': timing, 'model_bytes': (root / 'model.safetensors').stat().st_size, + 'model_sha256': digest(root / 'model.safetensors'), + 'baseline_model_sha256': prior['model_sha256'], + 'baseline_receipt_sha256': hashlib.sha256(prior_bytes).hexdigest(), + 'source_sha256': {name: digest(Path(__file__).with_name(name)) for name in + ('espfi_context.py', 'espfi_motion.py', 'espfi_generalize.py', 'espfi_kernel.py')}, + 'elapsed_seconds': time.monotonic() - start, + 'scope': '18 configurations selected by original training folds; adaptive research on reused development data. ' + '1000 paired batch1 calls include feature extraction/checks/classifier, exclude acquisition/pooling/IO. ' + 'No cross-recording chronology, empty-room calibration, actual graph feature, final test or field accuracy.', + } + write_json(root / 'metrics.json', report) + write_json(root / 'status.json', {'status': 'complete', 'promotionAuthorized': False}) + return report + except Exception: + write_json(root / 'status.json', {'status': 'failed', 'promotionAuthorized': False}) + raise + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ('manifest', 'baseline', 'output'): + parser.add_argument('--' + name, required=True) + parser.add_argument('--max-seconds', type=int, default=600) + args = parser.parse_args() + from .espfi import load_development + parts, provenance = load_development(args.manifest) + result = search(parts, provenance, args.baseline, args.output, args.max_seconds) + print(json.dumps({k: result[k] for k in ('selected', 'development', 'incumbent_development', + 'retention_checks', 'retained_as_research_candidate', 'latency', 'model_bytes')}, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/scripts/field-context-probe/.gitignore b/scripts/field-context-probe/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/scripts/field-context-probe/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/scripts/field-context-probe/Cargo.lock b/scripts/field-context-probe/Cargo.lock new file mode 100644 index 000000000..b74bbee23 --- /dev/null +++ b/scripts/field-context-probe/Cargo.lock @@ -0,0 +1,547 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rufield-adapters" +version = "0.1.0" +source = "git+https://github.com/ruvnet/rufield?rev=b74bc035e4a3d4023665aa5479b41e938facd113#b74bc035e4a3d4023665aa5479b41e938facd113" +dependencies = [ + "rufield-core", + "rufield-provenance", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "rufield-core" +version = "0.1.0" +source = "git+https://github.com/ruvnet/rufield?rev=b74bc035e4a3d4023665aa5479b41e938facd113#b74bc035e4a3d4023665aa5479b41e938facd113" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "rufield-fusion" +version = "0.1.0" +source = "git+https://github.com/ruvnet/rufield?rev=b74bc035e4a3d4023665aa5479b41e938facd113#b74bc035e4a3d4023665aa5479b41e938facd113" +dependencies = [ + "rufield-core", + "rufield-provenance", + "serde", + "toml", +] + +[[package]] +name = "rufield-provenance" +version = "0.1.0" +source = "git+https://github.com/ruvnet/rufield?rev=b74bc035e4a3d4023665aa5479b41e938facd113#b74bc035e4a3d4023665aa5479b41e938facd113" +dependencies = [ + "ed25519-dalek", + "rufield-core", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ruview-field-context-probe" +version = "0.1.0" +dependencies = [ + "rufield-adapters", + "rufield-core", + "rufield-fusion", + "rufield-provenance", + "serde_json", + "wifi-densepose-worldgraph", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wifi-densepose-geo" +version = "0.1.0" +source = "git+https://github.com/ruvnet/worldgraph?rev=9b1c79c836cdacfb7b44f058c593157bac4c1dab#9b1c79c836cdacfb7b44f058c593157bac4c1dab" +dependencies = [ + "serde", +] + +[[package]] +name = "wifi-densepose-worldgraph" +version = "0.3.2" +source = "git+https://github.com/ruvnet/worldgraph?rev=9b1c79c836cdacfb7b44f058c593157bac4c1dab#9b1c79c836cdacfb7b44f058c593157bac4c1dab" +dependencies = [ + "petgraph", + "serde", + "serde_json", + "thiserror", + "wifi-densepose-geo", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/scripts/field-context-probe/Cargo.toml b/scripts/field-context-probe/Cargo.toml new file mode 100644 index 000000000..78756f139 --- /dev/null +++ b/scripts/field-context-probe/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ruview-field-context-probe" +version = "0.1.0" +edition = "2021" +rust-version = "1.90" +publish = false + +[workspace] + +[dependencies] +rufield-core = { git = "https://github.com/ruvnet/rufield", rev = "b74bc035e4a3d4023665aa5479b41e938facd113" } +rufield-provenance = { git = "https://github.com/ruvnet/rufield", rev = "b74bc035e4a3d4023665aa5479b41e938facd113" } +rufield-fusion = { git = "https://github.com/ruvnet/rufield", rev = "b74bc035e4a3d4023665aa5479b41e938facd113" } +wifi-densepose-worldgraph = { git = "https://github.com/ruvnet/worldgraph", rev = "9b1c79c836cdacfb7b44f058c593157bac4c1dab" } +serde_json = "1.0" +rufield-adapters = { git = "https://github.com/ruvnet/rufield", rev = "b74bc035e4a3d4023665aa5479b41e938facd113" } diff --git a/scripts/field-context-probe/README.md b/scripts/field-context-probe/README.md new file mode 100644 index 000000000..7c72bd6a5 --- /dev/null +++ b/scripts/field-context-probe/README.md @@ -0,0 +1,94 @@ +# RuField to WorldGraph integrity probe + +This standalone Rust executable calls the actual pinned `rufield-core`, +`rufield-provenance`, `rufield-fusion`, and `wifi-densepose-worldgraph` crates. +It demonstrates evidence handling with deterministic **SYNTHETIC** fixtures. +It does not train an activity classifier or establish sensing accuracy. + +## Run + +Requires Rust 1.90.0 or newer. Dependencies are pinned to full Git revisions in +`Cargo.toml`; `Cargo.lock` locks their resolved transitive dependencies. + +```sh +cd scripts/field-context-probe +cargo test --locked +cargo run --locked --quiet +cargo run --release --locked --quiet -- --benchmark +# Optional bounded format-only inspection, no calibration or fusion: +cargo run --release --locked --quiet -- --inspect-jsonl /path/to/capture.csi.jsonl +``` + +No network, file writes, hardware access, person tracking, or external release +occurs when the built executable runs. Initial Cargo dependency resolution needs +network access. The public fixed signing seed is exclusively a test fixture. + +## Actual pipeline + +1. Construct a signed RuField `FieldEvent` with `synthetic: true`. Its clock, + amplitude vector and presence feature are explicitly authored test values. +2. Verify its signature with `rufield_provenance::verify_event`, then ingest it + through the actual `RuFieldFusion` simulation trust policy and a tiny explicit + presence rule. The output score is a heuristic, not a calibrated probability. +3. A small bridge checks fresh, unique root evidence and inference expiry before + calling `WorldGraph::add_semantic_state`. Original event IDs become mandatory + provenance handles and `DerivedFrom` edges to concrete event nodes. +4. Serialize and restore the actual WorldGraph. Assert preserved node counts, + edge counts and ancestry. + +The bridge has a fixed capacity of 32 root events. The synthetic bridge accepts no external input. The separate optional replay inspector reads a regular UTF-8 file of at most 1 MiB and 10,000 nonempty lines. It calls actual `CsiReplayAdapter` parsing, but never calibration, event generation or graph insertion. Every malformed JSON line is rejected, including a truncated tail which the native adapter deliberately tolerates. Parsed frame count must also match nonempty line count, preventing schema-invalid final objects from being silently discarded. Rejection exits with status 2; accepted format exits with status 0. Format acceptance establishes no labels, empty-room baseline or sensing accuracy. +The executable prints a JSON receipt; all rejected fixtures leave the WorldGraph +unchanged. Nothing is automatically promoted. + +## Native guarantees versus bridge policy + +| Check | Responsible component | +| --- | --- | +| Exact event replay and nonmonotonic timestamp rejection | Actual RuField trust verifier, including simulation mode | +| Rejection of synthetic input under production policy | Actual RuField production verifier | +| Signature corruption | Actual RuField signature verification invoked explicitly by bridge | +| Wall clock freshness for this simulation | Bridge; native simulation deliberately skips wall clock freshness | +| Repeated content hash under a different event ID | Bridge | +| Derived evidence feeding back as independent root evidence | Bridge conservatively rejects every nonempty ancestry declaration | +| Expired inference, missing ancestry, duplicate belief | Bridge | +| Typed belief and source ancestry persistence | Actual WorldGraph | +| General cycle prevention | Not supplied by native WorldGraph; a separate boundary test demonstrates that native self edges are accepted | + +This is deliberately conservative. It is not a complete general purpose graph +cycle detector or a production authorization service. Production use requires +independently enrolled sensor keys, appropriate privacy policy, fresh calibrated +measurements and application specific lineage rules. + +## Why no ESP-Fi samples enter this probe + +The available activity recordings do not establish calibrated empty room +baselines, synchronized wall clock times, persistent physical tracks, or complete +world topology. Fabricating those fields would create false evidence. The +synthetic probe therefore validates software integrity only. The independent +activity training harness continues to evaluate its own actual dataset. + +## Pinned source + +* RuField: `b74bc035e4a3d4023665aa5479b41e938facd113` +* WorldGraph: `9b1c79c836cdacfb7b44f058c593157bac4c1dab` + +Acceptance: all six tests pass; the receipt reports synthetic evidence, +uncalibrated confidence, complete root ancestry, rejected duplicate/stale/derived +fixtures, successful graph roundtrip and `promotionAuthorized: false`. + +## Measured overhead + +The release benchmark uses 100 warmups and 1,000 alternating pairs on the same +signed synthetic fixture. Both paths construct a fresh engine, parse the rule, +verify the signature, clone the fixture, ingest and infer. The bridge additionally +checks its guards, constructs WorldGraph and publishes ancestry. Signing, sensing, +training, calibration and persistent-stream behavior are outside the measurement. +The JSON receipt reports both paths and the distribution of paired differences; +its target is added p95 below 0.1 ms. This is a software integrity cost measurement, +not predictive improvement or a production throughput guarantee. + +The optional recording inspector also applies stricter policy than the native +adapter: required numeric timestamp and amplitude array, finite nonnegative +values, nanoseconds below u64 maximum, amplitudes within f32 range, 1–2,048 +channels, fixed channel width and strictly increasing integer nanosecond times. +These format/representation guards do not certify capture provenance or labels. diff --git a/scripts/field-context-probe/example-result.json b/scripts/field-context-probe/example-result.json new file mode 100644 index 000000000..1a5d4d5b3 --- /dev/null +++ b/scripts/field-context-probe/example-result.json @@ -0,0 +1,56 @@ +{ + "benchmark": { + "bridge_ms": { + "p50": 0.08661999999999999, + "p95": 0.12714 + }, + "evidence": "MEASURED software timing on SYNTHETIC fixtures", + "extra_p95_target_met": true, + "extra_p95_target_ms": 0.1, + "iterations": 1000, + "native_ms": { + "p50": 0.08199000000000001, + "p95": 0.122819 + }, + "paired_extra_ms": { + "p50": 0.00452000000000001, + "p95": 0.03833999999999997 + }, + "profile": "release required for reported comparison", + "scope": "Fresh isolated engine per call: signature verify, fixture clone, rule parsing, ingestion and inference; bridge adds guards, WorldGraph creation and ancestry publication. Excludes signing, sensing, training and persistent-stream costs.", + "warmup": 100 + }, + "bridge": { + "ancestryEdges": 1, + "derivedCircularRejected": true, + "duplicateBeliefRejected": true, + "duplicateRejected": true, + "edges": 1, + "expiredBeliefRejected": true, + "missingAncestryRejected": true, + "nodes": 2, + "rejectionsDoNotMutateWorldGraph": true, + "staleRejected": true, + "worldGraphRoundTrip": true + }, + "confidenceIsCalibrated": false, + "evidence": "SYNTHETIC", + "heuristicScore": 0.8999999761581421, + "limitations": [ + "No real hardware or ESP-Fi timestamps/labels used", + "Bridge freshness/ancestry guards are additional policy, not native WorldGraph guarantees", + "Simulation trust is not production sensor enrollment", + "No model training or predictive accuracy uplift established" + ], + "native": { + "duplicateRejected": true, + "productionRejectsSynthetic": true, + "simulationSkipsWallClockFreshness": true, + "worldGraphAllowsCycles": true + }, + "promotionAuthorized": false, + "purpose": "integrity and reliability, not activity accuracy", + "rawDataIncluded": false, + "ruFieldRevision": "b74bc035e4a3d4023665aa5479b41e938facd113", + "worldGraphRevision": "9b1c79c836cdacfb7b44f058c593157bac4c1dab" +} diff --git a/scripts/field-context-probe/src/main.rs b/scripts/field-context-probe/src/main.rs new file mode 100644 index 000000000..5fd7564a4 --- /dev/null +++ b/scripts/field-context-probe/src/main.rs @@ -0,0 +1,378 @@ +//! Synthetic reliability probe using actual pinned RuField and WorldGraph. +//! No activity model, physical capture, confidence calibration or person track. +use std::collections::{BTreeMap, BTreeSet}; +use rufield_core::{FieldEvent, FieldTensor, FieldAxis, Modality, Observation, PrivacyClass, + ProvenanceRef, SensorDescriptor, FusionEngine, InferenceQuery, FieldInference}; +use rufield_fusion::{RuFieldFusion, RuleSet}; +use rufield_provenance::{sha256_hex, Signer, verify_event, TrustVerifier, TrustPolicy, TrustedKeyRegistry}; +use wifi_densepose_worldgraph::{WorldGraph, WorldNode, WorldId, WorldEdge, SemanticProvenance}; +use serde_json::{json, Value}; + +const NOW: u64 = 1_800_000_000_000_000_000; // Authored simulation clock, not capture metadata. +const MAX_AGE: u64 = 2_000_000_000; +const MAX_EVENTS: usize = 32; +const RULES: &str = r#" +[rule.synthetic_presence] +inputs = ["wifi_csi"] +method = "weighted_bayes" +feature = "presence" +threshold = 0.5 +privacy_max = "P2" +"#; + +fn fixture(id: &str, timestamp: u64) -> FieldEvent { + let tensor = FieldTensor::new(timestamp, Modality::WifiCsi, + vec![FieldAxis::Frequency], vec![3], vec![1., 2., 3.], + 0.9, 0.01, Some("synthetic_room_cal_v1".into()), PrivacyClass::P2).unwrap(); + let mut observation = Observation::occupancy(0.9, PrivacyClass::P2); + observation.features.insert("presence".into(), 0.9); + let mut event = FieldEvent::new(id, timestamp, SensorDescriptor { + modality: "wifi_csi".into(), vendor: "SYNTHETIC_FIXTURE".into(), + device_id: "synthetic_sensor".into(), placement: "authored_fixture".into(), + coordinate_frame: None, position_m: None, orientation_xyzw: None, + clock_domain: "synthetic_clock".into(), + }, tensor, observation, ProvenanceRef { + raw_hash: sha256_hex(format!("SYNTHETIC fixture {id} {timestamp}").as_bytes()), + firmware_hash: sha256_hex(b"SYNTHETIC fixture generator"), + model_id: "authored_feature_not_trained_model".into(), + calibration_id: "synthetic_room_cal_v1".into(), synthetic: true, + signature_hex: None, signer_pubkey_hex: None, + }); + // Public deterministic test key; never a production credential. + Signer::from_seed(&[17; 32]).sign_event(&mut event).unwrap(); + event +} + +/// Thin conservative bridge policy; these guards are NOT native WorldGraph features. +/// Only fresh signed synthetic root events enter this demonstration. Derived +/// observations cannot feed back into the root evidence stream. +struct Bridge { + fusion: RuFieldFusion, + world: WorldGraph, + roots: BTreeMap, + hashes: BTreeSet, + published: BTreeSet, +} +impl Bridge { + fn new() -> Self { + Self { fusion: RuFieldFusion::with_rules(RuleSet::from_toml(RULES).unwrap()), + world: WorldGraph::new(Default::default()), roots: BTreeMap::new(), + hashes: BTreeSet::new(), published: BTreeSet::new() } + } + fn ingest(&mut self, event: FieldEvent, ancestry: &[String], now: u64) -> Result<(), String> { + if !event.provenance.synthetic { return Err("synthetic_probe_only".into()); } + if !ancestry.is_empty() { return Err("derived_or_circular_evidence_excluded".into()); } + if event.timestamp_ns > now || now.saturating_sub(event.timestamp_ns) > MAX_AGE { + return Err("stale_or_future_bridge_gate".into()); + } + if self.roots.len() >= MAX_EVENTS { return Err("bounded_capacity".into()); } + if self.roots.contains_key(&event.event_id) || self.hashes.contains(&event.provenance.raw_hash) { + return Err("duplicate_evidence_bridge_gate".into()); + } + verify_event(&event).map_err(|e| format!("signature: {e}"))?; + let id = event.event_id.clone(); + let hash = event.provenance.raw_hash.clone(); + let at = i64::try_from(event.timestamp_ns / 1_000_000).map_err(|_| "timestamp_overflow")?; + self.fusion.ingest_at(event, now).map_err(|e| e.to_string())?; + let node = self.world.upsert_node(WorldNode::Event { id: WorldId::UNASSIGNED, + event_type: format!("SYNTHETIC root {id}"), at_unix_ms: at, located_in: None }); + self.roots.insert(id, node); + self.hashes.insert(hash); + Ok(()) + } + fn inference(&self) -> FieldInference { + self.fusion.infer(&InferenceQuery::all()).unwrap().into_iter().next().unwrap() + } + fn publish(&mut self, inference: &FieldInference, now: u64) -> Result { + if inference.expires_ns <= now || inference.produced_ns > now { + return Err("expired_inference_bridge_gate".into()); + } + if !inference.confidence.is_finite() || !(0.0..=1.0).contains(&inference.confidence) { + return Err("invalid_heuristic_score".into()); + } + let sources: BTreeSet<_> = inference.supporting_events.iter().cloned().collect(); + if sources.is_empty() || sources.len() != inference.supporting_events.len() || + sources.iter().any(|id| !self.roots.contains_key(id)) { + return Err("missing_or_repeated_root_ancestry".into()); + } + let fingerprint = format!("{}:{:?}:{:?}", inference.model_id, sources, inference.produced_ns); + if self.published.contains(&fingerprint) { return Err("duplicate_belief_bridge_gate".into()); } + let nodes: Vec<_> = sources.iter().map(|id| self.roots[id]).collect(); + let handles: Vec<_> = sources.into_iter().collect(); + let state = self.world.add_semantic_state( + "SYNTHETIC fixture presence; heuristic score, not calibrated probability".into(), + inference.confidence, (inference.produced_ns / 1_000_000) as i64, + SemanticProvenance { evidence: handles, model_version: inference.model_id.clone(), + calibration_version: inference.calibration_id.clone().unwrap_or_default(), + privacy_decision: "SYNTHETIC P2 local probe; no external release".into() }, &nodes); + self.published.insert(fingerprint); + Ok(state) + } +} + +fn probe() -> Value { + let event = fixture("root-1", NOW); + let mut native = TrustVerifier::simulation(); + native.verify_and_record_at(&event, NOW).unwrap(); + let native_duplicate = native.verify_and_record_at(&event, NOW).is_err(); + let old_event = fixture("old-root", NOW - MAX_AGE - 1); + let mut simulation = TrustVerifier::simulation(); + let native_simulation_accepts_old = simulation.verify_and_record_at(&old_event, NOW).is_ok(); + let mut production = TrustVerifier::new(TrustPolicy::production(), TrustedKeyRegistry::new()); + let native_production_rejects_synthetic = production.verify_and_record_at(&event, NOW).is_err(); + + let mut bridge = Bridge::new(); + bridge.ingest(event.clone(), &[], NOW).unwrap(); + let before = bridge.world.node_count(); + let duplicate_blocked = bridge.ingest(event, &[], NOW).is_err(); + let stale_blocked = bridge.ingest(old_event, &[], NOW).is_err(); + let cycle_blocked = bridge.ingest(fixture("derived-1", NOW + 1), &["derived-1".into()], NOW + 1).is_err(); + assert_eq!(before, bridge.world.node_count()); + let inference = bridge.inference(); + let belief = bridge.publish(&inference, NOW).unwrap(); + let before_rejections = bridge.world.to_json().unwrap(); + let repeated_belief = bridge.publish(&inference, NOW).is_err(); + let expired = bridge.publish(&inference, inference.expires_ns).is_err(); + let mut missing = inference.clone(); + missing.supporting_events = vec!["unknown-root".into()]; + let missing_blocked = bridge.publish(&missing, NOW).is_err(); + assert_eq!(before_rejections, bridge.world.to_json().unwrap()); + let ancestry = bridge.world.neighbors(belief); + assert_eq!(ancestry.len(), 1); + assert_eq!(ancestry[0].0, bridge.roots["root-1"]); + assert!(matches!(&ancestry[0].1, WorldEdge::DerivedFrom { evidence } if evidence == "root-1")); + let wire = bridge.world.to_json().unwrap(); + let restored = WorldGraph::from_json(&wire).unwrap(); + assert_eq!(restored.node_count(), bridge.world.node_count()); + assert_eq!(restored.edge_count(), bridge.world.edge_count()); + let restored_ancestry = restored.neighbors(belief); + assert_eq!(restored_ancestry[0].0, bridge.roots["root-1"]); + assert!(matches!(&restored_ancestry[0].1, WorldEdge::DerivedFrom { evidence } if evidence == "root-1")); + + // Characterize native WorldGraph's boundary honestly in an isolated graph. + let mut unrestricted = WorldGraph::new(Default::default()); + let a = unrestricted.upsert_node(WorldNode::Event { id: WorldId::UNASSIGNED, + event_type: "SYNTHETIC cycle boundary fixture".into(), at_unix_ms: 0, located_in: None }); + let native_cycle_allowed = unrestricted.add_edge(a, a, WorldEdge::DerivedFrom { evidence: "fixture".into() }).is_ok(); + json!({"evidence":"SYNTHETIC", "purpose":"integrity and reliability, not activity accuracy", + "ruFieldRevision":"b74bc035e4a3d4023665aa5479b41e938facd113", + "worldGraphRevision":"9b1c79c836cdacfb7b44f058c593157bac4c1dab", + "native": {"duplicateRejected":native_duplicate, + "simulationSkipsWallClockFreshness":native_simulation_accepts_old, + "productionRejectsSynthetic":native_production_rejects_synthetic, + "worldGraphAllowsCycles":native_cycle_allowed}, + "bridge": {"duplicateRejected":duplicate_blocked,"staleRejected":stale_blocked, + "derivedCircularRejected":cycle_blocked,"duplicateBeliefRejected":repeated_belief, + "expiredBeliefRejected":expired,"missingAncestryRejected":missing_blocked, + "rejectionsDoNotMutateWorldGraph":true,"ancestryEdges":ancestry.len(), + "worldGraphRoundTrip":true,"nodes":restored.node_count(),"edges":restored.edge_count()}, + "heuristicScore":inference.confidence,"confidenceIsCalibrated":false, + "rawDataIncluded":false,"promotionAuthorized":false, + "limitations":["No real hardware or ESP-Fi timestamps/labels used", + "Bridge freshness/ancestry guards are additional policy, not native WorldGraph guarantees", + "Simulation trust is not production sensor enrollment", + "No model training or predictive accuracy uplift established"]}) +} + +fn native_once(event: &FieldEvent) { + verify_event(event).unwrap(); + let mut fusion = RuFieldFusion::with_rules(RuleSet::from_toml(RULES).unwrap()); + fusion.ingest_at(event.clone(), NOW).unwrap(); + std::hint::black_box(fusion.infer(&InferenceQuery::all()).unwrap()); +} +fn bridge_once(event: &FieldEvent) { + let mut bridge = Bridge::new(); + bridge.ingest(event.clone(), &[], NOW).unwrap(); + let inference = bridge.inference(); + std::hint::black_box(bridge.publish(&inference, NOW).unwrap()); +} +fn quantile(values: &[f64], q: f64) -> f64 { + let mut ordered = values.to_vec(); + ordered.sort_by(f64::total_cmp); + ordered[((ordered.len() - 1) as f64 * q).round() as usize] +} +fn benchmark() -> Value { + let event = fixture("benchmark-root", NOW); + for _ in 0..100 { native_once(&event); bridge_once(&event); } + let mut native = Vec::with_capacity(1000); + let mut bridge = Vec::with_capacity(1000); + for i in 0..1000 { + let mut measure = |with_bridge: bool| { + let start = std::time::Instant::now(); + if with_bridge { bridge_once(&event); } else { native_once(&event); } + let elapsed = start.elapsed().as_secs_f64() * 1000.; + if with_bridge { bridge.push(elapsed); } else { native.push(elapsed); } + }; + if i % 2 == 0 { measure(false); measure(true); } else { measure(true); measure(false); } + } + let differences: Vec<_> = bridge.iter().zip(&native).map(|(b,n)| b-n).collect(); + json!({"evidence":"MEASURED software timing on SYNTHETIC fixtures", + "iterations":1000,"warmup":100,"profile":"release required for reported comparison", + "scope":"Fresh isolated engine per call: signature verify, fixture clone, rule parsing, ingestion and inference; bridge adds guards, WorldGraph creation and ancestry publication. Excludes signing, sensing, training and persistent-stream costs.", + "native_ms":{"p50":quantile(&native,0.5),"p95":quantile(&native,0.95)}, + "bridge_ms":{"p50":quantile(&bridge,0.5),"p95":quantile(&bridge,0.95)}, + "paired_extra_ms":{"p50":quantile(&differences,0.5),"p95":quantile(&differences,0.95)}, + "extra_p95_target_ms":0.1,"extra_p95_target_met":quantile(&differences,0.95)<0.1}) +} +// Strict wrapper deliberately differs from the adapter's truncated-tail recovery. +fn inspect_text(text: &str) -> Value { + let hash = sha256_hex(text.as_bytes()); + let lines: Vec<_> = text.lines().filter(|line| !line.trim().is_empty()).collect(); + if lines.len() > 10_000 { return json!({"accepted":false,"reason":"line_limit","sha256":hash}); } + let mut previous_timestamp = None; + let mut channel_width = None; + for (index, line) in lines.iter().enumerate() { + if serde_json::from_str::(line).is_err() { + return json!({"accepted":false,"reason":"strict_json_parse","nonempty_line":index+1,"sha256":hash, + "native_adapter_accepts":rufield_adapters::CsiReplayAdapter::from_jsonl(text).is_ok(), + "calibration_performed":false,"graph_mutated":false}); + } + } + for line in &lines { + let value: Value = serde_json::from_str(line).unwrap(); + let Some(timestamp) = value.get("timestamp").and_then(Value::as_f64) else { + return json!({"accepted":false,"reason":"strict_schema","sha256":hash}); + }; + let Some(amplitudes) = value.get("subcarriers").and_then(Value::as_array) else { + return json!({"accepted":false,"reason":"strict_schema","sha256":hash}); + }; + let nanos = timestamp * 1e9; + if !timestamp.is_finite() || timestamp < 0. || !nanos.is_finite() || nanos >= u64::MAX as f64 + || amplitudes.is_empty() || amplitudes.len() > 2048 + || amplitudes.iter().any(|v| v.as_f64().map_or(true, |x| !x.is_finite() || x < 0. || x > f32::MAX as f64)) { + return json!({"accepted":false,"reason":"strict_numeric_range","sha256":hash}); + } + let timestamp_ns = nanos as u64; + if previous_timestamp.is_some_and(|previous| timestamp_ns <= previous) + || channel_width.is_some_and(|width| width != amplitudes.len()) { + return json!({"accepted":false,"reason":"strict_recording_order_or_width","sha256":hash}); + } + previous_timestamp = Some(timestamp_ns); + channel_width = Some(amplitudes.len()); + } + match rufield_adapters::CsiReplayAdapter::from_jsonl(text) { + Ok(adapter) => { + let frames = adapter.frames(); + if frames.len() != lines.len() { + return json!({"accepted":false,"reason":"native_adapter_dropped_line","sha256":hash, + "nonempty_lines":lines.len(),"parsed_frames":frames.len(), + "calibration_performed":false,"graph_mutated":false}); + } + let span = frames.last().unwrap().timestamp_ns.saturating_sub(frames[0].timestamp_ns); + json!({"accepted":true,"sha256":hash,"frames":frames.len(),"timestamp_span_seconds":span as f64 / 1e9, + "calibration_performed":false,"graph_mutated":false,"labels_inferred":false, + "evidence":"Format-only replay parsing using actual pinned CsiReplayAdapter"}) + }, + Err(_) => json!({"accepted":false,"reason":"native_adapter_validation","sha256":hash, + "calibration_performed":false,"graph_mutated":false}) + } +} +fn inspect_path(path: &str) -> Result> { + use std::io::Read; + const MAX_BYTES: u64 = 1_048_576; + let metadata = std::fs::symlink_metadata(path)?; + if !metadata.file_type().is_file() || metadata.len() > MAX_BYTES { return Err("regular file <= 1 MiB required".into()); } + let mut text = String::new(); + std::fs::File::open(path)?.take(MAX_BYTES+1).read_to_string(&mut text)?; + if text.len() as u64 > MAX_BYTES { return Err("file exceeds limit".into()); } + Ok(inspect_text(&text)) +} +fn main() { + let args: Vec<_> = std::env::args().skip(1).collect(); + if args.len() == 2 && args[0] == "--inspect-jsonl" { + match inspect_path(&args[1]) { + Ok(report) => { println!("{}", serde_json::to_string_pretty(&report).unwrap()); + if report["accepted"] != true { std::process::exit(2); } }, + Err(_) => { eprintln!("Replay rejected: invalid, unreadable, oversized or non-UTF8 regular file"); std::process::exit(2); } + } + return; + } + let mut report = probe(); + if args == ["--benchmark"] { report["benchmark"] = benchmark(); } + else if !args.is_empty() { eprintln!("Usage: ruview-field-context-probe [--benchmark | --inspect-jsonl PATH]"); std::process::exit(2); } + println!("{}", serde_json::to_string_pretty(&report).unwrap()); +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn strict_replay_rejects_truncated_tail_without_calibrating() { + let valid = r#"{"timestamp":1.0,"subcarriers":[1.0,2.0]} +{"timestamp":1.1,"subcarriers":[2.0,3.0]}"#; + let report = inspect_text(valid); + assert_eq!(report["accepted"], true); + assert_eq!(report["frames"], 2); + assert_eq!(report["calibration_performed"], false); + let truncated = format!("{valid}\n{{"); + let report = inspect_text(&truncated); + assert_eq!(report["accepted"], false); + assert_eq!(report["native_adapter_accepts"], true); + assert_eq!(report["graph_mutated"], false); + for tail in ["{}", r#"{"timestamp":"wrong-type","subcarriers":[1.0]}"#] { + let report = inspect_text(&format!("{valid}\n{tail}")); + assert_eq!(report["accepted"], false); + assert_eq!(report["reason"], "strict_schema"); + } + for bad in [ + r#"{"timestamp":1e300,"subcarriers":[1.0]}"#, + r#"{"timestamp":1.0,"subcarriers":[1e300]}"#, + r#"{"timestamp":1.0,"subcarriers":[-1.0]}"#, + r#"{"timestamp":1.0,"subcarriers":[]}"#, + ] { assert_eq!(inspect_text(bad)["accepted"], false); } + for tail in [r#"{"timestamp":1.0,"subcarriers":[1.0,2.0]}"#, + r#"{"timestamp":1.2,"subcarriers":[1.0]}"#] { + assert_eq!(inspect_text(&format!("{valid}\n{tail}"))["reason"], "strict_recording_order_or_width"); + } + assert_eq!(inspect_text("")["accepted"], false); + } + #[test] + fn actual_libraries_and_bridge_contract() { + let result = probe(); + for key in ["duplicateRejected", "staleRejected", "derivedCircularRejected", + "duplicateBeliefRejected", "expiredBeliefRejected", "missingAncestryRejected", + "rejectionsDoNotMutateWorldGraph", "worldGraphRoundTrip"] { + assert_eq!(result["bridge"][key], true, "{key}"); + } + assert_eq!(result["native"]["duplicateRejected"], true); + assert_eq!(result["native"]["simulationSkipsWallClockFreshness"], true); + assert_eq!(result["native"]["productionRejectsSynthetic"], true); + assert_eq!(result["native"]["worldGraphAllowsCycles"], true); + } + #[test] + fn synthetic_replay_is_deterministic() { assert_eq!(probe(), probe()); } + #[test] + fn tampering_and_future_evidence_cannot_change_graph() { + let mut bridge = Bridge::new(); + let mut event = fixture("tamper", NOW); + event.tensor.values[0] = 999.; + assert!(bridge.ingest(event, &[], NOW).is_err()); + assert!(bridge.ingest(fixture("future", NOW + 1), &[], NOW).is_err()); + assert_eq!(bridge.world.node_count(), 0); + } + #[test] + fn capacity_and_invalid_score_fail_without_graph_mutation() { + let mut bridge = Bridge::new(); + for i in 0..MAX_EVENTS { + bridge.ingest(fixture(&format!("capacity-{i}"), NOW+i as u64), &[], NOW+i as u64).unwrap(); + } + let before = bridge.world.to_json().unwrap(); + assert!(bridge.ingest(fixture("overflow", NOW+100), &[], NOW+100).is_err()); + let mut inference = bridge.inference(); + inference.confidence = f32::NAN; + assert!(bridge.publish(&inference, NOW+100).is_err()); + assert_eq!(before, bridge.world.to_json().unwrap()); + } + #[test] + fn repeated_raw_hash_under_new_id_is_not_independent_evidence() { + let mut bridge = Bridge::new(); + let event = fixture("a", NOW); + let hash = event.provenance.raw_hash.clone(); + bridge.ingest(event, &[], NOW).unwrap(); + let mut correlated = fixture("b", NOW + 1); + correlated.provenance.raw_hash = hash; + Signer::from_seed(&[17; 32]).sign_event(&mut correlated).unwrap(); + assert!(bridge.ingest(correlated, &[], NOW + 1).is_err()); + assert_eq!(bridge.world.node_count(), 1); + } +} diff --git a/scripts/field-context-probe/verification-receipt.json b/scripts/field-context-probe/verification-receipt.json new file mode 100644 index 000000000..00542a803 --- /dev/null +++ b/scripts/field-context-probe/verification-receipt.json @@ -0,0 +1,66 @@ +{ + "date_utc": "2026-10-12", + "rust": "1.90.0", + "evidence": "Measured software integrity and format parsing; no predictive accuracy claim", + "test_result": { + "passed": 6, + "failed": 0 + }, + "commands": [ + "cargo test --locked", + "cargo build --release --locked", + "target/release/ruview-field-context-probe --benchmark", + "target/release/ruview-field-context-probe --inspect-jsonl EXCLUDED_FIXTURE_PATH" + ], + "completionVerified": true, + "test_build_benchmark_exit_code": 0, + "independent_review": "NO BLOCK on final source", + "primary_agent_test_rerun": { + "completionVerified": true, + "exitCode": 0, + "passed": 6, + "source_hashes_match": true + }, + "files_sha256": { + "Cargo.toml": "6cbae892247fc9fb3c5e9c02c1d8fc3c5cdea9efc15f8794a5885db797ca1c75", + "Cargo.lock": "c614e88fbf703f79f7f15a273f03b185a580efd04cc8452f302c23013d4c3267", + "src/main.rs": "c85aa8bcdfbc49a13ae0ebc807fcb09e4d0d619a4125d33dbe3c105d7f4eaa2a", + "example-result.json": "dbfbeac396588fefb3c0067897f74b80c76cdadb88cdfce60fd0e3566fe6a6fa" + }, + "public_fixture_source_revision": "995567283606b86594439d6594387ff551a21ede", + "public_replay": [ + { + "name": "real_csi_small.jsonl", + "sha256": "af90245da89f91b2ac9408aa933e654b43b57033f1d7c77db4f0ab27b801eeab", + "accepted": true, + "exit_code": 0, + "frames": 3, + "timestamp_span_seconds": 0.200999936 + }, + { + "name": "real_csi_medium.jsonl", + "sha256": "fb0c7cd81a968f99020ce16b4c35fecd5638062278aecf0f0f1ed883afba1f67", + "accepted": false, + "exit_code": 2, + "reason": "strict_json_parse", + "nonempty_line": 200, + "native_adapter_accepts": true + } + ], + "raw_capture_committed": false, + "calibration_performed": false, + "labels_inferred": false, + "production_promotion": false, + "strict_schema_tail_regressions": [ + "empty object rejected", + "wrong timestamp type rejected" + ], + "strict_numeric_recording_regressions": [ + "timestamp1e300", + "amplitude1e300", + "negative amplitude", + "empty channels", + "nonincreasing timestamp", + "channel width change" + ] +} diff --git a/scripts/tests/test_espfi_context.py b/scripts/tests/test_espfi_context.py new file mode 100644 index 000000000..dc9bb76c4 --- /dev/null +++ b/scripts/tests/test_espfi_context.py @@ -0,0 +1,130 @@ +"""Synthetic feature and experiment contracts, not real sensing accuracy.""" +import copy +import json +from pathlib import Path +import sys +import tempfile +import unittest + +import numpy as np +from safetensors.numpy import save_file + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from csi_training.espfi_context import BASELINE, CONTRACT, GRID, WIDTHS, ContextBundle, context_features, search +from csi_training.espfi_kernel import fit +from csi_training.espfi_motion import ROBUST_CONTRACT, motion_features +from csi_training.espfi_train import digest, write_json +from test_espfi_generalize import fixture + + +def make_baseline(parts, provenance, root): + root.mkdir() + state = fit(motion_features(parts['train']['x'], 'motion_acf'), parts['train']['y'], BASELINE) + save_file(state, str(root / 'model.safetensors')) + write_json(root / 'status.json', {'status': 'complete', 'promotionAuthorized': False}) + write_json(root / 'metrics.json', { + 'status': 'complete', 'promotionAuthorized': False, 'task': 'public_motion_ablation', + 'contract': ROBUST_CONTRACT, 'selected': {'candidate': BASELINE}, + 'model_sha256': digest(root / 'model.safetensors'), 'provenance': provenance, + }) + + +class ContextTests(unittest.TestCase): + def test_order_reversal_gain_and_exact_control(self): + rng = np.random.default_rng(43) + x = np.exp(rng.normal(size=(2, 52, 190))) + actual = context_features(x, 'ordered_trend') + reverse = context_features(x[..., ::-1], 'ordered_trend') + np.testing.assert_array_equal(actual[:, :468], motion_features(x, 'motion_acf')) + np.testing.assert_allclose(actual[:, :468], reverse[:, :468], atol=1e-12) + np.testing.assert_allclose(actual[:, 468:624].reshape(2, 52, 3), + reverse[:, 468:624].reshape(2, 52, 3)[..., ::-1], atol=1e-12) + np.testing.assert_allclose(actual[:, 624:], -reverse[:, 624:], atol=1e-12) + self.assertGreater(np.max(np.abs(actual[:, 468:] - reverse[:, 468:])), .01) + gain = np.exp(rng.normal(size=(2, 52, 1))) + np.testing.assert_allclose(actual, context_features(x * gain, 'ordered_trend'), atol=1e-12) + np.testing.assert_array_equal(context_features(x, 'ordered_mean'), actual[:, :624]) + # Independent mathematical reference for the added descriptors. + z = np.log(x) + z -= z.mean(-1, keepdims=True) + z /= np.sqrt(np.mean(z**2, axis=(1, 2), keepdims=True)) + expected = np.stack((z[..., :63].mean(-1), z[..., 63:127].mean(-1), z[..., 127:].mean(-1)), -1) + np.testing.assert_allclose(actual[:, 468:624], expected.reshape(2, -1), atol=1e-12) + t = np.linspace(-1., 1., 190) + np.testing.assert_allclose(actual[:, 624:], np.sum(z * t, -1) / np.sum(t*t), atol=1e-12) + + def test_numerical_edges_and_input_bounds(self): + for family, width in WIDTHS.items(): + for level in (0., 1e-20, 1., 1e100): + result = context_features(np.full((1, 52, 190), level), family) + self.assertEqual(result.shape, (1, width)) + self.assertTrue(np.isfinite(result).all()) + x = np.ones((1, 52, 380))[:, :, ::2] + x[..., 100] += 1e-12 + self.assertTrue(np.isfinite(context_features(x, family)).all()) + for x in (np.ones((0, 52, 190)), np.ones((1, 52, 189)), + -np.ones((1, 52, 190)), np.full((1, 52, 190), np.nan)): + with self.assertRaises(ValueError): + context_features(x, 'ordered_mean') + with self.assertRaises(ValueError): + context_features(np.ones((1, 52, 190)), 'room_history') + + def test_selection_isolation_control_reload_and_tamper(self): + parts, provenance = fixture() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_baseline(parts, provenance, root / 'baseline') + report = search(parts, provenance, root / 'baseline', root / 'a') + changed = copy.deepcopy(parts) + changed['validation']['x'] = changed['validation']['x'][..., ::-1] * 1.2 + changed['validation']['y'] = (changed['validation']['y'] + 1) % 7 + changed['validation']['identity'][:, 2] = changed['validation']['y'] + 1 + other = search(changed, provenance, root / 'baseline', root / 'b') + self.assertEqual(len(GRID), 18) + self.assertEqual(report['selected'], other['selected']) + self.assertEqual(report['model_sha256'], other['model_sha256']) + self.assertTrue(report['baseline_state_exact']) + self.assertEqual(report['baseline_control']['candidate'], BASELINE) + winner = max(report['candidates'], key=lambda r: (r['minimum_cv_macro_f1'], + r['mean_cv_macro_f1'], -r['scalar_count'])) + self.assertEqual(report['selected'], winner) + self.assertEqual(report['contract'], CONTRACT) + self.assertEqual(report['evidence'], 'SYNTHETIC') + self.assertFalse(report['promotionAuthorized']) + self.assertEqual(report['reload_max_abs_logit_error'], 0) + self.assertEqual(ContextBundle(root / 'a').predict(parts['validation']['x']).shape, (7, 7)) + # Changing the scored development labels cannot alter the persisted choice. + selection = json.loads((root / 'a' / 'selection.json').read_text()) + self.assertEqual(selection['selected'], report['selected']) + self.assertNotIn('development', selection) + original = (root / 'a' / 'model.safetensors').read_bytes() + (root / 'a' / 'model.safetensors').write_bytes(original + b'corruption') + with self.assertRaisesRegex(ValueError, 'hash mismatch'): + ContextBundle(root / 'a') + (root / 'a' / 'model.safetensors').write_bytes(original) + write_json(root / 'a' / 'status.json', {'status': 'failed', 'promotionAuthorized': False}) + with self.assertRaises(ValueError): + ContextBundle(root / 'a') + + def test_partition_provenance_and_failed_control(self): + parts, provenance = fixture() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + make_baseline(parts, provenance, root / 'baseline') + invalid = dict(parts, test=parts['validation']) + with self.assertRaises(ValueError): + search(invalid, provenance, root / 'baseline', root / 'bad_split') + with self.assertRaisesRegex(ValueError, 'provenance mismatch'): + search(parts, dict(provenance, manifest_sha256='different'), root / 'baseline', root / 'bad_hash') + # Wrong learned control cannot silently support a comparison. + changed = copy.deepcopy(parts) + changed['train']['x'][:, :, 10] *= 2. + with self.assertRaisesRegex(ValueError, 'did not reproduce'): + search(changed, provenance, root / 'baseline', root / 'bad_control') + self.assertEqual(json.loads((root / 'bad_control' / 'status.json').read_text())['status'], 'failed') + with self.assertRaises(FileExistsError): + search(parts, provenance, root / 'baseline', root / 'bad_control') + + +if __name__ == '__main__': + unittest.main()