mutation

package
v1.0.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: AGPL-3.0, AGPL-3.0-or-later Imports: 7 Imported by: 0

Documentation

Overview

Package mutation implements the taskman-mutation/0 envelope of SPEC §3.3, its operation-specific closed payloads, the taskman-outcome/0 result, the pure post-record computation of every §3.3 operation (TM-V0-003, TM-V0-005), the request-ID replay rule of TM-V0-006 against an explicit index, and the §3.3 ADOPT_FILE composition (R2 F3, AS-35).

The package is pure: it depends on internal/wire, internal/ticket and internal/intent only. It reads no file, takes no lock, consults no clock and draws no randomness. Every fact it needs (the canonical record, the queue manifest, the policy, the inventory, attempt liveness, the request index, the logical timestamp and the trusted actor binding) is an explicit input, and every result is an explicit output: a Plan carrying the outcome, the planned post record and, for CREATE, the planned queue manifest. A Plan is never a receipt. Nothing here is durable, and a Plan with outcome COMPLETED means "this mutation would commit as the following post record"; the journal commit (receipt, request index entry, intent-file projection) is TCP-02/TCP-02b work and is the only thing that makes a mutation real.

Security boundary (owner-requested expert panel). The envelope's actor.role and actor.id are untrusted data supplied by whoever wrote the envelope; they are not authentication. Apply and Adopt therefore require a separate trusted Binding in the Context that names the role and id of the invoking principal, and refuse UNAUTHORIZED when the binding is absent or the envelope actor differs from it in either field. The binding is never derived from the envelope, from ticket text, from the policy or from any registration; it must be furnished by the caller's authority layer. This library can validate a binding it is given; it cannot authenticate the process that supplies it, and in particular it cannot distinguish two processes running under the same UID. There is no default OWNER binding and no privileged entry point: an empty Binding fails closed. The runtime enforcement profile that decides who may furnish which binding remains qualification work outside this package.

Index

Constants

View Source
const (
	OpCreate          = "CREATE"
	OpRefine          = "REFINE"
	OpPrioritize      = "PRIORITIZE"
	OpSetDependencies = "SET_DEPENDENCIES"
	OpSetGates        = "SET_GATES"
	OpSetEffects      = "SET_EFFECTS"
	OpHold            = "HOLD"
	OpReleaseHold     = "RELEASE_HOLD"
	OpReopen          = "REOPEN"
	OpArchive         = "ARCHIVE"
	OpRestore         = "RESTORE"
	OpCompleteManual  = "COMPLETE_MANUAL"
	OpGrantApproval   = "GRANT_APPROVAL"
	OpRevokeApproval  = "REVOKE_APPROVAL"
)

Operation names (§3.3).

View Source
const (
	OutcomeCompleted         = "COMPLETED"
	OutcomeRevisionConflict  = "REVISION_CONFLICT"
	OutcomeValidationFailed  = "VALIDATION_FAILED"
	OutcomeUnauthorized      = "UNAUTHORIZED"
	OutcomeBlocked           = "BLOCKED"
	OutcomeUnsupported       = "UNSUPPORTED"
	OutcomeCapacityExhausted = "CAPACITY_EXHAUSTED"
	OutcomeUncertainEffect   = "UNCERTAIN_EFFECT"
	OutcomeRequestIDConflict = "REQUEST_ID_CONFLICT"
	OutcomeStorageFailed     = "STORAGE_FAILED"
)

Outcome values of taskman-outcome/0 (§3.3), closed.

View Source
const AdoptOperation = "ADOPT_FILE"

AdoptOperation names the reconcile operation in the adoption request digest preimage (§3.3 "ADOPT_FILE request digest").

View Source
const OutcomeProfile = "taskman-outcome/0"

OutcomeProfile is the taskman-outcome/0 profile (§3.3).

View Source
const Profile = "taskman-mutation/0"

Profile is the mutation envelope profile (§3.3).

