# R-8 Product Placement — Decision Record (2026-07-18)

Audit finding B-3/H-5: V-series products live under two placement conventions,
and "where does V<N> code live" has different answers per N.

## Measured state (2026-07-18)

| Convention                                                      | Products                                      |
| --------------------------------------------------------------- | --------------------------------------------- |
| **Split** (`apps/v<N>` + `libs/v<N>` + `V<N>/` for UE/docs/ops) | V3, V6, V7, V8, V9, V10 (6 of 9 code-bearing) |
| **In-root** (`V<N>/{services,web,scripts}`)                     | V2, V4, V5                                    |
| Docs/planning only                                              | V1                                            |

25 workflows path-filter on `V*/` trees; 8 on `apps/v*`/`libs/v*`.

## Decision

**The split convention wins** — it is the 2:1 majority, matches the
workspace-wide `apps/`+`libs/` taxonomy, keeps UE binary trees at product roots
(path-length and LFS-locality reasons the audit itself accepted), and is what
V10 adopted at birth per the audit's §0 addendum. `products/…` (the audit's
alternative) is rejected: it would move all nine trees instead of three and
invalidate all 25 V-tree workflow filters instead of the holdouts'.

## Execution plan (three dedicated changes, smallest first)

Move `V<N>/{services,web,scripts}` → `apps/v<N>/…` (deployables) and product TS
libs → `libs/v<N>/…`, one product per change:

1. **V5** (2,122 files; `V5/services` is in `pnpm-workspace.yaml`): update the
   workspace glob, V5 workflows' filters, and the V5 checker scripts that
   self-assert paths. V5 nightly uses `lfs: true` — content stays under `V5/ue`,
   unaffected.
2. **V2** (6,448 files; `V2/services` glob; V2 checker scripts self-assert
   manifest paths — the cargo-umbrella work already catalogued them).
3. **V4 last** (3,053 files; the heaviest coupling:
   `V4/.ci/ gates-manifest.json` carries 996 path filters and its validators
   grep the manifest for invocation strings — every filter row referencing a
   moved path must be rewritten in the same change and the round-trip validator
   re-run).

Per-change verification recipe (the audit G-2 method): glob-test every touched
workflow/manifest filter against `git ls-files` before and after; run the
product's own gate fleet (`scripts/verify.mjs` family) against its pre-move
baseline requiring zero pass/fail flips; `pnpm install --lockfile-only` for
workspace-glob changes.

Why not executed today: each move is a multi-thousand-file change wired into
product gate fleets that parallel sessions are actively extending; the zero-flip
baseline requires a quiet window per product. The convention decision above is
binding now — **new product code goes to `apps/v<N>`/`libs/v<N>`**, and V2/V4/V5
converge via the three staged changes.

## Execution record (2026-07-18, prelaunch-authorized)

All three staged changes landed the same day, each with its zero-flip proof:

1. **V5**: verify-services 16/16, docs + cell validators, service typechecks and
   the auth contract suite identical before/after.
2. **V2**: the full 1,155-gate `check-v2` fleet identical before/after (all
   green); remote-config suite 28/28; content inventory + seed registry
   regenerated at identical counts.
3. **V4**: the `@v4/scripts` gate suite at its exact 190/191 baseline; all 996
   gates-manifest filters glob-match tracked files before and after; cargo
   metadata green on the moved workspace. Depth couplings fixed en route: 98
   repo-root anchors ('../../..' → four-up) and the checker path allowlist
   gaining `apps/v4/`.

**R-8 is complete: every code-bearing product (V2–V10) now follows the split
convention.** Product roots keep ue/docs/ops only.

## Slice 4 (deep-audit B-1, later 2026-07-18)

The deep audit found the "product roots keep ue/docs/ops only" claim was not yet
true: `V2/tools/esports` (a live nx library reached via a leftover `V2/tools/*`
workspace glob) and `V5/tools` remained. Adjudication + action:

- **`V2/tools/esports` → `apps/v2/esports-tools`** (this slice): workspace glob
  removed, tsconfig aliases, grpc-services vitest alias (which had been silently
  broken by slice 2 — it resolved to a nonexistent `apps/tools/` path, leaving
  the grpc-services suite unable to load), four esports checkers, the UE
  contract JSON, 10 doc references, and the lockfile all rewired. Esports-tools
  7/7 tests + typecheck green; grpc-services loads again (13/16; the 3 failures
  are pre-existing commentary-seam contract drift, red-at-HEAD).
- **`V5/tools` stays** — it is the V5 validation/authoring gate fleet
  (validate-\*-content.py, templates), invoked from repo root by
  `v5-doc-validation.yml`, `v5-nightly-cell-validation.yml`, and the pre-commit
  hook. That is the "ops" class the split convention keeps at product roots; the
  audit's B-1 counted it as product code — corrected.
- **En route**: the fleet-wide staleness this exposed was repaired — 54 checkers
  still asserting the retired `DOMAINS/` tree (→ `docs/domains/<x>/deep-dive/`),
  125 checkers asserting pre-slice `V2/{services,web}` paths (→ `apps/v2/…`), 18
  deep-dive domain docs + 5 V2 docs with stale paths, and 12 checkers whose
  contiguous-phrase assertions broke on prettier line-wraps (now
  whitespace-normalized). `check-v2-service-observability` remains red on its
  pre-existing @oshun/metrics|tracing dependency debt across ~40 former
  V2/services packages (red before R-8 as well) — product-owner work, now
  visible again instead of vacuous.
