Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Feature Files

Behavioral specifications (Gherkin) for elench.

Anchor Resolution

Feature: Anchor resolution
  Anchors point at code within a tree. If anchors rot, revocation
  targets the wrong code and blast radius is fiction. E1 measures
  survival rate for each strategy over real refactor sequences.

  Scenario: A path-range anchor resolves at the same tree
    Given a claim anchored to src/lib.rs lines 1-10 at tree T0
    And tree T0 contains a blob at src/lib.rs with at least 10 lines
    When the anchor is resolved at tree T0 using the multi strategy
    Then the path-range strategy resolves to src/lib.rs
    And the result is Correct

  Scenario: A path-range anchor fails when the path is not in the tree
    Given a claim anchored to nonexistent.rs at tree T0
    When the anchor is resolved at tree T0
    Then the path-range strategy fails
    And if all strategies fail, the result is Failed

  Scenario: A path-range anchor fails when the range exceeds the blob
    Given a claim anchored to src/lib.rs lines 1-100 at tree T0
    And tree T0 contains a blob at src/lib.rs with only 5 lines
    When the anchor is resolved at tree T0
    Then the path-range strategy fails

  Scenario: A symbol anchor finds a function definition in the tree
    Given a claim anchored to symbol "parse_input" at tree T0
    And tree T0 contains a blob with "fn parse_input" in src/parser.rs
    When the anchor is resolved at tree T0 using the multi strategy
    Then the symbol strategy resolves to src/parser.rs
    And the result is Correct

  Scenario: A symbol anchor dies on rename
    Given a claim anchored to symbol "parse_input" at tree T0
    And tree T0 contains no definition of "parse_input"
    When the anchor is resolved at tree T0
    Then the symbol strategy fails

  Scenario: A content-digest anchor finds matching content in the tree
    Given a claim anchored with content_digest D at tree T0
    And tree T0 contains a blob whose SHA-256 equals D
    When the anchor is resolved at tree T0 using the multi strategy
    Then the content-digest strategy resolves to that blob's path
    And the result is Correct

  Scenario: A content-digest anchor fails when no blob matches
    Given a claim anchored with content_digest D at tree T0
    And no blob in tree T0 has SHA-256 equal to D
    When the anchor is resolved at tree T0
    Then the content-digest strategy fails

  Scenario: Strategies disagree when path and symbol point to different files
    Given a claim anchored to path src/main.rs and symbol "parse_input"
    And tree T0 has src/main.rs (path-range resolves) but parse_input is in src/parser.rs
    When the anchor is resolved at tree T0
    Then the result is Degraded
    And the disagreements include path-range → src/main.rs and symbol → src/parser.rs

  Scenario: Wrong-resolution is reported distinctly from failure
    Given a claim anchored at tree T0 using path-range strategy
    When the anchor is resolved at tree T1 and resolves to the wrong code
    Then the result is "wrong-resolution", not "unresolved"
    And wrong-resolution rate > 2% disqualifies the strategy

  Scenario: Reconciliation reports intact and drifted claims
    Given a tree T0 with two claims: one anchored correctly, one with a missing path
    When reconciliation is run for tree T0
    Then the report lists 1 intact claim and 1 drifted claim
    And the drifted claim's resolution is Failed

  Scenario: Reconciliation CLI command
    Given a claims file with claims anchored to tree T0
    When elench reconcile <tree_oid> <claims.json> runs
    Then the output shows intact and drifted counts
    And the exit code is 0

Build Provenance

Feature: Build provenance digest
  `elench build <tree> [--artifact <path>] -- <command...>` runs a build,
  captures the exit code, and emits a harness-observed provenance claim
  (ADR-0003, INV-22). The digest is SHA-256 of the build artifact when
  `--artifact <path>` names an existing file; otherwise it falls back to
  SHA-256 of stdout. The same producer cannot forge a digest it did not
  observe — the harness reads the file, not the agent.

  Scenario: digest is SHA-256 of the artifact file when --artifact is present
    Given a build that writes 42 bytes to /tmp/artifact.bin
    And a known content for /tmp/artifact.bin
    When elench build <tree> --artifact /tmp/artifact.bin -- <command> runs
    Then the emitted claim's content_digest equals SHA-256(/tmp/artifact.bin)
    And the claim's evidence[0].digest equals SHA-256(/tmp/artifact.bin)
    And the claim's evidence[0].exit_code equals the build's exit code

  Scenario: digest falls back to stdout when --artifact is absent
    Given a build that writes "hello" to stdout and exits 0
    When elench build <tree> -- <command> runs
    Then the emitted claim's content_digest equals SHA-256("hello")
    And the claim's evidence[0].digest equals SHA-256("hello")

  Scenario: missing artifact file is rejected
    Given no file exists at /tmp/nonexistent.bin
    When elench build <tree> --artifact /tmp/nonexistent.bin -- <command> runs
    Then the command exits non-zero
    And the error names the missing artifact

  Scenario: --artifact is parsed before --, not passed to the build
    Given a build command echo --artifact foo
    When elench build <tree> --artifact /tmp/real.bin -- echo --artifact foo runs
    Then the digest is SHA-256(/tmp/real.bin), not influenced by echo's args
    And echo receives "--artifact foo" as its own argument

