Skip to content

feat(dialectic): conformance fixture for the Rust port, embedding comparability, and the review record - #70

Open
aurascoper wants to merge 14 commits into
mainfrom
feat/dialectic-conformance-fixture
Open

feat(dialectic): conformance fixture for the Rust port, embedding comparability, and the review record#70
aurascoper wants to merge 14 commits into
mainfrom
feat/dialectic-conformance-fixture

Conversation

@aurascoper

Copy link
Copy Markdown
Owner

Lands feat/dialectic-conformance-fixture on main: 14 commits, 26 files changed, 6485 insertions(+), 109 deletions(-), clean merge, 0 behind.

What it carries:

  • the committed conformance-fixture harness that pins DialecticalDynamics for the Rust port, plus the boundary cases the first fixture could not discriminate
  • Embedding.cosineSimilarity returning nil for incomparable spaces (fix(embedding): cosineSimilarity returns nil for incomparable spaces, not 0 #68), with isComparable(with:) and a centroid that refuses mixed spaces
  • ParticleNavigatorEnv pinned against WorldModel/env.py
  • the review record: embedding comparability, absent-values-given-numeric-meaning, EmbeddingProfileTerms location, SemanticEval reachability, JEPA corpus scan, FeatureExtractor reachability
  • track B block-aware CV with the leak claim withdrawn; TP10 floor preregistration

Commits:

  • 1d89103 fix(embedding): cosineSimilarity returns nil for incomparable spaces, not 0
  • 5705082 test(worldmodel): pin ParticleNavigatorEnv against WorldModel/env.py
  • 01681b5 docs(review): the JEPA corpus scan came back empty — no captures exist
  • ad67756 docs(review): check FeatureExtractor's reachability instead of inheriting it
  • e7596f9 docs(review): an absent value given a legitimate numeric meaning, x5
  • 3f4afeb test(dialectic): add boundary cases the first fixture could not discriminate
  • 4581cdf docs(review): correct EmbeddingProfileTerms location — third repo, not absent
  • 52b7958 docs(review): correct the SemanticEval reachability claim, add a third option
  • d7c0f9c test(dialectic): add committed conformance-fixture harness for the Rust port
  • 026b159 research(eeg): preregister TP10 floor before collection
  • b452015 fix(trackb): block-aware CV and a time-index control -- and withdraw the leak claim
  • a0fd3db docs(validation): lead with 7.11x — what the old protocol reported on no signal
  • 384cbbb fix(validation): the alpha contrast could not tell eye closure from elapsed time

Not 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

aurascoper and others added 14 commits August 5, 2026 20:52
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant