db

package
v0.0.0-...-69654ad Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ActionVerdictPass      = "pass"
	ActionVerdictFail      = "fail"
	ActionVerdictUnmatched = "unmatched"
)

Action verdicts as stored, mirroring the gate vocabulary exactly. `unmatched` is a FIRST-CLASS outcome: the action name matched no trust entry, so nothing ran — which is neither a pass nor an error (§6.2 A3).

View Source
const (
	TrustKindGate   = "gate"
	TrustKindAction = "action"
)

Trust-cache kinds (§6.3). `trust_cache` gains a `kind` column rather than a second table: the table answers "what did this run consider trusted, and when", and splitting that answer by the SHAPE OF THE CALLER would make the one question two queries.

View Source
const (
	// DispatchOpen is the state `idx_dispatches_one_open` admits one of per run.
	DispatchOpen = "open"
	// DispatchClosed is `dispatch close`: the relay reconciled its batch.
	DispatchClosed = "closed"
	// DispatchAbandoned is `dispatch abandon` or the TTL's lazy auto-abandon:
	// the batch was given up on rather than reconciled.
	DispatchAbandoned = "abandoned"
)

Dispatch statuses. A dispatch is `open` until exactly one of the two closing paths moves it, and the status is what the partial unique index keys on — so these strings are load-bearing rather than decorative.

View Source
const (
	// CloseReasonTTL is P14's lazy auto-abandon: the manifest outlived
	// `dispatch.ttl` and `next` retired it.
	CloseReasonTTL = "ttl"
	// CloseReasonReconciled is the ordinary `dispatch close` (P18).
	CloseReasonReconciled = "reconciled"
	// CloseReasonAcceptedMissingUsage is P19: closed over missing-usage
	// discrepancies, with the acceptance RECORDED rather than implied.
	CloseReasonAcceptedMissingUsage = "accepted-missing-usage"
	// CloseReasonOperator is `dispatch abandon` driven by a person (P21).
	CloseReasonOperator = "abandoned"
)

Close reasons core writes. They are a small closed vocabulary because `run report` and `events list` render them, and a free-text reason from the engine's own paths would make the feed inconsistent with itself. A caller's `--reason` on `abandon` rides in the EVENT's data, not here (P21).

View Source
const (
	// AckByGuardSpawn is `guard spawn --ack-reap`, group 3's entry point.
	AckByGuardSpawn = "guard-spawn"
	// AckByDispatchOpen is `dispatch open --ack-reap`, this group's.
	AckByDispatchOpen = "dispatch-open"
)

Acknowledgers — A8's closed pair. The value records the acknowledging VERB and never a user identity, because core has no identity model.

View Source
const (

	// KeyLeaseTTLDefault is the fallback lease TTL for any class without its
	// own entry.
	KeyLeaseTTLDefault = "lease.ttl.default"
	// KeyLeaseTTLPrefix is the per-class TTL namespace: lease.ttl.<class>.
	// The class is an opaque string — core never interprets its value
	// (engine-spec.md §11.1 [limits]).
	KeyLeaseTTLPrefix = "lease.ttl."
	// KeyAttemptMax caps retries per entity (engine-spec.md §11.1
	// max_attempts).
	KeyAttemptMax = "attempt.max"
	// KeyBudgetDefault is the per-run budget cap; 0 means unlimited.
	// Enforcement lands with runs; the key exists now so the default is
	// pinned before the verbs that read it.
	KeyBudgetDefault = "budget.default"
	// KeyBudgetUnit names WHICH recorded usage unit the run cap counts
	// (docs/tdd/runs-dispatch.md §4.5 B16). Empty — the default and the only
	// value core ships — means `reported` is 0 and the cap rests on the
	// declared-cost floor alone.
	//
	// A config key rather than a workflow field because the cap is a run-level
	// control (engine-spec §11.3), so the unit it counts is run-level too.
	// Putting it on a step would let two steps in one run disagree about what
	// the run's budget means.
	KeyBudgetUnit = "budget.unit"
	// KeyUsageBudgetDefault is the default cap over MEASURED usage — the
	// second budget dimension (DKT-238); 0 means unlimited.
	//
	// It is a SEPARATE key from budget.default because the two count different
	// things and are not commensurable. `budget.default` counts declared
	// expected costs, which is a discipline over how much WORK a run schedules;
	// this counts what the ledger recorded, which is a bound on what the work
	// actually consumed. A run wants both, and one number cannot say both.
	KeyUsageBudgetDefault = "budget.usage.default"
	// KeyUsageBudgetUnit names WHICH recorded usage unit the measured cap
	// counts. Empty — the default — leaves the dimension DORMANT: a cap with
	// no unit has nothing to count, so it enforces nothing.
	//
	// Deliberately distinct from budget.unit. That key names the unit whose
	// reported total may RAISE the declared-cost spend (B16's max); this one
	// names the unit the measured cap is taken over. Sharing one key would
	// make an operator setting a token unit for one dimension silently arm the
	// other.
	KeyUsageBudgetUnit = "budget.usage.unit"
	// KeyDispatchTTL is how long a dispatch manifest stays open before `next`
	// auto-abandons it (docs/tdd/runs-dispatch.md §4.11, §5.5 P12).
	//
	// It is engine-side rather than instance policy for the reason §11.3 gives
	// for every other enforced number: the expiry is what keeps a crashed relay
	// from wedging a run, and a bound only a live relay could set would be a
	// bound the crash case never gets.
	KeyDispatchTTL = "dispatch.ttl"
	// KeyDispatchGrace is how long a claimed step may go unrecorded before it
	// counts as a dispatch discrepancy (§5.8 D1).
	//
	// It is DELIBERATELY a different key from `lease.ttl.default` even though
	// both measure silence. A lease TTL decides when the engine takes work back;
	// this decides when a relay's batch is judged unreconciled. Sharing one key
	// would make an operator lengthening leases silently stop `next` refusing.
	KeyDispatchGrace = "dispatch.grace"

	// KeyContextWarnBytes and KeyContextErrorBytes are the context-size caps.
	KeyContextWarnBytes  = "context.warn_bytes"
	KeyContextErrorBytes = "context.error_bytes"

	// KeyEventsRetain is the RETENTION WINDOW: how long an event must have
	// existed before `events prune` may delete it
	// (docs/tdd/events-follow.md §5.3 P12).
	//
	// It is engine-spec §3's "artifact-retention boundary", implemented as the
	// one window a future artifact GC would also read. §3 lists prune and
	// "artifact GC per run-retention config" as one lifecycle, and no artifact
	// GC ships at stage 7 — so read literally there would be no boundary to
	// cross. Implementing it as this window means the GC inherits a boundary
	// already enforced rather than one retrofitted, and the reading can only
	// ever REFUSE MORE than the literal one would.
	//
	// THE READING IS RECORDED AS AN AMENDMENT rather than made silently,
	// including the objection a reviewer would raise — that a key spelled
	// `events.retain` governing only events is not what "artifact-retention"
	// names.
	//
	// The default is "0", which means RETAIN EVERYTHING: prune refuses every
	// event until an operator states a policy. That is the dormant posture —
	// Docket deletes nothing an operator did not ask it to delete.
	KeyEventsRetain = "events.retain"

	// KeyVoteRulePrefix is the named-threshold-configuration namespace:
	// vote.rule.<name>.threshold and vote.rule.<name>.criticality
	// (gates-trust §8.3).
	//
	// A workflow's `type="vote"` step names a rule rather than passing flags,
	// because a step cannot pass flags. The <name> is an OPAQUE string exactly
	// as lease.ttl.<class>'s class is, and this reuses the config machinery
	// rather than adding a table: a rule "exists" iff its `.threshold` is set.
	KeyVoteRulePrefix = "vote.rule."
	// KeyVoteRuleThresholdSuffix and KeyVoteRuleCriticalitySuffix complete a
	// rule's two keys.
	KeyVoteRuleThresholdSuffix   = ".threshold"
	KeyVoteRuleCriticalitySuffix = ".criticality"

	// KeyVoteHoldRule and KeyVoteHoldVoters configure how a MATERIALIZED HELD
	// step is decided: by one operator (the default) or by a tally.
	//
	// A held step is the one step in a run no author declared — the engine
	// mints it when a `hold_spread` trips, so its `voters` and `vote_rule`
	// cannot come from a `[[step]]` table the way a declared vote step's do.
	// These two keys are where an instance says them instead, and they sit in
	// the `vote.` family beside `vote.rule.<name>` because that is the same
	// subject: how a vote is tallied and who casts it.
	//
	// BOTH ARE EMPTY BY DEFAULT, and empty means EXACTLY the prior behavior —
	// held steps are minted `human` and one operator approves or rejects them.
	// A tally is something an instance opts into, never something core assumes,
	// for the same reason core ships no default threshold: a roster nobody
	// chose is not a roster.
	//
	// They are ONE PAIR rather than per-workflow settings because a hold is the
	// engine's own question about its own computation. It is asked identically
	// whichever pipeline held, so who answers it is a project-level policy.
	KeyVoteHoldRule   = "vote.hold.rule"
	KeyVoteHoldVoters = "vote.hold.voters"

	// KeyAutoRegister toggles §9's auto-registration: whether `run activate`
	// registers a workflow/schema it finds in an instance-config root
	// (~/.docket/config, <repo>/.docket/config) on its own, or leaves that to
	// an explicit `workflow register` / `schema register`.
	//
	// DEFAULT TRUE — auto-registration is the zero-touch behavior §9 exists
	// for, and an operator who wants it off states that, rather than every
	// operator who wants it (the common case) opting in project by project.
	//
	// Project-scoped, like any other config key (v12): `--global` sets the
	// store-wide default every project without its own override falls back
	// to, and a bare `config set` overrides ONE project — the two knobs the
	// requirement asks for ("a given project vs all projects") are exactly
	// GetConfig's existing project-override-then-store-wide resolution, not a
	// second mechanism.
	//
	// It gates ONLY registration, not the pinning half of the same scan
	// (contracts/, fragments/, policy.toml). Pinning has no version to adopt —
	// it is "read the current bytes", which is what a repo with this off
	// still needs to render a step's `packet`. Turning registration off and
	// pinning off together would leave a project unable to activate at all
	// without hand-supplying every `--pin` the corpus already offers, which is
	// not what "I don't want silent version upgrades" is asking for.
	KeyAutoRegister = "registration.auto"
)

Engine configuration keys (engine-spec.md §1: "engine defaults: lease TTLs per class, attempt caps, budget default, context caps"). Values live in the meta table, prefixed so they cannot collide with schema_version or any other internal key.

View Source
const (
	GateVerdictPass      = "pass"
	GateVerdictFail      = "fail"
	GateVerdictUnmatched = "unmatched"
	// GateVerdictSkipped: the gate's tree was gone at spawn time, so nothing
	// ran and nothing was measured (DKT-169). Not a pass for routing.
	GateVerdictSkipped = "skipped"
)

Gate verdicts as stored. `unmatched` is a FIRST-CLASS outcome, not an error and not a pass: the command was not trusted, so it did not run (§6.2 N1-N4).

View Source
const (
	ScopeIssueCreate  = "issue.create"
	ScopeDocCreate    = "doc.create"
	ScopeVoteCreate   = "vote.create"
	ScopeIssueComment = "issue.comment.add"
	ScopeDocComment   = "doc.comment.add"
	ScopeRunStart     = "run.start"
)

Idempotency scopes. One per create verb, so the same key used on two different verbs does not collide.

View Source
const (
	PinKindWorkflow = "workflow"
	PinKindFile     = "file"
	// PinKindSchema pins a registered payload schema by its source hash
	// (docs/tdd/payloads-thresholds.md §4.7 P1). A schema is a registered
	// object, and §2's pinning clause is "registered objects by version" —
	// therefore it pins. What a run validates its payloads against is a fact
	// about the run, not about the table's current contents.
	PinKindSchema = "schema"
)

Pin kinds (TDD §5.1). `workflow` pins a registered `name@version` by its source hash; `file` pins an arbitrary operator-supplied path by its content hash — "how the reference instance pins its contracts, fragments, and policy without core knowing what they are" (engine-spec §2). Core reads bytes, hashes them, stores the path, and never opens the content again.

View Source
const (
	StepPending      = "pending"
	StepClaimed      = "claimed"
	StepRunning      = "running"
	StepGated        = "gated"
	StepDone         = "done"
	StepWaitingHuman = "waiting-human"
	StepSkipped      = "skipped"
	StepSuperseded   = "superseded"
	StepFailedRouted = "failed-routed"

	// StepReady is the COMPUTED status a read verb renders when the §6.3
	// predicate holds. It is a rendering value, never a column value, and
	// TestReadyIsNeverPersisted asserts nothing writes it.
	StepReady = "ready"

	// StepStaged is the second COMPUTED status, rendered only on offer rows
	// (`next`, `dispatch open`): the step is NOT ready — its `after`
	// predecessors have not all recorded — but every unsatisfied predecessor
	// is itself in the same offer at a lower stage, so a dispatcher that runs
	// the offer's stages in order will find it claimable by the time its stage
	// begins. Like `ready` it is a rendering value, never a column value, and
	// never an answer `step show` gives (stagedness is a property of one
	// offer's membership, not of the step). A `staged` row is NOT claimable
	// yet: `claim` re-checks the predicate and refuses until the predecessors
	// actually record, which is what keeps a stage-skipping dispatcher safe.
	StepStaged = "staged"
)

The nine PERSISTED step statuses (TDD §6.2). `ready` is the tenth member of the MACHINE and is deliberately absent here: it is computed at read time by the §6.3 predicate and never stored as intent, exactly as v6's effective lease status is. Both numbers are stated in the TDD because the enum's size and the machine's size are different questions.

View Source
const (
	// SagaRecorded is stage 1's commit: the artifact is in, the token is
	// retired, status is `gated`. From here the saga is engine-owned and needs
	// no lease.
	SagaRecorded = "recorded"
	// SagaRouting is the last stage's resume point: gates are all recorded and
	// routing is what remains.
	SagaRouting = "routing"
	// SagaHeld is the stage an `aggregate` step enters when `hold_spread` trips
	// (payloads-thresholds §7.7 H8). Its artifact is recorded and its
	// `<step>-held` question is open; its routing is DEFERRED until an operator
	// resolves that question.
	//
	// It is a stage rather than a status because "gating the routing step" is a
	// DEFERRAL OF ROUTING, and the machinery for deferring a saga already
	// exists. The step's status stays `gated` — non-terminal — so every
	// downstream successor fails R3 and nothing proceeds: no new status, no
	// synthetic `after` edge, no second readiness rule.
	SagaHeld = "held"
	// SagaGatePrefix prefixes a per-gate resume point, `gate:<name>`.
	SagaGatePrefix = "gate:"
)

Saga stages (TDD §6.8). The value stored in `steps.saga_stage` is the RESUME POINT: the stage that has committed, so the next engine invocation knows which one to run next. NULL means "not in the saga" — either never entered or complete.

View Source
const ArtifactMaxBytes = 1 << 20

ArtifactMaxBytes is §3's explicit cap. An artifact over it is a VALIDATION_ERROR naming the size and the cap — a refusal rather than a truncation, because a silently truncated artifact is one a downstream step consumes as if it were whole.

View Source
const DefaultProjectID = 1

DefaultProjectID is the row every pre-v12 datum backfilled to, and the row an invocation with no resolvable identity falls back to.

View Source
const EngineAuthor = "docket-engine"

EngineAuthor is the fixed Author value for comments the engine writes itself (step claimed, gate/vote opened, step failed, issue completed or abandoned), so they read as machine-authored without a schema column to mark them.

View Source
const IssueResolutionAbandoned = model.ResolutionAbandoned

IssueResolutionAbandoned is the resolution the `abandon-issue` routing and `run abandon --issue` write: the machine stopped working this issue and did not finish it. It is model.ResolutionAbandoned rather than a second literal, so the value the engine writes and the value the renderer tests for cannot drift apart.

View Source
const NameMaxBytes = 64

NameMaxBytes caps an opaque name stored in config or recorded in a ledger, per §1.3's security note: such a name is attacker-controlled text that lands in a ledger and in a report. ONE number for every opaque-name validator, so two of them cannot drift apart.

View Source
const UnitNameMaxBytes = NameMaxBytes

UnitNameMaxBytes is NameMaxBytes under the name `--usage`'s validator has always called it.

View Source
const UnregisteredProjectID = -1

UnregisteredProjectID is the id an invocation resolves to when its identity has NO project row and this invocation is not allowed to create one (DKT-58: a read verb, or an identity that is not a repository).

It is negative so it references nothing: every scoped SELECT returns empty and every scoped INSERT fails its foreign key rather than silently landing in someone else's project. That is the honest answer to "show me this directory's issues" when the directory has no project — the pre-DKT-58 code answered it by MINTING one, and a read that creates permanent state is how the store filled with rows nobody asked for.

It is deliberately NOT DefaultProjectID: falling back to project 1 would show a legacy store's issues under an unrelated repository and, worse, let a write from an unregistered directory land in that project's history.

View Source
const UsageSourceReported = "reported"

UsageSourceReported is the only source core writes at completion: a claimant said so. A back-fill supplies its own (engine.UsageSourceBackfilled).

View Source
const UsageUnitsMax = 32

UsageUnitsMax is B36's first cap: at most 32 units per report.

The caps exist because `--usage` is ATTACKER-CONTROLLED JSON FROM A CLAIMANT (§1.3) that lands in a ledger and in a report — bytes going to a terminal. A claimant that could write ten thousand units would make every subsequent `run report` on that run unreadable, which is a denial of the verb an operator reaches for when something has gone wrong.

View Source
const VoteMetadataMaxBytes = 16 << 10

VoteMetadataMaxBytes caps the encoded size of a vote's opaque KV bag, in the shape ArtifactMaxBytes uses: the limit lives beside the column it protects, so every writer of `votes.metadata` crosses it — `vote cast`, and `import` through InsertVoteWithID — rather than only the one command that happens to parse a flag. The value matches the 16 KiB a step's own metadata bag gets; it is duplicated rather than imported because internal/engine already imports this package.

Variables

View Source
var (
	// ErrLeaseHeld means a live lease is held by someone else — the claim
	// race's loser. Surfaced as CONFLICT (exit 4).
	ErrLeaseHeld = errors.New("lease held")

	// ErrNotHolder means the caller presented no matching capability: either
	// the entity is unclaimed, or the token is wrong. Surfaced as AUTH_ERROR
	// (exit 5).
	//
	// The two cases are deliberately not distinguished. "Unclaimed" and "wrong
	// token" are the same answer to the caller — you do not hold this lease —
	// and separating them would leak whether a lease exists to a caller
	// holding no capability.
	ErrNotHolder = errors.New("not the lease holder")

	// ErrLeaseExpired means the token is RIGHT but the lease lapsed. Surfaced
	// as STALE_LEASE (exit 6), which is the entire value of a separate code: a
	// holder seeing it knows to re-claim (its work may be redone), while
	// AUTH_ERROR means it never held the lease at all.
	ErrLeaseExpired = errors.New("lease expired")
)

Lease refusal sentinels. Each maps to exactly one CLI error code, per the refusal matrix in docs/tdd/claims-leases.md §4 (engine-spec.md §9 item 3).

View Source
var (
	ErrSelfRelation      = errors.New("self-referential relation")
	ErrDuplicateRelation = errors.New("duplicate relation")
	ErrCycleDetected     = errors.New("cycle detected")
)

Sentinel errors for relation operations.

View Source
var (
	// ErrSchemaConflict means `name@version` is already registered with
	// DIFFERENT bytes. Surfaced as CONFLICT (exit 4), naming both hashes.
	//
	// Why a conflict and not an overwrite: engine-core §4's pinning property —
	// "editing a pipeline never changes an in-flight run" — is worth nothing if
	// the pinned bytes can be swapped underneath the run. A schema decides
	// whether a worker's payload is ACCEPTED, so a mutable findings@1 means a
	// run's acceptance criteria change mid-flight. Bump the version.
	ErrSchemaConflict = errors.New("schema already registered with different content")

	// ErrSchemaNotFound means no row matches the requested name/version.
	// Surfaced as NOT_FOUND (exit 2).
	ErrSchemaNotFound = errors.New("schema not found")
)