Claim Emission

Feature: Claim emission
  Agents emit signed claims about a tree, stored in the elench
  content-addressed store. Claims are append-only; no tree mutation
  occurs. The validator enforces emission rules before a claim is
  accepted.

  Scenario: An agent-asserted predicate claim is stored
    Given an elench store with at least one tree
    When an agent emits a predicate claim asserting "parse('') == Err(EmptyInput)" about tree <oid>
    Then a new claim object exists in the store with a DSSE envelope
    And the statement's predicate has form = "predicate"
    And the statement's predicate has origin.kind = "agent-asserted"
    And the tree is unchanged — no blobs or tree entries were added or modified

  Scenario: An annotation claim is stored but cannot gate
    Given an elench store with a claim log containing one annotation claim
    When the release gate is evaluated against the log
    Then the annotation claim does not contribute to the gate verdict
    And the annotation's status is reported as "unevaluated"

  Scenario: An agent cannot emit a harness-observed claim
    Given an agent attempting to emit a claim
    When the agent sets origin.kind = "harness-observed"
    Then the claim is rejected by the validator
    And no object is written to the store

  Scenario: A predicate without an expression is rejected
    Given an agent attempting to emit a predicate claim
    When the claim's assertion has form = "predicate" but no expression field
    Then the claim is rejected by the validator
    And the rejection reason includes "expression required for predicate"

  Scenario: A claim with dependsOn lists its premises
    Given an agent emits a claim that relies on premises cl_a3f... and cl_91c...
    When the claim is stored
    Then the claim's dependsOn field contains [cl_a3f..., cl_91c...]
    And the blast radius from any falsification of cl_a3f... includes this claim

  Scenario: A claim with cyclic dependsOn is rejected
    Given a claim log containing claim cl_a with dependsOn [cl_b]
    And claim cl_b with dependsOn [cl_a]
    When an agent emits a new claim cl_c with dependsOn [cl_a, cl_c]
    Then cl_c is rejected by the validator
    And the rejection reason includes "cyclic dependency"

  Scenario: A claim with empty dependsOn is accepted with a warning
    Given an agent emits an assertion claim with no dependsOn
    When the claim is validated
    Then the claim is accepted
    And a warning is emitted: "dependsOn is empty — claim asserts it was reached from nothing"

Claim Revocation

Feature: Claim revocation without tree mutation
  A claim's status is changed by appending a falsification record. No
  code changes, no history rewrite. The prior status remains visible.
  Blast radius propagates through the dependsOn closure.

  Scenario: A falsification record changes a claim's status
    Given a claim log containing claim cl_a3f... with computed status "passed"
    When a falsification claim is appended targeting cl_a3f...
    Then the claim log contains two records: the original and the falsification
    And cl_a3f...'s computed status is now "falsified"
    And the original claim's content is byte-identical to before
    And no tree state was added or modified

  Scenario: Blast radius propagates through dependsOn
    Given claims cl_a, cl_b, cl_c where cl_b dependsOn [cl_a] and cl_c dependsOn [cl_b]
    When cl_a is falsified
    Then cl_a, cl_b, and cl_c all have computed status "falsified"
    And the blast radius report lists all three claims in dependency order

  Scenario: A claim with no dependsOn has no blast radius
    Given a claim cl_x with an empty dependsOn field
    When cl_x is falsified
    Then the blast radius report contains only cl_x
    And no other claims change status

  Scenario: A previously released artifact's status changes after falsification
    Given an artifact D released from tree T under policy P with gate passing
    When a load-bearing claim in T's claim set is falsified after release
    Then re-evaluating the gate for (T, P) now fails
    And no byte of D was changed
    And no re-signing occurred

  Scenario: Falsifying a verification reverts the target to unevaluated
    Given claim cl_a with computed status "passed" via verification cl_v
    When a falsification cl_fv targets cl_v (the verification, not cl_a)
    Then cl_v's computed status is "falsified"
    And cl_a's computed status reverts to "unevaluated" (no valid verification remains)
    And cl_a's original claim content is byte-identical to before

  Scenario: Falsifying a falsification reverts the target to its previous status
    Given claim cl_a with computed status "falsified" via falsification cl_f1
    When a falsification cl_f2 targets cl_f1 (the falsification, not cl_a)
    Then cl_f1's computed status is "falsified"
    And cl_a's computed status reverts to its previous status ("unevaluated" or "passed")
    And the revert is recursive — cl_a's dependents also revert

  Scenario: A falsification that changes no status is rejected
    Given claim cl_x with computed status "falsified"
    When an agent emits a new falsification targeting cl_x
    Then the new falsification is rejected
    And the rejection reason includes "falsification changes no status — target already falsified"

