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 ¶
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 ¶
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).
type Predicate ¶
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 ¶
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 ¶
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. |