# CORRECTIONS — appended 2026-08-31 after decoding the live synapse file

I made claims in this document from reading source alone. I then decoded
`[internal module]` directly (label table + 20-byte records, per
`persistence/compact.rs:322`). Three of my claims were wrong.

## WRONG #1 — "restoring the recovered edges would mostly be refused by the fan-out cap"

`[internal module]` pushes straight into `layer.synapses` and
re-serialises the file. **It never calls `apply_ltp`, so the fan-out cap does not
apply to it at all.** The cap is a runtime guard on edge *creation*, not a
property of the file. Restoring works. I said the opposite.

## WRONG #2 — "the graph is at its ceiling, nothing new can be created"

Measured out-degree, 49,731 labels:

| | |
|---|---|
| labels with outgoing edges | 48,341 |
| **at or over the cap (15)** | **25,065 (51.9%)** |
| **under the cap — still have room** | **24,666** |
| **total free edge slots** | **189,542** |
| out-degree p50 / p75 / p90 / max | 15 / 16 / 19 / **293** |

Half the graph is frozen. The other half is not. And 189,542 free slots is
**three times** the 63,815 recovered edges — there is room for all of them even
through the runtime path. "At the ceiling" was true of the average and false of
the distribution.

(The max of 293 is a fossil: those edges were made when the lattice was ~359k
cells and the cap was 30. The cap never removes what it already allowed.)

## WRONG #3 — the count

I reported 605,674 synapses from the boot log. The file on disk holds
**613,276** across **49,731 labels**. It has grown ~7,600 since that boot.

---

# MEASURED STATE OF THE GRAPH (2026-08-31, decoded from disk)

**Weight** — min 0.0105, p25 **0.0792**, p50 0.3573, p75 1.0000, p99 5.0000
- **174,203 (28.4%) are pinned at MAX_WEIGHT 1.0** — saturated, cannot grow.
- Only **245** sit below 0.02. Nothing is at immediate risk of the prune line.
- p25 at 0.0792 against a death line of 0.01 is the Codex's standing warning:
  a quarter of the graph is roughly 90 minutes of live sweeps from deletion.

**Clock / idle** — `last_fire_tick` min 1815, p25 2970, p50 3529, max 4650.
- **Zero stamps of exactly 0.** The Codex recorded 281,557 zero-stamps (49.1%)
  on 2026-08-29. They are gone — the selective rebase worked. Every stamp on
  the graph now carries real information.
- Death line is `4650 - 2500 = tick 2150`. **1,401 synapses (0.2%) are already
  past it**, 230 of those under weight 0.05. That is the live exposure if
  `[config flag]` goes to `1` right now — small, and survivable.

**fire_count** — p50 13, p90 175, p99 2158, max 196,865.
- **140,641 are "hyper" (>50 fires)** with their ceiling lifted to 5.0. That is
  23% of the graph on the rich-get-richer track described in section 8.

---

# RECOVERY WORK DONE

`[internal module]` (63,815 pairs, already re-screened through the
tightened v9.10.838 gate) still carried assistant filler. Measured: **2,206 pairs
(3.5%)** had chat/tutor scaffolding on at least one side — `you're welcome`,
`I'd be happy to`, `as an AI`, `here's a tiny lesson`, `let's`, Q:/A: drills.
120 pairs had it on both sides.

That is a small fraction — the 838 gate did most of its job, and I should not
imply otherwise. But those 2,206 are exactly the class that produced the
"0 bridges" seeds in the log you asked about.

**Written:**
- `[internal module]` — **61,609 pairs**, filler removed
- `[internal module]` — the 2,206, kept for inspection

Nothing in `data/` that the engine reads has been modified.

---

# SYNAPTOGENESIS — FULL TRACE AUDIT
**Recorded 2026-08-31. Every function in the loop, read in source, with what it does to what.**

Live numbers used throughout, from the boot log: **40,093 cells**, **605,674 synapses**.

---

## THE CHAIN

