merge: orchestration
This commit is contained in:
commit
6013048f68
|
|
@ -0,0 +1 @@
|
||||||
|
{"id":"orch-plan-20260622-085006-1","ticket_id":"00001KVNKD56W","kind":"accepted_plan","accepted_plan":{"summary":"Align Workspace server SQLite bootstrap/migrations and tests with the canonical `schema-v0.md` artifact: typed repository/ticket-target/worker-link/artifact/audit schema, no canonical runs/hosts/workers/generic JSON/validation result tables, while preserving filesystem Ticket/Objective authority and existing read APIs.","branch":"impl/00001KVNKD56W-workspace-db-schema-v0","worktree":"/home/hare/Projects/yoi/.worktree/00001KVNKD56W-workspace-db-schema-v0","role_plan":"Orchestrator creates a dedicated child worktree and spawns a narrow-scope Coder. Reviewer will be spawned read-only after Coder reports implementation/design alignment commit(s). After approval, Orchestrator integrates into `orchestration`, validates workspace-server schema/tests/Nix if needed, records closure, and cleans only the child worktree/branch."},"author":"yoi-orchestrator","at":"2026-06-22T08:50:06Z"}
|
||||||
|
|
@ -512,3 +512,9 @@ If implementation is included in this Ticket, prefer a small non-breaking migrat
|
||||||
- Do not create a full `actors` table in v0.
|
- Do not create a full `actors` table in v0.
|
||||||
- Do not create `hosts` / `workers` canonical tables in v0.
|
- Do not create `hosts` / `workers` canonical tables in v0.
|
||||||
- Do not create a separate `runs` table in v0; use structured Ticket events and TicketWorkerLink relationships.
|
- Do not create a separate `runs` table in v0; use structured Ticket events and TicketWorkerLink relationships.
|
||||||
|
|
||||||
|
## Implementation alignment notes
|
||||||
|
|
||||||
|
The `yoi-workspace-server` SQLite bootstrap migration implements this v0 schema as schema version 2. Fresh databases create the typed tables listed above and deliberately do not create canonical `runs`, `hosts`, `workers`, `actors`, or check/validation result tables. Host and Worker HTTP read APIs remain live runtime views backed by local inspection, not DB tables.
|
||||||
|
|
||||||
|
For databases created by the earlier workspace-server bootstrap, migration version 2 preserves old `workspaces`, `repositories`, `runs`, `artifacts`, `ticket_projections`, and `objective_projections` data by renaming those tables to `legacy_workspaces`, `legacy_repositories`, `legacy_runs`, `legacy_artifacts`, `legacy_ticket_projections`, and `legacy_objective_projections`, then creating the v0 typed tables. Existing legacy workspace rows are copied into the canonical v0 `workspaces` table with `state = active` when the old row had no typed state. The legacy names are compatibility preservation only and are not canonical schema tables or active write authority.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Workspace DB canonical schema design'
|
title: 'Workspace DB canonical schema design'
|
||||||
state: 'queued'
|
state: 'closed'
|
||||||
created_at: '2026-06-21T17:24:43Z'
|
created_at: '2026-06-21T17:24:43Z'
|
||||||
updated_at: '2026-06-22T08:48:39Z'
|
updated_at: '2026-06-22T09:27:06Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-06-22T08:48:39Z'
|
queued_at: '2026-06-22T08:48:39Z'
|
||||||
|
|
|
||||||
36
.yoi/tickets/00001KVNKD56W/resolution.md
Normal file
36
.yoi/tickets/00001KVNKD56W/resolution.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
Workspace DB canonical schema v0 design を `schema-v0.md` artifact と workspace-server SQLite bootstrap/migrations に整合させ、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- SQLite schema version を `2` に進め、canonical schema v0 を実装。
|
||||||
|
- Typed v0 tables を追加:
|
||||||
|
- `tickets`
|
||||||
|
- `ticket_events`
|
||||||
|
- `ticket_relations`
|
||||||
|
- `objectives`
|
||||||
|
- `objective_ticket_links`
|
||||||
|
- `repositories`
|
||||||
|
- `ticket_targets`
|
||||||
|
- `ticket_target_paths`
|
||||||
|
- `ticket_worker_links`
|
||||||
|
- `artifacts`
|
||||||
|
- `audit_events`
|
||||||
|
- Canonical v0 から top-level `runs`, `hosts`, `workers`, `actors`, validation/CI result tables を除外。
|
||||||
|
- Generic `metadata_json`, `payload_json`, `diagnostics_json` のような catch-all payload columns を canonical v0 tables に追加しない方針を維持。
|
||||||
|
- `/api/runs` と frontend Runs card/reference を削除し、404 test を追加。
|
||||||
|
- Host/Worker APIs は DB authority ではなく live runtime views として維持。
|
||||||
|
- Legacy bootstrap tables は non-canonical `legacy_*` preservation tables に demote。
|
||||||
|
- Legacy `workspaces` は `legacy_workspaces` に preserve し、active canonical `workspaces` を v0 column set で作り直して既存行を copy。
|
||||||
|
- Post-upgrade `upsert_workspace()` for new workspace id が通る regression test を追加。
|
||||||
|
- `schema-v0.md` に SQLite version 2 / legacy preservation alignment notes を追加。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `38bd122d merge: workspace db schema v0`
|
||||||
|
- Implementation commits: `5149ab70`, `d89ace5b`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi-workspace-server`, `cargo check -p yoi-workspace-server`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外:
|
||||||
|
- Ticket/Objective write authority migration to DB is not implemented。
|
||||||
|
- Host/Worker canonical DB tables are not added。
|
||||||
|
- Validation/CI result tables and Actor table are not added。
|
||||||
|
- Full TicketEvents/TicketWorkerLinks/Artifacts write surfaces are not implemented beyond schema/bootstrap alignment。
|
||||||
|
|
@ -30,4 +30,512 @@ Marked ready by `yoi ticket state`.
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-22T08:49:46Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: `implementation_ready`
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Ticket includes a concrete `schema-v0.md` artifact defining the Workspace DB canonical schema target and migration stance。
|
||||||
|
- Artifact is precise enough to implement a non-breaking schema/migration alignment slice and/or finish design documentation without inventing semantics。
|
||||||
|
- Relations are non-blocking context relations; no blocker relation is recorded。
|
||||||
|
- Current Workspace backend has an older bootstrap SQLite schema in `crates/workspace-server/src/store.rs` with `runs`, `ticket_projections`, `objective_projections`, and run-linked artifacts, while `schema-v0.md` explicitly says no separate `Run` entity and Host/Worker are live views. This gives a concrete implementation alignment target。
|
||||||
|
- Orchestrator worktree is clean on `orchestration` at `b7c890d3`; target worktree / branch is not present。
|
||||||
|
- Current queued Ticket is this Ticket only。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / artifacts via `TicketShow` and direct read of `artifacts/schema-v0.md`。
|
||||||
|
- `TicketRelationQuery(00001KVNKD56W)`: 2 relation records, no blocking relation reported。
|
||||||
|
- `TicketOrchestrationPlanQuery(00001KVNKD56W)`: no records。
|
||||||
|
- `TicketList(state=queued)`: this Ticket is the only queued Ticket。
|
||||||
|
- Orchestrator git state / branch list checked from `/home/hare/Projects/yoi/.worktree/orchestration` only。
|
||||||
|
- Bounded code map:
|
||||||
|
- `crates/workspace-server/src/store.rs` currently owns SQLite migrations and schema versioning。
|
||||||
|
- Current store still has bootstrap `runs` and projection-oriented tables; `schema-v0.md` says v0 should not include separate `runs`, Host/Worker canonical DB tables, generic JSON blobs, or validation result tables。
|
||||||
|
- Current repository/objective/ticket read APIs are filesystem read-through and must keep working。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Turn the Workspace DB canonical schema v0 design into a durable implementation/design artifact and align the current SQLite bootstrap migration where safely non-breaking。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Ticket and Objective filesystem records remain the active authority; do not switch write authority to DB。
|
||||||
|
- No generic JSON/payload/metadata columns in the v0 canonical schema。
|
||||||
|
- Do not add canonical `hosts` / `workers` DB tables; Host/Worker remain live runtime views。
|
||||||
|
- Do not add a top-level `runs` canonical table; execution history belongs to Ticket events, TicketWorkerLinks, and Artifacts。
|
||||||
|
- Do not add CI/validation result tables in this Ticket。
|
||||||
|
- Do not add Actor table in v0; authorship is embedded typed snapshot fields。
|
||||||
|
- Keep existing Workspace APIs/tests working。
|
||||||
|
- Non-breaking migration is acceptable; destructive migration of existing user DBs is not required in this slice unless clearly safe and tested。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- `schema-v0.md` remains consistent with implemented/bootstrap migration semantics。
|
||||||
|
- Workspace-server migration/schema tests cover new v0 schema version and expected tables/absence of forbidden tables。
|
||||||
|
- Empty/safe tables from minimal implementation guidance may be added: `repositories`, `ticket_targets`, `ticket_target_paths`, `ticket_worker_links`, `artifacts`, `audit_events`, and supporting Ticket/Objective current/event tables if included。
|
||||||
|
- Remove or replace obsolete `runs` table/API/store concept if it conflicts with v0, or clearly demote it to non-canonical compatibility only if removal is out of scope。
|
||||||
|
- No canonical host/worker tables are added。
|
||||||
|
- Existing filesystem read-through APIs for Tickets/Objectives/Repository pages keep passing。
|
||||||
|
- Docs/tests make clear that Host/Worker API is live view and Ticket thread/events are execution authority。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- If full schema migration is too large, implement the smallest safe migration alignment plus tests and record remaining tables as future follow-up comments。
|
||||||
|
- Prefer typed tables/columns over catch-all text/json columns。
|
||||||
|
- SQLite schema version can advance if migrations change。
|
||||||
|
- `runs` API placeholder may be removed if no longer used; ensure frontend/tests do not depend on it。
|
||||||
|
- Keep API surface read-only unless Ticket explicitly requires otherwise; this one does not require write APIs。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Removing current `runs` table/API would break existing accepted frontend or tests in a way that needs product decision。
|
||||||
|
- Existing DB migration compatibility requires a complex destructive migration or data backfill policy。
|
||||||
|
- Implementing full TicketEvents/TicketWorkerLinks/Artifacts read/write surfaces grows beyond schema/bootstrap alignment。
|
||||||
|
- Schema design conflicts with recent Repository/Objectives pages in a way not anticipated by artifact。
|
||||||
|
|
||||||
|
Validation plan:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -p yoi-workspace-server`
|
||||||
|
- `cargo check -p yoi-workspace-server`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo run -p yoi -- ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link` if package/source/dependency behavior changes。
|
||||||
|
- Deno check/build only if frontend/runs API removal touches frontend。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-22T08:49:57Z from: queued to: inprogress reason: human_authorized_schema_design_alignment field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Human authorized queue routing from Workspace Dashboard. Ticket has a concrete schema-v0 design artifact and current workspace-server migration has a clear non-breaking alignment target, so Orchestrator accepts implementation/design alignment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-22T08:51:00Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation start report:
|
||||||
|
|
||||||
|
- Created child implementation worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Created branch:
|
||||||
|
- `impl/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Base commit:
|
||||||
|
- `f17a458a ticket: accept workspace db schema v0`
|
||||||
|
- Confirmed tracked Ticket project records are visible in the child worktree。
|
||||||
|
- Confirmed `.yoi/memory` has no tracked/untracked entries in the child worktree check。
|
||||||
|
- Spawned sibling Coder Pod:
|
||||||
|
- `yoi-coder-00001KVNKD56W`
|
||||||
|
- Coder scope:
|
||||||
|
- read: `/home/hare/Projects/yoi`
|
||||||
|
- write: `/home/hare/Projects/yoi/.worktree/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Wait for Coder implementation/design-alignment report, then spawn Reviewer read-only for the implementation diff。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-22T09:02:51Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder implementation report received from `yoi-coder-00001KVNKD56W`.
|
||||||
|
|
||||||
|
Implementation commit:
|
||||||
|
- `5149ab70 workspace: implement db schema v0 bootstrap`
|
||||||
|
|
||||||
|
Changed areas:
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/artifacts/schema-v0.md`
|
||||||
|
- Added implementation alignment notes for SQLite schema version 2。
|
||||||
|
- Documented legacy preservation/demotion for old bootstrap tables。
|
||||||
|
- `crates/workspace-server/src/store.rs`
|
||||||
|
- Replaced old bootstrap schema with Workspace DB canonical schema v0 tables。
|
||||||
|
- Advanced schema version to `2`。
|
||||||
|
- Added typed v0 tables: `tickets`, `ticket_events`, `ticket_relations`, `objectives`, `objective_ticket_links`, `repositories`, `ticket_targets`, `ticket_target_paths`, `ticket_worker_links`, `artifacts`, `audit_events`。
|
||||||
|
- Removed active/canonical `runs` store concept and `RunSummary`。
|
||||||
|
- Added legacy migration handling that renames old bootstrap tables to `legacy_*` preservation tables instead of treating them as canonical。
|
||||||
|
- Added tests for fresh v0 schema boundaries and legacy upgrade behavior, including forbidden table absence。
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- Removed `/api/runs` route。
|
||||||
|
- Kept Host/Worker APIs as live runtime views。
|
||||||
|
- Added test coverage that `/api/runs` is no longer served。
|
||||||
|
- `web/workspace/src/routes/+page.svelte`
|
||||||
|
- Removed Runs endpoint card/reference。
|
||||||
|
|
||||||
|
Coder validation reported:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `git diff --check`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `cd web/workspace && deno task check`: passed
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
|
||||||
|
Not run by Coder:
|
||||||
|
- `nix build .#yoi --no-link`: not run because no dependency/package/source-filter changes。
|
||||||
|
|
||||||
|
Orchestrator evidence checked:
|
||||||
|
- Worktree clean on `impl/00001KVNKD56W-workspace-db-schema-v0` at `5149ab70`。
|
||||||
|
- Diff `f17a458a..HEAD` touches expected design/backend/frontend files only。
|
||||||
|
- `git diff --check f17a458a..HEAD`: passed。
|
||||||
|
- Deno validation generated ignored frontend artifacts; Orchestrator removed `web/workspace/node_modules/`, `.svelte-kit/`, and `build/` before Reviewer routing。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to read-only Reviewer Pod for acceptance review。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: comment author: yoi-orchestrator at: 2026-06-22T09:03:49Z -->
|
||||||
|
|
||||||
|
## Comment
|
||||||
|
|
||||||
|
Review routing:
|
||||||
|
|
||||||
|
Spawned read-only Reviewer Pod:
|
||||||
|
- `yoi-reviewer-00001KVNKD56W-r1`
|
||||||
|
|
||||||
|
Review target:
|
||||||
|
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Branch: `impl/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Baseline: `f17a458a`
|
||||||
|
- Implementation commit: `5149ab70 workspace: implement db schema v0 bootstrap`
|
||||||
|
- Diff: `f17a458a..5149ab70`
|
||||||
|
|
||||||
|
Reviewer scope:
|
||||||
|
- read-only `/home/hare/Projects/yoi`
|
||||||
|
|
||||||
|
Reviewer focus:
|
||||||
|
- Consistency between `schema-v0.md` and implemented SQLite migration/version 2。
|
||||||
|
- Absence of canonical `runs`, `hosts`, `workers`, `actors`, generic JSON/payload/metadata columns, and validation/CI result tables。
|
||||||
|
- Legacy table preservation/demotion behavior。
|
||||||
|
- Existing read APIs for Tickets/Objectives/Repositories/Hosts/Workers still work。
|
||||||
|
- `/api/runs` removal is intentional and test-covered。
|
||||||
|
- Design artifact updates remain concise and consistent。
|
||||||
|
|
||||||
|
Orchestrator will wait for reviewer verdict before integration。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KVNKD56W-r1 at: 2026-06-22T09:13:06Z status: request_changes -->
|
||||||
|
|
||||||
|
## Review: request changes
|
||||||
|
|
||||||
|
Verdict: `request_changes`
|
||||||
|
|
||||||
|
確認範囲:
|
||||||
|
- Ticket/design authority:
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/item.md`
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/thread.md`
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/artifacts/schema-v0.md`
|
||||||
|
- Implementation diff `f17a458a..5149ab70`:
|
||||||
|
- `crates/workspace-server/src/store.rs`
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- `web/workspace/src/routes/+page.svelte`
|
||||||
|
- schema artifact updates
|
||||||
|
- Target branch/head:
|
||||||
|
- Branch: `impl/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Head: `5149ab703f63`
|
||||||
|
|
||||||
|
Blocking issue:
|
||||||
|
|
||||||
|
1. Legacy-upgraded `workspaces` table remains non-canonical and can reject new workspace inserts。
|
||||||
|
|
||||||
|
Fresh schema defines canonical `workspaces` as:
|
||||||
|
- `workspace_id`
|
||||||
|
- `display_name`
|
||||||
|
- `state`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
This matches `schema-v0.md`。
|
||||||
|
|
||||||
|
However, legacy migration does not rebuild or demote the old bootstrap `workspaces` table. It only adds `state`:
|
||||||
|
- `crates/workspace-server/src/store.rs:198-200`
|
||||||
|
- `ALTER TABLE workspaces ADD COLUMN state TEXT NOT NULL DEFAULT 'active';`
|
||||||
|
|
||||||
|
Then `create_schema_v0_tables()` is called, but because it uses `CREATE TABLE IF NOT EXISTS workspaces`, the old table is left in place:
|
||||||
|
- `crates/workspace-server/src/store.rs:218-227`
|
||||||
|
|
||||||
|
The old v1 bootstrap `workspaces` table had extra `local_root TEXT NOT NULL` and `record_authority TEXT NOT NULL` columns. Those remain on upgraded DBs even though they are not part of canonical v0。
|
||||||
|
|
||||||
|
This is not merely cosmetic: `upsert_workspace()` now inserts only v0 columns:
|
||||||
|
- `crates/workspace-server/src/store.rs:87-89`
|
||||||
|
|
||||||
|
For an upgraded legacy DB, inserting a new `workspace_id` would fail on old `local_root` / `record_authority` NOT NULL columns because the insert no longer supplies them. Updating the already-existing legacy workspace may pass due conflict/update path, which is why current tests do not catch this。
|
||||||
|
|
||||||
|
Why this blocks:
|
||||||
|
- Upgraded canonical schema does not actually match `schema-v0.md`。
|
||||||
|
- Legacy handling is not safe enough because active canonical `workspaces` remains partly old schema and incompatible with the new write path。
|
||||||
|
|
||||||
|
Required fix:
|
||||||
|
- Rebuild/copy `workspaces` into canonical v0 shape, or otherwise explicitly preserve old workspace data without leaving obsolete NOT NULL columns on the active canonical table。
|
||||||
|
- Add tests asserting upgraded `workspaces` column set。
|
||||||
|
- Add a post-upgrade insert/upsert test for a new workspace id。
|
||||||
|
|
||||||
|
Non-blocking concerns:
|
||||||
|
- Existing tests cover fresh forbidden table absence and legacy demotion for old `runs` / projections / artifact / repository tables, but not upgraded `workspaces` column set or post-upgrade insert; add coverage with the fix。
|
||||||
|
- Runs card/reference removal is narrow and `/api/runs` removal is test-covered with 404 assertion。
|
||||||
|
|
||||||
|
Reviewer validation:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `git diff --check f17a458a..HEAD`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`10 passed`)
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check && deno task build`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
- `nix build .#yoi --no-link --print-out-paths`: passed/cached output `/nix/store/i0dr920x6kf217zddbnaqyljh8l1gwri-yoi-0.1.0`
|
||||||
|
|
||||||
|
Generated/ignored artifacts remain in reviewer worktree after validation:
|
||||||
|
- `target/`
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
|
||||||
|
Worktree status:
|
||||||
|
- Tracked status clean; only ignored generated artifacts present。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-22T09:18:29Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Coder r1-fix report received from `yoi-coder-00001KVNKD56W`.
|
||||||
|
|
||||||
|
New fix commit:
|
||||||
|
- `d89ace5b workspace: canonicalize legacy workspaces`
|
||||||
|
|
||||||
|
Branch commits now:
|
||||||
|
- `5149ab70 workspace: implement db schema v0 bootstrap`
|
||||||
|
- `d89ace5b workspace: canonicalize legacy workspaces`
|
||||||
|
|
||||||
|
Fix summary:
|
||||||
|
- `crates/workspace-server/src/store.rs`
|
||||||
|
- Legacy migration now preserves old non-canonical `workspaces` by renaming it to `legacy_workspaces`。
|
||||||
|
- Active canonical v0 `workspaces` is created with only:
|
||||||
|
- `workspace_id`
|
||||||
|
- `display_name`
|
||||||
|
- `state`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
- Existing legacy workspace rows are copied into canonical `workspaces`, defaulting `state` to `active` when no typed state existed。
|
||||||
|
- Previous legacy demotion for `repositories`, `runs`, `artifacts`, `ticket_projections`, and `objective_projections` is preserved。
|
||||||
|
- Added upgrade test assertions for:
|
||||||
|
- upgraded `workspaces` column set exactly matches v0。
|
||||||
|
- `legacy_workspaces` preserves old obsolete columns。
|
||||||
|
- old workspace data is copied into canonical table。
|
||||||
|
- post-upgrade `upsert_workspace()` succeeds for a new workspace id。
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/artifacts/schema-v0.md`
|
||||||
|
- Updated implementation alignment notes to document `legacy_workspaces` preservation and canonical copy behavior。
|
||||||
|
|
||||||
|
Coder validation reported:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `git diff --check f17a458a..HEAD`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed
|
||||||
|
|
||||||
|
Not rerun by Coder for the r1 fix:
|
||||||
|
- Deno check/build and Nix build, because the fix touched only schema artifact and workspace-server store code and Reviewer had validated frontend/Nix before the blocker。
|
||||||
|
|
||||||
|
Generated artifact cleanup:
|
||||||
|
- Coder removed reviewer-generated frontend artifacts:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
|
||||||
|
Orchestrator evidence checked:
|
||||||
|
- Worktree clean on `impl/00001KVNKD56W-workspace-db-schema-v0` at `d89ace5b`。
|
||||||
|
- r1-fix diff `5149ab70..HEAD` touches only `crates/workspace-server/src/store.rs` and `schema-v0.md`。
|
||||||
|
- `git diff --check f17a458a..HEAD`: passed。
|
||||||
|
- No ignored frontend artifacts remain under `web/workspace`。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route back to Reviewer for r2 verification of legacy `workspaces` canonicalization and final acceptance。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KVNKD56W-r1 at: 2026-06-22T09:24:03Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
Verdict: `approve`
|
||||||
|
|
||||||
|
確認範囲:
|
||||||
|
- r1-fix diff `5149ab70..d89ace5b`:
|
||||||
|
- `crates/workspace-server/src/store.rs`
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/artifacts/schema-v0.md`
|
||||||
|
- Overall diff `f17a458a..d89ace5b`:
|
||||||
|
- `.yoi/tickets/00001KVNKD56W/artifacts/schema-v0.md`
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- `crates/workspace-server/src/store.rs`
|
||||||
|
- `web/workspace/src/routes/+page.svelte`
|
||||||
|
|
||||||
|
Previous blocker verification:
|
||||||
|
- Resolved。
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Non-canonical legacy `workspaces` is now detected by exact v0 column comparison and renamed to `legacy_workspaces`。
|
||||||
|
- Canonical v0 `workspaces` is then created with exactly:
|
||||||
|
- `workspace_id`
|
||||||
|
- `display_name`
|
||||||
|
- `state`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
- Legacy workspace rows are copied into canonical `workspaces`, with `state` defaulted to `active` when absent。
|
||||||
|
- Tests assert:
|
||||||
|
- upgraded `workspaces` columns exactly match v0。
|
||||||
|
- `legacy_workspaces` preserves old obsolete columns。
|
||||||
|
- legacy workspace data is readable through canonical `get_workspace`。
|
||||||
|
- post-upgrade `upsert_workspace()` succeeds for a new workspace id。
|
||||||
|
|
||||||
|
Blocking issues:
|
||||||
|
- None。
|
||||||
|
|
||||||
|
Non-blocking concerns:
|
||||||
|
- None requiring changes。
|
||||||
|
|
||||||
|
Ticket/design fit:
|
||||||
|
- Fresh schema creates typed v0 tables and does not create canonical top-level `runs`, `hosts`, `workers`, `actors`, or validation/CI result tables。
|
||||||
|
- Legacy `runs` is preserved only as `legacy_runs`; it does not masquerade as canonical。
|
||||||
|
- Host/Worker APIs remain live runtime views, not DB authority。
|
||||||
|
- `/api/runs` remains removed/404-tested。
|
||||||
|
- Canonical schema avoids generic `metadata_json`, `payload_json`, `diagnostics_json`, and catch-all payload columns。
|
||||||
|
- `schema-v0.md` update is narrow and consistent with migration behavior。
|
||||||
|
- No accidental frontend authority/business-logic expansion found。
|
||||||
|
|
||||||
|
Reviewer validation:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `git diff --check f17a458a..HEAD`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`10 passed`)
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link --print-out-paths`: passed, output `/nix/store/fnz7650sshl3md2vdwb4a1q8dc0hf542-yoi-0.1.0`
|
||||||
|
|
||||||
|
Not rerun in r2:
|
||||||
|
- Deno frontend check/build, because r1-fix diff touches only `store.rs` and `schema-v0.md`; r1 had already validated frontend for original `/api/runs` card removal。
|
||||||
|
|
||||||
|
Worktree status:
|
||||||
|
- Clean tracked status on `impl/00001KVNKD56W-workspace-db-schema-v0` at `d89ace5b`。
|
||||||
|
- Ignored artifact present: `target/` only; no ignored frontend artifacts under `web/workspace`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-22T09:25:49Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Reviewer r2 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
統合:
|
||||||
|
- Merge commit: `38bd122d merge: workspace db schema v0`
|
||||||
|
- Merged branch: `impl/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Implementation commits:
|
||||||
|
- `5149ab70 workspace: implement db schema v0 bootstrap`
|
||||||
|
- `d89ace5b workspace: canonicalize legacy workspaces`
|
||||||
|
|
||||||
|
Orchestrator worktree validation:
|
||||||
|
- `cargo fmt --check`: passed
|
||||||
|
- `git diff --check HEAD^1..HEAD`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: passed (`10 passed`)
|
||||||
|
- `cargo check -p yoi-workspace-server`: passed
|
||||||
|
- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`)
|
||||||
|
- `cd web/workspace && deno task build`: passed
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
|
||||||
|
Validation generated ignored frontend artifacts in Orchestrator worktree:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
|
||||||
|
These were removed after validation. Final Orchestrator worktree status after validation cleanup is clean on `orchestration` at `38bd122d`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-22T09:26:02Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Reviewer approval、Orchestrator worktree への統合、workspace-server tests/check、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-22T09:26:19Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-22T09:26:19Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Workspace DB canonical schema v0 design を `schema-v0.md` artifact と workspace-server SQLite bootstrap/migrations に整合させ、Orchestrator worktree の `orchestration` branch に統合した。
|
||||||
|
|
||||||
|
主な成果:
|
||||||
|
- SQLite schema version を `2` に進め、canonical schema v0 を実装。
|
||||||
|
- Typed v0 tables を追加:
|
||||||
|
- `tickets`
|
||||||
|
- `ticket_events`
|
||||||
|
- `ticket_relations`
|
||||||
|
- `objectives`
|
||||||
|
- `objective_ticket_links`
|
||||||
|
- `repositories`
|
||||||
|
- `ticket_targets`
|
||||||
|
- `ticket_target_paths`
|
||||||
|
- `ticket_worker_links`
|
||||||
|
- `artifacts`
|
||||||
|
- `audit_events`
|
||||||
|
- Canonical v0 から top-level `runs`, `hosts`, `workers`, `actors`, validation/CI result tables を除外。
|
||||||
|
- Generic `metadata_json`, `payload_json`, `diagnostics_json` のような catch-all payload columns を canonical v0 tables に追加しない方針を維持。
|
||||||
|
- `/api/runs` と frontend Runs card/reference を削除し、404 test を追加。
|
||||||
|
- Host/Worker APIs は DB authority ではなく live runtime views として維持。
|
||||||
|
- Legacy bootstrap tables は non-canonical `legacy_*` preservation tables に demote。
|
||||||
|
- Legacy `workspaces` は `legacy_workspaces` に preserve し、active canonical `workspaces` を v0 column set で作り直して既存行を copy。
|
||||||
|
- Post-upgrade `upsert_workspace()` for new workspace id が通る regression test を追加。
|
||||||
|
- `schema-v0.md` に SQLite version 2 / legacy preservation alignment notes を追加。
|
||||||
|
|
||||||
|
統合・検証:
|
||||||
|
- Merge commit: `38bd122d merge: workspace db schema v0`
|
||||||
|
- Implementation commits: `5149ab70`, `d89ace5b`
|
||||||
|
- Reviewer final verdict: approve
|
||||||
|
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi-workspace-server`, `cargo check -p yoi-workspace-server`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。
|
||||||
|
|
||||||
|
範囲外:
|
||||||
|
- Ticket/Objective write authority migration to DB is not implemented。
|
||||||
|
- Host/Worker canonical DB tables are not added。
|
||||||
|
- Validation/CI result tables and Actor table are not added。
|
||||||
|
- Full TicketEvents/TicketWorkerLinks/Artifacts write surfaces are not implemented beyond schema/bootstrap alignment。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-22T09:27:06Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Post-close cleanup completed。
|
||||||
|
|
||||||
|
- Stopped child Pods and reclaimed scope:
|
||||||
|
- `yoi-coder-00001KVNKD56W`
|
||||||
|
- `yoi-reviewer-00001KVNKD56W-r1`
|
||||||
|
- Removed ignored frontend validation artifacts from child worktree before worktree removal if present:
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- Removed implementation worktree:
|
||||||
|
- `/home/hare/Projects/yoi/.worktree/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Deleted implementation branch:
|
||||||
|
- `impl/00001KVNKD56W-workspace-db-schema-v0`
|
||||||
|
- Orchestrator worktree remains clean on `orchestration` at `7a7891d4`。
|
||||||
|
|
||||||
|
Root/original workspace was not used for merge/validation/cleanup operations。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::records::{
|
||||||
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary,
|
||||||
};
|
};
|
||||||
use crate::repositories::{LocalRepositoryReader, RepositoryLogRead, RepositorySummary};
|
use crate::repositories::{LocalRepositoryReader, RepositoryLogRead, RepositorySummary};
|
||||||
use crate::store::{ControlPlaneStore, RunSummary, WorkspaceRecord};
|
use crate::store::{ControlPlaneStore, WorkspaceRecord};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
@ -74,8 +74,7 @@ impl WorkspaceApi {
|
||||||
.upsert_workspace(&WorkspaceRecord {
|
.upsert_workspace(&WorkspaceRecord {
|
||||||
workspace_id: config.workspace_id.clone(),
|
workspace_id: config.workspace_id.clone(),
|
||||||
display_name,
|
display_name,
|
||||||
local_root: config.workspace_root.clone(),
|
state: "active".to_string(),
|
||||||
record_authority: "local_yoi_project_records".to_string(),
|
|
||||||
created_at: "1970-01-01T00:00:00Z".to_string(),
|
created_at: "1970-01-01T00:00:00Z".to_string(),
|
||||||
updated_at: "1970-01-01T00:00:00Z".to_string(),
|
updated_at: "1970-01-01T00:00:00Z".to_string(),
|
||||||
})
|
})
|
||||||
|
|
@ -127,7 +126,6 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||||
"/api/repositories/{repository_id}/tickets",
|
"/api/repositories/{repository_id}/tickets",
|
||||||
get(repository_tickets),
|
get(repository_tickets),
|
||||||
)
|
)
|
||||||
.route("/api/runs", get(list_runs))
|
|
||||||
.route("/api/hosts", get(list_hosts))
|
.route("/api/hosts", get(list_hosts))
|
||||||
.route("/api/workers", get(list_workers))
|
.route("/api/workers", get(list_workers))
|
||||||
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
|
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
|
||||||
|
|
@ -398,20 +396,6 @@ async fn repository_tickets(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_runs(
|
|
||||||
State(api): State<WorkspaceApi>,
|
|
||||||
) -> ApiResult<Json<RuntimeListResponse<RunSummary>>> {
|
|
||||||
let limit = api.config.max_records.min(200);
|
|
||||||
let items = api.store.list_runs(api.workspace_id(), limit).await?;
|
|
||||||
Ok(Json(RuntimeListResponse {
|
|
||||||
workspace_id: api.config.workspace_id,
|
|
||||||
limit,
|
|
||||||
items,
|
|
||||||
source: "sqlite_runtime_tables".to_string(),
|
|
||||||
diagnostics: Vec::new(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_hosts(
|
async fn list_hosts(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
) -> ApiResult<Json<RuntimeListResponse<HostSummary>>> {
|
||||||
|
|
@ -729,6 +713,18 @@ mod tests {
|
||||||
let host_workers = get_json(app.clone(), "/api/hosts/local-local-test/workers").await;
|
let host_workers = get_json(app.clone(), "/api/hosts/local-local-test/workers").await;
|
||||||
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
assert!(host_workers["items"].as_array().unwrap().is_empty());
|
||||||
|
|
||||||
|
let runs_response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/api/runs")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(runs_response.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let runners_response = app
|
let runners_response = app
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(
|
.oneshot(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|
@ -8,92 +8,38 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
const MIGRATIONS: &[Migration] = &[Migration {
|
const WORKSPACES_V0_COLUMNS: &[&str] = &[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MIGRATIONS: &[Migration] = &[
|
||||||
|
Migration {
|
||||||
version: 1,
|
version: 1,
|
||||||
name: "bootstrap workspace control plane",
|
name: "workspace db canonical schema v0 bootstrap",
|
||||||
sql: r#"
|
apply: create_schema_v0_tables,
|
||||||
CREATE TABLE IF NOT EXISTS workspaces (
|
},
|
||||||
workspace_id TEXT PRIMARY KEY,
|
Migration {
|
||||||
display_name TEXT NOT NULL,
|
version: 2,
|
||||||
local_root TEXT NOT NULL,
|
name: "align legacy workspace bootstrap with schema v0",
|
||||||
record_authority TEXT NOT NULL,
|
apply: align_legacy_bootstrap_schema,
|
||||||
created_at TEXT NOT NULL,
|
},
|
||||||
updated_at TEXT NOT NULL
|
];
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS repositories (
|
|
||||||
repository_id TEXT PRIMARY KEY,
|
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
|
||||||
local_root TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Projection tables are intentionally empty in this bootstrap: `.yoi/tickets`
|
|
||||||
-- and `.yoi/objectives` remain canonical, but the tables reserve a future
|
|
||||||
-- projection/cache seam without migrating authority.
|
|
||||||
CREATE TABLE IF NOT EXISTS ticket_projections (
|
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
|
||||||
ticket_id TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (workspace_id, ticket_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS objective_projections (
|
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
|
||||||
objective_id TEXT NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (workspace_id, objective_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS runs (
|
|
||||||
run_id TEXT PRIMARY KEY,
|
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
|
||||||
subject_kind TEXT NOT NULL,
|
|
||||||
subject_id TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS artifacts (
|
|
||||||
artifact_id TEXT PRIMARY KEY,
|
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
|
||||||
run_id TEXT REFERENCES runs(run_id) ON DELETE SET NULL,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
content_type TEXT,
|
|
||||||
created_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
"#,
|
|
||||||
}];
|
|
||||||
|
|
||||||
struct Migration {
|
struct Migration {
|
||||||
version: i64,
|
version: i64,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
sql: &'static str,
|
apply: fn(&Connection) -> Result<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct WorkspaceRecord {
|
pub struct WorkspaceRecord {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub local_root: PathBuf,
|
pub state: String,
|
||||||
pub record_authority: String,
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct RunSummary {
|
|
||||||
pub run_id: String,
|
|
||||||
pub workspace_id: String,
|
|
||||||
pub subject_kind: String,
|
|
||||||
pub subject_id: String,
|
|
||||||
pub status: String,
|
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +49,6 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
async fn schema_version(&self) -> Result<i64>;
|
async fn schema_version(&self) -> Result<i64>;
|
||||||
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
|
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
|
||||||
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>>;
|
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>>;
|
||||||
async fn list_runs(&self, workspace_id: &str, limit: usize) -> Result<Vec<RunSummary>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -148,18 +93,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"INSERT INTO workspaces (
|
r#"INSERT INTO workspaces (
|
||||||
workspace_id, display_name, local_root, record_authority, created_at, updated_at
|
workspace_id, display_name, state, created_at, updated_at
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
) VALUES (?1, ?2, ?3, ?4, ?5)
|
||||||
ON CONFLICT(workspace_id) DO UPDATE SET
|
ON CONFLICT(workspace_id) DO UPDATE SET
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
local_root = excluded.local_root,
|
state = excluded.state,
|
||||||
record_authority = excluded.record_authority,
|
|
||||||
updated_at = excluded.updated_at"#,
|
updated_at = excluded.updated_at"#,
|
||||||
params![
|
params![
|
||||||
record.workspace_id,
|
record.workspace_id,
|
||||||
record.display_name,
|
record.display_name,
|
||||||
record.local_root.to_string_lossy(),
|
record.state,
|
||||||
record.record_authority,
|
|
||||||
record.created_at,
|
record.created_at,
|
||||||
record.updated_at,
|
record.updated_at,
|
||||||
],
|
],
|
||||||
|
|
@ -171,17 +114,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>> {
|
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
r#"SELECT workspace_id, display_name, local_root, record_authority, created_at, updated_at
|
r#"SELECT workspace_id, display_name, state, created_at, updated_at
|
||||||
FROM workspaces WHERE workspace_id = ?1"#,
|
FROM workspaces WHERE workspace_id = ?1"#,
|
||||||
params![workspace_id],
|
params![workspace_id],
|
||||||
|row| {
|
|row| {
|
||||||
Ok(WorkspaceRecord {
|
Ok(WorkspaceRecord {
|
||||||
workspace_id: row.get(0)?,
|
workspace_id: row.get(0)?,
|
||||||
display_name: row.get(1)?,
|
display_name: row.get(1)?,
|
||||||
local_root: PathBuf::from(row.get::<_, String>(2)?),
|
state: row.get(2)?,
|
||||||
record_authority: row.get(3)?,
|
created_at: row.get(3)?,
|
||||||
created_at: row.get(4)?,
|
updated_at: row.get(4)?,
|
||||||
updated_at: row.get(5)?,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -189,28 +131,6 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_runs(&self, workspace_id: &str, limit: usize) -> Result<Vec<RunSummary>> {
|
|
||||||
self.with_conn(|conn| {
|
|
||||||
let limit = limit.min(200) as i64;
|
|
||||||
let mut stmt = conn.prepare(
|
|
||||||
r#"SELECT run_id, workspace_id, subject_kind, subject_id, status, created_at, updated_at
|
|
||||||
FROM runs WHERE workspace_id = ?1 ORDER BY updated_at DESC, run_id DESC LIMIT ?2"#,
|
|
||||||
)?;
|
|
||||||
let rows = stmt.query_map(params![workspace_id, limit], |row| {
|
|
||||||
Ok(RunSummary {
|
|
||||||
run_id: row.get(0)?,
|
|
||||||
workspace_id: row.get(1)?,
|
|
||||||
subject_kind: row.get(2)?,
|
|
||||||
subject_id: row.get(3)?,
|
|
||||||
status: row.get(4)?,
|
|
||||||
created_at: row.get(5)?,
|
|
||||||
updated_at: row.get(6)?,
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(Error::from)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_sqlite(conn: &Connection) -> Result<()> {
|
fn configure_sqlite(conn: &Connection) -> Result<()> {
|
||||||
|
|
@ -246,7 +166,7 @@ fn apply_migrations(conn: &Connection) -> Result<()> {
|
||||||
.filter(|migration| migration.version > current)
|
.filter(|migration| migration.version > current)
|
||||||
{
|
{
|
||||||
let tx = conn.unchecked_transaction()?;
|
let tx = conn.unchecked_transaction()?;
|
||||||
tx.execute_batch(migration.sql)?;
|
(migration.apply)(&tx)?;
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)",
|
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (?1, ?2)",
|
||||||
params![migration.version, migration.name],
|
params![migration.version, migration.name],
|
||||||
|
|
@ -256,9 +176,311 @@ fn apply_migrations(conn: &Connection) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn align_legacy_bootstrap_schema(conn: &Connection) -> Result<()> {
|
||||||
|
if table_exists(conn, "repositories")?
|
||||||
|
&& column_exists(conn, "repositories", "local_root")?
|
||||||
|
&& !column_exists(conn, "repositories", "uri")?
|
||||||
|
{
|
||||||
|
rename_legacy_table(conn, "repositories", "legacy_repositories")?;
|
||||||
|
}
|
||||||
|
if table_exists(conn, "runs")? {
|
||||||
|
rename_legacy_table(conn, "runs", "legacy_runs")?;
|
||||||
|
}
|
||||||
|
if table_exists(conn, "artifacts")?
|
||||||
|
&& (column_exists(conn, "artifacts", "run_id")?
|
||||||
|
|| column_exists(conn, "artifacts", "path")?
|
||||||
|
|| !column_exists(conn, "artifacts", "uri")?)
|
||||||
|
{
|
||||||
|
rename_legacy_table(conn, "artifacts", "legacy_artifacts")?;
|
||||||
|
}
|
||||||
|
if table_exists(conn, "ticket_projections")? {
|
||||||
|
rename_legacy_table(conn, "ticket_projections", "legacy_ticket_projections")?;
|
||||||
|
}
|
||||||
|
if table_exists(conn, "objective_projections")? {
|
||||||
|
rename_legacy_table(
|
||||||
|
conn,
|
||||||
|
"objective_projections",
|
||||||
|
"legacy_objective_projections",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let legacy_workspaces = preserve_noncanonical_workspaces(conn)?;
|
||||||
|
create_schema_v0_tables(conn)?;
|
||||||
|
if let Some(legacy_table) = legacy_workspaces {
|
||||||
|
copy_legacy_workspaces(conn, &legacy_table)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preserve_noncanonical_workspaces(conn: &Connection) -> Result<Option<String>> {
|
||||||
|
if !table_exists(conn, "workspaces")? {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let columns = table_columns(conn, "workspaces")?;
|
||||||
|
if columns
|
||||||
|
.iter()
|
||||||
|
.map(String::as_str)
|
||||||
|
.eq(WORKSPACES_V0_COLUMNS.iter().copied())
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let legacy_table = "legacy_workspaces";
|
||||||
|
rename_legacy_table(conn, "workspaces", legacy_table)?;
|
||||||
|
Ok(Some(legacy_table.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_legacy_workspaces(conn: &Connection, legacy_table: &str) -> Result<()> {
|
||||||
|
let columns = table_columns(conn, legacy_table)?;
|
||||||
|
for required_column in ["workspace_id", "display_name", "created_at", "updated_at"] {
|
||||||
|
if !columns.iter().any(|column| column == required_column) {
|
||||||
|
return Err(Error::Store(format!(
|
||||||
|
"cannot migrate legacy workspaces: `{legacy_table}` is missing `{required_column}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let state_expr = if columns.iter().any(|column| column == "state") {
|
||||||
|
"COALESCE(NULLIF(state, ''), 'active')"
|
||||||
|
} else {
|
||||||
|
"'active'"
|
||||||
|
};
|
||||||
|
conn.execute_batch(&format!(
|
||||||
|
r#"INSERT OR IGNORE INTO workspaces (
|
||||||
|
workspace_id, display_name, state, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT workspace_id, display_name, {state_expr}, created_at, updated_at
|
||||||
|
FROM {legacy_table};"#
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_legacy_table(conn: &Connection, table_name: &str, legacy_name: &str) -> Result<()> {
|
||||||
|
if table_exists(conn, legacy_name)? {
|
||||||
|
return Err(Error::Store(format!(
|
||||||
|
"cannot preserve legacy table `{table_name}` because `{legacy_name}` already exists"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
conn.execute_batch(&format!(
|
||||||
|
"ALTER TABLE {table_name} RENAME TO {legacy_name};"
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_schema_v0_tables(conn: &Connection) -> Result<()> {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS workspaces (
|
||||||
|
workspace_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
priority TEXT,
|
||||||
|
assignee_kind TEXT,
|
||||||
|
assignee_key TEXT,
|
||||||
|
assignee_display TEXT,
|
||||||
|
body_md TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
closed_at TEXT,
|
||||||
|
resolution_event_id TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_events (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
event_id TEXT PRIMARY KEY,
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
event_seq INTEGER NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
activity_id TEXT,
|
||||||
|
author_kind TEXT NOT NULL,
|
||||||
|
author_key TEXT NOT NULL,
|
||||||
|
author_display TEXT NOT NULL,
|
||||||
|
author_source_kind TEXT,
|
||||||
|
author_source_key TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
body_md TEXT,
|
||||||
|
subject_kind TEXT,
|
||||||
|
subject_id TEXT,
|
||||||
|
previous_state TEXT,
|
||||||
|
new_state TEXT,
|
||||||
|
status TEXT,
|
||||||
|
artifact_id TEXT,
|
||||||
|
worker_ref_kind TEXT,
|
||||||
|
worker_ref_key TEXT,
|
||||||
|
worker_display TEXT,
|
||||||
|
host_ref_kind TEXT,
|
||||||
|
host_ref_key TEXT,
|
||||||
|
host_display TEXT,
|
||||||
|
repository_id TEXT,
|
||||||
|
caused_by_event_id TEXT,
|
||||||
|
UNIQUE (ticket_id, event_seq)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_relations (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
source_ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
target_ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
author_kind TEXT NOT NULL,
|
||||||
|
author_key TEXT NOT NULL,
|
||||||
|
author_display TEXT NOT NULL,
|
||||||
|
author_source_kind TEXT,
|
||||||
|
author_source_key TEXT,
|
||||||
|
note TEXT,
|
||||||
|
PRIMARY KEY (source_ticket_id, target_ticket_id, kind)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS objectives (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
objective_id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
body_md TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS objective_ticket_links (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
objective_id TEXT NOT NULL REFERENCES objectives(objective_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (objective_id, ticket_id, kind)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS repositories (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
repository_id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
provider TEXT,
|
||||||
|
uri TEXT NOT NULL,
|
||||||
|
default_ref TEXT,
|
||||||
|
auth_ref_kind TEXT,
|
||||||
|
auth_ref_key TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_targets (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
repository_id TEXT NOT NULL REFERENCES repositories(repository_id) ON DELETE CASCADE,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
intent TEXT NOT NULL,
|
||||||
|
ref_selector TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (ticket_id, target_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_target_paths (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (ticket_id, target_id, path),
|
||||||
|
FOREIGN KEY (ticket_id, target_id) REFERENCES ticket_targets(ticket_id, target_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_worker_links (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES tickets(ticket_id) ON DELETE CASCADE,
|
||||||
|
worker_ref_kind TEXT NOT NULL,
|
||||||
|
worker_ref_key TEXT NOT NULL,
|
||||||
|
worker_display TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
activity_id TEXT,
|
||||||
|
assigned_at TEXT,
|
||||||
|
released_at TEXT,
|
||||||
|
last_event_id TEXT,
|
||||||
|
PRIMARY KEY (ticket_id, worker_ref_kind, worker_ref_key, role)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS artifacts (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
artifact_id TEXT PRIMARY KEY,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
uri TEXT NOT NULL,
|
||||||
|
media_type TEXT,
|
||||||
|
sha256 TEXT,
|
||||||
|
size_bytes INTEGER,
|
||||||
|
summary TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
created_by_kind TEXT NOT NULL,
|
||||||
|
created_by_key TEXT NOT NULL,
|
||||||
|
created_by_display TEXT NOT NULL,
|
||||||
|
created_by_source_kind TEXT,
|
||||||
|
created_by_source_key TEXT,
|
||||||
|
ticket_id TEXT,
|
||||||
|
objective_id TEXT,
|
||||||
|
event_id TEXT,
|
||||||
|
worker_ref_kind TEXT,
|
||||||
|
worker_ref_key TEXT,
|
||||||
|
worker_display TEXT,
|
||||||
|
repository_id TEXT,
|
||||||
|
source_kind TEXT,
|
||||||
|
source_revision TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_events (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
audit_event_id TEXT PRIMARY KEY,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
actor_kind TEXT NOT NULL,
|
||||||
|
actor_key TEXT NOT NULL,
|
||||||
|
actor_display TEXT NOT NULL,
|
||||||
|
actor_source_kind TEXT,
|
||||||
|
actor_source_key TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
target_kind TEXT NOT NULL,
|
||||||
|
target_id TEXT,
|
||||||
|
outcome TEXT NOT NULL,
|
||||||
|
request_id TEXT,
|
||||||
|
summary TEXT
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_exists(conn: &Connection, table_name: &str) -> Result<bool> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?1)",
|
||||||
|
params![table_name],
|
||||||
|
|row| row.get::<_, bool>(0),
|
||||||
|
)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_exists(conn: &Connection, table_name: &str, column_name: &str) -> Result<bool> {
|
||||||
|
Ok(table_columns(conn, table_name)?
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == column_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_columns(conn: &Connection, table_name: &str) -> Result<Vec<String>> {
|
||||||
|
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table_name})"))?;
|
||||||
|
let rows = stmt.query_map([], |row| row.get::<_, String>(1))?;
|
||||||
|
rows.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn migrates_sqlite_and_preserves_workspace_record() {
|
async fn migrates_sqlite_and_preserves_workspace_record() {
|
||||||
|
|
@ -266,30 +488,355 @@ mod tests {
|
||||||
let db = dir.path().join("control-plane.sqlite");
|
let db = dir.path().join("control-plane.sqlite");
|
||||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
|
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 1);
|
assert_eq!(store.schema_version().await.unwrap(), 2);
|
||||||
|
|
||||||
let record = WorkspaceRecord {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
display_name: "Yoi Dev".to_string(),
|
display_name: "Yoi Dev".to_string(),
|
||||||
local_root: dir.path().to_path_buf(),
|
state: "active".to_string(),
|
||||||
record_authority: "local_yoi_project_records".to_string(),
|
|
||||||
created_at: "2026-01-01T00:00:00Z".to_string(),
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
updated_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
};
|
};
|
||||||
store.upsert_workspace(&record).await.unwrap();
|
store.upsert_workspace(&record).await.unwrap();
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
assert_eq!(reopened.schema_version().await.unwrap(), 1);
|
assert_eq!(reopened.schema_version().await.unwrap(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fresh_schema_matches_workspace_db_v0_boundaries() {
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
configure_sqlite(&conn).unwrap();
|
||||||
|
apply_migrations(&conn).unwrap();
|
||||||
|
|
||||||
|
let tables = table_names(&conn);
|
||||||
|
for expected in [
|
||||||
|
"workspaces",
|
||||||
|
"tickets",
|
||||||
|
"ticket_events",
|
||||||
|
"ticket_relations",
|
||||||
|
"objectives",
|
||||||
|
"objective_ticket_links",
|
||||||
|
"repositories",
|
||||||
|
"ticket_targets",
|
||||||
|
"ticket_target_paths",
|
||||||
|
"ticket_worker_links",
|
||||||
|
"artifacts",
|
||||||
|
"audit_events",
|
||||||
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
reopened
|
tables.contains(expected),
|
||||||
.list_runs("local-dev", 20)
|
"missing expected v0 table {expected}"
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.is_empty()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for forbidden in [
|
||||||
|
"runs",
|
||||||
|
"hosts",
|
||||||
|
"workers",
|
||||||
|
"actors",
|
||||||
|
"validation_results",
|
||||||
|
"ci_results",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!tables.contains(forbidden),
|
||||||
|
"fresh v0 schema must not create forbidden table {forbidden}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!tables.iter().any(|table| table.starts_with("legacy_")),
|
||||||
|
"fresh v0 schema should not create legacy compatibility tables: {tables:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_columns(
|
||||||
|
&conn,
|
||||||
|
"workspaces",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_columns(
|
||||||
|
&conn,
|
||||||
|
"repositories",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"repository_id",
|
||||||
|
"name",
|
||||||
|
"kind",
|
||||||
|
"provider",
|
||||||
|
"uri",
|
||||||
|
"default_ref",
|
||||||
|
"auth_ref_kind",
|
||||||
|
"auth_ref_key",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_columns(
|
||||||
|
&conn,
|
||||||
|
"ticket_events",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"event_id",
|
||||||
|
"ticket_id",
|
||||||
|
"event_seq",
|
||||||
|
"kind",
|
||||||
|
"activity_id",
|
||||||
|
"author_kind",
|
||||||
|
"author_key",
|
||||||
|
"author_display",
|
||||||
|
"author_source_kind",
|
||||||
|
"author_source_key",
|
||||||
|
"created_at",
|
||||||
|
"body_md",
|
||||||
|
"subject_kind",
|
||||||
|
"subject_id",
|
||||||
|
"previous_state",
|
||||||
|
"new_state",
|
||||||
|
"status",
|
||||||
|
"artifact_id",
|
||||||
|
"worker_ref_kind",
|
||||||
|
"worker_ref_key",
|
||||||
|
"worker_display",
|
||||||
|
"host_ref_kind",
|
||||||
|
"host_ref_key",
|
||||||
|
"host_display",
|
||||||
|
"repository_id",
|
||||||
|
"caused_by_event_id",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert_columns(
|
||||||
|
&conn,
|
||||||
|
"artifacts",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"artifact_id",
|
||||||
|
"kind",
|
||||||
|
"uri",
|
||||||
|
"media_type",
|
||||||
|
"sha256",
|
||||||
|
"size_bytes",
|
||||||
|
"summary",
|
||||||
|
"created_at",
|
||||||
|
"created_by_kind",
|
||||||
|
"created_by_key",
|
||||||
|
"created_by_display",
|
||||||
|
"created_by_source_kind",
|
||||||
|
"created_by_source_key",
|
||||||
|
"ticket_id",
|
||||||
|
"objective_id",
|
||||||
|
"event_id",
|
||||||
|
"worker_ref_kind",
|
||||||
|
"worker_ref_key",
|
||||||
|
"worker_display",
|
||||||
|
"repository_id",
|
||||||
|
"source_kind",
|
||||||
|
"source_revision",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
for table in ["workspaces", "repositories", "ticket_events", "artifacts"] {
|
||||||
|
let columns = table_columns(&conn, table).unwrap();
|
||||||
|
for forbidden_column in [
|
||||||
|
"payload",
|
||||||
|
"payload_json",
|
||||||
|
"metadata",
|
||||||
|
"metadata_json",
|
||||||
|
"diagnostics_json",
|
||||||
|
"run_id",
|
||||||
|
"local_root",
|
||||||
|
"record_authority",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!columns.iter().any(|column| column == forbidden_column),
|
||||||
|
"{table} must not contain obsolete/generic column {forbidden_column}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn upgrades_legacy_bootstrap_without_canonical_runs_table() {
|
||||||
|
let conn = Connection::open_in_memory().unwrap();
|
||||||
|
configure_sqlite(&conn).unwrap();
|
||||||
|
conn.execute_batch(LEGACY_BOOTSTRAP_SQL).unwrap();
|
||||||
|
conn.execute(
|
||||||
|
r#"INSERT INTO workspaces (
|
||||||
|
workspace_id, display_name, local_root, record_authority, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||||
|
params![
|
||||||
|
"legacy-workspace",
|
||||||
|
"Legacy Workspace",
|
||||||
|
"/tmp/legacy-workspace",
|
||||||
|
"local_yoi_project_records",
|
||||||
|
"2026-01-01T00:00:00Z",
|
||||||
|
"2026-01-02T00:00:00Z",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (1, 'bootstrap workspace control plane')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||||
|
assert_eq!(store.schema_version().await.unwrap(), 2);
|
||||||
|
|
||||||
|
store
|
||||||
|
.with_conn(|conn| {
|
||||||
|
let tables = table_names(conn);
|
||||||
|
for expected in [
|
||||||
|
"workspaces",
|
||||||
|
"repositories",
|
||||||
|
"tickets",
|
||||||
|
"ticket_events",
|
||||||
|
"ticket_worker_links",
|
||||||
|
"artifacts",
|
||||||
|
"audit_events",
|
||||||
|
"legacy_workspaces",
|
||||||
|
"legacy_repositories",
|
||||||
|
"legacy_runs",
|
||||||
|
"legacy_artifacts",
|
||||||
|
"legacy_ticket_projections",
|
||||||
|
"legacy_objective_projections",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
tables.contains(expected),
|
||||||
|
"missing {expected} after upgrade"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for forbidden in ["runs", "hosts", "workers", "actors", "validation_results"] {
|
||||||
|
assert!(
|
||||||
|
!tables.contains(forbidden),
|
||||||
|
"upgraded schema must not retain forbidden canonical table {forbidden}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_columns(
|
||||||
|
conn,
|
||||||
|
"workspaces",
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"display_name",
|
||||||
|
"state",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let legacy_workspace_columns = table_columns(conn, "legacy_workspaces")?;
|
||||||
|
assert!(
|
||||||
|
legacy_workspace_columns
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == "local_root")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_workspace_columns
|
||||||
|
.iter()
|
||||||
|
.any(|column| column == "record_authority")
|
||||||
|
);
|
||||||
|
let artifact_columns = table_columns(conn, "artifacts")?;
|
||||||
|
assert!(artifact_columns.iter().any(|column| column == "uri"));
|
||||||
|
assert!(!artifact_columns.iter().any(|column| column == "run_id"));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
store.get_workspace("legacy-workspace").await.unwrap(),
|
||||||
|
Some(WorkspaceRecord {
|
||||||
|
workspace_id: "legacy-workspace".to_string(),
|
||||||
|
display_name: "Legacy Workspace".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
created_at: "2026-01-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-01-02T00:00:00Z".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_record = WorkspaceRecord {
|
||||||
|
workspace_id: "new-workspace".to_string(),
|
||||||
|
display_name: "New Workspace".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
created_at: "2026-02-01T00:00:00Z".to_string(),
|
||||||
|
updated_at: "2026-02-01T00:00:00Z".to_string(),
|
||||||
|
};
|
||||||
|
store.upsert_workspace(&new_record).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store.get_workspace("new-workspace").await.unwrap(),
|
||||||
|
Some(new_record)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_names(conn: &Connection) -> BTreeSet<String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let rows = stmt.query_map([], |row| row.get::<_, String>(0)).unwrap();
|
||||||
|
rows.collect::<rusqlite::Result<BTreeSet<_>>>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_columns<const N: usize>(conn: &Connection, table: &str, expected: [&str; N]) {
|
||||||
|
let columns = table_columns(conn, table).unwrap();
|
||||||
|
let expected = expected.map(str::to_string).to_vec();
|
||||||
|
assert_eq!(columns, expected, "unexpected columns for {table}");
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGACY_BOOTSTRAP_SQL: &str = r#"
|
||||||
|
CREATE TABLE workspaces (
|
||||||
|
workspace_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
local_root TEXT NOT NULL,
|
||||||
|
record_authority TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE repositories (
|
||||||
|
repository_id TEXT PRIMARY KEY,
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
local_root TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE ticket_projections (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, ticket_id)
|
||||||
|
);
|
||||||
|
CREATE TABLE objective_projections (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
objective_id TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, objective_id)
|
||||||
|
);
|
||||||
|
CREATE TABLE runs (
|
||||||
|
run_id TEXT PRIMARY KEY,
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
subject_kind TEXT NOT NULL,
|
||||||
|
subject_id TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE artifacts (
|
||||||
|
artifact_id TEXT PRIMARY KEY,
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
run_id TEXT REFERENCES runs(run_id) ON DELETE SET NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
content_type TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"#;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user