# Workspace governance

FetchCatch lets a tenant edit flows from **two places**: the web console and the `fcc` CLI. To
keep the question "who is the source of truth?" answered before it's ever asked, every workspace
declares a **governance mode** that gates the `publish` event.

> Editing drafts is unrestricted in all modes. The governance mode only controls who is allowed
> to mint a new **published version** — i.e. who can change what `/v1/evaluate/{slug}` returns.

## The three modes

| Mode | Designer publish | CLI publish | Best for |
|---|---|---|---|
| `DesignerOnly` | ✅ allowed | ❌ refused (HTTP 400) | Solo founders, no-code teams, demos |
| `CodeManaged`  | ❌ refused (HTTP 400) | ✅ allowed | Teams with PR review + CI/CD |
| `Hybrid`       | ✅ allowed | ✅ allowed | Default. Power-user opt-in; three-way merge + conflict detection apply |

The mode is stored as a column on `Workspaces` and enforced on every publish attempt by the
`GovernanceEnforcer` service. An Owner/Admin can change it at any time from
**Settings → Workspaces → Settings** without restarting anything.

### What "publish" means

Publishing snapshots the current draft graph as an immutable, numbered version and sets
`Flows.PublishedVersionId` to point at it. From that moment on, every call to
`POST /v1/evaluate/{slug}` returns the new version's decisions.

The publish event has **provenance**: every `FlowVersions` row carries

- `Source` — one of `designer-publish`, `cli-publish`, `cli-apply`, `designer-draft`, `rollback`
- `CreatedByEmail` — actor email at the time of the write
- `GitCommitSha` — commit SHA the CLI advertised via `X-FCC-Git-Commit`

This shows up in three places:

- Console **flow history panel** (sidebar opened from the designer) — two tabs,
  *Published* (only events that changed `/v1/evaluate` output) and *All snapshots*
  (every save/apply). Each row renders the actor as a colored initials chip, the
  source as a semantic badge, and the git commit short-SHA as a link to the
  workspace's configured repo URL (`github.com`, `gitlab.com`, `bitbucket.org` supported).
- Console **Runs** dashboard — every row now shows the targeted version's number,
  source badge (`cli` / `console` / `cli·draft` / `console·draft`), and short commit
  SHA. A *Source: cli / console / all* filter chip sits next to the status chips.
- CLI:
  - `fcc history flows/<slug>.json` — full save/apply log with `commit` column.
  - `fcc history flows/<slug>.json --publishes` — only publishes; marks the live
    version with `*`.

### Querying provenance from your tools

The publish timeline is exposed as a stable JSON endpoint so you can plug it
into release notes, Slack notifications, or dashboards without screen-scraping:

```bash
curl -H "Authorization: Bearer $FETCHCATCH_TOKEN" \
     https://api.fetchcatch.com/v1/sync/history/flow/<flowId>/publishes
```

Response (each item):

```json
{
  "version": 17,
  "publishedAtUtc": "2026-05-26T14:31:02Z",
  "source": "cli-publish",
  "createdByEmail": "ci-bot@acme.com",
  "gitCommitSha": "a1b2c3d4e5f6789",
  "isCurrent": true
}
```

## Choosing a mode

### DesignerOnly

- Console publishes only.
- `fcc apply` and `fcc publish` are refused with: "This workspace is set to DesignerOnly.
  Publishing from the CLI is disabled. …"
- Use when you want a clean "edit in the console, no scripting required" experience.
- You can still run `fcc pull` to back up the workspace into git as read-only.

### CodeManaged

- CLI publishes only. The console **shows** the draft and lets you edit it (useful for inspection
  and last-mile tweaks before committing), but the **Publish** button is disabled with a banner
  pointing at your repo URL.
- Use when every change should go through a pull request — the same workflow you have for code.
- CI runs `fcc publish` after merge to ship the new version. The commit SHA is stamped onto the
  resulting `FlowVersions` row for audit.