Dogfooding

Feature: Dogfooding
  elench eats its own dog food. Agents working on elench emit claims
  about elench's own code — assertions that specific invariants are
  enforced by specific file locations. The claims are gated, reconciled,
  and projected using elench's own CLI. This is the closed loop.

  Scenario: Dogfooding claims are emitted and stored
    Given dogfooding/claims.json with 10 claims about elench's invariants
    When elench emit dogfooding/claims.json runs
    Then each claim is signed and stored
    And the output shows the computed claim ID

  Scenario: Gate evaluates dogfooding claims
    Given dogfooding/claims.json with 10 unevaluated assertions
    When elench gate <tree> dogfooding/claims.json runs
    Then the gate passes (no falsified premises)
    And the verdict contains no failure reasons

  Scenario: Log statistics show all unevaluated
    Given dogfooding/claims.json with 10 assertions, 0 verifications
    When elench log dogfooding/claims.json runs
    Then total is 10, assertions is 10, unevaluated is 10
    And noise ratio is 0.00

  Scenario: Git projection from dogfooding claims
    Given dogfooding/claims.json with 10 tree-changing claims
    When elench git dogfooding/claims.json runs
    Then the projection shows commits, blobs, and trees
    And git log oneline output contains "elench claim:"

  Scenario: Reconciliation reports drift when trees are not in the store
    Given dogfooding/claims.json with anchors pointing at trees not in the store
    When elench reconcile <tree> dogfooding/claims.json runs
    Then the report shows drifted claims (anchors cannot resolve)
    And the exit code is 0

  Scenario: Continuous dogfooding emits harness-observed claims
    Given elench's own source tree
    When make dogfood-continuous runs
    Then 4 claims are emitted (build, test, lint, fmt)
    And each claim has origin.kind = harness-observed
    And the gate passes (all verifications, no falsifications)
    And the claims are accumulated in a single JSON file

  Scenario: Continuous dogfooding runs in CI nightly
    Given the nightly CI job triggers
    When the tier-3 job completes
    Then make dogfood-continuous runs
    And the resulting claims are uploaded as a CI artifact

Git Projection

Feature: Git projection
  The git CLI works because elench synthesizes git-compatible objects
  from the claim log. Synthesis is deterministic (BC4): two parties
  with the same claim log produce byte-identical git objects. The
  projection is read-only — writes go through elench, never git.

  Scenario: git log shows synthesized commits
    Given an elench store with a claim log containing N tree-changing claims
    When the git projection is materialized
    Then `git log` shows N commits, one per tree-changing claim
    And each commit's tree OID is a deterministic function of the elench tree
    And each commit's author is derived from the claim's producer.id

  Scenario: Two parties produce identical git objects
    Given an elench store with claim log L
    When party A synthesizes git objects from L on machine A
    And party B synthesizes git objects from L on machine B
    Then A and B produce byte-identical commit OIDs
    And A and B produce byte-identical tree OIDs
    And `git log` output is identical on both machines

  Scenario: git blame maps to claims
    Given an elench store with claims that introduced specific lines
    When the git projection is materialized and `git blame` is run
    Then each line maps to the commit synthesized from the claim that introduced it
    And the commit's author is the claim's producer, not a user-configured name

  Scenario: Write through git is rejected
    Given a materialized git projection of an elench store
    When a user runs `git commit` in the projected repository
    Then the commit is rejected or has no effect on the elench store
    And the elench claim log is unchanged

Origin Typing

