Help us shape the future of Go! Take the 2026 Go Developer Survey

pinfile

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package pinfile reads, classifies, and edits the pin list of .seamark/lessons.yaml. The console uses it for every pin edit. The shared apply of internal/proposals uses it for the CLI and for the console, and so do the retarget and the prune of internal/maintain. A pin is a current entry of that list. The file is the authority for the installed guidance, and the proposal ledger is the authority for recorded decisions. This package relates the two and never repairs one from the other.

A read has two layers. The listing decodes the pin items of the first YAML document, so the console shows the installed guidance even when the file cannot be edited. The editing gate then decides whether a splice can keep every other byte. A gate failure sets a block reason; it never empties the listing, because an error is not an absence.

Classify links each entry to the proposal rows by one canonical key (reviews.ProposalKey). Plan computes the complete candidate file for a retarget, a prune, or a rewrite of the rule, the note, or the region set. It proves the candidate before it returns it. The candidate loads, keeps the other settings, and changes only the reviewed entries. The line check states the D-7 and D-8 line rules again without the span code that built the splices; verify.go names its one exception. PlanAppend computes the candidate of an append. An apply puts the new entries at the head of the list. A pin that a person writes goes after the last entry. Every existing line stays, and PlanAppend also proves its candidate before it returns it. RenderPin and EncodeNote write the text of an authored pin. KeyOwners and AuthorKeys name what already holds a pin key, for the authoring of pins. RenderedKey is the key of the pin that an apply writes for a row. Prepare, PrepareAppend, and Apply write a candidate through the guarded setup engine (integration Route A). Restore puts the old bytes back only while the file still holds the written bytes.

Layouts

The editor changes only the layouts whose lines it can splice and prove. Every other layout stays listed and read-only, and the block reason names its line, because an error is not an absence. The rules:

  • Supported: the items of a block list under pin: at any consistent indentation, keys in any order, comments and blank lines between and inside items, regions as a one-line flow list or as a block list with one value per line, quoted or plain values, and keys that no reader uses.
  • Blocks the whole file (yamledit and structuralGate): more than one YAML document, a top level that is not a block mapping, CRLF or bare CR line breaks, mixed line breaks, text that is not UTF-8, a tab in the indentation, duplicate keys, a pin value that is not a block list, a tag or an anchor in the pin section, an alias or a merge key, an empty item, and a file that the lessons reader rejects. A splice in such a file cannot prove that it keeps every other byte.
  • Blocks the write, not the plan (Prepare): a symbolic link on the path, a file without the owner write bit, and a file that this user cannot write, for example through an access control list. Route A refuses to replace such a file.
  • Blocks one entry (entryBlock, regionsBlock): a flow-style entry, an alias entry, a rule or region value that is not text or spans several lines, a flow regions list over several lines, and a block regions item that is not one value on its own line.
  • Blocks a retarget or a rewrite only: a key removal or a move that would drop or separate a comment, a comment line inside the lines of a note that changes, a note that changes and goes on at a line that is not deeper than its key, a region without a safe one-line YAML form, and a rule or a note change of an entry without a rule key.

An apply (PlanAppend at the head) writes CRLF files and keeps their line breaks, so its refusals are narrower than the edit gate. It still refuses a pin value that is not a plain block list, for example pin: [], more than one YAML document, and mixed line breaks. An append at the tail follows the edit gate, so a new pin never blocks a later edit of the file.

What an edit keeps

A retarget changes only the region and regions lines of each reviewed entry, in place. It can add or remove a regions key, and it writes region: "*" for a repository-wide scope. A prune removes the lines of the entry and the provenance comments directly above it that name its proposal. The rule spelling, the live note, other comments, unknown keys, blank lines, the entry order, other pins, mutes and settings stay. A retarget target that another pin, another selected pin or an applied proposal already has is refused (ErrCollision), because the editor never merges or overwrites pins.

