DRIFTLANDS co-op survival on drifting tides — studio progress

Architecture Review Report — 2026-09-07

Mode: /architecture-review (full; incremental over architecture-review-2026-09-01.md; RTM built — stories and tests now exist) · Verdict: CONCERNS
Engine: Unity 6.3 LTS (6000.3.18f1) · URP 17.3.0 2D (Render Graph) · Shader Graph 17.3.0 · Input System 1.20.0 · Addressables 2.9.1 · Newtonsoft 3.2.2 · C# 9 / .NET Standard 2.1 · NGO not installed (Gate 0)
Loaded: 5 GDDs (+ systems-index.md, game-concept.md) · 10 ADRs (all Accepted) · tr-registry.yaml (111 IDs) · traceability-index.md · control-manifest.md · docs/registry/architecture.yaml (49 stances) · engine reference (VERSION, breaking-changes, deprecated-apis, current-best-practices, modules) · 87 story files · 118 test files
Method: the prior review's resolutions re-verified against source, not the changelog; ADR diffs since dc7c1b0 read in full; an independent cross-ADR + implementation-deviation pass (19 rulings, 8 conflicts, 22 cleared items) and an engine-specialist audit (16 findings), both cross-checked by the reviewer against the files they cite. Every "unverified" item from both passes is carried in Appendix D rather than silently dropped.


Prior review's items — verified against source

Item State Evidence
E14/E15 — Scope = Global, FindPropertyIndex == -1✅ appliedadr-0013 §1 (4 occurrences), V1 line 254; V1 spike PASSED 2026-09-03 on 6000.3.18f1 / URP 17.3.0 (prototypes/tide-tint-v1-spike-2026-09-03/REPORT.md)
F1 — UI atlas separate, ≈ +2 draw calls✅ appliedadr-0014 §9 (line 213), Performance (365), Amendment History (461)
F2 — SocketId unwrap stated✅ appliedadr-0013 §5; Tide/ITideClock.cs:32-36 documents the exception on the code side
systems-index duplicate row 15✅ fixedrows renumbered 16–22 (commit 3a9842d)
E10 — DEPRECATED — DO NOT COPY banners on modules/rendering.md, modules/ui.md❌ still absent — third review running0 occurrences in either file (networking.md, input.md have theirs). rendering.md:60-68 still shows the pre-Render-Graph AddRenderPasses(ScriptableRenderer, ref RenderingData) signature; ui.md:236-260 still teaches TextMeshProUGUI + EventSystem, both project-forbidden. No shipped code copied from either.
ADR-0015 per-frame ms budget❌ still unregistered§Performance Implications is three prose bullets (CPU / Memory / Network) with no ms figure and no allocation gate — the only Accepted ADR whose Performance section carries no number a test can fail against

Pre-gate checklist (Glob-checked): tests/unit/ ✅ · tests/integration/ ✅ · .github/workflows/tests.yml ✅ · design/accessibility-requirements.md ✅ · design/ux/interaction-patterns.md ✅.


Traceability Summary

No GDD technical requirement changed since 2026-09-01. game-concept.md's edits are the Gate-0 re-affirmation prose; systems-index.md was renumbered. No ADR was added. Coverage is therefore unchanged: 73 ✅ / 26 ⚠️ / 12 ❌ of 111 (66 %). Foundation and Presentation are gap-free; all 12 gaps route to ADR-0008/0009/0010/0011/0012 or the TR-cam-002 implementation deferral. tr-registry.yaml is unmodified — no new IDs, no rewording, no deprecations.

Requirements Traceability Matrix — new this review

Full chain GDD → ADR → Story → Test, every stated evidence path checked on disk (docs/architecture/requirements-traceability.md, 111 rows).

Status Count %
COVERED — ADR + Done story + existing test file6155 %
NO STORY — ADR exists, requirement not yet storied3834 %
NO ADR — architectural gap (the 12 ❌ minus 2 already storied + tested)109 %
BLOCKED (Gate 0) — storied, blocked22 %

Per layer: Foundation 31/39 (79 %) · Presentation 6/8 (75 %) · Core 21/38 (55 %) · Feature 3/26 (12 %).

Findings from building it:


Cross-ADR Conflicts and Deviation Rulings

Severity-ordered. In every case below the shipped code is correct; the defect is in ADR text — except C7/#3, where the code should change. Full rulings (19) are in Appendix A; full conflict blocks (8) in Appendix B.