Schema registration sentinels. They mirror the workflow ones exactly, because the two registries have the same immutability contract (TDD §4.4) and a caller mapping errors to exit codes should not have to learn it twice.

View Source
var (
	// ErrStepNotFound means no `steps` row matches. NOT_FOUND (exit 2).
	ErrStepNotFound = errors.New("step not found")

	// ErrSagaStageMoved means a saga stage's CAS guard matched zero rows:
	// another engine invocation advanced the saga first (TDD §6.8, "resume is
	// lazy and idempotent"). It is not a failure — the loser re-reads and
	// either finds the work done or advances the next stage — so no CLI verb
	// maps it to an exit code; the saga driver handles it internally.
	ErrSagaStageMoved = errors.New("saga stage already advanced")
)

Step sentinels.

View Source
var (
	// ErrWorkflowConflict means `name@version` is already registered with
	// DIFFERENT bytes. Surfaced as CONFLICT (exit 4), naming both hashes.
	//
	// A registered name@version is frozen: re-registering identical bytes is
	// an idempotent success, and re-registering different bytes is this. The
	// version pinning engine-core §4 requires ("editing a pipeline never
	// changes an in-flight run") is worth nothing if the pinned bytes can be
	// swapped underneath a run.
	ErrWorkflowConflict = errors.New("workflow already registered with different content")

	// ErrWorkflowNotFound means no row matches the requested name/version.
	// Surfaced as NOT_FOUND (exit 2).
	ErrWorkflowNotFound = errors.New("workflow not found")
)

Workflow registration sentinels.

View Source
var ErrConflict = errors.New("conflict")

ErrConflict is returned when an operation violates a uniqueness or state constraint.

View Source
var ErrDispatchAlreadyOpen = errors.New("a dispatch is already open for this run")

ErrDispatchAlreadyOpen is C1's loser — the CONFLICT of P6.

View Source
var ErrIssueHasParent = errors.New("issue has a parent")

ErrIssueHasParent refuses a project migration of a non-root issue: sub-issues follow their root, so the root is what migrates (or the issue is reparented first).

View Source
var ErrIssueInRun = errors.New("issue belongs to a run")

ErrIssueInRun refuses a project migration of an issue any run holds: the run's snapshots, steps, and events are project-scoped bookkeeping, and an issue migrated from under them would strand every record.

View Source
var ErrLabelColorConflict = errors.New("label color conflict")

ErrLabelColorConflict is returned when --color specifies a different color than an existing label already has.

View Source
var ErrNoOpenDispatch = errors.New("no dispatch is open")

ErrNoOpenDispatch is returned when a verb that needs an open manifest finds none. It is a sentinel so the CLI maps it once rather than matching a message.

View Source
var ErrNotAttached = errors.New("label not attached")

ErrNotAttached is returned when a label is not attached to the specified issue.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned when a requested resource does not exist.

View Source
var ErrPrefixTaken = errors.New("prefix already held by another project")

ErrPrefixTaken refuses a prefix another project already holds (DKT-60). The prefix is a project's only discriminator in a listing or an event feed, so two projects sharing one makes every id in the store ambiguous about its owner.

View Source
var ErrProjectInUse = errors.New("project still has rows")

ErrProjectInUse refuses the deletion of a project anything still references (DKT-59).

View Source
var ErrProjectIsDefault = errors.New("the default project cannot be deleted")

ErrProjectIsDefault refuses the deletion of the default row.

View Source
var (
	// ErrRunNotFound means no `runs` row matches. NOT_FOUND (exit 2).
	ErrRunNotFound = errors.New("run not found")
)

Run sentinels.

View Source
var ErrUnknownConfigKey = errors.New("unknown config key")

ErrUnknownConfigKey is returned for a key outside the known set. A typo'd key must not silently store a value nothing reads.

View Source
var ErrUsageAlreadyRecorded = errors.New("usage already recorded for this step, attempt, and unit")

ErrUsageAlreadyRecorded is the (step_id, attempt, unit) key firing — this unit was already recorded for this attempt.

It is a SENTINEL rather than a raw constraint error so a caller can phrase the refusal for whoever hit it: the same violation means "the claimant already reported this" at completion and "you are back-filling twice" at back-fill, and the two want different sentences.

View Source
var ErrValidation = errors.New("validation")

ErrValidation is returned when an input fails a validation precondition that the DB layer enforces (e.g., negative revision number). CLI surfaces map this to output.ErrValidation. See TDD docket-doc-cli §6.4.

View Source
var ErrVersionConflict = errors.New("version conflict")

ErrVersionConflict is returned when a CAS-guarded mutation is attempted against a row whose version differs from the caller's expectation — someone else wrote to it in between. Callers surface this as CONFLICT (exit 4), distinct from ErrNotFound (exit 2).

View Source
var ErrWorkflowAlreadyDeprecated = errors.New("workflow version is already deprecated")

ErrWorkflowAlreadyDeprecated means the version is already retired. Surfaced as CONFLICT (exit 4): retiring twice is not a silent success, because the second caller's mental model ("I am the one taking this out of service") is wrong and the timestamp they would expect to see is not the one stored.

Functions

func AddLabelToIssue

func AddLabelToIssue(db *sql.DB, issueID int, labelName, color string, author string) error

AddLabelToIssue attaches a label to an issue within a transaction. The label is created if it does not already exist (with the given color). Activity is recorded and the issue's updated_at timestamp is touched.

func AddLabelsToIssue

func AddLabelsToIssue(db *sql.DB, issueID int, labelNames []string, color string, author string) error

AddLabelsToIssue attaches multiple labels to an issue atomically within a single transaction. Labels are created if they do not already exist (with the given color). Activity is recorded for each newly attached label and the issue's updated_at timestamp is touched once.

func AddRunIssue

func AddRunIssue(db *sql.DB, runID, issueID int) error

AddRunIssue attaches an issue to a run before activation. The binding, snapshots, and expansion timestamp are all NULL until activation fills them — a run in `planning` carries its issue list and nothing else.

func AdvanceSagaTx

func AdvanceSagaTx(tx *sql.Tx, id int, from, to string, nowMS int64) error

AdvanceSagaTx moves a step's saga stage under a CAS guard on the CURRENT stage — §6.8's "each stage's transaction is WHERE saga_stage = <expected> CAS-guarded, so two concurrent engine invocations resuming the same saga produce exactly one advance".

The loser matches zero rows and gets ErrSagaStageMoved, which is not an error condition: it re-reads and either finds the saga finished or advances the stage that is now current. This is the whole of the concurrent-resume story, and it is one WHERE clause because a saga guarded by a read-then-write would have the window this exists to close.

`from` is "" for the NULL stage (entering the saga) and `to` is "" to leave it (the saga completing).

func AllProjectPrefixes

func AllProjectPrefixes(conn *sql.DB) ([]string, error)

AllProjectPrefixes reads every prefix registered in the store — the root hook's roster for model.SetKnownProjectPrefixes (DKT-110), so id parsing accepts exactly the prefixes some project actually holds and refuses the rest (`ANSI-16` resolving issue 16 was the defect).

func AttachFiles

func AttachFiles(db *sql.DB, issueID int, filePaths []string, changedBy string) error

AttachFiles inserts rows into issue_files for each file path. Duplicate attachments are silently ignored (INSERT OR IGNORE). Activity is recorded for each batch of newly attached files.

func AuthorizeLeaseMutation

func AuthorizeLeaseMutation(tx *sql.Tx, id int, token string, nowMS int64) error

AuthorizeLeaseMutation verifies that a caller may mutate an issue whose lease may or may not be live, and ends the lease when the caller is the holder.

This is the guard for terminal verbs (`issue close`), which end a lease as a side effect the way a step's token retires when its artifact records (engine-spec.md §2).

The dormancy rule is here, in the first branch: an issue with no LIVE lease is outside the mechanism entirely. No token is required, nothing is refused, and behavior is exactly what it was at v5. The token check fires only when a live lease exists — which is what makes engine-spec.md §9 item 8 hold for a repo that never claims.

func AuthorizeStepRead

func AuthorizeStepRead(db *sql.DB, id int, token string, nowMS int64) error

AuthorizeStepRead is AuthorizeStepTx's predicate over a READ, for the one caller that must refuse a non-holder before opening a write transaction: stage 0's payload validation (payloads-thresholds §4.8 C6). It shares the predicate rather than restating it, so the two cannot disagree, and it is ADVISORY — AuthorizeStepTx remains the authority.

func AuthorizeStepTx

func AuthorizeStepTx(tx *sql.Tx, id int, token string, nowMS int64) (*model.Lease, error)

AuthorizeStepTx is §6.8 stage 0's holder check and §6.9's R1-R4 in one call: the token must hold a LIVE lease on the step. It is leaseSteps.authorize, exported under a name the saga reads naturally.

func AutoRegisterEnabledTx

func AutoRegisterEnabledTx(tx *sql.Tx, projectID int) (bool, error)

AutoRegisterEnabledTx resolves KeyAutoRegister inside a CALLER'S transaction — activation's own, since that is the only place this reads (§9's scan runs inside activateTx, and a pool read there would deadlock against the one-connection pool exactly as VoteRuleExistsTx's doc explains).

The parse cannot fail on a value that reached storage: SetConfig already ran it through ValidateConfigValue's KindBool case, which is the same strconv.ParseBool this calls.

func BindRunIssueTx

func BindRunIssueTx(tx *sql.Tx, ri *RunIssue) error

BindRunIssueTx records an issue's binding and its activation-time snapshots — stages 1 and 4 of the fat transaction, written together because they are one fact: this issue, bound to this workflow, as it read at this moment.

func BreachRunBudgetTx

func BreachRunBudgetTx(tx *sql.Tx, runID int, reason string, nowMS int64) (bool, error)

BreachRunBudgetTx is B20 and B22: the run flips `active -> waiting-human` with its reason, CAS-guarded on the status it is moving FROM.

The CAS is the whole mechanism (C6). Two invocations that both observe the cap crossed both call this; exactly one matches a row, and the loser writes neither a second reason nor — because the caller keys the event on this return — a second event. The guard is the STATUS ITSELF rather than a flag we maintain, so there is no second piece of state that could disagree with it.

`reason` is written to BOTH `reason` and `breach_reason`. `reason` is the run machine's general "why is it parked" field that `run status` already renders, and `breach_reason` is the budget's own, so a later `pause` for an unrelated cause cannot overwrite the record of the breach.

`pause_origin` is written in the SAME statement (DKT-305): this park is a RUN-LEVEL decision — it parks no step — and the reconciliation rollup reads that column to know it must not auto-resume. Before the column existed the rollup read `breach_reason` for the same purpose, which worked only because a breach is the one run-level park that leaves a second trace; an operator's `run pause` leaves none, and was silently undone.

func BudgetDefaultTx

func BudgetDefaultTx(tx *sql.Tx, projectID int) (float64, error)

BudgetDefaultTx is B1's second branch: the config cap, read in the deciding transaction. 0 means unlimited (B2).

func BudgetUnitTx

func BudgetUnitTx(tx *sql.Tx, projectID int) (string, error)

BudgetUnitTx is B16's resolution: WHICH recorded usage unit the run cap counts.

Empty — the default and the only value core ships — means `reported` is 0 and the enforcement rests entirely on the floor (B17). That is the honest default and it is exactly §9 item 7's configuration: with reporting disabled, the run still pauses at the cap from the floor.

func CacheRunFloorTx

func CacheRunFloorTx(tx *sql.Tx, runID int, floor float64) error

CacheRunFloorTx writes `runs.usage_floor`.

IT IS A CACHE FOR THE REPORT AND NOTHING ELSE (§4.3, §3.2). No enforcement path reads it: the floor that decides is a SUM over claim events, computed inside the deciding transaction, because a stored running total is a read-modify-write and a read-modify-write is the one shape C4 has no defense against. TestFloorIsNeverReadFromCache poisons this column and asserts every decision behaves as though it said the truth.

It does NOT bump `row_version`: the cache is a derived number, not a state change an operator's CAS should collide with. A claim that bumped the run's version for a number nobody asserted against would make `--if-version` on runs unusable during any active run.

func CascadeDeleteIssue

func CascadeDeleteIssue(db *sql.DB, id int) error

CascadeDeleteIssue deletes an issue and all its descendants recursively in a single transaction. The recursive CTE finds all descendant issues; ON DELETE CASCADE constraints on comments, issue_labels, issue_relations, and activity_log handle cleanup of related rows automatically.

func CheckAndBumpVersion

func CheckAndBumpVersion(tx *sql.Tx, table string, id int, ifVersion *int) error

CheckAndBumpVersion enforces an --if-version precondition inside tx and increments the row's version.

ifVersion nil means "no precondition": the version is still bumped, so every mutation advances it and concurrent CAS writers are detected.

The zero-rows case is deliberately re-probed rather than assumed: a missing row is ErrNotFound (exit 2) while a present row at a different version is ErrVersionConflict (exit 4). Collapsing the two would report a live conflict as a missing entity.

func ClaimIssue

func ClaimIssue(db *sql.DB, id int, owner string, ttlMS int64, nowMS int64) (token string, lease *model.Lease, err error)

ClaimIssue takes a lease on an issue and returns the minted capability token. It is one CAS transaction: exactly one of N concurrent claimants wins, and the losers get ErrLeaseHeld (engine-core.md §5).

Expiry is reaped lazily, here and only here (engine-spec.md §6: "lazy lease reaping confined to next/claim; reads never write"). The `expires_ms <= now` disjunct IS the reaping — there is no reaper, no background pass, and no write from any read path. An expired lease is therefore re-claimable with no operator action beyond the claim itself, which is the liveness mechanism engine-spec.md §9 item 4 requires.

attempt increments on every winning claim, including the one that replaces a holder that died mid-work. That is the complete attempt trail: it counts claims for all time and is never decremented or reset.

func ClaimStep

func ClaimStep(db *sql.DB, id int, owner string, ttlMS, nowMS int64) (string, *model.Lease, error)

ClaimStep is the standalone claim, for tests and for any caller that needs only the lease. `step claim` uses ClaimStepTx.

func ClaimStepTx

func ClaimStepTx(
	tx *sql.Tx, id int, owner string, ttlMS, nowMS int64,
) (token string, lease *model.Lease, err error)

ClaimStepTx takes a lease on a step inside the caller's transaction, through the SAME generalized implementation issues use (TDD §6.6).

It is transaction-scoped rather than standalone because `step claim` must mint the token and assemble the §11.4 context bundle in ONE transaction — "an unclaimed executor has nothing, a claimed one has everything" (engine-core §8) — so the caller owns the transaction and this is one statement inside it.

func ClearAllData

func ClearAllData(db *sql.DB) error

ClearAllData deletes all data from every persistent table within a single transaction. The schema and meta table are preserved.

Tables are deleted in FK-correct order — children before parents. FK CASCADE would handle dependents implicitly, but the explicit ordering keeps behaviour identical to the pre-v4 function and makes the contract auditable from the function body.

Doc tables and the pre-existing proposals/votes/proposal_issues tables are included; prior to v4 the latter three were silently omitted, which broke `--replace` import on any DB containing proposals (TDD §5.4 S4 / R7).

func ClearAllDataTx

func ClearAllDataTx(tx *sql.Tx) error

func ClearProjectDataTx

func ClearProjectDataTx(tx *sql.Tx, projectID int) error

ClearProjectDataTx is ClearAllDataTx scoped to ONE project (v12): the same tracker tables, children before parents, but only the rows reachable from this project's roots. Under the shared store, "replace everything" scoped any wider would delete projects the operator was not looking at.

func ClearRunBreachTx

func ClearRunBreachTx(tx *sql.Tx, runID int, newReason string, nowMS int64) error

ClearRunBreachTx clears `breach_reason` once a cap change has resolved the breach (DKT-80): a row still asserting "budget: spend N of cap M reached" after the cap moved past N misleads every reader that trusts it. When `newReason` is non-empty the run machine's general `reason` is rewritten too — the caller passes it only when the row's reason IS the breach reason, so an unrelated pause's reason is never overwritten.

`row_version` is not bumped here: every caller runs inside a transaction that already bumped it for the cap write itself.

func ClearStaleBudgetReasonTx

func ClearStaleBudgetReasonTx(tx *sql.Tx, runID int, newReason string, nowMS int64) error

ClearStaleBudgetReasonTx retires a run's `reason` when it still carries a PREVIOUS raise's "cap changed from X to Y" sentence (DKT-80's own text, written by `ClearRunBreachTx`'s `newReason` argument) and no breach is currently standing. `newReason` replaces it; an empty `newReason` blanks the field instead, for a run with no other reason to state.

`ClearRunBreachTx` only rewrites `reason` on the raise that RESOLVES a standing breach — right for `breach_reason` itself, since there is nothing left to clear a second time, but the decorative sentence that raise wrote survives every later, unrelated raise untouched, naming a cap that has since moved again (DKT-47). This is the caller's own leftover text retired, never an operator's pause reason: SetRunBudget only reaches for it once it has confirmed `reason` carries the exact prefix this package writes, and it supplies `newReason` itself only for a run still parked on the sentence (waiting-human) — blanking that one would leave a parked run with no stated reason at all.

func ClearStepStartTx

func ClearStepStartTx(tx *sql.Tx, id int) error

ClearStepStartTx clears the schedule-to-close clock, so a step returned to the unclaimed pool starts a fresh budget on its next claim. Reaping and explicit failure both go through it.

func CloseDispatchTx

func CloseDispatchTx(tx *sql.Tx, id int, status, reason string, nowMS int64) (bool, error)

CloseDispatchTx is C2: close and abandon are both CAS on (id, status='open').

It reports whether it MOVED the row. A close racing the TTL abandon matches zero rows and learns so, which is what lets the caller report `CONFLICT` naming what actually happened rather than "not open" (P22) — the loser needs to know WHY, and the row it then reads says.

func CloseOpenProposalsTx

func CloseOpenProposalsTx(tx *sql.Tx, ids []int, reason string) (int, error)

CloseOpenProposalsTx closes every OPEN proposal among `ids`, inside a caller's transaction, and reports how many it closed (DKT-262).

It exists because closing a stranded proposal is not an operator act — it is the tail of a transition that already happened. `run abandon` ends the run that opened them; the ack of a reap answers the question its proposal asked. Those transitions are transactional, so the close has to be able to ride inside them: a close committed separately can be lost while the transition stands, which puts the row back in the state this exists to prevent.

ONLY `open` ROWS MOVE, exactly as CloseProposal insists. Every other status is the record of a decision, and a bulk close that rewrote one would be the overwrite the immutable-record rule forbids — which matters more here than in the single-id case, because a caller passing a set has not looked at each one.

`reason` lands in `final_outcome`, so the row itself says how the question ended. It should name the TRANSITION, not the verdict: these proposals were never decided, and a reason that read like a decision would be a worse lie than the stale `open` was.

func CloseProposal

func CloseProposal(db *sql.DB, id int, reason string) error

CloseProposal retires an OPEN proposal without a tally (DKT-114).

The case it exists for: a gate's underlying decision was made another way — an operator authorized the guarded action directly — and the proposal the panel would have decided has no votes and no future. Before this verb such a proposal sat `open` forever, misreporting a settled question as a pending one.

Only `open` closes. Every other status is the record of a decision, and a close that rewrote one would be exactly the overwrite the immutable-record rule forbids. The reason lands in `final_outcome`, so the row itself says how the question ended.

func CommitProposal

func CommitProposal(db *sql.DB, id int, outcome string, escalationReason string) error

CommitProposal transitions an approved proposal to committed status with a final outcome. If escalationReason is non-empty, it is stored on the proposal.

func CountActivity

func CountActivity(db *sql.DB, issueID int) (int, error)

CountActivity returns the total number of activity log entries for an issue, ignoring any limit. Callers pair it with GetActivity to report an honest pre-limit total and flag truncation.

func CountByPriority

func CountByPriority(db *sql.DB, projectID int) (map[string]int, error)

CountByPriority returns a map of priority -> count, scoped to a project when projectID is non-zero.

func CountByStatus

func CountByStatus(db *sql.DB, projectID int) (map[string]int, error)

CountByStatus returns a map of status -> count, scoped to a project when projectID is non-zero.

func CountIssues

