# ADR 0003: Canonical Snapshot — enlyzeam-current as Production Seed

**Date:** 2026-05-03
**Phase:** 03 close

## Status

**Accepted** — locked at end of Phase 3 (SDOC-06). Re-evaluation gates listed
in **Forcing Functions for Re-Open** below; absent any of those triggers, the
canonical-snapshot lock is binding through Phase 4 SRV-09/10/11 (legacy
account import + first-login rehash), Phase 5 DEP-07 (RESTORE.md procedure),
and Phase 7 PAR-05 (`.bnu` character migration).

Supersedes: nothing. Superseded by: nothing.

## Context

Three drift'd legacy server snapshots exist under
[`legacy/servers/`](../../legacy/servers/) (gitignored per CLAUDE.md hard
rule #8). Per
[`.planning/phases/03-server-documentation-schemas/03-CONTEXT.md`](../../.planning/phases/03-server-documentation-schemas/03-CONTEXT.md)
§D-02 + §"Canonical snapshot (D-02 lock)" and
[`.planning/codebase/CONCERNS.md`](../../.planning/codebase/CONCERNS.md)
§"Drift'd snapshots", the load-bearing dataset deltas across the three are:

| Snapshot         | `.bnu` count | `localList.txt`     | `MB_Log.bnb`                            | Disposition                                  |
|------------------|--------------|---------------------|-----------------------------------------|----------------------------------------------|
| enlyzeam-current | 973          | 597 lines           | 39549 B (mtime newer than archive copy) | **canonical**                                |
| enlyzeam-archive | 967          | 597 lines           | 39549 B (mtime older, identical bytes)  | rejected — older world-state run             |
| local-current    | 211          | 90 lines            | (per dir contents, smaller)             | rejected — 73% smaller world-state, truncated |

These numbers were verified at planning time by file-count + line-count +
`stat` walks of `legacy/servers/{enlyzeam-current,enlyzeam-archive,local-current/BNO_Server}/`
and recorded in
[`.planning/phases/03-server-documentation-schemas/03-RESEARCH.md`](../../.planning/phases/03-server-documentation-schemas/03-RESEARCH.md)
§Canonical Snapshot Selection. The `legacy/` tree is gitignored per
[CLAUDE.md](../../CLAUDE.md) hard rule #8 (repo private through Phase 7), so
ADR readers without a local checkout of `legacy/` cannot follow the citation
links here directly — the dataset numbers are the durable record.

Per [CLAUDE.md](../../CLAUDE.md) hard rule #4 (`.bno`/`.bnb`/`.bnu` parsing
requires extracted GML first) and the ground-truth grammar inventory in
[`docs/extracted-server/save-formats.json`](../extracted-server/save-formats.json),
every save-format is line-based `file_text_*`. The dataset-size delta — not
byte-level format reverse-engineering — drives the canonical choice.

Returning BNO players must see their last character / inventory / username on
first login against the new server. Per
[`.planning/phases/03-server-documentation-schemas/03-CONTEXT.md`](../../.planning/phases/03-server-documentation-schemas/03-CONTEXT.md)
D-01, the canonical snapshot is **the production seed for migration**, not
a historical-preservation reference. Phase 4 SRV-10 (legacy account import
script) and Phase 7 PAR-05 (`.bnu` character migration) consume only this
seed.

Legacy-credentials handling: `legacy/servers/enlyzeam-current/localList.txt`
contains plaintext usernames+passwords for ~298 accounts (per
[`.planning/codebase/CONCERNS.md`](../../.planning/codebase/CONCERNS.md)
spot-check). Per [CLAUDE.md](../../CLAUDE.md) hard rule #2 (no faithful port
of plaintext password storage), plaintext rows MUST NOT be copied verbatim
into the production `accounts` table.
[`tools/db-schema/src/tables.ts`](../../tools/db-schema/src/tables.ts)
already declares the `legacy_credentials_staging` 6-column table per D-04
spec (Plan 03-06).

## Decision

