db

package
v0.0.0-...-795fd7f Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 16 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.

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

Trust-cache kinds. `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 the lazy auto-abandon: the manifest outlived
	// `dispatch.ttl` and `next` retired it.
	CloseReasonTTL = "ttl"
	// CloseReasonReconciled is the ordinary `dispatch close`.
	CloseReasonReconciled = "reconciled"
	// CloseReasonAcceptedMissingUsage: closed over missing-usage
	// discrepancies, with the acceptance RECORDED rather than implied.
	CloseReasonAcceptedMissingUsage = "accepted-missing-usage"
	// CloseReasonOperator is `dispatch abandon` driven by a person.
	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.

View Source
const (
	// AckByGuardSpawn is `guard spawn --ack-reap`.
	AckByGuardSpawn = "guard-spawn"
	// AckByDispatchOpen is `dispatch open --ack-reap`.
	AckByDispatchOpen = "dispatch-open"
	// AckByDispatchClose is a reconciled `dispatch close` acknowledging a
	// NON-FORCED reap of a claim admitted under that same dispatch
	// (DKT-3286). Abandon, forced reaps, and claims from another dispatch keep
	// the hold.
	AckByDispatchClose = "dispatch-close"
)

Acknowledgers — a closed set. 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.
	KeyLeaseTTLPrefix = "lease.ttl."
	// KeyClassMaxPrefix is the per-class concurrency cap namespace:
	// limits.max.<class>. A set value replaces the pinned workflows' merged
	// `[limits]` max for that class. The scheduler reads it on every snapshot,
	// never pins it, so a run moved to a different host follows that host's
	// value. Unset leaves the pinned max in force.
	KeyClassMaxPrefix = "limits.max."
	// KeyAttemptMax caps retries per entity (`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. 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, 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; 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.
	//
	// It is engine-side rather than instance policy for the reason that holds
	// 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.
	//
	// 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.
	//
	// It is the "artifact-retention boundary", implemented as the one window a
	// future artifact GC would also read. Prune and "artifact GC per
	// run-retention config" are one lifecycle, and no artifact GC ships — 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 DELIBERATE, and it carries a known objection: a key
	// spelled `events.retain` governing only events is not what
	// "artifact-retention" names.
	//
	// The default is "0", which imposes NO RETENTION WINDOW: prune at 0 is
	// bounded only by its `--before` or `--before-run` target and the live-run
	// refusal, not by age. That is the dormant posture — nothing is deleted
	// until an operator runs prune with a target.
	KeyEventsRetain = "events.retain"

	// KeyVoteRulePrefix is the named-threshold-configuration namespace:
	// vote.rule.<name>.threshold, vote.rule.<name>.criticality,
	// vote.rule.<name>.sealed and vote.rule.<name>.hold_on_dissent.
	//
	// 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, KeyVoteRuleCriticalitySuffix,
	// KeyVoteRuleSealedSuffix and KeyVoteRuleHoldOnDissentSuffix complete a
	// rule's four keys.
	//
	// `.sealed` is opt-in and defaults to false: a sealed rule opens proposals
	// whose casts the read verbs withhold (verdict, weights, findings, summary)
	// until the tally closes the proposal, so a seat reading the ballot cannot
	// anchor on a sibling's verdict. It is a RENDERING rule only — the tally
	// and the one-cast-per-voter constraint never consult it.
	//
	// `.hold_on_dissent` is opt-in and defaults to false (DKT-2449): under a
	// keyed rule an APPROVED tally that carries at least one `reject` parks
	// the vote step for the operator instead of passing. The tally itself is
	// untouched — the weighted mean is still db.CastVote's — and the park is
	// strictly additive: it displaces a `pass` and never a fail route.
	KeyVoteRuleThresholdSuffix     = ".threshold"
	KeyVoteRuleCriticalitySuffix   = ".criticality"
	KeyVoteRuleSealedSuffix        = ".sealed"
	KeyVoteRuleHoldOnDissentSuffix = ".hold_on_dissent"

	// 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"
	// KeyVoteHoldCost is the declared `expected_cost` a MATERIALIZED HELD vote
	// step is minted with (DKT-584). An engine-minted held ballot has no
	// `[[step]]` table to declare a cost in, so it carried 0 by construction —
	// and since a vote step's declared cost accrues to the budget floor at
	// materialization, that made every held panel invisible to the floor.
	//
	// Default "0", which is EXACTLY the prior behavior: a held ballot accrues
	// nothing until an instance states what its panels cost. It applies only
	// when a hold is minted as `vote` (both vote.hold.* keys set); a hold
	// minted `human` is one operator's decision, not a panel's spend.
	KeyVoteHoldCost = "vote.hold.cost"

	// KeyRoutingSecurityLabel names the label that sends an issue down the
	// security review track, and KeyRoutingSecurityLexicon the terms that make
	// an UNLABELLED issue look like it belongs there (DKT-4275). `run activate
	// --dry-run` lints against both; nothing else reads them, and neither
	// changes routing. The lexicon ships empty because the engine attaches no
	// domain meaning to a word: the instance that knows its own security
	// surface supplies the vocabulary. An empty label turns the lint off.
	KeyRoutingSecurityLabel   = "routing.security_label"
	KeyRoutingSecurityLexicon = "routing.security_lexicon"

	// KeyAutoRegister toggles 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 it 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: 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.

