Skip to content

feat(api): AIN-141 strip retired AAMC models from prod catalog (DB migration)#20

Merged
varda-elentari merged 2 commits into
mainfrom
hizrian/ain-141-db-migration
May 18, 2026
Merged

feat(api): AIN-141 strip retired AAMC models from prod catalog (DB migration)#20
varda-elentari merged 2 commits into
mainfrom
hizrian/ain-141-db-migration

Conversation

@varda-elentari
Copy link
Copy Markdown
Contributor

Summary

Forward-fix migration: DELETE 3 retired AAMC v1.0 slugs from models table per Charter v3 §Don't (Llama-3.3-70B + DeepSeek R1 retired 2026-05-16).

Branch history

  • e8ab736 — initial migration (uses bind.exec_driver_sql(..., %s) — psycopg2 syntax). Founder ran alembic upgrade head against prod which advanced 20260517_0009 + 20260517_0010 successfully, then this migration failed with syntax error at or near % (asyncpg uses $1, not %s).
  • 4af45ed — bug fix: replace with SQLAlchemy text() + named :slug binds, dialect-agnostic.

Prod alembic state after the failed run

Per transactional DDL: 0011 rolled back. Prod alembic_version is at 20260517_0010. After this PR merges, re-running alembic upgrade head applies the fixed 0011.

⚠️ Bundling note (small)

Commit 4af45ed also includes 1 file from founder's WIP (tests/integration/test_aamc_invariants.py) that was already staged in the working tree when Aule wrote the migration fix. The bundle happened because git stash --keep-index preserved staged-but-not-by-me changes. Diff is 11+ / 7- lines — review the test_aamc_invariants.py portion of 4af45ed and either accept (if WIP was ready) or surgically revert before merge.

Founder next action

cd ~/code/ainfera-ai/api
# 1. Review + decide on the bundled test_aamc_invariants.py portion (see above)
# 2. If approved: merge this PR via 'gh pr merge --squash --delete-branch'
# 3. After merge:
doppler run --project ainfera-os --config prd -- alembic upgrade head
# 4. Verify console rowcount output (~1-3 deletes across 3 retired slugs)
# 5. Close AIN-141

Refs

Closes AIN-141 DB-side.

Aule and others added 2 commits May 18, 2026 14:21
…gration)

Forward-fix migration: DELETE 3 retired AAMC v1.0 slugs from models table.
Per Charter v3 §Don't: do not propagate Llama-3.3-70B or DeepSeek R1.

Retired slugs targeted:
- deepseek-r1-novita (from 20260516_0008 t9_catalog INSERT)
- meta-llama/Llama-3.3-70B-Instruct-Turbo (pre-T9 grandfathered)
- llama-3-3-70b-instruct-turbo-together (T9-canonical form)

Rowcount logged per memory feedback_migration_rowcount_assertion; asserted
non-negative (not ==N) because CI bootstrap state varies vs prod.

Bug fix vs initial draft: initial used bind.exec_driver_sql with %s
psycopg2-style placeholder, which failed on prod (asyncpg driver) with
'syntax error at or near %'. This version uses SQLAlchemy text() with
named :slug binds — works across asyncpg + psycopg2 + sqlite.

Downgrade is intentional no-op (retired rows absent by design).

Closes AIN-141 DB-side once founder runs:
    doppler run --project ainfera-os --config prd -- alembic upgrade head

Co-Authored-By: Claude <noreply@anthropic.com>
…bility