### Hybrid

- Both surfaces are allowed.
- Use when you want the speed of in-console fixes during incidents but also git history for
  the long-run audit trail.
- Risk: changes from one surface can shadow the other. Mitigate with:
  - The **recently-pulled banner** in the designer (already wired in).
  - The **"diverged from main" badge** + `fcc drift` exit-code 2 (see below).

## Drift detection

`GET /v1/sync/drift` (also `fcc drift` from CI) returns a small JSON payload:

```json
{
  "workspace": { "slug": "billing-prod", "governanceMode": "Hybrid", "environmentKind": "Production" },
  "git":       { "branch": "main", "lastCommitSha": "a1b2c3d4…", "lastCommitSyncedAtUtc": "…" },
  "pending":   { "push": 0, "pull": 0, "conflicts": 0 },
  "designerPublishesSinceLastGitCommit": 2,
  "inSync":    false,
  "checkedAtUtc": "…"
}
```

`designerPublishesSinceLastGitCommit > 0` means the console minted a published version that has
no matching git commit. That's the canonical "someone hot-fixed in production" signal.

Wire `fcc drift` into a nightly job:

```yaml
- name: Check for designer drift
  run: fcc drift
  env:
    FETCHCATCH_TOKEN: ${{ secrets.FETCHCATCH_TOKEN }}
    CI: "true"
```

Exit codes:
- `0` — in sync, no drift
- `2` — drift detected or local/remote pending changes
- `3` — conflicts (local + remote both changed since last pull)
- `1` — error

## API reference

| Verb | Route | Notes |
|------|-------|-------|
| `PATCH` | `/v1/workspaces/{id}`                          | `{ governanceMode?, environmentKind?, gitRepoUrl?, gitBranch?, name? }`. Owner/Admin only. |
| `POST`  | `/v1/sync/publish`                             | Body `{ flowIds: [] }` publishes every flow with a draft; non-empty list publishes only those. |
| `GET`   | `/v1/sync/drift`                               | CI-friendly drift summary. |
| `GET`   | `/v1/sync/history/{type}/{id}`                 | All save/apply snapshots for a resource. Returns `source`, `createdBy`, `gitCommitSha`. |
| `GET`   | `/v1/sync/history/flow/{flowId}/publishes`     | **New.** Just the publish events — `isCurrent=true` marks the live version. |
| `GET`   | `/v1/runs?source=cli\|designer`                | Runs list filtered by the targeted version's provenance family. Each row carries `flowVersion`, `versionSource`, `versionGitCommitSha`. |

## Migration safety

Two migrations ship together:

- `1748300000_GovernanceAndProvenance.sql` adds the columns. Every existing workspace
  defaults to `GovernanceMode = 'Hybrid'` (the pre-feature behavior). Existing
  `FlowVersions` get the catch-all `Source = 'designer'` so the audit trail still
  starts now and not retroactively.
- `1748400000_ProvenanceBackfillAndIndexes.sql` runs immediately after and:
  - Splits the catch-all `'designer'` into the precise `'designer-publish'` /
    `'designer-draft'` labels based on `PublishedAtUtc` so old rows render
    sensibly in the new history panel.
  - Joins `FlowVersions.CreatedBy → Users.Email` to backfill `CreatedByEmail`
    where the new column is still null.
  - Adds three indexes that make the new query shapes O(log n) instead of
    full scans:
    - `IX_FlowVersions_Workspace_Source_Commit` — powers `/v1/sync/drift`.
    - `IX_FlowVersions_Flow_PublishedAt` — powers the new
      `/v1/sync/history/flow/{id}/publishes` endpoint.
    - `IX_ResourceVersions_Resource_Version` — covers `fcc history` and the
      designer snapshots tab.

Both migrations are idempotent. No behavior changes until an Owner explicitly
switches a workspace to `DesignerOnly` or `CodeManaged`.
