projection

package
v0.1.0-dev.20260222210907 Latest Latest
Warning

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

Go to latest
Published: Feb 22, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package projection owns the concrete graph data model types shared by the execution engine, Starlark projection layer, and CLI tools.

Core Types

  • Graph: A directed graph of nodes and edges representing work to be done
  • Node: A single unit of work with an action to execute
  • Edge: A dependency relationship between nodes

Graph Lifecycle

The Graph represents both plans (before execution) and receipts (after execution):

  • Before Run(): State is "pending", nodes describe what will happen
  • After Run(): State is "executed", nodes describe what happened
  • Serialized before execution: "dry-run" or "purchase order"
  • Serialized after execution: "receipt"

Index

Constants

View Source
const (
	AccessImmediate = "immediate" // query only — no graph node
	AccessPlanned   = "planned"   // graph node only — no immediate call
	AccessBoth      = "both"      // available in both projections
)

Access level constants define when a provider method is available.

Variables

This section is empty.

Functions

func FillSlot

func FillSlot(node *Node, graph *Graph, slotName string, value starlark.Value) error

FillSlot fills a slot in a node from a Starlark value.

Any slot accepts:

  • A promise (Output): creates an edge, value flows at runtime
  • A gather (Gather): creates edges from all members (parallel execution)
  • An immediate value: stored directly, known at analysis time

func GenerateNodeID

func GenerateNodeID(prefix string, components ...string) string

GenerateNodeID creates a unique node ID with the given prefix and components.

func GitStyleChecksum

func GitStyleChecksum(objectType, basename string, content []byte) string

GitStyleChecksum computes a git-style checksum. Format: SHA256("<type> <basename> <len>\0<content>") Returns format "sha256:<hex>".

func GoToStarlarkValue

func GoToStarlarkValue(v any) (starlark.Value, error)

GoToStarlarkValue converts a native Go value to a Starlark value.

func ListToStringSlice

func ListToStringSlice(list *starlark.List) []string

ListToStringSlice converts a Starlark list to a Go string slice. Used by generated receivers to convert []string parameters.

func MakeAttr

func MakeAttr(name string, fn BuiltinFunc) starlark.Value

MakeAttr creates a starlark.Builtin from a receiver method.

func NoSuchAttrError

func NoSuchAttrError(receiver, attr string) error

NoSuchAttrError returns an error for an unknown attribute.

func StarlarkDictToMap

func StarlarkDictToMap(dict *starlark.Dict) (map[string]any, error)

StarlarkDictToMap converts a Starlark dict to a Go map[string]any.

func StarlarkListToSlice

func StarlarkListToSlice(list *starlark.List) (any, error)

StarlarkListToSlice converts a Starlark list to a Go slice. Returns []string if all elements are strings, []any otherwise.

func StarlarkValueToGo

func StarlarkValueToGo(v starlark.Value) (any, error)

StarlarkValueToGo converts a Starlark value to a native Go value.

func StubAction

func StubAction(name string) any

StubAction creates a named action stub for testing and receipt deserialization. The stub is not executable — the executor replaces stubs via HydrateGraph.

Types

type Attempt

type Attempt struct {
	// Number is the 1-based attempt number.
	Number int `json:"number" yaml:"number"`

	// Status is "completed" or "failed".
	Status string `json:"status" yaml:"status"`

	// Error is the error message if the attempt failed.
	Error string `json:"error,omitempty" yaml:"error,omitempty"`

	// Timestamp is when this attempt completed (RFC3339).
	Timestamp string `json:"timestamp" yaml:"timestamp"`
}

Attempt records one execution attempt of a phase.

type BackoffStrategy

type BackoffStrategy string

BackoffStrategy defines how delays increase between retries.

const (
	BackoffNone        BackoffStrategy = "none"
	BackoffLinear      BackoffStrategy = "linear"
	BackoffExponential BackoffStrategy = "exponential"
)

type BuiltinFunc

type BuiltinFunc func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error)

BuiltinFunc is the signature for builtin function implementations.

type Collision