# Finding Severity Action
C4ReconcileContext.LogDropped : Action<string> now carries rows that were kept — Runtime/Inventory/InventorySaveTables.cs:78 ("orphan kept (crafting EC9)") and :117 ("row kept … (BB EC15 orphan rule pending)"), both with Warning: inside the string. A channel named Dropped reporting keeps; severity recoverable only by substring; every future ISaveable adds a string. Escalated from story-008's carried note by evidence that post-dates it.HIGHAmend ADR-0003 §3 + ADR-0004 §5 step 5: Action<ReconcileFinding> Report, readonly struct ReconcileFinding(TableId, RowKey, FindingKind Kind, Detail), FindingKind { Dropped, KeptOrphan, Stale }; migrate the 3 call sites (one story).
#10ADR-0004 §2 places Reconcile(ReconcileContext) on the Data-level ISaveable; ReconcileContext names Rules service interfaces and Data may not reference Rules (ADR-0003 §1) — uncompilable as written. Shipped (save story-001): Rules.Reconcile.IReconcilable : ISaveable; HostContext.Persist keeps ISaveable.HIGHAmend ADR-0004 §2 to the shipped split.
C3 / #5ADR-0015 §Key Interfaces lists ICombatState's members; shipped ICombatState : ISwingerStateQuery { CanResolveHarvest, AcceptsHarvestIntent } (HostContextServices.cs:100-107). The combat epic implementing from the ADR's list will not compile against HostContext.Combat.MEDIUMAmend ADR-0015 to declare the inheritance and both members; CombatState stays the single writer.
#14 / E21ADR-0013 V2 "Frame Debugger shows one batch per sprite atlas" is unmeasurable as an automated gate: UnityStats.batches scales 1:1 with sprite count in all four SRP-Batcher × dynamic-batching settings (32 → 33, stock URP sprite shader included). The SRP Batcher removes per-draw CBUFFER setup; it does not merge draws.HIGHReplace V2 with the shipped structural gate ShaderUtil.GetSRPBatcherCompatibilityCode == 0 (+ a deliberately incompatible control that reads 10); Frame-Debugger grouping becomes a manual built-player check; setPassCalls is the CPU-cost meter.
#15 / E20 / E23ADR-0013 V4 "+1" measures +2 (3 → 5): EditorSettings.spritePackerMode = 3 packs atlases at build time only, so Play Mode never renders from an atlas and the ghost's "0" is unobservable in the editor by construction. No ADR names this setting; every atlas-batching claim in ADR-0013 §8 and ADR-0014 §9 depends on it.MEDIUMRevise V4 to ≤ 2 in-editor (grid 1 + ghost 1); record spritePackerMode as a load-bearing project setting; ghost-0 deferred to a built-player Frame Debugger check; correct §8's budget line.
C7 / #3No ADR pins numeric width for config records. HarvestConfig is float, CombatConfig is double; both feed one phase machine, and widening 0.10f put a boundary one ulp short (story-007). ADR-0006 — the config-shape owner — contains no occurrence of "float" or "double".MEDIUMAdd ADR-0006 §2a: every duration/distance/rate/fraction on a *Config record is double (float only where a Unity API demands it); retype HarvestConfig (one story); re-evaluate story-007's decimal snap.
U1 / #13ADR-0005 declares IPreloadSchedule(SocketId) twice (lines 92, 158) — cannot compile: SocketId is a Driftlands.Data type and Driftlands.Tide references nothing. Shipped int (Tide/IPreloadSchedule.cs). Separately, NextEvictionInstant sits on concrete TideClock (:199), unreachable through the pull seam ADR-0005:95 mandates for LandLifecycle.MEDIUMAmend ADR-0005 §Key Interfaces: int socket with the review-F2 rationale, and add long NextEvictionInstant(int socket) to the interface.
#9ADR-0005 §5 "Debug Build Layout is a checked-in setting" is false on Addressables 2.9 — ProjectConfigData persists to Library/AddressablesConfig.dat. Shipped: asserted and corrected in AddressablesSetup.Ensure(), which BuildContent() runs first.MEDIUMAmend §5 to the enforced-not-committed mechanism.
C1ADR-0003 §6's Asm_NoUpwardReferences row (line 203) still says "Data references nothing but BCL", contradicting §1's 2026-09-03 correction. AssemblyRules.cs:134-143 implements the amended rule.MEDIUMOne-line fix at adr-0003:203.
C8ADR-0006 §5's diagram (line 135) shows Restore → AdvanceEpoch → Advance; the prose (lines 115, 164) and shipped SaveLoader.cs:77 say epoch check after Advance; TideClock.cs:219 throws on the wrong order.MEDIUMFix the diagram line.
C6ADR-0001: "AdvanceEpoch is the only writer of EpochIndex" — Restore also assigns it (TideClock.cs:79); ApplyPoolChange (:168) is an undeclared public surface implementing tide EC13; the 09-03 amendment note's premise "the concrete TideClock, which does not exist yet" is stale.MEDIUMOne amendment pass: increment-vs-assign wording; declare IEpochAdvancer/ISaveClock; declare ApplyPoolChange with its two refusals; correct §5's "inside TideSchedule".
C5ADR-0003's Driftlands.Editor amendment ("no production asmdef may reference it") is enforced by nothing — AssemblyRules.FindUpwardReferences has cases for Runtime/Rules/Data only. Holds today (a reverse edge would be a compile-time cycle); a future editor assembly not referencing Runtime would lack that accidental protection.MEDIUMAdd the fence + companion negative; tidy "seven-assembly graph" arithmetic.
#2Supply formula duplicated in Data.Validation.HarvestRules — forced by ADR-0003 §1 (Data may not reference Rules) + ADR-0006 §4 (one pipeline); drift tripwire mutation-verified.MEDIUMRecord in ADR-0003 §1: a formula needed by both a Data-stage rule and a Rules consumer lives in Data and Rules calls in; interim duplication with an agreement test is accepted until a third consumer.
#12Deck chaining is O(r − anchor) on a cold cache — not an EC3 violation (rounds, not elapsed cycles; Advance is one evaluation) but unbounded in principle for a far-future resume.MEDIUMADR-0001 risk bullet + a cold-cache far-future benchmark inside AC-44's 50 µs. No such benchmark exists (Appendix D).
#16ADR-0007 amendment (4) "a rebind row edits binding index 0" has no answer for a 2D composite (Move on KB/M, index 0 is the composite head) — the settings spec's four Move part rows have no source.LOWAmend: composite parts not rebindable at MVP (shipped) or exposed via IRebindService.CompositeParts (deferred, not refused); correct design/ux/settings.md.
U2 (new)docs/architecture/ADR-0006-game-data-loading.md is an orphaned 7-line amendment note ("2026-09-04 — stage 9 gains its first rows") written to the wrong filename; referenced nowhere.LOWFold into adr-0006-game-data-loading-validation.md §Amendments; delete the stray file.
U5 (new)HostContext member count is stated three ways: ADR-0003's correction says 18, HostContextServices.cs:4 says 17, the reviewer's scan counts 17.LOWSettle by compiler at the next ADR-0003 edit.
#1, #4, #7, #8, #11, #17, #18, #19, C2ItemDef.MaxStack seam (consistent); SwingerState fold-in step for ADR-0015; ADR-0007 "UI Actions reference" → "the project-wide asset's map named UI" (1.20); build-mode QA rows to be corrected (implementation follows the ADR); AC-9 tuple deferral (ADR wins, correctly); MenuHeld add (permitted); real adapter over deleted fake (stronger; optionally soften ADR-0003 §5); stale HostCommitQueue header; drain steps 1/1b/4/5 absent with step 1 inverted — the soft-close set is supplied on HostContext, so "Advance has already run" is a convention until HostTickOrchestrator lands (must be that story's ordering test).LOWEditorial; C2's step-1 note goes into ADR-0015's story.