The committed migration in e8ab736 (this branch's previous commit) used
bind.exec_driver_sql with psycopg2 %s placeholder. Prod runs asyncpg which
uses $1 style placeholders and rejected the migration with syntax error
at or near '%' when founder ran alembic upgrade head.

Replace with SQLAlchemy text() + named binds (:slug), which dispatches
correctly across asyncpg, psycopg2, and sqlite test backends.

State after the failed run:
- 20260517_0009 + 20260517_0010 successfully applied (committed earlier
  but not previously deployed to prod)
- 20260518_0011 rolled back per transactional DDL
- alembic_version should be at 20260517_0010

Founder retries with:
  doppler run --project ainfera-os --config prd -- alembic upgrade head

Co-Authored-By: Claude <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 18, 2026

AIN-141 [Audit P1 · api] Strip retired AAMC models (`Llama-3.3-70B`, `DeepSeek R1`) from tests + scripts + migration

Phase 1 audit finding (2026-05-18). Retired AAMC v1.0 models still in active api source + DB catalog.

Status: code-side scope ✅ shipped (PR #19 merged, CI green); DB-side surfaced as founder DDL tap (migration drafted, awaiting founder review + apply).

Code-side cleanup (✅ DONE, PR #19, merged to main)

3 files, 12 insertions, 10 deletions:

  • tests/unit/test_adapters.py — swap retired meta-llama/Llama-3.3-70B-Instruct-TurboQwen/Qwen2.5-72B-Instruct-Turbo in test_together_adapter_inherits_openai_compat_wire. Test mocks upstream; model string is a passthrough for adapter wire-format coverage. Semantic intent unchanged.
  • scripts/fix_aamc_canon.py — update NOT-VALID-grandfather comment to note Llama-3.3 entry was retired + planned for DB deletion.
  • scripts/seed_dev.py — swap Together provider seed entry from retired Llama 3.3 → Qwen 2.5 72B Instruct Turbo.

CI conclusion: success. Pre-commit hooks all green (ruff + mypy --strict + pytest -x unit + smoke).

DB-side cleanup (🛑 FOUNDER TAP REQUIRED, migration drafted locally)

Local file: alembic/versions/20260518_0011_strip_retired_aamc_models.py (3308 bytes, untracked, ready for founder review).

Migration targets 3 retired slugs for DELETE from models table:

  • deepseek-r1-novita — from 20260516_0008_t9_catalog_models.py INSERT
  • meta-llama/Llama-3.3-70B-Instruct-Turbo — pre-T9 grandfathered legacy slug
  • llama-3-3-70b-instruct-turbo-together — T9-canonical Llama 3.3 form

Per memory feedback_migration_rowcount_assertion: rowcount logged per DELETE + asserted non-negative (not ==N because CI bootstrap state varies vs prod). Downgrade is intentional no-op (retired rows absent by design).

Why founder tap: Charter v3 §"Founder taps" lists "Supabase or Railway DDL" as founder-only. Auto-classifier correctly halted committing the migration to main without explicit DB-change authorization. Founder reviews the migration text + commits in a separate PR + runs alembic upgrade head against prod.

Founder next action:

cd ~/code/ainfera-ai/api
# 1. Review migration text
cat alembic/versions/20260518_0011_strip_retired_aamc_models.py
# 2. If approved: branch, add, commit, push, PR, merge, then:
doppler run --project ainfera-os --config prd -- alembic upgrade head
# 3. Verify deletion count matches expectation (likely 1-3 rows total)

Refs

PR #19 (code-side, merged): #19
Migration draft: alembic/versions/20260518_0011_strip_retired_aamc_models.py (untracked, on Aule's working tree)

Review in Linear

@varda-elentari varda-elentari merged commit 9e884b2 into main May 18, 2026
3 checks passed
@varda-elentari varda-elentari deleted the hizrian/ain-141-db-migration branch May 18, 2026 07:55
hizrianraz added a commit that referenced this pull request May 19, 2026
* feat(api): AIN-183 P0-2 · flip aa_index_source for AAMC voters gemini-3-1-pro + mistral-large-3

Two of the five AAMC v1.0 voter slugs landed in prod with
aa_index_source='estimate_2026_q2' because at the time the Artificial
Analysis Intelligence Index didn't have a stable reading for either model.
The other three (claude-opus-4-7, gpt-5-5, grok-4) shipped with
'aamc_v1_lock'. AA Index has since locked readings for both, and the
marketing /models page calls out 'AAMC v1.0' across the whole 5-voter
pool — leaving two on a provisional source label reads as a discipline #11
mismatch (spec / runtime / copy disagree).

## Migration

`20260519_0017_aamc_voter_source_lock.py` runs a single UPDATE filtered on
the current value, so re-applying is a no-op. The rowcount assertion
(Memory #20: silent no-ops ship as green CI) accepts 0 (fresh DB / re-apply)
or 2 (first prod apply) but raises on any other value.

Downgrade reverses the flip — safe because no row-level dependency hangs on
the source label.

## Invariant test

`test_aamc_5_voters_use_v1_lock_source` lives next to the existing
`test_aamc_5_canonical_voters_always_active` and asserts the same lock
shape but for `aa_index_source`. Any future migration that regresses one
of the five voters back onto a provisional source label fails CI here,
with the discipline #11 reasoning surfaced in the assertion message.

## Friction

The audit prompt called the column `aamc_index_source`. Actual ORM
declaration is `aa_index_source` (api/ainfera_api/orm.py:332) and the
public API response field is the same. Migration uses the real name.

Closes: AIN-183 P0-2 (AAMC source label drift)
Discipline: #1 (no-op no longer ships green), #11 (catalog labels align
across DB / API / marketing), Memory #20 (rowcount assertion).

* fix(api): AIN-183 P0-2 migration · drop updated_at clause (column does not exist)

CI integration failed with:
  asyncpg.exceptions.UndefinedColumnError: column "updated_at" of
  relation "models" does not exist

The models table has only created_at, not updated_at. My original
sketch carried updated_at over from a generic timestamp-touch pattern
that doesn't apply here. Drop the clause from both upgrade() and
downgrade() — the aa_index_source flip is the only data change.

Locally: alembic upgrade head succeeds; the data-integrity assertion
(0 or 2 rows affected) still guards against silent no-ops.

---------

Co-authored-by: Aule <aule@ainfera-internal.local>
hizrianraz pushed a commit that referenced this pull request May 19, 2026
…tion C)

/v1/audit/public was building canonical URIs as
`ainfera.ai/{owner_handle}/{agent_name}` where owner_handle was read off
the agent row. For founder-owned agents (Varda, Yavanna) this surfaced
`ainfera.ai/hizrianraz/varda` on the most-trafficked public endpoint we
run — a discipline #3 leak of founder GitHub-handle / PII.

The Discipline #12 fix landed in the AIN-183 audit prompt is Option C: add
a public-facing handle on the `tenants` row that's decoupled from the
GitHub handles on agent rows, and project that on the public surface
instead.

Three-phase, all in one upgrade():

1. Add `tenant_handle TEXT NULL` to `tenants`.
2. Backfill in priority order:
   a. Tenants that own at least one agent with `owner_handle='hizrianraz'`
      → `tenant_handle='ainfera-ai'` (founder tenant id is not hardcoded;
      lifted from data).
   b. Remaining tenants → MIN(owner_handle) across their agents (stable +
      deterministic + matches the GitHub handle most users registered as).
   c. Agent-less tenants → contact_email local part.
   d. Conflict resolution: collisions append a 6-char id-slice suffix in
      stable id order, so the first-by-id keeps the bare handle.
3. NOT NULL constraint + unique index. Pre-NOT-NULL the migration asserts
   zero rows remain NULL (Memory #20 silent-no-op guard).

- `TenantORM.tenant_handle` declared NOT NULL UNIQUE String(64).
- `routers/audit.py` public_feed projection joins through TenantORM and
  reads tenant_handle. The response key stays `owner_handle` — the public
  API contract is unchanged, only the value source moves.
- All four TenantORM instantiation sites populate the new column:
    - routers/signup.py (SDK-CLI signup → tenant_handle=owner_handle)
    - routers/github_oauth.py (OAuth login → tenant_handle=github_login)
    - routers/install.py (resolve-or-create on install → same as oauth)
    - routers/tenants.py (/v1/tenants/register → contact_email local part)

```
curl -s https://api.ainfera.ai/v1/audit/public | \
  jq -r '.events[].canonical_uri' | grep -c hizrianraz

curl -s https://api.ainfera.ai/v1/audit/public | \
  jq -r '.events[].canonical_uri' | grep -c ainfera-ai/varda
```

Once this lands, the marketing AuditTicker widget (already filters
`ainfera-ai/varda` and `ainfera-ai/yavanna` on the web side) starts
matching real events — closes PR E without a web-side code change.

- Prompt said `tenants.tenant_handle` is a new column. Confirmed via ORM
  read — column did not exist (only id/name/contact_email/api_key_hash/
  created_at). Migration adds it.
- Public response field stays named `owner_handle` to avoid breaking the
  API contract; only the underlying value changes. If a future PR wants
  to rename the response field to `tenant_handle`, that's a separate
  ContractDelta against the PublicAuditEvent Pydantic model.

Closes: AIN-183 P0-3 (founder PII on /v1/audit/public)
Discipline: #1 (claim "no founder PII on public" matches reality),
assertions on data migration).
hizrianraz added a commit that referenced this pull request May 19, 2026
…tion C) (#47)

/v1/audit/public was building canonical URIs as
`ainfera.ai/{owner_handle}/{agent_name}` where owner_handle was read off
the agent row. For founder-owned agents (Varda, Yavanna) this surfaced
`ainfera.ai/hizrianraz/varda` on the most-trafficked public endpoint we
run — a discipline #3 leak of founder GitHub-handle / PII.

The Discipline #12 fix landed in the AIN-183 audit prompt is Option C: add
a public-facing handle on the `tenants` row that's decoupled from the
GitHub handles on agent rows, and project that on the public surface
instead.

Three-phase, all in one upgrade():

1. Add `tenant_handle TEXT NULL` to `tenants`.
2. Backfill in priority order:
   a. Tenants that own at least one agent with `owner_handle='hizrianraz'`
      → `tenant_handle='ainfera-ai'` (founder tenant id is not hardcoded;
      lifted from data).
   b. Remaining tenants → MIN(owner_handle) across their agents (stable +
      deterministic + matches the GitHub handle most users registered as).
   c. Agent-less tenants → contact_email local part.
   d. Conflict resolution: collisions append a 6-char id-slice suffix in
      stable id order, so the first-by-id keeps the bare handle.
3. NOT NULL constraint + unique index. Pre-NOT-NULL the migration asserts
   zero rows remain NULL (Memory #20 silent-no-op guard).

- `TenantORM.tenant_handle` declared NOT NULL UNIQUE String(64).
- `routers/audit.py` public_feed projection joins through TenantORM and
  reads tenant_handle. The response key stays `owner_handle` — the public
  API contract is unchanged, only the value source moves.
- All four TenantORM instantiation sites populate the new column:
    - routers/signup.py (SDK-CLI signup → tenant_handle=owner_handle)
    - routers/github_oauth.py (OAuth login → tenant_handle=github_login)
    - routers/install.py (resolve-or-create on install → same as oauth)
    - routers/tenants.py (/v1/tenants/register → contact_email local part)

```
curl -s https://api.ainfera.ai/v1/audit/public | \
  jq -r '.events[].canonical_uri' | grep -c hizrianraz

curl -s https://api.ainfera.ai/v1/audit/public | \
  jq -r '.events[].canonical_uri' | grep -c ainfera-ai/varda
```

Once this lands, the marketing AuditTicker widget (already filters
`ainfera-ai/varda` and `ainfera-ai/yavanna` on the web side) starts
matching real events — closes PR E without a web-side code change.

- Prompt said `tenants.tenant_handle` is a new column. Confirmed via ORM
  read — column did not exist (only id/name/contact_email/api_key_hash/
  created_at). Migration adds it.
- Public response field stays named `owner_handle` to avoid breaking the
  API contract; only the underlying value changes. If a future PR wants
  to rename the response field to `tenant_handle`, that's a separate
  ContractDelta against the PublicAuditEvent Pydantic model.

Closes: AIN-183 P0-3 (founder PII on /v1/audit/public)
Discipline: #1 (claim "no founder PII on public" matches reality),
assertions on data migration).

Co-authored-by: Aule <aule@ainfera-internal.local>
hizrianraz added a commit that referenced this pull request May 19, 2026
… endpoints (#52)

Closes the backend half of AIN-182 §Phase 2 §5 + §6. Workflow templates
are reusable blueprints a tenant can apply to an agent — ordered tasks
with D11 model-class hints, optional HITL rules, D10 visibility model.

## Migration 20260519_0020

- New `templates` table (id, tenant_id NULL=system, name, description,
  visibility enum, tasks JSONB, hitl_rules JSONB, timestamps)
- Unique (tenant_id, name) per tenant + partial unique on name for
  system templates (tenant_id IS NULL)
- New `agents.template_id` FK (nullable, SET NULL on template delete)
- Seeds 6 Ainfera system templates: inference-frontier,
  inference-fast, inference-cost-light, inference-embedding,
  routing-balanced, routing-quality-first. Memory #20 rowcount-asserted.

## CRUD endpoints

- GET    /v1/templates           list (system + tenant own)
- GET    /v1/templates/{id}      detail (404 across-tenant)
- POST   /v1/templates           tenant-scoped create
- PUT    /v1/templates/{id}      owner-only update
- DELETE /v1/templates/{id}      owner-only delete

System templates (tenant_id NULL) are read-only via the API; the
migration is the only path that mutates them.

## OpenAPI contract

EXPECTED_OPERATIONS extended with 5 new paths.

Closes part of AIN-182 (Phase 2 §5 + §6 backend half).

Co-authored-by: Aule <aule@ainfera-internal.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant