Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ semantics have been stable since 1.0.0.

## [Unreleased]

C4 import-aware dependency binding. **No breaking changes** (a re-check *trigger* widening only;
warrant schema, checker grammar, exit codes, fold policy, and security posture are unchanged).
C4 import-aware dependency binding, and the opt-in truth-axis `--strength-gate`. **No breaking
changes** (both are opt-in or a re-check *trigger* widening only; warrant schema, checker grammar,
exit codes, fold policy, and security posture are unchanged; default behavior is identical).

### Added
- **C4 import-aware binding** (`src/dorian/test_deps.py`). A `pytest:` checker proves behavior *when
Expand All @@ -30,6 +31,29 @@ warrant schema, checker grammar, exit codes, fold policy, and security posture a
import-derived watches as **checker-exercised** (the test imports and runs them), so widening a
behavior claim's watch never spuriously flags it — and `--binding-gate=fail` does not start refusing
good C4 behavior claims.
- **`--strength-gate off|warn|fail`** (on `seal` and `verify`; default `off`) — the **truth-axis**
companion to `--binding-gate`. The protocol keeps two questions apart: binding gates *when* a claim
re-checks (trigger), strength gates *whether* its checker can falsify it (truth). `strength.py`
already classified checker strength and flagged adequacy mismatches, but only *printed* them
(advisory); a load-bearing `behavior` claim backed only by an existence check therefore still sealed
green — the review's named #1 false-confidence risk. `--strength-gate=warn` surfaces those
diagnostics after a successful seal; `--strength-gate=fail` refuses the seal (writing nothing,
exit 4, atomic no-write — mirroring `--binding-gate`) when a **load-bearing** claim is high-risk
(`behavior` backed only by existence/raw-text/opaque-shell, `quantity` backed only by existence, or
unbacked). It never marks a claim false and never touches trust/claim state; non-load-bearing claims
and merely-`medium` risk never block; default `off` is byte-identical to prior behavior. The
`strength` module stays out of the trust-state fold path (`fold.py`/`revalidate.py`); a regression
test pins that invariant.

### Fixed
- **Truth-strength inversion in the adequacy lint.** A `behavior` claim backed *only* by an opaque
C5 `shell:` checker (truth strength `shell_executable`, ranked *below* existence) received **no**
`adequacy_mismatch`, because the behavior rule fired only on strengths in `_WEAK_FOR_BEHAVIOR`,
which omitted `shell_executable` — so the weakest, un-introspectable backing silently passed a lint
that a stronger existence backing tripped. `shell_executable` is now treated as too weak for
`behavior` and `quantity` claims (it is opaque: dorian cannot see whether the command proves the
claim), and the same fix lets a quantity claim backed only by an opaque shell be flagged. Advisory
output only; no verdict, trust state, or exit code changes outside the new opt-in `--strength-gate`.