```
run_synaptogenesis_loop            oracle_server.rs [internal service]   every 2s, forever
  └─ get_ungrounded_concepts       oracle_server.rs [internal service]   pick 1 seed
       └─ is_junk_concept_label    oracle_server.rs [internal service]   reject noise
       └─ SynapticLayer::strongest_from  synapse.rs        "has this cell any synapses?"
  └─ NeuralBus::query_multi_hop    synapse.rs:973          walk the graph 3 hops
       ├─ Universe::query_in_regions                       hop 0, geometric
       ├─ SynapticLayer::propagate  synapse.rs:339         hops 1..3, follow edges
       ├─ Universe::get_cell_by_label                      LINEAR SCAN, 40k cells
       └─ effective_score           synapse.rs             rank
  └─ SynapticLayer::record_co_firing  synapse.rs:279       Hebbian LTP
       └─ apply_ltp                 synapse.rs:799         the only thing that WRITES
```

---

## 1. `run_synaptogenesis_loop` — oracle_server.rs [internal service]

**Does:** sleeps 2s, builds one seed, queries, writes, prints, repeats. Forever.

**Kill switch:** `TRAINING_DISABLED.flag` file, or `[config flag]=0`. Checked
each pass; when set it sleeps 2s and does nothing else.

**The throttle is dead code.**
```rust
let max_boost: f32 = 0.0;   // "Disabled boost to prevent parallel crashes"
let p_scaled  = ...;        // biological plateau curve
let ease      = p_scaled * p_scaled * (3.0 - 2.0 * p_scaled);   // smoothstep
let throttle  = 1.0 + max_boost * ease;                          // == 1.0, always
let batch_size = (throttle.max(1.0).round() as usize).min(15);   // == 1, always
```
An entire smoothstep curve is computed and then multiplied by zero. `batch_size` is
permanently 1.

**→ THIS IS WHY IT ALWAYS SAYS `seed[0]` AND `Processing 1 parallel concepts`.**
There is only ever one seed. The plural is a leftover.

**`P` is dead too.** `p = synapses / (cells * 4)` clamped to 1.0.
`605,674 / (40,093 x 4) = 605,674 / 160,372 = ` **3.78** → clamped to **1.0000**.
It has been pinned at maximum for a long time and cannot move. It reports nothing.
(And even if it moved: `p > 0.80` sends `p_scaled` to `(1-p)/0.20`, so at P=1.0 the
curve evaluates to 0 anyway.)

**Locking:** seeds are processed in `chunks(2)` with a `sleep(250ms)` between chunks,
specifically so `[internal endpoint]` can get the universe read lock. With batch_size 1 there
is only ever one chunk — so **every cycle still pays the 250ms sleep** for a
concurrency problem it no longer has.

---

## 2. `get_ungrounded_concepts` — oracle_server.rs [internal service]

**Does:** builds a `Vec<usize>` of **all 40,093 indices**, shuffles it, then walks it
until it finds `batch_size` (=1) cells that pass the junk filter AND have zero synapses.

**Cost:** a 40,093-element allocation + full Fisher-Yates shuffle, **30 times a
minute**, to select one item. If most cells already have synapses, it also calls
`strongest_from` on each candidate as it walks — and `strongest_from` copies every
synapse of that label into a Vec and sorts it, just to ask "is this empty?"

**Fallback:** if this returns nothing, the loop picks a **random** cell instead. So the
system moves from "ground the unconnected" to "re-stir the connected" without saying so
in the log.

---

## 3. `is_junk_concept_label` — oracle_server.rs [internal service]

**Does:** rejects a label if it is <8 chars, fails `ingest_filter::judge_ingest`, ends
with `,` or `(`, starts with `(`, contains chat pollution markers (`language sample`,
`[mirror]`, `**leo:**`, `pred_err=`, ...), starts with an agent name, or contains
`": "` in the first 40 characters.

**The hole your seeds came through:** the colon rule matches `": "` — colon **plus a
space**. Your seed was:

> `Here's a tiny lesson for your AI studen[local path]"so`

The colon is followed by a **newline**, not a space. It passes. Same for
`The correct answer is based on understanding how prefixes mo` — that has no early
colon at all. Both are LLM answer text stored as cells, which is exactly what this
filter exists to keep out, and exactly what is being fed in.