A rewrite changes only the lines of the fields whose decoded value changes. These are the rule value on its key line, the region keys as a retarget changes them, and the note key with its value lines. An absent note goes in after the rule key, and an empty note removes the note key. Every other field keeps its text byte for byte.

The package reads the ledger rows that the caller passes in. It does not open the store, and it never writes the ledger.

Index

Constants

View Source
const File = ".seamark/lessons.yaml"

File is the repository-relative path of the pin file.

Variables

View Source
var (
	// ErrUnsupported reports a layout that no splice can edit with proof,
	// or an edit request that is not valid. The user edits by hand.
	ErrUnsupported = errors.New("the entry cannot be edited safely")
	// ErrCollision reports a target key that another pin or another
	// proposal already uses. The planner never merges two pins.
	ErrCollision = errors.New("the target key is already in use")
	// ErrNoOp reports a retarget to the current region set, or a rewrite
	// whose fields all decode to their current values.
	ErrNoOp = errors.New("the edit changes nothing")
	// ErrUnverified reports a candidate that fails a check after the
	// splice. The planner then cannot prove that the edit keeps the file.
	ErrUnverified = errors.New("cannot prove the edit preserves the file")
	// ErrStale reports a file or an entry that changed after it was read.
	ErrStale = errors.New("the pin file changed since it was read")
	// ErrNotWritable reports a file that the setup engine does not
	// replace: a path behind a symbolic link, or a file that this user
	// cannot write.
	ErrNotWritable = errors.New("the pin file cannot be replaced")
)

The reasons an edit does not enter a plan. Each EditError wraps one.

View Source
var ErrUnreadable = errors.New("unknown: .seamark/lessons.yaml could not be read")

ErrUnreadable reports that the pin file could not be read, so no row is known to be absent. An error is not an absence: a read failure must never make an applied row look removed.

Functions

func AuthorKeys

func AuthorKeys(applied []model.Proposal, exclude []int64) []reviews.PinKey

AuthorKeys returns the keys that the applied rows own by the rule of KeyOwners, except the rows whose ID is in exclude. An authoring edit passes them to Plan as otherApplied. exclude holds the rows linked to the edited entry. A note edit of a linked pin then does not collide with its own proposal.

func EncodeNote

func EncodeNote(note string) ([]string, error)

EncodeNote returns the YAML text of a note value. lines[0] is the text after "note: " on the key line. Each later line is one line of a literal block without its indentation; an empty string is an empty line. The caller indents each nonempty later line two spaces deeper than the note key (noteBlock).

A note with a line break can get the literal form "|-". yaml.v3 chooses that form, as for distill.RenderPins. EncodeNote then builds a one-item probe file with the form. It accepts the form only when the probe passes the editing gate and decodes to the note byte for byte. The probe entry must also own every probe line. For example, the probe refuses a line that starts with a tab, because the gate then blocks every later edit of the file. Every other note gets one line from yamledit.BlockScalar, with escapes in double quotes.

The probe is not the file, so the caller still verifies the decoded candidate.

func FieldsAgree

func FieldsAgree(p model.Proposal) bool

FieldsAgree reports whether distill.RenderPins writes the pin that reviews.ProposalKey names for a row. RenderPins writes the region set of model.Proposal.RegionSet, and the key reads the stored fields. Both give one key, except for a row whose fields disagree:

  • Regions holds "" or "*" next to a real region in Region or Regions. RegionSet then gives repository-wide, and the key keeps the real regions.
  • Regions is not empty, and Region names a region that Regions does not hold. RegionSet ignores Region, and the key adds it.

No code path writes such a row, but an old or hand-edited ledger can hold one. No link and no absence can then be proven, so such a row reads LinkAmbiguousFields (Classify). Extraction and the record of an existing pin (proposals.PreviewReconcile) call this predicate too, so the rule exists only here.

func PlanAppend

func PlanAppend(d *Document, block string, at Placement) ([]byte, error)

