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) AllocationIDs() []string
- 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
// TrunkCollisionRenames maps a pre-rename path to its post-rename
// path, both as observed by the ids-unique trunk-collision rule
// (its one and only consumer, today and after G-0378): a
// same-id-different-path pair whose paths appear here as a match
// is the same entity moved, not a duplicate id allocation. Always
// keyed by trunk's current path and valued by the working tree's
// current path, regardless of which side's git history explained
// the rename.
//
// Populated by two mechanisms merged into one map (ADR-0031): a
// branch-side detector (gitops.RenamesFromRef — trailer walk + a
// `git diff -M` content-similarity fallback, G-0109) for renames
// the branch itself committed, and a trunk-side detector
// (gitops.TrunkRenamesFromRef — trailer walk only, no -M fallback,
// G-0378) for renames committed directly on trunk after the branch
// forked. The cmd dispatcher gates both behind
// check.DisputedTrunkIDs, an in-memory, git-free predicate: when
// no working-tree id is disputed against trunk, neither detector
// runs and this field stays nil.
//
// 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).
TrunkCollisionRenames 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
// LocalRefIDs is the entity-id set observed across every local
// branch ref (refs/heads/*), populated alongside TrunkIDs by the
// cmd dispatcher (M-0212). It feeds AllocationIDs — the allocator's
// broadened cross-branch view, which catches a sibling git
// worktree's freshly-committed id before it collides — but NOT the
// ids-unique check, which stays on its working-tree-vs-trunk basis
// (E-0052 decision: the widened set is allocation-only). Tests that
// build trees in-memory leave it nil, degrading the allocator to
// {working-tree + trunk} behavior.
LocalRefIDs []string
// RemoteRefIDs is the entity-id set observed across every
// remote-tracking ref (refs/remotes/*), populated alongside TrunkIDs
// by the cmd dispatcher (M-0214). The remote-side mirror of
// LocalRefIDs: it feeds AllocationIDs so an entity pushed to any
// remote branch (a teammate's not-yet-merged work) is skipped at
// allocation, but NOT the ids-unique check, which stays on its
// working-tree-vs-trunk basis (G-0316). Tests that build trees
// in-memory leave it nil.
RemoteRefIDs []string
// CrossBranchHits is the union of every local-branch-ref and
// remote-tracking-ref hit (trunk.LocalRefHits ++ trunk.RemoteRefHits,
// M-0259/AC-1), carrying kind/path/ref per hit rather than bare id
// strings. refs-resolve and body-prose-id consult it as a second-tier
// resolver on a local-tree miss, before firing unresolved (ADR-0030,
// M-0259/AC-2): a hit here classifies as the non-blocking
// cross-branch-pending subcode instead. Recomputed fresh on every
// `aiwf check` run (no cache), so a source branch's disappearance
// re-escalates the next reference resolution to unresolved on its own
// (M-0259/AC-4) — nothing here needs its own escalation-tracking
// mechanism.
//
// Populated alongside LocalRefIDs/RemoteRefIDs by the cmd dispatcher.
// Tests that build trees in-memory leave it nil, degrading resolution
// to today's two-tier (working tree, unresolved) behavior.
CrossBranchHits []trunk.RefHit
// CrossBranchCollisions is the canonicalized-id set for which
// CrossBranchHits carries divergent blob content across two or more
// refs (computed by trunk.ScanCrossBranch, M-0259/AC-3, G-0415).
// Divergence
// alone is ambiguous — it can mean a genuine duplicate-mint
// collision, or just an ordinary same-entity edit still unmerged on
// a sibling branch/worktree (D-0036) — so refs-resolve and
// body-prose-id escalate a hit here to the distinct, visible
// cross-branch-collision subcode instead of the ordinary
// cross-branch-pending one, but both are non-blocking warnings; a
// genuine duplicate mint is still caught, just later, by the
// blocking ids-unique/trunk-collision check once both copies land
// in a shared tree. Tests that build trees in-memory leave this
// nil, degrading every cross-branch hit to the pending tier (the
// pre-AC-3 default).
CrossBranchCollisions map[string]bool
}
Tree is the in-memory representation of every aiwf entity discovered in a consumer repository.
func (*Tree) AllocationIDs ¶ added in v0.20.0
AllocationIDs returns the id set the allocator must skip past: the union of the configured trunk ref's ids (TrunkIDStrings), every local branch ref's ids (LocalRefIDs, M-0212), and every remote-tracking ref's ids (RemoteRefIDs, M-0214). This is deliberately broader than TrunkIDs alone — the ids-unique check reads TrunkIDs directly and must NOT see LocalRefIDs/RemoteRefIDs (folding sibling branches into the uniqueness comparison would false-flag the same entity present on two branches; E-0052 takes only the prevention half). The trunk ref's ids now appear in both TrunkIDStrings and RemoteRefIDs (trunk is one of the refs/remotes/* the remote scan reads) — keeping TrunkIDStrings is deliberate defensive layering: if the remote scan degrades to nil (offline odd state) trunk still contributes. Duplicates across the three sources are harmless: AllocateID takes the max.
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.