**Pick `legacy/servers/enlyzeam-current/` whole as the canonical production
seed.** No per-record merge across snapshots.

Rationale (per
[`.planning/phases/03-server-documentation-schemas/03-CONTEXT.md`](../../.planning/phases/03-server-documentation-schemas/03-CONTEXT.md)
D-02):

- **Largest world-state by load-bearing dimensions:** 973 `.bnu` user files
  (vs 967 archive, 211 local-current); 597-line `localList.txt`; 39549-byte
  `MB_Log.bnb` with mtime newer than archive's identical-size copy.
- **enlyzeam-archive's larger total dir size** (~46 MB vs ~34 MB current) is
  older Master `.exe` + `.gb1` backups + dev cruft per
  [`.planning/codebase/CONCERNS.md`](../../.planning/codebase/CONCERNS.md)
  inspection — NOT world-state.
- **local-current is clearly truncated:** 73% smaller world-state despite
  a later activity timestamp suggests a partial copy or fresh-but-orphaned
  run.
- **Per-record merge across the three** would require reconciling mtimes,
  identifying authoritative-row-per-account heuristics, and risking data
  corruption in account migration. Rejected per D-02.

**Older-`.gmd`/`.gb1`/`.exe` extraction explicitly rejected** per
[`.planning/phases/03-server-documentation-schemas/03-CONTEXT.md`](../../.planning/phases/03-server-documentation-schemas/03-CONTEXT.md)
D-03 ([CLAUDE.md](../../CLAUDE.md) Source-of-Truth rule). Master 5-4 is the
canonical source; `Master 4.gmd`, `BN Online Master 5-1.exe`,
`BN Online Master 5-2 DEBUG.exe`, and `BN Online Master 5-3.exe` in
`legacy/servers/enlyzeam-archive/` are NOT extended into the Phase 1
extractor. **On-demand caveat:** if a specific opcode trace fails or a
script reference looks broken during Phase 4+ execution, on-demand extract
the relevant older artifact and document the deviation in the relevant
subsystem MD with an `<!-- UNRESOLVED -->` HTML comment; do not extend the
extractor or this ADR.

**Legacy-credentials pipeline (per D-04):**

1. Phase 4 SRV-10 imports usernames + (legacy_hash, algorithm) tuples from
   `legacy/servers/enlyzeam-current/localList.txt` into the
   `legacy_credentials_staging` table declared in
   [`tools/db-schema/src/tables.ts`](../../tools/db-schema/src/tables.ts)
   (6 columns per D-04 spec: `username` PK, `legacy_hash`, `algorithm`,
   `force_reset`, `legacy_source`, `imported_at`).
2. On first successful legacy-validation login, Phase 4 SRV-11 performs an
   argon2id rehash into `accounts.password_hash`, deletes the staging row in
   the same transaction, and — if `algorithm` ∈ {`plaintext`,
   `bcrypt-weak`} — sets `accounts.force_reset = true` to trigger the
   force-reset UI flow.
3. Phase 5 RESTORE.md documents the read-once-then-purge step explicitly so
   post-restore staging-table state never drifts back to plaintext on a
   fresh-volume restore.
4. Plaintext rows NEVER reach the production `accounts` table.
   [CLAUDE.md](../../CLAUDE.md) hard rule #2 is enforced at this boundary;
   the staging table is the single legitimate transit channel.