## [1.1.1] — 2026-06-19

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,21 @@ claims.
weak-binding review gate: `warn` prints binding diagnostics after a successful seal; `fail`
refuses the seal (writing nothing, exit 4) when a claim carries a high-risk weak-binding flag.
It never marks a claim false and never changes trust state; `single-file` is warn-only.
- `dorian verify … --strength-gate off|warn|fail` (also on `seal`; default `off`) — the **truth-axis**
companion to `--binding-gate`. Binding gates *when* a claim re-checks; strength gates *whether* its
checker can falsify it. `warn` prints checker-strength/adequacy diagnostics after a successful seal;
`fail` refuses the seal (writing nothing, exit 4) when a **load-bearing** claim's checker is too weak
to falsify its kind — a `behavior` claim backed only by an existence/text/opaque-shell checker, a
`quantity` claim backed only by existence, or an unbacked claim. It never marks a claim false and
never changes trust state; non-load-bearing claims and merely-`medium` risk never block.
- The two gates are **orthogonal and compose**, one per layer of the protocol (see
[Binding is a re-check trigger, not a behavior proof](#binding-is-a-re-check-trigger-not-a-behavior-proof)
and [`docs/VALIDATION_HONESTY.md`](docs/VALIDATION_HONESTY.md)): `--binding-gate` is the
**trigger/selection** axis (*will a relevant later change re-check this claim?*); `--strength-gate`
is the **truth/alarm** axis (*can the checker actually falsify this claim?*). A claim can be
perfectly bound yet weakly backed, or strongly backed yet weakly bound — turn on whichever axis
your review cares about, or both. Neither ever marks a claim false; both map to seal-refused (exit 4).
Copy-paste walkthrough: [`docs/STRENGTH_GATE_DEMO.md`](docs/STRENGTH_GATE_DEMO.md).
- `dorian blast <path|warrant-id> [--max-depth N]` — downstream warrants reachable through the
derives graph. When `revalidate` newly breaks a claim, every downstream warrant gets a `recalled`
event: a flag only — downstream is never re-checked and its states are untouched. Re-seal with
Expand Down
113 changes: 113 additions & 0 deletions docs/STRENGTH_GATE_DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# `--strength-gate` demo

A self-contained, copy-paste run on a throwaway repo showing the truth-axis gate across all four
states. It leaves nothing behind but a temp directory. The behaviour shown here is pinned by
`tests/test_adequacy_gate.py`, so it is executable and kept working, not just illustrative.

`--strength-gate` is the **truth-axis** companion to `--binding-gate`: binding gates *when* a claim
re-checks; strength gates *whether* its checker can actually falsify it. It is **opt-in (default
`off`)**, never marks a claim false, and maps a refusal to the existing seal-refused exit code (4) —
it changes no trust state and adds no code-execution path. See
[`VALIDATION_HONESTY.md`](VALIDATION_HONESTY.md) for the two-layer contract.

## Setup

```bash
tmp=$(mktemp -d) && cd "$tmp" && git init -q
# a real behavior: login rejects expired tokens
printf 'def login(user, token):\n """Authenticate; rejects expired tokens."""\n return bool(token) and not token.endswith("EXPIRED")\n' > auth.py
printf '# change note\n\nlogin() now rejects expired tokens.\n' > note.md
git add -A && git commit -q -m "auth + note"

# a LOAD-BEARING *behavior* claim, but backed only by an existence check (symbol:) —
# the checker can prove login() still EXISTS, but can never prove it rejects expired tokens.
cat > claims.json <<'JSON'
{"claims": [
{"id": "login-behavior", "text": "login() rejects expired tokens.",
"kind": "behavior", "load_bearing": true,
"checkers": [{"type": "C3", "program": "symbol:auth.py::login"}]}
]}
JSON
```

## 1. Default (`off`) — green-but-weak seals silently (today's behavior, unchanged)

```bash
dorian verify note.md --claims claims.json
# -> verified 1/1 claim(s) against current sources -> note.md.warrant (exit 0)
rm -f note.md.warrant
```

The claim seals TRUSTED even though its checker cannot catch the behavior going false. This is the
green-but-weak false confidence the gate exists to surface.

## 2. `--strength-gate=warn` — seals, but surfaces the truth-axis smell (exit 0)

```bash
dorian verify note.md --claims claims.json --strength-gate warn
# stderr:
# login-behavior: adequacy_mismatch: 'behavior' claim backed only by existence
# — only a C4 pytest checker proves behavior
# --strength-gate=warn: claim-risk: 1 high, 0 medium, 0 low; 1 load-bearing high-risk ...
# -> verified 1/1 claim(s) (exit 0) # warn NEVER blocks
rm -f note.md.warrant
```

## 3. `--strength-gate=fail` — refuses the seal (exit 4), writes nothing

```bash
dorian verify note.md --claims claims.json --strength-gate fail
# stderr:
# weak checker: claim 'login-behavior' (kind=behavior, backed only by existence) — adequacy_mismatch: ...
# --strength-gate=fail refused seal: 1 load-bearing claim(s) whose checker is too weak ...; no sidecar written
echo "exit=$?" # -> exit=4
test -f note.md.warrant && echo "sidecar written (BUG)" || echo "no sidecar (atomic no-write)"
```

The refusal runs **after** every checker passes and **before** any write, so nothing is sealed or
indexed. A claim whose checker is *false* would already have been refused earlier (`FAILED_AT_SEAL`),
so the gate never masks a false claim and never marks one BROKEN.

## 4. Fix the evidence — an adequate checker seals under `fail`

Replace the existence checker with one that actually constrains the behaviour. A **structural**
`py-signature:` (stdlib `ast`, no subprocess) is enough to clear the gate:

```bash
cat > claims.json <<'JSON'
{"claims": [
{"id": "login-behavior", "text": "login() takes user and token.",
"kind": "behavior", "load_bearing": true,
"checkers": [{"type": "C3", "program": "py-signature:auth.py::login::user, token"}]}
]}
JSON

dorian verify note.md --claims claims.json --strength-gate fail
# -> verified 1/1 claim(s) (exit 0) # structural backing is adequate; the gate allows it
```

For a claim that genuinely needs *behavioral* proof (not just a signature), back it with a **C4
`pytest:` test** instead — only a passing test proves the body still rejects expired tokens:

```jsonc
{"type": "C4", "program": "pytest:test_auth.py::test_rejects_expired"}
```

A C4-backed behavior claim also seals under `--strength-gate=fail` (its strength is `behavioral`,
the strongest tier). This is the intended authoring path: *let the gate push load-bearing behavior
claims toward tests.*

## What the gate does and does not refuse

| Load-bearing claim | strongest checker | `--strength-gate=fail` |
|---|---|---|
| `behavior` ← `symbol:` / `path:` (existence) | existence | **refuse** |
| `behavior` ← `string:` / `regex:` (raw text) | raw_text | **refuse** |
| `behavior` ← `shell:` (opaque) | shell_executable | **refuse** |
| `behavior` ← `code:` (semantic) | semantic_text | allow (warn-level `medium`) |
| `behavior` ← `py-signature:` / `py-const:` (structural) | structural | **allow** |
| `behavior` ← `pytest:` (behavioral) | behavioral | **allow** |
| `quantity` ← existence / opaque | existence / shell | **refuse** |
| `fact` / `reference` / `decision` ← existence | existence | **allow** (existence is adequate for a fact) |
| any of the above, **not** load-bearing | — | **allow** (a soft claim is the author's call) |
| unbacked, load-bearing | unbacked | **refuse** |
20 changes: 20 additions & 0 deletions src/dorian/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def build_parser() -> argparse.ArgumentParser:
" diagnostics after a successful seal; 'fail' refuses the seal (writing nothing)"
" on a high-risk weak binding. Never marks a claim false; 'single-file' is warn-only.",
)
seal.add_argument(
"--strength-gate",
choices=["off", "warn", "fail"],
default="off",
help="opt-in TRUTH-axis review gate (default off), the companion to --binding-gate:"
" 'warn' prints checker-strength/adequacy diagnostics after a successful seal; 'fail'"
" refuses the seal (writing nothing) when a load-bearing claim's checker is too weak to"
" falsify its kind (e.g. a behavior claim backed only by an existence check). Never"
" marks a claim false.",
)
_add_exec_policy_flags(seal)

vf = sub.add_parser(
Expand Down Expand Up @@ -154,6 +164,16 @@ def build_parser() -> argparse.ArgumentParser:
" diagnostics after a successful seal; 'fail' refuses the seal (writing nothing)"
" on a high-risk weak binding. Never marks a claim false; 'single-file' is warn-only.",
)
vf.add_argument(
"--strength-gate",
choices=["off", "warn", "fail"],
default="off",
help="opt-in TRUTH-axis review gate (default off), the companion to --binding-gate:"
" 'warn' prints checker-strength/adequacy diagnostics after a successful seal; 'fail'"
" refuses the seal (writing nothing) when a load-bearing claim's checker is too weak to"
" falsify its kind (e.g. a behavior claim backed only by an existence check). Never"
" marks a claim false.",
)
_add_exec_policy_flags(vf)

st = sub.add_parser("status", help="trust state of warranted artifacts")
Expand Down
53 changes: 53 additions & 0 deletions src/dorian/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
ScopeConfigError,
ScopeViolation,
SealError,
StrengthGateError,
referenced_paths,
seal_artifact,
)
Expand Down Expand Up @@ -127,6 +128,46 @@ def _print_binding_gate_refusal(prog: str, exc: BindingGateError) -> None:
print(f"{prog}: {exc}", file=sys.stderr)