type Collision struct {
	Target            string `json:"target" yaml:"target"`
	Winner            string `json:"winner" yaml:"winner"`
	WinnerLayer       string `json:"winner_layer,omitempty" yaml:"winner_layer,omitempty"`
	WinnerSpecificity int    `json:"winner_specificity,omitempty" yaml:"winner_specificity,omitempty"`
	Loser             string `json:"loser" yaml:"loser"`
	LoserLayer        string `json:"loser_layer,omitempty" yaml:"loser_layer,omitempty"`
	LoserSpecificity  int    `json:"loser_specificity,omitempty" yaml:"loser_specificity,omitempty"`
}

Collision records a source conflict resolved during tree building (writ-specific).

type Edge

type Edge struct {
	From string `json:"from" yaml:"from"`
	To   string `json:"to" yaml:"to"`
}

Edge represents a dependency relationship between two nodes. From must complete before To can begin execution.

type Encoder

type Encoder interface {
	Encode(v any) error
}

Encoder is the interface for graph serialization. Both *json.Encoder and *yaml.Encoder satisfy this interface.

type Gather

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

Gather represents a collection of outputs that can run in parallel. When used as a slot input, it creates edges from ALL members to the consumer, enabling parallel execution of the gathered nodes.

Usage in Starlark:

a = plan.file.copy(src1, dst1)
b = plan.file.copy(src2, dst2)
c = plan.file.copy(src3, dst3)
group = plan.gather(a, b, c)
d = plan.whatever(group)  # d waits for a, b, c (which run in parallel)

func NewGather

func NewGather(graph *Graph, outputs ...*Output) *Gather

NewGather creates a new Gather from multiple outputs.

func (*Gather) FillSlot

func (g *Gather) FillSlot(consumer *Node, slotName string)

FillSlot fills a slot in the consumer node with all gathered promises, creating edges from each member. This enables parallel execution.

func (*Gather) Freeze

func (g *Gather) Freeze()

func (*Gather) Hash

func (g *Gather) Hash() (uint32, error)

func (*Gather) Outputs

func (g *Gather) Outputs() []*Output

Outputs returns the gathered outputs.

func (*Gather) String

func (g *Gather) String() string

Starlark Value interface

func (*Gather) Truth

func (g *Gather) Truth() starlark.Bool

func (*Gather) Type

func (g *Gather) Type() string

type Graph