Feature: Origin is a type, not a label
  Evidence the harness observed and evidence an agent asserted must be
  structurally distinguishable and must never be merged into one status.
  Policies may and generally should discriminate on origin.kind.

  Scenario: Harness-observed and agent-asserted claims are distinct
    Given a claim log with one harness-observed claim and one agent-asserted claim
    When the log is queried for claims with origin.kind = "harness-observed"
    Then only the harness-observed claim is returned
    And the agent-asserted claim is excluded

  Scenario: Policy requires origin floor for load-bearing claims
    Given a release policy P requiring origin.kind = "harness-observed" for load-bearing claims
    And a claim log where a load-bearing claim has origin.kind = "agent-asserted"
    When the release gate is evaluated against P
    Then the gate fails with reason "origin floor not met for claim <id>"

  Scenario: Only the harness emits verification records
    Given an agent attempting to emit a claim with kind = "verification"
    When the claim is submitted to the validator
    Then the claim is rejected
    And the rejection reason includes "only harness may emit verification"

  Scenario: A human cannot emit verification records
    Given a human attempting to emit a claim with kind = "verification"
    When the claim is submitted to the validator
    Then the claim is rejected
    And the rejection reason includes "only harness may emit verification"

  Scenario: A harness cannot emit residue-acceptance records
    Given a harness attempting to emit a claim with kind = "residue-acceptance"
    When the claim is submitted to the validator
    Then the claim is rejected
    And the rejection reason includes "only human may emit residue-acceptance"

  Scenario: A human-asserted claim is distinct from an agent-asserted claim
    Given a claim log with one human-asserted claim and one agent-asserted claim
    When the log is queried for claims with origin.kind = "human-asserted"
    Then only the human-asserted claim is returned

Proptest Property

Feature: Property-based tests (proptest)
  Invariants that hold for ALL inputs, not just the examples in unit
  tests. Each property is tagged with the invariant it covers and
  classified as PROPERTY depth in the fidelity index.

  Scenario: INV-25 — content addressing is deterministic
    Given any two byte arrays with the same content
    When Oid::from_blob_data is called on each
    Then both produce the same OID
    And given any two byte arrays with different content
    Then both produce different OIDs

  Scenario: INV-28 — claim OID is content hash
    Given any claim C
    When ClaimId::from_content is called on C
    And the same claim C is constructed again with identical fields
    Then both produce the same ClaimId

  Scenario: INV-13 — compute_status is a pure function
    Given any claim ID K and any log L
    When compute_status(K, L) is called twice
    Then both calls return the same result

  Scenario: INV-29 — dependsOn is acyclic
    Given any log L that passes validate_claim for each claim
    When compute_status is called for any claim in L
    Then it does not loop forever (returns Ok or Err, not a hang)

  Scenario: INV-20 — git synthesis is deterministic
    Given any claim log L and any store S
    When synthesize(L, S) is called twice
    Then both calls produce byte-identical commit OIDs and tree OIDs

Release Gate

Feature: Release gate is a predicate over claims
  The gate is cheap and deterministic, evaluable without build
  capability (R3). An artifact's acceptability is a live evaluation
  against the current claim log, not a signature frozen at release
  time (R4). See docs/release-policy.md for the four conditions.

  Scenario: Gate passes when no falsified premises exist
    Given a tree T with claims all having status "passed" or "unevaluated"
    And a release policy P that allows the existing unevaluated residue
    When the release gate is evaluated for tree T under policy P
    Then the gate passes
    And the verdict contains no failure reasons

  Scenario: Gate fails when a falsified premise exists
    Given a tree T with a claim cl_x having status "falsified"
    And a release policy P that does not allow the falsified claim
    When the release gate is evaluated for tree T under policy P
    Then the gate fails with reason "falsified premise: cl_x"

  Scenario: Gate fails when unevaluated residue exceeds policy bounds
    Given a tree T with 5 unevaluated claims
    And a release policy P that allows at most 3 unevaluated claims
    When the release gate is evaluated for tree T under policy P
    Then the gate fails with reason "unbounded residue: 5 > 3"

  Scenario: Excess residue is covered by a human acceptance record
    Given a tree T with 5 unevaluated claims
    And a release policy P that allows at most 3 unevaluated claims
    And a residue-acceptance record signed by a human key naming the 2 excess claims
    When the release gate is evaluated for tree T under policy P
    Then the gate passes

  Scenario: Gate evaluates without build capability
    Given a party with only the claim log and no compute
    When the party evaluates the release gate for tree T under policy P
    Then the party reaches the same verdict as a party with a build farm

  Scenario: A previously released artifact's status changes after falsification
    Given an artifact D released from tree T under policy P with gate passing
    When a load-bearing claim is falsified after release
    And the gate is re-evaluated for the same (T, P)
    Then the gate now fails
    And no byte of D was changed and no re-signing occurred

  Scenario: Empty store: gate passes with no claims
    Given an elench store with zero claims and zero trees
    When the release gate is evaluated for any tree under policy P
    Then the gate passes
    And the verdict contains no failure reasons
    And condition 4 (builder agreement) is not evaluated when there are no claims

  Scenario: Single claim: gate evaluates correctly
    Given an elench store with one tree T and one unevaluated claim about T
    And a release policy P that allows 1 unevaluated claim
    When the release gate is evaluated for tree T under policy P
    Then the gate passes
    And the blast radius from that single claim is itself (no dependents)

  Scenario: Two contradictory predicates: gate passes (known limitation)
    Given a tree T with two unevaluated agent-asserted predicates
    And the predicates are contradictory: "parse('') == Err(EmptyInput)" and "parse('') == Ok(DefaultValue)"
    And a release policy P that allows agent-asserted claims and 2 unevaluated claims
    When the release gate is evaluated for tree T under policy P
    Then the gate passes
    And the gate does not detect logical inconsistency among unevaluated claims
    And this is a known limitation (FM-P2-02): contradictory unevaluated claims are invisible to the gate