PlanAppend computes the candidate file that adds the entries of block to the pin list of d, at the place that at names. block is the text of distill.RenderPins or RenderPin. It holds entries with or without provenance comments, and each line starts with two spaces. PlanAppend is pure and writes nothing.

At the head, the new lines go right after the pin: line, at the indentation of the list items. A file that d observed as absent starts with a header comment. A CRLF file keeps its CRLF line breaks. This is the apply of the CLI and the console, and its bytes stay as they are.

At the tail, the new lines go after the last entry. A list without entries takes them after the last line of the pin section. A comment under pin:, such as the commented example of the starter file, thus stays above them. A last entry with a block that keeps its trailing line breaks ("|+") is refused, because the new lines would take those breaks from it.

Without a pin section, a new section goes at the end of the document content, for both places. PlanAppend refuses a pin section that is not a plain block list. It also refuses a file that the editor cannot parse or that the reader rejects. It returns the candidate only after verifyAppend proves it. A link or a file that this user cannot write does not stop it; PrepareAppend refuses the write.

func RenderPin

func RenderPin(p reviews.PinRule) (string, error)

RenderPin renders one pin as an entry for PlanAppend. Each line starts with two spaces. No provenance comment comes first, because no proposal stands behind a pin that a person writes. The region keys follow reviews.PinForRegions, as in distill.RenderPins. RenderPin refuses the region sets that a rewrite refuses (validRegions), so a create and an edit of one pin follow one region rule.

The pin gets its own encoders on purpose. distill.RenderPins marshals with yaml.v3. Its bytes stay as they are, because the CLI apply goldens pin them. But yaml.v3 can write a note as a "|4-" or "|+" block, which later edits refuse or which loses line breaks. RenderPin writes the values with the yamledit encoders and the note with EncodeNote, which proves its form first.

RenderPin reads its block back and refuses a block that does not decode to exactly the pin.

func RenderedKey

func RenderedKey(p model.Proposal) reviews.PinKey

RenderedKey returns the key of the pin that distill.RenderPins writes for the row p: its rule and its region set (model.Proposal.RegionSet). The raw Region and Regions fields can disagree with that set, so the readers of the file link a row by reviews.ProposalKey instead. Both keys are equal when FieldsAgree is true. The apply of internal/proposals uses this key too, so the rule exists only here.

func Restore

func Restore(reg *integration.Registry, root string, written [32]byte, original []byte) error

Restore writes original back, but only while the file still holds the bytes with the SHA-256 written. Another writer's later edit is never overwritten: the restore then returns ErrStale and changes nothing. This includes another writer that already put original back (D-12).

Restore returns nil only after the restore write succeeds, because a caller reports a nil result as "restored".

Types

type Change

type Change struct {
	// Before is the reviewed file content; After is the candidate.
	Before, After []byte
	// Edits are the edits in the candidate, in request order.
	Edits []Edit
	// NewKeys holds the key of each edited entry after the change, in the
	// order of Edits. A deleted entry has the zero key.
	NewKeys []reviews.PinKey
	// Diff is the display diff from integration.UnifiedDiff: redacted and
	// sanitized. Added and Removed count the changed lines.
	Diff           string
	Added, Removed int
}

Change is a planned and verified candidate file.

func Plan

func Plan(d *Document, edits []Edit, otherApplied []reviews.PinKey) (*Change, error)

Plan computes the candidate file for the edits on the bytes of d. It is pure: it parses d.Bytes again and ignores the other fields, so a changed Document cannot redirect an edit. otherApplied holds the reviews.ProposalKey of every applied row that is not linked to an edited entry. A retarget or a rewrite to one of them is a collision. An authoring rewrite passes AuthorKeys, which adds the key of the pin that the apply of a row with disagreeing region fields wrote.

