repo

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package repo discovers, parses and relates the documents of a spec repository. Nothing here reads stdin, prompts, or exits.

Index

Constants

View Source
const DateLayout = "2006-01-02"

DateLayout is the ISO 8601 date form every date field uses.

View Source
const GlossaryPage = "glossary"

GlossaryPage is the spec page name the glossary always has.

View Source
const RejectionRationale = "Rejection rationale"

RejectionRationale is the section a rejected document gains, and which no other document may carry.

View Source
const Sources = "Sources"

Sources names what a document was checked or reconstructed against. Required on every ref, and on any backfilled document.

Variables

View Source
var (
	// The same two rules as linkPattern and wikiLinkPattern above, which carry
	// a capture group the maskers do not need. A group costs about 1% on these
	// inputs, so one declaration each is the better trade: they were three
	// separate spellings of two rules and could drift.
	MarkdownLinkPattern  = linkPattern
	ReferenceLinkPattern = regexp.MustCompile(`!?\[[^\]]*\]\[[^\]]*\]`)
	WikiLinkPattern      = wikiLinkPattern
	// WikiLinkSubmatchPattern is the same compiled rule under a second name, for
	// a caller that reads the name out rather than only blanking the span. One
	// declaration, as above: the two names mark the two uses.
	WikiLinkSubmatchPattern = wikiLinkPattern
)

Patterns for spans a suggestion must never be offered inside: an inline link, a reference link, or an unexpanded wiki link. Wrapping text inside any of them produces markup that means something else entirely, and in the wiki case it hides the very error that was being reported.

View Source
var BlankLinePattern = regexp.MustCompile(`\r?\n[ \t]*\r?\n`)

BlankLinePattern separates paragraphs. It matches CRLF as well as LF, because a file saved on Windows would otherwise read as one unbroken paragraph and L15 would pass anything. Exported for the same reason as FormerlyPattern: internal/glossary refuses a definition that contains one.

View Source
var FormerlyPattern = regexp.MustCompile(`^Formerly \*(.+)\*\.$`)

FormerlyPattern matches the line a rename leaves behind. It is not a paragraph, and several may accumulate as a term is renamed repeatedly.

Exported because internal/glossary asks the same question when it decides where to put a new note. A second spelling of the rule there could drift from this one with nothing to notice, which is why the link patterns are exported too.

Statuses is every valid status.

Types is every document type, in directory order.

Functions

func Anchor

func Anchor(text string) string

Anchor is the fragment GitHub would generate for a heading standing on its own. Headings inside a document take their anchors from a walk that numbers repeats, so this is only for naming the anchor a heading would have had when the heading is not there to ask.

func AnchorsIn

func AnchorsIn(source []byte) []string

AnchorsIn returns the heading anchors of arbitrary markdown, for checking a link into a file that is not itself a document.

func Contains

func Contains(root, full string) error

Contains reports whether full sits inside root once every directory component has been resolved.

config.ValidateRoot already checks the root itself, and that is not enough: a root can be perfectly ordinary while a document directory inside it points elsewhere, and every write into that directory then lands outside the repository while the tool reports a path inside it. A parent that does not exist yet is fine, because it will be created inside root.

func EscapeMarkdown

func EscapeMarkdown(text string) string

EscapeMarkdown makes arbitrary text safe to place inside generated markdown: a table cell, or a link label. A title is free text, and a document called "Pwned](https://evil.example) x" would otherwise become a live hyperlink in another document's body and in the index, with lint reporting nothing.

The backslash goes first, or escaping the others would double it.

func HeadingSurvives

func HeadingSurvives(text string) bool

HeadingSurvives reports whether text written as a heading reads back as itself. A trailing hash is a closing sequence and trailing whitespace is discarded, so a title carrying either produces an H1 that no longer matches the front matter, and a glossary term that can never be found by the name that was typed.

func Identifier

func Identifier(t Type, number int) string

Identifier renders the identifier for a type and number, as RFC-0001.

func ImplementedIn

func ImplementedIn(includedIn []string) []string

ImplementedIn is the spec pages whose inclusion of a document means the document has been built, which is every page but the glossary.

The full IncludedIn list is kept as written, because "which spec pages reference this" is a separate and factual question.

func IsHeading

func IsHeading(line string) bool