Store Backend

Feature: Store backend selection
  elench ships two storage backends: an in-memory store (default, no
  dependencies) and a persistent fjall-backed store (optional
  `fjall-backend` feature, ADR-0008). Both implement `StoreBackend`,
  so any command that reads or writes the store can target either.

  INV-18: elench owns the store. INV-25: content addressing. INV-26:
  the store is the sole source of truth — all views derive from it
  alone, so a persistent backend must round-trip trees and blobs
  across processes.

  Scenario: Default backend is in-memory
    Given no --store flag is supplied
    When a command runs
    Then it uses the in-memory backend
    And nothing is written to disk

  Scenario: Explicit in-memory backend
    Given the --store memory flag is supplied
    When a command runs
    Then it uses the in-memory backend

  Scenario: Persistent fjall backend is selected by --store fjall <path>
    Given the fjall-backend feature is enabled
    When a blob and a tree are stored with --store fjall <path>
    Then a separate process opening the store at <path> can read the blob back
    And it can read the tree back with the same entries and OID (INV-25)

  Scenario: read_tree round-trips the canonical serialization
    Given a tree with entries is stored in any backend
    When the tree is read back by OID
    Then the entries equal the original entries
    And the tree OID equals the SHA-256 of the canonical serialization

  Scenario: fjall backend without the feature is rejected
    Given the fjall-backend feature is not enabled
    When --store fjall <path> is supplied
    Then the command exits non-zero
    And the error names the missing feature

Unevaluated Residue

Feature: Unevaluated is a first-class status
  Brownfield code is overwhelmingly unevaluated. A system that cannot
  say so is unusable on real repositories. Policies must permit bounded
  unevaluated residue with a named signer accepting it (R5).

  Scenario: A new claim is unevaluated by default
    Given a freshly emitted assertion claim with no verification or falsification targeting it
    When the claim's status is computed
    Then the status is "unevaluated"
    And the status is NOT "passed"

  Scenario: Unevaluated residue is bounded by policy
    Given a tree T with 5 unevaluated claims
    And a release policy P that allows at most 3 unevaluated claims
    When the release gate is evaluated for tree T under policy P
    Then the gate fails with reason "unbounded residue: 5 > 3"

  Scenario: Excess residue is covered by a residue-acceptance record
    Given a tree T with 5 unevaluated claims
    And a release policy P that allows at most 3 unevaluated claims
    And a residue-acceptance record signed by a human key naming the 2 excess claims
    When the release gate is evaluated for tree T under policy P
    Then the gate passes
    And the acceptance record has kind = "residue-acceptance"
    And the acceptance record has origin.kind = "human-asserted"

  Scenario: A residue-acceptance record from an agent is rejected
    Given an agent attempting to emit a residue-acceptance record
    When the record is submitted to the validator
    Then the record is rejected
    And the rejection reason includes "only human may emit residue-acceptance"

  Scenario: A residue-acceptance names specific gaps, not a blanket acceptance
    Given a residue-acceptance record signed by a human key
    When the record is validated
    Then it must contain a non-empty target array naming the accepted gaps
    And a record with an empty target array is rejected

  Scenario: A corrupt claim cascades to unevaluated dependents
    Given a claim log with claims cl_a, cl_b, cl_c
    And cl_b dependsOn [cl_a] and cl_c dependsOn [cl_b]
    When cl_a becomes corrupt (unreadable in the store)
    Then cl_a's computed status is "unevaluated" (corrupt — cannot read)
    And cl_b's computed status is "unevaluated" (depends on corrupt claim)
    And cl_c's computed status is "unevaluated" (cascading from cl_b)
    And the status report marks them as "unevaluated: corrupt" not "unevaluated: no one checked"
    And the gate may fail if "unevaluated: corrupt" exceeds the residue bound