func CountIssues(db *sql.DB, projectID int) (int, error)

CountIssues returns the number of issues, scoped to a project when projectID is non-zero.

func CountRootIssues

func CountRootIssues(db *sql.DB, projectID int) (int, error)

CountRootIssues returns the number of issues with no parent, scoped to a project when projectID is non-zero.

func CountSteps

func CountSteps(db *sql.DB, runID int) (int, error)

CountSteps returns a run's step count, for `run status` without paging the whole table.

func CreateComment

func CreateComment(db *sql.DB, comment *model.Comment) (int, error)

CreateComment inserts a new comment for an issue, records activity, and returns its ID. The insert and activity log are wrapped in a single transaction so they succeed or fail together.

func CreateCommentIdempotent

func CreateCommentIdempotent(db *sql.DB, comment *model.Comment, idempotencyKey string) (int, error)

CreateCommentIdempotent is CreateComment with an optional idempotency key. A repeat call with the same key returns the original comment id and inserts nothing; the key is recorded in the same transaction as the insert.

func CreateDoc

func CreateDoc(db *sql.DB, doc *model.Doc) (int, error)

CreateDoc inserts a new doc and appends revision #1 with change_kind="create" in a single transaction. Returns the new doc ID. The supplied doc must have Type, Status, Title, Body, and Author set; CreatedAt/UpdatedAt are stamped by this function.

func CreateDocComment

func CreateDocComment(db *sql.DB, c *model.DocComment) (int, error)

CreateDocComment inserts a comment on a doc and returns its ID. The doc existence check and insert run in a single transaction. Returns ErrNotFound if the doc does not exist.

func CreateDocCommentIdempotent

func CreateDocCommentIdempotent(db *sql.DB, c *model.DocComment, idempotencyKey string) (int, error)

CreateDocCommentIdempotent is CreateDocComment with an optional idempotency key. A repeat call with the same key returns the original comment id and inserts nothing; the key is recorded in the same transaction as the insert.

func CreateDocIdempotent

func CreateDocIdempotent(db *sql.DB, doc *model.Doc, idempotencyKey string) (int, error)

CreateDocIdempotent is CreateDoc with an optional idempotency key. A repeat call with the same key returns the original doc id and inserts nothing; the key is recorded in the same transaction as the insert.

func CreateIssue

func CreateIssue(db *sql.DB, issue *model.Issue, labels []string, files []string) (int, error)

CreateIssue inserts a new issue and returns its ID. Labels are created (find-or-create) and linked to the issue within the same transaction. Files are attached to the issue if provided.

func CreateIssueIdempotent

func CreateIssueIdempotent(db *sql.DB, issue *model.Issue, labels []string, files []string, idempotencyKey string) (int, error)

CreateIssueIdempotent is CreateIssue with an optional idempotency key.

When idempotencyKey is non-empty and was already used for this scope, the original issue's id is returned and nothing is inserted — a retried create after a dropped response must succeed, not fail. The key record and the insert commit in the SAME transaction, so a crash between them cannot orphan either.

func CreateProposal

func CreateProposal(db *sql.DB, p *model.Proposal) (int, error)

CreateProposal inserts a new proposal and returns its ID.

func CreateProposalIdempotent

func CreateProposalIdempotent(db *sql.DB, p *model.Proposal, idempotencyKey string) (int, error)

CreateProposalIdempotent is CreateProposal with an optional idempotency key. A repeat call with the same key returns the original proposal id and inserts nothing. Unlike the plain path this runs in a transaction, so the insert and the key record commit together.

func CreateRelation

func CreateRelation(db *sql.DB, rel *model.Relation) (int, error)

CreateRelation inserts a new relation between two issues within a single transaction. It validates that both issues exist, rejects self-referential and duplicate relations, runs cycle detection for blocks/depends_on types, and records activity on both issues.

func DefaultProjectIDOr

func DefaultProjectIDOr(id int) int

DefaultProjectIDOr is projectOrDefault for callers outside this package that render their own SQL and need the same zero-means-default rule.

func DeleteDoc

func DeleteDoc(db *sql.DB, id int, cascade bool) error

DeleteDoc removes the doc with the given ID. When cascade is true, FK cascades drop doc_revisions, doc_comments, doc_issue_links, proposal_docs. When cascade is false, the call returns ErrConflict if any links (issue or proposal) exist for the doc — comments and revisions are part of the doc's own history and never block deletion. Returns ErrNotFound if no doc with that ID exists.

func DeleteIssue

func DeleteIssue(db *sql.DB, id int) error

DeleteIssue removes an issue by ID. Foreign key cascades handle cleanup of related rows (comments, labels, activity, relations).

func DeleteLabel

func DeleteLabel(db *sql.DB, labelID int, name, author string) ([]int, error)

DeleteLabel removes a label by ID. CASCADE constraints handle cleanup of issue_labels rows. Activity is recorded for each affected issue using the provided name. Returns the list of issue IDs that were attached to the label.

func DeleteProject

func DeleteProject(conn *sql.DB, id int) error

DeleteProject removes an EMPTY project row (DKT-59).

It exists because the auto-registration defect (DKT-58) minted permanent rows nobody asked for, and `docket project` had no verb that could take one back out: the operator's only remedy was a raw sqlite DELETE against a store shared by every repository on the machine.

It REFUSES a project any row references, and refuses the default row outright. That is what makes it safe to expose: the verb can remove junk and cannot remove history, so there is no version of "I meant the other project" that costs anything. Re-homing real rows is `issue move --project`'s job, and a project emptied that way becomes deletable by this verb afterwards.

func DeleteRelation

func DeleteRelation(db *sql.DB, sourceID, targetID int, relType string) error

DeleteRelation removes a relation matching the given source, target, and type. Activity is recorded on both issues within a single transaction.

func DeprecateWorkflow

func DeprecateWorkflow(db *sql.DB, projectID int, name string, version int, nowMS int64) (*model.Workflow, error)

DeprecateWorkflow retires ONE registered version from binding.

It writes a timestamp and NOTHING ELSE. The row, its body, its parsed form, and its hash are untouched, so:

  • `workflow show name@n` still renders it, and `--source` still emits the exact registered bytes;
  • definitionByID still resolves it, so a run that pinned this version before it was retired continues to completion — retirement is a binding-time filter, not a retraction;
  • the lineage stays legible: `workflow list` shows the version with its retirement date rather than a gap where a version used to be.

There is deliberately no delete verb. The operator was offered one and rejected it: old versions stay registered, and that is the point.

func DerivePrefix

func DerivePrefix(name string) string

DerivePrefix proposes a display prefix from a project's name (DKT-60).

A multi-word name becomes its INITIALS — `agentic-mcp-services` reads as `AMS` — because the alternative, the first three letters, collapses whole families of sibling repositories onto the same three characters. A single-word name takes its first three letters instead, since `D` alone carries nothing.

The result is always a legal prefix per model.ValidateProjectPrefix (letters only, 1-8) or empty when the name yields no letters at all; uniqueness and the reserved names are availablePrefix's job, because both are facts about the store rather than about the name.

func DetachFiles

func DetachFiles(db *sql.DB, issueID int, filePaths []string, changedBy string) error

DetachFiles deletes rows from issue_files matching the issue ID and file paths. Activity is recorded for removed files.

func DispatchGraceTx

func DispatchGraceTx(tx *sql.Tx, projectID int) (time.Duration, error)

DispatchGraceTx is D1's window: how long a claimed step may go unrecorded.

func DispatchTTLTx

func DispatchTTLTx(tx *sql.Tx, projectID int) (time.Duration, error)

DispatchTTLTx and DispatchGraceTx resolve §4.11's two durations inside the caller's transaction, for the reason configValueTx exists: internal/db caps the pool at ONE connection, so a pool read from inside an open transaction deadlocks rather than failing.

func EnsureProject

func EnsureProject(conn *sql.DB, identity, name string, nowMS int64) (int, error)

EnsureProject resolves identity to a project id, creating the row on first contact.

The resolution ladder:

  1. A row already bound to this identity wins.
  2. The UNCLAIMED default row — id 1 with an empty identity, seeded by the v12 migration — is claimed in place. A legacy store holds exactly one project's history under project 1, and the first repository to open it is that project; claiming rather than inserting is what keeps that history attached to its repo.
  3. Otherwise a new row is inserted.

An EMPTY identity never claims and never inserts: it reads as "this invocation could not be resolved to a project" and falls back to the default row, which is the pre-v12 behavior exactly.

func EnsureProjectCreated

func EnsureProjectCreated(conn *sql.DB, identity, name string, nowMS int64) (int, bool, error)

EnsureProjectCreated is EnsureProject with the fact the caller needs in order to report a first contact: whether this call is what brought the row into being. The root hook writes a `project-registered` event on true (DKT-61).

func EventsRetain

func EventsRetain(db *sql.DB) (time.Duration, error)

EventsRetain resolves the retention window, with 0 meaning "retain everything" (docs/tdd/events-follow.md §5.3).

It returns the DURATION rather than a cutoff timestamp, because the caller computes the cutoff inside the prune's own transaction against that transaction's clock — a helper that read the clock here would hand back a boundary that had already moved by the time it was applied. EventsRetain is deliberately STORE-WIDE (projectID 0): the event stream and its prune are one machine-level lifecycle, and a per-project window would let one project's policy delete rows another project's audit still needs.

func GetActivity

func GetActivity(db *sql.DB, issueID int, limit int) ([]model.Activity, error)

GetActivity retrieves activity log entries for an issue, ordered by most recent first.

func GetAllDirectionalRelations

func GetAllDirectionalRelations(db *sql.DB) ([]model.Relation, error)

GetAllDirectionalRelations returns all relations where the relation type is "blocks" or "depends_on", in INSERTION order (`id`) — see GetAllRelations.

func GetAllRelations

func GetAllRelations(db *sql.DB, projectID int) ([]model.Relation, error)

GetAllRelations returns every relation in the database, in INSERTION order (`id`).

It was `ORDER BY created_at ASC` with no tiebreak, and `created_at` here is RFC3339 at SECOND resolution (see AddRelation). Relations created inside one second therefore had NO defined relative order, so `docket export` run twice against an unchanged database could emit the relations array in two different orders and a manifest diff would carry noise that is not a change (DKT-330).

`id` is `INTEGER PRIMARY KEY AUTOINCREMENT`: strictly ascending, never reused. Ordering by it is not an approximation of creation order, it IS creation order, and it is total. Thirteen of the fifteen export-manifest readers already order by a primary or natural key; this was the one with no total order at all.

Same defect class as DKT-378's on `comments`, reached through another table — and the same remedy, for the same reason.

func GetBatchSubIssueProgress

func GetBatchSubIssueProgress(conn *sql.DB, parentIDs []int) (map[int][2]int, error)

GetBatchSubIssueProgress returns (done, total) counts for descendants of each given parent ID in a single query, avoiding N+1 overhead.

func GetComment

func GetComment(db *sql.DB, id int) (*model.Comment, error)

GetComment retrieves a comment by ID.

func GetDoc

func GetDoc(db *sql.DB, id int) (*model.Doc, error)

GetDoc returns the doc with the given ID, or ErrNotFound.

func GetDocComment

func GetDocComment(db *sql.DB, id int) (*model.DocComment, error)

GetDocComment returns a single doc comment by ID, or ErrNotFound.

func GetDocIssues

func GetDocIssues(db *sql.DB, docID int) ([]int, error)

GetDocIssues returns issue IDs linked to a doc, ordered by issue_id ASC.

func GetDocProposals

func GetDocProposals(db *sql.DB, docID int) ([]int, error)

GetDocProposals returns proposal IDs linked to a doc, ordered by proposal_id ASC.

func GetDocRevision

func GetDocRevision(db *sql.DB, docID, rev int) (*model.DocRevision, error)

GetDocRevision returns revision rev of the doc with the given ID. Per TDD §3.3 / §6.3 (Q1): rev < 0 → ErrValidation; rev > MAX → ErrNotFound. rev == 0 is treated as "the current revision" (the most-recent one).

func GetIssue

func GetIssue(db *sql.DB, id int) (*model.Issue, error)

GetIssue retrieves an issue by ID.

func GetIssueDocs

func GetIssueDocs(db *sql.DB, issueID int) ([]int, error)

GetIssueDocs returns doc IDs linked to an issue, ordered by doc_id ASC.

func GetIssueFiles

func GetIssueFiles(db *sql.DB, issueID int) ([]string, error)

GetIssueFiles returns the file paths attached to an issue, sorted alphabetically.

func GetIssueLabelObjects

func GetIssueLabelObjects(db *sql.DB, issueID int) ([]*model.Label, error)

GetIssueLabelObjects returns the full Label objects attached to an issue, sorted alphabetically by name.

func GetIssueLabels

func GetIssueLabels(db *sql.DB, issueID int) ([]string, error)

GetIssueLabels returns the label names attached to an issue, sorted alphabetically.

func GetIssueLease

func GetIssueLease(db *sql.DB, id int) (*model.Lease, error)

GetIssueLease reads an issue's lease without writing anything.

Reads never write (engine-spec.md §6). Liveness is computed from ExpiresMS by the caller via Lease.Live; nothing here reaps.

func GetIssueProposals

func GetIssueProposals(db *sql.DB, issueID int) ([]model.Proposal, error)

GetIssueProposals returns the proposals linked to an issue, ordered by proposal id ascending. It is the reverse edge of GetProposalIssues.

func GetIssueRelations

func GetIssueRelations(db *sql.DB, issueID int) ([]model.Relation, error)

GetIssueRelations returns all relations where the given issue is either the source or the target, in INSERTION order (`id`) — see GetAllRelations for why that is the creation order rather than an approximation of it.

func GetIssuesByIDs

func GetIssuesByIDs(db *sql.DB, ids []int) (map[int]*model.Issue, error)

GetIssuesByIDs retrieves multiple issues by their IDs in a single query. The returned map is keyed by issue ID. IDs that don't exist are silently skipped (no error for missing rows). Labels are hydrated on all returned issues.

func GetLabelByName

func GetLabelByName(db *sql.DB, projectID int, name string) (*model.LabelWithCount, error)

GetLabelByName retrieves a label by name WITHIN ONE PROJECT, including the count of issues currently attached to it. Returns ErrNotFound if no label with that name exists in the project.

func GetProject

func GetProject(conn *sql.DB, id int) (*model.Project, error)

GetProject reads one project row.

func GetProposal

func GetProposal(db *sql.DB, id int) (*model.Proposal, error)

GetProposal returns a proposal by ID, or ErrNotFound if it does not exist.

func GetProposalDocs

func GetProposalDocs(db *sql.DB, proposalID int) ([]int, error)

GetProposalDocs returns doc IDs linked to a proposal, ordered by doc_id ASC.

func GetProposalIssues

func GetProposalIssues(db *sql.DB, proposalID int) ([]int, error)

GetProposalIssues returns the issue IDs linked to a proposal.

func GetProposalVotes

func GetProposalVotes(db *sql.DB, proposalID int) ([]*model.Vote, error)

GetProposalVotes returns all votes for a proposal, ordered by creation time.

func GetRun

func GetRun(db *sql.DB, id int) (*model.Run, error)

GetRun reads one run.

func GetRunTx

func GetRunTx(tx *sql.Tx, id int) (*model.Run, error)

GetRunTx is GetRun inside a transaction — the fat transaction's own reader.

func GetSchema

func GetSchema(db *sql.DB, projectID int, name string, version int) (*model.Schema, error)

GetSchema returns one registered schema visible to a project — its own registration or a builtin. A version of 0 selects the HIGHEST registered version, which is what `schema show NAME` without `@version` means.

func GetSchemaTx

func GetSchemaTx(tx *sql.Tx, projectID int, name string, version int) (*model.Schema, error)

GetSchemaTx is GetSchema at an exact version, inside a transaction. It is what activation's pin stage reads, so the hash it records and the row it checked are the same read (§4.7 P1).

func GetStepLease

func GetStepLease(db *sql.DB, id int) (*model.Lease, error)

GetStepLease reads a step's lease without writing anything (§6.3: reads never write).

func GetSubIssueProgress

func GetSubIssueProgress(db *sql.DB, parentID int) (int, int, error)

GetSubIssueProgress returns (done, total) counts for all descendants of an issue.

func GetSubIssueTree

func GetSubIssueTree(db *sql.DB, parentID int) ([]*model.Issue, error)

GetSubIssueTree returns the full recursive tree of all descendants under an issue.

func GetSubIssues

func GetSubIssues(db *sql.DB, parentID int) ([]*model.Issue, error)

GetSubIssues returns all direct children of an issue.

func GetVersion

func GetVersion(db *sql.DB, table string, id int) (int, error)

GetVersion returns the current CAS version of a row. The table must be one of versionedTables.

func GetWorkflow

func GetWorkflow(db *sql.DB, projectID int, name string, version int) (*model.Workflow, error)

GetWorkflow returns one registered workflow WITHIN ONE PROJECT (v12 — a name@version is a per-project registration). A version of 0 selects the HIGHEST registered version, which is what `workflow show NAME` without `@version` means.

func GrantLoopTx

func GrantLoopTx(tx *sql.Tx, runID, issueID int) (int, error)

GrantLoopTx authorizes ONE more fix loop for an issue and returns the new total.

It RAISES A GRANT rather than editing `max_fix_loops`, because the two say different things: the workflow's bound is the author's standing policy over every issue it matches, while this is one operator's decision about one issue on one occasion. Editing the bound to unstick a single issue would quietly loosen it for every issue after.

func HasGateResult

func HasGateResult(conn *sql.DB, stepID int, gate string) (bool, error)

HasGateResult reports whether a step+gate has any recorded result.

This is the at-least-once detector (§7.5 A1): a `saga_stage` of `gate:<name>` with no result row is a started-but-unrecorded gate — a crash between the `gate-started` commit and the result commit.

func HeartbeatIssue

func HeartbeatIssue(db *sql.DB, id int, token string, ttlMS int64, nowMS int64) (*model.Lease, error)

HeartbeatIssue extends a live lease held by token, and returns the new expiry. Any tool activity in the holder's session can drive this, so a working holder keeps its lease and a wedged or dead one lets it lapse (engine-core.md §5 "Leases").

attempt and owner are untouched: a heartbeat is not a new claim.

func HeartbeatStep

func HeartbeatStep(db *sql.DB, id int, token string, ttlMS, nowMS int64) (*model.Lease, error)

HeartbeatStep extends a live lease held by token. attempt is untouched — a heartbeat is not a new claim.

func HydrateDocs

func HydrateDocs(db *sql.DB, issues []*model.Issue) error

func HydrateFiles

func HydrateFiles(db *sql.DB, issues []*model.Issue) error

HydrateFiles bulk-loads files for a set of issues, populating each issue's Files field. This avoids N+1 queries in list views and the planner.

func HydrateLabels

func HydrateLabels(db *sql.DB, issues []*model.Issue) error

HydrateLabels bulk-loads labels for a set of issues, populating each issue's Labels field. This avoids N+1 queries when displaying lists.

func HydrateLinkedIssues

func HydrateLinkedIssues(db *sql.DB, docIDs []int) (map[int][]model.IssueRef, error)

func IdempotencyKeyOf

func IdempotencyKeyOf(db *sql.DB, scope string, entityID int) (string, bool, error)

IdempotencyKeyOf is the REVERSE lookup: the key recorded for (scope, entityID), and whether one exists. It exists for the caller that holds an entity and needs the identity its create was keyed under — the engine recovering a vote-step proposal's run from the key OpenVoteProposal recorded — without a second, disagreeable copy of that link on the entity's own row. A create performed without a key (the historical non-idempotent wrappers) simply reports no row.

func IncrementLoopCountTx

func IncrementLoopCountTx(tx *sql.Tx, runID, issueID int) (int, error)

IncrementLoopCountTx raises the issue's loop counter by one and returns the NEW value (§11.3 (1)).

The read-back is in the same statement's transaction rather than a separate SELECT so the value returned is the one this UPDATE wrote. Two concurrent routings incrementing the same issue would otherwise both read the same "new" count and both believe they were loop k+1 — and `max_fix_loops` would bound nothing.

func Initialize

func Initialize(db *sql.DB) error

Initialize creates all tables if they don't exist and sets the schema version.

func InsertActionResultTx