IsHeading reports whether a line is an ATX heading. A heading's raw text is a section's identity throughout this package and in lint, so a rewriter must leave one alone.

func LineEnding

func LineEnding(source []byte) string

LineEnding is the ending a file uses, so that an edit does not leave it with a mixture. The first line decides it. Three packages were deciding this separately, and one of them forgot.

func LinkDestination

func LinkDestination(path string) string

LinkDestination renders a path as a markdown link destination, percent- encoding every character that would otherwise end the destination, the link, or the table cell around it.

One function, because the index and the link rewriter each rendered destinations their own way and only one of them escaped anything. Encoding rather than escaping, because the two interact: the angle-bracket form this replaces turned a path's ">" into "\>", and a backslash already in the path then paired with that backslash and let the ">" close the destination early. An ordinary path contains none of these and comes through unchanged, and Document.Links decodes on the way back in, so the two are inverses.

func Prose

func Prose(body string, bodyLine int) iter.Seq[ProseLine]

Prose iterates the prose lines of a body: those not inside a fenced code block or an HTML comment. Leaving the loop stops the walk.

func Renumber

func Renumber(r *Repo, from, to string, frozen func(*Document) bool) ([]string, error)

Renumber changes a numbered document's identifier, rewriting the filename, the front matter id, the heading, and every reference to the old identifier in every other document.

The number lives in three places inside its own document and in an unbounded number of places outside it, so doing this by hand reliably trades one lint error for another. Two concurrent pull requests taking the same number is the case that makes it routine.

frozen reports whether a document may no longer be written to. Renumbering refuses when the document itself is frozen, and when any document that would have to change is, because a reference inside a frozen document could never be corrected and the rename would leave it pointing at a file that is gone. The refusal is complete: nothing is written unless everything can be.

func RequiredKeys

func RequiredKeys(t Type) []string

RequiredKeys lists the keys that must be present, whatever their value.

The schema may only ever gain optional keys. A new required key would put every frozen document in every existing repository in violation of L01, and L11 forbids the edit that would add it.

func RequiredSections

func RequiredSections(t Type) []string

RequiredSections lists the H2 sections a type must carry, in the order they must appear. Other sections are permitted between or after them.

func Schema

func Schema(t Type) []string

Schema lists the keys a type's front matter may carry. A key outside this list is an unknown field.

func SetField

func SetField(source []byte, key string, line int, value string) ([]byte, error)

SetField replaces the value of one front matter key, on the line it was written, and leaves everything else exactly as it was: the order of the other keys, their spacing, and any comments. Re-serialising the block through a YAML encoder would be shorter and would quietly discard all three.

func Slug

func Slug(title string) string

Slug derives the human-readable half of a filename from a title: lowercased, runs of anything else collapsed to one hyphen, and the ends trimmed. The number is the identity; this is only for reading.

func StartOfDay

func StartOfDay(t time.Time) time.Time

StartOfDay is an instant's calendar date at midnight UTC, which is how a date written in front matter is held once parsed.

It lives beside DateLayout because every comparison between a written date and "now" needs it. Comparing a parsed date against a wall-clock instant makes the answer depend on the hour the command ran and the zone it ran in: east of UTC it read today as the future, and west of it read tomorrow as the past. That was a defect twice, in two packages, before this was shared.

func UnclosedIn

func UnclosedIn(source []byte) bool

UnclosedIn is the same question about a whole file that is not a Document yet, which is what a writer has in hand just before it returns.

func WriteFile

func WriteFile(full string, source []byte, name string) error

WriteFile writes a file, refusing to write through a symbolic link. name is what the refusal calls the path, which for a document is its repository path.

Every writer in the program must come through here. os.WriteFile follows a symlink and truncates whatever is on the other end, so a symlinked INDEX.md committed to a repository, which git stores and clone restores, redirected `archdoc index` onto any file the user could write, reported only "INDEX.md", and exited 0.

Types

type Document

