graph

package
v0.5.0 Latest Latest
Warning

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

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

Documentation

Overview

Package graph builds the two-layer document graph and answers every question asked of it: invariants, reachability, resolution and degree statistics.

Index

Constants

View Source
const (
	ProjectionTrue  = config.ProjectionTrue
	ProjectionFalse = config.ProjectionFalse
)

A projection reads as a boolean attribute: the string "true" where it holds and "false" where it does not, so attr: {enforced: {eq: "true"}} and attr: {enforced: {not: "true"}} both say what they look like. The words themselves are the configuration's, so they are named there.

View Source
const TopReferencedLimit = 10

TopReferencedLimit caps the reference-layer in-degree ranking.

Variables

This section is empty.

Functions

func Adjacency

func Adjacency(g *model.Graph, types ...model.EdgeType) map[model.ID][]model.ID

Adjacency returns from-to adjacency over the given edge types, or over every typed edge when none are given. Neighbour lists are sorted.

func Ancestors

func Ancestors(g *model.Graph, id model.ID, types ...model.EdgeType) ([]model.ID, error)

Ancestors returns every document reachable from id by walking typed edges backwards, sorted and excluding id itself.

func AsOfDay added in v0.3.0

func AsOfDay(asOf time.Time) string

AsOfDay renders the day a run is about the way frontmatter writes one. Days are compared as text because ISO 8601 dates sort chronologically: there is no clock arithmetic to get wrong, and no timezone can carry a corpus past a deadline its reader has not reached. The zero time means today, which is what a caller with no opinion about the date means.

It is exported because every report that carries an as-of date has to write the same day the checks compared against, down to the spelling.

func Binding

func Binding(g *model.Graph, cfg config.Config, id model.ID, asOf time.Time) bool

Binding reports whether a document is binding on the day asked about: it satisfies the projection the configuration names under binding. A caller asking about many documents wants BindingSet, which evaluates the projections once.

func BindingSet

func BindingSet(g *model.Graph, cfg config.Config, asOf time.Time) []model.ID

BindingSet lists every document binding on the day asked about, sorted.

func Build

func Build(docs []*parse.Document, cfg config.Config) *model.Graph

Build assembles the typed constraint layer and the untyped reference layer from parsed documents, recording structural findings it observes on the way.

func Check

