feat(api): /api/v1 projects, states, labels and work items with the oracle gate on those operations

オープン
#58 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
20/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust, sql

調査の方向性

Start with crates/thrust-api/src/lib.rs, the migration at crates/thrust-adapters-postgres/migrations/0001_init.sql, and the pinned oracle schema and harness described in the issue. Read the referenced ADRs and resolve the dependencies on #35, #36, and #47 before taking this on. Run scripts/oracle-v1.sh against native-pg and worker-cf; done means both and the Plane control pass with zero unaccepted or stale exceptions, corpus comparisons match, and the 32-operation schema diff has no breaking changes.

索引モデルが issue の本文から書いたものです。

説明

area:app prio:p0 review:fable-2026-10-19

Context

No wire-compatible route exists. thrust_api::router() (crates/thrust-api/src/lib.rs:269-293) puts only the slice's non-Plane routes under /api/v1 (:278-291), and #47 moves them out. Requests authenticate with Authorization: Bearer through the SessionStore (:82-99), not with X-Api-Key. Errors use a Thrust shape, {"error":{"code","message"}} (:47-49), and every authentication failure is a 401 (:54). The schema has projects (id, workspace_id, name) and a work_items table with no state, priority, labels, assignees, parent or dates (crates/thrust-adapters-postgres/migrations/0001_init.sql:4-8,26-41), and no states or labels table. Spike 0008 (bead plane-b6a) built the v1 oracle as the schema, an exception register and a golden corpus (research/0005; ADR-0122 round 26). The oracle exists only on a local, unpushed branch of the Plane fork, feat/plane-b6a-upstream-sync-oracle (research/0005:4-5; Q-0122.4, QUESTIONS.md:293), and it has only ever run against Plane.

Problem

  • M1 includes /api/v1 for projects, states, labels and work items, with the oracle gate on those operations (review §3, FR-01). ADR-0031 keeps v1 wire-compatible and confirms it with the oracle (lines 23, 39). ADR-0032 requires a CI breaking-change diff of the code-first schema (line 41). That diff is not done yet because it "needs the Rust v1 slice" (line 70).
  • In the pinned schema (oracle/schema/openapi.v1.json, 130 operations), these resources are the tags Projects (8), States (5), Labels (5) and Work Items (14): 32 operations, including both the issues/ and work-items/ families, search and lookup by identifier. The fuzzer skips seven of them (the six DELETEs and archive, under SX-001 and SX-002, oracle/exceptions.yaml:25-44). The corpus covers six of those seven; it has no exchange for DELETE .../issues/{pk}/.
  • The gate cannot be pointed at a partial implementation as it stands. run_conformance.py runs the whole schema (:56-80), a register entry that matches nothing fails as stale, and the vacuity floors are sized for the full surface (:205-212; exceptions.yaml:22-23: 100 operations tested, 40 with a 2xx). Only seven of the 12 entries touch the 32 operations (EX-001 to EX-004, EX-006, EX-007, EX-011), so the other five would go stale. Staleness is also counted per entry, so reproducing a quirk on one of an entry's operations hides its absence on the others.
  • The owner chose to reproduce these quirks (ADR-0031 lines 64-74; ADR-0033 lines 79, 83). Only a missing key gets 401; an invalid, expired or inactive key gets 403 {"detail":"Given API token is not valid"} (Plane apps/api/plane/api/middleware/api_authentication.py:32-38). Error bodies come in two shapes. State create returns 200. A per_page of 0 or −1, and some work-item create payloads, give 500 (EX-001, EX-002). ADR-0052 still says "revoke key → 401" (line 53).
  • Every in-scope corpus exchange that passes authentication and permissions carries x-ratelimit-remaining and x-ratelimit-reset; the 401 and 403 answers carry neither (oracle/NORMALISATION.md:23; Plane apps/api/plane/api/rate_limit.py:47-48, default 60/minute at settings/common.py:154). The port can report remaining (crates/thrust-ports/src/lib.rs:208-211). The Workers Rate Limiting binding (ADR-0033, round-29 note) reports only whether a request is allowed.