---

## 4. `NeuralBus::query_multi_hop` — synapse.rs:973

Called as `query_multi_hop(&u, &sl, phi_g, seed, 15, &[], "", 3)` → `n=15`, `max_hops=3`.

**Does:**
- **Hop 0:** `universe.query_in_regions(text, n*2 = 30, ...)` — geometric/VSA retrieval.
- **Hops 1-3:** `synaptic_layer.propagate(frontier)` returns every post-synaptic partner
  of every frontier label, sorted by boost. Capped at `[config flag]` (default 64)
  per hop; next frontier truncated to `[config flag]` (default 24). Signal decays
  `0.7^hop`; anything under 0.05 after decay is dropped.
- Each surviving label needs `universe.get_cell_by_label(&label)`, which is a **linear
  scan over all 40,093 cells** (`universe.rs:2918`). This is the documented cause of the
  141-second `[internal endpoint]` (v9.10.845 note in-file).
- Dedupe by label keeping max score, sort descending, then:

```rust
merged.truncate(n + max_hops * 2);   // 15 + 3*2 = 21
```

**→ THE RESULT IS HARD-CAPPED AT 21 HITS.**

---

## 5. `record_co_firing` — synapse.rs:279

**Does:** `if labels.len() < 2 { return; }`, then for **every ordered pair** (i,j), i!=j,
calls `apply_ltp`.

```rust
chi_gate = (1.0 - chi*0.8).max(0.05)                        // chi=0.1 → 0.92
ltp_gain = BASE_LTP * (1 + dopamine*0.8) * (1 + phi_g*0.5) * chi_gate
         = 0.035    * (1 + 0.8*0.8)      * (1 + phi_g*0.5) * 0.92
         ≈ 0.0528 * (1 + phi_g*0.5)      ≈ 0.053 – 0.079
```

**The tick is 0.** This loop passes `tick = 0`. Since v9.10.565 the line is
`self.tick = self.tick.max(tick)`, so passing 0 can no longer rewind the clock — but it
also **does not advance it**. Only `ltd_sweep_inner` does `self.tick += 1`.

---

## 6. `apply_ltp` — synapse.rs:799 — THE ONLY FUNCTION THAT WRITES

Two paths:

**A. Edge already exists** → `weight = (weight + gain).min(max_w)`, `fire_count += 1`,
`last_fire_tick = tick`, `total_ltp += 1`. `max_w` is `MAX_WEIGHT = 1.0`, or **5.0**
once `fire_count > 50` ("hyper-synapse").

**B. Edge does not exist** → checks the fan-out limit **first**:

```rust
if fan_out >= dynamic_fan_out(lattice_size) { return; }   // silent, no-op

pub fn dynamic_fan_out(n) -> usize { (0.075*n).powf(1/3).ceil().max(8) }
```

For your lattice: `(0.075 * 40,093)^(1/3) = 3007^(1/3) = 14.44` → ceil → **15**.

### THE CEILING — the single most important number in this audit

**Max possible synapses = 40,093 cells x 15 fan-out = 601,395.**
**You have 605,674.** (Above the ceiling because the cap is recomputed as the lattice
grows; edges made when it was 16 stay.)

**Average out-degree = 605,674 / 40,093 = 15.1.** The graph is **at its fan-out
ceiling.** For most labels, path B returns immediately and **creates nothing**.

New-edge path also has a `latent_traces` hash check that multiplies gain by **15x**
for a pre/post pair that existed before and was pruned — a relearning bonus.

---

## 7. What the log lines actually mean

### `Logistic Throttle Velocity: 1.00x (P=1.0000) | Processing 1 parallel concepts...`
Two dead constants and a hard-coded 1. Zero information.

### `seed[0]: "…"` cut off mid-word
`s.chars().take(60)` — 60-char truncation, no ellipsis. The **full** label is sent to
the query (a v9.10.x note says truncating to 60 broke resonance and produced 0 hits).
The blank line you see is a literal `\n` inside the cell label, printed raw.

