|
| 1 | +import type { JsonObject } from "../effect_program.ts"; |
| 2 | +import type { AuthorityStore } from "./authority_store.ts"; |
| 3 | +import { canonicalAuthorityBytes, canonicalAuthorityObject, canonicalAuthoritySha256, requireAuthorityStoreId } from "./authority_store_codec.ts"; |
| 4 | +import { prepareCoordinationProjectionCommit, indexCoordinationProjection, validateCoordinationTodoReadModel } from "./coordination_projection.ts"; |
| 5 | + |
| 6 | +export const TODO_COMPATIBILITY_EDIT_SCHEMA = "loopx_todo_compatibility_edit_request_v0"; |
| 7 | +export const TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA = "loopx_todo_compatibility_edit_result_v0"; |
| 8 | + |
| 9 | +/** |
| 10 | + * A compatibility editor proposes only text/note changes against an exact |
| 11 | + * provider revision. It cannot submit a snapshot, change ownership/lifecycle, |
| 12 | + * clear unrepresented fields, or make Markdown the commit authority. |
| 13 | + * |
| 14 | + * This trusted embedded entrypoint does not grant remote service authority. |
| 15 | + * Conflict requires rereading and rerunning the editor, never snapshot rebasing. |
| 16 | + */ |
| 17 | +export async function editCoordinationTodo( |
| 18 | + store: AuthorityStore, |
| 19 | + value: unknown, |
| 20 | +): Promise<JsonObject> { |
| 21 | + const failure = (reason_code: string, reason: string): JsonObject => ({ |
| 22 | + schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, |
| 23 | + status: "failed", changed: false, reason_code, reason, |
| 24 | + }); |
| 25 | + try { |
| 26 | + const input = canonicalAuthorityObject(value, "Todo compatibility edit"); |
| 27 | + if (input.schema_version !== TODO_COMPATIBILITY_EDIT_SCHEMA) { |
| 28 | + throw new Error("Todo compatibility edit schema mismatch"); |
| 29 | + } |
| 30 | + const allowed = new Set(["schema_version", "goal_id", "todo_id", "operation_id", |
| 31 | + "expected_provider_revision", "actor_agent_id", "registered_agents", "patch", "dry_run", "observed_at"]); |
| 32 | + if (Object.keys(input).some((key) => !allowed.has(key))) { |
| 33 | + throw new Error("Todo compatibility edit contains unsupported fields"); |
| 34 | + } |
| 35 | + const goalId = requireAuthorityStoreId(input.goal_id, "goal id"); |
| 36 | + const todoId = requireAuthorityStoreId(input.todo_id, "todo id"); |
| 37 | + const operationId = requireAuthorityStoreId(input.operation_id, "operation id"); |
| 38 | + const revision = requireAuthorityStoreId(input.expected_provider_revision, "expected provider revision"); |
| 39 | + const actor = requireAuthorityStoreId(input.actor_agent_id, "actor agent id"); |
| 40 | + if (typeof input.observed_at !== "string" || |
| 41 | + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(input.observed_at) || |
| 42 | + Number.isNaN(Date.parse(input.observed_at))) { |
| 43 | + throw new Error("observed_at must be an ISO timestamp"); |
| 44 | + } |
| 45 | + const updatedAt = new Date(input.observed_at).toISOString(); |
| 46 | + if (!Array.isArray(input.registered_agents) || |
| 47 | + input.registered_agents.some((agent) => typeof agent !== "string") || |
| 48 | + new Set(input.registered_agents).size !== input.registered_agents.length) { |
| 49 | + throw new Error("registered_agents must be a unique string array"); |
| 50 | + } |
| 51 | + if (typeof input.dry_run !== "boolean") throw new Error("dry_run must be a boolean"); |
| 52 | + const patch = canonicalAuthorityObject(input.patch, "compatibility patch"); |
| 53 | + if (Object.keys(patch).length === 0 || Object.entries(patch).some(([key, item]) => |
| 54 | + !["text", "note"].includes(key) || typeof item !== "string" || !item.trim() |
| 55 | + )) { |
| 56 | + throw new Error("compatibility patch accepts only non-empty text and note strings"); |
| 57 | + } |
| 58 | + const requestSha = canonicalAuthoritySha256({ |
| 59 | + goal_id: goalId, todo_id: todoId, actor_agent_id: actor, |
| 60 | + expected_provider_revision: revision, patch, |
| 61 | + }); |
| 62 | + const readReceipt = async (status: string): Promise<JsonObject | null> => { |
| 63 | + const receipt = await store.readReceipt(operationId); |
| 64 | + if (receipt.status === "missing") return null; |
| 65 | + if (receipt.status !== "found") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...receipt, changed: false}; |
| 66 | + const original = receipt.receipts[0]; |
| 67 | + if (receipt.receipts.length !== 1 || |
| 68 | + original?.schema_version !== "loopx_todo_compatibility_edit_receipt_v0" || |
| 69 | + original.operation_id !== operationId || original.request_sha256 !== requestSha || |
| 70 | + typeof original.changed !== "boolean") { |
| 71 | + return failure("coordination_operation_identity_mismatch", "operation id names a different compatibility edit"); |
| 72 | + } |
| 73 | + return { |
| 74 | + schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, |
| 75 | + status: status === "applied" && !original.changed ? "no_change" : status, |
| 76 | + changed: status !== "replayed" && original.changed, |
| 77 | + provider_revision: receipt.provider_revision, cursor: receipt.cursor, |
| 78 | + projection_delivery: original.changed ? "pending" : "not_required", |
| 79 | + projection_source: "committed_authority_journal", |
| 80 | + }; |
| 81 | + }; |
| 82 | + // Preview evaluates current eligibility and never consumes/replays identity. |
| 83 | + if (!input.dry_run) { |
| 84 | + const replay = await readReceipt("replayed"); |
| 85 | + if (replay !== null) return replay; |
| 86 | + } |
| 87 | + if (!input.registered_agents.includes(actor)) { |
| 88 | + return failure("actor_not_registered", "compatibility edit requires a registered actor"); |
| 89 | + } |
| 90 | + const head = await store.loadAuthority(); |
| 91 | + if (head.status !== "loaded") return {schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...head, changed: false}; |
| 92 | + validateCoordinationTodoReadModel(head.head, goalId); |
| 93 | + const index = indexCoordinationProjection(head.head, goalId); |
| 94 | + if (head.provider_revision !== revision) return { |
| 95 | + schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, |
| 96 | + status: "conflict", changed: false, conflict_kind: "provider_revision_mismatch", |
| 97 | + current_provider_revision: head.provider_revision, current_cursor: head.cursor, |
| 98 | + }; |
| 99 | + const todo = index.todos.get(todoId); |
| 100 | + if (todo === undefined) return failure("todo_not_found", "canonical Todo is missing"); |
| 101 | + if (todo.role !== "agent" || todo.status !== "open" || todo.archive_state !== "active") { |
| 102 | + return failure("unsupported_compatibility_edit_target", "compatibility editing requires an active open agent Todo"); |
| 103 | + } |
| 104 | + if (todo.claimed_by !== actor || |
| 105 | + (Array.isArray(todo.excluded_agents) && todo.excluded_agents.includes(actor)) || |
| 106 | + (typeof todo.removed_continuation_policy === "string" && todo.removed_continuation_policy)) { |
| 107 | + return failure("compatibility_edit_owner_mismatch", "compatibility editing requires the current non-excluded claim owner"); |
| 108 | + } |
| 109 | + // Lease-bearing edits need a separate execution-instance proof; do not |
| 110 | + // infer that proof merely from actor identity or bypass the existing fence. |
| 111 | + if (![undefined, "legacy", "soft_claim"].includes(head.head.handoff_mode as string | undefined) || index.leases.has(todoId)) { |
| 112 | + return failure("compatibility_edit_lease_unsupported", "lease-bearing compatibility edits are not yet supported"); |
| 113 | + } |
| 114 | + const next = {...todo, ...patch}; |
| 115 | + const changed = !canonicalAuthorityBytes(next).equals(canonicalAuthorityBytes(todo)); |
| 116 | + if (input.dry_run) return { |
| 117 | + schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, status: changed ? "planned" : "no_change", |
| 118 | + changed, provider_revision: revision, cursor: head.cursor, |
| 119 | + }; |
| 120 | + if (changed) next.updated_at = updatedAt; |
| 121 | + const commit = changed ? prepareCoordinationProjectionCommit({ |
| 122 | + goal_id: goalId, operation_id: operationId, |
| 123 | + expected_provider_revision: revision, |
| 124 | + projection: head.head, |
| 125 | + mutations: [{kind: "todo_upsert", todo: next}], |
| 126 | + }) : {operation_id: operationId, expected_provider_revision: revision, |
| 127 | + next_projection: head.head, events: [], receipts: []}; |
| 128 | + commit.receipts = [{schema_version: "loopx_todo_compatibility_edit_receipt_v0", |
| 129 | + operation_id: operationId, request_sha256: requestSha, changed}]; |
| 130 | + const result = await store.commitAuthority(commit); |
| 131 | + const readback = await readReceipt(result.status === "applied" ? "applied" : "recovered"); |
| 132 | + if (readback !== null) return readback; |
| 133 | + if (result.status === "applied") return failure("compatibility_commit_readback_mismatch", "applied edit lacks its durable receipt"); |
| 134 | + return { |
| 135 | + schema_version: TODO_COMPATIBILITY_EDIT_RESULT_SCHEMA, ...result, |
| 136 | + changed: false, |
| 137 | + }; |
| 138 | + } catch (error) { |
| 139 | + return failure("invalid_todo_compatibility_edit", error instanceof Error ? error.message : "invalid compatibility edit"); |
| 140 | + } |
| 141 | +} |
0 commit comments