func InsertActionResultTx(tx *sql.Tx, r ActionResultRow) error

InsertActionResultTx records one action result.

It takes a transaction because the result commits with the routing stage's own writes — the subprocess ran OUTSIDE any transaction (engine-spec §6) and only its recorded fact lands in one.

func InsertActivityWithID

func InsertActivityWithID(tx *sql.Tx, a *model.Activity) (bool, error)

InsertActivityWithID inserts an activity_log row with a caller-supplied ID, skipping if the ID already exists. Must be called within an existing transaction. Returns true if inserted. Mirrors InsertIssueWithID.

func InsertArtifactTx

func InsertArtifactTx(tx *sql.Tx, a Artifact, nowMS int64) (int, error)

InsertArtifactTx records one artifact inside the caller's transaction — §6.8 stage 1, alongside the token's retirement, because "the token retires when the artifact records" is one commit or it is not the hinge it is specified to be.

func InsertCommentWithID

func InsertCommentWithID(tx *sql.Tx, comment *model.Comment) (bool, error)

InsertCommentWithID inserts a comment with a specific ID (not auto-increment), skipping if the ID already exists. Returns true if the row was inserted. Must be called within an existing transaction.

func InsertDispatchRowTx

func InsertDispatchRowTx(tx *sql.Tx, dispatchID int, row DispatchRow) error

InsertDispatchRowTx stores one manifest row at its position.

func InsertDispatchTx

func InsertDispatchTx(tx *sql.Tx, runID int, openedSeq, expiresMS, nowMS int64) (int, error)

InsertDispatchTx opens a manifest, and C1 IS THE INSERT.

`idx_dispatches_one_open` is a partial UNIQUE index on (run_id) WHERE status='open', so two relays racing produce one row and one constraint violation — never a check-then-insert's window, in which both SELECT no open dispatch and both then INSERT one. The loser gets ErrDispatchAlreadyOpen and its whole computation is DISCARDED rather than merged (§5.4).

func InsertDocCommentWithID

func InsertDocCommentWithID(tx *sql.Tx, c *model.DocComment) (bool, error)

InsertDocCommentWithID inserts a doc_comments row with a caller-supplied ID, skipping if the ID already exists. Returns true if inserted. Must be called within an existing transaction. Mirrors InsertCommentWithID.

func InsertDocIssueLink(tx *sql.Tx, docID, issueID int, createdAt string) (bool, error)

InsertDocIssueLink inserts a doc_issue_links row, skipping on PK conflict. Used by export/import round-trip. Must be called within a transaction. Returns true if inserted.

func InsertDocRevisionWithID

func InsertDocRevisionWithID(tx *sql.Tx, r *model.DocRevision) (bool, error)

InsertDocRevisionWithID inserts a doc_revisions row with a caller-supplied ID, skipping if the ID already exists. Must be called within a transaction. Returns true if inserted. Mirrors InsertIssueWithID.

func InsertDocWithID

func InsertDocWithID(tx *sql.Tx, doc *model.Doc) (bool, error)

InsertDocWithID inserts a doc row with a caller-supplied ID, skipping if the ID already exists. Mirrors InsertIssueWithID (TDD §5.3 round-trip helpers). Must be called within an existing transaction. Returns true if inserted.

func InsertEngineComment

func InsertEngineComment(tx *sql.Tx, issueID int, body string, nowMS int64) (int, error)

InsertEngineComment inserts an engine-authored comment against the caller's already-open transaction and returns its auto-minted ID. It never begins or commits a transaction of its own, so engine code already inside a transaction can drop an activity-trail comment without nesting a second top-level transaction. The comment's Author is always EngineAuthor.

The caller supplies the timestamp as epoch milliseconds rather than the writer reading the clock: an engine transaction stamps the issue row, the activity log and this comment from ONE `nowMS`, and a second clock read here would let the narration of a transition carry a different time than the transition itself.

func InsertFenceTx

func InsertFenceTx(tx *sql.Tx, f RunFence) error

InsertFenceTx records one harvested command. Harvesting happens at ACTIVATION so "post-activation edits cannot inject" (engine-spec §4) — the hash is of the command as it read when the operator approved the plan.

func InsertGapIssueTx

func InsertGapIssueTx(tx *sql.Tx, projectID int, title, description string, relatedIssueID int) (int, error)

InsertGapIssueTx materializes a backlog issue from a recorded gap artifact (DKT-72), inside the completion saga's transaction, and relates it to the issue whose step recorded the gap.

One transaction with the artifact, because the pair is the whole point: a gap that recorded an artifact but no issue is residue nothing re-reads — the failure mode this replaces — and an issue without its artifact is a claim with no record behind it. `relates_to` rather than a directional relation: a gap is out-of-scope BY DEFINITION, so it must not block the issue that surfaced it.

func InsertGateResultTx

func InsertGateResultTx(tx *sql.Tx, r GateResultRow) error

InsertGateResultTx records one gate result.

It takes a transaction because the result commits with the saga's stage advance — the subprocess ran OUTSIDE any transaction (engine-spec §6) and only its recorded fact lands in one.

func InsertIssueFileMapping

func InsertIssueFileMapping(tx *sql.Tx, issueID int, filePath string) (bool, error)

InsertIssueFileMapping inserts a single file mapping using INSERT OR IGNORE. Returns true if inserted, false if already existed. Must be called within an existing transaction.

func InsertIssueLabelMapping

func InsertIssueLabelMapping(tx *sql.Tx, issueID, labelID int) (bool, error)

InsertIssueLabelMapping inserts an issue_labels row linking an issue to a label, skipping if the mapping already exists. Returns true if the row was inserted. Must be called within an existing transaction.

func InsertIssueWithID

func InsertIssueWithID(tx *sql.Tx, issue *model.Issue) (bool, error)

InsertIssueWithID inserts an issue with a specific ID (not auto-increment), skipping if the ID already exists. Returns true if the row was inserted. Must be called within an existing transaction.

func InsertLabelWithID

func InsertLabelWithID(tx *sql.Tx, label *model.Label) (bool, error)

InsertLabelWithID inserts a label with a specific ID (not auto-increment), skipping if the ID already exists. Returns true if the row was inserted. Must be called within an existing transaction.

func InsertNoteTx

func InsertNoteTx(tx *sql.Tx, issueID int, body, author, now string) (int, error)

InsertNoteTx records an author-attributed comment against the caller's already-open transaction: the row `issue comment add` writes, minus the transaction it opens for itself. It is what a verb taking `--note` uses to land the note and the mutation it explains in ONE transaction (DKT-480), so a failed move leaves no orphan comment and a recorded comment never narrates a move that did not happen.

Unlike InsertEngineComment the author is the caller's, not EngineAuthor: a note an operator typed is an operator's comment, indistinguishable from the two-verb form it replaces. It touches the issue's updated_at and records the same `comment_added` activity CreateComment does, for the same reasons.

The updated_at touch runs FIRST so a missing issue is ErrNotFound rather than a foreign-key error from the insert.

The caller supplies the timestamp rather than this reading the clock, so the note carries the same stamp as the mutation it narrates.

func InsertPinTx

func InsertPinTx(tx *sql.Tx, p Pin) error

InsertPinTx records a pin. `INSERT OR IGNORE` on the UNIQUE(run_id, kind, ref) key makes pinning the same workflow for two issues in one run a single row rather than a conflict — a run pins a workflow once, however many issues bind to it.

func InsertProposalDocLink(tx *sql.Tx, proposalID, docID int, createdAt string) (bool, error)

InsertProposalDocLink inserts a proposal_docs row, skipping on PK conflict. Must be called within a transaction. Returns true if inserted.

func InsertProposalIssueLink(tx *sql.Tx, proposalID, issueID int) (bool, error)

InsertProposalIssueLink inserts a proposal_issues row, skipping on PK conflict. Must be called within a transaction. Returns true if inserted.

func InsertProposalWithID

func InsertProposalWithID(tx *sql.Tx, p *model.Proposal) (bool, error)

InsertProposalWithID inserts a proposal row with a caller-supplied ID, skipping if the ID already exists. Must be called within an existing transaction. Returns true if inserted. Mirrors InsertIssueWithID; domain_tags and files_changed are JSON-encoded identically to CreateProposal.

func InsertReapAckTx

func InsertReapAckTx(tx *sql.Tx, ack ReapAck, nowMS int64) error

InsertReapAckTx records a reap as unacknowledged, in the reap's own transaction (A16, A19).

The caller is responsible for A16's OTHER half — calling this only for a class whose `[limits] max` is finite — because that decision needs the scheduler's merged limits, which the storage layer does not have and must not guess at.

func InsertRelationWithID

func InsertRelationWithID(tx *sql.Tx, rel *model.Relation) (bool, error)

InsertRelationWithID inserts a relation with a specific ID (not auto-increment), skipping if the ID already exists. Returns true if the row was inserted. Must be called within an existing transaction.

func InsertRun

func InsertRun(db *sql.DB, projectID int, request string, budget float64, nowMS int64) (*model.Run, error)

InsertRun creates a run in `planning` — `docket run start` (TDD §5.2).

`budget` is STORED and enforces nothing until S6. Accepting it now means the S6 upgrade adds enforcement rather than a flag: a flag appearing later would break the `run start` invocation an S3-era harness scripted.

func InsertRunWithContext

func InsertRunWithContext(db *sql.DB, projectID int, request string, budget float64, nowMS int64, ctx RunContext) (*model.Run, error)

InsertRunWithContext is InsertRun carrying the invocation's context.

func InsertRunWithContextIdempotent

func InsertRunWithContextIdempotent(db *sql.DB, projectID int, request string, budget float64, nowMS int64, ctx RunContext, idempotencyKey string) (*model.Run, error)

InsertRunWithContextIdempotent is InsertRunWithContext with an optional idempotency key — `docket run start --idempotency-key`.

A repeat call with the same key returns the ORIGINAL run unchanged and creates nothing, the same create-verb replay-protection pattern CreateIssueIdempotent, CreateDocIdempotent, and CreateProposalIdempotent use (internal/db/idempotency.go): a `run start` that commits but dies before its response must be safe to retry, or the key protects nothing. The key record and the insert commit in the SAME transaction, so a crash between them cannot orphan either.

func InsertSchema

func InsertSchema(db *sql.DB, s *model.Schema, nowMS int64) (stored *model.Schema, created bool, err error)

InsertSchema registers a schema document, or returns the existing row when the same bytes are already registered at that `name@version`.

The three outcomes are `InsertWorkflow`'s, verbatim in behavior (§4.4):

  • no row at name@version -> insert, created = true
  • a row with the SAME source_sha256 -> return it, created = false
  • a row with a DIFFERENT source_sha256 -> ErrSchemaConflict

Idempotency is decided on the CONTENT HASH, not on a normalized form: two documents that validate identically but differ in whitespace or key order are different registered bytes, because `source_sha256` is what pins refer to (§4.7) and what a run reproduces against.

func InsertSchemaTx

func InsertSchemaTx(
	tx *sql.Tx, s *model.Schema, nowMS int64,
) (stored *model.Schema, created bool, err error)

InsertSchemaTx is InsertSchema inside a CALLER'S transaction — the schema half of what S6's auto-registration needs (docs/tdd/runs-dispatch.md §9.2 F8).

It mirrors InsertWorkflowTx exactly, because the two registries have the same immutability contract and a caller should not have to learn it twice. The reason both are needed inside a transaction is F8's: auto-registration runs in activation's fat transaction, so a failure refuses the whole activation and leaves no definitions behind from a run that never started.

func InsertStepInputTx

func InsertStepInputTx(tx *sql.Tx, stepID, position, artifactID int) error

InsertStepInputTx records that a step consumed an artifact at a declared position — the resolution §6.7 computed, MATERIALIZED.

The record exists because resolution is a function of run state at ASSEMBLY time, and run state moves: a later ordinal's artifact would re-resolve the same input differently. Storing what was actually handed over is what makes the ledger answer "what did this step see" rather than "what would it see now".

func InsertStepTx

func InsertStepTx(tx *sql.Tx, s StepRow, nowMS int64) error

InsertStepTx writes one expanded step. The UNIQUE(run_id, issue_id, instance) index is the loop/fanout correctness guard — two rows claiming the same identity is the bug §11.3 exists to prevent — so a duplicate is an error here rather than a silently-ignored insert.

func InsertTrustCacheTx

func InsertTrustCacheTx(
	tx *sql.Tx, runID int, kind, gate, argvSHA256, entryName string,
	matched, prefix bool, atMS int64,
) error

InsertTrustCacheTx records what a run considered trusted, and when (§4.5).

IT IS AN AUDIT RECORD, NEVER AN AUTHORIZATION SHORTCUT. Every gate consults the LIVE trust store on every execution (§7.2 M1); nothing is ever executed because a row here says a previous run matched. The opposite implementation is the tempting one and it is a revocation failure: a cache hit that authorized a spawn would make `trust rm` take effect only after the cache cleared.

`kind` is TrustKindGate or TrustKindAction (§6.3). It exists so the one question this table answers stays one query when actions start consulting the same store: a second table keyed by the shape of the caller would split it. Every pre-v9 row reads `gate` from the column default, which is what it was.

func InsertUsageRowTx

func InsertUsageRowTx(tx *sql.Tx, row UsageRow, nowMS int64) error

InsertUsageRowTx records one unit's quantity for one attempt of one step.

The unique key is (step_id, attempt, unit), so a reaped-and-reclaimed step's SECOND attempt records beside the first rather than overwriting it — which is what makes "retries re-accrue" true on the reported side as well as on the floor side. A second `complete` for the SAME attempt is impossible (the saga's stage-0 CAS), so the key is a belt-and-braces assertion rather than an upsert: if it ever fires, something upstream broke a guarantee and silently merging the rows would hide it.

func InsertVoteUsageTx

func InsertVoteUsageTx(
	tx *sql.Tx, voteID int64, unit string, quantity float64, source string, nowMS int64,
) error

InsertVoteUsageTx records one unit's quantity against one SEAT's cast in the vote_usage ledger, source explicit (v17, DKT-115): the cast-time writer passes UsageSourceReported, the vote-scoped back-fill its own source. The (vote_id, unit) key firing maps to ErrUsageAlreadyRecorded so each caller can phrase the refusal for whoever hit it — the same split the step ledger's writer makes.

func InsertVoteWithID

func InsertVoteWithID(tx *sql.Tx, v *model.Vote) (bool, error)

InsertVoteWithID inserts a vote row with a caller-supplied ID, skipping if the ID already exists. Must be called within an existing transaction. Returns true if inserted. findings_json and metadata are JSON-encoded (NULL when absent) identically to CastVote, so an export/import round trip carries a vote's provenance claim exactly as it carries its findings.

func InsertWorkflow

func InsertWorkflow(db *sql.DB, wf *model.Workflow, nowMS int64) (stored *model.Workflow, created bool, err error)

InsertWorkflow registers a definition, or returns the existing row when the same bytes are already registered at that `name@version`.

The three outcomes, per §4.1:

  • no row at name@version -> insert, created = true
  • a row with the SAME source_sha256 -> return it, created = false
  • a row with a DIFFERENT source_sha256 -> ErrWorkflowConflict

Idempotency is decided on the CONTENT HASH rather than on the parsed form: two files that parse identically but differ in comments are different registered bytes, and `source_sha256` is what pins and audits refer to.

func InsertWorkflowTx

func InsertWorkflowTx(
	tx *sql.Tx, wf *model.Workflow, nowMS int64,
) (stored *model.Workflow, created bool, err error)

InsertWorkflowTx is InsertWorkflow inside a CALLER'S transaction.

It exists for S6's auto-registration, which runs inside activation's FAT TRANSACTION (docs/tdd/runs-dispatch.md §9.2 F8): a registration failure must refuse the whole activation and write nothing, and a registration that committed on its own could not be rolled back with the binding that followed it. It is also the only correct shape against a one-connection pool — the self-committing version would deadlock if called from inside a transaction.

The three outcomes and the immutability contract are §4.1's, unchanged: this is the SAME body InsertWorkflow ran, lifted so both callers share it rather than a second path drifting from the first (F7: "no `auto` variant with looser rules").

func IsDescendant

func IsDescendant(db *sql.DB, issueID, potentialDescendantID int) (bool, error)

IsDescendant returns true if potentialDescendantID is a descendant of issueID. This is used to detect cycles when reparenting an issue.

func IssueExists

func IssueExists(db *sql.DB, issueID int) (bool, error)

IssueExists returns true if an issue with the given ID exists.

func IssueOwnerPrefix

func IssueOwnerPrefix(conn *sql.DB, issueID int) (string, error)

IssueOwnerPrefix reports the prefix of the project that OWNS an issue, or "" when the issue does not exist or its project has none (DKT-256).

One indexed lookup joining the issue to its project. It is the single-id form deliberately: issue ids are store-wide and a rendering pass touches whichever handful it happens to render, so a caller that preloaded the whole store would pay for every issue to display five.

A missing row is "" AND NO ERROR. The caller is a renderer, and a renderer that failed because an id it was handed does not exist would turn a cosmetic question into a broken command — the fallback is the reader's own prefix, which is exactly what it rendered before this existed.

func IssueProjectID

func IssueProjectID(db *sql.DB, issueID int) (int, error)

IssueProjectID returns the project an issue is homed in, or ErrNotFound.

It exists for validations that need the project WITHOUT the issue's whole row: attaching issues to runs checks a whole set before writing anything (DKT-21), and loading each candidate's labels and lease state to read one column would make that loop's cost proportional to data it discards.

func IssueScopeGlobs

func IssueScopeGlobs(db *sql.DB, issueID int) (string, error)

IssueScopeGlobs reads an issue's declared scope as stored JSON, or "" when none is declared.

func IssueScopeGlobsTx

func IssueScopeGlobsTx(tx *sql.Tx, issueID int) (string, error)

IssueScopeGlobsTx is IssueScopeGlobs inside a transaction — the snapshot's reader at activation stage 4.

func IssueStepRuns

func IssueStepRuns(db *sql.DB, issueID int) ([]int, error)

IssueStepRuns reads the ids of every run holding a step for one issue, in run order. `step list --issue ISSUE-N` needs it because an issue's steps are not confined to one run — a re-activation mints a fresh round under a new run — and the caller who asks about an issue rarely knows which runs those are (DKT-244).

func KnownConfigKeys

func KnownConfigKeys() []string

KnownConfigKeys lists the fixed keys, plus the open-ended patterns.

func LeaseTTL

func LeaseTTL(db *sql.DB, projectID int, class string) (time.Duration, error)

LeaseTTL resolves the effective lease TTL for an executor class, falling back to lease.ttl.default when the class has no entry of its own. An empty class means "use the default".

func LinkDocIssue

func LinkDocIssue(db *sql.DB, docID, issueID int) error

LinkDocIssue links a doc to an issue. Returns ErrNotFound if either side is missing; ErrConflict if the link already exists.

func LinkProposalDoc

func LinkProposalDoc(db *sql.DB, proposalID, docID int) error

LinkProposalDoc links a proposal (vote) to a doc. Returns ErrNotFound if either side is missing; ErrConflict if the link already exists.

func LinkProposalIssue

func LinkProposalIssue(db *sql.DB, proposalID, issueID int) error

LinkProposalIssue links a proposal to an issue. Returns ErrNotFound if the proposal or issue does not exist. Returns ErrConflict if the link already exists.

func ListAllActivity

func ListAllActivity(db *sql.DB, projectID int) ([]*model.Activity, error)

ListAllActivity returns every activity_log row ordered by id ASC, for a full export.

func ListAllComments

func ListAllComments(db *sql.DB, projectID int) ([]*model.Comment, error)

ListAllComments returns every comment in the database across all issues, ordered by insertion (`id`) for the reason given above ListComments — the same table read through a second query, so the two agree on what "in order" means.

func ListAllDocComments

func ListAllDocComments(db *sql.DB, projectID int) ([]*model.DocComment, error)

ListAllDocComments returns every doc_comments row ordered by id ASC, for a full export.

func ListAllDocIssueLinks(db *sql.DB, projectID int) ([]model.DocIssueLink, error)

ListAllDocIssueLinks returns every doc_issue_links row ordered by (doc_id, issue_id), for a full export.

func ListAllDocRevisions

