diff --git a/docs/review/device-qa/README.md b/docs/review/device-qa/README.md index f856416..c9e3a5b 100644 --- a/docs/review/device-qa/README.md +++ b/docs/review/device-qa/README.md @@ -316,5 +316,15 @@ Each note records one fact about bytes or draw calls already on main. None of th | [civic-slots-2026-11-05.md](civic-slots-2026-11-05.md) | The civic slots | | [detection-shapes-2026-11-05.md](detection-shapes-2026-11-05.md) | The detection shapes | | [tutorial-flags-2026-11-05.md](tutorial-flags-2026-11-05.md) | The tutorial flags this tick | +| [sha-padding-2026-11-05.md](sha-padding-2026-11-05.md) | The hash padding | +| [bundle-load-2026-11-05.md](bundle-load-2026-11-05.md) | The bundled lookup | +| [telemetry-sample-2026-11-05.md](telemetry-sample-2026-11-05.md) | The telemetry sample | +| [q8-offset-2026-11-05.md](q8-offset-2026-11-05.md) | The offset along a direction | +| [live-solids-2026-11-05.md](live-solids-2026-11-05.md) | The solids this tick | +| [box-edges-2026-11-05.md](box-edges-2026-11-05.md) | The box edges | +| [vfx-budgets-2026-11-05.md](vfx-budgets-2026-11-05.md) | The effect budgets | +| [vfx-project-2026-11-05.md](vfx-project-2026-11-05.md) | The effect projection | +| [probe-sidestep-2026-11-05.md](probe-sidestep-2026-11-05.md) | The probe's sidestep | +| [probe-grid-2026-11-05.md](probe-grid-2026-11-05.md) | The probe's grid | Next session: follow [retest-checklist.md](retest-checklist.md) (D-105 allows device checks on any build). diff --git a/docs/review/device-qa/box-edges-2026-11-05.md b/docs/review/device-qa/box-edges-2026-11-05.md new file mode 100644 index 0000000..7ecf50a --- /dev/null +++ b/docs/review/device-qa/box-edges-2026-11-05.md @@ -0,0 +1,9 @@ +# The box edges + +Read 2026-11-05 from `AABB.contains` and `Bounds.aabb` on `ef68565`. No edge was moved. Penetration stays in [circle-slide-2026-11-04.md](circle-slide-2026-11-04.md). The extraction box stays in [extraction-presence-2026-11-04.md](extraction-presence-2026-11-04.md). The scale stays in [q8-scale-2026-11-04.md](q8-scale-2026-11-04.md). + +`minX` is `center.x - halfSize.x`, and the other three edges are the matching sum or difference. `contains` of an integer point is true when each coordinate is at least the minimum and at most the maximum. `contains` of a Q8 point compares the raw value with `Int64(edge) * Q8.scale` on those same four sides. + +`Bounds.aabb` sets the center to `(min + max) / 2` and the half-size to `(max - min) / 2`, on each axis. + +This does not move an edge. diff --git a/docs/review/device-qa/bundle-load-2026-11-05.md b/docs/review/device-qa/bundle-load-2026-11-05.md new file mode 100644 index 0000000..1b5d203 --- /dev/null +++ b/docs/review/device-qa/bundle-load-2026-11-05.md @@ -0,0 +1,11 @@ +# The bundled lookup + +Read 2026-11-05 from `BundledResource.data` on `ef68565`. No asset was admitted. Intake stays in [asset-intake-2026-11-05.md](asset-intake-2026-11-05.md). Reachable ids stay in [bundle-filter-2026-11-05.md](bundle-filter-2026-11-05.md). Catalog refusal stays in [catalog-gate-2026-11-05.md](catalog-gate-2026-11-05.md). + +`data` loads `Bundle.module` for `name` with extension `json` in `subdirectory`. When that URL is missing, the path is `#filePath` two directories up, then `Resources//.json`. Either path is read with `try!`. + +`RuntimeAssetBundle.url(forFile:)` looks in subdirectory `RuntimeAssets` with no extension. The fallback is `Resources/RuntimeAssets/`. A fallback path that `FileManager` reports as absent returns nil. + +`SpecBundle.contract` uses subdirectory `contracts`. `SpecBundle.fixture` uses subdirectory `fixtures`. + +This does not admit an asset. diff --git a/docs/review/device-qa/live-solids-2026-11-05.md b/docs/review/device-qa/live-solids-2026-11-05.md new file mode 100644 index 0000000..717945b --- /dev/null +++ b/docs/review/device-qa/live-solids-2026-11-05.md @@ -0,0 +1,7 @@ +# The solids this tick + +Read 2026-11-05 from `WorldState.liveSolids` on `ef68565`. No solid was changed. The slide stays in [circle-slide-2026-11-04.md](circle-slide-2026-11-04.md). The mount draw stays in [mount-solids-2026-11-04.md](mount-solids-2026-11-04.md). The reachability boxes stay in [arena-reach-2026-11-05.md](arena-reach-2026-11-05.md). + +The list starts as `arena.solidsForCollision`, which is each permanent solid's id and aabb. Every camera then appends `mountSolidId` and a square centered on the camera position whose half-size is `mountCollisionRadius` on both axes. A destroyed camera stays in that loop. Each gate with `closed` appends its id and its box. + +This does not change a solid. diff --git a/docs/review/device-qa/probe-grid-2026-11-05.md b/docs/review/device-qa/probe-grid-2026-11-05.md new file mode 100644 index 0000000..aa024cd --- /dev/null +++ b/docs/review/device-qa/probe-grid-2026-11-05.md @@ -0,0 +1,11 @@ +# The probe's grid + +Read 2026-11-05 from `ProbePilot.plan` and `breadthFirst` on `ef68565`. No route was changed. When `decide` calls `navigate` stays in [probe-order-2026-11-05.md](probe-order-2026-11-05.md). The step and the walkable test stay in [arena-reach-2026-11-05.md](arena-reach-2026-11-05.md). The line of fire stays in [line-of-fire-2026-11-04.md](line-of-fire-2026-11-04.md). + +The bounds are `arena.boundsUnits.aabb`. The column count is `(maxX - minX) / step + 1`, and the row count is the same on Y. `step` is `ArenaReachability.gridStep`. A cell point is the minimum plus the index times that step. `cell` adds half a step, divides, and clamps to the grid. + +`rebuildGrid` marks a cell walkable when `isWalkable` at radius 22. `neighbours` are the eight adjacent cells that are walkable and outside a hazard. A diagonal also needs both side cells walkable. `nearestWalkable` searches chebyshev radii 0 through 7 and returns on the first radius that contains a walkable cell. A clear line of fire beats a blocked one. The same clarity keeps the smaller distance. An equal distance keeps the earlier cell, scanned in increasing y then x. No cell in those radii returns nil. + +`plan` takes `nearestWalkable` of the start and of the goal, and returns an empty path when either is nil. A stealth profile whose field grid has one entry per cell uses the bucket queue. Every other call uses `breadthFirst`. In the bucket queue a step into a field costs 12 and any other step costs 1. A neighbour is written when the new cost is strictly lower. The search stops when the goal is taken from its bucket. `breadthFirst` records the first visit. Both rebuild the path from the goal back to the start, leave the start off, and reverse it. An unreached goal returns an empty path. + +This does not play a release build. diff --git a/docs/review/device-qa/probe-sidestep-2026-11-05.md b/docs/review/device-qa/probe-sidestep-2026-11-05.md new file mode 100644 index 0000000..719c369 --- /dev/null +++ b/docs/review/device-qa/probe-sidestep-2026-11-05.md @@ -0,0 +1,15 @@ +# The probe's sidestep + +Read 2026-11-05 from `ProbePilot.flee`, `evasion`, `steerOpen`, `escape`, and `orbit` on `ef68565`. No route was changed. When `decide` calls these stays in [probe-order-2026-11-05.md](probe-order-2026-11-05.md). The player radius stays in [player-movement-2026-11-04.md](player-movement-2026-11-04.md). `headingUnit` stays in [millidegree-2026-11-04.md](millidegree-2026-11-04.md). + +The eight headings are every neighbour offset, including the diagonals, multiplied by 8. `isOpen` is walkable at the player radius and outside every hazard. A hazard contains a point when the stored distance is below its radius. + +`flee` probes each heading times 3. An open probe scores the distance from the threat minus one tenth of the distance to the objective. A strictly higher score replaces the best. No open probe returns the vector from the threat back to the position. `steerOpen` probes each heading times 3 and keeps the open probe with the strictly higher dot against the requested direction. None returns an empty command. `escape` probes each heading times 2 through `ArenaReachability.isWalkable` at the player radius and keeps the probe with the strictly higher distance from the hazard. None returns the vector from the hazard back to the position. + +`orbit` tries twice. The step is `(-(position.y - point.y) * orbitSign, (position.x - point.x) * orbitSign)`, scaled by 24 over `max(1, Int(sqrt))`. An open probe returns that vector. Otherwise the sign flips. Two failures return an empty command. + +`evasion` skips an `emitterField`. A telegraph whose heading length is 0 is skipped. The player counts when the along distance is above `-body` and below the shape range plus `body`, and the absolute across distance is below the half-width plus `body`. `body` is the player radius plus 10. A lane's half-width is half its width. Any other shape uses `max(0, along) * tan` of the half-angle in radians, and that angle is capped at 1.4 before the tangent. The side is positive when across is at least 0. A shape with `remainingTicks` at most 3 sets urgent. A hostile bolt whose speed is above 0 counts when the closing parameter runs from 0 through 40 and the miss is below the bolt radius plus `body`. The added step is that bolt's perpendicular, on the side of the cross product. A parameter of at most 6 sets urgent. A zero sum returns nil. + +`vector` of a zero length returns an empty command. Otherwise each component is the direction times `32767 / magnitude`, rounded, and clamped to ±32767. Dodge is false. + +This does not play a release build. diff --git a/docs/review/device-qa/q8-offset-2026-11-05.md b/docs/review/device-qa/q8-offset-2026-11-05.md new file mode 100644 index 0000000..4e64d0e --- /dev/null +++ b/docs/review/device-qa/q8-offset-2026-11-05.md @@ -0,0 +1,9 @@ +# The offset along a direction + +Read 2026-11-05 from `VecQ8.offset` on `ef68565`. No scale was changed. The scale and the circle test stay in [q8-scale-2026-11-04.md](q8-scale-2026-11-04.md). The roots stay in [integer-roots-2026-11-05.md](integer-roots-2026-11-05.md). The perpendicular stays in [daemon-query-2026-11-04.md](daemon-query-2026-11-04.md). + +`Q8(units:)` multiplies by `Q8.scale`. `squared` is the raw value times itself. `lengthSquaredRaw` adds the two squared raw components. `distanceSquared` adds the squared raw deltas. `VecI.asQ8` builds `VecQ8(unitsX:unitsY:)`. `Circle.radiusQ8` is `Q8(units: radiusUnits)`. + +`offset` takes `isqrt` of `lengthSquaredRaw`. A magnitude of 0 returns the same vector. Otherwise each component adds `mulDivHalfAway` of that direction component, `Int64(units) * Q8.scale`, and the magnitude. + +This does not change a scale. diff --git a/docs/review/device-qa/sha-padding-2026-11-05.md b/docs/review/device-qa/sha-padding-2026-11-05.md new file mode 100644 index 0000000..234846c --- /dev/null +++ b/docs/review/device-qa/sha-padding-2026-11-05.md @@ -0,0 +1,13 @@ +# The hash padding + +Read 2026-11-05 from `SHA256.hash` on `ef68565`. No digest was changed. `sha256Hex` stays in [canonical-json-2026-11-04.md](canonical-json-2026-11-04.md). The state object stays in [state-digest-2026-11-04.md](state-digest-2026-11-04.md). + +`emptyDigest` is `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`. + +`hash` appends `0x80`, then zero bytes until the length modulo 64 is 56, then eight big-endian bytes of `UInt64(data.count) * 8`. Each 64-byte chunk loads sixteen big-endian words. A later schedule word is `w[i - 16] &+ s0 &+ w[i - 7] &+ s1`. `s0` rotates the word fifteen back by 7 and by 18, then shifts it right by 3. `s1` rotates the word two back by 17 and by 19, then shifts it right by 10. + +Each of the 64 rounds rotates `e` by 6, 11, and 25, and `a` by 2, 13, and 22. Choice is `(e & f) ^ (~e & g)`. Majority is `(a & b) ^ (a & c) ^ (b & c)`. The adds are wrapping. The digest is those eight words written big-endian, 32 bytes. + +`hex` of bytes formats that digest with `%02x` and joins it. `hex` of a string hashes its UTF-8 bytes. `rotateRight` is the right shift combined with the complementary left shift. + +This does not change a digest. diff --git a/docs/review/device-qa/telemetry-sample-2026-11-05.md b/docs/review/device-qa/telemetry-sample-2026-11-05.md new file mode 100644 index 0000000..9a192c1 --- /dev/null +++ b/docs/review/device-qa/telemetry-sample-2026-11-05.md @@ -0,0 +1,11 @@ +# The telemetry sample + +Read 2026-11-05 from `RunTelemetrySnapshot` on `ef68565`. No ceiling was changed. The peak store stays in [device-sample-2026-11-05.md](device-sample-2026-11-05.md). Frame time stays in [frame-times-2026-11-05.md](frame-times-2026-11-05.md). The finished-run predicate stays in [terminal-seal-2026-11-04.md](terminal-seal-2026-11-04.md). The civic slots stay in [civic-slots-2026-11-05.md](civic-slots-2026-11-05.md). Destroyed cameras stay in [camera-receipt-2026-11-05.md](camera-receipt-2026-11-05.md). The density scenarios stay in [peak-density-2026-11-05.md](peak-density-2026-11-05.md). + +The schema is `run-telemetry-001`. `camerasLive` counts cameras whose integrity is above 0. `enemiesLive` counts enemies that are alive. `projectilesLive` counts projectiles that are alive. `minesLive` counts mines whose `lifeRemaining` is above 0. `civicPoolLive` is `liveCount`. `civicPoolCapacity` is `slots.count`. `allocatorNext` is `nextRaw`. `totalLive` is the sum named in the device-sample note. + +Exposure stores the value, the peak, the detection state, and `lockdownEntered`. Damage stores player integrity, `damageTaken`, and `combat.damageDealt`. The outcome sample stores `destructions.count`, `networkBlackout`, whether extraction is armed, and `bossDefeated`. Its `terminal` flag is `outcome != .playing`. + +`canonical` writes the tick and `allocatorNext` as unsigned, the counts as integers, the flags as bools, and the schema, the outcome, and the detection state as strings. + +This does not settle a ceiling. diff --git a/docs/review/device-qa/vfx-budgets-2026-11-05.md b/docs/review/device-qa/vfx-budgets-2026-11-05.md new file mode 100644 index 0000000..38b00c9 --- /dev/null +++ b/docs/review/device-qa/vfx-budgets-2026-11-05.md @@ -0,0 +1,13 @@ +# The effect budgets + +Read 2026-11-05 from `ProceduralVFXLoader.decode` on `ef68565`. No budget was changed. The hit-stop caps, the coalesce window, the emitter ceiling, and the forbidden flash stay in [hit-stop-2026-11-04.md](hit-stop-2026-11-04.md). The integer cast that skips `JSONScalar.isBool` stays in [content-bands-2026-11-05.md](content-bands-2026-11-05.md). + +The schema string must equal `ContractVersions.proceduralVFX`. `visualVersion` must be `visual-civic-seam-001`. `atlas` must be `combat_vfx.atlas`. The emitter count must be an int above 0 and within the ceiling in the hit-stop note. The two hit-stop fields, the coalesce field, and `forbidFullScreenWhiteFlash` must match that note, and `forbidFullScreenWhiteFlash` must be the JSON bool `true`. A miss on that guard throws `invalidJSON`. A wrong schema or visual version throws `schemaVersion`. + +An unexpected catalog, recipe, or variant key throws `unexpectedKey` of the first sorted extra key. A missing required key throws `missingKey`. A repeated recipe id throws `missingRecipe` of that id. A required id that never appears throws `missingRecipe` of that id. The required ids, in order, are `cameraAcquire`, `exposureThreshold`, `playerHit`, `enemyHit`, `enemyDefeat`, `ghostStep`, `ricochet`, `lockdown`, `captainTelegraph`, `extraction`, `cameraDestroyed`, `networkBlackout`, `bossPhaseBreak`, `heatReinforcements`, and `enemyAlerted`. + +`intValue` returns `as? Int`, then `NSNumber.intValue`, then nil. An empty id, shape, or event list, or a variant that is not an object, throws `invalidJSON`. `poolSize` must be at least 1. An event name that is not an `EventType` raw value throws `budget`. A variant needs a non-empty language, a particle count of at least 0, a lifetime above 0, a hit-stop of at least 0, and bools for shake and flash. Either variant with `fullScreenFlash` true throws `fullScreenFlash`. A reduced variant with `screenShake` true throws `reducedShake`. A hit-stop above the recipe's cap throws `budget`. The captain-class ids are `captainTelegraph` and `bossPhaseBreak`. + +`validateBudgets` throws `budget` when `enemyHit`'s default lifetime is above 120, when `enemyDefeat`'s default particle count is outside 4 through 8, its default lifetime is above 350, or its reduced particle count is above 2, when `ghostStep`'s default lifetime is above 300 or its reduced particle count is above 1, or when either `networkBlackout` lifetime is not 1500. Every other id skips that switch. + +This does not change a budget. diff --git a/docs/review/device-qa/vfx-project-2026-11-05.md b/docs/review/device-qa/vfx-project-2026-11-05.md new file mode 100644 index 0000000..49543b6 --- /dev/null +++ b/docs/review/device-qa/vfx-project-2026-11-05.md @@ -0,0 +1,17 @@ +# The effect projection + +Read 2026-11-05 from `VFXProjector.project` on `ef68565`. No rank was changed. The renderer pass and the rank table stay in [vfx-pool-2026-11-04.md](vfx-pool-2026-11-04.md). The coalesce window stays in [hit-stop-2026-11-04.md](hit-stop-2026-11-04.md). The reinforcement count stays in [heat-caption-2026-11-04.md](heat-caption-2026-11-04.md). + +Stored damage ticks stay only while `tick` is at least the stored tick and the difference is below `playerDamageCoalesceTicks`. `reduced` is `reducedMotion` or `reducedFlash`. + +For each event, each recipe whose `eventTypes` contain the event type is considered. `playerHit` skips a key already stored. The key is `payload["contributors"]?.serialize()`, otherwise the `sourceEntityId` string, otherwise `"unknown"`. A kept hit stores the tick and sets `lastPlayerDamageTick`. `cameraAcquire` continues only when detection rose from hidden to observed. `exposureThreshold` continues only when the after state is tracked, hunted, or lockdown. The `ricochet` recipe is skipped in this loop. `heatReinforcements` continues only when `context.heatReinforcements` is above 0. `bossPhaseBreak` continues only when the payload has both `before` and `after` strings, and the label is `phaseTitle` of `after`. The comment on that guard says an activation publishes `bossPhaseChanged` with no `before`, and that arrival is the Captain. + +`present` copies the reduced or default variant's language, particle count, lifetime, hit-stop, and shake, plus the recipe shape, `primaryEntityId`, the sequence, the payload as evidence, and `reduced`. The sequence increments after each appended presentation. `networkBlackout` sets the label to `NETWORK BLACKOUT` and sets `cascade` from `context.cameraIds` over that presentation's lifetime. + +After the events, projectile hits are grouped by `primaryEntityId`. For each id in sorted order, the group must contain exactly two hits, one `civicPulse` and one `ricochet`, both `projectileId` strings equal to `id.decimalString`, two different `targetEntityId` strings, and both impact points. An impact point is `impactXQ8` and `impactYQ8` as integers, built with `Q8(raw:)`. The ricochet recipe is then presented from the civic-pulse event, with `impactPoints` of those two points. + +`VFXProjector.steal` runs while the count is above `maxConcurrentEmitters`. It removes the presentation whose rank is the maximum and, among that rank, the lowest sequence. An equal sequence keeps the earlier presentation. An id missing from the table has rank 9. `phaseTitle` starts a new word on an uppercase character when the current word is not empty, then uppercases each word and joins them with a single space. `cascade` unique-sorts camera ids by `raw`. An empty list returns an empty list. The offset is `lifetimeMs * index / ordered.count`. `reset` replaces the projector with a new one. + +Nothing in the projection context is read back into the simulation. + +This does not change a rank.