Documentation
¶
Overview ¶
Package tree loads aiwf entities from a consumer repository's directory tree into an in-memory model.
The loader is deliberately tolerant: per-file parse errors are collected and returned as LoadErrors alongside the (possibly partial) tree, not folded into a single failure. This matches the framework's "errors are findings" principle — `aiwf check` reports inconsistent state, it does not refuse to start.
Index ¶
- func Load(ctx context.Context, root string) (*Tree, []LoadError, error)
- type LoadError
- type Tree
- func (t *Tree) ByID(id string) *entity.Entity
- func (t *Tree) ByIDAll(id string) []*entity.Entity
- func (t *Tree) ByKind(k entity.Kind) []*entity.Entity
- func (t *Tree) ByPriorID(id string) *entity.Entity
- func (t *Tree) FilterByKindStatuses(k entity.Kind, statuses ...string) []*entity.Entity
- func (t *Tree) HasPlannedFile(path string) bool
- func (t *Tree) ReachesScope(target, scopeEntity string) bool
- func (t *Tree) ReachesScopeAny(targets []string, scopeEntity string) bool
- func (t *Tree) ReferencedBy(id string) []string
- func (t *Tree) ResolveByCurrentOrPriorID(id string) *entity.Entity
- func (t *Tree) ResolvedArea(e *entity.Entity) string
- func (t *Tree) ResolvedAreaByID(id string) string
- func (t *Tree) TrunkIDStrings() []string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Load ¶
Load walks the consumer repo's entity-bearing directories and parses every recognized entity file. Per-file errors are returned in the LoadError slice; the (*Tree) is always populated with whatever could be parsed, even if some files failed.
The third return is reserved for fatal errors that prevent the walk from completing (e.g., a permission error on a parent directory). A missing entity-bearing directory is not fatal — fresh repos may not yet have one.
Types ¶
type LoadError ¶
LoadError is a per-file error encountered during loading. The loader collects these instead of aborting; checks surface them as findings.
type Tree ¶
type Tree struct {
// Root is the absolute path to the consumer repo root the tree was
// loaded from.
Root string
// Entities holds every successfully-parsed entity. Order is the
// order encountered during the directory walk, which is stable
// across runs but not otherwise specified.
Entities []*entity.Entity
// Stubs holds entities whose source file failed to parse. Each
// carries only id (derived from the path), kind, and path; body
// fields are zero. Stubs are deliberately not in Entities so that
// frontmatter-shape, status-valid, and similar body-level checks
// don't emit spurious findings. They exist so reference resolution
// can still locate a target by id, preventing one file's parse
// failure from cascading into "unresolved reference" findings on
// every entity that links to it. The original parse failure is
// reported as a load-error finding.
Stubs []*entity.Entity
// PlannedFiles records repo-relative file paths (forward-slash form)
// that a verb plans to write but hasn't yet. Used by checks that
// otherwise consult disk so that validate-then-write verbs can
// validate the projected world, including files about to be created.
// Loaded trees leave this nil.
PlannedFiles map[string]struct{}
// ReverseRefs maps each entity id (and each composite id mentioned
// as a reference target) to the ids of entities that reference it.
// Built from entity.ForwardRefs at Load time; consumed by aiwf show's
// referenced_by field, by aiwf check audits ("ADR is unreferenced"),
// and by the I2.5 provenance scope-reachability check.
//
// Composite-id targets roll up to their parent: a gap with
// `addressed_by: M-007/AC-1` appears in the AC's referrer list AND
// in M-007's referrer list. Each value-slice is sorted ascending
// and de-duplicated for stable output.
ReverseRefs map[string][]string
// TrunkIDs is the entity-id set observed in the configured trunk
// ref's tree, used by AllocateID (so a new id can't collide with
// trunk) and by the ids-unique check (so a working-tree id that
// also exists at a different path on trunk surfaces as a finding
// before push).
//
// Tree.Load does not populate this field — the cmd dispatcher reads
// the trunk via the trunk package once per verb run and assigns
// here so the verb's projection check sees the same trunk view.
// Tests that build trees in-memory leave TrunkIDs nil, in which
// case the allocator and the check rule degrade to working-tree-
// only behavior (the previous default).
TrunkIDs []trunk.ID
// TrunkRef is the resolved trunk ref name (e.g.
// "refs/remotes/origin/main") or empty when no trunk read
// happened. The reallocate tiebreaker uses it as the second
// argument to `git merge-base --is-ancestor` when two entities
// collide on an id and the verb has to pick which side to
// renumber. Populated alongside TrunkIDs by the cmd dispatcher;
// empty in tests that don't set it (the verb falls back to
// today's "ambiguous, pass a path" error in that case).
TrunkRef string
// TrunkRenames maps a pre-rename path on TrunkRef to the
// corresponding post-rename path in the working tree, as detected
// by `git diff -M` (gitops.RenamesFromRef). Used by the ids-unique
// trunk-collision check to recognize that a branch-side slug
// rename of an existing entity is the same entity moved, not a
// duplicate id allocation (G-0109).
//
// Populated alongside TrunkIDs by the cmd dispatcher. Tests that
// build trees in-memory leave it nil, in which case the rule
// degrades to today's behavior (every different-path same-id pair
// surfaces as a collision, modulo the archive-sweep exception).
TrunkRenames map[string]string
// Strays holds repo-relative file paths (forward-slash form) that
// the loader walked under work/{epics,gaps,decisions,contracts}/
// but could not classify as a recognized entity file via
// entity.PathKind. The tree-discipline check (G40) reports each as
// `unexpected-tree-file`; without that field the loader's silent
// skip would let any LLM-written stray inside those subtrees
// linger undetected.
//
// Scope is the four entity-bearing subdirs, not all of work/.
// Files at the work/ root or under non-entity sibling dirs
// (work/migration/, work/scratch/, etc.) are outside the loader's
// walk roots and never appear here. docs/adr/ is walked but
// conventionally permissive (READMEs, templates, etc.), so its
// strays are not tracked. Files inside a contract's directory
// (work/contracts/C-NNN-*/) are recorded here but filtered by the
// check rule, since contracts legitimately carry schema/fixture
// artifacts alongside contract.md.
Strays []string
}
Tree is the in-memory representation of every aiwf entity discovered in a consumer repository.
func (*Tree) ByID ¶
ByID returns the first entity matching the id, or nil if absent. In a tree with duplicate ids (which the ids-unique check reports), ByID returns one; iterate Entities to enumerate all.
Both the query id and each candidate entity's id are run through entity.Canonicalize before comparison, so a narrow legacy query (`E-22`) resolves the same canonical entity as `E-0022`. This is the AC-2 lookup-seam canonicalization: the grammar accepts both widths, the loader stores whatever shape was on disk, and the lookup compares canonical form.
func (*Tree) ByIDAll ¶
ByIDAll returns every entity matching the id, in tree-walk order. Used by `aiwf reallocate` to detect the duplicate-id case so the trunk-ancestry tiebreaker can run; ByID alone would silently pick one and obscure that there's a choice to make.
Comparison goes through entity.Canonicalize on both sides — see the docstring on ByID for the AC-2 width-tolerance rule.
func (*Tree) ByPriorID ¶
ByPriorID returns the entity whose `prior_ids` lineage list includes id, or nil if no entity claims that id as a prior. When multiple entities claim the same prior id (a hand-edit accident or the rare lineage-broken case), ByPriorID returns the first match in tree-walk order; iterate Entities to enumerate all.
Comparison goes through entity.Canonicalize on both sides — see the docstring on ByID for the AC-2 width-tolerance rule.
func (*Tree) FilterByKindStatuses ¶
FilterByKindStatuses returns entities whose Kind matches k (when k is non-empty) and whose Status appears in statuses (when statuses is non-empty), sorted by ID ascending. Empty k means "any kind"; empty statuses means "any status". Used by both `aiwf list --kind X --status Y` and `aiwf status`'s per-section slices so the two verbs cannot drift on the same query.
func (*Tree) HasPlannedFile ¶
HasPlannedFile reports whether path (forward-slash, repo-relative) appears in PlannedFiles. Safe to call when PlannedFiles is nil.
func (*Tree) ReachesScope ¶ added in v0.9.0
ReachesScope reports whether `target` is within the scope-tree rooted at `scopeEntity`, per D-0006's three-edge reachability model. Unlike the (deprecated) full-graph Reaches, exactly three edges traverse:
- parent forward — target's parent chain reaches scopeEntity.
- composite rollup — an AC `M/AC-N` is reachable iff M is.
- discovered_in rev — target's discovered_in points into the scope subtree (one hop, then parent-climb).
No governance edge (depends_on, addressed_by, relates_to, supersedes, superseded_by, linked_adrs) traverses — scope is a governance boundary, not the full reference grammar. The function reads e.Parent / e.DiscoveredIn directly rather than filtering ForwardRefs, so a future governance edge cannot silently re-broaden it.
func (*Tree) ReachesScopeAny ¶ added in v0.9.0
ReachesScopeAny reports whether any of `targets` is within the scope-tree rooted at `scopeEntity` (the creation-act variant: a new entity's proposed outbound references are evaluated as a set).
func (*Tree) ReferencedBy ¶
ReferencedBy returns the ids of entities that reference id, in sorted order. Returns nil when no entity references id.
The lookup canonicalizes id first so a narrow query resolves to referrers of the canonical form (the AC-2 lookup-seam rule); the reverse-ref map itself is keyed by canonical id (see buildReverseRefs).
func (*Tree) ResolveByCurrentOrPriorID ¶
ResolveByCurrentOrPriorID resolves id to an entity by trying ByID first (current id), then ByPriorID (lineage match). Returns nil when neither matches. Used by `aiwf history` so a query for an old id transparently resolves to the current entity, then the caller walks the chain via the entity's PriorIDs slice.
func (*Tree) ResolvedArea ¶ added in v0.17.0
ResolvedArea returns the area an entity belongs to for grouping and filtering (E-0043, M-0171/AC-3). Root kinds (epic, ADR, gap, decision, contract) carry their own `area`; a milestone derives its area from its parent epic and never stores its own (the loader blanks any stored value). An acceptance criterion's area is its parent milestone's resolved area — callers resolve the milestone and call this. Returns "" when no area applies: an untagged entity, or a milestone whose parent epic is untagged or does not resolve.
func (*Tree) ResolvedAreaByID ¶ added in v0.17.0
ResolvedAreaByID resolves the effective area for any id, including a composite acceptance-criterion id (M-NNNN/AC-N rolls up to the parent milestone's resolved area via entity.CompositeRoot). A bare id passes through unchanged. Returns "" when the id does not resolve. This is the single entry point downstream filter and grouping consume, so AC area is not re-derived per verb (E-0043, M-0171/AC-3).
func (*Tree) TrunkIDStrings ¶
TrunkIDStrings returns the id strings from TrunkIDs. Convenience for AllocateID, which only needs id values; the full trunk.ID is kept on the tree so the ids-unique check can include the trunk-side path in its finding message.