type Document struct {
	Type Type
	// Path is relative to root, always slash-separated.
	Path string
	// ID is the identifier derived from the type and the filename number, such
	// as RFC-0001. Empty for spec pages, which are not numbered. This is the
	// authoritative identity; lint compares the front matter id against it.
	ID string
	// Number is the filename number for numbered types, 0 otherwise.
	Number int
	// Page is a spec page's path below spec/ without the extension, such as
	// http/routing. Empty for numbered types.
	Page string

	// FrontMatter is the parsed YAML block.
	FrontMatter FrontMatter
	// Body is everything after the closing delimiter.
	Body string
	// BodyLine is the 1-based line the body starts on, so that positions
	// found in the body can be reported against the file.
	BodyLine int
	// Source is the document exactly as it is on disk, never normalised on the
	// way in, because every writer splices into it by offset.
	Source []byte
	// Problems are the faults found while reading the document. Lint decides
	// what they mean; repo only records them.
	Problems []Problem

	// UpdatedBy, ObsoletedBy and DependedOnBy hold the identifiers of the
	// documents naming this one in the matching forward list. IncludedIn holds
	// the names of the spec pages listing this document in includes. All are
	// sorted and derived, never written to the document itself.
	UpdatedBy    []string
	ObsoletedBy  []string
	DependedOnBy []string
	IncludedIn   []string

	// EffectivelyObsolete is true when an accepted document obsoletes this one.
	// A claim by a draft, proposed, rejected or withdrawn document has no
	// effect.
	EffectivelyObsolete bool
	// Implemented is true for an accepted RFC or ADR that some spec page
	// includes.
	Implemented bool
	// Stale is true for a spec page including an effectively obsolete document.
	Stale bool
	// contains filtered or unexported fields
}

Document is one file in one of the four document directories.

func (*Document) ClosingSection

func (d *Document) ClosingSection() (string, bool)

ClosingSection is the section that must be a document's last H2, and whether it has one at all. Most documents do not: PROCESS.md states a closing section only for a ref and for a rejected document, and extra sections may otherwise sit after the required ones.

It is the last entry of RequiredSections when that entry is a closing kind, which is what keeps this rule and the relative-order rule from contradicting each other. They did: Sources was required last and the rationale required after Sources, so a backfilled rejected document could satisfy neither, and `archdoc new --backfill --status=rejected` wrote one that could never lint clean and that L11 then forbade anyone repairing. PROCESS.md's Document structure section calls the rationale final and its Backfilling section asks only that a backfilled document carry Sources, so the rationale closes the document when both are required.

func (*Document) Contents

func (d *Document) Contents() []Entry

Contents is the document as a flat list of its headings, in document order.

func (*Document) H1

func (d *Document) H1() (Heading, bool)

H1 returns the first heading when it is an H1.

func (*Document) Headings

func (d *Document) Headings() []Heading

Headings returns every heading in the body, in order.

func (d *Document) Links() []Link

Links returns every markdown link in the body, ignoring code. A link reference definition counts: its destination is a target like any other, and nothing was checking it.

func (*Document) Prose

func (d *Document) Prose() iter.Seq[ProseLine]

Prose is the same over one document's body.

func (*Document) Repo

func (d *Document) Repo() *Repo

Repo is the repository this document was discovered in.

func (*Document) RequiredSections

func (d *Document) RequiredSections() []string

RequiredSections is RequiredSections for the type, plus Sources when the document is backfilled. Both L08, which checks a section is present, and L14, which checks it is not empty, read this one list, so they cannot disagree about what a document must carry.

func (*Document) SectionTitles

func (d *Document) SectionTitles() []string

SectionTitles lists the document's H2 headings in order.

func (*Document) Sections

func (d *Document) Sections(title string) []Section

Sections returns every H2 with exactly this text.

func (*Document) Unclosed

func (d *Document) Unclosed() bool

Unclosed reports whether the body ends inside an HTML comment or a fenced code block.

Everything from such a marker to the end of the file is masked, which is correct, and it means the document's headings and sections stop there: the parse is a prefix of what the author wrote. A writer that computes a line range from that parse and splices on it cuts across the gap, which is how `term remove` deleted every entry below an entry holding a stray `<!--`, silently, on a page that lint passed. Writers ask this before they splice.

func (d *Document) WikiLinks() []WikiLink

WikiLinks returns every [[...]] in the body, ignoring code.

type Entry

type Entry struct {
	Heading Heading
	// Text is the body as written, from the line after the heading to the line
	// before the next one, so Heading.Line plus one is its first line.
	Text string
}

Entry is one heading and the lines beneath it, up to the next heading of any level, whatever that heading's level is.