Each edit is verified alone, and then all of them together. Plan returns the change of the edits that pass, and an error that lists an EditError for each other edit. A caller confirms only a plan without an error. A link or a file that this user cannot write does not stop Plan, so the user can still review the exact change; Prepare refuses the write.

type Document

type Document struct {
	// Guard is the observation from integration.ReadInput: existence,
	// content hash, mode, and a symbolic link on the path. Parse leaves it
	// zero.
	Guard integration.FileGuard
	// Bytes is the exact file content; nil for an absent file.
	Bytes []byte
	// Entries lists the pin items that decode as mappings, in file order.
	Entries []Entry
	// FileBlock is the reason no entry can be edited, or empty. A link or
	// a file that this user cannot write blocks the write but still lets
	// Plan compute the change for review.
	FileBlock string
	// DecodeErr is a YAML syntax error of the first document. The listing
	// is then empty.
	DecodeErr error
	// ReaderErr is the error of reviews.ParseConfig on Bytes. The listing
	// keeps every entry that still decodes.
	ReaderErr error
	// ListErr reports a listing that differs from the pins that the
	// reader delivers, for example through an alias pin value or a pin
	// key from a merge key. The listing then cannot show which pins are
	// absent.
	ListErr error
	// ErrLine is the 1-based line of DecodeErr or ReaderErr, or 0. The
	// text of ListErr names its line itself.
	ErrLine int
}

Document is one read of the pin file.

func Parse

func Parse(data []byte) *Document

Parse reads data as the content of the pin file. It is pure. An empty input is an empty list, the same as an absent file.

func Read

func Read(root string) (*Document, error)

Read reads the pin file under root. It follows a symbolic link, as the readers do, and records it in the guard; the link then blocks the write. It returns an error only for a path that is not a regular file and for an I/O failure. An absent file gives an empty document.

func (*Document) Unreadable

func (d *Document) Unreadable() bool

Unreadable reports whether d gives no reliable listing: no document, a YAML syntax error, a lessons reader error, or a listing that differs from the delivered pins. No pin is then known to be absent (D-5). Classify and the maintenance planner both use it, so they agree.

type Edit

type Edit struct {
	Ref EntryRef
	// Delete prunes the entry. Regions is then ignored, and a Rewrite is
	// not a valid request.
	Delete bool
	// Regions is the new region set of a retarget, in the order of the
	// region engine. An empty set means repository-wide.
	Regions []string
	// Provenance holds the IDs of the proposals linked to the entry. A
	// prune deletes only the provenance comments that name one of them.
	// The removal of an unlinked entry passes Entry.ProvenanceIDs, so the
	// provenance run of the entry goes with it. A run that stays would
	// read as the provenance of the next entry.
	Provenance []int64
	// Rewrite sets fields of the entry in place, for an entry that a
	// person edits in the console. With Rewrite set, Regions is ignored.
	Rewrite *Rewrite
}

Edit is one requested change of one entry.

type EditError

type EditError struct {
	Ref EntryRef
	Err error
}

EditError explains why one edit is not in the plan.

func EditErrors

func EditErrors(err error) []*EditError

EditErrors returns every EditError in an error from Plan.

func (*EditError) Error

func (e *EditError) Error() string

Error returns the reason.

func (*EditError) Unwrap

func (e *EditError) Unwrap() error

Unwrap returns the reason, which wraps one of the sentinel errors.

type Entry

type Entry struct {
	// Index is the position of the item in the pin sequence. Empty items
	// count, so Index follows the YAML text and can differ from the
	// position in reviews.Config.Pin.
	Index int
	// Line is the 1-based dash line. EndLine is the last line that the
	// entry owns. Both are exact when the file parses for editing; else
	// they are the first and the last line of its decoded values.
	Line, EndLine int
	// Rule, Region, Regions, and Note are the decoded pin fields.
	Rule, Region, Note string
	Regions            []string
	// Unknown lists the keys that no reader uses, in file order. They do
	// not block an edit; a retarget keeps them.
	Unknown []string
	// Raw is the exact text of the entry's own lines with LF breaks. It is
	// file data: a caller sanitizes it before display.
	Raw string
	// Key is the canonical identity of the pin as delivered.
	Key reviews.PinKey
	// Block is the reason the entry cannot be edited, or empty.
	Block string
	// ProvenanceIDs are the proposal IDs that seamark provenance comments
	// directly above the entry name, in file order. They are a display
	// hint; a link never depends on them.
	ProvenanceIDs []int64
}