def _emit_strength_gate_warnings(prog: str, repo: Path, artifact_uri: str, mode: str) -> None:
"""After a successful seal under --strength-gate warn|fail, print the TRUTH-axis
checker-strength / adequacy diagnostics for review (stderr). Informational only —
exit stays 0; weak truth backing is a false-confidence smell, never proof a claim
is false. Distinct from --binding-gate output so a CI integrator can tell a weak-
binding refusal from a weak-checker one."""
try:
claims = list(Warrant.load(repo / (artifact_uri + ".warrant")).claims)
except (gitio.GitError, *_SIDECAR_ERRORS):
print(
f"{prog}: warning: --strength-gate={mode} diagnostics could not be read back; "
"seal remains valid",
file=sys.stderr,
)
return
sdiags = strength.analyze(repo, claims)
for s in sdiags:
for note in s["adequacy"]:
print(f"{prog}: {s['claim_id']}: {note}", file=sys.stderr)
blocking = len(strength.gate_blocking(sdiags))
print(
f"{prog}: --strength-gate={mode}: {strength.summary_line(sdiags)}; {blocking} load-bearing"
" high-risk (checker too weak to falsify the claim's kind; not proof a claim is false)",
file=sys.stderr,
)


def _print_strength_gate_refusal(prog: str, exc: StrengthGateError) -> None:
"""--strength-gate=fail refused: print each blocking claim (id, kind, strongest
backing, adequacy notes), then the refusal."""
for d in exc.findings:
note = "; ".join(d["adequacy"]) or "; ".join(d["reasons"])
print(
f"{prog}: weak checker: claim {d['claim_id']!r} (kind={d['kind']}, "
f"backed only by {d['strength']}) — {note}",
file=sys.stderr,
)
print(f"{prog}: {exc}", file=sys.stderr)