**Repository privacy ([CLAUDE.md](../../CLAUDE.md) hard rule #8):** the
`legacy/servers/` tree remains private through Phase 7. `.gitignore` already
excludes it (verified during the Phase 1 reproducibility check). The
legal-prep gate (COM-02 v2) reviews public-release of `legacy/` separately;
this ADR does not pre-commit to that decision.

## Consequences

### Positive

- **Phase 4 SRV-10 has zero-ambiguity dataset target.** Importer reads
  exactly `legacy/servers/enlyzeam-current/localList.txt` and the
  `UserData/{HXB,Inv,MB_News}/` per-user `.bnu` files; no merge logic.
- **Phase 7 PAR-05 character migration consumes the same seed
  deterministically.** No "which snapshot?" branch in the migration tool.
- **Phase 5 RESTORE.md procedure is single-source** — no merge complexity in
  the runbook; restore is fresh-volume + Litestream replay against a
  pristine `legacy_credentials_staging` table populated from
  enlyzeam-current's localList.txt.
- **enlyzeam-archive / local-current preserved for one-off recovery**
  (account ownership disputes) without bloating the canonical pipeline.
  See Negative #1 + Forcing Function #1.

### Negative

1. **Returning players whose accounts exist ONLY in enlyzeam-archive** (older
   snapshot, possibly different username, or an account dropped between
   archive and current) are NOT migrated by default. Handled as one-off
   recovery: append the missing username + legacy_hash row to
   `legacy_credentials_staging` manually on a case-by-case basis. No
   general-purpose archive-merge tool is built.
2. **mtime-equal `MB_Log.bnb` between current and archive** (identical bytes,
   drift'd timestamps) introduces an audit-trail gap. Mitigated by
   mtime-newer-wins disposition + Phase 5 RESTORE.md noting the byte-level
   equivalence so post-restore audits don't double-count.
3. **enlyzeam-current may itself have plaintext credential leakage beyond the
   ~298 known per CONCERNS.md spot-check.** Mitigated by an audit pass
   during SRV-10: lint flags any non-staging code path that reads plaintext
   from `localList.txt`. The staging-table boundary is the only legitimate
   plaintext-transit point.

### Neutral

- **Pre-launch validation** requires sampling ≥10 random accounts from
  enlyzeam-current and confirming `legacy_credentials_staging` import +
  first-login argon2id rehash flow end-to-end on staging. Phase 5 dogfood
  checklist owns this verification step; it is neither blocking nor
  optional.

## Forcing Functions for Re-Open

If any one of the following fires, ADR 0003 is re-opened in a Phase-9
retrospective:

1. **A returning player provides original BNO username + plausible
   verification AND the username exists in enlyzeam-archive but not
   enlyzeam-current.** First firing is handled as a one-off staging-table
   append per Negative #1; ADR is NOT re-opened on first firing. ADR is
   re-opened on the **third** independent firing within any 30-day window —
   that pattern indicates a systematic recovery problem rather than an
   exception.
2. **Forensic discovery of a mass plaintext credential leak in
   enlyzeam-current beyond the ~298 known accounts** per CONCERNS.md
   spot-check. Re-evaluate redaction discipline + reconsider whether
   enlyzeam-archive's smaller `.bnu` count is in fact more sanitised, and
   whether the staging-table audit pass is sufficient (Negative #3).
3. **mtime/size delta between `MB_Log.bnb` in current vs archive widens
   beyond byte-level equivalence.** Current and archive copies are
   currently identical bytes (mtime newer in current); ≈0 risk today, but a
   future re-extraction from a backup could surface a divergence — re-
   evaluate canonical message-board state at that point.
4. **A Phase 4+ script reference fails because the relevant GML lives only
   in an older Master `.gmd`/`.exe`** (Master 4 / 5-1 / 5-2 / 5-3) AND the
   on-demand extraction caveat above doesn't suffice (e.g. the older
   artifact is needed by more than one downstream consumer). Re-evaluate
   the D-03 older-extraction lock.

## Per-Snapshot Disposition

| Snapshot         | Path                                                                     | Disposition                          | Reason                                                                                                                                            |
|------------------|--------------------------------------------------------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| enlyzeam-current | [`legacy/servers/enlyzeam-current/`](../../legacy/servers/enlyzeam-current/) | **canonical**                        | Largest world-state (973 `.bnu`, 597 `localList.txt` lines), 39549 B `MB_Log.bnb` with mtime newer than archive's identical-byte copy.            |
| enlyzeam-archive | [`legacy/servers/enlyzeam-archive/`](../../legacy/servers/enlyzeam-archive/) | rejected — older world-state run     | Smaller `.bnu` count (967); larger total dir size from older Master `.exe`/`.gb1` binary backups, NOT world-state. One-off recovery only.         |
| local-current    | [`legacy/servers/local-current/BNO_Server/`](../../legacy/servers/local-current/BNO_Server/) | rejected — truncated                 | 73% smaller world-state (211 `.bnu`, 90 `localList.txt` lines) despite later activity timestamp; clearly partial copy or fresh-but-orphaned run.  |

## Related ADRs

- [ADR 0001 — Client Engine](0001-client-engine.md) (Phaser 3.90.0 locked at
  Phase 2 close).
- [ADR 0002 — Persistence Layer](0002-persistence-layer.md) (`better-sqlite3`
  + Drizzle + Litestream locked at Phase 3 plan 07); the persistence runtime
  hosting the `legacy_credentials_staging` table referenced here.

## References

- `legacy/servers/enlyzeam-current/localList.txt` — 597 plaintext credential
  rows; the `legacy_credentials_staging` source per D-04.
- `legacy/servers/enlyzeam-current/UserData/{HXB,Inv,MB_News}/` — 973 `.bnu`
  per-user files; Phase 7 PAR-05 character-migration source.
- `legacy/servers/enlyzeam-current/MB_Log.bnb` — 39549-byte board log; Phase
  4 message-board seed.
- `legacy/servers/enlyzeam-current/{Settings,MSettings}.bno` — admin-config
  baseline (plaintext admin creds rehashed via the staging-table path per
  D-04).
- [`tools/db-schema/src/tables.ts`](../../tools/db-schema/src/tables.ts) —
  `legacy_credentials_staging` schema (6-column D-04 spec; Plan 03-06).
- [`docs/extracted-server/parity-checklist.json`](../extracted-server/parity-checklist.json)
  — `legacy-superweird-import` row (`disposition: deferred-stage-8` per A7
  finding) and `legacy-userlist-import` row (`disposition: in-phase-7`,
  reason cites the read-once-then-purge contract).
- [`docs/extracted-server/save-formats.json`](../extracted-server/save-formats.json)
  — `file_text_*` grammar inventory; ground-truth that the dataset-size
  delta is the load-bearing canonical-choice signal.
- [`docs/extracted-server/account-auth.md`](../extracted-server/account-auth.md)
  — narrative companion documenting the D-04 staging-pipeline mechanic at
  prose depth.
- [`.planning/codebase/CONCERNS.md`](../../.planning/codebase/CONCERNS.md) —
  plaintext credentials + drift'd-snapshots origin; ~298-account
  spot-check.
- [`.planning/phases/03-server-documentation-schemas/03-CONTEXT.md`](../../.planning/phases/03-server-documentation-schemas/03-CONTEXT.md)
  D-01..D-04 — verbatim ground for purpose, resolution, older-extraction
  rejection, and PII-handling staging schema.
- [`.planning/phases/03-server-documentation-schemas/03-RESEARCH.md`](../../.planning/phases/03-server-documentation-schemas/03-RESEARCH.md)
  §Canonical Snapshot Selection + §Per-Snapshot Disposition Table — the
  dataset-size delta numbers (973 / 967 / 211; 597 / 597 / 90; 39549 B)
  verified at planning time.
- [CLAUDE.md](../../CLAUDE.md) hard rule #2 (no faithful port of plaintext
  passwords; argon2id from packet 1) — enforced at the
  `legacy_credentials_staging` → `accounts` boundary.
- [CLAUDE.md](../../CLAUDE.md) hard rule #4 (extract → document → rewrite;
  `.bno`/`.bnb`/`.bnu` parsing requires extracted GML first) — grounds the
  dataset-size-delta argument as the load-bearing signal.
- [CLAUDE.md](../../CLAUDE.md) hard rule #8 (repo private through Phase 7) —
  governs why `legacy/` is gitignored and why path citations here may be
  broken-link from a public-release reader's perspective.
- **Phase 5 RESTORE.md (forward-link)** — read-once-then-purge runbook step
  authored when Phase 5 DEP-07 lands; this ADR is the upstream lock.
