spec

package
v0.29.0 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package spec is the canonical Go encoding of aiwf's legal-workflow surface.

The package is built and maintained per ADR-0011's three-pass methodology (Pass A audit, Pass B first-principles, Pass C reconcile). M-0123 is the reconciliation milestone; this package is its load-bearing deliverable.

The cell-by-cell table lives in Rules() and AntiRules(); per-cell positive and negative coverage lives under internal/policies/ (M-0124, M-0125). The bidirectional drift policy under internal/policies/ closes the spec against the impl in both directions.

Anti-rules. The kernel deliberately excludes patterns that contributors might mis-assume are policed. AntiRules() catalogs them. The meta-policy is: a candidate becomes an anti-rule only when (a) reconciliation surfaces a plausibly mis-assumed pattern and (b) Pass C judges the spec better served by an explicit non-rule entry than by silence. The list grows as future PRs surface new near-rules; small follow-up acts (a gap, a one-row spec amendment) are the expected cadence.

Schema invariants enforced by drift policies in internal/policies/:

  • Outcome != OutcomeUnspecified for every cell.
  • Outcome == OutcomeIllegal implies RejectionLayer != RejectionLayerNone.
  • RejectionLayer == RejectionLayerVerbTime implies BlockingStrict == true.
  • Outcome == OutcomeLegal implies ExpectedErrorCode == "".
  • Sources.Decision (when non-empty) resolves to a planning-tree entity.
  • The (Kind, FromState, Verb) triple uniquely keys each Rule.

Index

Constants

View Source
const (
	KindAC       entity.Kind = "ac"
	KindTDDPhase entity.Kind = "tdd-phase"
)

Kind extensions for sub-FSM cells. ACs and TDD-phase are not first-class entity kinds (entity.Kind covers the six top-level kinds), but they have their own FSMs (entity.IsLegalACTransition, entity.IsLegalTDDPhaseTransition) that the spec must encode. These constants extend the Kind value space for spec-table purposes only — they are NOT added to entity.transitions.

Variables

This section is empty.

Functions

func EvaluatePredicate

func EvaluatePredicate(p Predicate, e *entity.Entity, t *tree.Tree, ctx EvalContext) (bool, error)

EvaluatePredicate reports whether p holds against e in t, with verb-invocation context ctx. The closed (Subject, Op) vocabulary covered here is exactly what appears in Rules() at the M-0124 ship boundary:

self.target-state == <state>
self.addressed_by non-empty
self.addressed_by == ""
self.superseded_by non-empty
self.superseded_by == ""
self.tdd_phase != <phase>
parent.tdd == <policy>
any-child.status ∉ <named-set>
any-child-ac.status == <state>
all-children-acs.status != <state>
scope-reach == true / scope-reach == false (verb-invocation;
    reads ctx.Target + ctx.ScopeEntity, delegates to
    tree.ReachesScope)

Unknown Subject, Op, or named-set Value return a typed error so a future rule that widens the vocabulary fails the matching atom's per-cell test with a clear "unknown subject/op/named set" message — discoverable, fixable.

The named-set vocabulary is its own closed table; today it carries exactly one entry ("milestone-terminal-set" = {done, cancelled}). New named sets land here as the spec grows.

Types

type AntiRule

type AntiRule struct {
	ID        string
	Statement string
	Reasoning string
	Sources   RuleSource
}

AntiRule catalogs a pattern that the kernel deliberately does NOT police. Anti-rules clarify scope by negation; they are not cells in the (Kind, FromState, Verb) keyed Rule table.

Pass C's anti-rule meta-policy: a candidate becomes an anti-rule only when reconciliation surfaces a plausibly-mis-assumed pattern. Examples from Pass B §10 and Q10:

  • A milestone is NOT required to have ≥1 AC.
  • An epic MAY transition proposed → active with zero milestones.
  • There is no kernel rule about which branch a verb is legal on.

AntiRules() returns the closed-set list; the order is the listing order in the spec body (loosely thematic, per-source).

func AntiRules

func AntiRules() []AntiRule

AntiRules returns the closed-set catalog of patterns the kernel deliberately does NOT police. Anti-rules clarify scope by negation; they are not (Kind, FromState, Verb)-keyed cells in Rules().

The list comprises eleven Pass B §10 entries (R-FP-0166..R-FP-0176) plus one Q10 addition (ANTI-0012, the zero-milestone-active legality), per M-0123 phase 1 enumeration. Listing order follows Pass B with the Q10 addition appended.

Each entry carries a structural Statement ("the kernel does NOT...") and a Reasoning ("...because..."). Together they pin both the anti-rule and why a future contributor shouldn't re-litigate it.

Growth is by spec amendment — surface a new mis-assumed pattern via Pass C-style reconciliation, then add a row. See spec.go package comment for the anti-rule meta-policy.