A Section is a different unit and stays one: it is an H2 and everything under it, subsections included, which is what the required-section rules mean by the word. An entry's body holds no other entry's prose, so walking the entries reads every line of a body exactly once.

type FrontMatter

type FrontMatter struct {
	ID        string
	Title     string
	Status    Status
	Created   time.Time
	Decided   time.Time
	Depends   []string
	Updates   []string
	Obsoletes []string
	Includes  []string
	Verified  time.Time
	// Backfilled is the date the document was written to record a decision
	// taken earlier. Optional, and the only optional key: created and decided
	// carry the historical dates, this one says when they were written down.
	Backfilled time.Time
	// contains filtered or unexported fields
}

FrontMatter holds every field any document type may carry. Fields that do not apply to a type stay zero; lint decides which are permitted where.

func (FrontMatter) Has

func (f FrontMatter) Has(key string) bool

Has reports whether a key was written in the block, whatever its value.

func (FrontMatter) LineOf

func (f FrontMatter) LineOf(key string) int

LineOf is the line a key was written on, or 0 if it was not.

func (FrontMatter) Unknown

func (f FrontMatter) Unknown() []UnknownKey

Unknown lists keys written in the block that belong to no schema.

type GlossaryEntry

type GlossaryEntry struct {
	Term   string
	Anchor string
	// Line is 1-based within the file, at the term's heading.
	Line int
	// End is the 1-based line just past the entry: the next heading of level
	// two or less, or 0 when the entry runs to the end of the file.
	//
	// It is recorded here rather than derived from the next entry's Line,
	// because those two answers differ whenever an ordinary H1 section sits
	// among the entries, and the difference deleted that section.
	End int
	// Paragraphs holds the entry's prose. A well-formed entry has exactly one;
	// L15 reports the rest.
	Paragraphs []string
	// Formerly holds the previous names recorded by `Formerly *Old Term*.`
	// lines, which are not paragraphs.
	Formerly []string
}

GlossaryEntry is one term in spec/glossary.md.

func GlossaryIn

func GlossaryIn(source []byte) ([]GlossaryEntry, bool)

GlossaryIn parses glossary entries out of arbitrary source, so that a rewritten page can be read back without being written to disk first.

type Heading

type Heading struct {
	Level int
	Text  string
	// Line is 1-based within the file.
	Line int
	// Anchor is the fragment GitHub would generate for this heading.
	Anchor string
	// contains filtered or unexported fields
}

Heading is one markdown heading in a document body.

type Link struct {
	Text   string
	Target string
	Type   LinkType
	// Line is 1-based within the file.
	Line int
}

Link is a relative markdown link found in a body.

type LinkType

type LinkType string

LinkType is the form a link was written in. An image resolves like any other link and renders as something else entirely, so a consumer rewriting destinations has to tell them apart.

const (
	LinkInline     LinkType = "inline"
	LinkImage      LinkType = "image"
	LinkDefinition LinkType = "definition"
)

type Problem

type Problem struct {
	// Line is 1-based within the document, or 0 when it is not known.
	Line    int
	Message string
}

Problem is something wrong with a document, found while reading it. Lint turns problems into findings; repo does not decide severity or rule identity.

type ProseLine

type ProseLine struct {
	// Raw is the line as written. Edits are applied to this one.
	Raw string
	// Masked is Raw with the contents of inline code spans and HTML comments
	// blanked, keeping their length so that a position found here is the same
	// column in Raw. Matches are looked for in this one.
	Masked string
	// Number is the line's 1-based number in the file.
	Number int
}

ProseLine is one line of a body that is neither inside a fenced code block nor inside an HTML comment.

The two strings are a pair, and which is which matters: a caller decides from Masked and applies the edit that follows to Raw, at the same offsets. They are named fields rather than adjacent parameters of the same type because confusing the two is the single root cause of three defects this package has shipped.

type Repo

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

Repo is a spec repository: its configuration and every document under root.

func Open

func Open(c *config.Config) (*Repo, error)

Open discovers and parses every document under the configured root.

func (*Repo) ByID

func (r *Repo) ByID(id string) *Document

ByID returns the document with this identifier, or nil. The identifier is the one derived from the type and filename, not whatever the front matter claims.

func (*Repo) ByPage

func (r *Repo) ByPage(page string) *Document

ByPage returns the spec page with this name, or nil.

func (*Repo) ByPath

func (r *Repo) ByPath(path string) *Document

