Skip to content

core: Graph cohesion + StakeCovariance windowed acceleration — 15th+16th consolidated (Amara corrections #3+#4)#331

Open
AceHack wants to merge 1 commit intomainfrom
feat/graph-cohesion-conductance-plus-windowed-stake-covariance
Open

core: Graph cohesion + StakeCovariance windowed acceleration — 15th+16th consolidated (Amara corrections #3+#4)#331
AceHack wants to merge 1 commit intomainfrom
feat/graph-cohesion-conductance-plus-windowed-stake-covariance

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented Apr 24, 2026

Consolidates DIRTY #329 (Amara correction #3) with new StakeCovariance module (Amara correction #4).

44 GraphTests passing. 4 of 8 Amara 17th-ferry corrections now shipped.

Copilot AI review requested due to automatic review settings April 24, 2026 08:09
@AceHack AceHack enabled auto-merge (squash) April 24, 2026 08:09
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates Graph cohesion metrics (internal density, exclusivity, conductance) and introduces a new StakeCovariance primitive for windowed stake-delta covariance and “acceleration”, with accompanying tests.

Changes:

  • Added Graph.internalDensity, Graph.exclusivity, and Graph.conductance.
  • Added StakeCovariance module with windowedDeltaCovariance, covarianceAcceleration, and aggregateAcceleration.
  • Expanded Graph.Tests.fs with new unit tests covering these additions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/Core/Graph.fs Adds cohesion metrics and embeds new StakeCovariance APIs.
tests/Tests.FSharp/Algebra/Graph.Tests.fs Adds unit tests for cohesion metrics and StakeCovariance functions.

Comment thread src/Core/Graph.fs
Comment on lines +665 to +666
let aggregateAcceleration
(pairAccelerations: Map<int * int, double>)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregateAcceleration hard-codes Map<int * int, double>, which makes the StakeCovariance API inconsistent with the rest of Core’s generic node/key handling (e.g., Graph<'N>). Consider making this generic over the key type (e.g., Map<'N * 'N, double> with 'N : comparison) or just accepting a sequence of acceleration values if the keys aren’t used.

Suggested change
let aggregateAcceleration
(pairAccelerations: Map<int * int, double>)
let aggregateAcceleration<'N when 'N : comparison>
(pairAccelerations: Map<'N * 'N, double>)

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
/// acceleration in pairwise covariance, catching cartels that
/// coordinate economically even when their graph structure
/// looks ordinary.
[<AutoOpen>]
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graph.fs now defines a second [<AutoOpen>] module (StakeCovariance). Most Core modules appear to be one concept per file (e.g., src/Core/RobustStats.fs, src/Core/TemporalCoordinationDetection.fs), and auto-opening additional APIs from Graph.fs can unexpectedly pollute Zeta.Core. Consider moving StakeCovariance into its own StakeCovariance.fs (and only marking it AutoOpen if you really want those identifiers imported everywhere).

Suggested change
[<AutoOpen>]

Copilot uses AI. Check for mistakes.
Comment on lines +428 to +432
// ─── StakeCovariance ─────────

open Zeta.Core.StakeCovariance

[<Fact>]
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test suite, open directives are consistently kept at the top of the file (e.g., Algebra/RobustStats.Tests.fs, Algebra/TemporalCoordinationDetection.Tests.fs). Adding open Zeta.Core.StakeCovariance mid-file is inconsistent; consider moving it to the top-level opens or qualifying the calls instead.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +574 to +576
let allNodes = nodes g
if subset.Count = allNodes.Count then None
else
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: conductance treats a subset as “full” based on subset.Count = allNodes.Count, which can return None for non-full subsets that happen to have the same cardinality (e.g., {1;2;4} vs graph nodes {1;2;3}). Compare sets (or intersect with nodes g first) rather than comparing counts.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +620 to +645
/// Pairwise covariance of stake-delta series over the
/// trailing `windowSize` values. Returns None when either
/// series has fewer than `windowSize` points or when a
/// degenerate case (zero variance, insufficient samples)
/// makes covariance undefined.
let windowedDeltaCovariance
(windowSize: int)
(deltasA: double[])
(deltasB: double[])
: double option =
let n = min deltasA.Length deltasB.Length
if windowSize < 2 || n < windowSize then None
else
let start = n - windowSize
let mutable meanA = 0.0
let mutable meanB = 0.0
for i in 0 .. windowSize - 1 do
meanA <- meanA + deltasA.[start + i]
meanB <- meanB + deltasB.[start + i]
meanA <- meanA / double windowSize
meanB <- meanB / double windowSize
let mutable cov = 0.0
for i in 0 .. windowSize - 1 do
cov <- cov + (deltasA.[start + i] - meanA) *
(deltasB.[start + i] - meanB)
Some (cov / double windowSize)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

windowedDeltaCovariance doc says it returns None for “zero variance / insufficient samples”, but the implementation always returns Some when n >= windowSize (including constant/zero-variance windows, where covariance is well-defined and should be 0.0). Either implement the documented degeneracy checks or update the comment to match the actual behavior (and clarify whether you intend population vs sample covariance, since you divide by windowSize).

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +621 to +645
/// trailing `windowSize` values. Returns None when either
/// series has fewer than `windowSize` points or when a
/// degenerate case (zero variance, insufficient samples)
/// makes covariance undefined.
let windowedDeltaCovariance
(windowSize: int)
(deltasA: double[])
(deltasB: double[])
: double option =
let n = min deltasA.Length deltasB.Length
if windowSize < 2 || n < windowSize then None
else
let start = n - windowSize
let mutable meanA = 0.0
let mutable meanB = 0.0
for i in 0 .. windowSize - 1 do
meanA <- meanA + deltasA.[start + i]
meanB <- meanB + deltasB.[start + i]
meanA <- meanA / double windowSize
meanB <- meanB / double windowSize
let mutable cov = 0.0
for i in 0 .. windowSize - 1 do
cov <- cov + (deltasA.[start + i] - meanA) *
(deltasB.[start + i] - meanB)
Some (cov / double windowSize)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

windowedDeltaCovariance uses n = min deltasA.Length deltasB.Length and then takes the trailing window ending at n. If the two series have different lengths, this silently drops the tail of the longer series and can misalign time indices. Consider requiring equal lengths (return None/error otherwise) or explicitly documenting the alignment/truncation semantics.

Suggested change
/// trailing `windowSize` values. Returns None when either
/// series has fewer than `windowSize` points or when a
/// degenerate case (zero variance, insufficient samples)
/// makes covariance undefined.
let windowedDeltaCovariance
(windowSize: int)
(deltasA: double[])
(deltasB: double[])
: double option =
let n = min deltasA.Length deltasB.Length
if windowSize < 2 || n < windowSize then None
else
let start = n - windowSize
let mutable meanA = 0.0
let mutable meanB = 0.0
for i in 0 .. windowSize - 1 do
meanA <- meanA + deltasA.[start + i]
meanB <- meanB + deltasB.[start + i]
meanA <- meanA / double windowSize
meanB <- meanB / double windowSize
let mutable cov = 0.0
for i in 0 .. windowSize - 1 do
cov <- cov + (deltasA.[start + i] - meanA) *
(deltasB.[start + i] - meanB)
Some (cov / double windowSize)
/// trailing `windowSize` values. Returns None when the
/// series lengths differ, when either series has fewer
/// than `windowSize` points, or when a degenerate case
/// (zero variance, insufficient samples) makes covariance
/// undefined. Equal lengths are required so the trailing
/// window is aligned by time index in both series.
let windowedDeltaCovariance
(windowSize: int)
(deltasA: double[])
(deltasB: double[])
: double option =
if deltasA.Length <> deltasB.Length then None
else
let n = deltasA.Length
if windowSize < 2 || n < windowSize then None
else
let start = n - windowSize
let mutable meanA = 0.0
let mutable meanB = 0.0
for i in 0 .. windowSize - 1 do
meanA <- meanA + deltasA.[start + i]
meanB <- meanB + deltasB.[start + i]
meanA <- meanA / double windowSize
meanB <- meanB / double windowSize
let mutable cov = 0.0
for i in 0 .. windowSize - 1 do
cov <- cov + (deltasA.[start + i] - meanA) *
(deltasB.[start + i] - meanB)
Some (cov / double windowSize)

Copilot uses AI. Check for mistakes.
AceHack added a commit that referenced this pull request Apr 24, 2026
…correction) (#332)

Completes the input pipeline for TemporalCoordinationDetection.
phaseLockingValue (PR #298): PLV expects phases in radians but
didn't prescribe how events become phases. This ship fills the
gap.

17th graduation under Otto-105 cadence. Addresses Amara 17th-ferry
Part 2 correction #5: 'Without phase construction, PLV is just a
word.'

Surface (2 pure functions):
- PhaseExtraction.epochPhase : double -> double[] -> double[]
  Periodic-epoch phase. φ(t) = 2π · (t mod period) / period.
  Suited to consensus-protocol events with fixed cadence (slot
  duration, heartbeat, epoch boundary).
- PhaseExtraction.interEventPhase : double[] -> double[] -> double[]
  Circular phase between consecutive events. For sample t in
  [t_k, t_{k+1}), phase = 2π · (t - t_k) / (t_{k+1} - t_k).
  Suited to irregular event-driven streams.

Both return double[] of phase values in [0, 2π) radians. Empty
output on degenerate inputs (no exception). eventTimes assumed
sorted ascending; samples outside the event range get 0 phase
(callers filter to interior if they care).

Hilbert-transform analytic-signal approach (Amara's Option B)
deferred — needs FFT support which Zeta doesn't currently ship.
Future graduation when signal-processing substrate lands.

Tests (12, all passing):
epochPhase:
- t=0 → phase 0
- t=period/2 → phase π
- wraps cleanly at period boundary
- handles negative sample times correctly
- returns empty on invalid period (≤0) or empty samples

interEventPhase:
- empty on <2 events or empty samples
- phase 0 at start of first interval
- phase π at midpoint
- adapts to varying interval lengths (O(log n) binary search
  for bracketing interval)
- returns 0 before first and after last event (edge cases)

Composition with phaseLockingValue:
- Two nodes with identical epochPhase period → PLV = 1
  (synchronized)
- Two nodes with same period but constant offset → PLV = 1
  (perfect phase locking at non-zero offset is still locking)

This composes the full firefly-synchronization detection
pipeline end-to-end for event-driven validator streams:
  validator event times → PhaseExtraction → phaseLockingValue
  → temporal-coordination-detection signal

5 of 8 Amara 17th-ferry corrections now shipped:
#1 λ₁(K₃)=2 ✓ already correct (PR #321)
#2 modularity relational ✓ already correct (PR #324)
#3 cohesion/exclusivity/conductance ✓ shipped (PR #331)
#4 windowed stake covariance ✓ shipped (PR #331)
#5 event-stream → phase pipeline ✓ THIS SHIP
Remaining: #4 robust-z-score composite variant (future);
#6 ADR phrasing (already correct); #7 KSK naming (BACKLOG
#318 awaiting Max coord); #8 SOTA humility (doc-phrasing
discipline).

Build: 0 Warning / 0 Error.

Provenance:
- Concept: Aaron firefly-synchronization design
- Formalization: Amara 17th-ferry correction #5 with 3-option
  menu (epoch / Hilbert / circular)
- Implementation: Otto (17th graduation; options A + C shipped,
  Hilbert deferred)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…-ferry §B + §F + corrections #2 #7 #9 (#342)

Research-grade design doc for the Stage-2 rung of Amara's
corrected promotion ladder. Specifies: (a) placement under
src/Experimental/CartelLab/ (not src/Core/ — that's Stage 4);
(b) MetricVector type with PLV magnitude AND offset split
(correction #6); (c) INullModelGenerator interface +
Preserves/Avoids table columns; (d) IAttackInjector
forward-looking interface (Stage 3); (e) Wilson-interval
reporting contract with {successes, trials, lowerBound,
upperBound} schema (correction #2 — no more "~95% CI ±5%"
handwave); (f) RobustZScoreMode with Hybrid fallback
(correction #7 — percentile-rank when MAD < epsilon);
(g) explicit artifact-output layout under artifacts/
coordination-risk/ with five files + run-manifest.json
(correction #9).

6-stage promotion path (0 doc / 1 ADR / 2.a skeleton /
2.b full null-models + first attack / 3 attack suite /
4 Core/NetworkIntegrity / 5 Aurora-KSK) matches Amara's
corrected ladder and Otto-105 cadence.

Doc-only change; no code, no tests, no workflow, no
BACKLOG tail touch (avoids positional-conflict pattern
that cost #334#341 re-file this session).

This is the 7th of 10 18th-ferry operationalizations:
- #1/#10 test-classification (#339)
- #2 Wilson-interval design specified (this doc)
- #6 PLV phase-offset shipped (#340)
- #7 MAD=0 Hybrid mode specified (this doc)
- #9 artifact layout specified (this doc)
- #4 exclusivity already shipped (#331)
- #5 modularity relational already shipped (#324)

Remaining: Wilson-interval IMPLEMENTATION (waits on #323 +
Stage 2.a), MAD=0 Hybrid IMPLEMENTATION (waits on #333 +
Stage 2.a), conductance-sign doc (waits on #331), Stage-2.a
skeleton itself.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
Addresses Amara 18th-ferry correction #6: PLV = 1 can mean
anti-phase locking, not same-time synchronization. Downstream
detectors that rely on "PLV = 1 => synchronized" misread
anti-phase coordinators as same-time coordinators.

Two new functions in `TemporalCoordinationDetection`:

- `meanPhaseOffset phasesA phasesB : double option`
  Returns the argument (angle) of the mean complex phase-
  difference vector whose magnitude is the PLV. Returns
  None when series are empty, mismatched-length, or when
  the mean vector has effectively zero magnitude (1e-12
  floor) — in which case direction is mathematically
  undefined.

- `phaseLockingWithOffset phasesA phasesB : struct (double * double) option`
  Returns both magnitude and offset in one sequence pass.
  Zero-magnitude case: magnitude near 0, offset = nan;
  near-zero magnitude is the caller's reliable "offset is
  undefined" signal.

Existing `phaseLockingValue` contract unchanged; new primitives
are additive. Downstream `Graph.coordinationRiskScore*` and any
other detector consuming PLV can now add a separate offset-
based term instead of collapsing both into one scalar (Amara's
explicit recommendation in correction #6).

8 new xUnit tests covering:
- Identical series (offset = 0)
- Constant pi/4 offset (observed = -pi/4, a-minus-b convention)
- Anti-phase series (magnitude 1, offset = pi) — the correction
  #6 regression test, contrasted against in-phase (offset 0)
  with identical magnitude
- Uniformly-distributed differences (zero-magnitude => None)
- Empty / mismatched-length / single-element edge cases
- phaseLockingWithOffset magnitude matches phaseLockingValue
  (consistency property preventing silent detector divergence)
- phaseLockingWithOffset zero-magnitude returns (near-zero, nan)
- phaseLockingWithOffset returns None on empty/mismatched

All 37 TemporalCoordinationDetection tests pass locally.
0 Warnings / 0 Errors build.

6th of the 10 18th-ferry corrections operationalized this
week (after test-classification doc in #339, parser-tech in
#338). Remaining: Wilson CIs in CartelToy tests (needs #323
landed), MAD=0 percentile-rank fallback (needs #333 landed),
conductance-sign doc (needs #331 landed), artifact-output
layout (Stage-2 with calibration harness).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…18th graduation (Amara #4 robust)

Two ships consolidated per the 'parallel PRs hit positional
conflicts on tail-append' lesson:

1. RobustStats.robustZScore
   (baseline: double seq) -> (measurement: double) -> double option
   Returns (measurement - median) / (1.4826 · MAD). The 1.4826
   constant scales MAD to be consistent with Gaussian stddev.
   MadFloor prevents blow-up when every baseline value equal.

2. Graph.coordinationRiskScoreRobust
   alpha beta eigenTol eigenIter lpIter
   (baselineLambdas: double seq) (baselineQs: double seq)
   (attacked: Graph<'N>) -> double option
   Upgrades coordinationRiskScore (PR #328) from raw linear
   differences to robust-standardized z-scores per Amara
   17th-ferry correction #4. Caller provides baseline metric
   distributions; Z-scores calibrate thresholds from data.

Why robust z-scores: adversarial data isn't normally
distributed. An attacker can poison a ~normal distribution
by adding a few outliers that inflate stddev, making
subsequent real attacks look 'within one sigma'. Median+MAD
survives ~50% adversarial outliers. Standard move in robust
statistics literature; Amara's correction puts it on the
Zeta composite.

Tests (5 new; total 39 since main hasn't merged #331/#332 yet):
- robustZScore None on empty baseline
- robustZScore of measurement = median is 0
- robustZScore scales MAD by 1.4826 for Gaussian consistency
  (measurement 4 on baseline [1..5] ≈ 0.674)
- coordinationRiskScoreRobust fires strongly on K4-injected graph
  given 5 baseline samples
- coordinationRiskScoreRobust returns None on empty baselines

BACKLOG rows added this tick per Aaron Otto-139 directives:
1. Signal-processing primitives (FFT + Hilbert) — unblocks
   Amara correction #5 Option B; Aaron standing-approval
2. F# DSL for entry points + graph-query-language standards
   compliance (Cypher / GQL / Gremlin / SPARQL / Datalog)
3. LINQ-compatible entry points for C# consumers — pair with
   F# DSL; two frontends, one algebraic backend

6 of 8 Amara 17th-ferry corrections now shipped or confirmed:
Remaining: #6 ADR phrasing (already fine); #7 KSK naming
(BACKLOG #318 Max coord pending); #8 SOTA humility
(doc-phrasing discipline ongoing).

Build: 0 Warning / 0 Error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…mara #4 robust) + 3 BACKLOG rows (#333)

* core: RobustStats.robustZScore + Graph.coordinationRiskScoreRobust — 18th graduation (Amara #4 robust)

Two ships consolidated per the 'parallel PRs hit positional
conflicts on tail-append' lesson:

1. RobustStats.robustZScore
   (baseline: double seq) -> (measurement: double) -> double option
   Returns (measurement - median) / (1.4826 · MAD). The 1.4826
   constant scales MAD to be consistent with Gaussian stddev.
   MadFloor prevents blow-up when every baseline value equal.

2. Graph.coordinationRiskScoreRobust
   alpha beta eigenTol eigenIter lpIter
   (baselineLambdas: double seq) (baselineQs: double seq)
   (attacked: Graph<'N>) -> double option
   Upgrades coordinationRiskScore (PR #328) from raw linear
   differences to robust-standardized z-scores per Amara
   17th-ferry correction #4. Caller provides baseline metric
   distributions; Z-scores calibrate thresholds from data.

Why robust z-scores: adversarial data isn't normally
distributed. An attacker can poison a ~normal distribution
by adding a few outliers that inflate stddev, making
subsequent real attacks look 'within one sigma'. Median+MAD
survives ~50% adversarial outliers. Standard move in robust
statistics literature; Amara's correction puts it on the
Zeta composite.

Tests (5 new; total 39 since main hasn't merged #331/#332 yet):
- robustZScore None on empty baseline
- robustZScore of measurement = median is 0
- robustZScore scales MAD by 1.4826 for Gaussian consistency
  (measurement 4 on baseline [1..5] ≈ 0.674)
- coordinationRiskScoreRobust fires strongly on K4-injected graph
  given 5 baseline samples
- coordinationRiskScoreRobust returns None on empty baselines

BACKLOG rows added this tick per Aaron Otto-139 directives:
1. Signal-processing primitives (FFT + Hilbert) — unblocks
   Amara correction #5 Option B; Aaron standing-approval
2. F# DSL for entry points + graph-query-language standards
   compliance (Cypher / GQL / Gremlin / SPARQL / Datalog)
3. LINQ-compatible entry points for C# consumers — pair with
   F# DSL; two frontends, one algebraic backend

6 of 8 Amara 17th-ferry corrections now shipped or confirmed:
Remaining: #6 ADR phrasing (already fine); #7 KSK naming
(BACKLOG #318 Max coord pending); #8 SOTA humility
(doc-phrasing discipline ongoing).

Build: 0 Warning / 0 Error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(#333): 4 review-thread P1/P2s on robustZScore + coordinationRiskScoreRobust

Active PR-resolve-loop on #333.

1. Doc/impl contradiction on MAD=0 (thread 59VhYb, P1):
   RobustStats.robustZScore doc said "returns None when
   MAD(baseline)=0" but impl uses MadFloor and returns
   Some finite value. Rewrote doc to match impl:
   explicit "MadFloor substituted when MAD collapses to
   zero" — floor reflects "scale is below epsilon" not
   "undefined." Implementation is the contract.

2. Multi-enumeration of baseline seq (thread 59VhYq, P1):
   robustZScore previously passed `baseline` to both
   `median` + `mad` which each call `Seq.toArray`.
   Expensive AND inconsistent for lazy/non-repeatable
   sequences (different values between enumerations =
   undefined behavior). Fixed: `Seq.toArray` once at
   entry, pass the materialized array to both. O(n)
   instead of O(2n); stable across lazy sources.

3. Name attribution in Graph.fs doc comment (thread
   59VhY5, P1): "Amara 17th-ferry... Otto 18th
   graduation" → "external AI collaborator's 17th
   courier ferry... Eighteenth graduation under the
   Otto-105 cadence." Role-reference convention per
   AGENT-BEST-PRACTICES code/doc rule.

4. Array-vs-seq terminology (thread 59VhZG, P2):
   Graph.fs doc said callers "provide arrays" but the
   API is `double seq`. Rewrote: sequences + noted the
   materialize-once optimization in robustZScore so
   callers can pass any seq form without re-enumeration
   cost.

Thread 59VhX9 (P3-label-in-P2-section mismatch) — already
resolved on main via PR #341 which landed the signal-
processing row correctly labeled "P2 research-grade."
No fix needed on this branch.

Build: 0 Warning(s) / 0 Error(s). 53 RobustStats + Graph
tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
Addresses Amara 18th-ferry correction #6: PLV = 1 can mean
anti-phase locking, not same-time synchronization. Downstream
detectors that rely on "PLV = 1 => synchronized" misread
anti-phase coordinators as same-time coordinators.

Two new functions in `TemporalCoordinationDetection`:

- `meanPhaseOffset phasesA phasesB : double option`
  Returns the argument (angle) of the mean complex phase-
  difference vector whose magnitude is the PLV. Returns
  None when series are empty, mismatched-length, or when
  the mean vector has effectively zero magnitude (1e-12
  floor) — in which case direction is mathematically
  undefined.

- `phaseLockingWithOffset phasesA phasesB : struct (double * double) option`
  Returns both magnitude and offset in one sequence pass.
  Zero-magnitude case: magnitude near 0, offset = nan;
  near-zero magnitude is the caller's reliable "offset is
  undefined" signal.

Existing `phaseLockingValue` contract unchanged; new primitives
are additive. Downstream `Graph.coordinationRiskScore*` and any
other detector consuming PLV can now add a separate offset-
based term instead of collapsing both into one scalar (Amara's
explicit recommendation in correction #6).

8 new xUnit tests covering:
- Identical series (offset = 0)
- Constant pi/4 offset (observed = -pi/4, a-minus-b convention)
- Anti-phase series (magnitude 1, offset = pi) — the correction
  #6 regression test, contrasted against in-phase (offset 0)
  with identical magnitude
- Uniformly-distributed differences (zero-magnitude => None)
- Empty / mismatched-length / single-element edge cases
- phaseLockingWithOffset magnitude matches phaseLockingValue
  (consistency property preventing silent detector divergence)
- phaseLockingWithOffset zero-magnitude returns (near-zero, nan)
- phaseLockingWithOffset returns None on empty/mismatched

All 37 TemporalCoordinationDetection tests pass locally.
0 Warnings / 0 Errors build.

6th of the 10 18th-ferry corrections operationalized this
week (after test-classification doc in #339, parser-tech in
#338). Remaining: Wilson CIs in CartelToy tests (needs #323
landed), MAD=0 percentile-rank fallback (needs #333 landed),
conductance-sign doc (needs #331 landed), artifact-output
layout (Stage-2 with calibration harness).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…340)

* core: PLV mean phase offset — 19th graduation (Amara 18th-ferry #6)

Addresses Amara 18th-ferry correction #6: PLV = 1 can mean
anti-phase locking, not same-time synchronization. Downstream
detectors that rely on "PLV = 1 => synchronized" misread
anti-phase coordinators as same-time coordinators.

Two new functions in `TemporalCoordinationDetection`:

- `meanPhaseOffset phasesA phasesB : double option`
  Returns the argument (angle) of the mean complex phase-
  difference vector whose magnitude is the PLV. Returns
  None when series are empty, mismatched-length, or when
  the mean vector has effectively zero magnitude (1e-12
  floor) — in which case direction is mathematically
  undefined.

- `phaseLockingWithOffset phasesA phasesB : struct (double * double) option`
  Returns both magnitude and offset in one sequence pass.
  Zero-magnitude case: magnitude near 0, offset = nan;
  near-zero magnitude is the caller's reliable "offset is
  undefined" signal.

Existing `phaseLockingValue` contract unchanged; new primitives
are additive. Downstream `Graph.coordinationRiskScore*` and any
other detector consuming PLV can now add a separate offset-
based term instead of collapsing both into one scalar (Amara's
explicit recommendation in correction #6).

8 new xUnit tests covering:
- Identical series (offset = 0)
- Constant pi/4 offset (observed = -pi/4, a-minus-b convention)
- Anti-phase series (magnitude 1, offset = pi) — the correction
  #6 regression test, contrasted against in-phase (offset 0)
  with identical magnitude
- Uniformly-distributed differences (zero-magnitude => None)
- Empty / mismatched-length / single-element edge cases
- phaseLockingWithOffset magnitude matches phaseLockingValue
  (consistency property preventing silent detector divergence)
- phaseLockingWithOffset zero-magnitude returns (near-zero, nan)
- phaseLockingWithOffset returns None on empty/mismatched

All 37 TemporalCoordinationDetection tests pass locally.
0 Warnings / 0 Errors build.

6th of the 10 18th-ferry corrections operationalized this
week (after test-classification doc in #339, parser-tech in
#338). Remaining: Wilson CIs in CartelToy tests (needs #323
landed), MAD=0 percentile-rank fallback (needs #333 landed),
conductance-sign doc (needs #331 landed), artifact-output
layout (Stage-2 with calibration harness).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(#340): refactor shared accumulation + 5 review-thread fixes (Otto-216)

Active PR-resolve-loop on #340 (PLV mean phase offset).

1. Sentinel-default in test (thread 59WGi9): replaced
   Option.defaultValue -1.0 pattern in the
   phaseLockingWithOffset-magnitude-matches-phaseLockingValue
   consistency test with explicit pattern-match + fail
   on None. Sentinel form would silently pass the
   equality assertion if BOTH primitives returned None,
   masking regressions.

2. Broken ferry cross-reference path (thread 59WGjn):
   doc comment referenced docs/aurora/2026-04-24-amara-
   calibration-ci-hardening-deep-research-plus-5-5-
   corrections-18th-ferry.md which doesn't exist on
   main (only 7th / 17th / 19th ferries landed as
   standalone docs). Rewrote provenance to describe the
   ferry topically + cross-reference the related 19th-
   ferry DST audit that IS in the repo.

3. Misleading "same PLV-magnitude floor" wording
   (thread 59WGj4): doc said meanPhaseOffset's
   zero-magnitude check uses "the same PLV-magnitude
   floor" — phaseLockingValue has NO floor (returns
   values arbitrarily close to 0). Fixed: clarified
   that the phasePairEpsilon floor applies ONLY to
   the offset-undefined decision; phaseLockingValue
   returns magnitude without threshold.

4. Name-attribution in doc comment (thread 59WGkP):
   "Aaron + Amara 11th ferry" replaced with "the 11th
   ferry" per factory role-reference convention. Audit-
   trail surfaces (commit messages, tick-history, memory)
   retain direct attribution; code/doc comments use
   role references.

5. Duplicate sin/cos accumulation across 3 functions
   (thread 59WGkn): extracted private helpers
   phasePairEpsilon + meanPhaseDiffVector. All three
   functions (phaseLockingValue, meanPhaseOffset,
   phaseLockingWithOffset) now route through the
   shared accumulator. Eliminates drift risk — one
   function can no longer silently diverge from the
   others on accumulation or threshold.

Build: 0 Warning(s) / 0 Error(s). All 37 TemporalCoordinationDetection
tests pass.

All 5 threads replied via GraphQL next step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(#340): 2 review threads (stale ferry path + atan2 range)

Thread 59Yqkl (P1) — stale provenance reference:
  The doc cited `docs/aurora/2026-04-24-amara-temporal-
  coordination-detection-cartel-graph-influence-surface-
  11th-ferry.md`, but the 11th ferry has not yet landed
  under `docs/aurora/` (it's queued in the Otto-105
  operationalize cadence; PR #296 is its pending absorb).
  Replaced with the intent-preserving form: role references
  ("external AI collaborator's 11th courier ferry") plus a
  pointer at the MEMORY.md queue entry, so the provenance
  survives regardless of when the file-path question
  resolves. Also dropped the direct first-name so this
  factory-produced doc-comment tracks the name-attribution
  discipline.

Thread 59YqlC (P2) — atan2 range correction:
  Doc said `(-pi, pi]` but `System.Math.Atan2` is documented
  as `[-pi, pi]` (both endpoints reachable under IEEE-754
  signed-zero semantics: atan2(0, -1) = +pi,
  atan2(-0, -1) = -pi). Updated the doc to match the
  implementation. Behaviour unchanged.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…leration — 15th+16th graduation consolidated

Consolidates the DIRTY #329 cohesion primitives (Amara 17th-ferry
correction #3) with the new StakeCovariance module (Amara
correction #4).

Content from closed PR #329 (resurrect):
- Graph.internalDensity / exclusivity / conductance
- Wachs & Kertész 2019 cartel-detection cohesion/exclusivity
  replacing muddy 'subgraph entropy collapse'

NEW content (Amara correction #4):
- Zeta.Core.StakeCovariance module with:
  - windowedDeltaCovariance : int -> double[] -> double[] -> double option
    Pairwise cov over sliding window of stake-delta series
  - covarianceAcceleration : double option -> double option ->
    double option -> double option
    2nd-difference of three consecutive cov values
    (A(t) = C(t) - 2·C(t-1) + C(t-2))
  - aggregateAcceleration : Map<int * int, double> -> double option
    Mean pairwise acceleration over candidate group

Why a separate module: StakeCovariance operates on raw stake-
delta time series, not Graph edges. Composes WITH Graph via
downstream detector that combines graph signals + covariance
signals + sync signals into the full CoordinationRiskScore.

Addresses Amara's Part 2 correction #4 ambiguity: her Part 1
had 'C(t) = Cov({s_i(t)}, {s_j(t)})' which is undefined at a
single timepoint. This ship implements the windowed-delta +
2nd-difference formulation from her correction.

Tests (10 new, 44 total in GraphTests, all passing):
- internalDensity None on |S|<2
- internalDensity of K3 weight-10 ≈ 10
- exclusivity = 1 for isolated K3
- conductance < 0.1 for well-isolated subset
- windowedDeltaCovariance: None on too-small series
- windowedDeltaCovariance: high positive for synchronized motion
- windowedDeltaCovariance: negative for anti-correlated motion
- covarianceAcceleration = 2nd difference of cov series
- covarianceAcceleration None when any input missing
- aggregateAcceleration averages across pairs

4 of 8 Amara 17th-ferry corrections now shipped (#1 K₃=2 ✓,
conductance ✓, #4 windowed stake covariance acceleration ✓).
Remaining: #5 event→phase pipeline (future); #4 robust-z-score
variant of composite (future); #6 + #7 + #8 doc-phrasing /
BACKLOG.

Build: 0 Warning / 0 Error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 13:46
@AceHack AceHack force-pushed the feat/graph-cohesion-conductance-plus-windowed-stake-covariance branch from 913965d to 81994e0 Compare April 24, 2026 13:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/Core/Graph.fs
Comment on lines +682 to +685
/// trailing `windowSize` values. Returns None when either
/// series has fewer than `windowSize` points or when a
/// degenerate case (zero variance, insufficient samples)
/// makes covariance undefined.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The doc comment for windowedDeltaCovariance says it returns None for degenerate cases like zero variance, but the implementation always returns Some (often 0.0) whenever windowSize >= 2 and enough samples exist. Either implement the stated degeneracy checks or update the comment to reflect the actual behavior.

Suggested change
/// trailing `windowSize` values. Returns None when either
/// series has fewer than `windowSize` points or when a
/// degenerate case (zero variance, insufficient samples)
/// makes covariance undefined.
/// trailing `windowSize` values. Returns None only when
/// `windowSize < 2` or when either series has fewer than
/// `windowSize` aligned points. Otherwise returns the
/// trailing-window covariance, which may be `0.0` for
/// constant windows or other zero-covariance inputs.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +731 to +733
let values = pairAccelerations |> Map.toSeq |> Seq.map snd |> Seq.toArray
let sum = values |> Array.sum
Some (sum / double values.Length)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: aggregateAcceleration materializes the map values into an array (Seq.toArray + Array.sum) just to compute an average. This adds an avoidable allocation; a Map.fold/Seq.fold that tracks (sum, count) would compute the mean in one pass without extra memory.

Suggested change
let values = pairAccelerations |> Map.toSeq |> Seq.map snd |> Seq.toArray
let sum = values |> Array.sum
Some (sum / double values.Length)
let sum, count =
pairAccelerations
|> Map.fold (fun (sum, count) _ acceleration -> sum + acceleration, count + 1) (0.0, 0)
Some (sum / double count)

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +603 to +607
let (s, t) = entry.Key
if subset.Contains s && subset.Contains t then
acc <- acc + double entry.Weight
let pairs = double size * double (size - 1)
Some (acc / pairs)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: internalDensity counts self-loops in the numerator (because it only checks subset.Contains s && subset.Contains t) but the denominator uses |S|(|S|-1) which assumes ordered pairs with s <> t. Since this graph type explicitly allows self-loops, either exclude s = t edges from the numerator or change the denominator to include self-pairs (e.g., |S|^2) so the metric matches its definition.

Copilot uses AI. Check for mistakes.
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.

2 participants