Documentation
¶
Overview ¶
Package issuerecord is the issue ledger's record reader: the one judgement of whether a file in a status directory is a record the ledger takes, and if not, which reader stage refused it and why.
It is a leaf for the reason core/issueschema is: the ledger's surfaces (core/capture) and the gate on the committed ledger (core/lint) must reach one verdict on one file, and a record they disagree about sits in the ledger unread by every surface while the gate stays green (iss-2609261631132673). It is not inside core/capture because that package's own tests import core/lint, so a lint importing capture back is an import cycle.
Index ¶
- Variables
- func AcceptedValues(vals []string) string
- func Parse(text string) (map[string]any, string, error)
- func ParseBlock(lines []string) (map[string]any, error)
- func ParseScalarOrList(s string) (any, error)
- func ReadGuarded(path string) (string, error)
- func SplitKeepEnds(s string) []string
- func ValidateInvariants(fm map[string]any, status, path string) error
- func ValidateStrict(fm map[string]any) error
- type Layer
- type Refusal
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvariantViolation means frontmatter passed the schema but violates a // folder-status cross-field invariant. ErrInvariantViolation = errors.New("invariant violation") // ErrMalformedFrontmatter means frontmatter could not be parsed or failed // schema validation. ErrMalformedFrontmatter = errors.New("malformed frontmatter") // ErrMissingRequiredField means a schema-required field was absent. ErrMissingRequiredField = errors.New("missing required field") // ErrPathUnsafe means the ledger root or a status dir is a symlink, or a // record leaf is not a regular file the guarded read will open. ErrPathUnsafe = errors.New("path unsafe") )
The reader's refusal sentinels. core/capture re-exports each under its own name, so a caller testing errors.Is against either spelling tests one value.
var ( IssIDRe = regexp.MustCompile(`^iss-[0-9]+$`) ItdIDRe = regexp.MustCompile(`^itd-[0-9]+$`) SpcIDRe = regexp.MustCompile(`^spc-[0-9]+$`) // LinkIDRe is the shape of a typed link's target (duplicates, refines): an // issue or an intent, the two families the filing-time match compares with. LinkIDRe = regexp.MustCompile(`^(iss|itd)-[0-9]+$`) )
The id shapes the schema's id-list fields take, mirroring issue.schema.json.
Functions ¶
func AcceptedValues ¶
AcceptedValues renders a closed enum's legal set for a refusal message.
One helper rather than three literal lists, so the message and the membership test read the same slice: a value added to core/issueschema appears in the refusal without anyone remembering to add it.
func Parse ¶
Parse splits text into a frontmatter map and a body, mirroring _issue_lib._parse_text_with_body. The text MUST start with an opening --- line; the next un-indented --- closes the block. At most one leading blank line after the closing delimiter is stripped from the body.
Delimiter lines are recognised by frontmatter.IsDelimiter, the ONE rule every reader of these bytes shares (GitHub #338). This parser used to match `---` byte-exact while record-lint's ledger gate and the lifeboat graveyard read the same file through the canonical trimming scanner, so a `--- ` close made a lint-green record that every capture verb refused. The strictness that IS deliberate here — the restricted YAML subset, indented lines refused, duplicate top-level keys refused — is about the block's CONTENT and is untouched; only the delimiter compare moves to the shared primitive.
The frontmatter is parsed with a restricted YAML subset matching what buildIssueText/setScalarField emit: top-level `key: value` scalars, inline lists (`[]`, `[itd-4, fn-12]`, `["a", "b"]`), and a single level of nested object (used only by the optional resolved_by field). Values decode to string, int, []string, or map[string]any.
func ParseBlock ¶
ParseBlock parses the interior lines of a frontmatter block, deciding null-vs-string while the RAW scalar is still in hand.
An earlier draft split this in two and threaded a set of quoted keys out to the validator. The decision is made inline here, so that set was never populated and both callers discarded it — dead scaffolding, and the comment justifying it was false as well. One function, no reserved return.
func ParseScalarOrList ¶
ParseScalarOrList decodes one YAML value into string, int, or []string.
func ReadGuarded ¶
ReadGuarded reads one record file through the shared trust-boundary primitive: fsutil.ReadGuarded opens once with O_NOFOLLOW and O_NONBLOCK and validates on the SAME descriptor, so no symlink swap fits between a check and the read, and a FIFO or device at a record name cannot block the open. The cap is issueschema.RecordReadLimit, the ONE cap the ledger's families share — core/lint applies the same value, because a cap the board applies loosely and the verb applies tightly makes the ledger say two things about one file.
It is the reader for EVERY record family core/capture reads. The reading and disposition families used it from the start; the issue family read through a bare os.ReadFile until GHSA-fh9j-8xmg-m33f, so a committed FIFO hung `list`, an oversize record was serialized unbounded, and a committed symlink read an out-of-tree file into `list --json` (iss-2609012036271396). A record store a clone carries is a trust boundary whichever family the record belongs to.
The sentinels are mapped to ErrPathUnsafe: a non-regular leaf, or a symlink refused by O_NOFOLLOW, is what every caller already tests for.
func SplitKeepEnds ¶
SplitKeepEnds splits s into lines preserving their trailing newline(s), mirroring Python's str.splitlines(keepends=True) for \n and \r\n.
func ValidateInvariants ¶
ValidateInvariants enforces folder<->field invariants and filename<->id match, mirroring _issue_lib._validate_invariants. Assumes ValidateStrict ran.
func ValidateStrict ¶
ValidateStrict validates a frontmatter map against the issue schema. It special-cases schema_version first (mirrors _validate_strict) and rejects unknown keys (additionalProperties:false).
Types ¶
type Layer ¶
type Layer string
Layer is the reader stage that refused a ledger file, in scan order.
const ( // LayerName: the filename claims a record and is not a well-formed one. LayerName Layer = "filename" // LayerRead: the guarded read refused the leaf (a FIFO, a symlink, an // oversize body, an I/O error) — nothing about the record's content. LayerRead Layer = "read" // LayerFrontmatter: the bytes do not parse as a frontmatter block. LayerFrontmatter Layer = "frontmatter" // LayerSchema: the frontmatter parses and the issue schema refuses a key or // value in it. LayerSchema Layer = "schema" // LayerInvariant: schema-clean, and the record disagrees with where it sits // — its filename, or the status folder holding it. LayerInvariant Layer = "invariant" )
The reader's stages, in the order a file meets them.
type Refusal ¶
Refusal is the reader's reason for skipping one file: the stage that refused it and that stage's error.
func Judge ¶
Judge is the ledger reader's verdict on the file at path, which sits in the status directory named status (open, resolved or wontfix). claims is false for a file that claims no record — README.md, a stray note, the allocator lock — which every reader ignores. Otherwise exactly one of fm and refusal is set: the record's frontmatter and body when the reader takes it, the stage and reason when it skips it.