# GB5 Version Management & Release Flow

**Status as of 2026-09-07** (see [§9](#9-implementation-status) for what changed since 2026-08-27). This document describes the version-management and release model
across Frontend (gb4.7mfe), Backend (GB5 Services), Database schema, and system-supplied
metadata — how each is versioned, how they stay in sync, and what each stakeholder does at each
stage. Where a piece is designed but not yet live-verified in production, this is called out
explicitly in [§9](#9-implementation-status).

---

## 1. Overview — Two Version Tracks, Not Four

The request that motivates this document treats FE, BE, DB, and metadata as four separate
things to version. In the actual implementation they resolve into **two synchronized tracks**:

| Track | Covers | Version unit | Where the version lives |
|---|---|---|---|
| **App release** | Frontend (gb4.7mfe) + Backend Services | one release = one `MAppVersion` row | `MAppVersion.VERSIONNUMBER` (GB5System DB) |
| **DB release** | Schema (DDL) + system-supplied reference/config data (DML) | one release = one `MSWUPGRADEPACKAGE` | `MSWUPGRADEPACKAGE.RELEASEVERSION` (SqlWorkbench), reflected per-tenant in `MDBLevelSetting.BUILDVERSION` |

Schema and metadata are **not** versioned independently of each other. A SqlWorkbench upgrade
package bundles both together — `MSWUPGRADEPACKAGEDDL` (the DDL scripts) and, optionally,
`MSWUPGRADEPACKAGEMETA` (DML / reference-data sync groups) — because a schema change and the
seed data it depends on must always move as one unit. There is no such thing as "DB schema is on
release 12 but metadata is on release 10."

FE and Backend Services are likewise **not** versioned independently — one `MAppVersion` row
names a single release that pairs a specific FE build with a specific Backend build
(`FEBaseURL` + `ServiceBaseURL` together), because the FE and its Services are developed,
built, and shipped as one unit.

The two tracks are independent of each other in *cadence* (a DB release and an app release do
not have to ship together) but are compared against each other at login/session time to detect
drift — that comparison, and what happens when it fails, is [§5](#5-how-fe-be-and-db-versions-correlate).

```
        DEV                 SCRIPT REVIEW           RELEASE                    RUNTIME
   ┌───────────┐          ┌──────────────┐      ┌───────────────┐        ┌─────────────────┐
   │ FE code   │          │              │      │ CI build       │        │ FE bundle        │
   │ BE code   │──────────┼──────────────┼─────▶│ stamps          │──────▶│ + Services       │
   │           │          │              │      │ BuildInfo       │        │ (MAppVersion row)│
   └───────────┘          └──────────────┘      └───────────────┘        └────────┬─────────┘
                                                                                    │ compared at
   ┌───────────┐          ┌──────────────┐      ┌───────────────┐        ┌────────▼─────────┐
   │ DDL/DML   │  Draft   │ Reviewed     │Approve│ MSWUPGRADE-   │ Execute│ MDBLevelSetting  │
   │ script    │─────────▶│ Approved     │──────▶│ PACKAGE       │──────▶│ .BUILDVERSION    │
   │ (SwDDL)   │          │ (SwApprover) │      │ (RELEASEVERSION)│ per   │ (per tenant DB)  │
   └───────────┘          └──────────────┘      └───────────────┘ tenant └──────────────────┘
```

---

## 2. Where Each Version Identity Lives

| What | Field | Format | Set by | Scope |
|---|---|---|---|---|
| App release (FE+BE pair) | `MAppVersion.VERSIONNUMBER` | dotted numeric, e.g. `4.2.13` | Releaser, once per release | Global (GB5System DB) |
| App release routing | `MAppVersion.ServiceBaseURL` / `FEBaseURL` / `ESSURL` | URL | Releaser | Global |
| App rollout state | `MAppVersion.IsApplicable` / `SecondVersion` / `IsSecondApplicable` / `SecondServiceBaseURL` | bit flags + a second version slot | Releaser | Global — supports a **dual-version rollout window**: two `MAppVersion` rows can be `IsApplicable=0` (applicable) at once, e.g. mid phased-rollout |
| FE build identity | `BuildInfo` (from `GET /Version/BuildInfo`) | build fingerprint stamped by CI | CI pipeline | Per FE deployment |
| BE build identity | `BuildInfo` (Services) | build fingerprint | CI pipeline | Per Services deployment |
| DB script status | `MSWDDLSCRIPT.SCRIPTSTATUS` | Draft → Reviewed → Approved → Deployed | Script author → Approver | Per script |
| DB release identity | `MSWUPGRADEPACKAGE.RELEASEVERSION` | dotted numeric, nullable | Releaser, only on release-boundary packages | Per package |
| DB release ordering | `MSWUPGRADEPACKAGE.SEQUENCEINCHAIN` / `SUPERSEDESPACKAGEID` | integer / FK chain | Releaser | Per package |
| DB per-tenant applied version | `MDBLevelSetting.BUILDVERSION` | dotted numeric (copied from the resolved package's `RELEASEVERSION`) | **Automatic** — `VersionSyncBLL`, event-driven | Per tenant database (singleton row) |
| DB per-tenant execution ledger | `SW.LSWDDLEXECUTION` / `SW.LSWPROVISIONINGLOG` | append-only rows | **Automatic** — written by SqlWorkbench on every script/package run | Per tenant database |

The one field a human never sets directly is `MDBLevelSetting.BUILDVERSION` — it is written
automatically by an event-driven subscriber the moment a package finishes applying to a tenant
(mechanism in [§4.4](#44-keeping-mdblevelsettingbuildversion-in-sync-automatically)). Every other
version field above is a deliberate human action at a specific stage.

---

## 3. The DB + Metadata Release Model

This is owned by **SqlWorkbench** (`GB5Solution/SqlWorkbench`) — a deliberately general-purpose
DDL/DML lifecycle tool, not a GB5-exclusive one. GB5 is one *consumer* of SqlWorkbench, in the
same way any other project could be. This has a direct practical consequence in §3.5.

### 3.1 Dev-stage tracking

Every developer with a schema or reference-data change registers it in SqlWorkbench **as they
write it**, not at release time:

- Create (or use) a `MSWSCRIPTBRANCH` (`BRANCHTYPE = Feature`) scoped to their initiative —
  the same shape as a git feature branch, including a `PARENTBRANCHID` for branching off
  another in-flight initiative.
- Register the script as a `MSWDDLSCRIPT` row with `SCRIPTSTATUS = Draft` on that branch.

This gives visibility and review from day one — no script exists only as a local file. It
replaces ad hoc, untracked `.sql` file authoring with a tracked workflow, and it's what lets
multiple developers work on different modules/initiatives concurrently without silently
colliding: each script's status and branch are visible to everyone, before anything is bundled
into a release.

### 3.2 Script review and approval

`SCRIPTSTATUS` moves through a fixed workflow:

```
Draft ──(author submits)──▶ Reviewed ──(approver signs off)──▶ Approved ──(bundled into a package)──▶ Deployed
```

The **Script Approver** role reviews Draft/Reviewed scripts for correctness, tenant-safety
(mandatory tenant filters, no destructive operations without a plan, index notes present), and
alignment with the target `MSWDBMODEL`. Only `Approved` scripts are eligible to be bundled into
a release package — an unapproved script cannot ship.

### 3.3 Release-time batch capture — where a "version" is actually born

At release time, the **Releaser** bundles the cycle's `Approved` scripts into one new
`MSWUPGRADEPACKAGE`:

- `PKGSTATUS`: `Draft → Testing → Released`.
- The package carries both the DDL (`MSWUPGRADEPACKAGEDDL`) and, if the cycle includes
  reference-data/metadata changes, the DML/metadata sync group (`MSWUPGRADEPACKAGEMETA`) —
  bundled together, never released as two separate artifacts.
- `SEQUENCEINCHAIN` and `SUPERSEDESPACKAGEID` place the package into an ordered chain relative
  to every prior package for the same `MSWDBMODEL` — this chain is what makes "several releases
  behind, upgrade to target version as one consistent package" possible (details in [§8](#8-upgrading-from-several-releases-behind)).
- Only on a package that represents an actual **release boundary** does the Releaser set
  `RELEASEVERSION` (a dotted string, e.g. `4.2.13`) — most packages (a single client's interim
  feature-branch bundle, a hotfix) leave it null. `RELEASEVERSION` is nullable by design:
  most packages in the chain are not release boundaries, only the ones a Releaser explicitly
  marks as such carry a version identity.

A package moving to `PKGSTATUS = Released` is the moment a DB "version" is born — nothing before
this point (Draft/Reviewed/Approved scripts, Draft/Testing packages) is a version; it's
in-progress work.

### 3.4 Applying a package to a tenant — the ChangeRequest workflow

Getting an approved, released package onto a specific tenant's actual database goes through
SqlWorkbench's `ChangeRequest` workflow (`SwBLL.ChangeRequest.ChangeRequestBLL`):

```
Save ──▶ Submit ──▶ Approve ──▶ Execute
```

- `ChangeRequestCategory` distinguishes intent: `SchemaChange`, `DataMigration`, `Performance`,
  `Security`, `Feature`, `BugFix`, `ClientProvisioning`.
- `Execute` resolves the target tenant's connection (`SW.MSWCLIENTDATABASE` → `SW.MSWDBSERVER`),
  picks a least-privilege execution role (`ClientDbLoginRole`: `Dba` / `App` / `ReadOnly`,
  matched to the change's `QueryType`), and runs the script(s) directly against that tenant's
  database.
- Every execution is recorded append-only in `SW.LSWDDLEXECUTION` / `SW.LSWPROVISIONINGLOG` —
  this is the real, permanent record of "what has actually run against this tenant," independent
  of any version label.
- On success, `Execute` publishes a Dapr event: `sqlworkbench.changerequest.executed` for
  `ClientProvisioning` (new-tenant onboarding), or `sqlworkbench.changerequest.applied` for
  every other category (ordinary post-provisioning changes to an already-live tenant). This is
  the trigger that keeps `MDBLevelSetting.BUILDVERSION` in sync — see [§4.4](#44-keeping-mdblevelsettingbuildversion-in-sync-automatically).

### 3.5 Dev vs. live database — a required practice, not optional

Because SqlWorkbench is general-purpose, GB5's own live, company-operations database is *just
another tenant* to it — `SW.MSWCLIENTDATABASE` supports registering more than one database
against the same `DBMODELID`, with nothing structurally preventing a developer from testing
against the live one by mistake. The required practice:

- Register a dedicated **non-production** `MSWCLIENTDATABASE` (e.g. `CLIENTDBCODE =
  'GB5-DEV'`) under the same `DBMODELID` as GB5's real live database.
- All development and script testing happens against the dev-designated database only.
- The live database receives packages exclusively through the normal release/promotion path
  (§3.3–§3.4) — never a direct or ad hoc script run.

This is documented as a general SqlWorkbench convention (not GB5-specific) in
`GB5Solution/claude.md`, since any project onboarded onto SqlWorkbench needs the same
separation.

---

## 4. The FE + BE Release Model

### 4.1 Build stamping

Every FE and Services build is stamped at CI time with a build fingerprint, exposed via
`GET /Version/BuildInfo`. This is *not* the release version — it's a per-build identifier (think
commit SHA + build timestamp) used to prove exactly which artifact is actually running, distinct
from which logical release it belongs to.

### 4.2 Release identity

A release is one row in `MAppVersion`:

| Column | Meaning |
|---|---|
| `AppVersionId` | PK |
| `VersionNumber` | the dotted release string, e.g. `4.2.13` |
| `ServiceBaseURL` / `FEBaseURL` / `ESSURL` | where this release's Backend/FE/ESS live |
| `IsApplicable` | whether this release is currently one of the accepted/serving releases |
| `SecondVersion` / `IsSecondApplicable` / `SecondServiceBaseURL` | a second, concurrently-applicable release — supports a phased/dual rollout window (e.g. cutting over a subset of tenants gradually) |

Because more than one `MAppVersion` row can legitimately have `IsApplicable=0` at once (the
dual-rollout window), version comparison logic must never assume a single "the" applicable
release — see `CompareVersionStrings` in §5.

### 4.3 Release finalization

The Releaser writes the new `MAppVersion` row when the FE+BE release is finalized and ready to
serve traffic — this is a manual, deliberate step, the FE/BE analogue of a DB package moving to
`PKGSTATUS = Released`.

### 4.4 Keeping `MDBLevelSetting.BUILDVERSION` in sync — automatically

This is the mechanism that answers "how do these stay in sync without a human keying in a
version number after every DB release":

```
ChangeRequestBLL.Execute() succeeds
        │
        ▼  publishes Dapr event "sqlworkbench.changerequest.applied"
        │  { ChangeRequestId, ClientDatabaseId }  — on pubsub component "pubsub"
        ▼
VersionSyncSubscribeController (GB5Framework)
        │  reacts, forwards the event's own embedded Login (the same session that
        │  already succeeded against this tenant inside SqlWorkbench moments earlier)
        ▼
VersionSyncBLL.SyncVersionAsync
        │  1. calls SqlWorkbench GET /Provisioning/GetCurrentVersion?ClientDbId=
        │     → walks SupersedesPackageId from the tenant's most-recently-applied
        │       package back to the nearest package with a non-null RELEASEVERSION
        │  2. calls SqlWorkbench GET /ClientDatabase/GetClientDatabaseById?ClientDbId=
        │     → resolves ServerConfigId (the tenant's MSERVERCONFIG registration)
        │  3. opens a direct connection to THAT tenant's own database
        ▼
DBLevelSettingDAL.UpdateBuildVersionOnConnection
        → writes MDBLevelSetting.BUILDVERSION on the tenant's own database
```

Two properties of this design matter and are deliberate:

- **SqlWorkbench never gains GB5-specific knowledge.** It publishes a generic event with no
  awareness of `MDBLevelSetting` or any other GB5 table. All GB5-specific reaction logic lives
  entirely outside SqlWorkbench, in `GB5Framework`. This mirrors the same pattern already used
  by Entitlement's onboarding (`ClientProvisioningExecutedSubscriber`, reacting to the sibling
  `sqlworkbench.changerequest.executed` event for new-tenant provisioning).
- **`MAppVersion` is never touched by this path.** It stays the separate, manual, release-time
  write described in §4.3 — the automatic sync only ever updates the *DB side* of the equation,
  per tenant.

---

## 5. How FE, BE, and DB Versions Correlate

At login/session time, `VersionDAL.VersionCheck` runs one query that cross-joins the tenant's
own `MDBLevelSetting` row against every currently-applicable `MAppVersion` row:

```sql
SELECT a.BuildVersion AS DBVersionNumber, b.AppVersionId, b.VersionNumber AS AppVersionNumber, ...
FROM   MDBLevelSetting a
CROSS JOIN MAppVersion b
WHERE  b.IsApplicable = 0
```

Because more than one `MAppVersion` row can be applicable at once (§4.2's dual-rollout window),
this returns every *candidate* pairing, not a single answer — deciding which one this tenant
should actually be running is comparison logic in `VersionBLL`, not the query.

Comparison uses `CompareVersionStrings` — a per-segment **numeric** comparison of the dotted
version strings (`4.1.10` correctly sorts after `4.1.9`). This replaced an earlier bug where the
comparison was plain lexicographic `NVARCHAR` `</>` , which silently misordered any version once
a segment reached double digits.

The result feeds the FE's version-mismatch banner (`version-mismatch-banner.component`):

- If the tenant's `MDBLevelSetting.BUILDVERSION` doesn't match what the currently-running
  `MAppVersion` expects, the banner surfaces the mismatch instead of failing silently or
  showing a cryptic error.
- A **"More info"** panel shows the actual DB version, FE build info, and BE build info
  side by side (`dbVersionInfo` / `feBuildInfo` / `beBuildInfo`), plus **actionable guidance**
  (`DB_MISMATCH_ACTION` map) — telling the user concretely what to do (e.g. "a database update
  is pending — contact your administrator to apply it" vs. "your browser has a stale build,
  refresh") rather than an unexplained version number.

This is the single enforcement point where drift between the two tracks (App release vs. DB
release) becomes visible to a real user, with a concrete next step attached.

---

## 6. Stakeholder Playbook

### 6.1 FE Developer
1. Work on a feature branch as normal.
2. No DB/version action required unless the feature needs a new endpoint or DB field — in
   that case, coordinate with the BE/DB developer for that change (§6.2/§6.3).
3. On merge, CI stamps the build with a `BuildInfo` fingerprint automatically — no manual step.

### 6.2 BE Developer
1. Work on SL/BLL/DAL as normal, following the layer rules in `CLAUDE.md`.
2. If the change requires a schema or reference-data change, author it as a DDL/DML script and
   hand off to the DB Script Developer flow (§6.3) rather than applying it by hand.
3. On merge, CI stamps the Services build with a `BuildInfo` fingerprint automatically.

### 6.3 DB Script Developer
1. Register the script as a `MSWDDLSCRIPT` (`Draft`) on a `Feature`-type `MSWSCRIPTBRANCH`
   scoped to your initiative.
2. Write and test the script **only** against the dev-designated `MSWCLIENTDATABASE` (§3.5) —
   never GB5's own live database.
3. Submit for review when ready (`SCRIPTSTATUS → Reviewed`).

### 6.4 Script Approver
1. Review Draft/Reviewed scripts: correctness, tenant-safety (mandatory tenant filter present,
   no unreviewed destructive operations), index notes present, alignment with `MSWDBMODEL`.
2. Approve (`SCRIPTSTATUS → Approved`) or send back with comments.
3. Only `Approved` scripts are eligible for the next release bundle.

### 6.5 Build Engineer / CI
1. Build FE and Services on merge to the release branch.
2. Stamp each build with `BuildInfo` (fingerprint), exposed via `GET /Version/BuildInfo`.
3. No version-number decision happens here — that's the Releaser's step.

### 6.6 Releaser / Release Manager
1. **DB side:** bundle the cycle's `Approved` scripts into one new `MSWUPGRADEPACKAGE`
   (`PKGSTATUS: Draft → Testing`). Chain it correctly against the prior package
   (`SEQUENCEINCHAIN` / `SUPERSEDESPACKAGEID`). If this package represents an actual release
   boundary, set `RELEASEVERSION`. Move to `PKGSTATUS = Released` when testing passes.
2. **App side:** write the new `MAppVersion` row (`VersionNumber`, `ServiceBaseURL`,
   `FEBaseURL`, `IsApplicable`), coordinating the dual-rollout fields if this is a phased cutover.
3. These two steps are independent in timing — a DB release and an app release don't have to
   land together — but should use the **same `RELEASEVERSION`/`VersionNumber` string** whenever
   they're meant to be consumed as a matched pair, since that's what the mismatch banner
   compares.

### 6.7 Deployment / Ops
1. Deploy the new FE/Services build to the app tier(s).
2. For each tenant that should receive the DB release: drive the `ChangeRequest` workflow
   (Submit → Approve → Execute) against that tenant's own `ClientDatabaseId`. This can run
   tenant-by-tenant on the client's own schedule (§8), not necessarily all at once.
3. No manual `MDBLevelSetting` write is needed — the event-driven subscriber (§4.4) handles it
   the moment `Execute` succeeds.

### 6.8 Tenant Admin
1. Decides *when* their tenant adopts a pending release — a client may stay several releases
   behind deliberately, then upgrade later (§8 explains why this is always safe).
2. Approves/triggers the `ChangeRequest` execution for their own tenant (directly, or through
   whatever admin UI wraps that workflow).
3. Sees the version-mismatch banner (§5) if their tenant's DB and the currently-deployed app
   release have drifted, with concrete guidance on what to do next.

### 6.9 End User
1. Normally sees nothing — versions match, no banner appears.
2. On a genuine mismatch, sees the banner's actionable guidance (not a cryptic message): what's
   out of sync, and who to contact (the Tenant Admin) to resolve it.

---

## 7. End-to-End Walkthrough

A concrete example, tying every stage together:

1. A BE developer needs a new column and its default value. They register `ADD_COLUMN_X.sql`
   as `Draft` on `MSWSCRIPTBRANCH "feature/order-priority"`, test it against `GB5-DEV`.
2. The Script Approver reviews and marks it `Approved`.
3. At the next release cutoff, the Releaser bundles it (plus other approved scripts from the
   same cycle) into `MSWUPGRADEPACKAGE #47`, chains it after package `#46`
   (`SupersedesPackageId = 46`), tests it, sets `RELEASEVERSION = "4.3.0"`, and marks it
   `Released`.
4. Simultaneously (or separately — no ordering requirement), the Releaser finalizes the FE/BE
   build for the same cycle as `MAppVersion.VersionNumber = "4.3.0"`.
5. Ops deploys the new FE/Services build. Tenant *Acme Corp* is still on release `4.1.0` and
   has not yet upgraded — they keep running the old FE/Services build against their DB, no
   mismatch, no forced upgrade.
6. Three months later, *Acme Corp*'s admin decides to upgrade. Ops executes the `ChangeRequest`
   for their `ClientDatabaseId` — SqlWorkbench walks and applies every package in the chain
   from their current position up through `#47` (`4.2.0`, then `4.3.0`'s package), not just the
   latest one in isolation.
7. `Execute` succeeds, publishes `sqlworkbench.changerequest.applied`. `VersionSyncBLL` resolves
   the tenant's now-current version (walks the chain to the nearest `RELEASEVERSION`, finds
   `"4.3.0"`) and writes it to *Acme Corp*'s own `MDBLevelSetting.BUILDVERSION`.
8. *Acme Corp*'s users are pointed at the `4.3.0` FE/Services build (per the now-current
   `MAppVersion` row). `VersionCheck` compares `4.3.0` (DB) against `4.3.0` (App) — match, no
   banner.

---

## 8. Upgrading From Several Releases Behind

This is the guarantee the package-chain design (§3.3) exists specifically to provide: **a
tenant can be arbitrarily far behind and still upgrade to any target release as one consistent,
ordered operation, with no possibility of a partial or mismatched state.**

Mechanically:

- Every release-boundary package is linked to its predecessor via `SupersedesPackageId`, forming
  a single ordered chain per `MSWDBModel` (`SEQUENCEINCHAIN` gives the chain its total order).
- `ProvisioningBLL.GetCurrentVersion` resolves "what is this tenant's version *right now*" by
  finding the most recently fully-applied package (from `LSWPROVISIONINGLOG`, the real
  execution ledger — not a label) and walking `SupersedesPackageId` back to the nearest package
  that carries a `RELEASEVERSION`. "Unversioned" is a legitimate answer (a brand-new or
  never-released-to tenant), never an error.
- When a tenant upgrades to a target release, the Provisioning workflow resolves the **entire
  intervening chain segment** — every package between the tenant's current position and the
  target — and applies it as one ordered sequence, not just the target package in isolation.
  This is what prevents the exact failure mode the request calls out: a tenant landing with the
  target release's schema but missing an intermediate release's metadata (or vice versa), because
  every step in between is applied, in order, every time.
- Because DDL and metadata are bundled together in every package (§3.1), there is no window
  where a tenant has the new schema but old reference data, or new reference data with an old
  schema — the package is the atomic unit, not the individual script.

---

## 9. Implementation Status

Grounding this document in what's actually built, as of 2026-09-07:

| Piece | Status |
|---|---|
| `MSWSCRIPTBRANCH` / `MSWDDLSCRIPT` / `SCRIPTSTATUS` workflow | Live in SqlWorkbench |
| `MSWUPGRADEPACKAGE` (DDL+META bundling, chain fields) | Live in SqlWorkbench |
| `ChangeRequest` Save→Submit→Approve→Execute workflow | Live in SqlWorkbench |
| `MSWUPGRADEPACKAGE.RELEASEVERSION` + `ProvisioningBLL.GetCurrentVersion` | Merged to `dev`, **live-verified** on GB5DEMO 2026-09-05 after fixing a real bug (`GetCurrentVersion`'s `clientDbId <= 0` guard rejected every real, negative-autonumber `ClientDbId`) |
| `sqlworkbench.changerequest.applied` event (broadened from `ClientProvisioning`-only) | Merged to `dev`, live |
| `VersionSyncSubscribeController` / `VersionSyncBLL` (auto-writes `MDBLevelSetting.BUILDVERSION`) | **Merged to `dev` and deployed** to `gb5.service`/`gb5-platformhost.service` 2026-09-05. **Live-verified**: a direct `/versionsync/handle` call against a real tenant (GB5DEMO's `FullBaseTest1`) correctly wrote `BUILDVERSION`/`BUILDNUMBER`, confirmed via success log line and SQL query. Fixed 4 more real bugs found during that verification — see below |
| `VersionDAL.VersionCheck` + numeric `CompareVersionStrings` | Live on `dev` |
| Version-mismatch banner "More info" + actionable guidance | Code-complete on `GBDEV4.7`; **`gbhost` (the only app that mounts it) now builds cleanly** as of 2026-09-07 (see §9a) — the banner's compiled chunk is confirmed present and wired into the real bundle, not yet visually click-tested in a browser |
| `MAppVersion` release-time write | Manual step, always has been — no tooling change in this cycle |
| Dev-vs-live `MSWCLIENTDATABASE` practice | Documented in `GB5Solution/claude.md`, adoption is a process change, not a code change |

**Bugs found and fixed during 2026-09-05 live verification** (all on `dev`, all confirmed live
before/after on GB5DEMO): `ProvisioningBLL.GetCurrentVersion`'s sign-check guard rejected real
`ClientDbId`s; `GET_MOST_RECENTLY_APPLIED_PACKAGE` and `GET_LINEAGE` both excluded every
real package because they filtered a shared-template table (`MSWUPGRADEPACKAGE`/
`MSWUPGRADEPACKAGEDDL`) by the querying tenant instead of allowing the shared `-1` sentinel
already used elsewhere; `VersionSyncSubscribeController` itself had the same sign-check bug,
meaning it would have silently no-op'd on every real production event; `UPDATE_BUILD_VERSION`
never set `BUILDNUMBER`, violating a live `CHECK` constraint that ties it to `BUILDVERSION`.
The originally-flagged "no service-account LoginDTO pattern" design question turned out to be a
non-issue: `ChangeRequestBLL.Execute()` always forwards the real caller's own tenant-scoped
login, so the subscriber's reuse of the event's embedded `LoginDTO` is correct as designed — no
queue-worker path with a mismatched system login exists in this codebase.

**Not yet verified**: the *natural* trigger path (a real `ChangeRequest.Execute()` call
publishing the event, rather than a direct call to the subscriber's own endpoint) — blocked by
GB5DEMO's Vault being sealed, an unrelated pre-existing ops issue, not a defect in this feature.

### 9a. `gbhost` build blocker — resolved 2026-09-07

The long-standing `assert(compilation)`/esbuild-deadlock crash that had never once let `gbhost`
build cleanly (see the FE repo's own build-troubleshooting history) did not reproduce on a
genuinely fresh install. Recipe: `rm -rf node_modules .angular && npm ci`, then
`NG_BUILD_PARALLEL_TS=0 NODE_OPTIONS=--max-old-space-size=6144 ng build gbhost --configuration
production` — completed cleanly, producing a real `remoteEntry.json` (the federation-artifact
phase that always crashed before) and the version-mismatch banner's own compiled chunk, wired
into 4 chunks reachable from `main`. No `package.json`/dependency changes were needed. This
history's own documented non-determinism means this isn't guaranteed to hold forever — treat it
as a working, reproducible recipe, not a permanent fix, until it's been repeated a few more times
across sessions.

---

## 10. Summary — The Core Invariants

1. **A DB "version" is a package, never an individual script.** Schema and metadata inside it
   are bundled and move together, atomically.
2. **An App "version" is one `MAppVersion` row, pairing FE and BE together**, never versioned
   independently of each other.
3. **The two tracks compare, they don't drive each other.** A DB release and an app release can
   ship on different schedules; `VersionCheck` is the sync point that detects drift and a human
   (the Tenant Admin) decides when to close it.
4. **Every tenant can be arbitrarily far behind and still catch up safely**, because upgrading
   always applies the full ordered chain from the tenant's current position to the target, never
   just the target package in isolation.
5. **`MDBLevelSetting.BUILDVERSION` is never hand-typed** — it is written automatically,
   event-driven, the moment a package finishes applying to a tenant. Every other version field
   in this document is a deliberate human decision at a named stage (Approve, Release, Execute).