func ListAllDocRevisions(db *sql.DB, projectID int) ([]*model.DocRevision, error)

ListAllDocRevisions returns every doc_revisions row ordered by id ASC, for a full export.

func ListAllDocs

func ListAllDocs(db *sql.DB, projectID int) ([]*model.Doc, error)

ListAllDocs returns every doc row ordered by id ASC, for a full export.

func ListAllIssueFileMappings

func ListAllIssueFileMappings(db *sql.DB, projectID int) ([]model.IssueFileMapping, error)

ListAllIssueFileMappings returns all rows from issue_files as IssueFileMapping structs. This is needed by the export command.

func ListAllIssueLabelMappings

func ListAllIssueLabelMappings(db *sql.DB, projectID int) ([]model.IssueLabelMapping, error)

ListAllIssueLabelMappings returns all (issue_id, label_id) pairs from the issue_labels table.

func ListAllIssues

func ListAllIssues(db *sql.DB, projectID int) ([]*model.Issue, error)

ListAllIssues returns every issue in the database, including done issues, with no filters, sorting, or pagination. Labels are hydrated on all results.

func ListAllLabels

func ListAllLabels(db *sql.DB, projectID int) ([]*model.LabelWithCount, error)

ListAllLabels returns a project's labels along with the count of issues using each, sorted alphabetically by name. A zero projectID lists every project's labels.

func ListAllLabelsRaw

func ListAllLabelsRaw(db *sql.DB, projectID int) ([]*model.Label, error)

ListAllLabelsRaw returns every label as a model.Label object (without issue counts), sorted alphabetically by name.

func ListAllProposalDocs

func ListAllProposalDocs(db *sql.DB, projectID int) ([]model.ProposalDocLink, error)

ListAllProposalDocs returns every proposal_docs row ordered by (proposal_id, doc_id), for a full export.

func ListAllProposalIssues

func ListAllProposalIssues(db *sql.DB, projectID int) ([]model.ProposalIssueLink, error)

ListAllProposalIssues returns every proposal_issues row ordered by (proposal_id, issue_id), for a full export.

func ListAllProposals

func ListAllProposals(db *sql.DB, projectID int) ([]*model.Proposal, error)

ListAllProposals returns every proposal row ordered by id ASC, for a full export.

func ListAllVotes

func ListAllVotes(db *sql.DB, projectID int) ([]*model.Vote, error)

ListAllVotes returns every vote row ordered by id ASC, for a full export.

func ListComments

func ListComments(db *sql.DB, issueID int) ([]*model.Comment, error)

ListComments retrieves all comments for an issue, ordered by the auto-minted `id` alone — INSERTION ORDER, which for the activity trail is the only order that is always true (DKT-378).

It was `created_at ASC, id ASC`, and the tiebreak was the whole reason: the column is RFC3339 at SECOND resolution and the engine writes several trail comments per transaction, so same-second rows came back in whatever order SQLite chose. But `created_at` stopped being monotonic with insertion when InsertEngineComment began taking the CALLER's `nowMS` instead of reading the clock (that change is right — see its doc comment — and is not what is being undone here). The saga threads ONE `nowMS` through a whole gate execution and `next` drives every ready action step on a single one, so two comments committed seconds apart can carry the same stamp, or the later one an EARLIER stamp. A tiebreak cannot repair a primary key that is itself out of order.

`id` is `INTEGER PRIMARY KEY AUTOINCREMENT`: strictly ascending, never reused after a delete. Sorting by it alone makes the trail read in the order it was written, which is what a narrative of transitions IS.