### `Established 420 new geometric bridges`
```rust
let n = hits.len();
let pair_bridges = n * (n - 1);
total_wired += pair_bridges;
```
`hits` is capped at 21 by `merged.truncate(21)`. **21 x 20 = 420.**

**420 is a constant.** It means "the retrieval returned its maximum." It will print 420
every single time the seed is not isolated. It is not a count of anything created.

Worse, on three counts:
1. **Nothing is measured.** `record_co_firing` is called *once* and returns `()`.
   `total_wired` is a formula computed *before* any write, never compared to reality.
2. **"new" is unverified.** Most of those 420 pairs already exist; they got a weight
   bump, not a creation. `apply_ltp` knows which path it took and tells nobody.
3. **Many cannot be created at all** — fan-out is full at 15 (section 6), so path B
   silently returns.

### `(0 seed(s) with no multi-hop neighbors skipped)`
Count of seeds whose hit list had `len() <= 1`. Zero means the one seed found neighbors.

### `Established 0 new geometric bridges (1 seed(s) had <2 multi-hop hits — seed may be isolated or too noisy)`
The one seed came back with 0 or 1 hit. `record_co_firing` bailed at
`if labels.len() < 2 { return; }`. Nothing ran. Given the seeds are ungrounded cells —
cells with **no synapses by definition** — hops 1-3 have nothing to follow, so the
result depends entirely on hop 0 geometric retrieval finding something.

---

## 8. WHAT IT IS DOING TO THE SYSTEM

**Every 2 seconds, permanently:**
- allocate + shuffle a 40,093-element vector
- run a 3-hop graph walk, including linear scans over 40,093 cells per candidate
- sleep 250ms for a lock contention case that no longer exists
- bump the weight of up to 420 already-existing edges toward their cap

**The direction is one-way.** `ltd_mode()` defaults to **`"dry"`** (`[config flag]`
unset). Dry analyses and **mutates nothing**. So nothing is weakened and nothing is
pruned unless you explicitly set `[config flag]=1`. Weights only go up.

**Saturation.** At `ltp_gain ≈ 0.053-0.079` against `MAX_WEIGHT = 1.0`, a synapse
saturates after **~13-19 co-firings**. At 30 cycles/minute against a fixed retrieval
set, everything the retrieval keeps returning is already pinned at 1.0. Past that,
`fire_count > 50` lifts the ceiling to 5.0 for the ones that fire most — so the
already-strongest edges are the only ones that can still grow. **That is a rich-get-
richer ratchet with no counterweight.**

**No new information enters.** The set being strengthened is the set the retrieval
already returns. This reinforces what the graph already believes. It is not learning
from the world; it is deepening a groove.

---

## 9. WHAT TO CHANGE (ranked, cheapest first)

1. **Make the counter measure instead of compute.** Have `apply_ltp` return an enum
   (`Created` / `Strengthened` / `RefusedFanOutFull` / `Saturated`) and print the four
   counts. One afternoon. Turns the log from decoration into an instrument. **Do this
   before changing behaviour** — otherwise there is no way to tell if a change helped.
2. **Log mean weight delta per batch.** If it is ~0, the batch did nothing and you can
   see it.
3. **Drop the 250ms sleep when `seeds.len() <= 2`.** It is paying for a chunking
   strategy that no longer chunks.
4. **Cache the ungrounded set** instead of shuffling 40,093 indices 30x/minute.
   Recompute on a slow tick.
5. **Fix the junk filter's colon rule** — match `:` followed by whitespace, not `": "`.
   Both of the seeds in the reported log would have been rejected.
6. **Decide on LTD.** With `[config flag]` unset there is no forgetting at all. Run
   `dry` first and read the report before going live.
7. **Then decide about `max_boost`.** Raising it above 0 is the only way `batch_size`
   ever exceeds 1 — but the comment says parallel crashed. That is worth reproducing
   before re-enabling.

**Nothing here says the mechanism is wrong.** It says the log is not reporting it, the
graph is at its structural ceiling, and there is no counter-force. Those are three
separate problems and only the first one is cheap.