func Check(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

Check runs every built-in structural check. These cannot be disabled. asOf is the one day every time-dependent check compares against — the field sunsets, the periods, and everything the periods decide — and the zero time means today.

func CheckCardinality added in v0.2.0

func CheckCardinality(g *model.Graph, cfg config.Config) []model.Finding

CheckCardinality reports documents whose edge degree leaves the bounds the configuration puts on an edge type.

A bound speaks about the documents that may hold that degree, so where an edge names its endpoint kinds the bound is read over those kinds alone: the outbound bounds over `from:`, the inbound ones over `to:`. That is what makes `min_outbound: 1` on an edge from one kind sayable at all — a lower bound is the one bound a document with no such key can violate, so without the scoping it would report every document of every other kind, the edge's own targets included. A document of another kind that does hold such an edge is an edge_kind_mismatch, which says the actual mistake.

func CheckCycles

func CheckCycles(g *model.Graph, cfg config.Config) []model.Finding

CheckCycles reports one finding per cycle found in an acyclic edge type, and per cycle that only the union of those types closes.

func CheckDangling

func CheckDangling(g *model.Graph, cfg config.Config) []model.Finding

CheckDangling reports typed edges with an endpoint that is not a known document. Either endpoint can be the unknown one: a reverse-direction edge, such as the MADR "superseded by <ref>" status, puts the referenced document at the source. The finding is filed against the document that declared it.

func CheckDeprecatedFields added in v0.3.0

func CheckDeprecatedFields(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

CheckDeprecatedFields reports documents whose frontmatter still writes a key the configuration retired. asOf is the day the sunsets are compared against: it is handed in rather than read from the clock so a report is reproducible, and so the as-of projection a later ADR adds has one seam to arrive through. The zero time means today, which is what a caller with no opinion means.

func CheckDerived

func CheckDerived(g *model.Graph, cfg config.Config) []model.Finding

CheckDerived reports derived edges that contradict the structured edges and warns wherever a derived edge stands in for structured frontmatter.

func CheckDocuments

func CheckDocuments(docs []*parse.Document, cfg config.Config) []model.Finding

CheckDocuments reports the file-level structural findings that the graph container cannot express: id collisions, undecodable and absent frontmatter.

func CheckEdgeKinds added in v0.3.0

func CheckEdgeKinds(g *model.Graph, cfg config.Config) []model.Finding

CheckEdgeKinds reports edges whose endpoints are of a kind the edge does not allow. Only an endpoint the corpus holds is checked: a reference naming no document has no kind to be wrong about, and is a dangling_ref of its own.

func CheckExceptsStrict added in v0.3.0

func CheckExceptsStrict(g *model.Graph, cfg config.Config) []model.Finding

CheckExceptsStrict reports an exception recorded against a strict rule. A defeater does not draw a conclusion of its own; it stops a defeasible one from being drawn, and a strict rule's consequence follows without exception — so an excepts edge pointing at a MUST or a MUST_NOT records something that cannot happen. It is a finding of its own rather than the edge's `target:` because what is wrong is the exception, not a target gone stale.

func CheckFieldValues added in v0.3.0

func CheckFieldValues(g *model.Graph, cfg config.Config) []model.Finding

CheckFieldValues reports what a field declaration says about the value a document writes under it: a value outside a declared vocabulary, and a required key nothing wrote at all. Both read the declarations a document of its kind sees, so a kind that declares a field describes it for its own documents and nobody else's, and both answer for open kinds as well as closed ones: a closed kind is about which keys may appear, and this is about what the declared ones say.

func CheckImmutable added in v0.2.0

func CheckImmutable(repo *vcs.Repo, cfg config.Config, rev, root string) ([]model.Finding, error)

CheckImmutable reports documents that were closed at rev and have since changed in a way the append-only policy forbids. Paths are reported relative to root. A new document is always allowed.

A single-kind corpus is read under its one documents directory. A multi-kind corpus is read only under the kinds that declare append_only: true — the caller refuses a multi-kind configuration that declares none.

func CheckInverse added in v0.2.0

func CheckInverse(g *model.Graph, cfg config.Config) []model.Finding

CheckInverse reports frontmatter that disagrees with the edges it mirrors: an edge whose target does not name its source under the inverse key, and an entry under the inverse key that no edge backs.

func CheckModalityConflicts added in v0.3.0

func CheckModalityConflicts(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

CheckModalityConflicts reports the pairs of binding clauses whose modalities cannot both hold about a subject they share. It answers only for a configuration that declares the `about` edge and the `modality` field: what two clauses are about, and at what strength, is what the check is made of.

func CheckPathConstraints added in v0.3.0

func CheckPathConstraints(g *model.Graph, cfg config.Config) []model.Finding

CheckPathConstraints reports documents from which one path of edges reaches a document the comparison path does not. Neither preset declares any, so a configuration that writes none pays one length check for the whole corpus and reports nothing.

func CheckPeriods added in v0.3.0

func CheckPeriods(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

CheckPeriods reports what is wrong with the days a corpus wrote: a value that is not a date, an interval that ends before it begins, an end that disagrees with the successors, and a record whose end has passed while its status still says it is in effect. A corpus whose kinds declare no period sees none of them.

func CheckSections added in v0.5.0

func CheckSections(g *model.Graph, cfg config.Config) []model.Finding

CheckSections enforces required document body sections and their ordering.

func CheckStatusVocabulary

func CheckStatusVocabulary(g *model.Graph, cfg config.Config) []model.Finding

CheckStatusVocabulary reports statuses outside the vocabulary their kind answers to, which is the top-level one wherever a kind declares none.

func CheckTargets added in v0.3.0

func CheckTargets(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

CheckTargets reports edges whose target document does not satisfy the condition its edge spec puts on it. Only an edge that declares `target:` is checked, so a configuration that declares none — both presets did before the spec preset took one — sees nothing new.

The check is local: it asks one question about one document, one hop away. Walking the lineage to a replacement is what the fix suggestion does, and keeping the two apart is what keeps transitive reach out of the vocabulary.

func CheckUnmanaged added in v0.5.0

func CheckUnmanaged(cfg config.Config) []model.Finding

CheckUnmanaged reports Markdown files in the documents directory that are not managed documents and not exempt.

func Descendants

func Descendants(g *model.Graph, id model.ID, types ...model.EdgeType) ([]model.ID, error)

Descendants returns every document reachable from id by walking typed edges forwards, sorted and excluding id itself.

func EvalRule

func EvalRule(g *model.Graph, cfg config.Config, rule config.Rule, asOf time.Time) []model.Finding

EvalRule evaluates one declarative rule over every node.

func EvalRules

func EvalRules(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

EvalRules evaluates every configured rule over every node. The evaluation context is built once: the edge index, the periods and the projections are each linear in the graph, and rebuilding them per rule is not.

func FindCycle

func FindCycle(adj map[model.ID][]model.ID) []model.ID

FindCycle returns a closed cycle path such as [A B A], or nil when the adjacency is acyclic. Iterative three-colour search: document corpora are shallow but a recursive walk would blow the stack on a pathological chain.

func FindCycles

func FindCycles(adj map[model.ID][]model.ID) [][]model.ID

FindCycles returns one cycle path per strongly connected component with a cycle, in deterministic order.

func FixSetsField added in v0.3.0

func FixSetsField(cfg config.Config, rule string) (field, value string, ok bool)

FixSetsField reports the frontmatter key and value a rule's built-in remedy tells the reader to write, and whether the rule has such a remedy at all. It is the whole vocabulary of "set <field>: <value>" suggestions DocDag generates — the two status changes below — and it is exported because the preset lint compares those demands against each other: two rules that can both fire on one document and demand two different values for one key are an ambivalent pair, and reading the demand from here is what keeps the check and the suggestion from drifting apart.

func MatchCondition

func MatchCondition(g *model.Graph, cfg config.Config, cond config.Condition, id model.ID, asOf time.Time) bool

MatchCondition reports whether one node satisfies every clause of a rule condition, the configured projections and the computed in_force included. asOf is the day the periods are read against, the zero time meaning today.

func ProjectionValue added in v0.3.0

func ProjectionValue(held bool) string

ProjectionValue renders one projection result the way an attribute clause, and a listing column, read it.

func ReferenceAdjacency

func ReferenceAdjacency(g *model.Graph) map[model.ID][]model.ID

ReferenceAdjacency returns adjacency over the reference layer only.

func ReferenceNeighbors

func ReferenceNeighbors(g *model.Graph, id model.ID) []model.ID

ReferenceNeighbors returns the reference-layer neighbours of id, sorted.

func Resolve

func Resolve(g *model.Graph, id model.ID, t model.EdgeType) ([]model.ID, error)

Resolve walks forward along the reverse edges of t to the current sink documents. A document with no successors resolves to itself. It reports model.ErrUnknownID for an absent id and model.ErrCycle on a cyclic walk.

It reads every declared successor. A caller that has a configuration and a day in hand wants ResolveAt, which stops where a period says the replacement has not taken effect yet.

func ResolveAt added in v0.3.0

func ResolveAt(g *model.Graph, cfg config.Config, id model.ID, t model.EdgeType, asOf time.Time) ([]model.ID, error)

ResolveAt walks the lineage to the documents that stand in for a reference on one day. A successor replaces its predecessor only once somebody has accepted it and its own period has begun: until then the predecessor is what binds, and resolve says so — which is what keeps it answering the same set `--binding` does.

func Reverse

func Reverse(g *model.Graph, types ...model.EdgeType) map[model.ID][]model.ID

Reverse returns to-from adjacency over the given edge types, or over every typed edge when none are given. Neighbour lists are sorted.

func SortFindings

func SortFindings(findings []model.Finding)

SortFindings orders findings by severity, then path, line, rule, id and detail, so a report reads in file order and diffs cleanly.

func Suggest added in v0.2.0

func Suggest(findings []model.Finding, g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

Suggest fills in the Fix of every finding it recognizes. The checks say what is wrong; this says what to type, as a pass over a finished report so a check never has to carry a remedy. asOf is the day the run is about, so a remedy that walks a lineage stops where the check that reported it stopped.

A finding that arrives carrying one keeps it: where the remedy names the other document of a pair, only the check that paired them knows which, and recovering that from a finished finding would mean reading identifiers back out of prose.

func Summarize

func Summarize(g *model.Graph, findings []model.Finding) model.Summary

Summarize counts documents, typed edges and findings for the summary line. A suppressed finding is not counted: the corpus has already answered it, and the summary is what the exit code is read from.

func Touching added in v0.2.0

func Touching(findings []model.Finding, g *model.Graph, paths []string) []model.Finding

Touching keeps the findings a set of files is about: filed against one of them, related to one of them, or filed against a document one of them borders over a typed edge. A path naming a directory stands for everything under it.

func Validate

func Validate(g *model.Graph, cfg config.Config, asOf time.Time) []model.Finding

Validate runs the structural checks and the configured rules, returning the findings already recorded on the graph too, in deterministic order. asOf is the day the time-dependent checks compare against, the zero time meaning today: the CLI hands over the day it runs on, and a caller with no opinion about the date does not have to invent one.

Types

type DepthCount

type DepthCount struct {
	Depth int `json:"depth"`
	Count int `json:"count"`
}

DepthCount is the number of documents whose supersedes chain has one depth.

type Direction

type Direction string

Direction selects which way a reachability query walks the typed edges.

const (
	DirectionAncestors   Direction = "ancestors"
	DirectionDescendants Direction = "descendants"
)

Query directions.

type EdgeCount

type EdgeCount struct {
	Type  model.EdgeType `json:"type"`
	Count int            `json:"count"`
}

EdgeCount is the number of typed edges of one type.

type FieldUsage added in v0.3.0

type FieldUsage struct {
	Field      string `json:"field"`
	Documents  int    `json:"documents"`
	Deprecated bool   `json:"deprecated"`
	LastChange string `json:"last_change,omitempty"`
}

FieldUsage is how one frontmatter field is used across the corpus: how many documents write it, whether the configuration retired it, and the day a document that writes it last changed. LastChange is empty where no repository answered — a report says what it knows rather than failing for want of git.

func ComputeFieldUsage added in v0.3.0

func ComputeFieldUsage(g *model.Graph, cfg config.Config, changed map[string]string) []FieldUsage

ComputeFieldUsage summarises the frontmatter fields the corpus writes, plus the ones it declares and nobody writes: a migration is finished exactly when a retired field's count reaches zero, so that row has to outlive the last document that carried it. changed maps a document path onto the day it last changed; a path it does not hold contributes no date.

type Layer

type Layer string

Layer marks which graph layer produced a query result.

const (
	LayerTyped     Layer = "typed"
	LayerReference Layer = "reference"
)

Result layers.

type ModalityConflict added in v0.3.0

type ModalityConflict struct {
	A, B                 model.ID
	ModalityA, ModalityB string
	Topics               []model.ID
	Strong               bool
	// Suppressed reports that a weak conflict is defeated by Defeater, the
	// excepts edge recorded between the two clauses.
	Suppressed bool
	Defeater   model.Edge
}

ModalityConflict is one pair of binding clauses that say incompatible things about a subject they share. A and B are ordered by identifier, so the pair is named the same way whichever end the walk reached first, and Topics lists every subject they share, sorted.

Strong marks the pair whose members are both strict rules — a MUST against a MUST_NOT. Defeasible deontic logic's strict rules are the ones a defeater cannot overturn, so a recorded exception does not suppress that pair; every other conflicting pair is weak and an excepts edge between the two, in either direction, is the corpus saying it already knows.

func ModalityConflicts added in v0.3.0

func ModalityConflicts(g *model.Graph, cfg config.Config, asOf time.Time) []ModalityConflict

ModalityConflicts returns every conflicting pair of binding clauses, ordered by the pair's own identifiers. It is exported because a conflict is a fact about the corpus rather than only a finding: `context` reports the suppressed ones as part of a clause's neighbourhood and `stats` counts them.

The pairing is per subject and quadratic in the clauses that share one, which is the granularity the topics are cut at: a subject a paragraph defines carries a handful of clauses, so the walk is linear in practice. A vault that hangs a hundred clauses off one topic has a topic that says too little, which is a matter for the preset lint rather than for a cleverer algorithm here.

func (ModalityConflict) Detail added in v0.3.0

func (c ModalityConflict) Detail() string

Detail is what the finding says: the two modalities, the clause the reader is not looking at, and the subject they collide over. A suppressed conflict says what is holding it down on the same line, because that is the whole of what the reader has to check.

func (ModalityConflict) Fix added in v0.3.0

func (c ModalityConflict) Fix() string

Fix says what to type. A weak conflict is settled by recording the exception or by revising a modality; a strong one only by the revision, because the exception it would take is one excepts_strict refuses. A conflict already answered has nothing to type: telling a reader to declare the edge they are looking at would be worse than saying nothing.

func (ModalityConflict) Suppression added in v0.3.0

func (c ModalityConflict) Suppression() string

Suppression is the one line that says which recorded exception defeats a conflict, and what scope it was recorded under. It is empty for a conflict nothing defeats.

type ModalityCount added in v0.3.0

type ModalityCount struct {
	Modality string `json:"modality"`
	Count    int    `json:"count"`
}

ModalityCount is how many documents state one modality. Every declared value is a row, at zero where nobody states it: a standard with no MUST_NOT is a fact about the standard, and a missing row would read as a corpus that has not been asked.

type PendingSuccessor added in v0.5.0

type PendingSuccessor struct {
	ID          model.ID `json:"id"`
	Predecessor model.ID `json:"predecessor"`
	Status      string   `json:"status"`
}

PendingSuccessor is a successor that was not followed during resolution because its status is not currently binding (e.g. proposed, rejected, withdrawn) or its period has not yet begun.

func ResolveWithPending added in v0.5.0

func ResolveWithPending(g *model.Graph, cfg config.Config, id model.ID, t model.EdgeType, asOf time.Time) ([]model.ID, []PendingSuccessor, error)

ResolveWithPending walks the lineage along reverse edges of t from id to the current binding documents, stopping before any successor whose status is not binding-eligible (e.g. proposed, rejected, withdrawn) or not in force on asOf. Skipped successors are reported in the returned PendingSuccessor slice.

type Periods added in v0.3.0

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

Periods is what a corpus's periods say on one day: which documents are in force, and what is wrong with the days they wrote. It is computed before the projections, because a projection may read in_force and nothing the periods read is derived.

func EvalPeriods added in v0.3.0

func EvalPeriods(g *model.Graph, cfg config.Config, asOf time.Time) Periods

EvalPeriods computes the interval every document is in force for and answers it against one day. asOf is the day the corpus is being asked about, the zero time meaning today.

func (Periods) Day added in v0.3.0

func (p Periods) Day() string

Day is the day the periods were evaluated for, written as a frontmatter writes one.

func (Periods) Declared added in v0.3.0

func (p Periods) Declared(id model.ID) bool

Declared reports whether a document's kind declares a period, which is what makes its force a question about a day rather than a constant.

func (Periods) Ended added in v0.3.0

func (p Periods) Ended(id model.ID) (string, bool)

Ended reports the day a document's own frontmatter says it ends on, and whether it says one at all. Only the day the document wrote counts: an end derived from a successor is what supersession already reports.

func (Periods) InForce added in v0.3.0

func (p Periods) InForce(id model.ID) bool

InForce reports whether a document is in force on the day the periods were evaluated for: its period has begun and has not ended, over the closed-open interval [from, until). A document whose kind declares no period is always in force.

type Projections added in v0.3.0

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

Projections holds the evaluated projections of one graph: for every declared projection, the documents it holds for.

func EvalProjections added in v0.3.0

func EvalProjections(g *model.Graph, cfg config.Config, asOf time.Time) Projections

EvalProjections evaluates every configured projection over every document, in dependency order, so a projection that reads another as an attribute sees the evaluated value rather than an absent one. asOf is the day the periods a projection may read are answered for, the zero time meaning today.

func (Projections) Declares added in v0.3.0

func (p Projections) Declares(name string) bool

Declares reports whether a name is a projection at all, which is what makes it a virtual attribute rather than a frontmatter key.

func (Projections) Holds added in v0.3.0

func (p Projections) Holds(name string, id model.ID) bool

Holds reports whether a projection holds for one document. A name no configuration declares holds nowhere.

func (Projections) Names added in v0.3.0

func (p Projections) Names() []string

Names returns the declared projections in configuration order.

func (Projections) Set added in v0.3.0

func (p Projections) Set(name string) []model.ID

Set lists the documents a projection holds for, sorted.

type QueryOptions

type QueryOptions struct {
	Direction   Direction
	Types       []model.EdgeType
	IncludeRefs bool
}

QueryOptions parameterises a reachability query.

type QueryResult

type QueryResult struct {
	ID    model.ID `json:"id"`
	Layer Layer    `json:"layer"`
}

QueryResult is one reachable document and the layer it was reached through.

func Query

func Query(g *model.Graph, id model.ID, opts QueryOptions) ([]QueryResult, error)

Query runs a reachability query and overlays reference-layer neighbours when asked for them.

type ReferenceCount

type ReferenceCount struct {
	ID    model.ID `json:"id"`
	Count int      `json:"count"`
}

ReferenceCount is the reference-layer in-degree of one document.

type Statistics

type Statistics struct {
	Documents           int              `json:"documents"`
	Edges               []EdgeCount      `json:"edges"`
	Binding             int              `json:"binding"`
	ChainDepth          []DepthCount     `json:"chain_depth"`
	Orphans             int              `json:"orphans"`
	OrphanRate          float64          `json:"orphan_rate"`
	TopReferenced       []ReferenceCount `json:"top_referenced"`
	Topics              []TopicCount     `json:"topics,omitempty"`
	Modalities          []ModalityCount  `json:"modalities,omitempty"`
	SuppressedConflicts int              `json:"suppressed_conflicts,omitempty"`
}

Statistics is the degree-based corpus summary. Every field is computable in O(V+E); nothing here needs a full path enumeration.

The last three answer only for a corpus that declares the modality vocabulary and the subject edge, and are left out of the report entirely otherwise.

func ComputeStats

func ComputeStats(g *model.Graph, cfg config.Config, asOf time.Time) Statistics

ComputeStats summarises the graph on one day: what is binding, and how many conflicts a recorded exception defeats, are answers about a moment wherever a kind declares a period.

type TopicCount added in v0.3.0

type TopicCount struct {
	Topic   model.ID `json:"topic"`
	Clauses int      `json:"clauses"`
}

TopicCount is how many clauses speak to one subject. It is what a corpus watches its topic granularity with: a subject a paragraph defines carries a handful of clauses, and one carrying dozens is a subject that says too little to compare clauses under.

Jump to

Keyboard shortcuts

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