Clamping the stamp instead (the way engine/event.go's `monotonicAtMS` does for events, against 57s of measured drift) was the other workable remedy and was rejected here: a clamped `created_at` is no longer the transition's own time, which is precisely the invariant threading `nowMS` exists to hold. Events have no monotonic insertion key to fall back on; comments do.

func ListDocComments

func ListDocComments(db *sql.DB, docID int) ([]*model.DocComment, error)

ListDocComments returns all comments for a doc ordered by created_at ASC. Returns an empty slice (not nil) when the doc has no comments; returns ErrNotFound when the doc itself is missing.

func ListDocRevisions

func ListDocRevisions(db *sql.DB, docID int) ([]*model.DocRevision, error)

ListDocRevisions returns every revision row for the doc, ordered by revision_number ascending. Returns ErrNotFound when the doc itself is missing.

func ListDocs

func ListDocs(db *sql.DB, opts DocListOptions) ([]*model.Doc, int, error)

ListDocs returns docs matching opts, ordered and paginated. Returns the matching rows and the total count before limit/offset.

func ListIssues

func ListIssues(db *sql.DB, opts ListOptions) ([]*model.Issue, int, error)

ListIssues retrieves issues matching the given filters. It returns the matching issues, the total count of matching rows (ignoring Limit/Offset), and an error.

func ListProjects

func ListProjects(conn *sql.DB) ([]*model.Project, error)

ListProjects returns every project, default row first, then by id.

func ListProposals

func ListProposals(db *sql.DB, projectID int, status string, criticality string, domainTag string, limit int) ([]*model.Proposal, int, error)

ListProposals returns proposals with optional filters. It returns the matching proposals and the total count (before limit). A non-zero projectID scopes the list to one project (v12); zero lists every project.

func ListRuns

func ListRuns(db *sql.DB, opts RunListOptions) ([]*model.Run, int, error)

ListRuns returns runs newest first, and the TRUE total before the limit — the Collection contract (reliability-delta §4.1) needs a total a limit cannot distort, so truncation is computable rather than guessed.

func ListSchemas

func ListSchemas(db *sql.DB, opts SchemaListOptions) ([]*model.Schema, int, error)

ListSchemas returns registered schemas and the TRUE total before the limit — the Collection contract (reliability-delta §4.1) requires a total a limit cannot distort.

func ListWorkflows

func ListWorkflows(db *sql.DB, opts WorkflowListOptions) ([]*model.Workflow, int, error)

ListWorkflows returns registered workflows, newest registration first, and the TRUE total before the limit — the Collection contract (reliability-delta §4.1) requires a total that a limit cannot distort, so truncation is computable rather than guessed.

func LookupIdempotencyKey

func LookupIdempotencyKey(db *sql.DB, scope, key string) (int, bool, error)

LookupIdempotencyKey returns the entity id previously recorded for (scope, key), and whether such a record exists.

A hit means the caller already performed this create — the correct response is to return the original entity with exit 0, not an error. A retried create after a dropped response must succeed, or the key is useless to the caller it exists for.

func LookupIdempotencyKeyTx

func LookupIdempotencyKeyTx(tx *sql.Tx, scope, key string) (int, bool, error)

LookupIdempotencyKeyTx is LookupIdempotencyKey inside a CALLER'S transaction.

It exists for the same reason the plural form does: internal/db caps the pool at one connection, so a reader holding an open transaction cannot ask this question through the pool without deadlocking permanently. The single-key form is the one a caller wants when it knows exactly which key it is after — a prefix scan to find one row would read the whole family to discard it.

func LookupIdempotencyKeysTx

func LookupIdempotencyKeysTx(tx *sql.Tx, scope, prefix string) (map[string]int, error)

LookupIdempotencyKeysTx returns every (key, entity id) in one scope whose key starts with prefix, inside a CALLER'S transaction.

It exists for the reader that needs MANY of these at once and holds the single pooled connection while asking: one query over a family of keys rather than one per key, resolved where a pool read would deadlock rather than fail. What a prefix MEANS is the caller's business — this matches bytes.

A `%` or `_` inside prefix is escaped rather than passed through to LIKE, so a caller cannot accidentally widen its own question with a key it built out of data.

func LookupProject

func LookupProject(conn *sql.DB, identity string) (int, bool, error)

LookupProject resolves an identity to its project WITHOUT creating anything.

It is the read half EnsureProject used to keep private, split out for DKT-58: every invocation must be able to ask "which project is this?", and only some of them may answer "a new one".

func LoopGrantsTx

func LoopGrantsTx(tx *sql.Tx, runID, issueID int) (int, error)

LoopGrantsTx reads how many ADDITIONAL fix loops an operator has authorized for one issue in one run (DKT-237). Zero on every issue nobody has reopened.

func MarkExpandedTx

func MarkExpandedTx(tx *sql.Tx, runID, issueID int, nowMS int64) error

MarkExpandedTx stamps an issue as expanded — stage 6's record that this issue's phase is done, so re-activation (RA1) skips it.

func MarkStepAttemptFailedTx

func MarkStepAttemptFailedTx(tx *sql.Tx, id int, nowMS int64) error

MarkStepAttemptFailedTx counts one claim ended by an explicit `step fail` into the step's outcome breakdown (v23, DKT-490).

It exists because `attempt` spends one count per claim WHATEVER the ending, and a consumer that needed "how many attempts genuinely failed" had nothing to read but the event log — which is prunable, and whose instance labels repeat across a run's issues. Both `step fail` branches call it (the below-budget return to the pool and the exhausted routing): each is a claim whose holder measured its own work and recorded the failure.

It is a COUNTER BUMP ONLY — the lease write it accompanies (ReapStepTx or RetireStepTokenTx) stays the caller's, per ReapStepTx's mechanism/ classification split. It follows SetStepMetadataTx's shape — row_version bump, updated_at_ms — because a counter move is a step-row mutation CAS-guarded readers must see.

func MarkStepClaimReapedTx

func MarkStepClaimReapedTx(tx *sql.Tx, id int, nowMS int64) error

MarkStepClaimReapedTx counts one claim reaped WITHOUT a recorded failure into the step's outcome breakdown (v23, DKT-490) — MarkStepAttemptFailedTx's other half, called on the reap paths only: the lazy expiry reap (`next`, `dispatch open`, `claim`) and the forced `step reap`.

The distinction is the whole point. A reaped claim spent an `attempt` without anything failing — the holder went silent, or an operator asserted it dead — and an escalation policy that cannot tell that from a measured failure escalates on it, which is the DKT-490 misread. `step fail`'s return to the pool shares ReapStepTx's row reset but never this counter.

func MarkStepUsageRecordedTx

func MarkStepUsageRecordedTx(tx *sql.Tx, stepID int) error

MarkStepUsageRecordedTx sets `steps.usage_recorded` — the fast path group 2's discrepancy probe reads (§2.3).

Group 1 writes it so the column is populated from the moment the ledger exists: a probe that arrived later and found the column empty on steps that DID record usage would report every one of them as a discrepancy.

WHAT THE COLUMN MEANS is "the ledger question is SETTLED for this step", and it has exactly one reader — engine's `missingUsage` — which is what makes that reading the operative one. There are three ways to settle it: recording usage (budget.go), backfilling it (backfill.go), and an operator's `dispatch close --accept-missing-usage` (DKT-315), which settles the question without answering it. The three are distinguished in the RECORD — the ledger rows exist for the first two and not the third, and the acceptance rides in the `dispatch-closed` event — not in this flag, whose whole job is to let the probe skip a join.

func Migrate

func Migrate(db *sql.DB) error

Migrate checks the current schema version and applies any pending migrations sequentially. It is a no-op when already at the latest version.

func MoveIssueProject

func MoveIssueProject(
	conn *sql.DB, issueID, targetProjectID int, author string, nowMS int64,
) ([]int, error)

MoveIssueProject migrates a ROOT issue — and its entire sub-issue tree — to another project (DKT-27).

Gaps recorded by `step complete --gap-file` land in the run's own project unconditionally, because cwd is the record's only routing; when the surfaced work belongs to another repository, this verb is how the residue gets re-homed without export/import. Labels are re-mapped by NAME into the target project (created there when missing, color preserved) because label rows are per-project; relations and comments ride along untouched — ids are store-wide, so cross-project relations stay resolvable.

It returns the migrated issue ids, the root first.

func NextActionOrdinalTx

func NextActionOrdinalTx(tx *sql.Tx, stepID int, action string) (int, error)

NextActionOrdinalTx returns the ordinal a new result for this step+action takes.

Ordinals are per (step, action) and ascend, carrying §4's "flaky-declared re-runs recorded individually" (A8) and, with it, the resume case: a routing stage re-entered after a crash must not collide with the interrupted attempt's row, which the UNIQUE(step_id, action, ordinal) index would refuse.

func NextGateOrdinal

func NextGateOrdinal(conn *sql.DB, stepID int, gate string) (int, error)

NextGateOrdinal returns the ordinal a new result for this step+gate takes.

Ordinals are per (step, gate) and ascend, which is what carries §4's "flaky-declared re-runs recorded individually": each attempt is its own row, never an overwrite and never an aggregate.

func Open

func Open(dbPath string) (*sql.DB, error)

Open opens or creates the SQLite database at the given path. It sets pragmas for WAL mode, foreign key enforcement, and busy timeout.

func OpenReader

func OpenReader(dbPath string) (*sql.DB, error)

OpenReader opens a SEPARATE connection to the same database file, for best-effort point reads that must never contend with Open's single writer connection.

Open caps its pool at one connection because SQLite is single-writer — but that same cap means a query issued on THAT connection while it holds an open transaction (a caller mid-`BeginTx`) blocks forever: database/sql serializes all use of one *sql.DB through its pool, transaction included, and there is no second connection for the query to check out. That is a guaranteed self-deadlock, not a race — it fires every time a caller formats or looks up something from inside its own open transaction.

WAL mode (set by Open, and already on disk by the time this connects) is what makes a SECOND, independent connection the fix rather than a new hazard: a reader on its own connection sees a consistent snapshot without blocking, or being blocked by, an in-flight writer transaction on Open's connection.

This is for best-effort lookups only — id rendering, not decision logic — exactly the callers that already tolerate a failed read as "unknown" rather than an error.

func OrphanSubIssues

func OrphanSubIssues(db *sql.DB, parentID int, author string) error

OrphanSubIssues sets parent_id to NULL for all direct children of the given issue. Activity is recorded for each affected child within a transaction.

func PrefixHolder

func PrefixHolder(conn *sql.DB, prefix string, exclude int) (int, error)

PrefixHolder reports the id of the project holding prefix, other than `exclude`. It returns 0 when the prefix is free.

func ProjectPrefix

func ProjectPrefix(conn *sql.DB, id int) (string, error)

ProjectPrefix reads one project's display prefix — the root hook's second query, feeding model.SetDisplayPrefix before any command runs.

func ProjectRefCounts

func ProjectRefCounts(conn *sql.DB, id int) (map[string]int, error)

ProjectRefCounts reports how many rows in each project-scoped table belong to a project. Tables with no rows are omitted.

func ProposalStatusesTx

func ProposalStatusesTx(tx *sql.Tx, ids []int) (map[int]model.ProposalStatus, error)

ProposalStatusesTx reads many proposals' statuses in ONE query, inside a caller's transaction.

It exists for the reader that holds the single pooled connection and needs a handful of these at once: internal/db caps the pool at one connection, so a pool read from inside an open transaction deadlocks permanently rather than failing, and a per-proposal GetProposal loop from such a reader is the shape that produces it.

Status ONLY. A report that wanted the whole row would be asking for a different function; narrowing it here keeps this from becoming a second GetProposal that drifts from the first.

func ReapStepTx

func ReapStepTx(tx *sql.Tx, id int, nowMS int64) error

ReapStepTx returns an expired step to the unclaimed pool: lease cleared, status back to `pending`, `started_ms` cleared, `attempt` LEFT ALONE.

attempt already counted this try — it incremented at claim — so incrementing again here would double-count a single death. §6.3's "returning them to ready with attempt++" is satisfied by the claim's own increment: the trail records one attempt per claim, for all time (claims-leases §5), which is exactly what §9 item 4's "attempt trail is complete" asks for.

It is MECHANISM, not classification: `step fail`'s below-the-budget branch shares it to return a failed step to the pool, so the outcome counters (DKT-490) deliberately live outside it — each caller records what actually ended the claim, MarkStepClaimReapedTx on the reap paths and MarkStepAttemptFailedTx on the failure paths. Folding either bump in here would classify a failure as a reap at exactly the call site that knows better.

This is one of the two places a lease write may happen (§6.3: "lazy reaping confined to next/claim"). No read verb reaches it.

func RecordActivity

func RecordActivity(ex execer, issueID int, field, oldVal, newVal, changedBy string) error

RecordActivity logs a field change on an issue.

func RecordIdempotencyKeyTx

func RecordIdempotencyKeyTx(tx *sql.Tx, scope, key string, entityID int) error

RecordIdempotencyKeyTx records (scope, key) -> entityID inside tx.

It MUST be called in the same transaction as the insert it protects, so a crash between the two cannot orphan either. The (scope, key) primary key makes a concurrent duplicate a database constraint rather than an application-level check.

created_at_ms and seq are millisecond-resolution and monotonic per engine-spec.md §5. They live here, in a table created at v5, and never on a pre-existing column — mutating an existing timestamp format would break byte-compatibility for every existing verb.

func RefreshClaimLeaseTx

func RefreshClaimLeaseTx(
	tx *sql.Tx, id int, tokenHash string, ttlMS, nowMS int64,
) (bool, error)

RefreshClaimLeaseTx recomputes a live claim's `expires_ms` from the caller's own time, guarded by the claim identity (TDD docs/tdd/gates-trust.md §7.6.1.1 LR1/LR2).

WHY IT EXISTS: a claim with pre-gates runs subprocesses between the CAS and the response, and all of that wall time would otherwise be deducted from a lease the caller has not yet received. With a short TTL the caller can be handed an ALREADY-EXPIRED lease, so its first `step complete` fails on a lease it never had a chance to use — a livelock shaped like a too-short TTL but caused by docket's own pre-gate phase.

IT IS A HEARTBEAT BY ANOTHER NAME, NOT A SECOND AUTHORIZATION. The guard is the token hash transaction A wrote: the claimant still holds the CAS, which is exactly the condition the lease model already sanctions for extending a live claim. It can only FAIL, never award a claim (LR4), so the single-winner property is untouched — a claim lost during phase 2 matches zero rows here and the caller gets CONFLICT in the ordinary way.

It reports whether the refresh landed rather than erroring on a miss, because "the claim moved on" is an expected outcome the caller renders as CONFLICT, not a database failure.

func RegisteredVoteRules

func RegisteredVoteRules(db *sql.DB, projectID int) ([]string, error)

RegisteredVoteRules lists the rules that have a threshold set, so a refusal can name the alternatives rather than only the mistake.

func RegisteredVoteRulesTx

func RegisteredVoteRulesTx(tx *sql.Tx, projectID int) ([]string, error)

RegisteredVoteRulesTx lists the set rules — store-wide plus the project's own — so a refusal inside activation can name the alternatives exactly as `workflow register`'s does.

func ReleaseIssue

func ReleaseIssue(db *sql.DB, id int, token string, nowMS int64) (*model.Lease, error)

ReleaseIssue ends a live lease held by token.

attempt survives: it counts claims for all time, so releasing does not erase the trail of what has already been tried.

func ReleaseStepLeaseTx

func ReleaseStepLeaseTx(tx *sql.Tx, id int, nowMS int64) error

ReleaseStepLeaseTx returns a step to the UNCLAIMED pool without touching its status, its attempt, or its saga (DKT-259).

It exists because `pending` and a live lease are a CONTRADICTION that the system had no way to express and every reader disagreed about. `claimPredicate` says a step is claimable only when `owner IS NULL OR owner = ” OR expires_ms <= now`, so a step returned to `pending` with its lease intact is a step the scheduler offers and no claimant can take. What CAN still happen is the worst case: the ORIGINAL holder's token is still valid, so it re-records without re-claiming — and `attempt` increments only at claim, so the second execution lands on the first one's attempt number.

RUN-13 STEP-132 is what that costs. The step ran twice with two gate rounds and two artifact sets, `run report` said `attempts: 1`, and one execution's usage was permanently unrecordable because `usage_ledger`'s `UNIQUE(step_id, attempt, unit)` had already been taken by the other.

It is deliberately NARROWER than ReapStepTx: no status write, no `started_ms` clear. A reap decides what the step becomes; this decides only that nobody holds it, and leaves the caller to say the rest. Two callers with different intentions sharing one helper is how a release grows a status write that surprises one of them.

func RemoveLabelFromIssue

func RemoveLabelFromIssue(db *sql.DB, issueID int, labelName string, author string) error

RemoveLabelFromIssue detaches a label from an issue. Returns an error if the label is not found or is not attached to the issue. Activity is recorded and the issue's updated_at timestamp is touched.

func RemoveLabelsFromIssue

func RemoveLabelsFromIssue(db *sql.DB, issueID int, labelNames []string, author string) error

RemoveLabelsFromIssue detaches multiple labels from an issue atomically within a single transaction. Returns an error if any label is not found or not attached — no labels are removed on failure. Activity is recorded for each removed label and the issue's updated_at timestamp is touched once.

func RemoveRunIssue

func RemoveRunIssue(db *sql.DB, runID, issueID int) error

RemoveRunIssue detaches an issue from a run. The caller enforces WHEN this is legal (DKT-53: only while the run is in `planning` — after activation the issue is bound, snapshotted, and possibly scheduled, and a bare row delete would strand all three). Returns ErrNotFound when the issue was not attached, so a typo reads as a miss rather than a success.

func ReportedUsageTx

func ReportedUsageTx(tx *sql.Tx, runID int, unit string) (float64, error)

ReportedUsageTx sums the ledger for ONE unit — B16's `reported`.

One unit, never all of them. §4.5's whole argument is that summing `{tokens: 4000, seconds: 12}` to 4012 would be core asserting those add up.

func ResetStepRetryBudgetTx

func ResetStepRetryBudgetTx(tx *sql.Tx, id int, nowMS int64) error

ResetStepRetryBudgetTx refreshes a step instance's retry budget — `step resolve --as retry` (§2: "retry = attempts reset") — by moving `attempt_base` to the current attempt. Exhaustion compares `attempt - attempt_base` against `max_attempts`, so the budget reads zero-spent from here on.

`attempt` ITSELF IS NEVER RESET (DKT-86, DKT-90). It is the usage ledger's key half (`UNIQUE(step_id, attempt, unit)`) and §11.4's "claims made against this step, ever": zeroing it here made a retried step's next claim re-mint an attempt number the ledger had already recorded, and the re-execution's genuinely distinct usage became permanently unrecordable through `dispatch backfill-usage`. The retry budget and the ledger key are different facts, and the base column is what lets one column serve as neither's lie.

This is still a DIFFERENT counter from the issue-level `attempt` v6 declared monotonic, on a different entity, and claims-leases §5 anticipated exactly this: the step's budget is live against `max_attempts`, the issue's trail is permanent. Both statements stay true because they are about different rows.

func RestoreWorkflow

func RestoreWorkflow(db *sql.DB, projectID int, name string, version int) (*model.Workflow, error)

RestoreWorkflow clears a version's retirement, returning it to binding.

It exists because retirement is a routing decision and routing decisions get reversed. Without it the only way back would be to re-register the same name at a HIGHER version, which changes what runs pin for a change the operator did not intend to make.

func RetireStepTokenTx

func RetireStepTokenTx(tx *sql.Tx, id int) error

RetireStepTokenTx is §6.8 stage 1's hinge: the token retires when the artifact records.

Retirement is the same state change as release — no owner, no hash, no expiry — so it is the shared clearLeaseTx rather than a second UPDATE that must agree with it. What differs is the AUTHORITY: release is the holder giving the lease back, retirement is the engine taking ownership of a saga that no longer needs one. After this commits, `complete` is AUTH_ERROR (R9) and any engine invocation may resume the saga.

func RunEverDispatchedTx

func RunEverDispatchedTx(tx *sql.Tx, runID int) (bool, error)

RunEverDispatchedTx answers §5.8 D2's SCOPE as the 2026-08-03 review fixed it: has this run EVER opened a dispatch?

The question is deliberately about HISTORY rather than about an open manifest. A relay that ever dispatched is accountable for every step of that run, including the ones it spawned outside a manifest — which is the drift D6 exists to catch. A run no relay ever drove has nobody owing usage, which is what keeps a solo rehearsal and a human-only demo refusal-free.

It reads the `dispatches` table rather than the event log because the row outlives nothing: a dispatch that was opened and closed leaves its row, and the row is the cheaper and more direct record of the same fact.

func RunPauseOriginTx

func RunPauseOriginTx(tx *sql.Tx, id int) (model.RunPauseOrigin, error)

RunPauseOriginTx reads where a run's park was decided. A run that is not parked, and one parked by its own steps, both read `model.RunPauseOriginNone`.

func RunProjectID

func RunProjectID(conn *sql.DB, runID int) (int, error)

RunProjectID resolves the project a run belongs to — the engine's way into the dimension: an engine verb operates on a run or step it was handed, and the run row, not any ambient state, says whose project that work is.

func RunProjectIDTx

func RunProjectIDTx(tx *sql.Tx, runID int) (int, error)

RunProjectIDTx is RunProjectID inside a caller's transaction.

func SchemaVersion

func SchemaVersion(db *sql.DB) (int, error)

SchemaVersion returns the current schema version from the meta table.

func SetConfig

func SetConfig(db *sql.DB, projectID int, key, value string) error

SetConfig stores a validated engine-configuration value. A non-zero projectID writes the project's override; zero writes the store-wide default every project falls back to.

func SetIssueFiles

func SetIssueFiles(db *sql.DB, issueID int, filePaths []string, changedBy string) error

SetIssueFiles replaces all files for an issue (delete existing, insert new). Activity is recorded showing the change from old files to new files.

func SetIssueResolutionTx

func SetIssueResolutionTx(tx *sql.Tx, issueID int, resolution string) error

SetIssueResolutionTx records how a routing left an issue.

It touches ONLY `resolution`. `abandon-issue` is deliberate about not forcing the issue's status — the run stopping work is a statement about the run, and closing the issue here would take the operator's triage decision away — so the resolution is an additional fact beside the status, never a replacement for it (DKT-245). `updated_at` moves because the row changed; `version` does not, because this is the machine recording an outcome rather than a CAS-guarded edit competing with a caller's read.

func SetIssueScopeGlobs

func SetIssueScopeGlobs(db *sql.DB, issueID int, globsJSON string) error

SetIssueScopeGlobs stores an issue's declared scope as a JSON array, or NULL when the list is empty. NULL and `[]` are different facts — "no scope declared" versus "declared to touch nothing" — and only NULL is the dormant default every pre-existing row carries.

func SetProjectPrefix

func SetProjectPrefix(conn *sql.DB, id int, prefix string) error

SetProjectPrefix stores a project's display prefix, uppercased. Validation is the caller's (model.ValidateProjectPrefix); this only writes.

func SetRunBudgetTx

func SetRunBudgetTx(tx *sql.Tx, runID int, budget float64, ifVersion *int, nowMS int64) error

SetRunBudgetTx writes a run's cap — `docket run budget --set` (docs/tdd/events-follow.md §7.2).

IT ALWAYS BUMPS `row_version`, whether or not a precondition was given (B-7). Before this verb existed, operations.md §4's runbook told an operator to raise a cap with `sqlite3` and reminded them to increment the column by hand — "or a concurrent `--if-version` check will pass against a row that changed underneath it". Making that structural rather than a step somebody remembers is most of why the verb is better than the edit.

`ifVersion` is the optimistic-concurrency precondition (B-6): non-nil means the UPDATE also requires that version, and a mismatch affects zero rows and becomes ErrVersionConflict. That is the same shape every other CAS verb uses, so `--if-version` behaves identically here and there.

It runs in the CALLER'S transaction because the write and its event are one fact: a cap that moved with nothing in the log explaining why is exactly the gap operations.md §4 warned about when the manual edit was the only option.

func SetRunPauseOriginTx

func SetRunPauseOriginTx(tx *sql.Tx, id int, origin model.RunPauseOrigin) error

SetRunPauseOriginTx records WHERE a run's park was decided, and clears the record — `model.RunPauseOriginNone` — when the park ends (DKT-305).

It is written beside a status transition, never on its own: `origin` is only meaningful for a run that IS `waiting-human`, and a stale origin on a running run would make the rollup decline to resume a run nobody parked. Every caller therefore pairs a set with the pause it describes and a clear with the move that ends it, in the same transaction.

It does NOT bump `row_version`, for the same reason `CacheRunFloorTx` does not: the caller's own status write in this transaction already bumped it, and a second bump for one fact would make `--if-version` collide with itself.

func SetRunStatus

func SetRunStatus(db *sql.DB, id int, status model.RunStatus, reason string, nowMS int64) error

SetRunStatus moves a run's status and stamps `updated_at_ms`, bumping the CAS column. `reason` records why a run is parked or abandoned (engine-core §1.1); passing "" leaves the existing reason alone rather than clearing it, so a resume does not erase why the run was paused.

func SetRunStatusTx

func SetRunStatusTx(tx *sql.Tx, id int, status model.RunStatus, reason string, nowMS int64) error

SetRunStatusTx is SetRunStatus inside a caller's transaction, for verbs that must write the transition and its event atomically (DKT-86): a status write that commits while its event does not is a transition the ledger never saw.

func SetStepGateTrailTx

func SetStepGateTrailTx(tx *sql.Tx, id int, trail string, nowMS int64) error

SetStepGateTrailTx records the accumulated gate results (TDD §6.1's `gate_trail`, the recorded storage-location deviation: §11.4's `gate result` shape rides here until v8's `gate_results` table).

func SetStepMetadataTx

func SetStepMetadataTx(tx *sql.Tx, id int, metadata string, nowMS int64) error

SetStepMetadataTx records a step's opaque KV bag, already merged.

The merge itself is engine-side, in Go (engine.mergeMetadata), so this stays a plain assignment and no JSON function dependency enters the schema layer. The bag is OPAQUE here as everywhere: this writes bytes it never parses.

It follows SetStepRoutingTx's shape — same row_version bump, same updated_at_ms — because a metadata write is a step-row mutation like any other and CAS-guarded readers must see it move.

func SetStepRoutingTx

func SetStepRoutingTx(tx *sql.Tx, id int, routing, status string, nowMS int64) error

SetStepRoutingTx records the routing a step resolved to, alongside its final status. They are ONE statement because they are one fact — the step ended this way, for this reason — and a status without its routing is a step whose disposition cannot be explained.

func SetStepStatusTx

func SetStepStatusTx(tx *sql.Tx, id int, status string, nowMS, activityMS int64) error

SetStepStatusTx moves a step's status inside the caller's transaction, bumping the CAS column and refreshing `updated_at_ms`.

`activityMS` refreshes the saga's activity clock when non-zero — §6.8's "every stage commit refreshing the step's activity clock". Passing 0 leaves it alone, which is what a non-saga transition wants.

func SortedUnits

func SortedUnits[V any](byUnit map[string]V) []string

SortedUnits orders unit names for a deterministic rendering. It is here rather than at the call site so every renderer of an opaque-unit map orders it the same way (R9), whatever the map carries beside the names.

func SplitNameList

func SplitNameList(value string) []string

SplitNameList parses a KindNameList value into its names, ignoring the space an author may have written after a comma. An empty value yields no names, which is how every KindNameList key spells "unset".

It is the ONE reader of the encoding, so a caller can never disagree with the validator about where one name ends and the next begins.

func StartStepTx

func StartStepTx(tx *sql.Tx, id int, nowMS int64) error

StartStepTx stamps `started_ms` — the schedule-to-close clock `max_step_duration` is evaluated against (§6.3).

It is set at CLAIM, not at first heartbeat, and only when NULL. That is what makes the bound schedule-to-close rather than activity-to-close: a runaway holder that heartbeats forever cannot push its own deadline out, because the deadline was fixed the moment it took the lease. A re-claim after expiry re-stamps it (the new holder gets its own full budget); the `IS NULL` guard is against the same holder's later stages, not against a new attempt.

func StepAttemptsFor

func StepAttemptsFor(db *sql.DB, runID int) (map[int]int, error)

StepAttemptsFor reports each step's attempt count for the report's R3 line, keyed by step id.

func StepOffScheduler

func StepOffScheduler(status string) bool

StepOffScheduler reports whether a status takes a step OUT OF THE SCHEDULER'S ANSWER — a strictly wider question than StepTerminal's (DKT-65).

The two are different because a step's life can pause without ending. A `waiting-human` step is very much alive — an operator will approve, reject, or resolve it — but `next` does not offer it, so anything comparing a stored manifest against a fresh recomputation must expect it to be ABSENT rather than to have moved. `dispatch verify` used StepTerminal for that comparison and consequently reported

does not match its current rendering ... recomputed: (no row at this position)

with exit 4 for a step that had recorded correctly and legitimately parked.

THE ASK NAMED THREE STATUSES AND THE STEP MACHINE HAS ONE. `paused` is a RUN status and `held` is a SAGA STAGE (SagaHeld); neither is ever written to `steps.status`, whose persisted vocabulary is the nine constants above. A paused run removes its steps from the scheduler through their run, not through their own status, so this predicate covers what a step can actually say about itself and the run-level case stays the run's to answer.

It is a SEPARATE predicate rather than a widening of StepTerminal because the callers that ask "is this step over?" — reap, claim, re-offer — must keep getting `false` for `waiting-human`: a step waiting on a person is one an operator is still expected to act on.

func StepStatusCounts

func StepStatusCounts(db *sql.DB, runID int) ([]model.StatusCount, error)

StepStatusCounts returns a run's steps grouped by status, sorted by status so the rendering is stable.

func StepTerminal

func StepTerminal(status string) bool

StepTerminal reports whether a status ends a step's life. A terminal step is never re-offered by `next`, never claimable, and never reaped.

func UnlinkDocIssue

func UnlinkDocIssue(db *sql.DB, docID, issueID int) error

UnlinkDocIssue removes a doc↔issue link. Returns ErrNotFound if no such link exists.

func UnlinkProposalDoc

func UnlinkProposalDoc(db *sql.DB, proposalID, docID int) error

UnlinkProposalDoc removes a proposal↔doc link. Returns ErrNotFound if no such link exists.

func UnlinkProposalIssue

func UnlinkProposalIssue(db *sql.DB, proposalID, issueID int) error

UnlinkProposalIssue removes a link between a proposal and an issue. Returns ErrNotFound if the link does not exist.

func UpdateDoc

func UpdateDoc(db *sql.DB, id int, upd DocUpdate) (int, error)

UpdateDoc applies the changes in upd to the doc with the given ID and appends one revision row capturing the combined change_kind. Returns the new revision number (0 if no revision appended because nothing changed).

Per TDD §5.4 C8, every persisted field change appends one revision; the change_kind is comma-joined ("+" separator) for multi-field edits. Body equality uses strings.TrimRight(s, "\n") so trailing-newline-only edits are no-ops and do NOT append a revision (C6).

Returns ErrNotFound if id does not exist.

func UpdateIssue

func UpdateIssue(db *sql.DB, id int, updates map[string]interface{}, changedBy string) error

UpdateIssue updates an existing issue. Only keys present in the updates map are modified. The updated_at timestamp is always set to the current time. Activity is recorded for each changed field within the same transaction.

Field names are validated against validUpdateFields, but callers are responsible for validating field values (e.g. ensuring status/priority/kind are valid enums) before calling this function.

func UpdateIssueCAS

func UpdateIssueCAS(db *sql.DB, id int, updates map[string]interface{}, changedBy string, ifVersion *int) error

UpdateIssueCAS is UpdateIssue with an optional optimistic-concurrency precondition. When ifVersion is non-nil the update applies only if the issue's current version matches, and returns ErrVersionConflict otherwise. The version is bumped either way, so concurrent CAS writers are detected even when this caller did not supply a precondition.

func UpdateIssueCASLease

func UpdateIssueCASLease(db *sql.DB, id int, updates map[string]interface{}, changedBy string, ifVersion *int, token string) error

UpdateIssueCASLease is UpdateIssueCAS for a verb that ends a lease: when the issue carries a LIVE lease, the caller must hold it (token) and the lease is cleared as part of the same transaction — the issue-level analog of a step's token retiring when its artifact records (engine-spec.md §2).

When no live lease exists the token is ignored entirely and behavior is exactly UpdateIssueCAS's. That is the dormancy guarantee: an unclaimed issue is outside the lease mechanism, so a repo that never claims sees no change on any verb (engine-spec.md §9 item 8).

func UpdateIssueCASNote

func UpdateIssueCASNote(db *sql.DB, id int, updates map[string]interface{}, changedBy string, ifVersion *int, note string) error

UpdateIssueCASNote is UpdateIssueCAS with a note recorded as an issue comment in the SAME transaction as the update (DKT-480), so moving an issue and saying why is one verb: the two either both land or neither does, where the two-verb `issue comment add` + `issue move` form can leave a comment narrating a move that was refused.

An empty note is exactly UpdateIssueCAS, down to the writes it makes. A note with no field to update still records — a move to the status an issue already holds must not swallow the reason it was given — and does NOT bump the issue's version, since a comment never has.

func UsageBudgetUnitTx

func UsageBudgetUnitTx(tx *sql.Tx, projectID int) (string, error)

UsageBudgetUnitTx reads `budget.usage.unit` — the unit the MEASURED cap counts (DKT-238). Empty leaves that whole dimension dormant.

func ValidateConfigValue

func ValidateConfigValue(spec ConfigSpec, value string) error

ValidateConfigValue checks a value against its key's kind.

func ValidateNameList

func ValidateNameList(noun, value string) error

ValidateNameList checks a comma-separated list of opaque names: each entry's shape, no empty entry, and no duplicate.

`noun` names one entry in the refusal, for validateName's reason.

func ValidateUnitName

func ValidateUnitName(name string) error

ValidateUnitName is B36's shape rule for a unit name, shared by `--usage`'s parser and `docket config set budget.unit`.

`tokens`, `pages`, and `sheets` are equally valid: core has no list.

func VoteRuleCriticalityKey

func VoteRuleCriticalityKey(rule string) string

func VoteRuleExists

func VoteRuleExists(db *sql.DB, projectID int, rule string) (bool, error)

VoteRuleExists reports whether a rule is registered.

A rule EXISTS IFF its threshold is set (§8.3). Criticality has a default, so it cannot be the existence test: a rule with only a criticality set would tally at no threshold at all.

func VoteRuleExistsTx

func VoteRuleExistsTx(tx *sql.Tx, projectID int, rule string) (bool, error)

VoteRuleExistsTx and RegisteredVoteRulesTx are the two above inside a CALLER'S transaction, for S6's auto-registration (docs/tdd/runs-dispatch.md §9.2 F7/F8): the scan validates a workflow's `vote_rule` references with the SAME rules `workflow register` applies, and it does so inside activation's fat transaction, where a pool read would deadlock against the one-connection pool.

They read `meta` directly rather than going through GetConfig, because a vote rule's key is DYNAMIC (`vote.rule.<name>.threshold`) and has no spec row to look up — which is exactly what VoteRuleExists' own `Source == "set"` check is testing for. "Set" here means "a row exists", and that is one query.

func VoteRuleSetElsewhere

func VoteRuleSetElsewhere(
	db *sql.DB, projectID int, rule string,
) (projects int, value string, err error)

VoteRuleSetElsewhere counts how many OTHER projects configure a rule, and returns one of the values they use (DKT-264).

It exists because "vote_rule X is not registered" was true and useless in the one case that actually happens: a corpus workflow is shared across every project and its thresholds are not, so a freshly-registered project refuses a definition that works everywhere else in the store. The remedy is one store-wide set — the fallback for it has existed all along — and nothing said so, which is how thirteen projects came to hold thirteen per-project copies of the same three thresholds while `config.vote.rule.*` sat empty.

The VALUE is returned so the refusal can quote a real number instead of `<0-1>`. It is one of the values in use rather than a consensus: if two projects disagree, the operator is the one who decides which is right, and inventing an average would be core forming an opinion about a threshold.

The project's OWN row is excluded, so this answers "elsewhere" literally. A caller reaching this has already established the rule is unset here.

func VoteRuleSetElsewhereTx

func VoteRuleSetElsewhereTx(
	tx *sql.Tx, projectID int, rule string,
) (projects int, value string, err error)

VoteRuleSetElsewhereTx is VoteRuleSetElsewhere inside a caller's transaction, for activation's auto-registration — where a pool read would deadlock against the one-connection pool rather than fail.

func VoteRuleThresholdKey

func VoteRuleThresholdKey(rule string) string

VoteRuleThresholdKey and VoteRuleCriticalityKey build a rule's two keys, so the string concatenation lives in one place rather than at every reader.

Types

type AckReapResult

type AckReapResult int

AckReapResult is what an acknowledgment did, so the caller can tell A10's idempotent success from A9's forgery refusal without re-querying.

const (
	// AckRecorded is the first acknowledgment of a real reap.
	AckRecorded AckReapResult = iota
	// AckAlreadyDone is A10: a second ack of the same seq succeeds and changes
	// nothing, so a relay retrying its hook does not fail.
	AckAlreadyDone
	// AckNoSuchReap is A9: the seq names no unacknowledged-or-acknowledged reap
	// of this run. The caller raises VALIDATION_ERROR — an ack must name a real
	// reap, and this is the forgery point.
	AckNoSuchReap
)

func AckReapTx

func AckReapTx(tx *sql.Tx, runID int, seq int64, ackedBy string, nowMS int64) (AckReapResult, error)

AckReapTx is A7: CAS on `acked_at_ms IS NULL` for the row whose `reaped_seq` matches, SCOPED TO THE RUN.

The run scope is A9's other half and it is not incidental: an ack naming another run's reap is a forgery in exactly the way naming a non-reap is, and a CAS without the scope would let a relay clear a hold on a run it is not driving.

The three-way return is what makes A10 and A9 distinguishable. A CAS that matched zero rows means EITHER already-acknowledged OR no-such-reap, and those need opposite answers — one is a success, the other is a refusal — so the existence probe runs on the zero-rows path rather than being inferred.

type ActionResultRow

type ActionResultRow struct {
	ID     int
	RunID  int
	StepID int
	// Action is the `action` value — the builtin's name, or the trust entry
	// name a non-builtin looked up. Core carries it and never interprets it.
	Action     string
	Ordinal    int
	Argv       []string
	Exit       *int
	DurationMS int64
	Output     string
	Truncated  bool
	Verdict    string
	// Builtin marks a result core computed itself. It is the field that lets an
	// operator tell `aggregate` — which consults no trust store, by B1 — from a
	// user-trusted command that happened to succeed.
	Builtin bool
	// Reason explains an `unmatched` verdict, a refusal, or a failure, for the
	// same four-causes-one-verdict reason the gate row carries one.
	Reason      string
	CreatedAtMS int64
}

ActionResultRow is one recorded action execution — the `action result` shape of docs/tdd/payloads-thresholds.md §6.3, recorded as an amendment.

IT IS `GateResultRow` FOR ACTIONS, deliberately and field for field. The alternative — recording nothing and leaving the routing reason as the only trace — makes an `unmatched` action invisible in exactly the way gates-trust T11's audit argument says it must not be, and makes a failed computation indistinguishable from a failed threshold in a run report. Same shape, same ordinal semantics, same reason discipline, so `run report` (S6) reads one pattern twice rather than two patterns once.

Argv and Exit are POINTERS for the reason they are on a gate result: a builtin spawns nothing (§6.1 B2) and an unmatched action never ran (§6.2 A3), so NULL is the honest encoding of "no process existed". A zero exit on something that did not execute reads as success.

func ActionResultsForStep

func ActionResultsForStep(conn *sql.DB, stepID int) ([]ActionResultRow, error)

ActionResultsForStep returns every recorded result for a step, in insertion order.

type Artifact

type Artifact struct {
	ID     int
	RunID  int
	StepID int
	Kind   string
	Body   string
	// Payload is the structured half, validated for SHAPE only at S3 (§6.8
	// stage 0); the schema register is S5's. It is JSON text or "".
	Payload string
	SHA256  string
	// Stub is 1 ONLY on artifacts the S3/S4 stub action runner produced, and it
	// is `omitempty` for the reason gate_results' own marker is: a result THIS
	// stage produces serializes with no `stub` key at all (§6.3 S4).
	//
	// NOTHING WRITTEN FROM NOW ON SETS IT. The column exists so a migrated
	// artifact stays distinguishable forever — the migration marks the rows
	// whose payload carries the old `{"stub":true,…}` wrapper and leaves their
	// BYTES alone, because rewriting them would destroy the evidence that a
	// computation did not run.
	Stub bool `json:"stub,omitempty"`
	// Supersedes names the artifact this one REVISES, or nil for an original
	// (v15, DKT-70).
	//
	// A held cluster's resolution records a new artifact rather than annotating
	// the old one (H13). It shares the original's `kind`; its body is
	// regenerated from the resolved payload and its `sha256` covers body and
	// payload both (DKT-112), so two links of the chain never share a content
	// address while their payloads differ.
	//
	// The pointer says which of the two is the record of a computation and
	// which is the record of a decision, WITHOUT rewriting either. A consumer
	// counting work counts `Supersedes == nil`.
	Supersedes  *int `json:"supersedes,omitempty"`
	CreatedAtMS int64
}

Artifact is one `artifacts` row: what a step produced.

func ListRunArtifacts

func ListRunArtifacts(db *sql.DB, runID int) ([]*Artifact, error)

ListRunArtifacts reads a run's artifacts ordered by id — the tie-break §6.7's resolution order ends on, so the read order and the resolution order agree without a second sort.

func ListRunArtifactsTx

func ListRunArtifactsTx(tx *sql.Tx, runID int) ([]*Artifact, error)

ListRunArtifactsTx is ListRunArtifacts inside a transaction — context assembly's reader, which must see the artifact set as of the claim it is part of.

func ListStepArtifacts

func ListStepArtifacts(db *sql.DB, stepID int) ([]*Artifact, error)

ListStepArtifacts reads the artifacts ONE step produced, ordered by id.

The run-scoped reader above answers "what does this run hold", which is the question context assembly and the run report ask. This answers "what did this step produce", which is the question an operator reading a finished step asks — and which previously had no answer short of opening .docket/issues.db with sqlite.

`step_id` is nullable in the schema (a run-scoped artifact has none), so this deliberately matches on equality and never returns those: an artifact with no producing step is not this step's output.

type CastVoteResult

type CastVoteResult struct {
	Vote           *model.Vote
	ProposalStatus model.ProposalStatus
	VotesCast      int
	VotesRequired  int
	QuorumReached  bool
	WeightedScore  *float64
}

CastVoteResult holds the outcome of a CastVote operation, including whether quorum was reached and the proposal's updated status.

func CastVote

func CastVote(db *sql.DB, v *model.Vote) (*CastVoteResult, error)

CastVote inserts a vote and auto-finalizes the proposal when quorum is reached. Returns ErrNotFound if the proposal does not exist. Returns ErrConflict if the voter already voted or the proposal is already finalized.

type ConfigEntry

type ConfigEntry struct {
	Key    string `json:"key"`
	Value  string `json:"value"`
	Source string `json:"source"` // "set" or "default"
}

ConfigEntry is one key's effective value and where it came from.

func GetConfig

func GetConfig(db *sql.DB, projectID int, key string) (ConfigEntry, error)

GetConfig returns a key's effective value for one project, and whether it was explicitly set.

Resolution (v12): the project's own override, then the store-wide value, then the builtin default. A zero projectID reads the store-wide value directly. An unset key returns its default with source "default", so a caller can tell "nobody configured this" from "somebody configured this to the same value".

func GetConfigTx

func GetConfigTx(tx *sql.Tx, projectID int, key string) (ConfigEntry, error)

GetConfigTx is GetConfig inside a CALLER'S transaction, for readers that must resolve a key while holding the single pooled connection — the scheduler's snapshot, which is loaded entirely inside one transaction and would deadlock against a pool read rather than fail (the same constraint VoteRuleExistsTx was written for).

It resolves in GetConfig's order — the project's override, the store-wide value, the builtin default — by delegating the shape decisions to the SAME spec lookup and the same key builders. What it does not share is the per-class TTL fallback: nothing reads a lease TTL from inside a transaction, and a second implementation of a fallback is how two readers of one key start disagreeing. A caller that needs one reaches for LeaseTTL outside the transaction, as every current caller already does.

func ListConfig

func ListConfig(db *sql.DB, projectID int) ([]ConfigEntry, error)

ListConfig returns every fixed key's effective value for one project, plus every per-class TTL that has been explicitly set — store-wide or as a project override, the override winning. Sorted for deterministic output.

type ConfigSpec

type ConfigSpec struct {
	Key     string
	Kind    ConfigValueKind
	Default string
	Doc     string
}

ConfigSpec describes one engine-configuration key.

func LookupConfigSpec

func LookupConfigSpec(key string) (ConfigSpec, error)

LookupConfigSpec resolves a key to its spec. Per-class lease TTLs (lease.ttl.<class>) are matched dynamically: the class is an opaque string, so the set of valid keys is open by design.

type ConfigValueKind

type ConfigValueKind int

ConfigValueKind classifies a config key for validation at `set` time — so a bad value fails where the user can see it, not later at read time.

const (
	// KindDuration is a Go duration string ("15m", "2h30m").
	KindDuration ConfigValueKind = iota
	// KindPositiveInt is an integer >= 1.
	KindPositiveInt
	// KindNonNegativeNumber is a number >= 0.
	KindNonNegativeNumber
	// KindNonNegativeInt is an integer >= 0.
	KindNonNegativeInt
	// KindUnitFraction is a float in (0, 1] — a vote rule's approval
	// threshold, which the existing `vote create --threshold` already takes in
	// exactly that range.
	KindUnitFraction
	// KindCriticality is one of low|medium|high|critical, the criticality the
	// existing proposal machinery already understands.
	KindCriticality
	// KindRetentionWindow is a duration that may also be ZERO, where zero means
	// "retain everything" rather than "retain nothing"
	// (docs/tdd/events-follow.md §5.3 P13).
	//
	// It is a separate kind from KindDuration because that one rejects zero —
	// correctly, since a zero lease TTL would expire a claim the instant it was
	// made. Here zero is the DEFAULT and the safe end of the range: a retention
	// window nobody set must protect every event, not expose every event.
	KindRetentionWindow
	// KindUnitName is an OPAQUE unit name, or empty. Core never enumerates
	// units and never has a default one, so the only validation possible is the
	// shape a name must have to be usable as a ledger key — the same caps
	// `--usage` applies to the names it records (§4.9 B36): at most 64 bytes,
	// printable ASCII, no whitespace. Anything beyond that would be core
	// deciding which units exist.
	KindUnitName
	// KindName is a single OPAQUE name, or empty — KindUnitName's rule without
	// the unit vocabulary, for a key whose value names something other than a
	// recorded unit.
	KindName
	// KindNameList is a comma-separated list of OPAQUE names, or empty.
	//
	// It validates each entry's SHAPE and nothing else, for the same reason
	// KindUnitName does: core never enumerates the members of such a list and
	// holds no opinion about what any of them denotes. What it does check is
	// that the list can be split back into the names that were put in — no
	// empty entry, and no duplicate, since a caller that counts the entries
	// would count a repeat twice and demand a decision from a name that can
	// only make one.
	KindNameList
	// KindBool is a boolean, in any of strconv.ParseBool's spellings
	// ("true"/"false", "1"/"0", "t"/"f"). Stored and read back verbatim, like
	// every other kind — a reader that needs the parsed bool calls ParseBool
	// itself, the same way a reader of KindDuration calls ParseDuration.
	KindBool
)

type CycleError

type CycleError struct {
	Path []int
}

CycleError wraps ErrCycleDetected and carries the path of IDs forming the cycle.

func (*CycleError) Error

func (e *CycleError) Error() string

func (*CycleError) Unwrap

func (e *CycleError) Unwrap() error

type Dispatch

type Dispatch struct {
	ID     int
	RunID  int
	Status string
	// OpenedSeq is the event seq at open time — the manifest's place in the log,
	// and §6's boundary for "reaps this relay has not yet seen" (P2).
	OpenedSeq   int64
	ExpiresMS   int64
	ClosedAtMS  *int64
	CloseReason string
	CreatedAtMS int64
	RowVersion  int
}

Dispatch is one manifest's row.

func GetDispatchTx

func GetDispatchTx(tx *sql.Tx, id int) (*Dispatch, error)

GetDispatchTx reads one manifest by id, whatever its status — the read a closing verb makes after its CAS to report what actually happened.

func OpenDispatchTx

func OpenDispatchTx(tx *sql.Tx, runID int) (*Dispatch, error)

OpenDispatchTx reads the run's open manifest, or ErrNoOpenDispatch.

It is the probe P24 runs and the one D2 asks about, so it is ONE query with one definition of "open": the status the partial index keys on.

func (*Dispatch) Expired

func (d *Dispatch) Expired(nowMS int64) bool

Expired reports whether the manifest has outlived its TTL (P12).

It is a method on the row rather than a query so the ONE definition of expiry serves the lazy abandon, the refusal's message, and any read verb that renders a manifest — three callers that must not be able to disagree about whether a dispatch is still live.

type DispatchRow

type DispatchRow struct {
	Position  int
	StepID    int
	Instance  string
	RowJSON   string
	RowSHA256 string
}

DispatchRow is one stored manifest row: the canonical bytes, their hash, and the identity they describe.

Both `RowJSON` and `RowSHA256` are stored. `verify` derives its stageless comparison from `RowJSON`, first asserting the bytes still hash to `RowSHA256` — the integrity check on the stored pair — and the spawn guard compares proposed rows against `RowSHA256` verbatim, stage included. The bytes are what a refusal SHOWS the operator: the differing row rather than a report that two rows differ.

func ListDispatchRowsTx

func ListDispatchRowsTx(tx *sql.Tx, dispatchID int) ([]DispatchRow, error)

ListDispatchRowsTx reads a manifest's rows IN POSITION ORDER.

The order is the manifest (P1: "records the resulting rows in order"), so it is an ORDER BY rather than an insertion-order assumption: `verify` compares position by position, and a row set that came back in rowid order would make the comparison depend on how SQLite happened to store them.

type DocListOptions

type DocListOptions struct {
	ProjectID int // scope to one project (v12); 0 = every project
	Types     []string
	Statuses  []string
	Author    string
	Sort      string
	SortDir   string
	Limit     int
	Offset    int
}

DocListOptions holds filtering, sorting, and pagination options for ListDocs and ListDocsWithCounts. Mirrors ListOptions/issues but with the doc-table columns.

type DocSummary

type DocSummary struct {
	Doc             *model.Doc
	RevisionsCount  int
	CurrentRevision int
}

DocSummary is a row from ListDocsWithCounts: a Doc plus the JOIN-derived revision count and current revision number. Returned shape per TDD §6.3.

func ListDocsWithCounts

func ListDocsWithCounts(db *sql.DB, opts DocListOptions) ([]*DocSummary, int, error)

ListDocsWithCounts returns DocSummary rows including JOIN-derived revisions_count and current_revision in a single query (TDD §5.4 S1 — no N+1). Returns total count (before limit) as the second value.

type DocUpdate

type DocUpdate struct {
	Title  *string
	Body   *string
	Status *string
	Type   *string
	Author string
}

DocUpdate is the set of fields UpdateDoc may change. Nil pointers mean "leave unchanged"; non-nil with the current value is detected as a no-op (body equality additionally applies trailing-newline trimming — C6).

type GateResultRow

type GateResultRow struct {
	ID         int
	RunID      int
	StepID     int
	Gate       string
	Ordinal    int
	Argv       []string
	Exit       *int
	DurationMS int64
	Output     string
	Truncated  bool
	Verdict    string
	Pre        bool
	// Stub is 1 ONLY on rows migrated from an S3 `gate_trail`. Nothing this
	// stage records sets it, which is what makes the S3->S4 window auditable
	// after the fact.
	Stub bool
	// StubEntry is 1 when the trust entry that authorized this gate declared
	// itself a PLACEHOLDER (DKT-265) — an echo, a `/usr/bin/true`, a script
	// that exits 0 without looking at anything.
	//
	// IT IS A DIFFERENT FACT FROM `Stub` AND THE NAMES ARE NOT AN ACCIDENT.
	// `Stub` is about WHICH ERA of this codebase produced the row; `StubEntry`
	// is about WHAT RAN. A row can be either, both, or neither, and a reader
	// asking "did a secret scan actually happen" is asking the second question
	// only.
	StubEntry bool
	// Reason explains an `unmatched` verdict or a timeout (§6.3, amendment A6).
	// An unmatched verdict has four distinct causes needing four different
	// remedies, and without this field they render identically to an operator.
	Reason      string
	CreatedAtMS int64
}

GateResultRow is one recorded gate execution — §11.4's `gate result` shape as it lives in the v8 table (TDD docs/tdd/gates-trust.md §4.2, §6.1).

Argv and Exit are POINTERS because NULL is meaningful here and zero is not: an `unmatched` gate never ran, so it has no argv and no exit code. Recording `exit = 0` for a process that does not exist is exactly the confusion the gate-forgery threat (T11) exists to prevent — a zero exit reads as success. NULL is the honest encoding of "no process existed".

func GateResultsForStep

func GateResultsForStep(conn *sql.DB, stepID int) ([]GateResultRow, error)

GateResultsForStep returns every recorded result for a step, in insertion order.

func GateResultsForStepTx

func GateResultsForStepTx(tx *sql.Tx, stepID int) ([]GateResultRow, error)

GateResultsForStepTx is GateResultsForStep inside a transaction, for the readers that run within one.

type ListOptions

type ListOptions struct {
	ProjectID  int      // scope to one project (v12); 0 = every project
	Statuses   []string // filter by status (multiple = OR)
	Priorities []string // filter by priority (multiple = OR)
	Labels     []string // filter by label name (multiple = AND)
	Types      []string // filter by kind (multiple = OR)
	Assignee   string   // filter by assignee
	ParentID   *int     // filter by parent issue ID
	RootsOnly  bool     // only issues with no parent
	// RunID scopes the listing to one run's ROSTER — the issues bound to it in
	// `run_issues` (DKT-405); 0 = every issue. It is the same membership
	// ListRunIssues reads, expressed as a filter so it composes with the other
	// filters, the sort, and the pre-limit COUNT rather than being intersected
	// afterwards in the caller.
	RunID       int
	IncludeDone bool   // include done status (default: exclude)
	Sort        string // field name
	SortDir     string // "asc" or "desc"
	Limit       int    // max results
	Offset      int    // for pagination
}

ListOptions holds filtering, sorting, and pagination options for ListIssues.

type MetadataKeyRollup

type MetadataKeyRollup struct {
	Key    string               `json:"key"`
	Values []MetadataValueCount `json:"values"`
}

MetadataKeyRollup is one metadata key and every distinct value recorded under it, with counts.

func MetadataRollup

func MetadataRollup(db *sql.DB, runID int) ([]MetadataKeyRollup, error)

MetadataRollup is R7: step `metadata` keys rolled up to their distinct values with counts, VERBATIM AND UNINTERPRETED.

THIS IS THE GENERICITY LINE AT ITS THINNEST, so it is worth stating what the implementation does and does not do. It groups by key and by value, both as OPAQUE STRINGS, and reports counts. It does not know that any particular instance puts anything in particular there: it reports `{"tier": {"a": 3, "b": 1}}` for exactly the same reason it would report `{"desk": {"front": 3, "back": 1}}`.

TestMetadataRollupReadsNoKey asserts the implementation contains no key-name literal, which is the mechanical form of that promise — a rollup that special-cased one key would be core having an opinion about what a workflow author's bag of strings means.

The nesting is key -> value -> count, and both levels are returned SORTED so R9's determinism holds through a two-level structure that a naive implementation would emit in map order.

func VoteMetadataRollup

func VoteMetadataRollup(db *sql.DB, scope, prefix string) ([]MetadataKeyRollup, error)

VoteMetadataRollup is MetadataRollup over CAST VOTES: every metadata bag the run's vote-step proposals collected, keys to distinct values with counts, verbatim and uninterpreted (DKT-71).

It exists because vote seats are the one spend the ledger cannot see: a vote step is never claimed, so nothing accrues usage rows for it, and until v13 nothing recorded what model a seat resolved to. The `--metadata` claim on `vote cast` closed the write half; this is the run-level read that makes routing drift measurable again — the same question the step rollup answers, asked of the casts.

The proposals are selected by the caller-supplied idempotency scope and prefix — the engine owns that spelling (voteIdempotencyPrefix) and this package must not restate it. The same genericity line holds here as in MetadataRollup: keys and values are opaque strings, counted, never interpreted, and TestMetadataRollupReadsNoKey covers both readers.

type MetadataValueCount

type MetadataValueCount struct {
	Value string `json:"value"`
	Count int    `json:"count"`
}

MetadataValueCount is one value of one key, and how many steps carried it.

type Pin

type Pin struct {
	RunID  int
	Kind   string
	Ref    string
	SHA256 string
	// Bytes is the pinned content's size, carried IN MEMORY ONLY during
	// activation — it is not a column and is not persisted.
	//
	// It exists so the closure-size arithmetic (§1.5) can count a
	// declared packet file without opening it a second time: the activation
	// scan already read the bytes to hash them, and re-reading at expansion
	// would both cost an extra pass and open a window where the two reads
	// disagree.
	Bytes int
}

Pin is one `pins` row.

func ListPins

func ListPins(db *sql.DB, runID int) ([]Pin, error)

ListPins returns a run's pins, ordered so the set is stable across reads — `context.pins` is golden-diffed byte for byte (§8.3), so an unordered read would make the goldens flap.

func ListPinsTx

func ListPinsTx(tx *sql.Tx, runID int) ([]Pin, error)

ListPinsTx is ListPins inside a transaction — RA2's reader. Re-activation INHERITS the pin set rather than recomputing it, so an in-flight run cannot silently adopt an edited workflow (engine-core §4).

type ReapAck

type ReapAck struct {
	ID       int
	RunID    int
	StepID   int
	Class    string
	Instance string
	// ReapedSeq is the `seq` of the `lease-reaped` event this ack is OF. §2 says
	// the relay acknowledges "the `reaped` event", so the event's seq is the
	// ack's identity — which makes the ack idempotent for free (C7) and a forged
	// ack impossible to construct without naming a real reap.
	ReapedSeq   int64
	AckedAtMS   *int64
	AckedBy     string
	CreatedAtMS int64
}

ReapAck is one unacknowledged-or-acknowledged reap.

func UnacknowledgedReapsTx

func UnacknowledgedReapsTx(tx *sql.Tx, runID int) ([]ReapAck, error)

UnacknowledgedReapsTx lists a run's outstanding reaps, oldest first.

It is the query behind A12's predicate AND behind the human-readable reason `next` prints, so the two cannot name different rows: a headroom denial with nothing running is baffling unless the same rows that caused it are the ones reported.

The `instance` join is for the MESSAGE. The predicate needs only the class, but a refusal that named `STEP-7` rather than `implement@0` would make an operator look the step up to understand a sentence about their own run.

type ResultTrailRow

type ResultTrailRow struct {
	Instance string `json:"step"`
	StepID   string `json:"step_id"`
	Issue    string `json:"issue"`
	Name     string `json:"name"`
	Ordinal  int    `json:"ordinal"`
	Verdict  string `json:"verdict"`
	Reason   string `json:"reason,omitempty"`
	Output   string `json:"output,omitempty"`
}

ResultTrailRow is one step's one result — the per-step trail R4 and R5 carry beside their counts.

StepID and Issue joined on (DKT-77): instance names collide across issues in one run — two issues running the same workflow both have an `implement@0` — so a trail keyed on instance alone was unattributable. Output rides ONLY on rows that did not pass, as a bounded tail: a failing gate's diagnosis used to require re-running it out-of-band, which is at its most expensive exactly where a false failure blocks a security path.

func ActionTrail

func ActionTrail(db *sql.DB, runID int) ([]ResultTrailRow, error)

func GateTrail

func GateTrail(db *sql.DB, runID int) ([]ResultTrailRow, error)

GateTrail is R4's per-step trail, and ActionTrail is R5's.

type RunBudgetFacts

type RunBudgetFacts struct {
	CachedFloor  float64
	BreachReason string
}

RunBudgetFacts are the stored budget columns a read verb renders: the cached floor and the breach reason, if any.

func RunBudgetFactsFor

func RunBudgetFactsFor(db *sql.DB, runID int) (RunBudgetFacts, error)

RunBudgetFactsFor reads the v10 columns for one run.

func RunBudgetFactsTx

func RunBudgetFactsTx(tx *sql.Tx, runID int) (RunBudgetFacts, error)

RunBudgetFactsTx is RunBudgetFactsFor inside a caller's transaction, for the one writer that must read the breach record while deciding whether a cap change resolved it (DKT-80). The pool is capped at one connection, so a pool read from inside a transaction deadlocks rather than merely racing.

type RunContext

type RunContext struct {
	ExecRoot  string
	Branch    string
	CommitSHA string
	Hostname  string
	// UsageBudget is the cap over MEASURED usage (DKT-238), resolved at
	// `run start` from `--usage-budget` or `budget.usage.default` and pinned
	// on the row for the same reason `budget` is: a config change must not
	// re-cap a live run.
	UsageBudget float64
}

RunContext is the execution context stamped on a run at creation (G8): which checkout, on which branch and commit, on which machine. Empty fields record as empty — a run started outside a checkout has no branch, and inventing one would make the record an opinion.

type RunFence

type RunFence struct {
	RunID   int
	IssueID int
	Tag     string
	Ordinal int
	Command string
	SHA256  string
}

RunFence is one harvested fenced command (TDD §5.1).

func ListFences

func ListFences(db *sql.DB, runID int) ([]RunFence, error)

ListFences returns a run's harvested commands in a stable order.

type RunIssue

type RunIssue struct {
	RunID         int
	IssueID       int
	WorkflowID    *int
	BodySnapshot  string
	BodySHA256    string
	IssueSnapshot string
	ExpandedAtMS  *int64
	// LoopCount is the ISSUE's loop counter (§11.3 (1), TDD §7.1). It lives here
	// rather than on `steps` because the spec says "the issue's loop counter":
	// one issue loops, and `max_fix_loops` bounds that one number.
	LoopCount int
}

RunIssue is one `run_issues` row: an issue's membership in a run, its binding, and the activation-time snapshots the context bundle reads.

func GetRunIssueTx

func GetRunIssueTx(tx *sql.Tx, runID, issueID int) (*RunIssue, error)

GetRunIssueTx reads one `run_issues` row — the issue's binding, snapshots, and loop counter — inside a transaction.

The loop routing needs the counter under the SAME lock that increments it, so this reads inside the caller's transaction rather than through a *sql.DB helper. (It also must: internal/db caps the pool at one connection, so a pool read from inside a transaction deadlocks — the failure TestNoPoolReadsInsideTransactions exists to prevent.)

func ListRunIssues

func ListRunIssues(db *sql.DB, runID int) ([]*RunIssue, error)

ListRunIssues returns a run's issues in ascending issue order, so every caller walks them the same way and two activations of the same inputs agree.

func ListRunIssuesTx

func ListRunIssuesTx(tx *sql.Tx, runID int) ([]*RunIssue, error)

ListRunIssuesTx is ListRunIssues inside a transaction.

func (*RunIssue) Expanded

func (ri *RunIssue) Expanded() bool

Expanded reports whether this issue's phase has been expanded. Re-activation expands ONLY issues for which this is false (RA1), which is what makes expansion idempotent and re-entrant.

type RunListOptions

type RunListOptions struct {
	// ProjectID scopes the list to one project (v12); 0 = every project.
	ProjectID int
	// ActiveOnly restricts the list to runs that are not terminal — the
	// `--active` flag. `planning` counts as active: a run that exists but has
	// not been activated is still live work an operator is mid-way through.
	ActiveOnly bool
	Limit      int
}

RunListOptions filters `run status` without an id.

type SchemaListOptions

type SchemaListOptions struct {
	// ProjectID scopes the list to one project's visible schemas — its own
	// plus builtins (v12); 0 = every project's.
	ProjectID int
	Name      string
	Limit     int
}

SchemaListOptions filters `schema list`.

type Step

type Step struct {
	ID           int
	RunID        int
	IssueID      int
	WorkflowID   int
	StepName     string
	Ordinal      int
	SiblingIndex *int
	Instance     string
	Kind         string
	Executor     string
	Class        string
	Status       string
	Attempt      int
	// AttemptBase is the attempt count the retry budget counts FROM (v16,
	// DKT-86/DKT-90). `attempt` is monotonic for the step's whole life — it is
	// the usage ledger's key half and §11.4's "claims made against this step,
	// ever" — so `step resolve --as retry` refreshes the budget by moving this
	// base to the current attempt instead of zeroing the counter. Exhaustion
	// compares Attempt-AttemptBase against MaxAttempts.
	AttemptBase int
	// FailedAttempts and ReapedClaims are the OUTCOME breakdown of the claims
	// Attempt counts (v23, DKT-490): how many ended in an explicit `step fail`,
	// and how many were reaped without one (lease expiry, `max_step_duration`,
	// `step reap`). Attempt alone cannot answer that — it spends one count per
	// claim whatever the ending — and a consumer that read it as "attempts
	// that failed" escalated on claims that never failed at all. A claim that
	// RECORDED counts in neither, and `step resolve --as retry` touches
	// neither. FailedAttempts+ReapedClaims never exceeds Attempt; the
	// remainder is live claims, recorded completions, and pre-v23 history
	// (the migration back-fills nothing — see migrateV22ToV23).
	FailedAttempts int
	ReapedClaims   int
	MaxAttempts    *int
	ExpectedCost   float64
	Owner          string
	TokenHash      string
	ExpiresMS      int64
	StartedMS      *int64
	ActivityMS     *int64
	SagaStage      string
	GateTrail      string
	Routing        string
	Metadata       string
	ContextBytes   int
	// Materialized reports a step the ENGINE minted rather than one the pinned
	// definition declares — the `<step>-held` human step a tripped `hold_spread`
	// creates (payloads-thresholds §7.7 H4). Its spec is synthesized from the
	// pinned bytes, so nothing unpinned enters a run; the column exists so a
	// reader can tell a declared question from a computed one.
	Materialized bool
	// UsageRecorded is the v10 column group 1 writes on every `--usage` and
	// group 2's discrepancy probe reads (§2.3, §5.8 D2). It is the LEDGER'S OWN
	// answer to "did this step report", written in the recording transaction, so
	// the probe needs no join per step.
	UsageRecorded bool
	// WorkRoot is the checkout the step's work happened in — recorded at
	// complete/record via --worktree (v12, G8), read by the diff stage so a
	// resumed saga diffs the tree the work touched, not the resumer's cwd.
	WorkRoot    string
	CreatedAtMS int64
	UpdatedAtMS int64
	RowVersion  int
}

Step is one `steps` row, read whole. It is a superset of StepRow — which activation writes — because phase 3 reads the lease, saga, and routing columns activation never fills.

func GetStep

func GetStep(db *sql.DB, id int) (*Step, error)

GetStep reads one step by id.

func GetStepTx

func GetStepTx(tx *sql.Tx, id int) (*Step, error)

GetStepTx is GetStep inside a transaction — every saga stage's reader, so the stage's guard and its mutation see one consistent row.

func ListActiveRunSteps

func ListActiveRunSteps(db *sql.DB) ([]*Step, error)

ListActiveRunSteps reads the steps of every non-terminal run — `guard stop`'s reader (§6.12) and the scope-conflict check's, since a claimed step in ANOTHER active run excludes just as surely as one in this run.

func ListRunSteps

func ListRunSteps(db *sql.DB, runID int) ([]*Step, error)

ListRunSteps reads every step of a run, ordered by (issue, id) — creation order within an issue, which is declaration order, which is what the topology goldens compare against (§8.3).

func ListRunStepsTx

func ListRunStepsTx(tx *sql.Tx, runID int) ([]*Step, error)

ListRunStepsTx is ListRunSteps inside a transaction — the readiness predicate's reader, which must see one consistent snapshot of the run because R3 (predecessors done) and R4 (scope non-overlap) are questions about the same set of rows at the same instant.

func (*Step) InSaga

func (s *Step) InSaga() bool

InSaga reports whether the step is mid-saga — recorded but not yet routed. Such a step needs NO lease to advance: the token retired at stage 1, so any later engine invocation may resume it (§6.8).

func (*Step) Lease

func (s *Step) Lease() *model.Lease

Lease returns the step's lease, for the same effective-status computation issues use. A step and an issue carry the same lease columns by contract (TDD §2), so they share the model type as well as the SQL.

func (*Step) Ref

func (s *Step) Ref() string

Ref renders the step's `STEP-N` display identity.

type StepRow

type StepRow struct {
	ID           int
	RunID        int
	IssueID      int
	WorkflowID   int
	StepName     string
	Ordinal      int
	SiblingIndex *int
	Instance     string
	Kind         string
	Executor     string
	Class        string
	Status       string
	MaxAttempts  *int
	ExpectedCost float64
	Metadata     string
	ContextBytes int
	// Materialized marks a step the engine minted rather than one expansion
	// read out of the pinned definition (payloads-thresholds §7.7 H4). Ordinary
	// expansion never sets it, so every row activation writes reads 0.
	Materialized bool
	CreatedAtMS  int64
}

StepRow is one `steps` row as activation writes it. Phase 3 adds the lease, saga, and routing readers; expansion fills only the identity, the shape, and the initial status.

func ListSteps

func ListSteps(db *sql.DB, runID int) ([]*StepRow, error)

ListSteps returns a run's steps ordered by (issue, id) — creation order within an issue, which is declaration order, which is what the topology goldens compare against.

type StepUsageRow

type StepUsageRow struct {
	Step     string  `json:"step"`
	Instance string  `json:"instance"`
	Attempt  int     `json:"attempt"`
	Unit     string  `json:"unit"`
	Quantity float64 `json:"quantity"`
	Source   string  `json:"source"`
}

StepUsageRow is one row of the per-step ledger view: which step, which attempt, which unit, how much, and who measured it.

func UsageByStep

func UsageByStep(db *sql.DB, runID int) ([]StepUsageRow, error)

UsageByStep lists the run's ledger row by row, joined to each step's instance, ordered by (step, attempt, unit).

It exists because nothing exposed per-step usage. `run report` rolled the ledger up per UNIT only, so the one question a back-fill's refusal sends you to answer — WHICH steps already have usage recorded — could not be answered from any read verb, and conductors hand-filtered batches by trial and error (DKT-241). The rollup is still the headline; this is the detail behind it.

ORDERED BY A TOTAL KEY (R9), like UsageByUnit and for the same reason: two reports of the same rows must be byte-identical.

type UnitTotal

type UnitTotal struct {
	Unit     string  `json:"unit"`
	Quantity float64 `json:"quantity"`
	Rows     int     `json:"rows"`
}

UnitTotal is one unit's rollup for the report (R2's reported-per-unit line).

func UsageByUnit

func UsageByUnit(db *sql.DB, runID int) ([]UnitTotal, error)

UsageByUnit rolls the run's ledger up per unit, ordered by unit name.

ORDERED BY A TOTAL KEY, never by map iteration (R9): the report is deterministic given the same rows, for the same golden-stability reason `referencedSchemas` is ordered.

func VoteUsageRollup

func VoteUsageRollup(db *sql.DB, scope, prefix string) ([]UnitTotal, error)

VoteUsageRollup sums the vote_usage ledger per unit over the run's vote-step proposals (DKT-95) — UsageByUnit's question, asked of the seats. The proposals are selected by the same caller-supplied idempotency scope and prefix VoteMetadataRollup reads by, and units stay opaque: summed and counted, never interpreted.

type UsageRow

type UsageRow struct {
	RunID    int
	StepID   int
	Attempt  int
	Unit     string
	Quantity float64
	// Source is engine-core §7's "source recorded". Core writes
	// UsageSourceReported and nothing else at this stage; the column exists so a
	// harness back-filling from its own journal can record its own source later
	// without a migration.
	Source string
}

UsageRow is one `usage_ledger` row: what a step reported, in one unit, on one attempt.

func ParseUsage

func ParseUsage(raw string) ([]UsageRow, error)

ParseUsage is B33, B35, and B36: `--usage '{"unit": n, …}'` parsed as a JSON object of string -> number, capped, with every refusal NAMING what it refused. It is the ONE implementation of the usage-report contract: the step ledger's writer (`step complete --usage`, via the engine) and the vote ledger's (`vote cast --usage`, DKT-95) both parse through it, so the two flags cannot drift on what a valid report is.

It returns the units in SORTED ORDER so a call's ledger rows land in a deterministic order — the same total-key discipline the report's rendering uses (R9), applied at the writer so two identical calls produce identical row ids.

Refusals are plain errors naming only the offending key or value, never a flag: each caller prefixes its own surface's name and shape (the engine as a VALIDATION_ERROR on `--usage`, the CLI as a `--usage` flag refusal), so this package never authors wording for a surface it cannot see.

type VerdictCount

type VerdictCount struct {
	Name      string `json:"name"`
	Pass      int    `json:"pass"`
	Fail      int    `json:"fail"`
	Unmatched int    `json:"unmatched"`
	// Skipped counts rows that MEASURED NOTHING (DKT-254). Before this column
	// a skipped row landed in none of the three above and vanished from the
	// report entirely — an absence that reads as green, which is the exact
	// inversion of what the verdict means.
	//
	// It partitions with pass/fail/unmatched: every row lands in exactly one
	// of the four. (`Stub` below does not — see its own note.)
	Skipped int `json:"skipped"`
	// Stub counts rows whose authorizing trust entry declared itself a
	// PLACEHOLDER (DKT-265). It is a count of ROWS, not of passes, so it
	// overlaps the three columns above rather than partitioning with them —
	// `pass 3, stub 3` means every one of those passes was hollow.
	//
	// It is zero for actions, which have no trust-entry stub declaration. That
	// is an honest zero and not a missing feature: an action's `builtin` column
	// already says whether core computed it.
	Stub int `json:"stub"`
}

VerdictCount is one subject's pass/fail/unmatched tally.

func ActionRollup

func ActionRollup(db *sql.DB, runID int) ([]VerdictCount, error)

ActionRollup is the same rollup over `action_results` (R5).

The same shape, deliberately: gates and actions are the two execution seams and their results carry the same verdict vocabulary, so a reader who understands one section understands the other. Reusing the query rather than writing a second is what keeps the two from drifting into different definitions of "pass".

`skipped` is counted for actions too, and is always zero there today: no action path produces that verdict. That is an honest zero and the right shape — the alternative, omitting the column for one of the two seams, is how the two definitions of "what outcomes exist" start to drift.

func GateRollup

func GateRollup(db *sql.DB, runID int) ([]VerdictCount, error)

GateRollup counts a run's gate results per gate name (R4).

type VoteUsageCoverage

type VoteUsageCoverage struct {
	// Casts is every vote cast on this run's vote-step proposals.
	Casts int `json:"casts"`
	// Reported is the subset that recorded at least one vote_usage row.
	Reported int `json:"reported"`
}

VoteUsageCoverage is how many of a run's seat-casts reported their spend, and how many did not (DKT-257).

func VoteUsageCoverageFor

func VoteUsageCoverageFor(db *sql.DB, scope, prefix string) (VoteUsageCoverage, error)

VoteUsageCoverageFor counts a run's seat-casts and how many reported spend.

It exists because a MISSING report and a FREE panel were the same number (DKT-257). The vote_usage ledger has existed since v14 and held zero rows for an entire store epoch while 21+ seat-votes did real verification work: the table, the writer, and the rollup were all wired, and nothing supplied a number, so `vote_usage: 0` read as "this panel cost nothing".

In-wave panels ARE counted, because their seats run as steps — RUN-22 STEP-379/381 journaled ~39.5k output tokens to step_usage. Conductor-side panels of identical shape recorded 287/0. Roughly 40k tokens per panel that the run's budget never saw, and the only difference was WHERE the ballot executed, not how much work it did.

Core cannot observe a conductor-side seat's spend, so it cannot fix the number. What it can do is stop the silence from looking like a zero, which is this: a run whose seats all reported reads `12/12`, and one whose seats reported nothing reads `0/12` instead of an absent section.

func (VoteUsageCoverage) Silent

func (c VoteUsageCoverage) Silent() int

Silent is the gap: seats that deliberated and reported nothing.

type WorkflowListOptions

type WorkflowListOptions struct {
	// ProjectID scopes the list to one project (v12); 0 = every project.
	ProjectID int
	Name      string
	Limit     int
}

WorkflowListOptions filters `workflow list`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL