Documentation
¶
Overview ¶
Package check validates an in-memory aiwf tree and returns findings.
Each check is a small pure function from the tree (and its load errors) to a slice of findings. Findings carry a code, a severity, a message, and optional context (path / entity id / subcode). Run composes all eight checks plus per-file load-errors-as-findings into a single slice.
"Errors are findings, not parse failures": Run never returns an error. A load error becomes a load-error finding; a malformed entity becomes a frontmatter-shape finding; the tree is loaded and validated as far as it can go.
Index ¶
- Constants
- func ApplyArchiveSweepThreshold(findings []Finding, threshold int, set bool, count int)
- func ApplyTDDStrict(findings []Finding, strict bool)
- func CountPendingSweep(t *tree.Tree) int
- func HasErrors(fs []Finding) bool
- func HintFor(code, subcode string) string
- func SortFindings(fs []Finding)
- type Finding
- type Severity
- type UntrailedCommit
Constants ¶
const ( CodeProvenanceTrailerIncoherent = "provenance-trailer-incoherent" CodeProvenanceForceNonHuman = "provenance-force-non-human" CodeProvenanceActorMalformed = "provenance-actor-malformed" CodeProvenancePrincipalNonHuman = "provenance-principal-non-human" CodeProvenanceOnBehalfOfNonHuman = "provenance-on-behalf-of-non-human" CodeProvenanceAuthorizedByMalformed = "provenance-authorized-by-malformed" CodeProvenanceAuthorizationMissing = "provenance-authorization-missing" CodeProvenanceAuthorizationOutOfScope = "provenance-authorization-out-of-scope" CodeProvenanceAuthorizationEnded = "provenance-authorization-ended" CodeProvenanceNoActiveScope = "provenance-no-active-scope" CodeProvenanceAuditOnlyNonHuman = "provenance-audit-only-non-human" // I2.5 step 7b: pre-push trailer audit (G24). Surfaces the // audit-trail hole when a manual `git commit` lands on entity // files without an aiwf-verb: trailer. Warning, not error: the // user's intended response is `aiwf <verb> --audit-only --reason // "..."` which fills the hole without rewriting history. CodeProvenanceUntrailedEntityCommit = "provenance-untrailered-entity-commit" // Companion to the above: emitted once when the untrailered- // audit scope cannot be determined (the branch has no upstream // and the operator passed no `--since <ref>`). The audit is // skipped — scanning all of HEAD on a long-lived branch with // many merges from trunk produces a flood of warnings against // commits that are someone else's responsibility. The hint // names the two opt-in paths (configure upstream, or pass // --since) so the operator can re-enable a deliberate scan. CodeProvenanceUntrailedScopeUndefined = "provenance-untrailered-scope-undefined" )
Provenance finding codes — the I2.5 standing rules from docs/pocv3/design/provenance-model.md §"`aiwf check` rules". Each fires on commit-history audit, not on tree state.
Variables ¶
This section is empty.
Functions ¶
func ApplyArchiveSweepThreshold ¶ added in v0.8.0
ApplyArchiveSweepThreshold bumps the aggregate `archive-sweep-pending` finding from warning to error when the consumer has set `archive.sweep_threshold` in aiwf.yaml and the pending-sweep count **strictly exceeds** that threshold (M-0088 AC-2). Mutates the findings slice in place; no-op when set=false (default-permissive) or when count ≤ threshold (consumer's declared ceiling not breached).
The escalation rewrites the aggregate's Message so the human reading `aiwf check` output sees both the count and their declared threshold cited explicitly. Per-file `terminal-entity-not-archived` leaf findings are NOT escalated — the aggregate is the single actionable signal; escalating leaves would flood the operator with duplicate "this gap is pending" warnings once they have already seen the aggregate.
Per ADR-0004 §"Drift control" (layer 2): "Configurable hard threshold. aiwf.yaml's archive.sweep_threshold (default unset) flips the advisory finding to blocking past the named count."
Callers run this AFTER Run() (or after appending the rule's findings to their own slice) so the rule's emission stays config- agnostic and the strictness bump is a separate, testable transform. The threshold is read via config.Config.ArchiveSweepThreshold; the count is the number of pending sweeps (i.e. the same value the aggregate's Message already names).
func ApplyTDDStrict ¶
ApplyTDDStrict bumps every entity-body-empty finding's severity from warning to error when strict=true (M-066/AC-2). Mutates the findings slice in place. The function is the single source of truth for which codes are covered by `aiwf.yaml: tdd.strict` — today only entity-body-empty; M-065's `milestone-tdd-undeclared` will be added to the same bumper when its rule lands. The bumper is intentionally narrow: codes outside this set pass through unchanged regardless of the flag.
Callers run this AFTER `Run` (or after appending the rule's findings to their own slice) so the rule's emission stays config-agnostic and the strictness bump is a separate, testable transformation.
func CountPendingSweep ¶ added in v0.8.0
CountPendingSweep returns the number of terminal-status entities still in the active tree (i.e. the count of pending sweeps). The same predicate as archiveSweepPending and terminalEntityNotArchived — extracted so the verb dispatcher can compute the count once and hand it to ApplyArchiveSweepThreshold without duplicating the iteration logic.
Exported so callers outside the check package can read the value; it is the same number the aggregate finding's Message names.
func HintFor ¶
HintFor returns the canonical action hint for a given code+subcode. Returns "" when no hint is registered. Verb-side findings (e.g., reallocate-body-reference) call this so the human-facing suggestion stays in one place.
func SortFindings ¶
func SortFindings(fs []Finding)
SortFindings orders findings by severity (errors first), then code, then path. Stable so callers that pre-sort within a code group keep their order. Exported for callers that merge findings from multiple sources (e.g. the CLI's `aiwf check` after appending contract findings to the entity-tree slice).
Types ¶
type Finding ¶
type Finding struct {
Code string `json:"code"`
Severity Severity `json:"severity"`
Message string `json:"message"`
Path string `json:"path,omitempty"`
Line int `json:"line,omitempty"`
EntityID string `json:"entity_id,omitempty"`
Subcode string `json:"subcode,omitempty"`
Hint string `json:"hint,omitempty"`
Field string `json:"-"`
}
Finding is one structured report from a check. The finder fills in Code, Severity, and Message; Path and EntityID provide locator context where they apply; Subcode distinguishes variants of the same finding (e.g., "unresolved" vs "wrong-kind" within refs-resolve).
Line is a 1-based line number in the file at Path. It is filled in post-hoc by Run() based on the Field annotation each check sets; when the field cannot be located in the file (or no field applies), Line is 1 so editors still receive a clickable file:line link.
Hint is a one-line suggestion for what to change to clear the finding. It is set by Run() from a Code+Subcode → hint table; checks don't populate it directly, so the wording stays consistent.
Field is an internal annotation naming the YAML key the finding is "about" (e.g., "parent", "status"). It is not part of the JSON envelope; it exists so Run() can resolve a useful Line.
func Run ¶
Run executes every check against the tree and returns all findings, ordered first by severity (errors first), then by code, then by path. Per-file load errors from the tree loader are surfaced as load-error findings ahead of the regular checks.
Run also fills in Line (1-based) and Hint on every finding. Line is derived from the field name the check annotated; Hint is looked up from a Code+Subcode → hint table.
func RunProvenance ¶
RunProvenance returns provenance findings for the given commit history. commits must be ordered oldest-first (`git log --reverse`) and should already be filtered to those carrying any `aiwf-*` trailer; pre-aiwf commits are silently skipped by the per-rule checks anyway, but pre-filtering keeps the work proportional.
t is the current entity tree, consulted by the authorization-out-of-scope rule for reference reachability.
The function is pure (no I/O, no git subprocess); the caller (cmd/aiwf) gathers commits via gitops and hands them in.
func RunUntrailedAudit ¶
func RunUntrailedAudit(commits []UntrailedCommit) []Finding
RunUntrailedAudit returns `provenance-untrailered-entity-commit` findings — one per (commit, entity) pair — for every untrailered commit in the supplied slice that touched an entity file. The caller is expected to scope `commits` to the unpushed range (typically `@{u}..HEAD`) so already-pushed pre-aiwf history is silently ignored.
One finding per entity, not per commit: a manual commit that touches three entity files emits three findings, each tagged with the entity id. This is what makes per-entity audit-only suppression work — `aiwf <verb> M-001 --audit-only` clears the M-001 finding without affecting the others on the same commit. Each finding's message names exactly one entity, which keeps individual lines short even when commits touch many entity files (matters for squash, merge, and bulk-import commits).
The finding is a WARNING. The intended user response is `aiwf <verb> --audit-only --reason "..."` (step 5b), which records the transition without rewriting history; an error severity here would block pushes for state that is already correct.
Coverage by audit-only: when a later commit in the same range carries `aiwf-audit-only:` and its `aiwf-entity:` matches the entity id, the warning for that (commit, entity) pair is suppressed. Composite ids on audit-only commits roll up to the parent milestone for matching, mirroring how composite ids on manual commits resolve to the parent file.
Defensive fallback: if a commit touched paths that PathKind recognizes but IDFromPath cannot parse to an id (a bug in the path scheme would be the only realistic cause), one path-tagged finding fires per such path. EntityID is empty in that branch.
func TreeDiscipline ¶
TreeDiscipline reports any file under work/* that the loader walked but could not classify as a recognized entity file. This is the G40 mechanical guarantee: the LLM must not write directly to the entity tree — tree-shape changes go through verbs, body-prose edits stay inside existing entity files, and stray hand-written files are flagged at validation time.
Filtering, in order:
- Files inside a recognized contract's directory (work/contracts/C-NNN-<slug>/) are auto-exempt — contracts legitimately carry schema/fixture artifacts alongside contract.md, and the binding lives in aiwf.yaml.
- Files matching any pattern in `allow` (filepath.Match, forward-slash, repo-relative) are exempt. Consumers configure these via `aiwf.yaml: tree.allow_paths`.
- Everything else surfaces as a finding.
Severity is warning by default. When `strict` is true (consumer opts in via `aiwf.yaml: tree.strict: true`), the severity is promoted to error so the pre-push hook blocks the push.
Run does *not* call this — it lives outside the standard rule chain so render/status callers don't get tree-discipline noise on every read. `runCheck` invokes it at the validation chokepoint.
type Severity ¶
type Severity string
Severity classifies a finding. Errors block `aiwf check` (exit 1); warnings are surfaced but don't change the exit code unless errors are also present.
type UntrailedCommit ¶
UntrailedCommit is the input shape for RunUntrailedAudit: the commit's SHA, its subject (first line of the message), its trailer set, and the relative paths it touched (as reported by `git diff-tree`).
The Subject is consulted to specialize the warning when the commit looks like a GitHub squash-merge ("…(#NNN)" suffix) — see G31. It can be left empty by callers that don't need that specialization; the bare warning still fires.