Checked — no conflict (22 items with evidence, Appendix B): the amended Data → Tide edge is consistent with every other ADR and both purity fences (which carry a vacuity guard); Presentation → Runtime is the legal direction; asmdef GUID references are refused so the fence cannot pass vacuously; the toolchain corrections (readonly struct + IEquatable, IReadOnlyCollection<SocketId>) are implemented as specified; drain step 2's ≥ threshold and step 3's post-loop, snapshot-walked release match ADR-0002 §5 exactly; the seq-less tiebreak and the interim OwnerClientId dedup key are declared and dated; harvestDiffs is the one Droppable and its keys are never cached; TD-ADR C3 is enforced by a call-order assertion; IInstanceMetadata : IGearHooks is the precedent story-004 followed; ADR-0013/0014 declare mutual independence from both sides; ADR-0002↔0006 and 0004↔0006 cycles remain broken; all 49 control-manifest stances exist in docs/registry/architecture.yaml and vice versa (49 = 49).


ADR Dependency Order

Foundation (Accepted):
  1. ADR-0001 Tide Scheduler pure assembly
  2. ADR-0002 Host commit model               (← 0001; 0006 is Related, not a dependency)
  3. ADR-0003 Assembly + purity boundaries    (← 0001, 0002)
  4. ADR-0004 Save format + reconcile         (← 0001, 0002, 0003; 0006 Amends, not a dependency)
  5. ADR-0006 Game data loading + validation  (← 0001, 0003)
  6. ADR-0005 Land lifecycle on Addressables  (← 0001, 0002, 0003, 0004, 0006)
  7. ADR-0007 Input action map                (← 0003, 0004, 0006)
