Documentation
¶
Overview ¶
Package entityview holds the read-side projection helpers shared by aiwf's read verbs (show, history, render, check, status): parsing an entity's lifecycle out of git log trailers, assembling the scope table that describes which authorization grants touched it, and reading an entity file's body prose. The package is free of internal/cli dependencies (no Cobra, no cliutil) so it can be unit-tested and reused without pulling in a verb's command-wiring package.
Index ¶
- func HasAuthorizedBy(events []HistoryEvent) bool
- func HasOwnScope(events []HistoryEvent) bool
- func HasScopeData(events []HistoryEvent) bool
- func LastEventSHA(s *scope.Scope, match scope.State) string
- func LookupCommitDateCached(ctx context.Context, root, sha string, cache map[string]string) string
- func ReadEntityBody(root, relPath string) []byte
- func ShortHash(sha string) string
- func SplitMultiValueTrailer(raw string) []string
- func StripTrailers(body string) string
- type HistoryEvent
- type ScopeView
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HasAuthorizedBy ¶
func HasAuthorizedBy(events []HistoryEvent) bool
HasAuthorizedBy reports whether any event carries an `aiwf-authorized-by` reference — i.e. the entity was worked under a foreign authorization scope. It is the guard for `aiwf show`'s global authorize-opener grep: source (a) — resolving which foreign scope authorized this entity's work — is the only consumer that needs the repo-wide opener map. Scopes opened directly on the entity (source (b)) come from the entity's own history via cliutil.LoadEntityScopes, so an active direct-scope opener (which has no `aiwf-authorized-by`) must NOT trigger the global grep.
func HasOwnScope ¶
func HasOwnScope(events []HistoryEvent) bool
HasOwnScope reports whether any event is an authorize-opener on the entity itself (aiwf-verb: authorize + aiwf-scope: opened) — i.e. at least one scope was opened directly on it. It is show's guard for the per-entity cliutil.LoadEntityScopes(id) walk (source (b)): an entity whose own history carries no authorize-opener has no direct scopes, so that walk is skipped. Combined with the HasAuthorizedBy guard on the global grep, a scopeless entity resolves no scope table without any git read beyond the history it already loaded — the walk skipped by this guard is what makes `aiwf show` on a scopeless entity as cheap as `aiwf history` (E-0054 / M-0223).
The predicate is exact: cliutil.LoadEntityScopes builds a scope only from an authorize+opened commit carrying aiwf-entity: id, and ReadHistory greps the same aiwf-entity: id (width-tolerantly), so the opener appears in both — if LoadEntityScopes would return a scope, HasOwnScope sees its opener event.
func HasScopeData ¶
func HasScopeData(events []HistoryEvent) bool
HasScopeData reports whether any event carries scope provenance the history text renderer resolves via the global authorize-opener map: an `aiwf-authorized-by` reference or an `aiwf-scope-ends` terminator. It is the guard for `aiwf history`'s scope-entity map grep. When it returns false, the chip renderer never reads the map — an authorize opener's own `[scope: opened]` chip renders from e.Scope without the map — so the grep is pure waste.
func LastEventSHA ¶
LastEventSHA returns the SHA of the latest event in s whose state equals match, or "" when none. Used by ScopeView assembly to look up the ending commit's date (when the scope is ended).
func LookupCommitDateCached ¶
LookupCommitDateCached returns the ISO-8601 author date of the commit at sha, caching results so we never hit `git show` twice for the same SHA in one show call. Errors fall back to an empty string (the caller renders dates as omitempty in JSON).
func ReadEntityBody ¶
ReadEntityBody reads the entity file at root/relPath and returns the body bytes (the prose after the closing `---`). Errors are swallowed — `aiwf show` already emits findings for unreadable / malformed entities via the load-error finding; surfacing the same problem on the body field would double-count. Empty body or missing file produces nil.
Entity.Path is repo-relative (the loader normalizes it that way) so callers must join with root before hitting the filesystem; doing the join in this helper keeps each caller from re-deriving it.
func ShortHash ¶
ShortHash returns the first 7 hex digits of a SHA, the conventional short form. Falls back to the full hash if it is shorter.
func SplitMultiValueTrailer ¶
SplitMultiValueTrailer splits a `git log %(trailers:key=..., valueonly=true,unfold=true)` cell into one entry per repeated trailer. Multi-value trailers (notably aiwf-scope-ends) are rendered newline-separated by git; we split, trim, and drop empty entries.
func StripTrailers ¶
StripTrailers removes the trailing trailer block from a commit body. `git log %(body)` includes everything after the subject and the separating blank line, including trailers; we only want the prose.
The heuristic walks backward through a contiguous run of trailer-shape `<Token>: <value>` lines at the end of the body. The run is only treated as a trailer block when (a) the run is preceded by a blank line or is the entire body, and (b) the run contains at least one `aiwf-*` trailer. The aiwf-* marker is what distinguishes real trailers (which we always emit) from body prose that happens to look like a trailer (e.g. "decided: 30 days" written by a human).
Types ¶
type HistoryEvent ¶
type HistoryEvent struct {
Date string `json:"date"`
Actor string `json:"actor"`
Verb string `json:"verb"`
Detail string `json:"detail"`
Commit string `json:"commit"`
Body string `json:"body,omitempty"`
To string `json:"to,omitempty"`
Force string `json:"force,omitempty"`
AuditOnly string `json:"audit_only,omitempty"`
Principal string `json:"principal,omitempty"`
OnBehalfOf string `json:"on_behalf_of,omitempty"`
AuthorizedBy string `json:"authorized_by,omitempty"`
Scope string `json:"scope,omitempty"`
ScopeEnds []string `json:"scope_ends,omitempty"`
Reason string `json:"reason,omitempty"`
Tests *gitops.TestMetrics `json:"tests,omitempty"`
}
HistoryEvent is one line of `aiwf history`. The JSON representation is the structured form callers consume.
Body carries the commit's free-form body — typically the human's `--reason` for a status transition, or empty when the verb wasn't invoked with one. Trailers are stripped before storage so Body is pure prose.
To is the target status of a `promote` event, extracted from the `aiwf-to:` trailer (added in I2). Empty for non-promote events and for pre-I2 promote commits that were written before the trailer schema landed; the renderer shows a dash for those rows.
Force is the reason value of an `aiwf-force:` trailer. Empty for non-forced transitions; non-empty marks the event as having bypassed the FSM's transition-legality rule.
AuditOnly is the reason value of an `aiwf-audit-only:` trailer (I2.5 G24 recovery mode). Empty for normal verb commits; non-empty marks the event as a backfilled audit trail for state that was reached via a manual commit. Renders as a `[audit-only: <reason>]` chip in text output, mirroring the `[forced: ...]` rendering.
Principal, OnBehalfOf, AuthorizedBy, Scope, ScopeEnds, Reason expose the I2.5 provenance trailer set. Principal is the human on whose authority the actor ran (always `human/<id>` when set); OnBehalfOf names the human inside whose scope the act lands; AuthorizedBy is the SHA of the authorize commit that opened the scope. Scope carries the lifecycle event for `aiwf authorize` commits (`opened` / `paused` / `resumed`); ScopeEnds is the slice of authorize-SHAs whose scopes the commit terminated (multiple ends per commit are allowed). Reason carries the free-text rationale from `aiwf-reason:`. All fields are empty for pre-I2.5 commits — the renderer treats absence as "no chip".
func EventFromCommit ¶
func EventFromCommit(sha, authorDate, subject, body string, trailers []gitops.Trailer) (HistoryEvent, bool)
EventFromCommit builds a HistoryEvent from one commit's raw fields. sha is the full hash (shortened here to match ReadHistoryChain); authorDate is git's %aI; subject is %s; body is %B (the full raw message, from which the prose body %b is derived); trailers is the parsed trailer block.
Returns ok=false for the prose-mention false-positive ReadHistoryChain also drops (G30): a commit whose aiwf-entity trailer matched a grep but which carries neither aiwf-verb nor aiwf-actor is not a real entity event. Callers skip such commits rather than bucket a blank row.
func ReadHistory ¶
func ReadHistory(ctx context.Context, root, id string) ([]HistoryEvent, error)
ReadHistory shells out to `git log` and returns one HistoryEvent per commit whose `aiwf-entity:` or `aiwf-prior-entity:` trailer matches id. Events are returned oldest-first.
The git format string carries seven fields per record separated by the ASCII unit separator (\x1f), with the ASCII record separator (\x1e) between commits — none of these appear in subjects or trailers, so a single split suffices. Pre-I2 commits without `aiwf-to:` or `aiwf-force:` trailers produce empty strings for those fields; the renderer treats empty as "absent" and emits a dash, which is the load-bearing backwards-compat behavior.
For a bare milestone id (e.g. `M-007`), the query also matches composite-id trailers under that milestone (`M-007/AC-N`) so the milestone view shows its AC events alongside its own. The match is anchored on the literal `/` boundary so `M-007/` cannot prefix- match `M-070/`. A composite id queried directly (`M-007/AC-1`) matches only that AC's events.
func ReadHistoryChain ¶
ReadHistoryChain is ReadHistory's lineage-aware variant: it greps git log for any aiwf-entity / aiwf-prior-entity trailer matching any id in chain, dedupes by commit SHA, and returns a single oldest-first chronological slice. Used by `aiwf history <id>` after the cmd dispatcher has expanded id through prior_ids lineage. A single-element chain is the pre-G37 behavior; longer chains weave pre-rename and post-rename history into one timeline.
type ScopeView ¶
type ScopeView struct {
AuthSHA string `json:"auth_sha"`
Entity string `json:"entity"`
Agent string `json:"agent"`
Principal string `json:"principal"`
State string `json:"state"`
Opened string `json:"opened,omitempty"`
EndedAt string `json:"ended_at,omitempty"`
EventCount int `json:"event_count"`
}
ScopeView is one scope's projection on `aiwf show`. It captures the authorization grant's metadata (SHA, agent, principal) and its current FSM state, plus the open/end dates and the count of transitions the scope has gone through.
Auth SHA is the full git SHA of the authorize-opened commit; callers that want a short form truncate. Entity is the scope- entity id at the time the scope was opened (rename-chain resolution lives in the verb gate, not here — show is descriptive, not gating).
func AssembleScopeViews ¶
func AssembleScopeViews( id string, events []HistoryEvent, ownScopes []*scope.Scope, openers map[string]string, foreignScopes func(ent string) ([]*scope.Scope, error), dateOf func(sha string) string, ) ([]ScopeView, error)
AssembleScopeViews is the pure, git-free core of `aiwf show`'s scope table: given an entity's loaded history events, its own scopes (source b), the repo-wide authorize-opener map, a resolver for a foreign scope-entity's scopes (source a), and a commit-date resolver, it assembles the scope-view list. The git-touching gather is the caller's job, so render's single pass (E-0054 / M-0221) assembles byte-identical views from its shared HEAD walk (opener map + replayed scopes + %aI dates) through this exact code path — no fourth copy of the assembly logic.
The M-0223 cost gates live in the caller (only load ownScopes / openers when the events warrant it); Assemble stays gate-free because the gates are pure optimizations — an empty openers map yields no foreign lookups, and empty ownScopes contributes nothing, so the assembled views are identical whether or not the caller gated.
foreignScopes is invoked only for a scope-entity that (a) an aiwf-authorized-by event references and (b) differs from id, so a caller that passes a nil openers map never triggers a foreign resolve. dateOf resolves a commit SHA to its author date (git show for the verb path; a lookup into the shared pass's %aI map for render).