Contract: P0.3.3 β Writeback Contract Enforcement

Task: P0.3.3
Branch: feature/p0-3-3-writeback-enforcement
Author: Claude (T3 Executor)
Date: 2026-04-17
Status: APPROVED (self-approved per Sigma-PM dispatch; no external reviewer gate)


1. Overview

This contract governs the new writeback.ts module and the minimal hook added to repository.ts. It defines the exact semantics of the three public exports (WritebackRequiredError, writebackRequired, enforceWriteback) and the integration contract with the β task transition path.


2. Scope

In scope:

  • src/domains/tasks/writeback.ts — new module (all three exports)
  • Minimal hook in src/domains/tasks/repository.ts:updateTask
  • src/__tests__/domains/tasks/writeback.test.ts — ≥10 tests

Out of scope:

  • FSM legality checking (P0.3.4)
  • MCP tool registration for writeback
  • Cross-task or cross-session chain verification (P0.7.3)
  • Any schema changes

3. WritebackRequiredError — error class contract

3.1 Declaration

export class WritebackRequiredError extends Error {
  override readonly name = 'WritebackRequiredError';
  readonly taskId: string;
  readonly missing_fields: string[];
  // ...
}

3.2 Invariants

  1. name is always the string literal 'WritebackRequiredError'.
  2. taskId is the task UUID passed to enforceWriteback.
  3. missing_fields is a non-empty string array. In Phase 0 the only missing field is 'thought_record', so missing_fields is always ['thought_record'].
  4. The instance is both instanceof WritebackRequiredError AND instanceof Error (prototype chain restored via Object.setPrototypeOf).
  5. message format: Writeback required for task <taskId>: missing <fields_csv> where <fields_csv> is missing_fields.join(', ').

3.3 Constructor

constructor(args: { taskId: string; missing_fields: string[] })

4. writebackRequired(db, taskId) — semantic contract

4.1 Signature

export function writebackRequired(
  db: Database.Database,
  taskId: string,
): boolean

4.2 Semantics

Returns true when the task exists AND is currently in state 'DONE' AND has zero thought_records for that taskId — i.e., the task is in a dirty done state (transitioned to done without writeback).

Returns false in ALL other cases:

  • The task is not in 'DONE' state (regardless of thought_record presence).
  • The task is in 'DONE' state and has ≥ 1 thought_record for the taskId.
  • The taskId does not exist.
  • The row is soft-deleted (soft-deleted rows have a status value too; the function checks the raw DB row via tasks table query, not the live-only getTask view).

4.3 Query shape

SELECT t.status, COUNT(tr.id) AS thought_count
FROM tasks t
LEFT JOIN thought_records tr ON tr.task_id = t.id
WHERE t.id = ?
GROUP BY t.id

Logic: thought_count === 0 && status === 'DONE' → return true.

4.4 Purpose

writebackRequired is a post-hoc audit predicate — it answers “has this task already been dirtied to done without a writeback?”. It is NOT the primary enforcement path (that is enforceWriteback). It exists for monitoring / repair tooling and is exported as a first-class function for testability.

4.5 Side effects

None. Read-only query.


5. enforceWriteback(db, taskId) — semantic contract

5.1 Signature

export function enforceWriteback(
  db: Database.Database,
  taskId: string,
): void

5.2 Semantics

Checks whether the task identified by taskId has at least one thought_record in the ζ trail. If yes, returns void (enforcement satisfied). If no, throws WritebackRequiredError.

5.3 Query shape

SELECT COUNT(*) AS cnt FROM thought_records WHERE task_id = ?

Logic: cnt === 0 → throw WritebackRequiredError({ taskId, missing_fields: ['thought_record'] }).

5.4 Calling convention

enforceWriteback is called by updateTask ONLY when patch.status === 'DONE', BEFORE the SQL UPDATE is executed. This means:

  • If writeback is absent, the task status is NOT changed. The exception propagates and the caller handles it.
  • If writeback is present, execution continues and the UPDATE fires normally.

