feat(drift): tangle_ratio hygiene gate — the antisymmetry half of health (#186)#241
Conversation
First of three #186 macro-track deliverables: a tangle_ratio hygiene invariant (normalized transpose-fixed mutual-motif mass, Σ M·triad / 3) as the symmetric partner of cycle_ratio, closing the antisymmetry half of health = acyclicity + antisymmetry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the antisymmetry half of health = acyclicity + antisymmetry: a domain of mutual coupling now fails the gate like a cyclic one. Registered in _COMPONENT_NAMES (the hygiene-constraint whitelist), declared in both patterns.yaml and the bundled ontology header (kept parse-identical by the staleness pin), enforced by the existing _hygiene_check, ratchet-able via hygiene_baseline (#151). Threshold audit (measured intra-domain + quotient tangle_ratio): cgis-self 0.036 (green floor) owner-api 0.078 (coupled-but-shipping) mesh ~1.0 (rider-web, math-bounded; TS not ingestible) default 0.25 clears coupled code ~3x, cgis-self ~7x, fires near mesh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
asdict skips the tangle_ratio property, so enrich each domain/quotient entry explicitly from report.actual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the tangle_ratio hygiene gate to measure and enforce antisymmetry (mutual coupling) across import and call layers. It adds the mathematical formulation for tangle_mass, integrates the new metric into the drift detection pipeline, updates the MCP server payload, and includes comprehensive unit tests. The review feedback correctly points out a technical inaccuracy in both the documentation and docstrings, where 030C (a cyclic triad) is incorrectly grouped under a 'pure DAG' description, and suggests revising the terminology to 'antisymmetric graph' or 'graph without mutual dyads'.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Sonar: float-equality checks (==) in tangle_mass / tangle_ratio tests now
go through pytest.approx.
gemini: 030C is a cyclic triad, so labelling the M=0 set {021*,030T,030C}
a "pure DAG" was wrong. It is an *antisymmetric* graph (no mutual dyads) —
cycles included; acyclicity is cycle_ratio's orthogonal job. Reworded the
tangle_mass docstring, the tangle_ratio property docstring, and
PATTERNS_AND_TRIADS.md accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces the tangle_ratio hygiene gate to measure and enforce graph antisymmetry alongside acyclicity. It adds the design specification, implements the tangle_mass metric calculation based on mutual-dyad counts, exposes the new ratio on PatternFingerprint and API payloads, and includes comprehensive unit tests. The review feedback suggests returning None instead of 0.0 when there is no shape signal (such as an empty census) in tangle_mass to avoid false-positive fit quality bands, and adding corresponding test assertions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Code review —
|
✅ Re: 🟡1 —
|
zaebee
left a comment
There was a problem hiding this comment.
Colleague review — tangle_ratio hygiene gate
Verdict: 🟢 Approve, three non-blocking notes. Checked the math and wiring by hand, not on faith.
Verified ✅
- Weight map correct.
_TANGLE_WEIGHTS = (0,0,0,1,1,0,0,2,1,1,1,2,3)matchestuple(int(name[0]) for name in TRIAD_ORDER)exactly, and the MAN first digit = mutual-dyad count.test_tangle_weights_align_with_triad_orderpins this. [0,1]guarantee holds.t_imports/t_callsare alwaysnormalized_census(Σ=1) orZERO_TRIADS; max M = 3,÷3→[0,1].- Score is byte-identical.
tangle_ratiois absent fromdrift_weights→ weight 0 in the score, so it's a pure gate. Adding it to_COMPONENT_NAMESis required (otherwise_parse_constraintssilently drops the hygiene line) — correct. - Self-drift stays green with margin. Ran ingest on
src/: maxtangle_ratioacross cgis.* domains = 0.0228 (cgis.extractors), rest 0.0. Default 0.25 is safe. - Quotient works — goes through the same
extract(), census populated. - 213 relevant tests green.
Notes
1. (medium) CALLS-layer tangle is not discounted by unresolved_ratio — risk of false gate_failed on sparse call graphs.
tangle_ratio = max(tangle_mass(t_imports), tangle_mass(t_calls)), but t_calls is normalized over resolved intra-domain CALLS only. In a domain whose calls are mostly raw/external, a single connected mutual triple (e.g. one 201) makes t_calls≈one-hot → tangle_mass=0.67 → hard gate_failed on thin evidence. The drift-distance path deliberately fades the calls layer via (1 − unresolved_ratio); the gate does not, and max() picks the worse (calls) layer. Worth discussing: apply the discount to calls-tangle, or add a minimum-triad-count floor.
2. (medium) The spec's CLI tangle column is missing. Spec §"Where it lives" lists src/cgis/cli.py — an append-only column in the drift table. The diff doesn't touch cli.py: the table shows Drift / TV imp / TV calls / Fit / Status but no tangle. The MCP payload exposes the value, but the primary human surface (cgis drift) does not — a breach shows as ⛔ in Status with no number. Either finish the column or strike it from the spec.
3. (low-medium) Self-drift guardrail does not protect against tangle regressions. tests/self_parsing/test_drift.py::_assert_within_tolerance gates on drift_score > tolerance, and tangle (weight 0 in the score) never enters it. The spec claims "re-checked in CI's self-parsing path", but a future tangle rise on cgis-self would not fail CI. Harmless now (0.0228), but adding a status != "gate_failed" assertion to the self-parsing path would make the safety claim real.
All three are improvements, not blockers. Mergeable per our merge rule; I'd at least file note #1 as a follow-up (it's a real noise source).
Colleague-review follow-ups (PR #241, notes 2 & 3): - note 3: _assert_within_tolerance now also fails on status == gate_failed. Hygiene breaches (cycle/unresolved/tangle) force gate_failed independently of drift_score, which the tolerance check alone misses — so a future tangle_ratio rise on the self-graph now fails CI instead of passing with weight 0 in the score. Makes the spec's self-parsing safety claim real. - note 2: spec "Where it lives" no longer lists a CLI tangle column (dropped by design — breach shows via gate_failed + hygiene note; value in MCP). Also corrected the fingerprint row (property, not stored field) and the drift row (_COMPONENT_NAMES whitelist). - note 1 (calls-layer discount) deferred to #244; recorded as a known follow-up in the spec and a NOTE in the tangle_ratio docstring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re: colleague review — three notes addressed (3da7d19)Thanks, all three verified against the code and were real. Resolution: Note 1 (calls-tangle not discounted) → deferred to #244. Confirmed: Note 3 (self-parsing guard didn't cover the gate) → fixed. Note 2 (CLI column vs spec) → struck from spec. The column was dropped by design (breach surfaces via No code-behavior change to the gate itself this round — these are a CI-safety hardening + doc sync. 1016 tests green. |
|



Summary
First of three #186 macro-track deliverables: a
tangle_ratiohygieneinvariant — the normalized mass of transpose-fixed (mutual) triad motifs — as the
symmetric partner of
cycle_ratio. It closes the antisymmetry half of thehealth predicate
health = acyclicity + antisymmetry: a domain made of mutualcoupling (
M ≥ 1motifs) now fails the gate instead of passing silently.The pattern-algebra basis (validated on #186): a mutual dyad
A↔Bis its owntranspose;
300is the maximal fixed point.tangle_ratio = Σ Mᵢ·tᵢ / 3measuresexactly that transpose-fixed mass.
cycle_ratiorejects the cyclic back-edge(
030C);tangle_ratiorejects mutual coupling — together they complete thehealth predicate.
What changed (all append-only, Lane A)
triads.py— puretangle_mass(census) -> float, M-weights aligned toTRIAD_ORDER.fingerprint.py—tangle_ratiocomputed property =maxover IMPORTS/CALLS layers (worst-layer breaches).drift.py— registeredtangle_ratioin the hygiene-constraint whitelist; the existing_hygiene_checkenforces it. No gate-logic change.patterns.yaml+ bundled ontology header —tangle_ratio: {max: 0.25}(kept parse-identical by the staleness pin). Ratchet-able per domain viahygiene_baseline(ci: enforce the drift-tolerance ratchet (values may only go down) #151).mcp_server.py—cgis_driftpayload exposestangle_ratioper domain/quotient. (No CLI column — the breach already surfaces viagate_failed+ the hygiene note.)PATTERNS_AND_TRIADS.md— documents the gate within the pattern-algebra grand structure.Threshold = 0.25, empirically grounded
Measured
tangle_ratio(intra-domain + quotient) on real repos:0.25clears coupled production code ~3× and cgis-self ~7×, while firing nearmesh — biased to the safe end for a new gate (avoid false-positive fatigue).
Test Plan
tangle_massunit table: DAG→0,300→1,201→2/3, empty→0, weight-alignment guard.tangle_ratioproperty: worst-layer max, empty/DAG→0.gate_failedwithtangle_ratioviolation; clean → not failed;hygiene_baselineratchet acknowledges a breach.tangle_ratio ∈ [0,1].gate_failed, 0 tangle breaches under the new gate.Spec:
docs/specs/2026-06-13-tangle-anti-pattern-design.md.Remaining #186 deliverables (separate PRs):
closure-gapcoupling metric,cgis_fractaltool.🤖 Generated with Claude Code