Granite — Market Studio cutover (Fleet API only)

Deploy and migrate Forge Market Studio on Granite without SSH file operations.

Updated

Prerequisites

  • Fleet GW-1/GW-2 code deployed on Granite (POST /v1/admin/git-self-update or Fleet upgrade SSH exception for the daemon only).
  • Local backup: forge-market/scripts/backup-market-studio.sh (optionally include broker/raw-sec per manifest flags).
  • FORGE_FLEET_BASE_URL and FORGE_FLEET_BEARER_TOKEN on the operator workstation.
  • forge-migrator installed locally (./scripts/start-migrator.sh) for sandbox/debug cycles — optional when using Market Studio Settings.

Preferred operator path — Market Studio Settings

  1. Launch UI against Granite data plane (operator optional for migrate only):

bash cd forge-market ./scripts/start-market-studio.sh --ui-remote --with-operator

Thin client: all /api and /health go to the Fleet app gateway. With --with-operator, /api/operator/* stays on local :9792 for Settings migrate/wipe. Default thin mode (no flag) runs Vite only.

  1. After cutover, daily use:

bash ./scripts/start-market-studio.sh --ui-remote

  1. On Granite host, install edge plane + scheduler — granite-market-edge-plane.md.

  2. Open Settings → Move data to Granite — selects corpus/DB/raw flags, runs local backup, builds a chunked Fleet bundle, and runs Fleet steps in order.

  3. After completion, Test remote data compares gateway /health and /api/storage/inventory to the pre-migrate local snapshot.

  4. Optional Remove local data — separate dialog; typed confirm DELETE LOCAL MARKET DATA; never deletes config/ or .env.local.

Operator HTTP surface (local studio-server only):

Method Path Role
GET /api/operator/hosting Fleet/gateway readiness + local inventory
POST /api/operator/migrations Create session
POST /api/operator/migrations/{id}/start Background upload + Fleet steps (market-prod)
GET /api/operator/migrations/{id} Poll upload bytes + Fleet step status
POST /api/operator/migrations/{id}/verify Remote smoke vs local snapshot
GET /api/operator/wipe/preview List delete candidates + keep-list
POST /api/operator/wipe Dry-run or delete local data/ (guarded)

API-only cutover sequence (curl / migrator)

  1. Rollout compose stack registration (once per Fleet data-dir):

bash curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-studio-rollout" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"sync": true}'

Rollout syncs forge-market, rebuilds market-app, starts Postgres, runs python -m forge_market.db.migrate upgrade (schema deltas), then starts the API. Poll GET /v1/admin/forge-market-studio-rollout-log for progress. Optional body: "run_schema_migrate": false to skip schema step (not recommended).

Env-gated migration m059 (dictionary TEXT drops): set "forge_market_confirm_dictionary_drops": "1" on the rollout POST body. The rollout script checks dictionary_parity_ok on /health before migrate when that flag is set. Completed 2026-10-08 on dev and prod (main, backend 1.0.70); use "forge_market_git_ref": "main" for routine rollouts. Historical dev rehearsal used branch m059-cutover (deleted).

Large harvest backlog: set catch-up env in the stack .env before rollout (FORGE_MARKET_ASYNC_ROLLUP=1, FORGE_MARKET_HARVEST_DERIVE_ON_UPLOAD=0, FORGE_MARKET_STUDIO_PRIORITY_WORKERS=8), or pass "forge_market_harvest_catch_up": "1" on this POST (Fleet persists compose .env during rollout). See forge-market/docs/operate/ibgw-harvest-speed.md (Catch-up drain profile). Restore derive-on-upload after the queue drains.

SEC filing intelligence flags (FM-SEC-FIC-001): FORGE_MARKET_SEC_FIC_UI / FORGE_MARKET_SEC_FIC_ANALYZE default 0 in the stack compose. Flip them either by adding the lines to ~/.config/forge-fleet/forge-market-granite.env (sourced on non-dev rollouts and persisted to compose .env) or by passing "forge_market_sec_fic_ui": "1" / "forge_market_sec_fic_analyze": "1" on this POST. A forge_market_skip_build: true rollout is enough to recreate the container with the new env (Fleet ≥ 0.3.145).

Verify: curl -fsS http://127.0.0.1:19792/health should show matching schema_version and schema_head.

Two-tier deploy (source vs image rebuild): the rollout script compares the running market-app image label forge.market.reqs_hash to the md5 of studio-server/requirements.txt. When unchanged, it uses a source-only path (docker cp + docker restart, ~20–30s). When requirements.txt changed, it runs a full docker compose build.

Job drain modes (POST body → env):

forge_market_job_drain_mode Behavior
off Skip harvest/enrichment guards
pause (default for auto) Pause harvest, wait for SQL checkpoint, restart, resume same subprocess
cancel Pause, cancel harvest subprocesses, restart — operator starts new harvest when runner code changed

Schema migrate default: run_schema_migrate: "auto" reads schema_online_pending from /health before stopping market-app. When nothing is pending, migrate is skipped.

Monitor rollout progress and failures:

```bash curl -sS "${FORGE_FLEET_BASE_URL}/v1/managed-services/market-studio/maintenance-status" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}"

curl -sS "${FORGE_FLEET_BASE_URL}/v1/managed-services/market-studio/rollout-log" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" ```

Workstation Forge Market Studio polls the same status via GET /api/maintenance/status and shows a maintenance overlay during Granite deploys.

Typical PATCH deploy (Python source only, harvest unchanged):

bash curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-studio-rollout" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "forge_market_env": "prod", "run_schema_migrate": "auto", "forge_market_job_drain_mode": "pause", "forge_market_pause_scheduler": true }'

Pattern cell rollups backfill (after migration m043_pattern_cell_rollups): rollout creates empty rollup tables; reconcile them via Fleet (no SSH):

```bash # Full corpus — background (checkpoint persisted in appdata volume) curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"forge_market_env":"prod"}'

# Scoped smoke (sync, dry-run) curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"tickers":"NVDA,AAPL","limit":10,"dry_run":true,"sync":true}'

curl -sS "${FORGE_FLEET_BASE_URL}/v1/admin/forge-market-pattern-rollups-backfill-log" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" ```

Ongoing scope refresh remains Market Studio POST /api/candle-memory/scope/rollups/jobs (Patterns Update rollups button). Fleet backfill is for bootstrap or operator-triggered full reconcile.

  1. Run migrator recipe forge-market in the Electron wizard — or equivalent curl sequence:

```bash # Create migration session curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"source_label":"forge-market-local","target_label":"market-prod","meta":{"recipe":"forge-market","app_slug":"forge-market"}}'

# Upload backup tarball (replace MIGRATION_ID) — prefer chunked upload-session for multi-GB payloads curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/data-bundle/upload-session" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"sha256":"<64 hex>","total_bytes":N,"chunk_size":67108864}'

# PUT chunks, POST finalize, then run steps using UUIDs from GET /v1/migrations/MIGRATION_ID FLEET_STEP_ID="$(curl -sS "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ | jq -r '.steps[] | select(.kind=="seed_corpus_volume") | .id')" curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/steps/${FLEET_STEP_ID}/run" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d '{}' ```

  1. Verify

bash curl -sS "${FORGE_FLEET_BASE_URL}/v1/container-services/market-prod" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" curl -sS "${FORGE_FLEET_BASE_URL}/v1/app-gateways/market-studio/health" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}"

  1. Edge cache (optional) — after gateway register, refresh Market hot-path TTLs (or run ./scripts/register-market-studio-edge-cache.sh market-studio):

bash curl -sS -X PUT "${FORGE_FLEET_BASE_URL}/v1/app-gateways/market-studio/edge-cache" \ -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \ -H "Content-Type: application/json" \ -d @<(PYTHONPATH="${FORGE_MARKET_STUDIO_ROOT:-../forge-market}/src" python3 -c 'from forge_market.operator.fleet_edge_cache import registration_body; import json; print(json.dumps(registration_body()))')

Requires Fleet edge.cache.enabled on the Granite host. Repeat an identical authenticated GET and check CF-Cache-Status: HIT.

Rollback (API only)

curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/container-services/market-prod/stop" \
  -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{}'

FLEET_RESTORE_ID="$(curl -sS "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID" \
  -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
  | jq -r '.steps[] | select(.kind=="restore_from_bundle") | .id')"
curl -sS -X POST "${FORGE_FLEET_BASE_URL}/v1/migrations/MIGRATION_ID/steps/${FLEET_RESTORE_ID}/run" \
  -H "Authorization: Bearer ${FORGE_FLEET_BEARER_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{}'

Schema migrate failure behaviour

rollout-forge-market-studio.sh stops market-app before python -m forge_market.db.migrate upgrade. Since 2026-10-08 a failed or stale migrator:

  1. logs the full migrator output into the rollout log (GET /v1/admin/forge-market-studio-rollout-log) — previously the error was swallowed and only schema migrate failed was visible;
  2. restarts the previous market-app container before the rollout exits non-zero, so a bad migration leaves the lane on the old code/schema instead of down;
  3. reports the last three migrator lines in the maintenance error field.

Reproduce a migrator failure read-only with the plan subcommand through a Fleet job (docker_argv on forge-market-app:studio, mounts src/ + studio-server/, python -m forge_market.db.migrate plan).

NLP tone and pgvector (FM-NLP-001)

Compose image is pgvector/pgvector:pg16 (deploy/forge-market-studio/compose.yaml). Granite overlay enables FORGE_MARKET_NARRATIVE_TONE=1 and FORGE_MARKET_NARRATIVE_EMBED=1 by default; FORGE_MARKET_NARRATIVE_ENCODER stays 0 until the ONNX artifact and fm_nlp_gpu_worker template are on the host. Rollout is still POST /v1/admin/forge-market-studio-rollout (no SSH). The first start runs CREATE EXTENSION vector from migrate_narrative_nlp.

Forbidden on Granite SSH

  • scp, rsync, manual tar extract for Market data
  • Manual docker compose for Market Studio
  • Manual Caddy file edits (use register_edge_route — Fleet app gateway, no new tunnel)