/** * THE WORKING DOCUMENT AND STEERING FROM IT (program DES-POWER-MODE §8.3, §9; spike 3). * * - Each run may have ONE working document: a wicked-interactive document (kind `phase-report`) in the run's * project, bound by crew (`store.ts`). Versions are interactive's: write-once, parent-linked, each batch with * its `author`. A section is rewritten with one deterministic `content-edit`: no model, never a governed run. * - Attribution is crew's to decide. A person's edit (`author: 'operator'`) is accepted only from a HUMAN actor and * is recorded as theirs, with the plugin that hosted it named as the surface. A plugin's own generated lines * (`author: 'plugin'`) are recorded as `plugin:@` and may touch only the plugin's own section. * Nothing a plugin writes is ever recorded as a person's, and a plugin's lines never steer. * - STEERING. A person's version becomes ONE note for the run, sent through the operator's existing back-channel * (`injectWorkerMessage`, the `POST /runs/:id/inject` path): queued by the engine, it rides the next step's * prompt as an operator context block, and `workerMessageInjected` says which step picked it up. * Goal, Approach and Next go to the next step once; Constraints and steering is a STANDING steer, re-sent before * every later step until a later edit replaces it. Decisions and Open questions are recorded, not sent. * - DOCUMENT TEXT HAS NO POWER OVER GATES. This module calls `injectWorkerMessage` and nothing else on the engine: * no gate answer, no plan edit, no intent amendment, no rigor change. A note says in its first line that it * adds to the step's brief and changes no gate, check or review. (Tested: `tests/working-doc.test.ts`.) */ import type { CoreEvent } from '../core/types.js'; import { type SectionId } from './sections.js'; import type { SteerRecord, TrailEntry, WorkingDocStore } from './store.js'; /** How crew reads and writes interactive documents (the bridge in production, a fake in tests). */ export interface DocBackend { /** Create the document with its first version; marks it `phase-report` (the sidecar) before it is used. */ create(projectId: string, docId: string, runId: string, html: string): Promise<{ head: number; }>; /** The head version and ITS HTML (one version, never a torn pair), or null when the document is gone. */ read(projectId: string, docId: string): Promise<{ head: number; html: string; } | null>; /** One version's HTML, or null when there is no such version (yet). */ readVersion(projectId: string, docId: string, version: number): Promise; /** One feedback batch of content edits on `expectHead`; resolves when ITS version landed (checked by content), or * `headMoved` when the base was stale. Throws {@link UndeterminedWriteError} when the outcome is not known. */ edit(projectId: string, docId: string, batch: { expectHead: number; items: Array<{ selector: string; value: string; }>; author: string; }): Promise<{ version: number; parent: number; } | { headMoved: true; head: number; }>; } /** The ONLY engine call steering makes. */ export interface SteerEngine { injectWorkerMessage(runId: string, message: string, target: string): Promise; } export interface RunFacts { title: string; problem: string; projectId: string; /** `completed`, `cancelled`, `failed`: nothing can be steered any more. */ terminal: boolean; /** A unit's step id (its `phase_ref`), for "picked up at Fix". */ stepOf(ord: number): string | null; } export interface WorkingDocDeps { store: WorkingDocStore; backend: DocBackend; engine: SteerEngine; run(runId: string): Promise; /** A change worth telling studio about (`workingDocChanged` on `/ws`). */ changed?(runId: string, head: number | null): void; audit?(action: string, actor: { id: string; kind: string; }, runId: string, detail: Record): void; now?(): number; log?(m: string): void; } export interface WorkingDocView { runId: string; doc: { projectId: string; docId: string; head: number; } | null; sections: Record | null; labels: Record; trail: TrailEntry[]; steers: SteerRecord[]; } export type ProposeResult = { ok: true; version: number | null; unchanged: boolean; steer: SteerRecord | null; view: WorkingDocView; } | { ok: false; code: 400 | 403 | 404 | 409; error: string; head?: number; }; export interface ProposeInput { base: number; sections: Record; /** Who wrote it, as the HOST decided: the operator typing, or the plugin's own generated text. */ author: 'operator' | 'plugin'; /** The plugin that hosted the edit (`operator`) or wrote it (`plugin`). */ plugin?: { id: string; version: string; } | undefined; } /** A doc name interactive accepts (lowercase, digits, hyphens; ≤ 64): one per run. */ export declare function workingDocName(runId: string): string; /** The marker a note ends with, by which `workerMessageInjected` is matched back to its steer. */ export declare function steerMarker(runId: string, steerId: string): string; /** The one note a person's version becomes: the sections that steer once, and the standing constraint, together. */ export declare function steerNote(version: number, by: string, standing: boolean, texts: Array<{ id: SectionId; text: string; }>, marker: string): string; /** The standing constraint alone, re-sent before every later step. */ export declare function standingNote(version: number, by: string, text: string, marker: string): string; /** Every note's second line. */ export declare const NO_GATE_LINE = "It adds to your brief. It does not answer, skip or change any gate, check, review or QE step."; /** A write whose outcome is not known (no version in time, or a version that is not this batch's). */ export declare class UndeterminedWriteError extends Error { constructor(message: string); } export declare class WorkingDocService { private readonly d; private readonly now; /** The newest dispatch of each run (unit and attempt): the step a note injected now is picked up by. */ private readonly lastDispatch; /** A standing constraint with a copy queued and not yet picked up. */ private readonly queued; /** One write per run at a time (a second waits its turn); steering renewals take the same turn. */ private readonly locks; /** A standing steer is re-queued once per dispatch that picked it up (a live worker hears it once, not in a loop). */ private readonly renewedFor; constructor(d: WorkingDocDeps); private exclusive; view(runId: string): Promise; /** The run's working document, created on first ask (its Goal seeded from the run's intent, by crew). */ ensure(runId: string): Promise; /** * One version from the block. `actor` is the HTTP caller: only a person may write as the operator. */ propose(runId: string, input: ProposeInput, actor: { id: string; kind: string; }): Promise; private proposeNow; /** A version that landed: its trail entry, its steer (a person's only), its audit line and the change frame. */ private record; /** An edit whose outcome was unknown: once the head moved past it, the version is checked against what it wrote and * recorded as its author's if (and only if) it is that write. */ private reconcile; /** * A person's version → ONE steering note for the run (§9): Goal, Approach and Next for the next step, and Constraints * and steering as a standing constraint (an emptied Constraints is said once, as cleared). The record is stored * BEFORE the engine is asked, so a pickup reported at once is never lost. */ private steer; /** * The daemon's CoreEvent tap. * * - `unitDispatched` / `unitExecuting` (every run, also before its document exists): the dispatch a note injected now * is picked up by, as `ord` + `attempt` (a retry of the same unit is another dispatch). * - `workerMessageInjected`: the step whose prompt carried a note (recorded at once, its name read after). * - A STANDING constraint is kept one copy ahead: when a dispatch picks a copy up, the next copy is queued at once * (once per dispatch), so the next dispatch finds it waiting — never raced against `unitDone`. A live worker that * takes the copy in the same dispatch leaves none waiting; then the step's `unitDone` queues it. Every copy says it * replaces any earlier standing constraint, so a copy already queued when a person replaces it is overruled by the * newer one (the engine has no way to withdraw a queued note). */ onEvent(e: CoreEvent): void; /** Queue one copy of a standing constraint, inside the run's turn, if it is still the one in force. */ private renew; } //# sourceMappingURL=service.d.ts.map