# Who owns what
Every bug that cost a day in this pipeline has had the same shape: two systems
both setting the same thing, and whichever ran last silently won. This file says
who owns each concern so the next person deletes the loser instead of adding a
third writer.
All four examples below are real, from 2026-08-06.
## Computer name - the PPKG owns it
The provisioning package declares:
```xml
F%SERIAL%
```
so a bay comes up as `F`.
`run-enrollment.ps1` used to also run `Rename-Computer -NewName "E$serial"`.
Both are pending renames; last writer wins at reboot. The script ran twice, and
its second run landed after the package had already queued `F579C144`, so the
bay came up `E579C144` with the package reporting no errors at all.
**Rule:** nothing in this repo renames a machine. If the naming convention
changes, it changes in the package.
## Drive letters - PESetup owns them
PESetup hardcodes `W:` in nine places - every copy destination, both DISM
offline sessions, `bcdboot`, `reagentc` - and creates it during its own disk
preparation.
`startnet.cmd` briefly had a volume finder that scanned for "the applied
Windows volume" and `diskpart`-assigned it to `W:`. On a re-image it found the
*previous* install, relabelled a partition PESetup was about to erase, and every
staging copy failed into a volume that no longer existed.
**Rule:** wait for `W:\Windows\System32\config\system` - the hive only exists
once the WIM apply has written it. Never run `diskpart` while PESetup is
running.
See `docs/PESETUP-INTERNALS.md`.
## Enrollment - the PPKG owns it, the orchestrator drives reboots
The SFLD package joins Entra using the BPRT token in
`0__Accounts_Azure.provxml`. A human then assigns the device category in Intune.
The vendor's `Start-BulkEnrollOrchestrator.ps1` has two branches. The normal one
aborts the package's own reboot, registers `AutoSecondReboot`, and drives the
Entra join to completion. The `-ManualFallback` one runs `sysprep /oobe /reboot`
- it is an interactive escape hatch for handing a machine back to OOBE, meant to
be triggered by a person.
The shopfloor unattend registered `-ManualFallback` as an at-logon scheduled
task. So OOBE completed, autologon fired, and four seconds later the machine
syspreped itself back to OOBE, losing the deployment chain permanently.
**Rule:** shopfloor bays enrol. Never wire `-ManualFallback` to anything
automatic.
## Kiosk URLs - GE-Enforce owns them
`plugins/geenforce/seed_display_scope.py` in shopdb-flask is authoritative:
| display-type.txt | route |
|---|---|
| `Dashboard` | `/shopdb/shopfloor` |
| `Lobby` | `/shopdb/tv` |
| `3DPrintRoom` | `/shopdb/parts-kiosk` |
Its dispatcher prefers the server-side role from Settings > Dashboard Defaults
(resolved by device IP), falls back to `C:\Enrollment\display-type.txt`, writes
the Startup shortcut itself, and **sweeps** any shortcut matching
`shopfloor-dashboard` or `/shopdb/`.
`site-config.json` also carries `edgeHomepage` per display type. Those are a
backstop for the window before the kiosk installer runs - a stale value there
gets deleted on the next enforce cycle rather than honoured.
**Rule:** if a kiosk points at the wrong page, fix the GE-Enforce scope first.
Keep `site-config.json` correct, but do not expect it to win.
## Repo vs share
The share is production; the repo is meant to describe it. Drift runs both ways -
live hand-edits nobody committed, and repo fixes never deployed.
`scripts/share-drift.py` classifies every mapped pair as `git-owned` (repo wins,
safe to push) or `unreconciled` (diverged, nobody has decided). It reports and
never writes. Run it before a build day; `scripts/preflight.py` covers the rest.
The unattends are `unreconciled` on purpose: the live copies are ~17 KB and the
repo copies ~12 KB, so pushing the repo would regress production.