Presentation / Core (Accepted):
  8. ADR-0013 Tide-tint render pipeline       (← 0001, 0003, 0005)
  9. ADR-0015 Combat model                    (← 0002, 0003, 0006, 0007)
 10. ADR-0014 UI stack                        (← 0003, 0004, 0006, 0007)
Not yet written (Enables rows only — correct usage):
  ADR-0008 Netcode wire (playtest-gated) → 0009, 0010, 0011, 0012

**No cycles. No Depends On names a non-existent or non-Accepted ADR.** Numbering gap 0008–0012 is intentional (ADR numbers are Required-ADR row numbers).


GDD Revision Flags (Architecture → Design Feedback)

None in design/gdd/ — no GDD assumption is contradicted by verified engine behaviour. No systems-index row changes.

One art-bible clause (not a GDD, recorded for the art director): design/art/art-bible.md:261 — "2–5 concurrent lands stays well under budget with URP 2D SRP batching" — did not receive the N > 1 caveat §4.3 got on 2026-09-01 (ADR-0013 §4: no two sockets are ever in phase, so one global pair cannot represent two lands), and "SRP batching" reads as a draw-call merge, which it is not (it removes per-draw CBUFFER setup). The hard batching rule that follows it (globals, never MPBs) is verified correct.


Engine Compatibility

