summaryrefslogtreecommitdiff
path: root/makima/frontend/src/lib/api.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-08 11:29:56 +0100
committerGitHub <noreply@github.com>2026-05-08 11:29:56 +0100
commite00be74c8b575c725829677aadeb755ee81454d0 (patch)
tree6804ba099978b32f94e3436bb373c2b0bd07b84d /makima/frontend/src/lib/api.ts
parentd7048aaef8ffa483c63a765d2d35ae01389e331f (diff)
downloadsoryu-e00be74c8b575c725829677aadeb755ee81454d0.tar.gz
soryu-e00be74c8b575c725829677aadeb755ee81454d0.zip
feat(directives): unified contracts surface — backbone (#128)
This is the backbone PR for the unified directive workflow. A directive holds a sequence of contracts; each contract is a spec body whose execution drives tasks in the directive's shared worktree. Lifecycle (Lock & Start, queue scheduler, drag-reorder) lands in follow-ups. What's in this PR: - Migration adds `position` (queue order) and `merge_mode` (shared|own_pr) columns to directive_documents. The actual table rename is deferred — the legacy `contracts` table from the old contracts system still exists, and the rename collision waits for Phase 5 to drop legacy contracts. - Repository: list orders by position; create assigns next-position; update accepts merge_mode; new reorder_directive_document_position shifts siblings inside a transaction. - HTTP: endpoints aliased under /api/v1/directives/{id}/contracts and /api/v1/contracts/{id}/... with a new /contracts/{id}/reorder. - Frontend: api types renamed `DirectiveContract*` (avoiding the legacy `Contract` type collision); document-directives.tsx imports via aliases so the rest of the file is untouched. Internal struct + table names stay `DirectiveDocument` / `directive_documents` until the legacy contracts cleanup. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/lib/api.ts')
-rw-r--r--makima/frontend/src/lib/api.ts127
1 files changed, 76 insertions, 51 deletions
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 80da511..17cf1d0 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3583,116 +3583,141 @@ export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersRes
}
// =============================================================================
-// Directive Documents API
+// Directive Contracts API
//
-// A directive_document is one of N markdown documents owned by a directive.
-// Each has its own lifecycle (draft → active → shipped → archived) and may be
-// attached to a PR. The frontend never calls the /ship endpoint — the backend
-// invokes that itself when PRs are raised. Editing a shipped document
-// auto-reactivates it on the backend (see PATCH handler).
+// A "directive contract" (DB table: directive_documents) is one of N spec
+// documents owned by a directive. Each has its own lifecycle
+// (draft → active → shipped → archived) and may be attached to a PR.
+// Contracts run sequentially in the directive's shared worktree (or, when
+// mergeMode = 'own_pr', on a contract-specific branch).
+//
+// Naming note: types are `DirectiveContract*` (rather than `Contract*`)
+// because the legacy contracts system still defines a `Contract` type at
+// the top of this file. Once Phase 5 drops legacy contracts, we'll rename
+// to plain `Contract`. URLs and the user-facing UI already say "contract".
// =============================================================================
-export type DirectiveDocumentStatus = "draft" | "active" | "shipped" | "archived";
+export type DirectiveContractStatus = "draft" | "active" | "shipped" | "archived";
+
+/** How a contract's commits land. `shared` (default): on the directive's
+ * branch — multiple contracts feed one PR. `own_pr`: a contract-specific
+ * branch + PR carved out at activation time. */
+export type DirectiveContractMergeMode = "shared" | "own_pr";
-export interface DirectiveDocument {
+export interface DirectiveContract {
id: string;
directiveId: string;
title: string;
body: string;
- status: DirectiveDocumentStatus;
+ status: DirectiveContractStatus;
prUrl: string | null;
prBranch: string | null;
shippedAt: string | null;
archivedAt: string | null;
version: number;
+ /** 0-indexed queue position within the parent directive. */
+ position: number;
+ mergeMode: DirectiveContractMergeMode;
createdAt: string;
updatedAt: string;
}
-export interface CreateDirectiveDocumentRequest {
+export interface CreateDirectiveContractRequest {
title?: string;
body?: string;
}
-export interface UpdateDirectiveDocumentRequest {
+export interface UpdateDirectiveContractRequest {
title?: string;
body?: string;
+ mergeMode?: DirectiveContractMergeMode;
}
-export async function listDirectiveDocuments(directiveId: string): Promise<DirectiveDocument[]> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/documents`);
- if (!res.ok) throw new Error(`Failed to list directive documents: ${res.statusText}`);
+export async function listDirectiveContracts(
+ directiveId: string,
+): Promise<DirectiveContract[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/contracts`);
+ if (!res.ok) throw new Error(`Failed to list contracts: ${res.statusText}`);
return res.json();
}
-export async function createDirectiveDocument(
+export async function createDirectiveContract(
directiveId: string,
- req: CreateDirectiveDocumentRequest = {},
-): Promise<DirectiveDocument> {
- const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/documents`, {
+ req: CreateDirectiveContractRequest = {},
+): Promise<DirectiveContract> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/contracts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
- if (!res.ok) throw new Error(`Failed to create directive document: ${res.statusText}`);
+ if (!res.ok) throw new Error(`Failed to create contract: ${res.statusText}`);
return res.json();
}
-export async function getDirectiveDocument(documentId: string): Promise<DirectiveDocument> {
- const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}`);
- if (!res.ok) throw new Error(`Failed to get directive document: ${res.statusText}`);
+export async function getDirectiveContract(contractId: string): Promise<DirectiveContract> {
+ const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}`);
+ if (!res.ok) throw new Error(`Failed to get contract: ${res.statusText}`);
return res.json();
}
/**
- * Update a directive document's title and/or body. The backend auto-reactivates
- * a shipped document when its body changes (re-stamps status to `active`),
- * which is why we don't expose a separate "reactivate" call.
+ * Update a contract's title, body, and/or merge mode. Backend auto-
+ * reactivates a shipped contract when its body changes (status flips
+ * back to `active`), so there's no separate reactivate call.
*/
-export async function updateDirectiveDocument(
- documentId: string,
- req: UpdateDirectiveDocumentRequest,
-): Promise<DirectiveDocument> {
- const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}`, {
+export async function updateDirectiveContract(
+ contractId: string,
+ req: UpdateDirectiveContractRequest,
+): Promise<DirectiveContract> {
+ const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(req),
});
- if (!res.ok) throw new Error(`Failed to update directive document: ${res.statusText}`);
+ if (!res.ok) throw new Error(`Failed to update contract: ${res.statusText}`);
+ return res.json();
+}
+
+export async function archiveDirectiveContract(contractId: string): Promise<DirectiveContract> {
+ const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/archive`, {
+ method: "POST",
+ });
+ if (!res.ok) throw new Error(`Failed to archive contract: ${res.statusText}`);
return res.json();
}
-export async function archiveDirectiveDocument(documentId: string): Promise<DirectiveDocument> {
- const res = await authFetch(`${API_BASE}/api/v1/directive-documents/${documentId}/archive`, {
+/** Move a contract to a new 0-indexed queue position inside its directive. */
+export async function reorderDirectiveContract(
+ contractId: string,
+ position: number,
+): Promise<DirectiveContract> {
+ const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/reorder`, {
method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ position }),
});
- if (!res.ok) throw new Error(`Failed to archive directive document: ${res.statusText}`);
+ if (!res.ok) throw new Error(`Failed to reorder contract: ${res.statusText}`);
return res.json();
}
-/** Steps and tasks attached to a single directive document. Drives the
- * per-document `tasks/` subfolder in the sidebar — when the document
- * ships, its tasks visually move with it under shipped/. */
-export interface DocumentTasksResponse {
+/** Steps and tasks attached to a single contract. Drives the per-contract
+ * `tasks/` subfolder in the sidebar — when the contract ships, its
+ * tasks visually move with it. */
+export interface DirectiveContractTasksResponse {
steps: DirectiveStep[];
tasks: Task[];
}
/**
- * List the steps and ephemeral tasks attached to a specific directive
- * document. Used by the sidebar to render a `tasks/` subfolder beside each
- * document — including shipped documents, whose tasks remain attached so
- * they continue to render under shipped/ alongside the document.
+ * List the steps and ephemeral tasks attached to a specific contract.
+ * Used by the sidebar to render a `tasks/` subfolder beside each
+ * contract — including shipped ones, whose tasks remain attached.
*/
-export async function listDirectiveDocumentTasks(
- documentId: string,
-): Promise<DocumentTasksResponse> {
- const res = await authFetch(
- `${API_BASE}/api/v1/directive-documents/${documentId}/tasks`,
- );
- if (!res.ok) {
- throw new Error(`Failed to list directive document tasks: ${res.statusText}`);
- }
+export async function listDirectiveContractTasks(
+ contractId: string,
+): Promise<DirectiveContractTasksResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contracts/${contractId}/tasks`);
+ if (!res.ok) throw new Error(`Failed to list contract tasks: ${res.statusText}`);
return res.json();
}