type EvalContext

type EvalContext struct {
	TargetState string
	AC          *entity.AcceptanceCriterion

	// Target and ScopeEntity carry verb-invocation context for the
	// scope-reach predicate (M-0145) — the first subject that is not
	// entity-side. Target is the entity the verb acts on; ScopeEntity
	// is the actor's active authorization scope-entity. The scope-reach
	// arm delegates to tree.ReachesScope(Target, ScopeEntity) — D-0006's
	// source of truth — rather than re-deriving reachability. Both are
	// empty for the entity-side predicates (self.*/parent.*/*-child*).
	Target      string
	ScopeEntity string
}

EvalContext carries the bits of state that don't live on the passed-in *entity.Entity:

TargetState — the second positional arg of `aiwf promote <id>
              <new-status>` (or AC's `--phase` value). Used by the
              "self.target-state" predicate.
AC          — populated when the rule's Kind is KindAC and the
              predicate references "self.tdd_phase" (which is a
              field on AcceptanceCriterion, not on Entity). ACs
              are sub-elements of their parent milestone, not
              standalone tree entities; the caller resolves the
              specific AC slot and hands it in.

Widening EvalContext requires widening the Subject vocabulary, which is itself constrained per the M-0123 body's predicate- vocabulary commitment.

type Outcome

type Outcome int

Outcome is the legal/illegal axis of a Rule cell.

The zero value OutcomeUnspecified is a sentinel that surfaces "forgot to set Outcome on a Rule literal" bugs at drift-policy time (the policy test asserts no cell carries OutcomeUnspecified).

const (
	OutcomeUnspecified Outcome = iota
	OutcomeLegal
	OutcomeIllegal
)

Outcome values.

type Predicate

type Predicate struct {
	Subject string
	Op      string
	Value   string
}

Predicate is a precondition expressed against the planning tree at verb-time. Subject vocabulary is closed to five forms:

self.<field>         — the entity the verb operates on
parent.<field>       — the entity's parent (e.g., milestone's epic)
all-children.<field> — every child satisfies the predicate
any-child.<field>    — at least one child satisfies
scope-reach          — actor's active-scope-entity reaches target via
                       the scope-tree edges (D-0006); not a field
                       comparison

Op vocabulary is closed to six forms: ==, !=, ∈, ∉, non-empty, exists.

Widening either vocabulary requires a decision entity per the M-0123 body's predicate-vocabulary constraint.

type RejectionLayer

type RejectionLayer int

RejectionLayer names where in the kernel pipeline an illegal cell is rejected. Verb-time rejections are returned by the verb itself (non-zero exit, no commit, no side effect); check-time rejections are surfaced as findings by aiwf check (the verb may have succeeded structurally).

The zero value RejectionLayerNone is meaningful — it applies to legal cells (where the field is irrelevant). The drift policy asserts that illegal cells carry a non-zero RejectionLayer.

const (
	RejectionLayerNone RejectionLayer = iota
	RejectionLayerVerbTime
	RejectionLayerCheckTime
)

RejectionLayer values.

type Rule

type Rule struct {
	ID                string
	Kind              entity.Kind
	FromState         string
	Verb              string
	Preconditions     []Predicate
	Outcome           Outcome
	ExpectedErrorCode string
	RejectionLayer    RejectionLayer
	BlockingStrict    bool
	Sources           RuleSource
}

Rule is one legality cell in the spec table.

Keyed by (Kind, FromState, Verb). Outcome carries Legal/Illegal; for Illegal cells, RejectionLayer + BlockingStrict + ExpectedErrorCode pin the rejection mode. Preconditions narrow when the cell applies (e.g., a legal-only-if-children-all-terminal precondition pairs with a companion illegal-when-any-child-non-terminal cell).

Sources records the catalogs that motivated the cell — Audit (R-AUDIT-NNNN ids), FP (R-FP-NNNN ids), Decision (D-NNNN, populated only for FP-only and Conflict classes).

Cross-cutting precondition rules that are NOT (Kind, FromState, Verb) cells (ADR-0013, e.g. the scope-reach rule) live in GlobalRules, a separate accessor — they are deliberately absent from Rules so every per-cell consumer iterates cells only, with no per-rule exclusion. Only the code-oriented AC-5 drift arms union the two.

ID (optional, added by M-0158) carries an explicit string identifier for cells that live outside the (Kind, FromState, Verb) keyspace — layer-4 branch-choreography cells (e.g. `branch-cell-1`, `branch-cell-override- preflight`). Layers 1–3 leave ID empty and continue to be keyed by the natural tuple; their tests and meta-policies are unaffected. The ID is the consumer-facing name when an explicit cell-id-to-test-name convention is required (M-0158/AC-2, AC-3, AC-5).

