Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / DEPLOY_PLAN.md |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | DEPLOY_PLAN.md |
| Original SHA-256 | f6e870f400a48efcbe2aaa6e22665d5d7ec399bb0d33da7ee1563f867048ef3c |
| Git blob SHA | 33eb6fe1d6c7d30651433e168c1d55e95c75b6ca |
| Reading-copy SHA-256 | f6e870f400a48efcbe2aaa6e22665d5d7ec399bb0d33da7ee1563f867048ef3c |
Every recorded occurrence
These 23 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / DEPLOY_PLAN.md | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | ai-brain-live | aac32fc49c878cc5bca099459307611562d0bac1 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / DEPLOY_PLAN.md | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓375 lines · 22.4 KB
# DEPLOY_PLAN.md — AI Intelligence Layer cutover
The build is validated in a staging posture. This is the exact, ordered, rollback ready plan for the
**human** to run the irreversible live cutover. It is the one manual step: Bubbles serves other live
paying client sites and this build provisions cleared data databases.
**Do not run the Bubbles steps automatically.** Everything below was dry run and validated; the operator
executes the Bubbles and live database steps.
## State at handoff (already done, staging)
- The MEGAMIND gateway is **running on M4** (`100.94.8.94:8090`, Tailscale only, via `nohup`) and smoke
tested. Ollama on M4 has `all-minilm` (384) and `llama3.1:8b`.
- The shared production database on M4 is at **goose v17**; `0017_ai_memory.sql` is applied (additive, a
new table the old API binary does not use, so it is safe alongside the running old binary).
- `skills.embedding` (61/61) and the demo org `candidates.resume_embedding` (1511/1511) are backfilled.
- A staging tenant database `tenant_staging_demo` was provisioned on M4 to validate `provision-tenant`.
- The Bubbles `mm-api` service is **unchanged** (still the pre AI binary).
## Merge sequence (do this FIRST, before any cutover step below)
The stack is three branches: the brain sits on W3, which sits on the AI intelligence layer. Merge them in
this order, as three DISCRETE steps, each merged and validated and individually rollback able, because each
carries its own migration and is a separate revertable unit. Do NOT collapse them into one merge.
**Step M1 - merge the AI intelligence layer branch first (the foundation; carries migration 0017,
ai_memory).** It is the base of the stack and the only branch the others depend on. Validate (build FIPS,
unit suite, integration on a staging database) before proceeding.
Rollback: revert the merge commit. If 0017 was already applied to a database, `goose down` one step (the
0017 down drops ai_memory), per the rollback in `db/migrations/0017_ai_memory.sql`.
**Step M2 - then merge W3 (carries migration 0018, recommendation_runs and staffing_recommendations).**
Only after M1 is merged and green. Validate again.
Rollback: revert the W3 merge commit. If 0018 was applied, `goose down` to v17 (the 0018 down DROPs the two
W3 tables), per the rollback referenced in Step W3.1.
**Step M3 - then merge the brain (carries migration 0019, the v1.1.0 model version registry row).** Only
after M2 is merged and green. The brain adds NO new schema table; 0019 inserts one row into
aedt_model_versions registering the recalibrated semantic threshold (v1.1.0), which the engine now stamps
onto every decision. Validate again.
Rollback: revert the brain merge commit. If 0019 was applied, `goose down` to v18 (the 0019 down DELETEs the
v1.1.0 registry row), per the rollback in `db/migrations/0019_model_version_v1_1_0.sql`. Note that reverting
the brain code without reverting 0019 is safe (a registry row with no stamping engine is inert), and
reverting to a binary that stamps v1.0.0 with the v1.1.0 row present is also safe.
**Hard rule.** Do not collapse these into one merge, and do NOT run the production cutover sequence below
until all three branches are merged in this M1, M2, M3 order AND the migrations through 0019 are applied to
the production shared database. The cutover steps assume a single merged tree at migration head v19.
## Build the artifacts (on the build host, no live mutation)
```
cd ~/missionmatch && git checkout ai-intelligence-layer
# API (Bubbles target), FIPS, verify the stamp:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFIPS140=v1.0.0 go build -trimpath -o /tmp/mm-api ./cmd/api
go version -m /tmp/mm-api | grep GOFIPS140 # must show GOFIPS140=v1.0.0
# Gateway + tools (M4 target):
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o /tmp/megamind-gateway ./cmd/megamind-gateway
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o /tmp/provision-tenant ./cmd/provision-tenant
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o /tmp/embed-backfill ./cmd/embed-backfill
# Frontend (unchanged this cutover; rebuild only if you changed it):
cd frontend && npm run build
```
## Step 1 — durable MEGAMIND gateway on M4 (launchd)
Replaces the staging `nohup` with a managed service.
```
scp /tmp/megamind-gateway joseph@100.94.8.94:/tmp/megamind-gateway
scp infra/launchd/com.missionmatch.megamind-gateway.plist joseph@100.94.8.94:/tmp/
ssh joseph@100.94.8.94 '
pkill -f /tmp/megamind-gateway || true # stop the staging nohup instance
sudo cp /tmp/megamind-gateway /usr/local/bin/megamind-gateway && chmod +x /usr/local/bin/megamind-gateway
cp /tmp/com.missionmatch.megamind-gateway.plist ~/Library/LaunchAgents/
launchctl unload ~/Library/LaunchAgents/com.missionmatch.megamind-gateway.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.missionmatch.megamind-gateway.plist
'
# Verify:
curl -s http://100.94.8.94:8090/healthz # -> ok
curl -s http://100.94.8.94:8090/v1/embed -d '{"inputs":["x"]}' | jq .dimension # -> 384
```
Rollback: `ssh joseph@100.94.8.94 'launchctl unload ~/Library/LaunchAgents/com.missionmatch.megamind-gateway.plist'`.
Optional quality upgrade (no migration, both 384): pull bge-small and set `MEGAMIND_EMBED_MODEL`:
`ssh joseph@100.94.8.94 'ollama pull <bge-small-en-v1.5-gguf>'` then re run the backfill (Step 4).
## Step 2 — env on Bubbles (operator; root owned 0600 files)
Add the AI plane variables to `/etc/missionmatch/api.env` (back it up first):
```
sudo cp /etc/missionmatch/api.env /etc/missionmatch/api.env.bak.$(date +%s)
# append (do not echo secrets to a log):
MM_EMBED_PROVIDER=megamind
MM_MEGAMIND_URL=http://100.94.8.94:8090
MM_EMBED_DIMENSION=384
MM_MEGAMIND_EMBED_MODEL=all-minilm
MM_MEGAMIND_GENERATE_MODEL=llama3.1:8b
MM_MEGAMIND_TEACHING_ENABLED=false
```
Remove the dead keys from `/etc/missionmatch/sam-sync.env` (back it up first): `DATA_GOV_API_KEY`,
`KAGGLE_USERNAME`, `KAGGLE_KEY`, `HF_TOKEN`.
Rollback: restore the `.bak` files.
## Step 3 — database (already at v17; only new tenants need provisioning)
The shared database is at v17. If you maintain it, confirm:
```
ssh joseph@100.94.8.94 'PGURL=... goose -dir db/migrations postgres "$PGURL" status' # 0017 applied
```
For a NEW client (database per tenant), provision a fresh database (this is the irreversible "create a
live client database" step; run only for a real onboarding):
```
ssh joseph@100.94.8.94 '/usr/local/bin/provision-tenant \
-admin-dsn "host=/tmp user=postgres dbname=postgres sslmode=disable" \
-name tenant_<client_slug>'
```
It applies the full public + eeo migration set, RLS forced, eeo segregated, vector(384) ready, and
verifies the app role has no grant on `eeo.candidate_demographics`. Rollback (only if the database is
brand new and unused): `DROP DATABASE tenant_<client_slug>`.
## Step 4 — embed backfill (idempotent)
For the shared database (the demo org is already done) or a freshly provisioned tenant database, once it
has candidates:
```
ssh joseph@100.94.8.94 '/usr/local/bin/embed-backfill \
-dsn "postgres://missionmatch_migrator:<pw>@127.0.0.1:5432/<db>?sslmode=disable" \
-megamind http://100.94.8.94:8090 -what all -only-missing -batch 64'
```
`-only-missing` makes it safe to re run. Rollback: `UPDATE skills SET embedding=NULL; UPDATE candidates
SET resume_embedding=NULL;` then re run (or leave; embeddings are additive).
## Step 5 — THE CUTOVER on Bubbles (operator, irreversible; brief downtime)
Swap the mm-api binary to the AI build and restart. Back up the current binary first.
```
scp /tmp/mm-api user@bubbles:/tmp/mm-api.new
ssh user@bubbles '
sudo cp /home/user/missionmatch/mm-api /home/user/mm-api.bak.$(date +%s) # backup
sudo systemctl stop mm-api
sudo cp /tmp/mm-api.new /home/user/missionmatch/mm-api && sudo chmod +x /home/user/missionmatch/mm-api
sudo systemctl start mm-api
sleep 2 && systemctl is-active mm-api
'
# Verify the live API still answers and FIPS is on:
curl -s https://missionmatch.thatwebhostingguy.com/healthz # -> ok
curl -s https://missionmatch.thatwebhostingguy.com/healthz/fips | jq .fips_enabled # -> true
```
**Rollback (fast):**
```
ssh user@bubbles '
sudo systemctl stop mm-api
sudo cp /home/user/mm-api.bak.<ts> /home/user/missionmatch/mm-api
sudo systemctl start mm-api
'
# and restore /etc/missionmatch/api.env.bak.<ts> if the env changes caused the issue.
```
Nginx is **unchanged** by this cutover (the gateway is reached over Tailscale, not through nginx). No
`nginx -t` or reload is required. The systemd unit `mm-api.service` is unchanged; only the binary and
the env file change.
## Step 6 — host service hygiene (operator)
The audit found the `mm-sam-sync.timer` showed no next elapse. The unit is valid (`OnCalendar=*-*-*
04:00:00`); ensure it is started:
```
ssh user@bubbles 'sudo systemctl daemon-reload && sudo systemctl enable --now mm-sam-sync.timer && systemctl list-timers | grep mm-sam-sync'
```
## Activating the AI HTTP surface (a later, separate change)
The orchestrator, TAG, RAG, and memory are built and tested as a library but are not yet mounted as
authenticated routes (deferred so this cutover does not change the user facing app). Mounting them is a
small code change behind the session resolver, RBAC, and the same audit, then a repeat of Step 5.
## Post cutover checks
- `curl https://missionmatch.thatwebhostingguy.com/healthz/fips` shows `fips_enabled: true`.
- The MEGAMIND gateway `launchctl list | grep megamind` is running on M4.
- `make ci-gates` on the build host stays green (backdoor + FIPS).
- No EEO reach: `go test ./internal/modules/ai/...` (the tripwire) stays green in CI.
## W3 AI Staffing Recommendations cutover (additive, on top of the AI layer)
W3 is a backend addition: migration 0018, four new authenticated routes already mounted in the mm-api
binary, and one new scheduled job. It is additive and reversible. As with the AI layer, the operator runs
the Bubbles and production database steps; everything below was built and validated on M4 staging
(migration 0018 applies cleanly through v18 plus eeo, and 5 of 5 W3 integration tests pass).
### State at handoff (already done, staging)
- Migration 0018 (recommendation_runs, staffing_recommendations) is written and proven on a fresh M4
staging database `tenant_w3_staging` (goose v18). It is NOT applied to the production shared database.
- The mm-api binary on the build host now carries the four W3 routes (staffing.recommend gated), but the
Bubbles mm-api is unchanged (still the pre AI binary), so nothing is live yet.
- The new `staffing-proactive-sync` binary and `mm-staffing-proactive.{service,timer}` are written but NOT
installed on Bubbles.
### Build the artifacts (build host, no live mutation)
```
cd ~/missionmatch && git checkout w3-ai-staffing-recommendations
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFIPS140=v1.0.0 go build -trimpath -o /tmp/mm-api ./cmd/api
go version -m /tmp/mm-api | grep GOFIPS140 # must show GOFIPS140=v1.0.0
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFIPS140=v1.0.0 go build -trimpath -o /tmp/staffing-proactive-sync ./cmd/staffing-proactive-sync
```
### Step W3.1 - apply migration 0018 to the production shared database (operator, irreversible)
This is the one irreversible step. 0018 only ADDS two tables and is safe alongside the running old binary
(the old binary does not use them). Run as the migrator role.
```
ssh joseph@100.94.8.94 'cd ~/missionmatch && \
goose -dir db/migrations postgres "postgres://missionmatch_migrator:<pw>@127.0.0.1:5432/missionmatch?sslmode=disable" up'
# verify: goose ... status shows 0018 applied, head v18
```
Rollback (only before any recommendation rows matter): `goose ... down` once, or
`DROP TABLE staffing_recommendations; DROP TABLE recommendation_runs;`.
### Step W3.2 - deploy the mm-api binary that carries the W3 routes (operator, brief downtime)
Same binary swap as the AI layer Step 5; this build also mounts the four W3 routes behind
staffing.recommend / slate.write / candidate.read. Back up the current binary first.
```
scp /tmp/mm-api user@bubbles:/tmp/mm-api.new
ssh user@bubbles '
sudo cp /home/user/missionmatch/mm-api /home/user/mm-api.bak.$(date +%s)
sudo systemctl stop mm-api
sudo cp /tmp/mm-api.new /home/user/missionmatch/mm-api && sudo chmod +x /home/user/missionmatch/mm-api
sudo systemctl start mm-api
sleep 2 && systemctl is-active mm-api
'
curl -s https://missionmatch.thatwebhostingguy.com/healthz/fips | jq .fips_enabled # -> true
```
Rollback: stop mm-api, restore `/home/user/mm-api.bak.<ts>`, start mm-api.
### Step W3.3 - install the proactive recompute timer (operator)
```
scp /tmp/staffing-proactive-sync user@bubbles:/home/user/missionmatch/staffing-proactive-sync
ssh user@bubbles '
sudo cp infra/systemd/mm-staffing-proactive.* /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mm-staffing-proactive.timer
systemctl start mm-staffing-proactive.service # one off, to seed the first bench watch
systemctl list-timers | grep mm-staffing-proactive
'
```
Rollback: `sudo systemctl disable --now mm-staffing-proactive.timer`. The watch freshness depends on the
ingestion timers (mm-sam-sync, mm-opp-sync); the proactive timer is scheduled after them at 05:00.
### Post cutover checks (W3)
- `curl -sf -X POST https://missionmatch.thatwebhostingguy.com/api/missionmatch/recommendations/portfolio` with a
valid session returns a 200 allocation (or 403 without staffing.recommend); an unauthenticated call is 403.
- A staffing run writes one `aedt.staffing.recommended` audit and a decision writes one `aedt.staffing.decided`.
- `go test -tags=integration -run TestStaffing ./internal/integration/` stays green against the database.
- The EEO tripwire `go test ./internal/modules/ai/...` stays green (it now walks the staffing package too).
### Note: feed freshness
The proactive bench watch only surfaces an opportunity once the ingestion timers (mm-sam-sync, mm-opp-sync)
have ingested it. The audit previously flagged the SAM sync timer as possibly not firing; confirm
`systemctl list-timers | grep mm-sam-sync` shows a next elapse before relying on the watch for new postings.
## AI brain activation cutover (mount the brain live, additive)
This activates the brain on the SHARED RLS database that runs today (per tenant pools are a sequenced
follow on, out of scope). It mounts one new route (POST /ai/query), switches the live embedder to
bge-small-en-v1.5, and re embeds the vectors. Validated end to end on M4 staging (the brain_staging
database, 46 of 48 integration pass, 1 environmental fail unrelated to the brain, 4 of 4 brain tests pass).
Nothing below was run against production.
### Dependency (must be true first)
- All three branches are merged in the M1, M2, M3 order above, and the migrations through 0019 are applied
to the production shared database (ai_memory is 0017 from the AI layer, the W3 tables are 0018, and the
v1.1.0 model version registry row is 0019 from the brain branch, added by the threshold recalibration).
The brain branch sits on top of both and carries only that one registry row migration, no new table.
- Apply 0019 to production if it is not yet at v19 (additive, a single registry row, safe alongside the
running binary):
```
ssh joseph@100.94.8.94 'PGURL="postgres://missionmatch_migrator:<pw>@127.0.0.1:5432/missionmatch?sslmode=disable"; goose -dir db/migrations postgres "$PGURL" up'
ssh joseph@100.94.8.94 'PGURL=...; goose -dir db/migrations postgres "$PGURL" status' # head v19, 0019 applied
```
Rollback: `goose -dir db/migrations postgres "$PGURL" down` to v18 (DELETEs the v1.1.0 registry row).
- bge-small-en-v1.5 is available on the production Ollama. It is NOT in the official Ollama library; pull
the community handle (384 dim, standard /api/embed):
```
ssh joseph@100.94.8.94 'curl -s http://127.0.0.1:11434/api/pull -d "{\"name\":\"qllama/bge-small-en-v1.5\",\"stream\":false}"'
# probe: the embedding must be 384 wide
ssh joseph@100.94.8.94 'curl -s http://127.0.0.1:11434/api/embed -d "{\"model\":\"qllama/bge-small-en-v1.5\",\"input\":\"x\"}" | head -c 60'
```
### Step B1 - env on Bubbles (operator, root owned 0600 files; back up first)
Append to /etc/missionmatch/api.env:
```
MM_EMBED_PROVIDER=megamind
MM_MEGAMIND_URL=http://100.94.8.94:8090
MM_EMBED_DIMENSION=384
MM_MEGAMIND_EMBED_MODEL=qllama/bge-small-en-v1.5
MM_MEGAMIND_GENERATE_MODEL=llama3.1:8b
```
Set the same MM_MEGAMIND_EMBED_MODEL in the sync env used by embed-backfill, and update the M4 gateway
launchd env MEGAMIND_EMBED_MODEL to qllama/bge-small-en-v1.5 (the rerank path uses the gateway default).
Rollback: restore the .bak files.
### Step B2 - re backfill the production vectors with bge (operator, irreversible to the prior vectors)
CRITICAL: production currently holds all-minilm vectors (from the AI layer). bge and all-minilm vectors are
NOT comparable, so the query (bge) versus stored (all-minilm) mismatch would silently corrupt retrieval.
Re embed EVERYTHING with bge BEFORE serving the brain. NULL the columns first so a row that errors mid run
is left unembedded (a safe no semantic match) rather than retaining a stale all-minilm vector, then run a
FULL re embed (no -only-missing, so every row is rewritten with bge):
```
ssh joseph@100.94.8.94 'psql -U postgres -d missionmatch -c "UPDATE candidates SET resume_embedding=NULL; UPDATE skills SET embedding=NULL"'
ssh joseph@100.94.8.94 '/usr/local/bin/embed-backfill \
-dsn "postgres://missionmatch_migrator:<pw>@127.0.0.1:5432/missionmatch?sslmode=disable" \
-megamind http://100.94.8.94:8090 -model qllama/bge-small-en-v1.5 -what all -batch 64'
```
THE RE EMBED GATE (enforced, not a glance). The verify below is a hard ASSERTION: it RAISES (non zero exit
under ON_ERROR_STOP) unless every embedding is present and 384 wide. Do NOT proceed to B3 unless this
assertion passes. The brain routes stay dark until the bge backfill is verified complete. Deleted and
anonymized candidates legitimately keep a NULL resume_embedding (the right to erasure), so the candidate
assertion scopes to live rows, exactly the set the backfill targets; skills must be embedded in full.
```
ssh joseph@100.94.8.94 'psql -U postgres -d missionmatch -v ON_ERROR_STOP=1 <<SQL
DO \$\$
DECLARE s_total int; s_emb int; c_total int; c_emb int; w int;
BEGIN
SELECT count(*), count(embedding) INTO s_total, s_emb FROM skills;
SELECT count(*), count(resume_embedding) INTO c_total, c_emb
FROM candidates WHERE deleted_at IS NULL AND anonymized_at IS NULL;
SELECT vector_dims(embedding) INTO w FROM skills WHERE embedding IS NOT NULL LIMIT 1;
IF s_emb <> s_total THEN RAISE EXCEPTION ''skills not fully embedded: %% of %%'', s_emb, s_total; END IF;
IF c_emb <> c_total THEN RAISE EXCEPTION ''live candidates not fully embedded: %% of %%'', c_emb, c_total; END IF;
IF w IS DISTINCT FROM 384 THEN RAISE EXCEPTION ''sampled vector width %% not 384'', w; END IF;
RAISE NOTICE ''BACKFILL VERIFIED: skills %%/%%, live candidates %%/%%, width %%'', s_emb, s_total, c_emb, c_total, w;
END \$\$;
SQL'
```
Rollback: the prior all-minilm vectors are not retained; if rolling back, leave bge vectors in place (the
prior binary re embeds skill names at request time and ignores the stored column, so it is unaffected).
### Step B3 - deploy the binary that mounts the brain (operator, brief downtime)
The new mm-api mounts POST /ai/query (capability brain.query) and uses the stored embedding skill matcher.
Back up the current binary first; same swap as the prior cutovers.
```
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFIPS140=v1.0.0 go build -trimpath -o /tmp/mm-api ./cmd/api
go version -m /tmp/mm-api | grep GOFIPS140 # must show GOFIPS140=v1.0.0
scp /tmp/mm-api user@bubbles:/tmp/mm-api.new
ssh user@bubbles '
sudo cp /home/user/missionmatch/mm-api /home/user/mm-api.bak.$(date +%s)
sudo systemctl stop mm-api
sudo cp /tmp/mm-api.new /home/user/missionmatch/mm-api && sudo chmod +x /home/user/missionmatch/mm-api
sudo systemctl start mm-api && sleep 2 && systemctl is-active mm-api
'
```
The skill matcher caches the global skill vectors on first use; the restart in this step picks up the bge
backfill. Rollback: stop mm-api, restore /home/user/mm-api.bak.<ts>, start mm-api (the /ai/query route
disappears; the older binary is unaffected by the bge vectors).
STARTUP VECTOR MODEL CHECK (the binary enforces the gate, belt and suspenders to the B2 assertion). On
boot, with a configured plane, the API runs a self check: it asserts the skills.embedding and
candidates.resume_embedding columns are vector(384), samples stored skill vectors, and re embeds them with
the configured MM_MEGAMIND_EMBED_MODEL to confirm the stored vectors were produced by that model (cosine
within tolerance). If the stored vectors do NOT match the configured model (the all-minilm versus bge
corruption mode, for example if an operator skipped or half ran B2), POST /ai/query returns a typed 503
vector_model_mismatch and the mismatch is logged, while the rest of the API serves normally; the brain
stays dark until the store is re embedded and the binary restarted. A boot time plane outage leaves the
check to self heal on the first query after the plane recovers. So /ai/query stays dark until BOTH the B2
re embed is verified complete AND the startup vector model check passes; the brain never serves a query
over a mismatched vector store.
### Step B4 - verify the brain answers end to end (operator)
```
# with a valid session + CSRF (capability brain.query):
curl -s -X POST https://missionmatch.thatwebhostingguy.com/api/missionmatch/ai/query \
-H 'Content-Type: application/json' -b "$COOKIES" -H "X-CSRF-Token: $CSRF" \
-d '{"question":"who are the top cleared cloud security candidates"}' | jq '{path, rationale_source, citations}'
# a demographic question must be refused (422), an aggregation intent must compute from the database:
curl -s ... -d '{"intent":"count_candidates_by_clearance","args":[""]}' | jq .answer
curl -s -o /dev/null -w "%{http_code}" ... -d '{"question":"list candidates by race"}' # -> 422
curl -s https://missionmatch.thatwebhostingguy.com/healthz/fips | jq .fips_enabled # -> true
```
### Post cutover checks (brain)
- A brain query writes one aedt.ai.answered (or aedt.ai.refused) audit, no PII, no prose.
- A plane outage returns a typed 503 (never a hang); a faithfulness fallback returns a 200 with
rationale_source deterministic.
- `go test -tags=integration -run TestBrain ./internal/integration/` stays green against the database, and
the EEO tripwire `go test ./internal/modules/ai/...` stays green (it walks the brain files).
- Staging cleanup when done: `DROP DATABASE brain_staging` (and the W3 tenant_w3_staging) on M4.