Entry is one pin item of the file, in file order. The fields hold the decoded values, as reviews.Config reads them, so Key equals the key of the delivered pin.

func (Entry) Ref

func (e Entry) Ref() EntryRef

Ref returns the binding of the entry for a later edit: its position, its key, and the hash of its own lines.

type EntryLink struct {
	Link Link
	// Applied, Pending, and Superseded list every row that matches the
	// entry, so an ambiguous entry can show all of them.
	Applied, Pending, Superseded []model.Proposal
	// NoteChanged is true for a linked entry whose live note differs from
	// the note that the proposal recorded.
	NoteChanged bool
}

EntryLink is the classification of one live entry.

type EntryRef

type EntryRef struct {
	Index  int
	Key    reviews.PinKey
	RawSHA [32]byte
}

EntryRef binds an edit to the entry that the user reviewed: its position in the pin sequence, its key, and the hash of its own lines. A later edit of the entry, or a move, makes the binding stale.

type Link int

Link is the relationship between a pin and the proposal rows. The zero value is LinkUnknown, so an unset link never reads as linked.

const (
	// LinkUnknown means that the pin file could not be read.
	LinkUnknown Link = iota
	// LinkLinked means one live entry and one applied row with agreeing
	// region fields share the key.
	LinkLinked
	// LinkNoProposal means that no applied or pending row has the key. It
	// does not prove that a person wrote the pin.
	LinkNoProposal
	// LinkPendingMatch means that no applied row but a pending row has the
	// key, and the region fields of every such pending row agree. The
	// proposal decision route records it as applied.
	LinkPendingMatch
	// LinkAmbiguousFile means that more than one live entry has the key.
	LinkAmbiguousFile
	// LinkAmbiguousLedger means that more than one applied row has the key.
	LinkAmbiguousLedger
	// LinkAmbiguousFields means that the one matching applied row, or a
	// matching pending row, has stored region fields that disagree with
	// each other (FieldsAgree). Such a row reads it too, also when no
	// entry has its key, because the pin that its apply wrote can be
	// live under another key.
	LinkAmbiguousFields
	// LinkNotPresent means that no live entry has the key of the row. For
	// a pending row this is the normal state.
	LinkNotPresent
)

The relationships. Only LinkLinked allows an automated action.

func (Link) Reason

func (l Link) Reason() string

Reason returns a short plain explanation of the link for display.

type Links struct {
	// Entries holds one link per Document.Entries item, in the same order.
	Entries []EntryLink
	// NotPresent lists the applied rows that no live entry carries. It is
	// nil when the file could not be read; NotPresentErr then says so. A
	// row whose region fields disagree is listed with the link
	// LinkAmbiguousFields, so every applied row is in Entries or here.
	NotPresent    []model.Proposal
	NotPresentErr error
	// ByProposal holds the link of each applied and pending row by ID.
	// Superseded rows are history and have no entry here.
	ByProposal map[int64]Link
}

Links is the classification of one document against the ledger.

func Classify

func Classify(d *Document, applied, pending, superseded []model.Proposal) *Links

Classify links the entries of d to the proposal rows. It is the only place of the link rules. A row of any status matches the entry whose key is its reviews.ProposalKey. The CLI, health, and outcome readers use the same key, so every surface links a row to the same pin. A row whose region fields disagree reads LinkAmbiguousFields wherever it is listed, so no surface claims a link or an absence for it.

