feat(dialectic): conformance fixture for the Rust port, embedding comparability, and the review record - #70
Open
aurascoper wants to merge 14 commits into
Open
feat(dialectic): conformance fixture for the Rust port, embedding comparability, and the review record#70aurascoper wants to merge 14 commits into
aurascoper wants to merge 14 commits into
Conversation
…lapsed time validate-muse-physiology.py recorded one 30 s eyes-open block then one 30 s eyes-closed block, always in that order, and reported alpha_closed/alpha_open against a 1.5x threshold. Condition was perfectly confounded with block position: impedance settling, electrode warming and drowsiness onset all push that ratio the same way real alpha does, and no analysis of such a recording can separate them. Four counterbalanced blocks (open, closed, closed, open; 15 s each, same 60 s of alpha data) put both conditions at mean position 2.5, so a linear drift subtracts out. First 2 s of every block is discarded for the alpha build-up, symmetrically across conditions so the discard is not its own bias. Cue onset is now logged separately from block start, so the discard is auditable rather than a magic constant. ABBA cancels a linear trend exactly and electrode settling is curved, so a residual survives whose sign depends on the curvature. The time-index baseline measures it: the identical statistic on the same blocks relabelled by position alone (1-2 vs 3-4), orthogonal to condition under ABBA. A channel passes only if the condition ratio clears 1.5x AND exceeds the time-index ratio. TIME_BASELINE_MARGIN is a named constant because that residual is empirical. --self-check runs without hardware. On synthetic data with zero alpha and pure drift the OLD statistic reports 7.11x -- a comfortable pass -- while the new gate rejects it. Asserting both on identical input is the point: otherwise the check proves the new gate works, not that it catches what the old one missed. Also caught: MIN_VALID_SAMPLES derived from the removed SEG_OPEN and would have been left 2x loose against the shorter blocks, passing every truncated block it exists to catch. The 2026-07-10 results stay in Validation.md, annotated as collected under the sequential protocol and provisional pending re-collection. Recorded there and in docs/reviews/time-leakage-2026-08-06.md: the "time-index alone reaches 0.883" figure that motivated this has no preserved artifact and is not to be cited. The same review documents the open StratifiedKFold(shuffle=True) leak in evaluate-imagined-signal.py, which inflates the 0.65 Track B gate now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzVWyZwh6x665nXGCVR3pg
… no signal The provisionality note argued from the design defect. The number is the argument: zero alpha, pure drift, old statistic reports 7.11x against a 1.5x threshold. A reader who sees that understands why 2.98x/3.88x cannot be cited as demonstrated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzVWyZwh6x665nXGCVR3pg
…the leak claim evaluate-imagined-signal.py used StratifiedKFold(shuffle=True) over time-ordered trials. Replaced with GroupKFold on contiguous acquisition blocks, plus a time-index control on the same folds (the identical pipeline trained on trial position alone); the gate now requires the EEG score to beat it. But the claim that motivated this does not survive measurement, and the doc now says so. On synthetic trials with zero class signal -- monotone amplitude drift, 50/50 shuffled labels producing same-class runs -- the OLD shuffled evaluator scored 0.4375/0.4500/0.5000/0.5750 across four seeds. It never cleared the 0.65 gate. LinearSVC on 16 global band-power features has too little capacity to exploit fold-local structure. A second construction with labels correlated to acquisition position also failed to pass (blocked 0.4656, time-index 0.5250). So "the 0.65 threshold is inflated by an unknown amount" is withdrawn. It was asserted from a design smell, in a review document written to warn against exactly that move. GroupKFold stays as the conservative design -- shuffled folds over time-ordered trials remain bad practice and the swap costs nothing -- not as a fix for a measured defect. --self-check asserts only what reproduces: drift-only data does not pass the gate. It prints the shuffled number every run and flags loudly if the leak ever does appear, so the non-result gets re-tested rather than remembered. Contrast with the alpha protocol in the same review: there the old statistic reports 7.11x on pure drift. Same reasoning applied to both, one reproduced. CLAUDE.md: the EEG validation scripts are not macOS-only. The PyPI brainflow wheel bundles libMuseLib.so and libBrainFlowBluetooth.so, and the DYLD block is guarded by os.path.isdir, so a Linux box needs only pip install brainflow. The "macOS prototype" framing would have sent the next session to the wrong machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzVWyZwh6x665nXGCVR3pg
…st port
`crates/neuralcompose-hypnagogic/src/dynamics.rs` in neuralcompose-client-native
is a hand port of DialecticalDynamics.swift + DialecticalCompetition.swift. A
hand port of ~460 lines of scoring, tension carry-over and a tension-sharpened
softmax can be internally consistent, fully unit-tested and still wrong — a
transcription slip in the temperature path produces plausible output forever.
So the Rust asserts against this program's output, the way band_power.rs asserts
against the Python reference to 12 significant figures. That is what makes
"port" a checkable claim rather than a description of intent.
Committed permanently, not a throwaway: a fixture whose generator has been
deleted cannot be regenerated, so a later divergence becomes unattributable —
there would be no way to tell whether the Rust drifted or the Swift moved.
Records the actual uniform DRAWS consumed, never a seed. Swift's and Rust's
PRNGs produce different sequences from the same seed, so a seeded fixture would
fail against a correct port and the "selection differs while scores match"
diagnostic would fire on every run and mean nothing.
Embeddings are constructed arithmetically from a fixed integer recurrence and
L2-normalized in-process — no model is loaded, so the fixture tests the DYNAMICS
rather than whichever embedder happened to be installed, and this builds under
plain CLT with no weights, no network and no headband.
30 cases: each profile x {early turn, both centroids, near-tie under high
tension, a six-point draw sweep across a bifurcation, decisive margin, three
candidates, single candidate, empty competition}.
NOT YET COMPILED — authored on the Linux box, which has no Swift toolchain.
Expect a first-build pass before the output is trusted.
Also records docs/reviews/embedding-comparability-unenforced-2026-08-12.md: a
separate live defect found during the port. Embedding.cosineSimilarity(to:)
guards on dimension only, so the modelID rule stated by ADR-010 and
embedding_contract.md 2.7/3.3 is enforced by nothing — and its 0 sentinel is
indistinguishable from a genuine orthogonal pair. Note only; the fix is a
32-call-site migration to Float?, deliberately not attempted without a compiler.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d option Two corrections to the embedding-comparability note, both answerable from the Linux box after all — neither needed the Mac. 1. SemanticEval does NOT compare across models by design. The earlier draft said it did and named it the likely first caller, which would have made this a history (past reports already carrying cross-space numbers) rather than a risk. It takes a single --model flag (main.swift:12), resolves one conformer (:59), populates one embeddingByText map from it (:101-103), and stamps its report directory with that one modelID (:217). It compares models at the REPORT level, across runs. Its past outputs are not suspect and "latent" is the right word. 2. The nil-vs-trap framing was too narrow. Both options assume cross-model comparison is a similarity operation that should either return nothing or crash. property_law.rs already says otherwise: IndexEntryKey is the pair (content digest, embedding-space identity), and shares_index() is this exact question as an executable law — same index only when embedding_space_identity matches, "because mixing spaces silently poisons retrieval". PR #30 likewise treats cross-backend divergence as a conformance MEASUREMENT with its own tolerances, not a similarity with a caveat. So a caller that genuinely needs to relate two spaces wants a distinct, explicitly-declared operation, not cosineSimilarity handing back a nil. That makes nil the honest default rather than a compromise. Also records that EmbeddingProfileTerms does not exist under that name in either repo; the concept is embedding_space_identity / IndexEntryKey / shares_index, and Swift has no equivalent today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t absent A previous commit in this branch claimed no EmbeddingProfileTerms type existed. That was true of the two repos searched (client-native crates/, NeuralCompose Sources/) and FALSE of the workspace: it is in neural-memory-server at crates/neural-memory-domain/src/terms.rs:444. A wrong location recorded in a review doc is exactly what gets quoted later, which was the argument for fixing the SemanticEval phrasing one commit ago. The finding gets stronger, not weaker. EmbeddingProfileTerms seals model family, revision, sorted weight and tokenizer digests, dimensions, pooling, normalization and task instruction — its doc comment says the field set mirrors neuralcompose-mobile-core's embedding_space_identity (model_pack.rs:305). And it already answers the operation question outright. Its header states the backend is deliberately EXCLUDED from the identity: "A CPU run and an NPU run of the same model produce vectors in the same space or they do not, and that is a question to be measured, not asserted by stamping a different identity on them ... whether the two may share an index is what conformance decides." So the third option is not a proposal — it is the position this workspace already took and wrote down: identity declares the space, conformance measures whether two things share one. cosineSimilarity is neither, which is why a cross-space pair has no good answer there and nil is the honest one. The gap is that all of it is Rust. Swift has no counterpart to any of EmbeddingProfileTerms / embedding_space_identity / IndexEntryKey / shares_index, and its Embedding carries a bare modelID string that nothing checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iminate A mutation run against the Rust port with the original fixture left two mutants alive, both comparison operators at exact equality: sample(): d < cumulative -> d <= cumulative SURVIVED compete(): tension >= high -> tension > high SURVIVED Both are the classic boundary-condition survivor: the fixture's draws never landed exactly on a cumulative probability, and tension derived from a cosine never landed exactly on the silence bar. Killed by placing cases exactly either side of each comparison rather than by adding more ordinary cases. - tensionOverride injects tension directly, so the silence gate can be exercised at exact equality with the bar (compete already takes tension as a parameter on both sides, so this needs no new machinery). Paired with a just-under case, because the at-bar case alone would also pass against code that always fell silent. - Two IDENTICAL candidate embeddings give identical potentials, hence probabilities exactly [0.5, 0.5], with the draw exactly on that boundary. Identical embeddings also make derived tension exactly 0, keeping the silence gate out of the way so selection is the only variable. Emits tensionInjected so the Rust side knows not to re-derive tension for those cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A recurring shape in BCICore, not isolated accidents: a value that is missing,
unfindable or incomparable is represented by a number the same field can
legitimately hold, so the failure is indistinguishable from a real reading and
produces no symptom.
1. Embedding.cosineSimilarity returns 0 for incomparable operands — and 0 is a
real cosine (orthogonal vectors from the SAME model return it). Detailed
separately in embedding-comparability-unenforced-2026-08-12.md.
2. DialecticalDynamics.centroid silently averages the comparable subset. A
subset centroid is a real vector and every later similarity against it is
plausible.
3. HypnagogicDialecticLoop.swift:241, roleFulfillment: role?.objective ?? 0 —
the sharpest one. roleFulfillment exists (per DialecticalCompetition.swift:88)
so a later stage can notice a role that FAILED ITS BRIEF, and 0 is precisely
that signal. A role-lookup miss therefore manufactures the exact reading the
field exists to detect. Not currently reachable; one refactor away.
4. FeatureExtractor.swift:122-126, energies["delta"] ?? 0 — 0 is a legitimate
band energy (a dead electrode), so a missing key and a flat channel are the
same number downstream.
5. ProsodyWobble.swift:49-52 substitutes concrete defaults for nil, converting
"let the engine choose" into a decision. Arguably deliberate.
THE CODEBASE ALREADY CONTAINS THE CORRECT IDIOM. SpeechProsody.blend
(SpeechSynthesizing.swift:128) averages each field only over contributors that
specify it — a nil field ABSTAINS rather than voting zero, and the result is nil
when nothing contributed. That is the pattern the other five should follow, so
this is an inconsistency rather than an unfamiliarity.
Correcting my own earlier summary, which listed the blend as a third instance of
the defect: it is the counter-example. The bug there was in the RUST PORT, which
had reimplemented it as a two-way lerp over non-optional fields. Swift right,
port wrong; fixed in client-native 39c642a.
Also records that HypnagogicDialogueLoop.chunk's punctuation-run behaviour is a
latency bug rather than a cosmetic one — one ellipsis costs three utterances,
three preUtteranceDelays and three subprocess spawns, over a second of dead air
inside one spoken turn on the contemplative voices.
None of these should be fixed only in the Rust. The dialectic's Rust half is now
checked against the Swift by a committed conformance fixture, so a one-sided
change surfaces as a conformance failure — the mechanism working, but it means
the Swift moves first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ting it
Two revisions after actually tracing the code rather than filing by proximity.
1. roleFulfillment IS DIFFERENT IN KIND, and the doc now says why. Instances 1,
2, 4 and 5 are a sentinel colliding with a plausible NUMBER. This one collides
with a DIAGNOSTIC: roleFulfillment exists so a later stage can spot a role
that failed its brief, and 0 is that signal, so a lookup miss does not produce
a suspicious zero — it produces a confident report that the displacement pole
utterly failed. The failure impersonates the signal rather than hiding among
legitimate values. That is the version of the argument that should convince
someone who finds the other four unremarkable.
2. FEATUREEXTRACTOR IS NOT THE LIVE PATH, and the two things that made it look
urgent are both false:
- The Core ML classifier does NOT consume it. CoreMLIntentClassifier builds
its MLMultiArray directly from the raw window (:119), channels x samples,
zero-padded. FeatureExtractor's only callers are MockIntentClassifier (the
stub path), JEPATransition, and one golden-recording test.
- The dictionary lookup is barely breakable. Band names are hardcoded literals
in the SAME private function twenty lines apart, written by a loop that
always emits all five, with no config and no caller able to influence them.
The `?? 0` is dead defensive code; its only fuse is a rename typo.
So it drops to lowest severity — fix as tidiness, not as a defect.
The sharper line in that function is a different one: :117's
`n > 0 ? ... : 0` reports 0 when every channel is shorter than the lag, which
is insufficient-data-as-measurement and depends on runtime values (delta at
256 Hz needs 51 samples) rather than on a literal. And it lands somewhere that
matters: JEPATransition.init? takes alpha/beta/theta straight into the capture
persisted for offline JEPA training, guarding only isFinite — which a spurious
0 passes. The cost is a poisoned training corpus discovered much later, not a
bad live decision. Given a corpus has already been lost to a confound once,
that is the reachability worth recording.
Suggested handling is now ranked by what the failure costs rather than by how
alarming the line looks, and keeps ProsodyWobble hedged — it is the one case
where a fix would change output rather than only diagnostics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An exact 0.0 band energy out of a float computation over real EEG is a measure-zero event: genuine delta power is small but never exactly zero. So whether the :117 sentinel has already poisoned the JEPA corpus is a query, not a question to reason about — any hit is either this bug or a dead channel, and both are worth finding. Ran it. THERE ARE NO CAPTURES. TransitionCaptureManager writes ~/Documents/NeuralCompose/JEPATransitions/jepa_transitions.jsonl (TransitionCaptureManager.swift:58-60) and that directory has never been created, on either machine. The app-data dir holds Calibration, EEGIntegration, InteractionLogs, Recordings, health.json and voice-profile.json, and nothing JEPA-shaped. Consistent with CLAUDE.md's own statement that there are zero logged interaction events because the capture is opt-in and off by default. That is the good version of the finding rather than the disappointing one: the corpus is empty, so the guard lands BEFORE the first capture run at zero cleanup cost and with no archaeology. It also relocates the check — reject a window whose band energies come back exactly zero at capture time, rather than scanning for them afterwards. Also records the implicit precondition that should be made explicit: :117's safety rests on window length >= lag (delta at 256 Hz needs 51 samples), which is real, load-bearing and currently unwritten. Assert it rather than rely on windows happening to be long enough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Swift env had four hand-computed literals and no reference. It now replays a 260-step trajectory generated from env.py itself and matches BIT-FOR-BIT — every step, including all 11 speed-clamped ones. Three independent implementations now agree exactly on this function: the Python original, the Rust port in neuralcompose-client-native, and this. Nothing pinned it in any language until today. The predicted divergence did not appear. env.py computes the speed norm and divides in float64 while this does the whole clamp in Float, and the client repository's registration (docs/acceptance/worldmodel-demo.md, section 10, written before either test ran) named the clamped steps as where a mismatch would land if there was one. There was not; for these values the two roundings coincide. Exact equality, not a tolerance: both sides are Float, the fixture stores each value as a Float widened to Double which is lossless, and on a deterministic function a tolerance could only hide a real divergence. Verified the test can actually fail. Scaling restitution by 1.0001 in step() turns it red in 6 places; restoring the file byte-identically turns it green again. A conformance test nobody has watched fail is a guess. A missing fixture FAILS rather than skipping, the same rule the dialectic fixture holds and for the same reason. Regenerate from a neuralcompose-client-native checkout with tools/worldmodel-fixture/generate.py under the .venv-muse interpreter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not 0 `Embedding.cosineSimilarity(to:)` guarded on dimension alone and returned `0` for a mismatch, which is also the legitimate score for orthogonal vectors from the same model. Per docs/reviews/embedding-comparability- unenforced-2026-08-12.md, that made "incomparable" indistinguishable from "unrelated" and scored a cross-space candidate a neutral 0.5. Mirrors the Rust port (neuralcompose-hypnagogic/src/embedding.rs): - `Embedding.isComparable(with:)`: same modelID, same dimension, non-empty. seed/version are not part of it because `centroid(of:)` builds centroids with seed 0 and must stay comparable to its members. - `cosineSimilarity(to:) -> Float?`, nil when not comparable. - `DialecticalDynamics.normalizedSimilarity(_:_:)` traps on nil with the two space identities in the message; all BCICore scoring sites route through it. One embedder per session makes incomparable a defect, not a state. - `centroid(of:)` returns nil on any incomparable member instead of silently averaging the comparable subset. - Evals force-unwrap (single map, single embedder); tests use XCTUnwrap. - New test `testIncomparableIsNilNotZero`: orthogonal-and-comparable is 0, incomparable is nil, mixed-space centroid is nil. - embedding_contract.md section 2.7 now cites the type and the test, not a doc comment. UNVERIFIED ON THIS HOST: written on Linux with no Swift toolchain; the package targets macOS 14. Run `swift test` on the Mac before merging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5MbSKyxJn8jZPoggQjrJT
fix(embedding): cosineSimilarity returns nil for incomparable spaces, not 0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands
feat/dialectic-conformance-fixtureonmain: 14 commits, 26 files changed, 6485 insertions(+), 109 deletions(-), clean merge, 0 behind.What it carries:
DialecticalDynamicsfor the Rust port, plus the boundary cases the first fixture could not discriminateEmbedding.cosineSimilarityreturningnilfor incomparable spaces (fix(embedding): cosineSimilarity returns nil for incomparable spaces, not 0 #68), withisComparable(with:)and a centroid that refuses mixed spacesParticleNavigatorEnvpinned againstWorldModel/env.pyCommits:
1d89103fix(embedding): cosineSimilarity returns nil for incomparable spaces, not 05705082test(worldmodel): pin ParticleNavigatorEnv against WorldModel/env.py01681b5docs(review): the JEPA corpus scan came back empty — no captures existad67756docs(review): check FeatureExtractor's reachability instead of inheriting ite7596f9docs(review): an absent value given a legitimate numeric meaning, x53f4afebtest(dialectic): add boundary cases the first fixture could not discriminate4581cdfdocs(review): correct EmbeddingProfileTerms location — third repo, not absent52b7958docs(review): correct the SemanticEval reachability claim, add a third optiond7c0f9ctest(dialectic): add committed conformance-fixture harness for the Rust port026b159research(eeg): preregister TP10 floor before collectionb452015fix(trackb): block-aware CV and a time-index control -- and withdraw the leak claima0fd3dbdocs(validation): lead with 7.11x — what the old protocol reported on no signal384cbbbfix(validation): the alpha contrast could not tell eye closure from elapsed timeNot in this PR: issue #69 (silent process death in
MindMonitorOSCStreamTests, pre-existing on both branches, CI skips that suite).🤖 Generated with Claude Code
https://claude.ai/code/session_01J5MbSKyxJn8jZPoggQjrJT