Variables

View Source
var AdoptProtectedFields = []string{
	"status", "archivedFrom", "completion", "approvals", "revision", "acceptanceRevision",
	"previousRecordSha256", "source", "shadowOverlay", "supersededBy", "createdAt", "updatedBy",
}

AdoptProtectedFields are the record fields a diverged intent file may never change through ADOPT_FILE (§3.3). A difference in any of them refuses VALIDATION_FAILED/ADOPT_UNSUPPORTED_FIELD and leaves the ticket diverged. `status` is listed but is derived, never adopted: a difference is tolerated only between the live statuses DRAFT, OPEN and HELD, and the composed operations (REFINE opening a DRAFT, HOLD/RELEASE_HOLD) decide the post status; see adoptStatusCovered. COMPLETED and ARCHIVED can neither be entered nor left by adoption.

Outcomes is the closed outcome vocabulary.

View Source
var PayloadKeys = map[string][]string{
	OpCreate:          createKeys,
	OpRefine:          RefineFields,
	OpPrioritize:      {"priority", "order"},
	OpSetDependencies: {"dependencies"},
	OpSetGates:        {"requiredGates"},
	OpSetEffects:      {"effects", "capabilities", "executionClass"},
	OpHold:            {"holdId", "reason"},
	OpReleaseHold:     {"holdId"},
	OpReopen:          {"reason"},
	OpArchive:         {"reason"},
	OpRestore:         {"reason"},
	OpCompleteManual:  {"reason", "evidence"},
	OpGrantApproval:   {"grantId", "actor", "operation", "targetRevision", "scope"},
	OpRevokeApproval:  {"grantId", "reason"},
}

PayloadKeys is each operation's closed payload key set, which decodePayload enforces and help prints. CREATE also takes an optional localToken, and REFINE takes a non-empty subset of its keys.

View Source
var RefineFields = []string{
	"acceptanceCriteria", "body", "dueDate", "estimateMinutes", "kind", "labels",
	"milestone", "owner", "requirementRefs", "supersedes", "title",
}

RefineFields are the keys a REFINE payload may carry (§3.3), sorted.

Functions

func AdoptDigest

func AdoptDigest(b Binding, queueID wire.QueueID, targetID wire.TicketID, requestID string, file []byte) wire.Digest

AdoptDigest is the TM-V0-006 request digest of an ADOPT_FILE request (§3.3 "ADOPT_FILE request digest"): the SHA-256 of the canonical encoding, trailing LF included, of the closed object

{actor:{id,role}, fileSha256, operation:"ADOPT_FILE", queueId, requestId, targetId}