Engine: Unity 6.3 LTS (6000.3.18f1) · ADRs with Engine Compatibility section: 10 / 10 · Version agreement: 10 / 10 Deprecated API references: 0 binding usages in shipped code — every hit (SetupRenderPasses, MaterialPropertyBlock, TextMeshProUGUI, EventSystem, PanelEventHandler, [ServerRpc], legacy UnityEngine.Input) is a negative-test fence literal or a ban comment. Stale version references: 0 in ADRs; 6 engine-reference module docs (rendering, ui, animation, audio, navigation, physics) cite 6000.0 URLs / 2022-LTS framing without a banner — E10, carried. Post-Cutoff API conflicts: 0 between ADRs; 1 ADR-vs-reference-doc conflict (ADR-0013 §6 vs modules/rendering.md's pre-Render-Graph signature — the ADR is right). Package pins vs ADR assumptions: Input System 1.20.0 ✅ (ADR-0007 exact) · Addressables 2.9.1 ✅ · URP / Shader Graph 17.3.0 ✅ · Newtonsoft ✅ (ADR-0004) · activeInputHandler: 1 ✅ · NGO absent — no com.unity.netcode.gameobjects, no transport, no Relay; zero using Unity.Netcode. Consistent with Gate 0; consequence: every NGO claim in ADR-0001/0002/0005/0015 is unverifiable against a real assembly and modules/networking.md is itself banner-marked 1.x-era. ADR-0008 must verify live.

Engine Specialist Findings

# ADR/File Claim Verdict Severity
E18modules/rendering.md, modules/ui.mdE10 banner requiredINCORRECT — still absent, third reviewCONCERN
E19Packages/manifest.json"NGO 2.x (prototype 2.13.2)"CORRECT as intent; package ABSENT (Gate 0)NOTE
E20ADR-0013 §8 / V4ghost 0 + grid 1 = +1INCORRECT on 6.3 as measured — +2 (spritePackerMode = 3)CONCERN
E21ADR-0013 V2"one batch per sprite atlas" automatablePARTIALLY CORRECT — true claim, wrong instrumentCONCERN
E22control-manifest.md guardrail rows 312/313/317budgets statedUNVERIFIED — name no meterCONCERN
E23ProjectSettings/EditorSettings.asset:12m_SpritePackerMode: 3CORRECT, load-bearing, undocumented in any ADRCONCERN
E24ADR-0004 durabilityFile.Replace + Flush(true) + F_FULLFSYNCCORRECT — incl. the File.Replace-throws-when-absent edge (AtomicFileWriter.cs:79-84)—
E25ADR-0006 §5 bootRuntimeInitializeOnLoadMethodPARTIALLY CORRECT — it is GameDataBoot, at SubsystemRegistration (earliest; correct before any scene NetworkManager.Awake)NOTE
E26ADR-0013 §5two SetGlobal*/frame, no cacheCORRECT verbatim (TideTintDriver.cs:43-55; UnityShaderGlobals sole caller)—
E27ADR-0005every load releasedCORRECT — enforced by AddressablesGateTests.cs:112,117—
E28ADR-0014 §1no EventSystem; mesh TextMeshProCORRECT and fenced (PanelManagerPlayTests.cs:70-72; FlyoutPool.cs)—
E29ADR-00071.20, project-wide actions, no legacy InputCORRECT—
E30ADR-0003 toolchain correctionsno record struct, IsExternalInit, no IReadOnlySetCORRECT — genuine 6.3 / netstandard 2.1 limits—
E314 mutable statics (AtomicFileWriter, GameDataBoot, FatalConfigScreen)safeCORRECT today, latent — safe only while domain reload stays on (m_EnterPlayModeOptions: 0)NOTE
E32ADR-0001 Post-CutoffNetworkVariable<long> NEEDS VERIFICATIONCORRECT to remain open (package absent)—
E33ADR-0005 Post-CutoffNetworkList UNVERIFIED; NodesReady fallback requiredCORRECT — honest asymmetry—

Ruling on the ADR-0013 batching measurements (E20/E21). Both halves are true and independent: the ADR is wrong about observable draw behaviour on 6.3 URP 2D, and UnityStats.batches is the wrong instrument. The spike controlled the confound (all 32 sprites share one material and one texture); the stock URP sprite shader scaled 1:1 exactly like DriftLayerSprite. The shipped substitute gate — ShaderUtil.GetSRPBatcherCompatibilityCode reading 0 for DriftLayerSprite and 10 for a deliberately incompatible control — is the correct instrument and strictly stronger than V2: it is the meter shown reading non-zero before a zero is trusted, and code 0 is the statement that the tint properties sit outside UnityPerMaterial, which the V1 spike left open. Correct instruments on 6.3: compatibility code (automatable) · Frame Debugger on a built player for grouping (manual; spritePackerMode = 3 makes editor grouping meaningless) · UnityStats.drawCalls as a marginal delta · UnityStats.setPassCalls for CPU cost. UnityStats is UnityEditor-internal and does not exist in a player build. Proposed V2/V4 wording is in Appendix C.


Architecture Document Coverage

Required-ADR rows 1–7, 13–15 map to the ten Accepted ADRs with no scope drift; rows 8–12 remain ⏳ Gate 0; the header's "5 remaining: rows 8–12" is correct. No orphaned architecture; every systems-index.md system appears in the layer tables. Carried housekeeping: docs/consistency-failures.md does not exist (not created — the skill appends only).


Verdict: CONCERNS

Not FAIL: no Foundation gap; no cross-ADR contradiction survives that changes a decision; in every conflict above the shipped code is on the right side and, in the three cases that matter most (C4, C8, U1), self-enforcing — TideClock throws on the wrong epoch order, the assembly graph refuses the wrong reference, the drift tripwire fires on a formula change. The project's testing discipline (mutation-verified fences, meters validated with controls, evidence paths on disk) is what made this review's findings documentation findings rather than shipped defects.

Off PASS for: one HIGH design defect shared by ADR-0003 and ADR-0004 (C4 — the cost of which rises with every table); two ADR sections that cannot compile as written (#10 ADR-0004 §2, U1 ADR-0005 IPreloadSchedule); one ADR the next epic will fail to build against (C3 ADR-0015); two verification criteria in an Accepted ADR that cannot be measured as worded (ADR-0013 V2/V4); no numeric-width rule for configs (C7); and E10 carried a third time.

Blocking issues — none (advisory verdict)

Required before the next epic is storied (recommended order)

Required ADRs (unchanged priority)


✅ Resolution — HIGH + MEDIUM amendment sets applied 2026-09-07 (same session, user-approved per ADR)

ADR Applied
ADR-0003C1 §6 row; C4 ReconcileContext.Report : Action<ReconcileFinding> + FindingKind; C5 Editor fence stated; #2 shared-formula rule
ADR-0004#10 IReconcilable split; C4 §5 steps 5–7 typed findings
ADR-0015C3/#5 ICombatState : ISwingerStateQuery; #4 migration step (0)
ADR-0013E21 V2 compatibility-code gate; E20 V4 + §8 budget → +2 in-editor; E23 V8 spritePackerMode
ADR-0006C7 §2a numeric width; C8 §5 diagram order
ADR-0005U1 int socket; #13 NextEvictionInstant; #9 Ensure()-asserted layout setting
ADR-0001C6 increment-vs-assign, IEpochAdvancer/ISaveClock/ApplyPoolChange declared, stale note folded; #12 deck-chaining risk

Not applied here (stories, not doc edits): ReconcileFinding type + 3 call-site migration (C4, HIGH); HarvestConfig → double (C7); Driftlands.Editor fence + negative (C5); NextEvictionInstant onto IPreloadSchedule (#13); spritePackerMode EditMode assertion (V8); HostCommitQueue header (#19); E10 banners; ADR-0015 ms budget (figure is the user's call); fold/delete the stray ADR-0006-game-data-loading.md (U2); regenerate control-manifest.md (E22). The control manifest's Manifest Version is now stale against seven amended ADRs — /story-done will flag it until regenerated.

Appendix A — Deviation rulings (19)

# Source Deviation Ruling Sev Action
1core-inventory-carry/story-001:120ItemDef.MaxStack on an IItemStackLimits seam; adapter throws until crafting makes ItemDef real; tripwire fails on success(c) CONSISTENTLOWCrafting epic DoD: landing ItemDef must land the real ItemDefStackLimits read
2core-harvesting/story-002:130Supply formula duplicated in Data (Data→Rules forbidden; one pipeline)(a) AMEND ADR-0003 §1MEDIUMShared formula lives in Data; Rules calls in; interim duplication + agreement test accepted
3core-harvesting/story-007:107HarvestConfig float vs CombatConfig double(b) IMPLEMENTATION + (a) AMEND ADR-0006MEDIUM§2a numeric-width rule; retype HarvestConfig; re-evaluate the decimal snap
4core-harvesting/story-006:120SwingerState owned by harvesting until combat's PlayerState(a) AMEND ADR-0015 (narrow)LOWMigration step 0: fold into PlayerState; harvest machine keeps its own phases
5core-harvesting/story-004:141ICombatState : ISwingerStateQuery(a) AMEND ADR-0015 §Key InterfacesMEDIUMDeclare the inheritance and both members
6core-harvesting/story-008:125LogDropped string channel(a) AMEND ADR-0003 §3 + ADR-0004 §5HIGHTyped ReconcileFinding (C4)
7foundation-input/story-001:208"UI Actions reference" wording pre-1.20(a) AMEND ADR-0007 (editorial)LOW"the project-wide asset's map named UI"; pin 1.20.0
8foundation-input/story-003:171QA rows read EdgeMask as delivered; Cancel as non-exit(c) CONSISTENT — QA doc fixLOWCorrect the story's QA rows (ADR-0007:126 is unambiguous)
9foundation-land-lifecycle/story-006:126Debug Build Layout not check-in-able on 2.9(a) AMEND ADR-0005 §5MEDIUMEnforced-not-committed via AddressablesSetup.Ensure()
10foundation-save-persistence/story-001:131IReconcilable : ISaveable split(a) AMEND ADR-0004 §2HIGHADR text uncompilable against the accepted graph
11foundation-tide-scheduler/story-007:58,180AC-9 type guard deferred; ADR-0001 tuple wins(c) CONSISTENTLOWRaise only if a caller reads progress while undocked; story-009's reflection sweep closes the caching hazard
12foundation-tide-scheduler/story-002:163Deck chaining O(r − anchor) vs EC3(c) CONSISTENT, risk registeredMEDIUMADR-0001 risk bullet + far-future cold-cache benchmark (unverified)
13foundation-tide-scheduler/story-009:47,130NextEvictionInstant on concrete TideClock(a) AMEND ADR-0005 §Key InterfacesMEDIUMAdd to IPreloadSchedule; LandLifecycle may not re-derive it
14presentation-tide-tint/story-003:92V2 unobservable via UnityStats.batches(a) AMEND ADR-0013 V2 + manifest rowHIGHCompatibility-code gate with control; Frame Debugger manual on a built player
15presentation-tide-tint/story-004:97V4 +1 reads +2; BuildConfig marker; GDD literals(a) AMEND ADR-0013 V4/§8 ×2MEDIUM≤ 2 in-editor; spritePackerMode decision; build.json must carry placementReach/gridCellSize with gridCellSize == 1 asserted (ADR-0003 §2)
16presentation-ui-hud-shell/story-009:116Move composite has no rebind row(a) AMEND ADR-0007 amendment (4)LOWComposite parts: not rebindable at MVP (a) or CompositeParts (b, deferred)
17presentation-ui-hud-shell/story-008:159IPanelManager.MenuHeld; no dashed USS borders(c) CONSISTENT + (a) minor ADR-0014 risk lineLOWUSS has no dashed borders — 9-slice or .dl-inert
18core-harvesting/EPIC.md:80FakeLandState deleted; real adapter in the fake context(c) CONSISTENTLOWOptionally soften ADR-0003 §5 "one fake per interface" to "where the real implementation is not EditMode-reachable"
19core-harvesting/EPIC.md:82HostCommitQueue header stale (steps 2/3 exist)(b) IMPLEMENTATION (comment)LOWName present steps (2, 3) and absent (1, 1b, 4, 5) with blockers

Appendix B — Cross-ADR conflict blocks

C1 — ADR-0003 §6 vs §1 (self-conflict). Type: stale rule text. §6 row 203 "Data references nothing but BCL"; §1 correction (2026-09-03) "Data → Tide; Asm_NoUpwardReferences allows BCL + Driftlands.Tide". Impact: §6 is the test's specification; a test written from it fails the correct graph. Code right on both counts (Data.asmdef → ["Driftlands.Tide"]; AssemblyRules.cs:134-143). Resolution: (1, recommended) fix row 203; (2) footnote. MEDIUM.

C2 — ADR-0002 §5 drain order vs HostCommitQueue.Drain. Type: partial implementation. Steps 2 (IsDroppedBySoftClose, :117-122) and 3 (ReleasePendingTeardowns, :104) shipped; 1, 1b, 4, 5 absent. Step 1 is inverted — the set arrives on HostContext.SoftClosedSockets, so the "Advance already ran" guarantee is a convention until HostTickOrchestrator (ADR-0015) lands. Latent: PlaceStructureCommand will need step 4. Resolution: (1, recommended) migration-status line in ADR-0002 §5 + header fix; (2) empty flush points — rejected (vacuous scaffold). LOW as conflict; HIGH-attention note for ADR-0015's orchestrator ordering test.

C3 — ADR-0015 ICombatState list vs ISwingerStateQuery. Type: interface surface divergence. Not a breach (add, not remove/retype; HostContext.Combat still ICombatState; follows IInstanceMetadata : IGearHooks). Resolution: (1, recommended) declare inheritance + members in ADR-0015; (2) move the seam into ADR-0003 §3's shape list; (3) collapse — rejected. MEDIUM.

C4 — ADR-0004 §5 / ADR-0003 §3 LogDropped. Type: under-specified channel. Both ADRs and code agree — a shared design defect. Three call sites (HarvestDiffTable.cs:109 drop; InventorySaveTables.cs:78,117 keeps). Resolution: (1, recommended, now) Action<ReconcileFinding> Report; (2) parallel channel — rejected; (3) defer to save-epic completion — rejected (eight stages will have written strings). HIGH.

C5 — Driftlands.Editor vs ADR-0003 fences. Type: fence gap. Rule holds in fact (only Editor itself and Tests.EditMode reference it; PlayMode reaches it by reflection); no AssemblyRules case enforces it. Resolution: (1, recommended) add the case + negative; (2) tidy "seven-assembly" arithmetic and state the EditMode-may/PlayMode-may-not asymmetry deliberately. MEDIUM.

C6 — ADR-0001 "only writer" vs IEpochAdvancer, Restore, ApplyPoolChange. Type: invariant wording vs surface. (i) seam consistent, note premise stale; (ii) Restore assigns (TideClock.cs:79, code comment at :222 says "only INCREMENT"); (iii) ApplyPoolChange (:168) undeclared. Resolution: (1, recommended) one amendment pass — wording in Appendix A #C6; (2) also retype TideConfig claims (readonly struct). MEDIUM.

C7 — no float/double pin. Type: missing decision. ADR-0001 pins tide math only; ADR-0006 silent; ADR-0003 §2's "gameplay geometry is float" is spatial, not durations. Resolution: (1, recommended) ADR-0006 §2a double + retype HarvestConfig; (2) float everywhere — rejected (CombatConfig accumulators); (3) rely on the snap — rejected. MEDIUM.

C8 — ADR-0006 §5 diagram vs prose. Type: internal contradiction. Line 135 Restore → AdvanceEpoch → Advance; lines 115/164 and SaveLoader.cs:77 after; TideClock.cs:219 throws. Resolution: (1, recommended) correct the line to Restore → Advance(loadUtc) → if N ≠ socketCount → AdvanceEpoch(config, clock.T) → reconcile 4..9; (2) drop ordering from the diagram. MEDIUM.

Checked — no conflict: N1 Data→Tide edge consistent (ADR-0006 GameConfigs.Tide, ADR-0005 TemplateId in Tide, Tide root) · N2 both purity fences + vacuity guard (AssemblyEnforcementTests.cs:111,119) · N3 Presentation→Runtime legal · N4 test implements amended rule · N5 absent Assembly-CSharp is PASS · N6 GUID refs refused · N7 toolchain corrections implemented · N8 ICommand/HostContext order · N9 ReasonCode append-only · N10 int exception documented both sides · N11 drain step 2 ≥ · N12 step 3 post-loop snapshot · N13 seq-less tiebreak, mixed batch refused · N14 OwnerClientId dedup interim declared · N15 IInstanceMetadata : IGearHooks · N16 ReconcileContext ten members verbatim · N17 TD-ADR C3 call-order assertion · N18 one Droppable · N19 diff keys never cached · N20 instance caches (bodies of Purity_NoStaticMutableFields not read) · N21 0013/0014 independence · N22 cycles broken · N23 (reviewer) manifest ↔ registry 49 = 49.

Appendix C — Proposed ADR-0013 V2/V4 wording (engine specialist; offered, not applied)

V2 / EDITOR + PLAYMODE (revised 2026-09-07, review E21). SRP-batcher compatibility is asserted structurally, not by counting: ShaderUtil.GetSRPBatcherCompatibilityCode(DriftLayerSprite) returns 0 after one rendered frame, measured alongside a deliberately incompatible control shader that must return non-zero (observed 10) — the meter must read non-zero before a zero is evidence. This subsumes the UnityPerMaterial CBuffer claim V1 left open. ⚠️ UnityStats.batches cannot verify this and must not be used: on 6000.3.18f1 / URP 17.3.0 2D it scales 1:1 with sprite count in all four batcher settings — the SRP Batcher does not merge draws, it removes per-draw CBUFFER setup. "One batch per sprite atlas" survives only as a manual Frame Debugger check on a built player (spritePackerMode = 3 packs at build time only). CPU cost, if wanted, is UnityStats.setPassCalls.

V4 / PLAYMODE (revised 2026-09-07, review E20). Entering build mode at maximum reach with the ghost dashed adds ≤ 2 draw calls in the editor — grid ≤ 1, ghost ≤ 1 — as a marginal delta, never "total < 100". §8's "ghost 0" is a built-player claim: with spritePackerMode = 3 no atlas exists in Play Mode, so the editor cost is necessarily +1. The atlas-batched 0 is DEFERRED to a Frame Debugger check on a built player and is not a gate until then. Structural guarantees (one material, pre-authored dashed frames, no MPB) are asserted by BuildOverlaySetupTests and TideTintNegativesTests.

§8 budget line: "ghost 0 + grid 1 = 1" → "grid 1 + ghost 1 = 2 in the editor; 1 in a built player if the home atlas batches the ghost as designed (unverified — deferred)."

Plus: a new Engine Compatibility → Verification Required row naming EditorSettings.spritePackerMode (E23); control-manifest.md rows 312/313/317 to name their meters (E22).

Appendix D — Not verified (carried, never guessed)


History

Date Verdict Covered / Partial / Gap Notes
2026-08-28CONCERNS64 / 28 / 19 of 111First review; 8 ADRs; TR registry v2
2026-08-28 (same session)amendments applied—All eight → Accepted
2026-09-01CONCERNS64 / 28 / 19 → 73 / 26 / 12ADR-0013/0014 reviewed, E14 adjudicated, both Accepted
2026-09-07CONCERNS73 / 26 / 12 (unchanged)First RTM: 61/111 full chain (55 %); Foundation 79 %. 19 deviation rulings, 8 conflicts (C4 HIGH; #10/U1 uncompilable ADR text; C3; ADR-0013 V2/V4 unmeasurable; C7 numeric width). 0 deprecated binding usages; NGO absent (Gate 0). E10 carried ×3. Registry unchanged. Same session: HIGH+MEDIUM amendment sets applied to ADR-0001/0003/0004/0005/0006/0013/0015 (user-approved per ADR).