feat(contracts): ingest published API contracts and gate changesets on them #567

Merged
gmackie merged 1 commit from feat/contract-ingest into main 2026-09-12 02:09:17 +00:00
Owner

What

Phase 2a of the semantic control plane: ForgeGraph can now receive the contract an app declares, store it against the commit it was compiled from, and use it as merge evidence.

Stacked on #564 — base is feat/contract-package, so this diff is only the Phase 2 work. Merge #564 first.

Plan: https://7n04n7hhuesf.postplan.dev — Phase 2, tasks 2.1–2.3 and 2.7.

Tables

table grain why
contract_snapshots one row per (app, commit) holds the whole validated IR document plus denormalized counts
contract_operations one row per operation flattened ir.operations, so operations can be listed, filtered and joined without opening the jsonb

operation_id is the semantic identity <serviceId>.<group>.<endpoint>. It survives route changes and is also the span name the app already reports, which is what a later observed-traffic join keys on.

Route

POST /api/fg/contracts validates with the same validateContract the compiler runs, upserts on (app, commit), and replaces the operation rows rather than upserting them — an endpoint can be removed from a contract, and a plain upsert would leave the deleted one behind looking like it still exists.

It satisfies the new api_contract gate only when the changeset's head is still that commit, so evidence from a superseded push cannot keep a moved changeset green. GET reads one back.

Warnings are advisory on purpose

contractWarnings reports a serviceId that is not the app slug (telemetry would never join), a declared authentication its middleware does not back, and a public anonymous mutation. None of them reject the publish: a contract that validated is a fact about the code, and refusing to record it would only hide the problem.

Verification

37 new tests: 8 on the pure projection, 10 on the route (including a genuinely fingerprinted document, a tampered one, and the superseded-commit case), 19 schema-shape. Typecheck clean across db, api and web; oxlint clean of errors.

⚠️ Before merging

  1. Apply packages/db/drizzle/0102_contracts.sql, then 0102a_contracts_grants.sql, to the live database. Merging first turns prod deploys red on schema drift while the app stays healthy on the old build.
  2. pnpm install — packages/api and apps/web gain @forgegraph/contract as a workspace dependency.

🤖 Generated with Claude Code

## What Phase 2a of the semantic control plane: ForgeGraph can now receive the contract an app declares, store it against the commit it was compiled from, and use it as merge evidence. **Stacked on #564** — base is `feat/contract-package`, so this diff is only the Phase 2 work. Merge #564 first. Plan: https://7n04n7hhuesf.postplan.dev — Phase 2, tasks 2.1–2.3 and 2.7. ## Tables | table | grain | why | |---|---|---| | `contract_snapshots` | one row per (app, commit) | holds the whole validated IR document plus denormalized counts | | `contract_operations` | one row per operation | flattened `ir.operations`, so operations can be listed, filtered and joined without opening the jsonb | `operation_id` is the semantic identity `<serviceId>.<group>.<endpoint>`. It survives route changes and is also the span name the app already reports, which is what a later observed-traffic join keys on. ## Route `POST /api/fg/contracts` validates with the same `validateContract` the compiler runs, upserts on (app, commit), and **replaces** the operation rows rather than upserting them — an endpoint can be *removed* from a contract, and a plain upsert would leave the deleted one behind looking like it still exists. It satisfies the new `api_contract` gate only when the changeset's head is still that commit, so evidence from a superseded push cannot keep a moved changeset green. `GET` reads one back. ## Warnings are advisory on purpose `contractWarnings` reports a `serviceId` that is not the app slug (telemetry would never join), a declared authentication its middleware does not back, and a public anonymous mutation. None of them reject the publish: a contract that validated is a fact about the code, and refusing to record it would only hide the problem. ## Verification 37 new tests: 8 on the pure projection, 10 on the route (including a genuinely fingerprinted document, a tampered one, and the superseded-commit case), 19 schema-shape. Typecheck clean across db, api and web; oxlint clean of errors. ## ⚠️ Before merging 1. Apply `packages/db/drizzle/0102_contracts.sql`, then `0102a_contracts_grants.sql`, to the live database. Merging first turns prod deploys red on schema drift while the app stays healthy on the old build. 2. `pnpm install` — `packages/api` and `apps/web` gain `@forgegraph/contract` as a workspace dependency. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(contracts): ingest published API contracts and gate changesets on them
Some checks failed
CI / gitleaks (pull_request) Successful in 8s
CI / storybook (pull_request) Successful in 1m26s
forgegraph/ci CI failed
CI / ci (pull_request) Failing after 7m0s
9997bcdb63
Phase 2a of the semantic control plane: ForgeGraph can now receive the
contract an app declares, store it against the commit it was compiled from,
and use it as merge evidence.

- packages/db: contract_snapshots (one row per app+commit; holds the whole
  validated IR document plus denormalized counts) and contract_operations
  (the flattened projection of ir.operations, so operations can be listed,
  filtered and joined without opening the jsonb). operation_id is the
  semantic identity <serviceId>.<group>.<endpoint>, which is stable across
  route changes and is also the span name the app reports — that is what a
  later observed-traffic join keys on.
- packages/api: projectContract / contractWarnings, kept pure and free of the
  database so the interesting decisions are unit-testable without mocks.
  Warnings are advisory by design: a contract that validated is a fact about
  the code, and refusing to record it would only hide the problem. They cover
  a serviceId that is not the app slug (telemetry would never join), a
  declared authentication its middleware does not back, and a public
  anonymous mutation.
- apps/web: POST /api/fg/contracts validates with the same validateContract
  the compiler runs, upserts on (app, commit), REPLACES the operation rows so
  a removed endpoint disappears, and satisfies the new `api_contract` gate —
  but only when the changeset's head is still that commit, so evidence for a
  superseded push cannot keep a moved changeset green. GET reads one back.
- packages/api: `api_contract` added to the built-in attestation types,
  ruleless (external), so an execution policy can require it.

37 new tests. `pnpm install` is required after this lands: packages/api and
apps/web gain @forgegraph/contract as a workspace dependency.

⚠️ The migration must be applied to the live database BEFORE this merges, or
prod deploys go red on schema drift while the app stays healthy on the old
build. Files: 0102_contracts.sql then 0102a_contracts_grants.sql.

Plan: https://7n04n7hhuesf.postplan.dev (Phase 2, tasks 2.1-2.3, 2.7)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gmackie force-pushed feat/contract-ingest from 9997bcdb63
Some checks failed
CI / gitleaks (pull_request) Successful in 8s
CI / storybook (pull_request) Successful in 1m26s
forgegraph/ci CI failed
CI / ci (pull_request) Failing after 7m0s
to 551d078178
All checks were successful
CI / gitleaks (pull_request) Successful in 6s
CI / storybook (pull_request) Successful in 2m7s
forgegraph/ci CI passed
CI / ci (pull_request) Successful in 10m3s
2026-09-10 01:02:39 +00:00
Compare
gmackie changed target branch from feat/contract-package to main 2026-09-12 02:09:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gmackie/ForgeGraph!567
No description provided.