type Graph struct {
	// Version is the graph format version.
	Version string `json:"version" yaml:"version"`

	// Tool identifies which tool created this graph ("writ" or "lore").
	Tool string `json:"tool" yaml:"tool"`

	// Timestamp is when the graph was created/executed.
	Timestamp time.Time `json:"timestamp" yaml:"timestamp"`

	// State is the execution state (pending, executed, failed).
	State GraphState `json:"state" yaml:"state"`

	// Platform records the OS and architecture.
	Platform Platform `json:"platform" yaml:"platform"`

	// Context contains tool-specific metadata.
	Context GraphContext `json:"context" yaml:"context"`

	// Nodes are the actions to perform/performed.
	Nodes []*Node `json:"nodes" yaml:"nodes"`

	// Edges are the dependencies between nodes.
	Edges []Edge `json:"edges,omitempty" yaml:"edges,omitempty"`

	// Phases defines the ordered lifecycle phases (nil for non-phased graphs).
	// When present, the executor uses phase-aware execution with retry and rollback.
	// When nil, the executor falls back to flat node execution.
	Phases []*Phase `json:"phases,omitempty" yaml:"phases,omitempty"`

	// Collisions records source conflicts resolved during tree building (writ-specific).
	Collisions []Collision `json:"collisions,omitempty" yaml:"collisions,omitempty"`

	// Summary contains execution statistics (populated after Run).
	Summary Summary `json:"summary,omitempty" yaml:"summary,omitempty"`

	// Rollback records compensating actions executed during rollback (populated on failure).
	Rollback []RollbackEntry `json:"rollback,omitempty" yaml:"rollback,omitempty"`

	// Checksum is the git-style integrity hash.
	Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`

	// Signature contains the cryptographic signature (optional).
	Signature *Signature `json:"signature,omitempty" yaml:"signature,omitempty"`
}

Graph represents an execution graph containing nodes and edges. This is THE graph used by both writ and lore - they differ only in content.

Before Run(): State is "pending", represents the plan After Run(): State is "executed", represents the receipt

func (*Graph) CanonicalContent

func (g *Graph) CanonicalContent() ([]byte, error)

CanonicalContent returns the graph serialized as YAML without checksum and signature. This is used for computing checksums and verifying signatures.

func (*Graph) CollectPhaseNodes

func (g *Graph) CollectPhaseNodes(phase *Phase) ([]*Node, []Edge)

CollectPhaseNodes returns the nodes and intra-phase edges for the given phase. Nodes are returned in graph order; edges are filtered to only those between phase-internal nodes.

func (*Graph) ComputeSummary

func (g *Graph) ComputeSummary()

ComputeSummary calculates summary statistics from nodes. For phased graphs, node statuses reflect the phase execution outcome (nodes in rolled-back phases may show as completed from before rollback).

func (*Graph) Filename

func (g *Graph) Filename() string

Filename returns the standard filename for this graph. Format: "<tool>-<timestamp>.yaml"

func (*Graph) PhaseByID

func (g *Graph) PhaseByID(id string) *Phase

PhaseByID returns the phase with the given ID, or nil if not found.

func (*Graph) Serialize

func (g *Graph) Serialize(enc Encoder) error

Serialize writes the graph to the given encoder. The checksum is computed before encoding.

Usage:

enc := yaml.NewEncoder(file)
enc.SetIndent(2)
defer enc.Close()
g.Serialize(enc)

type GraphContext

type GraphContext struct {
	// SourceRoot is the source directory (writ: repo path, lore: registry cache).
	SourceRoot string `json:"source_root,omitempty" yaml:"source_root,omitempty"`

	// TargetRoot is the target directory (typically $HOME).
	TargetRoot string `json:"target_root,omitempty" yaml:"target_root,omitempty"`

	// Projects lists the projects included (writ-specific).
	Projects []string `json:"projects,omitempty" yaml:"projects,omitempty"`

	// Packages lists the packages included (lore-specific).
	Packages []string `json:"packages,omitempty" yaml:"packages,omitempty"`

	// Segments contains platform segment values (writ-specific).
	Segments map[string]string `json:"segments,omitempty" yaml:"segments,omitempty"`

	// Layers lists repository layers used (writ-specific).
	Layers []string `json:"layers,omitempty" yaml:"layers,omitempty"`

	// Platform is the target platform string (lore-specific, e.g., "Darwin", "Linux.Debian").
	TargetPlatform string `json:"target_platform,omitempty" yaml:"target_platform,omitempty"`

	// Features enabled for package installation (lore-specific).
	Features []string `json:"features,omitempty" yaml:"features,omitempty"`

	// Settings for package installation (lore-specific).
	Settings map[string]string `json:"settings,omitempty" yaml:"settings,omitempty"`
}

GraphContext contains tool-specific metadata stored in the graph. Both writ and lore populate this with their relevant context.

type GraphState

type GraphState string

GraphState represents the execution state of the graph.

const (
	StatePending  GraphState = "pending"
	StateExecuted GraphState = "executed"
	StateFailed   GraphState = "failed"
)

type Node

type Node struct {
	// ID is the unique identifier (typically relative target path or package name).
	ID string `json:"id" yaml:"id"`

	// Action to perform. Stored as any because the Action interface lives in
	// internal/execution. The executor type-asserts before calling Do.
	// Serialized as the action name string; deserialized as a stubAction.
	Action any `json:"-" yaml:"-"`

	// Status of this node: pending, completed, skipped, failed.
	Status NodeStatus `json:"status" yaml:"status"`

	// Timestamp is when this action completed.
	Timestamp string `json:"timestamp,omitempty" yaml:"timestamp,omitempty"`

	// Slots holds input values for this node. Each slot can be:
	// - Immediate: value known at analysis time
	// - Promise: reference to another node's output (creates edge)
	Slots map[string]SlotValue `json:"slots,omitempty" yaml:"slots,omitempty"`

	// Project this node belongs to.
	Project string `json:"project,omitempty" yaml:"project,omitempty"`

	// Layer is the repository layer (base, team, personal).
	Layer string `json:"layer,omitempty" yaml:"layer,omitempty"`

	// Error message if status is failed.
	Error string `json:"error,omitempty" yaml:"error,omitempty"`

	// Retry is the retry policy for this node (nil = no retry).
	Retry *RetryPolicy `json:"retry,omitempty" yaml:"retry,omitempty"`

	// Annotations holds extensible metadata (serialized to receipts).
	Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}

Node represents a single unit of work in an execution graph.

func (*Node) ActionName

func (n *Node) ActionName() string

ActionName returns the action name. Works for both live nodes (Action set by executor) and deserialized receipt nodes (stubAction).

func (*Node) GetID

func (n *Node) GetID() string

GetID returns the node's unique identifier.

func (*Node) GetProject

func (n *Node) GetProject() string

GetProject returns the project name.

func (*Node) GetSlot

func (n *Node) GetSlot(name string) any

GetSlot returns the resolved value of a slot. If the slot is a promise, returns nil (must be resolved by executor).

func (Node) MarshalJSON

func (n Node) MarshalJSON() ([]byte, error)

MarshalJSON serializes the node with Action as its name string.

func (Node) MarshalYAML

func (n Node) MarshalYAML() (any, error)

MarshalYAML serializes the node with Action as its name string. Note: we cannot use the nodeJSONWire embedding pattern here because yaml.v3 panics on unexported concrete types behind interfaces in shadowed embedded fields, and fails to decode embedded fields when names collide (unlike encoding/json which handles both correctly). We round-trip through JSON instead.

func (*Node) RequireStringSlot

func (n *Node) RequireStringSlot(name string) (string, error)

RequireStringSlot returns the string value of a required slot. Returns an error if the slot is not set, or holds a non-string value. An empty string is valid — use GetSlot for optional slots where zero value is acceptable.

func (*Node) ResolvedSlots

func (n *Node) ResolvedSlots(results map[string]any, proxyCtx ...map[string]any) map[string]any

ResolvedSlots returns all slot values as a flat map. Promise slots are resolved from the results map; immediate slots are returned directly. Proxy slots are resolved from the optional proxyCtx map (used by gather for per-iteration item binding). Pass nil for results when all slots are immediate (e.g., in tests).

func (*Node) SetSlotImmediate

func (n *Node) SetSlotImmediate(name string, value any)

SetSlotImmediate sets a slot to an immediate value.

func (*Node) SetSlotPromise

func (n *Node) SetSlotPromise(name, nodeRef, slot string)

SetSlotPromise sets a slot to a promise (reference to another node).

func (*Node) SetSlotProxy

func (n *Node) SetSlotProxy(name, gatherRef, field string)

SetSlotProxy sets a slot to a gather proxy reference.

func (*Node) UnmarshalJSON

func (n *Node) UnmarshalJSON(data []byte) error

UnmarshalJSON deserializes a node, creating a stubAction from the action name.

func (*Node) UnmarshalYAML

func (n *Node) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML deserializes a node, creating a stubAction from the action name. Like MarshalYAML, we avoid the embedded struct pattern and decode manually.

type NodeStatus

type NodeStatus string

NodeStatus represents the execution status of a node.

const (
	StatusPending   NodeStatus = "pending"
	StatusCompleted NodeStatus = "completed"
	StatusSkipped   NodeStatus = "skipped"
	StatusFailed    NodeStatus = "failed"
)

type Output

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

Output represents a promise - a handle to a node's output that can flow through the graph to fill slots in other nodes.

When passed to a plan function's slot, it creates an edge in the graph. The same promise can flow to multiple slots (fan-out).

func NewOutput

func NewOutput(node *Node, graph *Graph, slot string) *Output

NewOutput creates a new Output (promise) representing a node's output.

func ResolveInput

func ResolveInput(value starlark.Value) (*Output, error)

ResolveInput extracts an *Output from a Starlark value. Returns an error if the value is not an Output.

func (*Output) Attr

func (o *Output) Attr(name string) (starlark.Value, error)

Starlark HasAttrs interface

func (*Output) AttrNames

func (o *Output) AttrNames() []string

func (*Output) DependOn

func (o *Output) DependOn(consumer *Node)

DependOn creates an edge making the given node depend on this output's node.

func (*Output) FillSlot

func (o *Output) FillSlot(consumer *Node, slotName string)

FillSlot fills a slot in the consumer node with this promise, creating an edge. This is called when a promise is passed to a plan function.

func (*Output) Freeze

func (o *Output) Freeze()

func (*Output) Graph

func (o *Output) Graph() *Graph

Graph returns the execution graph.

func (*Output) Hash

func (o *Output) Hash() (uint32, error)

func (*Output) Node

func (o *Output) Node() *Node

Node returns the node that produces this output.

func (*Output) Path

func (o *Output) Path() string

Path returns a path from the node's slots.

func (*Output) Slot

func (o *Output) Slot() string

Slot returns which output slot this represents.

func (*Output) String

func (o *Output) String() string

Starlark Value interface

func (*Output) Truth

func (o *Output) Truth() starlark.Bool

func (*Output) Type

func (o *Output) Type() string

type Phase

type Phase struct {
	// ID is the unique identifier (e.g., "phase.install").
	ID string `json:"id" yaml:"id"`

	// Name is the phase name (e.g., "install").
	Name string `json:"name" yaml:"name"`

	// Status of this phase: pending, completed, failed, rolled_back, skipped.
	Status PhaseStatus `json:"status" yaml:"status"`

	// Retry governs retry behavior when inner nodes fail.
	Retry *RetryPolicy `json:"retry,omitempty" yaml:"retry,omitempty"`

	// NodeIDs lists the IDs of inner nodes belonging to this phase.
	NodeIDs []string `json:"nodes,omitempty" yaml:"nodes,omitempty"`

	// Compensate is the ID of the compensating action for rollback.
	Compensate string `json:"compensate,omitempty" yaml:"compensate,omitempty"`

	// Attempts records retry history (populated during execution).
	Attempts []Attempt `json:"attempts,omitempty" yaml:"attempts,omitempty"`

	// State holds execution metadata captured during the forward action.
	// The compensating action reads this to know what to undo.
	State map[string]any `json:"state,omitempty" yaml:"state,omitempty"`
}

Phase represents a lifecycle phase in the execution graph. Each phase owns a set of inner nodes and acts as an error boundary with retry and compensating action support (the Saga Pattern).

type PhaseStatus

type PhaseStatus string

PhaseStatus represents the execution state of a phase.

const (
	PhasePending    PhaseStatus = "pending"
	PhaseCompleted  PhaseStatus = "completed"
	PhaseFailed     PhaseStatus = "failed"
	PhaseRolledBack PhaseStatus = "rolled_back"
	PhaseSkipped    PhaseStatus = "skipped"
)

type Platform

type Platform struct {
	OS   string `json:"os" yaml:"os"`
	Arch string `json:"arch" yaml:"arch"`
}

Platform records the OS and architecture.

type Receiver

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

Receiver provides common implementations for Starlark binding namespaces. Embed this in concrete types to satisfy starlark.Value. Concrete types must implement starlark.HasAttrs (Attr and AttrNames) themselves.

func NewReceiver

func NewReceiver(name string) Receiver

NewReceiver creates a new Receiver with the given namespace name.

func (Receiver) Freeze

func (r Receiver) Freeze()

Freeze implements starlark.Value.

func (Receiver) Hash

func (r Receiver) Hash() (uint32, error)

Hash implements starlark.Value.

func (Receiver) String

func (r Receiver) String() string

String implements starlark.Value.

func (Receiver) Truth

func (r Receiver) Truth() starlark.Bool

Truth implements starlark.Value.

func (Receiver) Type

func (r Receiver) Type() string

Type implements starlark.Value.

type RetryPolicy

type RetryPolicy struct {
	// MaxAttempts is the maximum number of retries (0 = no retry, fail immediately).
	MaxAttempts int `json:"max_attempts" yaml:"max_attempts"`

	// Backoff is the delay strategy: none, linear, exponential.
	Backoff BackoffStrategy `json:"backoff" yaml:"backoff"`

	// InitialDelay is the delay before the first retry (Go duration string, e.g. "1s").
	InitialDelay string `json:"initial_delay,omitempty" yaml:"initial_delay,omitempty"`

	// MaxDelay caps the delay between retries (Go duration string, e.g. "30s").
	MaxDelay string `json:"max_delay,omitempty" yaml:"max_delay,omitempty"`
}

RetryPolicy configures retry behavior for a phase.

func (*RetryPolicy) ComputeDelay

func (r *RetryPolicy) ComputeDelay(attempt int) time.Duration

ComputeDelay returns the delay for a given attempt number (0-based).

func (*RetryPolicy) ParseInitialDelay

func (r *RetryPolicy) ParseInitialDelay() time.Duration

ParseInitialDelay parses the InitialDelay string into a time.Duration. Returns 0 if the string is empty or unparseable.

func (*RetryPolicy) ParseMaxDelay

func (r *RetryPolicy) ParseMaxDelay() time.Duration

ParseMaxDelay parses the MaxDelay string into a time.Duration. Returns 0 if the string is empty or unparseable.

type RollbackEntry

type RollbackEntry struct {
	// Phase is the phase name that was rolled back.
	Phase string `json:"phase" yaml:"phase"`

	// Compensate is the ID of the compensating action.
	Compensate string `json:"compensate" yaml:"compensate"`

	// Status is "completed" or "failed".
	Status string `json:"status" yaml:"status"`

	// Error is the error message if the compensating action failed.
	Error string `json:"error,omitempty" yaml:"error,omitempty"`
}

RollbackEntry records a compensating action executed during rollback.

type Signature

type Signature struct {
	// Method is the signing method used (gpg, aws_kms, gcp_kms, azure_kv).
	Method string `json:"method" yaml:"method"`

	// Value is the signature data (base64-encoded).
	Value string `json:"value" yaml:"value"`

	// KeyID identifies the key used for signing.
	// For GPG: fingerprint, for KMS: key ARN/ID/URL.
	KeyID string `json:"key_id" yaml:"key_id"`
}

Signature contains the cryptographic signature of a graph.

type SlotValue

type SlotValue struct {
	// Immediate is the direct value (any type, known at analysis time).
	Immediate any `json:"immediate,omitempty" yaml:"immediate,omitempty"`

	// NodeRef is the ID of the node that produces this value (promise).
	NodeRef string `json:"node_ref,omitempty" yaml:"node_ref,omitempty"`

	// Slot is which output slot of the referenced node (empty = default output).
	Slot string `json:"slot,omitempty" yaml:"slot,omitempty"`

	// GatherRef is the gather node ID for proxy resolution.
	GatherRef string `json:"gather_ref,omitempty" yaml:"gather_ref,omitempty"`

	// Field is the field name to access on the proxy item.
	Field string `json:"field,omitempty" yaml:"field,omitempty"`
}

SlotValue represents a value that fills a slot in a node. Three variants, mutually exclusive:

  • Immediate: value known at analysis time
  • Promise: reference to another node's output (NodeRef)
  • Proxy: reference to a gather iteration item (GatherRef + Field)

func (SlotValue) IsImmediate

func (s SlotValue) IsImmediate() bool

IsImmediate returns true if this slot value is an immediate value.

func (SlotValue) IsPromise

func (s SlotValue) IsPromise() bool

IsPromise returns true if this slot value is a promise (reference to another node).

func (SlotValue) IsProxy

func (s SlotValue) IsProxy() bool

IsProxy returns true if this slot value is a gather proxy reference.

type Summary

type Summary struct {
	TotalFiles int `json:"total_files,omitempty" yaml:"total_files,omitempty"`
	Links      int `json:"links,omitempty" yaml:"links,omitempty"`
	Copies     int `json:"copies,omitempty" yaml:"copies,omitempty"`
	Templates  int `json:"templates,omitempty" yaml:"templates,omitempty"`
	Secrets    int `json:"secrets,omitempty" yaml:"secrets,omitempty"`
	Packages   int `json:"packages,omitempty" yaml:"packages,omitempty"`
	Skipped    int `json:"skipped,omitempty" yaml:"skipped,omitempty"`
	Failed     int `json:"failed,omitempty" yaml:"failed,omitempty"`
	BackedUp   int `json:"backed_up,omitempty" yaml:"backed_up,omitempty"`
}

Summary contains execution statistics.

func (Summary) String

func (s Summary) String() string

String returns a human-readable summary.

Jump to

Keyboard shortcuts

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