# v0.2.x — Change Request track (cycle handover)

> 2026-05-12. A v0.2.x hardening cycle — does **not** open a new
> minor version. Lives alongside the open frontend stack (#230–#235)
> and the v0.2 → v0.3 platform-readiness gate.
>
> Read order: `CLAUDE.md` → this file → `docs/ARCHITECTURE.md §5.6`
> (the trust zone) → `ROADMAP.md` v0.2.x section.

---

## 1. Problem

Every write inside JAMES today follows one of two shapes:

| Shape | Example | Governance today |
|---|---|---|
| Self-evolution | `/admin/proposals/{id}/approve` | ✅ `approver_username` recorded, audit-bridged, eval-gated |
| Everything else | wiki edit, workspace job run, ontology patch, config save | ❌ **none** — the write lands as soon as the call returns |

The first row exists because v0.1 already learned a hard rule: a
self-modifying system without a human approver is a bug, not a
feature (CLAUDE.md rule #3). The second row is the rest of the
system — and most user-visible "collaboration" friction comes from
its absence: there is no proposal, no review, no diff, no rollback,
no shared timeline.

**This cycle generalises the approver_username pattern into a single
primitive — the Change Request — and routes one new write through
it (wiki entity edits) so the pattern is real before we make it
universal.**

The primitive is deliberately **domain-neutral**: the same CR object
makes sense for a legal corp doing clause edits, a food retailer
editing nutrition labels, or a single operator editing their own
notes. Domain coupling enters the picture only at v1.0, per
`docs/PLATFORM_READINESS.md` and CLAUDE.md rule #1.

---

## 2. Scope (frozen for v0.2.x)

### In scope
- A new `core/change_request.py` module owning the CR lifecycle
- A new `core/change_request_apply.py` module owning the target-
  specific apply dispatcher + merge orchestrator
- Two SQLite tables — `change_requests` + `cr_reviews`
- Admin endpoints (`/admin/cr/...`) for propose / list / detail /
  approve / reject / review
- One `target_type=wiki_entity` apply path (markdown file edit with
  `base_hash` conflict detection)
- One follow-up `target_type=run_jobs` apply path (same cycle —
  shipped in CR-D)
- Workspace UI panel that lists CRs and lets admins approve/reject

### Out of scope (deferred follow-ups)
- **Self-evolution gate wrap onto CR** — descoped from this cycle
  (see § 5). The existing approver_username / eval-gate / rollback
  path stays as-is for v0.2.x. Re-routes into CR in CR-E or v0.3.
- External `target_type` registration API (today's `target_type` is
  a closed enum; new entries require a code change — on purpose)
- Multi-approver workflows ("2 of 3 admins must approve")
- Team / project / department scoping
- Hierarchical labels (flat `labels` CSV column today; v0.3 may
  migrate to a proper tag table)
- Status-machine extensions for domain-specific phases
  (Draft → Review → Published per-domain)
- External notification routing (Slack / e-mail on CR events)

Anything in the second list, if asked for during this cycle, gets a
flat **no** with a pointer back to this section.

---

## 3. Trust zone (single sentence)

`proposer (any authenticated user) → CR row inert in
change_requests → admin reviewer → apply() under transaction →
audit_bridge row`. Nothing else writes to the target.

The full statement lives in `docs/ARCHITECTURE.md §5.6` (the
architecture PR that ships **with** this handover).

### Invariants (will be enforced by tests in PR-CR-B)

1. `merged_at` / `merged_by` are NOT NULL iff `status='merged'`.
2. Approver MUST differ from proposer (no self-approval).
3. `base_hash` mismatch at merge time → 409 + `status='superseded'`.
4. Merge is a single SQLite transaction across (a) `change_requests`
   row update, (b) target apply call, (c) `audit_bridge` row insert.
   apply() raising rolls all three back.
5. apply() failure leaves `status='open'` (NOT `rejected`). A
   reject is an explicit reviewer decision, never an apply-side
   accident.
6. `target_type` unknown to the apply dispatcher → 400 at propose
   time (fail closed; matches the PolicyEngine `can_use_feature`
   pattern).
7. Every state transition writes one row to `audit_bridge` so the
   CR table can be reconstructed from the audit log even if the
   `change_requests` table itself is lost.

---

## 4. Tables (frozen schema)

```sql
CREATE TABLE change_requests (
  cr_id          TEXT PRIMARY KEY,         -- ULID-like sortable id
  target_type    TEXT NOT NULL,            -- 'wiki_entity' | 'run_jobs'
  target_id      TEXT NOT NULL,            -- e.g., entity_id or job spec hash
  title          TEXT NOT NULL,
  description    TEXT,
  proposed_diff  TEXT NOT NULL,            -- JSON; structure is target_type-specific
  base_hash      TEXT NOT NULL,            -- sha256 of target state at propose time
  proposer       TEXT NOT NULL,
  status         TEXT NOT NULL,            -- 'open' | 'merged' | 'rejected' | 'superseded'
  labels         TEXT,                     -- CSV; FLAT (v0.3 may migrate to a tag table)
  created_at     INTEGER NOT NULL,         -- unix epoch
  updated_at     INTEGER NOT NULL,
  merged_at      INTEGER,                  -- NULL until merged
  merged_by      TEXT,                     -- approver_username; NULL until merged
  reject_reason  TEXT                      -- NULL unless status='rejected'
);
CREATE INDEX idx_cr_status_target ON change_requests(status, target_type, target_id);
CREATE INDEX idx_cr_proposer ON change_requests(proposer, status);

CREATE TABLE cr_reviews (
  review_id   TEXT PRIMARY KEY,
  cr_id       TEXT NOT NULL REFERENCES change_requests(cr_id) ON DELETE CASCADE,
  reviewer    TEXT NOT NULL,
  decision    TEXT NOT NULL,               -- 'approve' | 'request_changes' | 'comment'
  body        TEXT,
  created_at  INTEGER NOT NULL
);
CREATE INDEX idx_cr_reviews_cr ON cr_reviews(cr_id, created_at);
```

This schema is **frozen for v0.2.x**. Migrations belong to v0.3 if
the plugin contract requires them.

---

## 5. PR sequence

| PR | Branch | Scope | Depends on | Gate |
|---|---|---|---|---|
| CR-A | `docs/v0.2-cr-track` | This file + ARCHITECTURE.md §5.6 + ROADMAP entry + doc-presence test | — | `architecture` label, 1500+ tests still green |
| CR-B1 | `feat/v0.2-cr-backend-b1` | `core/change_request.py` model + state machine + audit + invariants tests | CR-A merged | Bench numbers if `core/retrieval/graph/reasoning` touched (they should not be) |
| CR-B2 | `feat/v0.2-cr-backend-b2` | `core/change_request_apply.py` apply dispatcher + `wiki_entity` apply + 6 `/admin/cr/...` endpoints + integration tests | CR-B1 merged | Same — purely additive on top of B1 |
| CR-C | `feat/v0.2-cr-ui` | Workspace UI panel + i18n keys | CR-B2 merged | A11y + event-delegation tests still green |
| CR-D | `feat/v0.2-cr-jobs` | `run_jobs` apply path (second target_type) | CR-C merged | Workspace jobs DB schema untouched |

**Scope decision (2026-05-12)** — the original plan bundled
self-evolution gate re-routing into CR-D ("run_jobs + self-
evolution gate re-routes via CR"). After implementation experience
in CR-B/C/D-jobs, the self-evolution wrap is too large to land in
the same cycle without risking the byte-identical regression that
CLAUDE.md rule #3 forbids:

  - 3 endpoints (`/admin/patch/approve`, `/admin/proposals/{id}/approve`,
    `/admin/proposals/{id}/reject`),
  - 4 test files locked to specific JSONL shapes
    (`test_self_evolution_gate.py`, `test_evolution_bench_gate.py`,
    `test_evolution_rollback.py`, `test_evolution_audit_query.py`),
  - the eval gate + rollback path that v0.1 hardened over multiple
    PRs (#69 / #77 / #78 / #79).

A shadow-CR row that records each self-evolution approval
alongside the existing flow is the obvious wrap shape, but
implementing it correctly without altering the eval-gate or
rollback semantics is a cycle in itself. **Deferred to a follow-up
PR (CR-E) or, more likely, the v0.3 plugin-contract cycle** where
the unified audit shape becomes part of the platform contract.

Each PR creates its own handover update under
`docs/handovers/v0.2.x-cr-*.md` if non-trivial decisions are made
during implementation.

---

## 6. What this cycle deliberately does NOT do

- Touch `core/retrieval` / `core/graph` / `core/reasoning` (CLAUDE.md
  rule #2 — would require bench numbers in every PR; this cycle is
  governance, not reasoning).
- Modify `docs/PLATFORM_READINESS.md` (the gate definitions stay).
- Disable or auto-approve the self-evolution path. CR-D **wraps**
  the existing gate; the approver field, the eval gate, and the
  audit_bridge writes are preserved byte-for-byte.
- Expose any `target_type` registration API to plugins. The closed
  enum is the v0.3 contract surface — locking it now would force a
  breaking change later (per `docs/PLATFORM_READINESS.md` v0.3 gate).
- Land any UI element that implies a team / project / department
  model. Flat `labels` only.

If a reviewer requests any of these in the cycle, the answer is
**defer to v0.3**, not "let's just add it quickly".

---

## 7. Files this cycle touches

Predicted (will sharpen during CR-B implementation):

```
core/change_request.py                    # NEW, ≤ 15 KB target (CLAUDE.md rule #5)
core/change_request_apply.py              # NEW, target-specific dispatcher
server_llmwiki.py                         # +~200 lines for the 6 endpoints
frontend/workspace.html                   # CR tab
frontend/static/workspace.js              # CR list + detail
docs/ARCHITECTURE.md                      # §5.6 (this PR)
docs/handovers/v0.2.x-cr-track.md         # this file
ROADMAP.md                                # one v0.2.x bullet
tests/test_change_request_core.py         # state machine + invariants
tests/test_change_request_wiki_apply.py   # wiki target apply path
tests/test_change_request_jobs_apply.py   # run_jobs target apply path (CR-D)
tests/test_change_request_audit.py        # audit_bridge integration
tests/test_workspace_cr_panel.py          # UI contract (CR-C)
tests/test_cr_docs_presence.py            # this PR: keeps the docs load-bearing
```

`core/change_request.py` budget: 15 KB. If apply dispatcher growth
pushes it over, split as `core/change_request/__init__.py` + apply
sub-package — same pattern as `core/memory/`.

---

## 8. Decision log

- **First `target_type` = `wiki_entity`** (not `run_jobs`) because
  wiki edits currently have zero governance and the data value is
  immediately visible. `run_jobs` is the second target in the same
  cycle (CR-D) — pattern reuse, ~80 lines.
- **`base_hash` over optimistic locking version numbers** because
  wiki entries are markdown files on disk. SHA256(file bytes) at
  propose time is the natural "what I was looking at" anchor.
- **`labels` is a CSV column** (not a tag table) because hierarchical
  scoping (team / project / department) is exactly the v0.3 plugin
  contract surface — locking it now would force a breaking change.
- **No external `target_type` registration API** for the same reason.
- **Approver ≠ proposer** is the only baked-in workflow rule.
  Multi-approver flows belong to v0.3.
- **Self-evolution stays untouched until CR-D** so that the
  approval / eval / rollback chain (`tests/test_self_evolution_gate.py`,
  `tests/test_evolution_bench_gate.py`, `tests/test_evolution_rollback.py`)
  has zero behavioural drift across CR-A/B/C.

---

## 9. 한국어 요약

자메스 안에서 쓰기 작업에 대한 **결재·감사·롤백 사이클**을 단일
원시(Change Request)로 모은다. 지금은 자가-진화만 결재 게이트가
있고 위키 편집 / 잡 실행 / 온톨로지 변경은 전부 무관문 통과 중.

v0.2.x 범위: `core/change_request.py` 신규 + `wiki_entity` /
`run_jobs` 두 타깃 + 워크스페이스 UI + 자가-진화 게이트 통합.
다중 결재자 · 팀/부서 · 외부 `target_type` 등록 API 는 v0.3
플러그인 계약과 함께. 도메인 가정은 들이지 않는다 (CLAUDE.md
규칙 #1).