def cmd_capture(args: argparse.Namespace) -> int:
repo = _repo(args)
if _missing_repo(repo, "capture"):
Expand Down Expand Up @@ -220,6 +261,7 @@ def cmd_seal(args: argparse.Namespace) -> int:
allow_restricted=args.allow_restricted,
no_quotes=args.no_quotes,
binding_gate=args.binding_gate,
strength_gate=args.strength_gate,
policy=ExecutionPolicy.from_flags_and_env(
deny_exec=args.deny_exec, deny_shell=args.deny_shell
),
Expand All @@ -233,11 +275,16 @@ def cmd_seal(args: argparse.Namespace) -> int:
except BindingGateError as exc: # before SealError: same exit 4, with the findings
_print_binding_gate_refusal("dorian seal", exc)
return EXIT_REVOKED
except StrengthGateError as exc: # before SealError: same exit 4, truth-axis findings
_print_strength_gate_refusal("dorian seal", exc)
return EXIT_REVOKED
except SealError as exc:
print(f"dorian seal: {exc}", file=sys.stderr)
return EXIT_REVOKED
if args.binding_gate in ("warn", "fail"):
_emit_binding_gate_warnings("dorian seal", repo, artifact_uri, args.binding_gate)
if args.strength_gate in ("warn", "fail"):
_emit_strength_gate_warnings("dorian seal", repo, artifact_uri, args.strength_gate)
print(warrant.id)
return EXIT_OK

Expand Down Expand Up @@ -298,6 +345,7 @@ def cmd_verify(args: argparse.Namespace) -> int:
no_quotes=args.no_quotes,
extra_watch=symbol_watch,
binding_gate=args.binding_gate,
strength_gate=args.strength_gate,
policy=ExecutionPolicy.from_flags_and_env(
deny_exec=args.deny_exec, deny_shell=args.deny_shell
),
Expand All @@ -311,6 +359,9 @@ def cmd_verify(args: argparse.Namespace) -> int:
except BindingGateError as exc: # before SealError: same exit 4, with the findings
_print_binding_gate_refusal("dorian verify", exc)
return EXIT_REVOKED
except StrengthGateError as exc: # before SealError: same exit 4, truth-axis findings
_print_strength_gate_refusal("dorian verify", exc)
return EXIT_REVOKED
except SealError as exc:
print(f"dorian verify: {exc}", file=sys.stderr)
return EXIT_REVOKED
Expand Down Expand Up @@ -344,6 +395,8 @@ def cmd_verify(args: argparse.Namespace) -> int:
)
if args.binding_gate in ("warn", "fail"):
_emit_binding_gate_warnings("dorian verify", repo, artifact_uri, args.binding_gate)
if args.strength_gate in ("warn", "fail"):
_emit_strength_gate_warnings("dorian verify", repo, artifact_uri, args.strength_gate)
return EXIT_OK


Expand Down
Loading