where actor is the trusted Binding (never the file's updatedBy or any claim), fileSha256 is the SHA-256 of the exact file bytes offered, queueId is the context queue and targetId the canonical record's ticketId. The preimage binds the operation, the request, the reconciling principal, the queue, the target and the bytes, and nothing that moves between an original and its retry (no timestamp, no current revision): an identical retry after commit replays, while the same request ID reused by another actor or role, for another target or queue, or with other bytes conflicts.

func ParseRequestID

func ParseRequestID(where, s string) (string, error)

ParseRequestID validates a request ID supplied outside an envelope (the ADOPT_FILE request).

func PayloadValue

func PayloadValue(p Payload) wire.Value

PayloadValue renders a payload as its closed wire object.

Types

type Actor

type Actor struct {
	ID   string
	Role string
}

Actor is the envelope's untrusted actor claim. It is compared against the trusted Binding of the Context and never used on its own.

type Binding

type Binding struct {
	ID   string
	Role string
}

Binding is the trusted invocation actor: the role and id of the principal that invoked the tool, as established by the caller's authority layer. It is deliberately a separate type from Actor so that it can never be filled from an envelope by accident. An empty Binding is not "anonymous" and not "OWNER"; it fails closed with UNAUTHORIZED.

Limit: this package validates the binding it is handed (well-formed label, known role, equal to the envelope claim). It cannot authenticate the process that handed it over, and it cannot tell two processes under the same UID apart. Deciding who may furnish which binding is the runtime enforcement profile's job (qualification work), not this library's.

type CompleteManualPayload

type CompleteManualPayload struct {
	Reason   string
	Evidence []wire.Digest
}

CompleteManualPayload is {reason, evidence}.

type Context

type Context struct {
	// Binding is the trusted actor (see Binding). Required.
	Binding Binding
	// Queue is the current queue manifest (canonical record). Required.
	Queue *intent.Queue
	// Policy is the current policy (roles row removal, gate ids). Required.
	Policy *intent.Policy
	// Inventory holds every current ticket record of the queue, including
	// tombstones, so that ID membership, dependency existence and cycles
	// are checked against the full set (TM-V0-005). Required.
	Inventory *ticket.Inventory
	// Attempts answers attempt liveness (§3.2). A nil oracle or a
	// NOT_OBSERVED answer is treated as "possibly live": an acceptance-
	// relevant mutation is then refused BLOCKED/ATTEMPT_LIVE (fail closed).
	Attempts ticket.AttemptOracle
	// Requests is the request-ID index consulted for TM-V0-006 replay.
	// Required: a nil index is refused VALIDATION_FAILED/MALFORMED before any
	// computation, never treated as "no prior request" (a Context built
	// without its index would otherwise commit every retry twice).
	Requests RequestIndex
	// Now is the logical timestamp recorded as updatedAt, createdAt,
	// placedAt, grantedAt and recordedAt. Advisory only (§2).
	Now wire.Timestamp
}

Context carries every fact a mutation depends on. All of it is explicit input; nothing is read from disk, a clock or the environment.

type CreatePayload

type CreatePayload struct {
	LocalToken         *string
	Title              string
	Body               *string
	Kind               string
	Owner              *string
	Milestone          *string
	Priority           string
	Order              wire.Count
	Labels             []string
	Dependencies       []ticket.Dependency
	AcceptanceCriteria []string
	RequirementRefs    []string
	Source             ticket.Source
	Effects            ticket.Effects
	Capabilities       []string
	RequiredGates      []string
	ExecutionClass     string
	DueDate            *string
	EstimateMinutes    *wire.Count
	Supersedes         *wire.TicketID
	SupersededBy       *wire.TicketID
}

CreatePayload is the CREATE payload: the full record minus the fields the tool derives (ticketId, revision, acceptanceRevision, previousRecordSha256, status, archivedFrom, completion, holds, approvals, createdAt, updatedAt, updatedBy, shadowOverlay), plus an optional localToken.

type Envelope

type Envelope struct {
	RequestID        string
	Actor            Actor
	QueueID          wire.QueueID
	TargetID         *wire.TicketID // nil only for CREATE
	ExpectedRevision *wire.Count    // nil only for CREATE
	Operation        string
	Payload          Payload
	IssuedAt         wire.Timestamp
	// Raw is the exact envelope bytes as decoded (canonical body plus LF).
	// The TM-V0-006 request digest is the SHA-256 of these bytes.
	Raw []byte
}

Envelope is a decoded taskman-mutation/0.

func Decode

func Decode(data []byte) (*Envelope, error)

Decode parses and validates one mutation envelope (canonical bytes with trailing LF, ≤256 KiB). Every key is checked against the closed schema of §3.3, the payload against the closed table for its operation, and the nullity rule for targetId/expectedRevision. Decoding trusts nothing: the actor claim is carried through for comparison with the Binding by Apply.

func (*Envelope) Sha256

func (e *Envelope) Sha256() wire.Digest

Sha256 is the SHA-256 of the canonical mutation bytes (TM-V0-006).

func (*Envelope) Value

func (e *Envelope) Value() wire.Value

Value renders the envelope as a canonical wire value. It is the inverse of Decode for a decoded envelope and lets tests and the CLI build envelopes from typed payloads.

type GrantApprovalPayload

type GrantApprovalPayload struct {
	GrantID        string
	Actor          string
	Operation      string
	TargetRevision wire.Count
	Scope          []string
}

GrantApprovalPayload is a grant minus revoked and grantedAt. The grant's actor must equal the trusted Binding id.

type HoldPayload

type HoldPayload struct {
	HoldID string
	Reason string
}

HoldPayload is {holdId, reason}. The hold's actor and placedAt are never taken from the payload: they come from the trusted Binding and the logical timestamp of the Context.

type IndexEntry

type IndexEntry struct {
	RequestID      string
	MutationSha256 wire.Digest
	Outcome        Outcome
}

IndexEntry is one recorded request: the request ID, the SHA-256 of the canonical mutation bytes and the outcome recorded with it (TM-V0-006).

type MemoryIndex

type MemoryIndex struct {
	// contains filtered or unexported fields
}

MemoryIndex is the explicit in-memory fake used by TCP-01 tests to exercise the TM-V0-006 replay and conflict rules (AS-03). It is never durable, never shared and never evidence of a commit: a test that records an entry here is simulating the transaction the journal would perform, nothing more.

func NewMemoryIndex

func NewMemoryIndex() *MemoryIndex

NewMemoryIndex returns an empty fake index.

func (*MemoryIndex) Len

func (m *MemoryIndex) Len() int

Len returns the number of recorded requests.

func (*MemoryIndex) Lookup

func (m *MemoryIndex) Lookup(requestID string) (IndexEntry, bool, error)

Lookup implements RequestIndex.

func (*MemoryIndex) Record

func (m *MemoryIndex) Record(e IndexEntry) error

Record stores an entry as the journal would in the same transaction as its receipt. A second record under the same request ID is refused: the index is append-only.

type Outcome

type Outcome struct {
	RequestID                   string
	Outcome                     string
	Replayed                    bool
	ResultingRevision           *wire.Count
	ResultingAcceptanceRevision *wire.Count
	ReleaseID                   *string
	ResultingReleaseRevision    *wire.Count
	ReceiptSeq                  *wire.Size
	Codes                       []string
}

Outcome is a taskman-outcome/0 document. ReceiptSeq is nil for every outcome this package produces: a planned result has no receipt. Only the journal commit of TCP-02b fills it, and a Replayed outcome carries whatever the recorded original carried.

func DecodeOutcome

func DecodeOutcome(data []byte) (*Outcome, error)

DecodeOutcome parses and validates a taskman-outcome/0 (≤64 KiB).

func (*Outcome) Encode

func (o *Outcome) Encode() ([]byte, error)

Encode validates the outcome against its closed schema and the §1 bound and returns the transport bytes.

func (*Outcome) HasCode

func (o *Outcome) HasCode(code string) bool

HasCode reports whether the outcome carries the §11 code.

func (*Outcome) Value

func (o *Outcome) Value() wire.Value

Value renders the outcome. Codes are a non-semantic array and are sorted without duplicates.

type Payload

type Payload interface {
	// contains filtered or unexported methods
}

Payload is one operation's closed payload (§3.3 table).

type Plan

type Plan struct {
	Outcome        Outcome
	MutationSha256 wire.Digest // SHA-256 of the canonical mutation bytes (TM-V0-006)
	Detail         string      // human explanation of a refusal; never queue prose
	Pre            *ticket.Record
	Post           *ticket.Record // non-nil iff Outcome is a fresh COMPLETED
	QueuePost      *intent.Queue  // non-nil iff CREATE allocated a serial
	Composed       []string       // ADOPT_FILE: the composed operations in order
}

Plan is the pure result of validating and computing one mutation. It is explicitly uncommitted: Outcome.ReceiptSeq is nil, no file changed, no receipt exists, and the request index was only read. A Plan with outcome COMPLETED says what the post record would be; the transaction writer of TCP-02b decides whether it becomes real.

func Adopt

func Adopt(ctx Context, requestID string, canonical *ticket.Record, file []byte) *Plan

Adopt computes the §3.3 ADOPT_FILE composition (R2 F3): the per-field difference between a diverged intent file and the canonical record is mapped, in the fixed order of the contract table, onto composed operations that are each validated by the same step rules as if the reconciling actor had issued them, and the whole composition is applied as one revision. The result is a Plan whose Post is the single RECONCILE post record: revision +1 exactly once, acceptanceRevision +1 iff an acceptance-relevant field changed, previousRecordSha256 chained from the canonical record and never from the file. Nothing is written; the ticket stays diverged until the transaction writer commits the plan.

The reconciling actor is the trusted Binding (OWNER or OPERATOR); the file's `updatedBy` and each hold's `actor`/`placedAt` are never adopted. The request digest is AdoptDigest, computed from the immutable request identity (binding, queue, canonical ticketId, requestId, file bytes) before the replay question is asked. A tombstone accepts only RESTORE (§3.2), so an ARCHIVED canonical record refuses BLOCKED/TICKET_STATE before anything is composed: not even an empty or updatedAt-only adoption may mint a revision on it.

func Apply

func Apply(ctx Context, env *Envelope) *Plan

Apply validates one decoded envelope against the context and computes the post record. It never mutates its inputs: the canonical record, the inventory, the queue and the envelope are read only, and the result is built from fresh copies.

func (*Plan) Planned

func (p *Plan) Planned() bool

Planned reports whether the plan is a fresh (not replayed) COMPLETED result carrying a post record.

type PrioritizePayload

type PrioritizePayload struct {
	Priority string
	Order    wire.Count
}

PrioritizePayload is {priority, order}.

type ReasonPayload

type ReasonPayload struct {
	Op     string
	Reason string
}

ReasonPayload is {reason} for REOPEN, ARCHIVE and RESTORE.

type RefinePayload

type RefinePayload struct {
	Present            map[string]bool
	Title              string
	Body               *string
	Kind               string
	Owner              *string
	Milestone          *string
	Labels             []string
	AcceptanceCriteria []string
	RequirementRefs    []string
	DueDate            *string
	EstimateMinutes    *wire.Count
	Supersedes         *wire.TicketID
}

RefinePayload is a non-empty subset of RefineFields. Present names the keys carried; a typed field is meaningful only when its key is present.

func (*RefinePayload) Has

func (p *RefinePayload) Has(key string) bool

Has reports whether the payload carries the key.

func (*RefinePayload) Keys

func (p *RefinePayload) Keys() []string

Keys returns the present keys in sorted order.

type ReleaseHoldPayload

type ReleaseHoldPayload struct {
	HoldID string
}

ReleaseHoldPayload is {holdId}.

type RequestIndex

type RequestIndex interface {
	Lookup(requestID string) (IndexEntry, bool, error)
}

RequestIndex answers whether a request ID has already been recorded. The durable implementation is the journal's requests/ index (TCP-02); this package only consults whatever it is given. Only false,nil proves absence; a lookup error refuses before a fresh plan is computed.

type RevokeApprovalPayload

type RevokeApprovalPayload struct {
	GrantID string
	Reason  string
}

RevokeApprovalPayload is {grantId, reason}.

type SetDependenciesPayload

type SetDependenciesPayload struct {
	Dependencies []ticket.Dependency
}

SetDependenciesPayload is the full replacement of dependencies.

type SetEffectsPayload

type SetEffectsPayload struct {
	Effects        ticket.Effects
	Capabilities   []string
	ExecutionClass string
}

SetEffectsPayload is the full replacement of effects, capabilities and executionClass.

type SetGatesPayload

type SetGatesPayload struct {
	RequiredGates []string
}

SetGatesPayload is the full replacement of requiredGates.

Jump to

Keyboard shortcuts

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