A document that Unreadable reports makes every link unknown. The entries that still decode keep their matches for display.

type Owners

type Owners struct {
	// Entries are the live entries with the key, in file order.
	Entries []Entry
	// Applied and Pending are the proposal rows that own the key, in the
	// order of the input rows.
	Applied, Pending []model.Proposal
}

Owners lists what already holds one pin key.

func KeyOwners

func KeyOwners(d *Document, applied, pending []model.Proposal, key reviews.PinKey, self *EntryRef) Owners

KeyOwners returns the owners of key in d and in the rows. An entry owns the key when its key is the key. self names the edited entry, which is never its own owner. A row owns the key by rowKeys: its link key, and the key of the pin that its apply writes. An applied row owns its keys also when its pin is absent from the file, because the ledger still records the pin.

KeyOwners trusts d as it is. The caller decides what an unreadable file or ledger blocks, because a missing listing is not an absence.

func (Owners) Any

func (o Owners) Any() bool

Any reports whether an entry or a proposal row owns the key.

type Placement

type Placement struct {
	// Tail puts the entries after the last entry of the list, for a pin
	// that a person writes in the console. A tail append must never
	// block later edits of the file, so it is stricter than an apply. It
	// refuses a file that fails the editing gate, CRLF line breaks
	// included. It also proves that the candidate passes the gate. An
	// absent file starts with the pin key alone, without a header.
	Tail bool
}

Placement says where PlanAppend puts the new entries. The zero value is the head of the list, the place of an apply.

type Prepared

type Prepared struct {
	// Guard is the observation of the file that the write replaces.
	Guard integration.FileGuard
	// Written is the SHA-256 of the bytes that Apply writes. A later
	// Restore uses it to prove that the file still holds them.
	Written [32]byte
	// contains filtered or unexported fields
}

Prepared is a guarded write of a candidate file, not yet applied.

func Prepare

func Prepare(reg *integration.Registry, root string, expect integration.FileGuard, after []byte) (*Prepared, error)

Prepare plans the write of after over the file that expect observed. It compares only the existence and the SHA-256 of the file with expect: the mode and the link state of the moment of the write come from the new observation, and the engine checks them itself. A file that changed returns ErrStale. A link or a file that this user cannot write returns ErrNotWritable. Bytes that the lessons reader rejects are refused, because the edit hook would then drop every pin without a word. An absent file returns ErrStale: a retarget or a prune edits an entry, so it needs the file.

func PrepareAppend

func PrepareAppend(reg *integration.Registry, root string, expect integration.FileGuard, after []byte) (*Prepared, error)

PrepareAppend plans the write of a candidate from PlanAppend. It follows the rules of Prepare, with one difference: when expect observed no file, the write creates it. The first apply of a workspace creates the pin file, as the CLI always did. The file must then still be absent at the write, so the write never replaces a file that another writer created.

func (*Prepared) Apply

func (p *Prepared) Apply() error

Apply writes the prepared bytes. The engine checks the guard once more right before the rename; a change since Prepare returns ErrStale and writes nothing.

type Rewrite

type Rewrite struct {
	// Rule is the new rule.
	Rule *string
	// Note is the new note. An empty note removes the note key, so the
	// pin reads as a rule-only reminder. A note key on the dash line
	// stays as note: "", because another key would have to move onto
	// the dash line.
	Note *string
	// Regions is the new region set, in the order of the user. An empty
	// set means repository-wide. The region keys change by the rules of
	// a retarget.
	Regions *[]string
}

Rewrite holds the new values of an authoring edit. A nil field keeps its text byte for byte, and so does a field whose new value equals its decoded value.

Directories

Path Synopsis
Package pinfiletest builds large pin files and the index rows around them, for the Phase 4 benchmarks (make tui-measure).
Package pinfiletest builds large pin files and the index rows around them, for the Phase 4 benchmarks (make tui-measure).

Jump to

Keyboard shortcuts

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