Proposal

  • Routes: serve the 32 operations under /api/v1/workspaces/{slug}/ as a thin surface over thrust-core use-cases (ADR-0031 line 60). The project and work-item use-cases are shared with #44; the state and label use-cases are new. Each one runs on a &Scope from authorize (#35), with the workspace resolved from {slug} through membership (#36).
  • Schema and types: add workspaces (with slug), workspace_members, api_tokens, states and labels, plus the work-item columns and join tables the v1 serializers return. Plane-shaped DTOs, the pagination envelope and error bodies stay out of thrust-contracts (the rule from #52). A reproduced 500 goes through plane_500() from #51, or is a handled 500 with the pinned body, never a panic (ADR-0031 line 74).
  • Auth: look up X-Api-Key by its SHA-256 hash (ADR-0052 K1; #38 tries this shape). Return 401 for a missing key, and 403 with Plane's body for an invalid, expired or inactive one. Rate-limit per key; a 429 carries {"error_code":5900,"error_message":"RATE_LIMIT_EXCEEDED"} and Retry-After. Send X-RateLimit-Remaining and X-RateLimit-Reset on every response that passes authentication and permissions. On Cloudflare the binding does not report these values, so they are derived; if the countdown cannot match Plane's, record a deviation.
  • Harness home: vendor oracle/ from the fork branch into tools/oracle-v1/ with a SOURCE file that names the commit, or push the branch instead (the owner decides, Q-0122.4). Either way, add the harness to the ADR-0006 tooling register; the uv rule applies.
  • Scoped gate: run_conformance.py --scope m1 passes Schemathesis --include-tag for the four tags (the script already passes --exclude-method and --exclude-path-regex, :77,79). It restricts the register to the in-scope operations, counts staleness per (entry, operation), and uses floors measured on Plane for this scope.
  • Seed and corpus:
    • A fixture loaded through /internal/seed (crates/thrust-api/src/lib.rs:150-156) reproduces the uuid5 ids, names and seven tokens of oracle/scripts/seed.py, so seed-manifest.json applies unchanged. Before every gate and control run, a reset script loads the fixture again.
    • record_corpus.py --only (:427) records against Thrust, and the result is compared exchange by exchange with the committed corpus for the in-scope operations. Add the missing DELETE .../issues/{pk}/ exchange to the corpus, recorded on Plane first.
    • scripts/oracle-v1.sh <target> runs the gate and the corpus comparison, and is one of the planned cells of gate.
  • Schema diff: generate the 32 operations with utoipa, then run oasdiff breaking against the pinned schema restricted to those operations. The EX-006 nullables are recorded exceptions. Never pass --open.

Acceptance criteria

  • scripts/oracle-v1.sh native-pg and scripts/oracle-v1.sh worker-cf end in VERDICT: PASS with unaccepted=0, stale=0 (per entry and operation) and the scoped floors met. The same scoped run against Plane passes too.
  • Control: with run_controls.py --upstream <thrust>, the gate fails C1, C2 and C3 (each breaks one field of GET .../states/, run_controls.py:33-44) on that operation, and passes C0. The corpus comparison catches C4 (a dropped id, which the schema cannot see). The results are committed.
  • After normalisation, the corpus comparison shows no difference for the in-scope exchanges in projects, states, labels, work_items, pagination and errors. A run that compares zero exchanges fails. Control: making state create return 201 makes it fail.
  • Contract tests pass on all four targets: no key gives 401; a bogus, expired or inactive key gives 403 with Plane's body; a create by a guest gives 403.
  • oasdiff reports no breaking change for the 32 operations, and the generated schema is committed.
  • The four list endpoints stay within the round-trip budget from #39.

References

  • Review: .agents/plans/contrail/reviews/2026-10-19-fable-architecture-review.md (FR-01; §3 M1 row; FR-06)
  • ADR-0031 (lines 23, 39, 60, 64-74), ADR-0032 (lines 29, 41, 70), ADR-0122 (lines 44, 77-79, 86, 90), ADR-0033 (lines 31, 79, 83; round-29 note), ADR-0052 (line 53), ADR-0006 (register), ADR-0034 (round-29 note)
  • research/0005-oracle-harness.md (lines 4-5, 49-55, 64-72, 88-89); spikes/0008-v1-oracle-harness.md; FEATURES §A5 (FEATURES.md:88-91), F-X06 (:135); Q-0122, Q-0122.4, Q-0120.3, Q-0033.2; DEVIATIONS C-2 (DEVIATIONS.md:32), D-002, D-007, D-013
  • crates/thrust-api/src/lib.rs:47-49,54,82-99,150-156,269-293; crates/thrust-ports/src/lib.rs:208-211; crates/thrust-adapters-postgres/migrations/0001_init.sql:4-8,26-41
  • Plane fork branch feat/plane-b6a-upstream-sync-oracle (72b2e01497): oracle/exceptions.yaml:12-23,25-44,45-89; oracle/harness/run_conformance.py:56-80,205-212; oracle/harness/run_controls.py:33-44; oracle/harness/record_corpus.py:427; oracle/NORMALISATION.md:23; oracle/scripts/seed.py; oracle/corpus/seed-manifest.json
  • Plane oracle (oracle/v1-pin-20261018): apps/api/plane/api/middleware/api_authentication.py:32-38; apps/api/plane/api/rate_limit.py:12-48; apps/api/plane/settings/common.py:154

Blocked by: #35, #36, #47

Finding: FR-01

主要言語
Rust
スター
0
フォーク
0
平均マージ
1時間 35分
マージ済み PR(30日)
67

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

aRustyDev/thrust のほかの issue

aRustyDev/thrust の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。