View Source
const (
	ScopeIssueCreate  = "issue.create"
	ScopeDocCreate    = "doc.create"
	ScopeVoteCreate   = "vote.create"
	ScopeIssueComment = "issue.comment.add"
	// ScopeStepComment keys the comment a step records with `step complete
	// --comment-file`. No verb takes a caller-supplied key in it, so a key
	// cannot be pre-seeded to suppress a step's comment.
	ScopeStepComment = "step.comment"
	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. A
	// schema is a registered object, and the pinning rule 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. `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". 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 readiness
	// 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. `ready` is the tenth member of the MACHINE and is deliberately absent here: it is computed at read time by the readiness predicate and never stored as intent, exactly as v6's effective lease status is. Both numbers are stated because the enum's size and the machine's size are different questions.

View Source
const (
	ClaimEndFailed = "failed"
	ClaimEndReaped = "reaped"
)

Claim-end reasons: the value `last_claim_end` holds (v27, DKT-1279). Each names how the MOST RECENT claim to leave a step ended — `ClaimEndFailed` for an explicit `step fail`, `ClaimEndReaped` for a lease expiry, `max_step_duration`, or a forced `step reap`, the same split FailedAttempts/ReapedClaims already count. A claim that RECORDED writes neither, so the column keeps whatever it held before — that step is terminal and `last_claim_end` is never read again.

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. 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 the predecessors-done clause 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. 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 the explicit artifact size 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 ClaimPhasePreGates = "pre-gates"

ClaimPhasePreGates marks a claim whose lease committed and whose pre-gate phase and context transaction have not yet both committed.

View Source
const ClassMaxAuto = "auto"

ClassMaxAuto is the limits.max.<class> value that derives the cap from the host's CPU count when the scheduler reads it.

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, because 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 the first cap: at most 32 units per report.

The caps exist because `--usage` is ATTACKER-CONTROLLED JSON FROM A CLAIMANT 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 in the refusal matrix.

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: the 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 and a caller mapping errors to exit codes should not have to learn it twice.

View Source
var (
	// ErrSchemaAlreadyDeprecated means the version is already retired.
	// Surfaced as CONFLICT (exit 4), for ErrWorkflowAlreadyDeprecated's
	// reason: the second caller's "I am the one taking this out of service"
	// is wrong, and the timestamp they would expect to see is not the one
	// stored.
	ErrSchemaAlreadyDeprecated = errors.New("schema version is already deprecated")

	// ErrSchemaBuiltin means the version ships in the binary. It is refused
	// rather than retired because a builtin row is visible to EVERY project
	// through its flag, so retiring it in one project's registry would retire
	// it for the whole store — and nothing an operator can register replaces
	// what the binary seeds.
	ErrSchemaBuiltin = errors.New("schema is builtin and cannot be deprecated")
)

Schema retirement sentinels (v36). They mirror the workflow ones, because the two registries share the retirement contract 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 ("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 the engine 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 the open race's loser — a CONFLICT.

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.

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 ActionOutputsFor

func ActionOutputsFor(db *sql.DB, stepID int, action string) ([]string, error)

ActionOutputsFor reads the `output` column of a step's PASSING rows for one action name, ordered by ordinal.

This is deliberately NOT ActionTrail: that rollup's Output is blank on any passing row (its output channel exists for diagnosis, and a pass carries no diagnosis worth the read), and even on a failing row it is tail-truncated for display. The `aggregate` builtin's `route_at` split (DKT-593) records its below-floor clusters as a PASSING row's own output — a fact to compute from, not a diagnostic — so a reader of that fact needs the untruncated column directly.

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 — "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.

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 keeps the dormancy guarantee 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. 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 the saga's stage-0 holder check and the step refusal matrix 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 (the auto-registration 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 flips the run `active -> waiting-human` with its reason, CAS-guarded on the status it is moving FROM.

The CAS is the whole mechanism. 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 the config cap, read in the deciding transaction. 0 means unlimited.

func BudgetUnitTx

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

BudgetUnitTx resolves 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. That is the honest default: 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. 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 with no defense against concurrent writers. 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.

Expiry is reaped lazily, here and only here (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.

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.

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

func ClaimStepWithRecoveryTx

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

ClaimStepWithRecoveryTx is ClaimStepTx for a claimant that supplies a recovery secret. The step row keeps only model.HashToken(recoverySecret) in `recovery_hash`, or NULL when the secret is empty. Every claim writes the column, so a hash never outlives the lease it was claimed with.

func ClassMaxOverridesTx

func ClassMaxOverridesTx(tx *sql.Tx, projectID int) (map[string]ClassMaxOverride, error)

ClassMaxOverridesTx resolves every set limits.max.<class> inside the scheduler's snapshot transaction, keyed by class, in one statement however many classes a run names. A project's override wins over the store-wide value, GetConfig's order. A class absent from the map keeps its pinned `[limits]` max.

ClassMaxAuto resolves here, at read time, to half the host's logical CPUs and never below one, so the value tracks whichever host runs the scheduler; its entry keeps Auto set so a report can say the cap was derived.

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.

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 ClearStepInputsTx

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

ClearStepInputsTx drops a step's recorded input bindings, so a re-claim can record the bindings of the attempt that is about to run in their place (DKT-1054).

The table's key is (step, position, artifact), so without this a retried step's second claim would ADD its bindings beside the first attempt's rather than replace them, and a read-back would see the union of two attempts — neither of which any executor saw. The step's snapshot is the snapshot of its CURRENT attempt; a lapsed or retried attempt's bindings are history the event log keeps, not a second answer to "what did this step see".

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: 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" — 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 CountBySize

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

CountBySize returns a map of size -> count, scoped to a project when projectID is non-zero. The "" key counts issues with no size declared.

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 CoverGateOverrideGrantsTx

func CoverGateOverrideGrantsTx(tx *sql.Tx, ids []int) error

CoverGateOverrideGrantsTx bumps `covered_steps` on each grant that just auto-passed one step, in the same transaction as the routing it decided — the counter and the pass must not be separable by a crash.

A miss is a caller error rather than a no-op: the ids came from a read of this same table moments ago, and a grant that vanished between the read and the cover means the routing was decided on evidence that no longer stands.

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 CreateCommentIdempotentTx

func CreateCommentIdempotentTx(tx *sql.Tx, comment *model.Comment, scope, idempotencyKey string) (int, bool, error)

CreateCommentIdempotentTx is CreateCommentIdempotent inside the caller's transaction, for a writer whose comment must commit or roll back with something else (a step's recorded artifact). It reports whether it inserted: a key already recorded returns that comment's id and false, and inserts nothing. The key is recorded in scope. It neither begins nor commits.

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, bool, 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 with inserted=false and nothing is inserted — a retried create after a dropped response must succeed, not fail. inserted is true only when this call created the issue, so a caller that writes more after the insert keys on it rather than on a lookup of its own, which another process could race. 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 DeprecateSchema

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

DeprecateSchema retires ONE registered schema version from service.

It writes a timestamp and NOTHING ELSE, DeprecateWorkflow's contract applied to the other registry: the row, its bytes, its ordered index, and its hash are untouched, so `schema show name@n` still renders it, a run that pinned it keeps validating payloads against it, and the lineage stays legible. The only thing that changes is that a NEW `payload` reference to it is refused at registration.

There is deliberately no delete verb, for the workflow registry's reason.

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 the grace 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 the two dispatch 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 DoneTransitionClearsResolution

func DoneTransitionClearsResolution(prior, target model.Status, explicitResolution bool) bool

DoneTransitionClearsResolution reports whether a status write must clear the issue's resolution. Every writer that can move an issue into done decides through it.

Moving an issue INTO done supersedes a routing's earlier "abandoned": the delivered issue must not read as cancelled. `run_disposition` keeps the run-level record. Re-asserting done on an issue already there is not a transition and keeps the resolution, and a caller that sets `resolution` explicitly keeps the value it set.

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 "no retention window".

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 ExemptStepAttemptFromBudgetTx

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

ExemptStepAttemptFromBudgetTx exempts ONE spent attempt from a step's retry budget — the forced reap's classification (DKT-585) — by moving `attempt_base` forward by exactly one. Exhaustion compares `attempt - attempt_base` against `max_attempts`, so from here on the budget reads one fewer spent: the attempt still happened, it just does not count against the declared allowance.

It is a NUDGE, not ResetStepRetryBudgetTx's reset. A forced reap asserts that ONE claim's holder is gone — a relay declaring a dead spawn is not an executor failure (RUN-30 STEP-755: a wave stopped by an accidental interrupt consumed the step's last attempt, leaving it one interrupt from `waiting-human` on a healthy charter) — and the exemption must be as narrow as the assertion: this one attempt, nothing else. `attempt_base = attempt` here would also forgive every EARLIER genuinely-failed attempt, silently granting a full fresh budget on a verb that never claimed to be a retry.

`attempt` ITSELF IS UNTOUCHED, for exactly ResetStepRetryBudgetTx's reason (DKT-86, DKT-90): it is the usage ledger's key half (`UNIQUE(step_id, attempt, unit)`), and the dead attempt's usage stays back-fillable against its own attempt number after this runs — RUN-30's dead attempt had real measured usage back-filled, and decrementing the counter would have made that row collide with the successor's, the RUN-13 STEP-132 failure mode ReleaseStepLeaseTx documents.

The `attempt_base < attempt` guard keeps the base at or below the counter. Without it, reaching this twice for one claim — or on a row whose base has already caught up — would push the base PAST the counter, and `attempt - attempt_base` would go negative: budget minted out of nothing.

func ExtendDispatchTx

func ExtendDispatchTx(tx *sql.Tx, id int, expiresMS, extendedSeq int64) (bool, error)

ExtendDispatchTx records an append on an open manifest — its new expiry and the event seq it happened at — as a CAS on (id, status='open').

Both fields move together because they describe ONE append: an extend that wrote the seq without the expiry would name a manifest grown past the wall clock it was budgeted for, and one that wrote the expiry without the seq is the state DKT-2071's AC2 refuses — a manifest that has demonstrably grown and cannot say when.

The CAS is the same exclusion CloseDispatchTx relies on: a close or a TTL abandon racing an extend matches zero rows and the extend learns it lost rather than writing onto a manifest that is no longer open.

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: 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 GetIssueFilesTx

func GetIssueFilesTx(tx *sql.Tx, issueID int) ([]string, error)

GetIssueFilesTx is GetIssueFiles inside a transaction: the issue's attached paths sorted by file_path. The result is never nil; an issue with no attachments yields an empty slice, so callers can encode it as a JSON [].

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. 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 GetProposalTx

func GetProposalTx(tx *sql.Tx, id int) (*model.Proposal, error)

GetProposalTx is GetProposal inside a CALLER'S transaction — the engine's context assembly resolves `<step>.vote-record` inputs (DKT-545) in the claim transaction, and reading through the pool there would see a different snapshot than the bundle it is assembling.

func GetProposalVoteUsageSources

func GetProposalVoteUsageSources(db *sql.DB, proposalID int) (map[int]map[string]string, error)

GetProposalVoteUsageSources returns, per vote id, each usage unit's stored source (UsageSourceReported for the seat's cast-time report, or the back-fill's own source) read from that unit's own vote_usage row (DKT-2775). Every vote of the proposal has an entry; a vote with no rows maps to an empty, non-nil map. It is a db-level return rather than a field on model.Vote because the source describes the ledger row, not the cast.

func GetProposalVotes

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

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

func GetProposalVotesTx

func GetProposalVotesTx(tx *sql.Tx, proposalID int) ([]*model.Vote, error)

GetProposalVotesTx is GetProposalVotes inside a CALLER'S transaction — see GetProposalTx for why the vote-record input resolution needs one.

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 version still IN SERVICE, which is what `schema show NAME` without `@version` means — GetWorkflow's rule (v36 mirrors v11): a name whose every version is retired resolves to ErrSchemaNotFound, while an explicit version still resolves a retired row, because retired versions stay registered and reachable for the runs that pinned them.

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.

func GetStepLease

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

GetStepLease reads a step's lease without writing anything (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 NON-DEPRECATED registered version, which is what `workflow show NAME` without `@version` means — and, by design, the same version `run activate` binds (engine.bindableDefinitions retires deprecated rows first, then takes the highest of what remains; DKT-616). A name whose every version is deprecated therefore resolves to ErrWorkflowNotFound, mirroring binding removing the name from routing altogether. An explicit version still resolves a deprecated row: retired versions stay registered and reachable for the runs that pinned them.

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: 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.

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.

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 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 — saga 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 the race guard 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.

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 (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 — the hash is of the command as it read when the operator approved the plan.

func InsertGapIssueTx

func InsertGapIssueTx(tx *sql.Tx, projectID int, gap GapIssue, 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.

The ranking rides in the same transaction as the row it ranks (DKT-1082): a drained high-severity cluster that landed `priority none` was invisible to every priority-ordered planning pass, which is the same "residue nothing re-reads" failure one layer up.

func InsertGateOverrideGrantTx

func InsertGateOverrideGrantTx(tx *sql.Tx, g GateOverrideGrant) (int, error)

InsertGateOverrideGrantTx records one grant and returns its id.

It takes a transaction because the grant commits with the resolution that raised it (the DKT-237 loop-grant discipline): a grant recorded without the override-pass would cover failures the operator never ruled on, and an override-pass without the grant would silently drop the ruling's reach.

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 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.

The caller is responsible for 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`.

`budget` was STORED before anything enforced it. Accepting it early meant the enforcing upgrade added enforcement rather than a flag: a flag appearing later would have broken the `run start` invocation an earlier harness scripted.

func InsertRunNoteTx

func InsertRunNoteTx(tx *sql.Tx, n RunNote) (int, error)

InsertRunNoteTx records one note and returns its id.

It takes a transaction because the verb records the note and its `run-note-added` event together: a note with no event would be a packet change no feed reader can attribute, and an event with no note would name a ruling that renders nowhere.

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, issueIDs ...int) (run *model.Run, inserted bool, err 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 with inserted=false 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. The roster (issueIDs, one run_issues row each) commits in that same transaction, so a replay finds the roster the original call wrote; a replay's own issueIDs are ignored, like its other parameters. inserted tells the caller whether this call created the run.

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:

  • 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 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 activation's auto-registration needs.

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: 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 InsertStaleTargetWaiverTx

func InsertStaleTargetWaiverTx(tx *sql.Tx, w StaleTargetWaiver) (int, error)

InsertStaleTargetWaiverTx records one waiver and returns its id.

It takes a transaction because the verb records one waiver per named step plus one event per waiver, and a batch that half-applied would leave the operator's ruling covering rows they never listed — or missing rows they did.

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 input resolution, 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" — and ListStepInputArtifactsTx is how a read-back asks it (DKT-1054).

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 instance naming 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.

IT IS AN AUDIT RECORD, NEVER AN AUTHORIZATION SHORTCUT. Every gate consults the LIVE trust store on every execution; 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. 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:

  • 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 auto-registration, which runs inside activation's FAT TRANSACTION: 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 unchanged: this is the SAME body InsertWorkflow ran, lifted so both callers share it rather than a second path drifting from the first: 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 IssueStatusResolutionTx

func IssueStatusResolutionTx(tx *sql.Tx, issueID int) (model.Status, string, error)

IssueStatusResolutionTx reads an issue's status and resolution inside tx, or returns ErrNotFound.

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 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 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 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 LookupIdempotencyKeys

func LookupIdempotencyKeys(db *sql.DB, scope, prefix string) (map[string]int, error)

LookupIdempotencyKeys is LookupIdempotencyKeysTx through the POOL, for a reader that holds no transaction — the run report's rollup phase, which deliberately reads everything it can before its snapshot opens (DKT-584's reap-ack attribution). Same query, same escaping; two spellings of one scan would drift at the first key family that mattered.

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 — the record that this issue's phase is done, so re-activation 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.

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, so flaky-declared re-runs are recorded individually. The same holds for 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 "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, and makes every transaction not opened with sql.TxOptions{ReadOnly: true} BEGIN IMMEDIATE; a read-only one is a deferred BEGIN.

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 ProposalIDsNaming

func ProposalIDsNaming(db *sql.DB, token string) ([]int, error)

ProposalIDsNaming returns the ids of proposals whose description or rationale names `token` as a whole word, ordered by id (DKT-584).

It exists for the conversational-gate ballots a conductor opens with `vote create` — an activation panel, say — which carry no idempotency key and no step, and whose ONLY link to the run they gate is that their text names it ("activation panel for RUN-40"). The LIKE narrows candidates in SQL; the word-boundary check runs in Go because LIKE '%RUN-4%' would also match RUN-40, and a run must never inherit another run's panels.

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 ReMintStepTokenTx

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

ReMintStepTokenTx replaces the token on a LIVE lease held by `owner`, leaving every other fact of the claim alone (DKT-1564).

It exists for the executor whose own claim committed and then lost its token — the process exited before the response reached it. The lease is genuinely that executor's, but a token is returned exactly once, so without this the only exits are the full TTL and an operator-gated reap.

IT IS NOT A CLAIM. `attempt`, `started_ms` and the claim's event are untouched: nothing about the scheduling facts changed, and the budget floor sums `step-claimed` events, so a second one would bill the run twice for one claim. The guard is the OWNER plus lease liveness — the two conditions that make the claim the caller's — so it can only ever re-key a lease the caller already holds, never award one.

The previous token stops authorizing, which is the point: one lease carries one live capability, and the stranded one is by definition unreachable.

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. "Returning them to ready with attempt++" is satisfied by the claim's own increment: the trail records one attempt per claim, for all time, which is what a complete attempt trail 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 (lazy reaping is 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. 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.

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, 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 empty 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 RenderMetadataValue

func RenderMetadataValue(v any) string

RenderMetadataValue turns one opaque metadata value into the string the rollup groups by.

A non-string value is rendered as its JSON, which keeps `1` and `"1"` DISTINCT — they are different values in the bag, and merging them would be the rollup deciding they mean the same thing.

EXPORTED for DKT-868's per-step section, which renders the same column in the same document: the rollup spells a value one way and a report that spelled the per-step copy another would show a reader `1` in one section and `"1"` in the next and invite them to conclude the two came from different bags. One definition, so the two cannot disagree.

func ReportedUsageTx

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

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

One unit, never all of them: 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` (retry resets attempts) — 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 the "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: 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 RestoreSchema

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

RestoreSchema clears a version's retirement, returning it to service. It is idempotent, as RestoreWorkflow is: a version that was never retired comes back unchanged.

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 the saga's stage-1 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 and any engine invocation may resume the saga.

func RoutingRecord

func RoutingRecord(routing, reason string) string

RoutingRecord is the stored routing string: the routing alone, or `<routing>: <reason>` when a reason was given. It is exported because the engine builds the same shape for the `step-routed` event and for the cascade-abandon update, and a second spelling of it would let the readers that split this string drift from the write that produces it.

func RunConductorHash

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

RunConductorHash is RunConductorHashTx on the connection, for the CLI's advisory read: whether a verb must look for a token at all. The authoritative check runs inside the verb's own transaction.

func RunConductorHashTx

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

RunConductorHashTx reads a run's conductor capability hash inside a transaction. The empty string means UNBOUND — a run activated before v29 and never conducted — which the engine treats as "no capability exists, so nothing is required", never as a hash the empty token could match.

func RunEverDispatchedTx

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

RunEverDispatchedTx answers the usage check's SCOPE: 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 the check 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.

It is SetConfigTx in a transaction of its own; the validation, the key shape and the upsert are that function's, stated once.

func SetConfigTx

func SetConfigTx(tx *sql.Tx, projectID int, key, value string) (prior string, err error)

SetConfigTx is SetConfig inside a CALLER'S transaction (DKT-2563), and it returns the value the SAME scope row held before the write — empty when the key was unset at that scope.

It exists so a write to a security-load-bearing key can be recorded in the same transaction as the write itself: the config-changed event carries the prior and the new value, and an event committed separately from the write it describes could survive a write that rolled back, or vice versa. The prior value is read here, under the transaction, rather than by the caller through GetConfig — a pool read from inside a transaction deadlocks against internal/db's single connection, the same constraint GetConfigTx exists for.

"Prior" is the row at THIS scope (the project's override, or the store-wide default), not the effective value GetConfig would resolve: the event describes what the write replaced, and a project override replaces nothing store-wide.

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`.

IT ALWAYS BUMPS `row_version`, whether or not a precondition was given. Before this verb existed, the 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: 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 the runbook warned about when the manual edit was the only option.

func SetRunConductorHashTx

func SetRunConductorHashTx(tx *sql.Tx, id int, hash string) error

SetRunConductorHashTx binds a run to a conductor capability, or re-keys one (DKT-2465): `hash` is the SHA-256 of a token the caller minted with model.MintToken and returns exactly once. Only the hash is ever stored, the discipline the lease columns follow, so a copied database yields no live capability.

It does NOT bump `row_version`, for `SetRunPauseOriginTx`'s reason: the binding is written beside an activation or as its own attributed event, and neither is an edit to the run an `--if-version` caller is racing.

func SetRunIssueBodyTx

func SetRunIssueBodyTx(tx *sql.Tx, runID, issueID int, body, sha string) error

SetRunIssueBodyTx rewrites ONE run-issue's description snapshot and the sha that names it, and nothing else (DKT-2291).

It is SetRunIssueSnapshotTx's counterpart on the other frozen column, and narrow for the same reason: the body refresh re-reads exactly the issue's description and must leave `workflow_id` and `issue_snapshot` untouched — a refresh that also re-bound the workflow, or re-snapshotted the scope, would smuggle a second ruling through a verb that says it is about the body. The two columns move together because they are one fact: this text, and the digest that names it.

A miss is a caller error rather than a no-op, as above: the row was read moments ago inside this same transaction, so zero rows affected means the membership the decision was made on no longer stands.

func SetRunIssueSnapshotTx

func SetRunIssueSnapshotTx(tx *sql.Tx, runID, issueID int, snapshot string) error

SetRunIssueSnapshotTx rewrites ONE run-issue's `issue_snapshot` blob and nothing else (DKT-869).

It is deliberately narrower than BindRunIssueTx, which writes the binding and all three snapshot columns together because activation produces them as one fact. The scope refresh is not that fact: it re-reads exactly one field of an already-frozen snapshot and must leave `workflow_id`, `body_snapshot`, and `body_sha256` untouched — a refresh that re-bound the workflow, or rewrote the description snapshot from the live issue, would smuggle the run's mid-run edit immunity out through a verb that says it is about scope.

A miss is a caller error rather than a no-op: the row was read moments ago inside this same transaction, so zero rows affected means the membership the decision was made on no longer stands.

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; 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 SetStepAuthorityTx

func SetStepAuthorityTx(
	tx *sql.Tx, id int, authority, authorityRef string, nowMS int64,
) error

SetStepAuthorityTx records the authority a ruling on the step was made under, and the standing authorization it names. A ruling site calls it in the transaction that writes the ruling's routing, so the row and the ruling event cannot disagree. authorityRef is "" except on a standing grant.

func SetStepClaimPhaseTx

func SetStepClaimPhaseTx(tx *sql.Tx, id int, phase string, nowMS int64) error

SetStepClaimPhaseTx writes the step's claim phase. It leaves `row_version` alone: the phase is bookkeeping inside one claim, not a transition a CAS reader must observe.

func SetStepGateTrailTx

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

SetStepGateTrailTx records the accumulated gate results in `gate_trail`, the storage-location deviation: the `gate result` shape rides here until v8's `gate_results` table.

func SetStepLoopHistoryTx

func SetStepLoopHistoryTx(
	tx *sql.Tx, id, roundsRun int, triggerStep, latestVerdict string, nowMS int64,
) error

SetStepLoopHistoryTx is SetStepLoopHistoryWithCapTx with no recorded cap: it stores loop_cap 0.

func SetStepLoopHistoryWithCapTx

func SetStepLoopHistoryWithCapTx(
	tx *sql.Tx, id, roundsRun, loopCap int, triggerStep, latestVerdict string, nowMS int64,
) error

SetStepLoopHistoryWithCapTx records the four facts a fix loop leaves behind when it exhausts its bound: the rounds that ran, the bound they were measured against, the instance whose verdict opened the loop, and the verdict the last round ended on.

They are ONE statement because they are one fact — this loop ran this many rounds against this bound from here and ended on this. loopCap is the bound the refusal applied; 0 means no recorded cap, which no refusal can apply because a bound of zero disables the check. latestVerdict is the verdict the trigger round recorded, verbatim, and "" when it recorded none; it is never the routing token that entered the loop. The caller derives all four from the loop state its exhaustion decision already holds.

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

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. It leaves `recorded_at_ms` alone: annotating a finished step is not a record, and the usage grace measures from records.

func SetStepRoutingTx

func SetStepRoutingTx(
	tx *sql.Tx, id int, routing, reason, status string, class ParkClass, 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.

`reason` is the explanation for that routing. It is appended to the routing record, as it always was, AND it lands in `park_reason` when — and only when — this write parks the step. That condition lives in the SQL rather than at the call sites so no later caller can park a step without recording why, and so a resolution, which writes a different status, cannot overwrite the question it is answering (DKT-1898).

`park_reason` is documented everywhere it is read as the ENGINE's own text, so a caller may use this form only where `reason` IS the engine's — a gate verdict, a tally, a loop bound's sentence. A site whose routing reason is a person's words (a worker's `--note`, a reject note) parks through SetStepRoutingWithParkReasonTx instead, which keeps those words in the routing record and takes the engine's park text separately.

`class` is that same park's ROUTABLE form (DKT-1900), written by the same statement under the same condition, so the two halves of one fact cannot disagree and no reader has to parse the prose to classify a row. The caller derives it from the facts its routing decision already holds — a gate row's verdict, a tally, LoopOutcome.Entered — never from `reason`.

A park with no class is REFUSED rather than stored. The column would otherwise fill with empty strings at whichever park site a later change forgot, and a classifier reading "" cannot tell an unclassified park from one that predates the column. The guard is the only runtime check here because it is the only one the invariant cannot get from a type: `class` being a ParkClass stops a wrong WORD, and this stops a missing one.

func SetStepRoutingWithParkReasonTx

func SetStepRoutingWithParkReasonTx(
	tx *sql.Tx, id int, routing, reason, status string, class ParkClass,
	parkReason string, nowMS int64,
) error

SetStepRoutingWithParkReasonTx is SetStepRoutingTx with the park's prose supplied apart from the routing's reason.

The two are one string at most park sites, because both are the engine's. They come apart where the routing reason is a person's words: the attempts-exhausted park routes on the failing worker's `--note`, and a refused fix loop on a rejection routes on the operator's reject note. Those words belong in the routing record and the event that carries the decision, where they always were; `park_reason` is the engine's account of why the row is waiting, and this form is how such a site says both without folding one into the other.

`parkReason` is written only when this write parks the step, under the same condition as `class`, and is otherwise ignored. A move into a terminal status stamps `recorded_at_ms`, as SetStepStatusTx does.

A park is NEVER stored with an empty reason: `step show` omits an empty `park_reason`, so the row would read as a step that never parked. A blank reason falls back to the class word — engine text, and true — rather than being refused like a missing class, because the reason at some sites is a gate's own output, and a gate that failed silently must still park its step rather than fail the completion that recorded it.

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`. A move into a terminal status also stamps `recorded_at_ms`.

`activityMS` refreshes the saga's activity clock when non-zero — "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, 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.

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 StatusReachedAt

func StatusReachedAt(db *sql.DB, ids []int, status string) (map[int]time.Time, error)

StatusReachedAt returns, for each of the issues, when its status was last set to status, read from the activity log (second precision). An issue the log holds no such change for is absent from the map.

func StepAttemptsFor

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

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

func StepInputCount

func StepInputCount(db *sql.DB, stepID int) (int, error)

StepInputCount counts the artifacts recorded as `stepID`'s inputs at its most recent claim (`step_inputs`, DKT-1054). It is the one generic way to answer "how many upstream artifacts fed this step": the table holds only the LATEST attempt's bindings, and a step never claimed (an action step) has no rows at all.

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).

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.

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.

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.

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 the 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. 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 auto-registration: 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 VoteRuleHoldOnDissentKey

func VoteRuleHoldOnDissentKey(rule string) string

func VoteRuleSealedKey

func VoteRuleSealedKey(rule string) string

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, VoteRuleCriticalityKey, VoteRuleSealedKey and VoteRuleHoldOnDissentKey build a rule's four 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 an idempotent success from a forgery refusal without re-querying.

const (
	// AckRecorded is the first acknowledgment of a real reap.
	AckRecorded AckReapResult = iota
	// AckAlreadyDone: a second ack of the same seq succeeds and changes
	// nothing, so a relay retrying its hook does not fail.
	AckAlreadyDone
	// AckNoSuchReap: 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 a CAS on `acked_at_ms IS NULL` for the row whose `reaped_seq` matches, SCOPED TO THE RUN.

The run scope is the forgery refusal'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 already-done and no-such-reap 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 — 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.

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 to an audit of the run, which 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` 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 and an unmatched action never ran, 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 at stage 0; schema
	// validation is the schema register's. It is JSON text or "".
	Payload string
	SHA256  string
	// Stub is 1 ONLY on artifacts the pre-v9 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.
	//
	// 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. 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 GetArtifactTx

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

GetArtifactTx reads ONE artifact by id, inside a transaction — the reader for an id pinned at activation (DKT-547's `issue.linked` form), where the row may belong to another run entirely and the run-scoped listing above cannot reach it. ErrNotFound when no such row exists.

func ListRunArtifacts

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

ListRunArtifacts reads a run's artifacts ordered by id — the tie-break the input 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.

func ListStepInputArtifactsTx

func ListStepInputArtifactsTx(tx *sql.Tx, stepID int) ([]*Artifact, error)

ListStepInputArtifactsTx reads the artifacts a step's claim RECORDED as its inputs (InsertStepInputTx), ordered by id like every other artifact reader — the snapshot `step context` replays for a step that has been handed out (DKT-1054).

It is the artifact rows themselves, not the (position, artifact) pairs, because the reader re-runs the input resolution over exactly this set: the declared-position order, the engine-produced forms that bind no artifact (`issue.body`, an empty `issue.diff`, `gate-results`), and the within-input sort all come from the same code the claim ran, so a replayed bundle has the claim-time bundle's shape by construction rather than by a second rendering of it. Empty for a step whose claim bound no artifact at all.

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.

The tally is the DECLARED one — each cast weighted by its own confidence times its own domain relevance — which is what every caller before DKT-2512 got and what an ad-hoc proposal still gets. A vote step that declares `weighting = "equal"` reaches the same function through CastVoteWeighted.

func CastVoteWeighted

func CastVoteWeighted(db *sql.DB, v *model.Vote, equal bool) (*CastVoteResult, error)

CastVoteWeighted is CastVote with the tally's weighting chosen by the caller (DKT-2512). With `equal` set, every cast weighs 1.0 × 1.0 in the score — the vote ROW still keeps the confidence and domain relevance the caster declared, in their existing columns, so what a seat claimed about its own testimony is on record even where it does not price the tally.

The flag is passed per cast rather than stored on the proposal because the tally happens inside the quorum-reaching cast, and the value comes from the vote step's PINNED definition, which cannot change under a live ballot.

type ClassMaxOverride

type ClassMaxOverride struct {
	Max  int
	Auto bool
}

ClassMaxOverride is one resolved limits.max.<class> value: the cap, and whether it came from ClassMaxAuto rather than a number.

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 and class cap 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
	// "no retention window": prune holds back no event by age.
	//
	// 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: a window nobody set adds no age boundary,
	// and prune's explicit target and live-run refusal still bound what it
	// deletes.
	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: 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
	// KindClassMax is a positive integer or ClassMaxAuto: a class's
	// concurrency cap, where zero or a negative number would admit nothing.
	KindClassMax
)

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 the boundary for "reaps this relay has not yet seen".
	OpenedSeq int64
	// ExtendedSeq is the event seq of the manifest's LATEST `dispatch extend`,
	// beside OpenedSeq because the two are one manifest's place in the log:
	// where it started, and where it last grew (DKT-2071). Zero means never
	// extended — an event seq is 1-based, so no append can write one.
	//
	// The latest, not every one: the event log keeps the full history of
	// appends, and a column accumulating them would be a second, worse copy.
	ExtendedSeq 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.

Every open-manifest probe goes through it, 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.

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 (it 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.

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 (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).

type GapIssue

type GapIssue struct {
	Title       string
	Description string
	Priority    model.Priority
	Kind        model.IssueKind
	Labels      []string
	// Files and Scope come from the gap's `Files:` and `Scope:` header lines
	// (DKT-3294). A nil Scope stores SQL NULL: no scope declared.
	Files []string
	Scope []string
}

GapIssue is the shape one recorded gap materializes into: the mechanically derived title and the verbatim body, plus whatever ranking the gap's own header block declared.

Priority and Kind carry the CALLER's decision, not a parse: an empty Priority means "the gap declared none" and lands `none`, an empty Kind lands `task`. Both defaults are applied here, once, so a caller that declares nothing gets exactly the row this insert always wrote (DKT-1082).

type GateOverrideGrant

type GateOverrideGrant struct {
	ID           int
	RunID        int
	OriginStepID int
	Gate         string
	Exit         *int
	Reason       string
	// Fingerprint is the origin row's `gate_results.fingerprint`, COPIED at
	// mint and never recomputed (DKT-1796): the ruling binds to the content the
	// operator read, not to whatever a later re-run happens to print.
	//
	// EMPTY MATCHES NOTHING, deliberately. A grant minted before v30 recorded
	// no content, so it can vouch for none; treating a blank as a wildcard
	// would preserve the gate-wide waiver for exactly the runs that were in
	// flight across the upgrade. Only a pre-v30 grant is ever blank — a row
	// recorded at v30 or later always carries a fingerprint, the empty
	// capture's included.
	Fingerprint string
	// Argv is the origin row's `gate_results.argv`, copied at mint. Nil means
	// the origin row recorded none (an unmatched gate never resolved a
	// command); it is a value, not a wildcard.
	Argv []string
	// ArgvLegacy reports a grant whose command is unknown: one recorded before
	// v38, or by a binary that does not write the column. Argv is nil for such
	// a grant, and the flag is what separates it from a nil-argv mint. The
	// insert path never writes it.
	ArgvLegacy   bool
	Note         string
	CoveredSteps int
	CreatedAtMS  int64
}

GateOverrideGrant is one operator ruling that a gate's failure signature is environmental for the remainder of ONE run (DKT-546): later steps of the same run whose SAME gate fails with the SAME exit, reason and content fingerprint auto-pass at routing instead of re-asking the operator.

Exit is a POINTER for gate_results' own reason: an `unmatched` gate never ran, so it has no exit code, and NULL must match only NULL — "no process existed" is not exit 0. Reason is compared verbatim; it is the engine's classification field (unmatched cause, timeout, network annotation), not free prose.

The grant is RUN-SCOPED BY CONSTRUCTION — the run_id foreign key is the whole scope rule. A new run re-asks, exactly as DKT-546 requires; nothing here consults project config, and there is deliberately no way to widen a grant past its run.

RUN-SCOPED INCLUDES STEPS THAT DO NOT EXIST YET (DKT-734). A fix round mints new steps INSIDE the same run, so a grant taken on `fix@7`'s park covers `fix@8` and `fix@9` when a later round's same gate fails the same way. That is the design and not a leak: RUN-51 read three authorizations there where one standing authorization was spent three times. Nothing narrower is available — the row has an `origin_step_id`, but it is provenance, never consulted by grantMatches — and the safety is elsewhere: the gates still run every round, a round failing DIFFERENTLY still parks, and each application records its own `step-batch-overridden` event naming this row's id, so the ledger distinguishes one ruling spent N times from N rulings.

func GateOverrideGrantsForRun

func GateOverrideGrantsForRun(conn *sql.DB, runID int) ([]GateOverrideGrant, error)

GateOverrideGrantsForRun returns every grant recorded for one run, in insertion order.

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 the older `gate_trail`. Nothing this
	// stage records sets it, which is what makes the migration 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.
	// An unmatched verdict has four distinct causes needing four different
	// remedies, and without this field they render identically to an operator.
	Reason string
	// Fingerprint is the SHA-256 of this row's capture with run-varying text
	// removed (engine.GateFingerprint), computed at record time — the CONTENT
	// half of the failure signature a batch override grant is keyed on
	// (DKT-1796). A gate that printed nothing hashes the empty capture, so a
	// row recorded at v30 or later always carries a value: EMPTY means the row
	// predates v30 and nothing else.
	Fingerprint string
	// TargetSHA is the commit this PRE-gate row was measured against when the
	// measurement ran OUTSIDE the claim — a detached pre-gate run started when
	// the step's target was recorded, so a gate longer than the claim's budget
	// can still hand the claim a complete result. The
	// claim reuses a recorded row only for the same step and the same target,
	// and this is the key it matches on. EMPTY on every row the claim itself
	// records, on every completion-side row, and on every row recorded before
	// v37: none of those was measured for reuse, and a blank is never a key.
	TargetSHA   string
	CreatedAtMS int64
}

GateResultRow is one recorded gate execution — the `gate result` shape as it lives in the v8 table.

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 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)
	Sizes      []string // filter by size (multiple = OR); "" never matches a row
	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 rolls step `metadata` keys 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 the report'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 ParkClass

type ParkClass string

ParkClass is the closed vocabulary of WHY a step parked (DKT-1900), the routable half of the fact `park_reason` states in prose.

It is a named type rather than a bare string so a caller cannot hand SetStepRoutingTx an ad-hoc word, and so the compiler lists every producer when a value is added. Each value below names ONE routing decision; no value is derived by reading a reason string, and none is inferred at read time.

const (
	// ParkClassGateFailed: a completion gate RAN and did not pass.
	ParkClassGateFailed ParkClass = "gate-failed"
	// ParkClassGateUnmatched: a gate named no trust entry, so it never ran.
	ParkClassGateUnmatched ParkClass = "gate-unmatched"
	// ParkClassGateSkipped: a gate was skipped because the judged commit could
	// not be bound, so it measured nothing.
	ParkClassGateSkipped ParkClass = "gate-skipped"
	// ParkClassThresholdRouted: a declared threshold evaluated to a park.
	ParkClassThresholdRouted ParkClass = "threshold-routed"
	// ParkClassLoopBound: a fix loop was refused because `max_fix_loops` is
	// spent — LoopOutcome.Entered is false, never the bound's wording.
	ParkClassLoopBound ParkClass = "loop-bound"
	// ParkClassLoopStalled: an unauthorized fix loop was refused for
	// non-convergence — the last round moved no scoped bytes, the routing step
	// repeated its verdict, or the routed volume held flat past
	// `max_stalled_rounds` — with budget still left. LoopOutcome.Stalled.
	ParkClassLoopStalled ParkClass = "loop-stalled"
	// ParkClassVoteRejected: a vote tallied to a rejection.
	ParkClassVoteRejected ParkClass = "vote-rejected"
	// ParkClassDissentHeld: a vote rule's `hold_on_dissent` decision parked an
	// approved tally because a seat cast `reject`. No threshold matched a park;
	// the operator's hold displaced the `pass` the tally would have routed.
	ParkClassDissentHeld ParkClass = "dissent-held"
	// ParkClassHeldRejected: an operator rejected a held cluster, and the
	// ROUTING step takes the consequence.
	ParkClassHeldRejected ParkClass = "held-rejected"
	// ParkClassGapOnly: a completion whose only artifacts were gaps.
	ParkClassGapOnly ParkClass = "gap-only"
	// ParkClassPartialWithGap: a completion that recorded a candidate beside a
	// gap its writer marked `AC-Blocking` (DKT-4279).
	ParkClassPartialWithGap ParkClass = "partial-with-gap"
	// ParkClassUnchangedHandBack: a loop round handed back the commit its
	// previous round recorded, so the review chain would re-read one tree.
	ParkClassUnchangedHandBack ParkClass = "unchanged-handback"
	// ParkClassPassFloor: a `pass` would have left declared-floor work standing.
	ParkClassPassFloor ParkClass = "pass-floor"
	// ParkClassActionFailed: an action step's computation could not run.
	ParkClassActionFailed ParkClass = "action-failed"
	// ParkClassAttemptsExhausted: the step spent its attempt budget.
	ParkClassAttemptsExhausted ParkClass = "attempts-exhausted"
	// ParkClassJoinMissed: a join completed below `min_siblings`.
	ParkClassJoinMissed ParkClass = "join-missed"
	// ParkClassTriageUndecided: a triage panel (DKT-1901) closed without a
	// usable verdict — declined, no quorum, or retired without a tally — so
	// its `on_fail` fell back to `waiting-human`, the same outcome a step
	// would have reached with no panel at all.
	ParkClassTriageUndecided ParkClass = "triage-undecided"
	// ParkClassHeld: the run's conductor parked one pending step for the
	// operator through `step hold` (DKT-3287), with its own stated reason.
	ParkClassHeld ParkClass = "held"
)

The park classes. Each is produced by exactly one routing decision, named beside it.

`paused` is deliberately ABSENT. It is a RUN status and is never written to `steps.status` (see StepOffScheduler's note), so no routing transaction can assign it and a constant for it would be a value with no producer.

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 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, 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 — re-activation's reader. Re-activation INHERITS the pin set rather than recomputing it, so an in-flight run cannot silently adopt an edited workflow.

type ProposalRunLink struct {
	ProposalID int
	RunID      int
	// IssueID is the lowest run-bound issue the proposal is linked through.
	IssueID   int
	CreatedAt time.Time
}

ProposalRunLink pairs a proposal with a run that binds an issue the proposal is linked to (proposal_issues to run_issues), for linked-panel attribution.

func ProposalRunLinks(db *sql.DB, filter ProposalRunLinkFilter) ([]ProposalRunLink, error)

ProposalRunLinks lists every (proposal, run) pair in which the run binds an issue the proposal is linked to, once each, ordered by proposal then run. A proposal whose created_at does not parse as RFC3339 is omitted: its creation cannot be placed in any run's window, so it is attributed to no run by inference.

type ProposalRunLinkFilter

type ProposalRunLinkFilter struct {
	RunID      int
	ProposalID int
}

ProposalRunLinkFilter narrows ProposalRunLinks; a zero field matches any.

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. The
	// relay acknowledges "the `reaped` event", so the event's seq is the
	// ack's identity — which makes the ack idempotent for free 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 the headroom 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 the gate and action rollups 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 the gate rollup's per-step trail, and ActionTrail is the action rollup'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: 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.

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. It lives here rather than on
	// `steps` because the counter belongs to the issue: 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, 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
	// `run status` default, lifted by `--all`. `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 RunNote

type RunNote struct {
	ID          int
	RunID       int
	Text        string
	Authority   string
	CreatedAtMS int64
}

RunNote is one standing statement recorded against a run by whoever drives it (DKT-1079): a fact the run's workers need that no other packet source can carry — a gate known to fail on clean HEAD, the issue tracking it, and the disposition the operator already gave. Every packet the run renders after the note lands carries it, for every step of every issue in the run.

The note is RUN-SCOPED BY CONSTRUCTION — the run_id foreign key is the whole scope rule, exactly gate_override_grants' and stale_target_waivers' shape. It is also APPEND-ONLY: no edit, no delete. A packet is the record of what a worker was told, and a note that could be rewritten after the fact would make two renders of one step disagree about that with nothing in the ledger explaining the difference. A ruling that changes is recorded as a second note, which renders after the first.

Authority is the authority the writer asserted when recording the note, stored verbatim. A note recorded before the column existed reads "": its authority is unknown, and nothing infers one for it.

func ListRunNotes

func ListRunNotes(conn *sql.DB, runID int) ([]RunNote, error)

ListRunNotes is ListRunNotesTx on the pooled connection, for the read verb.

func ListRunNotesTx

func ListRunNotesTx(tx *sql.Tx, runID int) ([]RunNote, error)

ListRunNotesTx returns every note recorded for one run, in insertion order — the order they render in, since a later note may qualify an earlier one.

It takes a transaction because its main consumer is context assembly, which reads every source inside the claim's own transaction so a claimant sees one consistent run state: one atomic mediation.

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
	// ExcludeDeprecated drops retired versions (`deprecated_at_ms` set) from
	// both the rows and the pre-limit total. Zero value is false so every
	// existing caller — the registry audit, which asks what the registry
	// HOLDS — keeps seeing every version; only `schema list`'s default opts
	// this in.
	ExcludeDeprecated bool
}

SchemaListOptions filters `schema list`.

type SilentVoteSeatRow

type SilentVoteSeatRow struct {
	ProposalID int
	Voter      string
	Role       string
}

SilentVoteSeatRow is one cast VoteUsageCoverageFor counts as silent: which seat, on which proposal, reported no spend at all (DKT-733).

The coverage COUNT told an operator that 12 of 57 seats went silent and nothing anywhere said WHICH twelve — so the backfill verb that exists precisely to close the gap (`vote backfill-usage`, DKT-115) could not be aimed. This row is the identity behind the count.

The seating path is deliberately NOT here: this package selects by an opaque scope and prefix and cannot know which key family or run-naming rule minted a proposal. The engine, which owns those spellings, labels each row (engine.SilentVoteSeat).

func SilentVoteSeatsFor

func SilentVoteSeatsFor(db *sql.DB, scope, prefix string, extraIDs ...int) ([]SilentVoteSeatRow, error)

SilentVoteSeatsFor enumerates the casts VoteUsageCoverageFor counts as silent, through the SAME membership builder — so the list and the count it explains cannot disagree about which casts belong to the run. Ordered by (proposal, voter): a total key, so the report stays deterministic.

type StaleTargetWaiver

type StaleTargetWaiver struct {
	ID           int
	RunID        int
	StepInstance string
	TargetSHA    string
	Note         string
	CreatedAtMS  int64
}

StaleTargetWaiver is one operator ruling that a specific stale-target warning — one (step instance, target sha) pair — has been adjudicated for the remainder of ONE run (DKT-742): dispatch open/verify stop re-firing the identical warning, and the standing precedent is engine-visible instead of session-memory-only.

The waiver is RUN-SCOPED BY CONSTRUCTION — the run_id foreign key is the whole scope rule, exactly gate_override_grants' shape (DKT-546). A new run re-warns, and there is deliberately no way to widen a waiver past its run.

THE SIGNATURE IS THE PAIR AND NOTHING ELSE. The warning's rendered reason names the shared HEAD, which moves at every integration — RUN-52's four firings of one adjudicated warning each carried a different HEAD — so the reason is not part of the match. A DIFFERENT target sha on the same row, or the same sha on a row this waiver does not name, is a different question and still warns.

TargetSHA may be a PREFIX of the recorded sha (>= 7 hex characters, enforced at the verb): the warning an operator copies it from renders the sha at 12 characters, and demanding the full 40 would make the verb unusable from its own advisory. Matching is case-insensitive prefix (engine: waiverCovers).

func StaleTargetWaiversForRun

func StaleTargetWaiversForRun(conn *sql.DB, runID int) ([]StaleTargetWaiver, error)

StaleTargetWaiversForRun returns every waiver recorded for one run, in insertion order.

It reads on the pooled connection because its one consumer — staleTargets — runs OUTSIDE every transaction by design (no subprocess inside one), exactly as batchOverrideCover reads grants.

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 the "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
	// LastClaimEnd is the END REASON of the most recent claim to leave this
	// step (v27, DKT-1279): ClaimEndFailed or ClaimEndReaped, overwritten by
	// whichever of MarkStepAttemptFailedTx / MarkStepClaimReapedTx ran last.
	// FailedAttempts and ReapedClaims answer "how many of each has this step
	// EVER had"; a router deciding how to treat THIS re-offer needs the LAST
	// one, not a tally — the two disagree the moment a step has both a
	// failure and a reap in its history, in either order. "" means no claim
	// has ended in either way (never claimed, every claim recorded, or
	// pre-v27 history — see migrateV26ToV27).
	LastClaimEnd string
	MaxAttempts  *int
	ExpectedCost float64
	Owner        string
	TokenHash    string
	ExpiresMS    int64
	StartedMS    *int64
	ActivityMS   *int64
	SagaStage    string
	GateTrail    string
	Routing      string
	// ParkReason is the ENGINE's text for why this step could not be decided,
	// written only where the step routes `waiting-human` and never rewritten by
	// the resolution that answers it. Routing holds the LATEST decision; this
	// holds the question.
	ParkReason string
	// ParkClass is the CLOSED-ENUM form of the same question ParkReason answers
	// in prose (DKT-1900), written by the same statement and under the same
	// condition. A conductor routing a parked row reads this; ParkReason is what
	// it shows the person it escalates to. "" on any row that is not parked, and
	// on parked rows that predate the column.
	ParkClass ParkClass
	// LoopRoundsRun, LoopTriggerStep, and LoopLatestVerdict are the loop
	// history of a fix loop that exhausted its `max_fix_loops` budget (v35):
	// the rounds that actually ran against the cap, the instance whose verdict
	// opened the loop, and the verdict the last round ended on. ParkClass tells
	// a router THAT the loop bound stopped this step; these tell it what the
	// loop did before it stopped, which was otherwise reconstructible only from
	// the prunable event log. LoopCap is the bound those rounds were measured
	// against (v44), 0 when the history was written without one. Zero and "" on
	// every row that never exhausted a loop, and on rows that predate the columns.
	LoopRoundsRun     int
	LoopTriggerStep   string
	LoopLatestVerdict string
	LoopCap           int
	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. 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. 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), 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
	// RecordedAtMS is the instant the step last moved into a terminal status
	// (v39), written only by that transition. UpdatedAtMS moves on every row
	// write after it, so it cannot say when the step recorded. Zero means the
	// step has not recorded a terminal result.
	RecordedAtMS int64
	// Authority is the authority the step's latest ruling was made under (v40):
	// operator, standing-grant, or conductor, the same word its ruling event
	// carries. AuthorityRef names the standing authorization and is set only
	// with standing-grant. Both are "" on a step no ruling has resolved, and on
	// rows resolved before the columns existed.
	Authority    string
	AuthorityRef string
	// ClaimPhase is ClaimPhasePreGates from the commit of a pre-gated claim's
	// first transaction until its context transaction commits (v41), and ""
	// otherwise. It describes the claim that last wrote it, so it is only
	// meaningful while that claim's lease is live.
	ClaimPhase string
}

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 and the scope-conflict check's, since a claimed step in ANOTHER active run excludes just as surely as one in this run.

func ListIssueStepsTx

func ListIssueStepsTx(tx *sql.Tx, runID, issueID int) ([]*Step, error)

ListIssueStepsTx reads every step of ONE issue in a run, inside a transaction — the `after_fired` cascade's reader (DKT-1085), which must see the rows the open routing transaction just terminalized and needs no other issue's steps to answer its question.

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.

func ListRunStepsTx

func ListRunStepsTx(tx *sql.Tx, runID int) ([]*Step, error)

ListRunStepsTx is ListRunSteps inside a transaction — the reader for the transaction-side questions asked about a whole run, which must see one consistent snapshot of it. The readiness predicate asks whether predecessors are done and whether scopes overlap, which are questions about the same set of rows at the same instant; repinQuiescenceGuard asks whether any step could straddle a moving agreement, which is the same instant's question about the same rows.

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.

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, 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. 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, 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'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: 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, extraIDs ...int) ([]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.

extraIDs widens the selection to proposals the caller attributed to the run some OTHER way than the vote-step key family (DKT-584): reap-ack ballots and conversational-gate proposals that name the run in their text. Membership is a set test per vote row, so an id also matched by the prefix counts once.

type UsageRow

type UsageRow struct {
	RunID    int
	StepID   int
	Attempt  int
	Unit     string
	Quantity float64
	// Source records where the usage came from. 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 parses `--usage '{"unit": n, …}'` 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, 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"`
	// Pre counts rows recorded by the PRE-GATE phase (DKT-862). Like Stub it
	// counts ROWS and overlaps the four verdict columns rather than
	// partitioning with them, because it describes WHEN the row was produced,
	// not what it decided.
	//
	// It matters because a pre-gate NEVER ROUTES: it runs at claim, with
	// its results carried into the step's context bundle, and is excluded
	// from the saga's verdict. Without this count `ac-commands: pass 0, fail 1`
	// read identically whether the failure blocked the step or was an advisory
	// input to it — RUN-61 had three such rows and a conductor nearly reported
	// a fix round as burned on one of them.
	//
	// It reads the SAME `pre` column `step show`'s `[pre]` marker reads, so the
	// two surfaces cannot disagree about which gates were advisory. It is zero
	// for actions, which have no pre phase.
	Pre int `json:"pre"`
}

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`.

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.

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, extraIDs ...int) (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. extraIDs widens the count to explicitly attributed proposals exactly as VoteUsageRollup's does (DKT-584), through the same membership builder — so a cast counted in the rollup is always counted in its coverage line.

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
	// ExcludeDeprecated drops retired versions (`deprecated_at_ms` set) from
	// both the rows and the pre-limit total. Zero value is false so every
	// existing caller — binding readers that need the full lineage to judge
	// staleness — keeps seeing every version; only `workflow list`'s default
	// opts this in.
	ExcludeDeprecated bool
}

WorkflowListOptions filters `workflow list`.

type WorkflowVersion

type WorkflowVersion struct {
	Name    string
	Version int
	// Binds is false for a version RETIRED from binding (`deprecated_at_ms` set).
	//
	// The distinction is the same one bindableDefinitions makes: retirement is a
	// binding-time filter, so the version a fresh run would pin is the highest
	// one that still binds, and a staleness count computed over retired rows
	// would tell an operator to chase a version nothing can bind.
	Binds bool
}

WorkflowVersion is one registered version's IDENTITY, without its bytes (DKT-594).

It exists because the staleness question — "how many versions has this name advanced since the run pinned it" — is answered by the version column alone, and the rows that answer it carry a `body` and a `parsed` each. A corpus with 41 commits in four days has names registered a dozen deep; loading every one of their bodies to subtract two integers would make a read verb's cost scale with the size of the definitions it is not reading.

func WorkflowVersionsFor

func WorkflowVersionsFor(conn *sql.DB, projectID int, names []string) ([]WorkflowVersion, error)

WorkflowVersionsFor returns every registered version of the NAMED workflows within one project, ordered by (name, version) so a caller's reduction is deterministic.

An empty `names` returns no rows and makes no query: the caller has nothing to ask about, and an unfiltered scan of the whole registry is never what "these names" means.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL