Compare commits

...

2 commits

Author SHA1 Message Date
alex
3bfde5a70d plan: pick the replace unit by its write set, not its name (lane CR's lesson) 2026-09-09 10:39:09 -04:00
alex
d12459d1e1 nav: lane DN, the instrument -- what the classifier hook declares and where each input comes from
Names the six regions (four result, two guard) that are the classifier's whole
output surface, and then names every predicate the decision consumes next to
which side computed it -- because the module takes those predicates as given, so
reading them off the live objects is its interface rather than a shortcut. Also
records that the node-graph indirect edge path-solver.md could not resolve
statically is resolved DYNAMICALLY here, by making the same call from the hook.
2026-09-09 10:32:02 -04:00
2 changed files with 66 additions and 0 deletions

View file

@ -148,6 +148,15 @@ it fails if a corpus test skips.
### Track 1 — displacement: `compared → replaced`, then `modelled → compared`
**Pick the replace unit by its WRITE SET, not by its name.** Lane CR's failure is the rule's
source: our code replaced `ProcessResearch` and produced **all 13** tech-tree leaves the turn moves
— the model was right — and the oracle still missed by 16, every one written by
`OnTechResearched`, a neighbouring function. A replace boundary that splits a write set cannot
pass an oracle no matter how correct the model is. So before attempting one, enumerate the leaves
the turn moves in that area and ask which functions write them; if the answer is more than one,
the unit is the union or the attempt is wasted. The compare mode's guard spans already name the
write set — CR's five spans predicted exactly the five fields the oracle later billed.
Only `replaced` means the original's code did not run. The bar, written once: an un-instrumented
oracle first (`certified-pairs.md` format), then the same procedure with the hook in `replace`
mode and the **same hashes**, with a count from the hook proving the path executed (rule 1).

View file

@ -130,3 +130,60 @@ from that readout rather than hoped for.
* **Three input gaps, counted per call rather than argued away:** the intercept solve for a
destination fleet already on a node leg (`intercept_gap`), the node bore (`bore_arm`), and the
unread friendliness test `FUN_00817890` on the point↔system arms (`friendly_recon`).
---
## 2. The instrument
Engine branch `wip/dn`, commit `17d635b`, two new hook descriptors in
`src/shim/hooks/path_classify.{h,cpp}` over a pure, host-buildable adapter in
`src/shim/hooks/path_inputs.{h,cpp}`.
### 2.1 What is declared
| region | kind | what |
|---|---|---|
| **return value** | result | the waypoint kind, diffed by the template |
| `range` | result | `*rangeInOut` — an IN parameter despite its name (`PathSolver` does the draw-down itself), declared so that the *original* not writing it is checked rather than assumed |
| `flags` | result | `*flagsOut` |
| `route` | result | `routeOut+4..+0xf` — `nrp`, `nrf`, `nrt`, the only three words the original writes |
| `route_obj` | **guard** | the whole 0x10-byte `NodeRoute`, so a vptr write would show |
| `fleet` | **guard** | the whole 0x120-byte `StarFleet`. The classifier is supposed to be pure with respect to the moving fleet; this is the region that would say otherwise |
That is the function's entire output surface. It returns a kind, ORs a flag word and fills a
route; there is nothing else to declare.
### 2.2 Where the inputs come from, one by one
The module takes its predicates as **given** — `FleetState` holds `anyShipGrounded`,
`canBoreNodeLines`, `tankCapacity` and the rest as plain booleans and scalars — so reading them
off the live objects is not a shortcut, it is the module's own interface. Named here so a
reader can see which side computed what:
| input | source | ours or theirs |
|---|---|---|
| species, ship count, gate cost, gate traffic used/capacity, `CstR` | field reads | facts |
| endpoint kind / id / position / system index | field reads | facts |
| "we have a gate here" | `(GFlags >> PlyrIdx) & 1`, computed in the hook | ours |
| point visible / known, and the species-4 bypass | the two masks at `+0x8c`/`+0x90`, in the hook | ours |
| "the destination fleet is on a node leg" | its front waypoint's type, in the hook | ours |
| tank capacity | `StarFleet_MinTankCapacity` | **theirs** (a pure reader) |
| any ship grounded | `StarFleet_AnyShipGroundedByDamage` | **theirs** |
| pending action mask | `StarFleet_PendingShipActionMask` | **theirs**; the 0x100 split into `0x800` vs `0x001` is ours |
| can bore | `StarFleet_HasFlagShips(fleet, 0x20000, 0)` | **theirs** |
| the single-hop node lookup | `NodeGraph_FindNodeLine`, **called before the original runs** | **theirs** — so `nrp` on an existing line is an input, not a result |
| the drive type | recorded from `StarFleet_GetDriveType` but **not fed to `ours`** | ours computes it from species; theirs is the check |
The node graph itself comes through `(fleet->galaxy(+0x10))->vft[1]()` — the indirect edge
`path-solver.md` §9 lists as *"the single most important one"* of three it could not resolve
statically. This lane resolves it **dynamically** instead, by making the same call from the
hook and validating the object before using it. That is recorded as
`Galaxy_vft_off_GetNodeGraph` in `ghidra/addresses.d/dn.json`.
### 2.3 Why `BoreLine` can never fire from our side
`ClassifyLeg`'s node arm *writes* when it bores: `FUN_006e4de0` creates a node line in the live
graph. Our `NodeGraph::BoreLine` therefore returns false unconditionally and sets a flag, and
the descriptor reports the call as reaching an uncompared arm. Two consequences worth stating:
a compare in which the bore arm fires is **not** evidence either way, and a `replace` mode on
this hook is refused outright (it would also have to solve an intercept it cannot).