5.5 Task existence

enforceWriteback does NOT verify that the task exists. It only checks thought_records. If the task does not exist, cnt === 0 and WritebackRequiredError is thrown. This ordering is intentional: the subsequent stmt.run (SQL update) will return changes === 0 and produce TaskNotFoundError only if the writeback check is somehow bypassed. In practice, WritebackRequiredError fires first for non-existent tasks with status 'DONE' patches — see §8 for rationale.

5.6 Idempotency

Calling enforceWriteback twice on a satisfied task returns void both times — it is a pure check with no side effects.

5.7 Side effects

None when writeback is satisfied (read-only). Throws when not satisfied.


6. Integration contract — updateTask hook

6.1 Location

Inside src/domains/tasks/repository.ts, function updateTask, BEFORE the stmt.run(bindings) call.

6.2 Code

// P0.3.3 writeback enforcement: block status→DONE without a thought_record.
if ((patch as Record<string, unknown>)['status'] === 'DONE') {
  enforceWriteback(db, id);
}

6.3 Precondition

The check fires only when patch.status === 'DONE'. For all other status values (or patches with no status field), the check is skipped entirely.

6.4 Import

import { enforceWriteback } from './writeback.js';

Added at the top of repository.ts alongside existing imports.

6.5 No other changes to repository.ts

createTask, getTask, deleteTask, listTasks are not modified. The enforcement is strictly opt-in per transition target.


7. Test acceptance criteria

All of the following must pass:

# Test Expected outcome
T1 updateTask to DONE with no thought_record Throws WritebackRequiredError; task status unchanged
T2 updateTask to DONE with ≥1 thought_record Succeeds; task status = 'DONE'
T3 updateTask to a non-DONE state with no thought_record Succeeds (no enforcement)
T4 updateTask to a non-DONE state with thought_record Succeeds
T5 writebackRequired returns false for non-DONE task regardless of thought_records false in all non-DONE cases
T6 writebackRequired returns true for DONE task with 0 thought_records true
T7 writebackRequired returns false for DONE task with ≥1 thought_records false
T8 writebackRequired returns false for unknown taskId false
T9 Direct enforceWriteback returns void when thought_record present void
T10 Direct enforceWriteback throws WritebackRequiredError when absent Throws; taskId and missing_fields correct
T11 enforceWriteback twice on satisfied task returns void both times Idempotent
T12 WritebackRequiredError error shape: name, taskId, missing_fields, instanceof Correct shape

8. Error ordering rationale

When a caller does updateTask(db, 'non-existent-id', { status: 'DONE' }):

  1. enforceWriteback fires first → finds 0 thought_records → throws WritebackRequiredError.
  2. TaskNotFoundError is never reached.

This is acceptable because:

  • The primary consumer (P0.3.4 MCP tools) validates task existence before calling updateTask and status via FSM before allowing DONE.
  • A caller who bypasses those guards and passes a non-existent task ID deserves some error. WritebackRequiredError is conservative (it correctly says “no proof of work”).
  • The alternative (task-existence check first) would add a second DB query to updateTask for every call, not just DONE transitions.

This ordering is documented here and in the verify doc so callers have clear expectations.


9. Non-goals

  • FSM legality enforcement (still deferred to P0.3.4 MCP tools).
  • Writeback enforcement on CANCELLED transitions.
  • Writeback enforcement on createTask with status: 'DONE'createTask is not modified (an impossible edge case in practice; FSM start is INIT).
  • Multiple missing_fields values — Phase 0 only requires thought_record.
  • MCP tool registration (P0.3.4 responsibility).

10. Module purity

writeback.ts follows the established pure-module convention:

  • Accepts db: Database.Database as an explicit parameter.
  • No module-level state, no module-level env reads.
  • No MCP tool registration.
  • No getDb() calls.
  • No console.* output.

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.