func GlobalRules

func GlobalRules() []Rule

GlobalRules returns the cross-cutting precondition rules that are NOT (Kind, FromState, Verb) cells (ADR-0013) — kept out of Rules so every per-cell consumer (the m0124/m0125 coverage drivers, the coordinate- resolution drift arms, key-uniqueness) iterates cells only, with no per-rule exclusion. Only the code-oriented AC-5 drift arms union Rules() and GlobalRules().

Today the global rules are:

  • scope-reach (D-0006): an authorized agent's verb is refused when the target is out of scope.
  • branch-context-required (E-0030 / M-0103 / ADR-0010): aiwf authorize refuses opening a scope on an ai/* agent when no ritual branch context is in play — neither --branch was supplied nor is the current checkout a recognized ritual shape. The precondition names "target-agent-role" (the role part of --to <role>/<id>) and "ritual-branch-context-present" (true when either signal is set).
  • branch-not-found (E-0030 / M-0103): aiwf authorize refuses when --branch <name> was supplied but no local branch by that name resolves under refs/heads/.

The two M-0103 entries are scaffold-quality: they pin the codes into the bidirectional drift net so M-0123/AC-5's legality-codes-referenced arm is satisfied. The layer-4 consolidation milestone (M-0158) elaborates them into the full branch-choreography cell set per ADR-0011 §"Scope".

func LookupRules

func LookupRules(kind entity.Kind, fromState, verb string) []Rule

LookupRules returns every Rule in Rules() that matches the (kind, fromState, verb) key. The plural name and slice return shape are load-bearing — per M-0123 phase 1's schema adjustment, the (Kind, FromState, Verb, Outcome) tuple keys the table, not (Kind, FromState, Verb). A preconditioned-pair key (e.g., epic.proposed.cancel, with a legal cell and a Q5/D-0003 illegal companion) returns both cells.

Semantics:

  • Hit (>=1 cell): returns a slice of every matching cell. The caller resolves which cell applies by walking Preconditions.
  • Miss (no cell): returns an empty slice (nil-equivalent on len).

Per the AC-2 schema invariant (TestM0123_AC2_KeyUnique), the slice contains at most one cell per Outcome value — within a single key, legal-vs-illegal is the only distinction.

LookupRules is the only public access surface for the table (AC-7); the Rules() slice is exported for the AC-2 / AC-5 drift policies that need to iterate the full table, but consumers should reach for LookupRules when answering "is this verb legal here?".

func Rules

func Rules() []Rule

Rules returns the closed-set legal-workflow table. Per M-0123 phase 1 concretization, every cell encodes one (Kind, FromState, Verb, Outcome) position. Cells may overlap on (Kind, FromState, Verb) — the (key, Outcome) tuple is what's required to be unique.

Drift policies under internal/policies/ assert:

  • Every (Kind, FromState) appearing in entity.transitions / entity.acTransitions / entity.tddPhaseTransitions has at least one corresponding cell.
  • Every top-level Cobra verb is referenced by at least one cell.
  • Every legality-pertinent finding code is referenced by at least one illegal-outcome cell.
  • Every Rule satisfies the schema invariants (Outcome != Unspecified; Illegal ⇒ RejectionLayer non-zero; VerbTime ⇒ BlockingStrict; Legal ⇒ ExpectedErrorCode empty; Sources.Decision resolves).

type RuleSource

type RuleSource struct {
	Audit    []string
	FP       []string
	Decision string
}

RuleSource records the catalog citations that motivated this cell. The population shape encodes the reconciliation class per M-0123 body §"Rule sources":

Agreement   → Audit non-empty, FP non-empty, Decision empty
Audit-only  → Audit non-empty, FP empty, Decision empty
FP-only     → Audit empty, FP non-empty, Decision non-empty (D-NNNN)
Conflict    → Audit non-empty, FP non-empty, Decision non-empty (D-NNNN)

AC-6's drift test asserts every cell's Sources.Decision (when non-empty) resolves to an existing D-NNNN entity via tree.Load + Tree.ByID.

Directories

Path Synopsis
Package branch holds the layer-4 branch-choreography spec cells per [ADR-0011] §"Scope": *"layer 4 — branch choreography — as E-0030's scope, deliberately separate because the test fixture shape differs (git state, not entity state)"*.
Package branch holds the layer-4 branch-choreography spec cells per [ADR-0011] §"Scope": *"layer 4 — branch choreography — as E-0030's scope, deliberately separate because the test fixture shape differs (git state, not entity state)"*.
branchtest
Package branchtest is the test-only Pin registry used by the M-0162 cell-expansion E2E tests and the bijection meta-test.
Package branchtest is the test-only Pin registry used by the M-0162 cell-expansion E2E tests and the bijection meta-test.

Jump to

Keyboard shortcuts

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