ByPath returns the document at this path relative to root, or nil.

func (*Repo) Config

func (r *Repo) Config() *config.Config

Config is the configuration this repository was opened with.

func (*Repo) Documents

func (r *Repo) Documents() []*Document

Documents returns every document, ordered by path.

func (*Repo) File

func (r *Repo) File(docPath string) string

File is the absolute location of a document path, which is stored slash-separated and relative to root.

func (*Repo) Glossary

func (r *Repo) Glossary() (entries []GlossaryEntry, ok bool)

Glossary parses spec/glossary.md. ok is false when the page does not exist, which is permitted: a repository need not have one until a term is added.

func (*Repo) Next

func (r *Repo) Next(t Type, title string) (id, path string, err error)

Next returns the identifier and path a new document of this type would take. The number is one past the highest in the directory, so a gap left by a document that was never written is not reused.

func (*Repo) Term

func (r *Repo) Term(name string) (GlossaryEntry, bool)

Term finds a glossary entry by name, case-insensitively as uniqueness is defined. It is a map lookup rather than a scan, because link asks once per candidate on every prose line.

func (*Repo) WriteFile

func (r *Repo) WriteFile(docPath string, source []byte) error

WriteFile puts a rewritten document back where it came from. Writing through the repository keeps the path translation and the permission bits in one place rather than at every call site that rewrites a document.

type Section

type Section struct {
	Heading Heading
	// Text is the section's body as written, which is what a rewrite needs.
	Text string
	// contains filtered or unexported fields
}

Section is a heading and everything under it, up to the next heading of equal or higher level.

func (Section) Empty

func (s Section) Empty() bool

Empty reports whether a section holds nothing but whitespace and HTML comments. This is what makes a comment count as nothing throughout: the same definition drives L14 and the pre-freeze gate.

type Status

type Status string

Status is the lifecycle status of an RFC or ADR. Spec pages and refs have none.

const (
	StatusDraft     Status = "draft"
	StatusProposed  Status = "proposed"
	StatusAccepted  Status = "accepted"
	StatusRejected  Status = "rejected"
	StatusWithdrawn Status = "withdrawn"
)

func StatusIn

func StatusIn(source []byte) Status

StatusIn reports the status recorded in a document's front matter, without reading the rest of it. L11 uses it to ask what a document looked like on the branch.

func (Status) Terminal

func (s Status) Terminal() bool

Terminal reports whether a status is final. A document in a terminal status is frozen once it reaches the branch.

type Type

type Type string

Type is one of the four kinds of document, and also the directory each kind lives in.

const (
	TypeRFC  Type = "rfc"
	TypeADR  Type = "adr"
	TypeRef  Type = "ref"
	TypeSpec Type = "spec"
)

func ParseIdentifier

func ParseIdentifier(id string) (Type, int, bool)

ParseIdentifier reads an identifier back into the type and number it names.

func ParseType

func ParseType(s string) (Type, bool)

ParseType turns user input into a Type. Type is a string type with no constructor, so Type("nonsense") is otherwise a perfectly valid value and every caller would need its own list of the real ones.

func TypeOf

func TypeOf(p string) (Type, bool)

TypeOf classifies a path relative to root. ok is false when the path is not a document: the wrong extension, outside the four directories, or nested in a type that does not permit subdirectories.

func (Type) HasLifecycle

func (t Type) HasLifecycle() bool

HasLifecycle reports whether documents of this type move through statuses. A spec page reflects what has already been decided and a ref records something outside the project, so neither has a status at all.

func (Type) Normative

func (t Type) Normative() bool

Normative reports whether a document of this type may be named in a depends or includes list. Those name designs and decisions; a ref is informational and is cited from the body instead.

It answers the same as HasLifecycle today and is a separate question: one is about whether a document can change state, the other about whether anything may rest on it. A fifth type would very likely separate them.

func (Type) Numbered

func (t Type) Numbered() bool

Numbered reports whether documents of this type carry an identifier derived from a number in the filename. Spec pages are named by path instead.

type UnknownKey

type UnknownKey struct {
	Name string
	Line int
}

UnknownKey is a front matter key belonging to no schema, and where it was written.

type WikiLink struct {
	Name string
	Line int
}

WikiLink is an unexpanded [[...]] reference.

Jump to

Keyboard shortcuts

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