diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72b3287..d7e7745 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,9 @@ comparable across the boundary: a mismatch between a pre-D-120 and a post-D-120
- :memo: docs(decisions): record D-125 — CHANGELOG drift gate placement and its cost
- :memo: docs(release): the changelog drift gate is in task check now, not outside it
- :memo: docs(changelog): name the second toolDigest fallback branch
+- :memo: docs(architecture): redraw the C4 diagrams from the real go list graph (AUD-S17)
+- :memo: docs: mark the rego backend and GitHub adapter as planned outside the C4 pages
+- :memo: docs: narrow the composition-root claim and hedge the planned modes in vision.md
### Features
- :sparkles: feat(cli): dispatch-table help listing the real subcommands (REQ-AUD-S05-01)
diff --git a/docs/architecture/c4-container.md b/docs/architecture/c4-container.md
index 7131d85..1374101 100644
--- a/docs/architecture/c4-container.md
+++ b/docs/architecture/c4-container.md
@@ -2,65 +2,181 @@
Hexagonal: a pure decision core, ports for everything with a side effect.
+!!! info "How to read this page"
+ Every **solid** node below names a package that exists today — verify with
+ `go list ./internal/... ./cmd/...`. Every **dashed** node is a *planned* seam that
+ has **no code**: it is designed, not shipped, and unlocks only when a named consumer
+ commits ([D-012](../decisions/decisions.md)). Arrows follow the decision path, and
+ every solid pair drawn is backed by a real import between those two packages. This is
+ not the complete edge set: `cmd/assent` is the composition root and **directly imports
+ 15 of the 22 internal packages** plus the root `schemas` package; those edges are
+ omitted for legibility. It reaches two more — `internal/glob` and
+ `internal/render/locale` — only transitively. The remaining five are not linked into
+ the binary at all (see *Packages with no production importer*). Derive both sets with
+ `go list -f '{{.Imports}}' ./cmd/assent` and `go list -deps ./cmd/assent`.
+
```mermaid
flowchart LR
- subgraph cli["assent CLI (one static binary, one run per MR)"]
+ classDef planned fill:none,stroke:#8a8a8a,stroke-width:1px,stroke-dasharray:5 5,color:#8a8a8a;
+
+ subgraph cli["cmd/assent — one static binary, one run per MR"]
direction TB
- subgraph inbound["Ingestion"]
- forgeIn["Forge adapter (read)\nGitLab | GitHub"]
- policyLoad["Policy loader\n(.assent/ from TARGET ref — ADR-0015)"]
- parsers["Format adapters\nJSON · YAML · HCL/tfvars\n(+ positions, resource limits)"]
- differ["Structural differ + classifier\n→ canonical ChangeSet (classes, env)"]
+
+ subgraph inbound["Ingestion (adapters + loaders)"]
+ forgeGitlab["internal/forge/gitlab
GitLab HTTP adapter"]
+ forgePort["internal/forge
Forge port: Snapshot / Resolve /
Reconcile / PublicationReceipt"]
+ forgeGithub["GitHub adapter
PLANNED — E10"]
+ change["internal/change
value tree · structural differ
JSON · YAML · HCL/tfvars
positions + resource limits"]
+ evaldecode["internal/evaldecode
strict decode of
evaluation inputs"]
+ policy["internal/core/policy
policy envelope types
+ loader (target ref, ADR-0015)"]
+ catalogue["internal/catalogue
catalogue load/combine
profile to pack activation"]
+ remotepacks["Remote packs
PLANNED — E13"]
end
+
subgraph core["Pure decision core (no I/O)"]
- input["PolicyInput\n(ChangeSet + facts + MR metadata)"]
- engine["Policy engine\n(envelope: match · effects · onFail · points)"]
- assertB["assert backend\n(CEL-leaf trees, cel-go — ADR-0013)"]
- regoB["rego backend\n(OPA, capability-sandboxed)"]
- decision["Decision + Findings + Trace + Pins\nAPPROVE | REVIEW | BLOCK"]
+ aggregate["internal/core/aggregate
CEL assert trees (cel-go, ADR-0013)
obligations · coverage · tri-state"]
+ classifyPkg["internal/core/classify
change-set classification"]
+ decision["internal/core/decision
Decision · Findings · Pins
APPROVE · REVIEW · BLOCK
+ report emission"]
+ hash["internal/core/hash
canonical JSON digests
(ADR-0017)"]
+ glob["internal/glob
path globs for selectors"]
+ purity["internal/core
purity guard (test-only):
core must not import I/O"]
+ rego["rego predicate backend
PLANNED — E11"]
end
+
subgraph providers["Provider host (no forge write token)"]
- builtin["Built-ins: forge groups,\nOIDC/Keycloak, LDAP, owners-file"]
- httpexec["HTTP / exec providers\n(digest-pinned)"]
- grpc["gRPC / WASM tiers"]
+ provider["internal/provider
host: HTTP + exec transports
digest pinning · sensitive tier
negotiation · max-age"]
+ builtin["internal/provider/builtin
builtin/gitlab-groups (forge-groups)
repo-file · resource-owner"]
+ grpcw["gRPC (tier 3) + WASM (tier 4)
PLANNED — ADR-0004"]
end
+
subgraph outbound["Publication"]
- renderer["Renderer (escaped, redacted,\nfinding lifecycle state machine)"]
- forgeOut["Forge adapter (write)\nthreads · comments · SHA-guarded\napprove/merge · auto-merge arm"]
- report["JSON report artifact\n(hermetic pins: SHA, policy, facts)"]
+ render["internal/render
Markdown renderer · redaction
finding lifecycle · summary"]
+ locale["internal/render/locale
locale string catalog"]
+ serve["serve (HTTP API)
PLANNED — E12"]
end
+
+ subgraph quality["Authoring + quality surfaces"]
+ lint["internal/lint
assent lint hard errors"]
+ adoptertest["internal/adoptertest
assent test harness (ADR-0014)"]
+ compare["internal/compare
assent compare suite"]
+ schemadrift["internal/schemadrift
presentation/config drift"]
+ forgeFake["internal/forge/fake
in-memory forge fake"]
+ forgeConf["internal/forge/conformance
SHA-guarded reconcile suite"]
+ end
+
+ schemas["schemas (root package)
embedded JSON schemas —
strict-decode authority"]
end
- forgeIn --> parsers --> differ --> input
- policyLoad --> engine
- providers -- facts --> input
- assertB --> engine
- regoB --> engine
- input --> engine --> decision
- decision --> renderer --> forgeOut
- decision --> report
+ forgeGitlab --> forgePort
+ change --> aggregate
+ evaldecode --> change
+ evaldecode --> aggregate
+ catalogue --> policy
+ policy --> schemas
+ policy --> aggregate
+ glob --> aggregate
+ aggregate --> classifyPkg
+ aggregate --> decision
+ builtin --> provider
+ provider --> aggregate
+ provider --> schemas
+ decision --> render
+ locale --> render
+ forgePort --> render
+ render --> forgeGitlab
+ aggregate --> lint
+ aggregate --> adoptertest
+ aggregate --> compare
+
+ forgeGithub -.-> forgePort
+ remotepacks -.-> catalogue
+ rego -.-> decision
+ grpcw -.-> provider
+ decision -.-> serve
+
+ class forgeGithub,remotepacks,rego,grpcw,serve planned;
```
+## Legend
+
+| Style | Meaning |
+| --- | --- |
+| Solid border | **Shipped** — the label names a real Go package or the real binary; present in `go list ./internal/... ./cmd/...` |
+| Dashed border, `PLANNED — E` / `PLANNED — ADR-0004` | **Planned** — a designed seam with **no implementation**. Deferred under [D-012](../decisions/decisions.md); unlocks only when a named consumer commits. See the feature-maturity table in the repository README |
+| Solid arrow | Decision-path flow; the pair is backed by a real import between those two packages |
+| Dashed arrow | The port a planned seam *would* plug into — no code today |
+
+Planned elements shown: **GitHub adapter** (E10), **Rego backend** (E11), **`serve` HTTP API**
+(E12), **remote packs** (E13), **gRPC / WASM provider tiers** ([ADR-0004](../adr/0004-plugin-architecture.md)
+tiers 3–4). Nothing else on this page is aspirational.
+
+## Packages with no production importer
+
+These packages exist and are exercised, but no production code imports them — which is why
+they carry no solid arrow above. This section dates faster than the diagram; re-derive with
+`go list -f '{{.ImportPath}} {{.Imports}} {{.TestImports}}' ./...`.
+
+| Package | Reality |
+| --- | --- |
+| `internal/core` | Test-only guard package (`purity_test.go`); asserts the core does not import I/O |
+| `internal/core/hash` | Canonical JSON digests (ADR-0017). **At this commit** imported only by `internal/change` tests — not yet on the decision path; AUD-S16 wires `internal/compare` to it |
+| `internal/schemadrift` | Drift gate; imported only by the tests of `cmd/assent`, `internal/render` and `internal/forge/conformance` |
+| `internal/forge/fake` | In-memory forge fake; test support only |
+| `internal/forge/conformance` | Port conformance suite; runs as tests, imported by none |
+
+## CLI surface
+
+`cmd/assent` is the only binary. Its dispatch table today is `run`, `doctor`, `lint`, `test`,
+`compare`, `catalogue`, `render`, `eval-input`, `version`, `help`. The authoritative listing
+lives in the [CLI reference](../usage/cli.md), which embeds the verbatim `assent help` output
+and is drift-tested against the dispatch table (`cmd/assent/main_clidoc_test.go`).
+
## Contracts (public, versioned)
| Contract | Consumers |
| --- | --- |
-| **PolicyInput** schema (incl. predicate scope) | policy authors, test harness |
-| **Decision/Findings/Pins** schema | forge adapters, audit tooling, `stats`, test harness |
-| **Provider** request/response (content-keyed FactQuery) | plugin authors (HTTP, exec, gRPC, WASM) |
-| **Forge port** semantics (SHA-guarded writes, thread lifecycle) | adapter implementers; defined by the conformance suite |
-| **Test fixture format** (ADR-0014) | adopters |
+| **PolicyInput / evaluation-input** schema (incl. predicate scope) | policy authors, `assent test` harness |
+| **Decision / Findings / Pins** schema | forge adapters, audit tooling, `assent compare`, test harness |
+| **Provider** request/response (content-keyed FactQuery) | plugin authors — HTTP and exec today; gRPC/WASM are planned tiers (ADR-0004) |
+| **Forge port** semantics (SHA-guarded writes, thread lifecycle) | adapter implementers; defined by `internal/forge/conformance` |
+| **Test fixture format** ([ADR-0014](../adr/0014-adopter-test-format.md)) | adopters |
-## Package sketch (subject to spec phase)
+The JSON-schema families behind these contracts are embedded in the root `schemas` package and
+are the strict-decode authority for the loaders above.
+## Package map
+
+The 22 packages under `internal/` plus `cmd/assent`, exactly as the diagram names them.
+Regenerate with `go list ./internal/... ./cmd/...`; per-package roles are tabulated in
+[`internal/README.md`](https://github.com/PlatformRelay/assent/blob/main/internal/README.md).
+
+```text
+cmd/assent CLI dispatch: run · doctor · lint · test · compare ·
+ catalogue · render · eval-input · version · help
+internal/change value tree, structural differ (JSON/YAML/HCL), ChangeSet
+internal/glob path glob matching for policy selectors
+internal/core purity guard (test-only package — no production code)
+internal/core/aggregate CEL assert trees, obligations, coverage, tri-state
+internal/core/classify change-set classification helpers
+internal/core/decision decision/findings/pins model + report emission
+internal/core/hash canonical JSON hashing (ADR-0017 digest vectors)
+internal/core/policy policy envelope types (profiles, bindings, prove blocks)
+internal/catalogue catalogue load/combine, profile to pack activation
+internal/compare comparison-suite loader, classifiers, gates, records
+internal/evaldecode strict decode of evaluation inputs from JSON/YAML
+internal/lint policy and presentation lint (hard-error fixtures)
+internal/schemadrift presentation/config drift detection
+internal/render Markdown renderer, redaction, summary, finding lifecycle
+internal/render/locale locale string catalog
+internal/forge Forge port (Snapshot/Resolve/Reconcile) + shared helpers
+internal/forge/gitlab GitLab HTTP adapter
+internal/forge/fake in-memory forge fake for tests
+internal/forge/conformance SHA-guarded reconciliation conformance suite
+internal/provider provider host (HTTP/exec transport, sensitive handling)
+internal/provider/builtin gitlab-groups (forge-groups), repo-file, resource-owner
+internal/adoptertest adopter-facing policy test helpers (ADR-0014 fixtures)
```
-cmd/assent/ CLI entrypoints: run, test, lint, explain, scan, stats, doctor, init
-internal/core/ engine, decision model, aggregation (pure)
-internal/change/ value tree, differ, classifier, ChangeSet (pure)
-internal/format/ json | yaml | hcl adapters (positions, limits)
-internal/policy/ envelope loader (target-ref), assert backend, rego backend
-internal/provider/ provider host + built-ins (token-isolated)
-internal/forge/ port + gitlab | github adapters (SHA-guarded writes)
-internal/render/ renderer, finding lifecycle, redaction
-internal/harness/ adopter-facing policy test runner
-```
+
+There is no `internal/format`, `internal/policy`, or `internal/harness` package: format
+adapters live in `internal/change`, the policy envelope in `internal/core/policy` +
+`internal/catalogue`, and the adopter harness in `internal/adoptertest`.
diff --git a/docs/architecture/c4-context.md b/docs/architecture/c4-context.md
index 7cf9232..cc261b7 100644
--- a/docs/architecture/c4-context.md
+++ b/docs/architecture/c4-context.md
@@ -1,26 +1,49 @@
# C4 — Level 1: System context
+Labels carry their own truth marker: **shipped** = implemented and covered by tests today;
+**PLANNED** = a designed seam with no implementation, deferred under
+[D-012](../decisions/decisions.md). The legend below repeats the distinction in prose,
+because `C4Context` diagrams cannot style nodes.
+
```mermaid
C4Context
- title assent — system context
+ title assent — system context (shipped vs PLANNED)
- Person(contributor, "Contributor", "Opens MRs/PRs against a self-service config repo")
+ Person(contributor, "Contributor", "Opens MRs against a self-service config repo")
Person(platform, "Platform engineer", "Owns the repo; authors and tests policies")
- System(assent, "assent", "Deterministic policy-driven auto-merge gate, executed as a CI job per MR/PR")
+ System(assent, "assent (cmd/assent)", "Deterministic policy-driven auto-merge gate; one static binary, run as a CI job per MR")
- System_Ext(forge, "Forge", "GitLab (first) / GitHub (next): hosts repo, MR/PR, threads, approvals, merge")
- System_Ext(ci, "CI runner", "GitLab CI / GitHub Actions: triggers assent per MR event")
- System_Ext(idp, "Permission sources", "Keycloak / LDAP / forge groups / ownership files — via provider plugins")
- System_Ext(facts, "Fact sources", "Site-specific systems answering context questions — via provider plugins")
+ System_Ext(forge, "Forge", "GitLab: shipped adapter (internal/forge/gitlab). GitHub: PLANNED (E10) — no adapter code exists")
+ System_Ext(ci, "CI runner", "GitLab CI: shipped path, triggers assent per MR event. GitHub Actions as a forge trigger: PLANNED (E10)")
+ System_Ext(idp, "Permission sources", "Shipped builtins: GitLab groups, repo-file, resource-owner. Keycloak / LDAP: no builtin — reachable only via the generic HTTP/exec provider transport")
+ System_Ext(facts, "Fact sources", "Site-specific systems answering context questions, via HTTP or digest-pinned exec providers (shipped)")
Rel(contributor, forge, "opens MR / pushes changes")
Rel(platform, forge, "maintains repo + policy dir (.assent/)")
- Rel(ci, assent, "runs per MR/PR")
- Rel(assent, forge, "reads diff & metadata; posts threads/comments; approves/denies; merges")
+ Rel(ci, assent, "runs per MR")
+ Rel(assent, forge, "reads diff & metadata; posts threads/comments; approves/denies; merges (SHA-guarded)")
Rel(assent, idp, "resolves author permissions")
Rel(assent, facts, "resolves external facts")
```
+## Legend
+
+| Marker | Meaning |
+| --- | --- |
+| no marker | **Shipped** — implemented today; the parenthetical names the real package or binary |
+| `PLANNED (E)` | **Planned** — designed seam, **no code**. Deferred under [D-012](../decisions/decisions.md); unlocks when a named consumer commits. `E` is the deferred epic in the meta-plan |
+
+Planned elements on this page: the **GitHub forge adapter** and the **GitHub Actions forge
+trigger** (both E10). Everything else named above exists — see the
+[container diagram](c4-container.md) for the package-level breakdown.
+
+!!! note "GitHub Actions appears twice, with different meanings"
+ assent's *own* repository is built and released on GitHub Actions. That is project
+ infrastructure, not a supported forge path: assent cannot evaluate a GitHub PR, because
+ the GitHub forge adapter (E10) does not exist. The `CI runner` box above refers to the
+ forge path only.
+
Key property: assent is **stateless per invocation** — every run recomputes the decision
-from (diff, repo snapshot, facts, policy version). No database, no long-lived service in v1.
+from (diff, repo snapshot, facts, policy version). No database and no long-lived service
+today; the `serve` HTTP API is PLANNED (E12).
diff --git a/docs/architecture/policy-profiles.md b/docs/architecture/policy-profiles.md
index 1e2be63..252c07a 100644
--- a/docs/architecture/policy-profiles.md
+++ b/docs/architecture/policy-profiles.md
@@ -15,7 +15,7 @@ profile is **recorder-only** (`spec.writes: false`).
Recorder-only evaluation is an **architectural invariant**, not a runtime best-effort
check: no code path reachable from a `writes: false` profile's evaluation may invoke
the forge `Reconcile` port (or any write adapter method that `Reconcile` would call).
-Side-effect-free comparison (`assent compare`, Phase 5+ / E6) evaluates recorder
+Side-effect-free comparison (`assent compare`, shipped in E6) evaluates recorder
profiles over the same ChangeSet solely to produce DecisionRecords for delta
classification.
diff --git a/docs/vision.md b/docs/vision.md
index 0b2368d..750ccf0 100644
--- a/docs/vision.md
+++ b/docs/vision.md
@@ -26,8 +26,9 @@ untested, invisible to the people governed by it, and dies with its author.
1. **Install**: add one job to the repo's pipeline (GitLab CI first; GitHub Actions next) and a
policy directory (e.g. `.assent/`) to the repo.
-2. **Describe**: write rules in **Rego** or a **Kyverno-style declarative YAML** against a
- canonical model of the change — not against raw diff text.
+2. **Describe**: write rules in a **Kyverno-style declarative YAML** envelope with CEL
+ assertions, against a canonical model of the change — not against raw diff text. (A
+ `rego` rule-body backend is a *planned* tier — E11, [D-012](decisions/decisions.md).)
3. **Trust**: assent evaluates every MR/PR deterministically and acts like a reviewer:
resolvable review threads for findings, comments explaining the decision, approve/deny, and
auto-merge when the decision is APPROVE and the platform's own gates (CI green, discussions
@@ -47,21 +48,24 @@ always produce the same decision. No LLM in the decision path.
### Modes
-The same pipeline runs as: a **CI job** (primary), a **local dry-run** ("what would the gate
-say?"), **explain** (full per-rule trace), a **historical scan** over past MRs (backtesting a
-policy before trusting it, feeding `stats` — no database, just report artifacts), and later a
-**webhook service** for orgs that prefer event-driven operation (ADR-0009).
+The same pipeline runs as: a **CI job** (primary) and a **local dry-run** ("what would the
+gate say?"). Three further modes are *planned*, not shipped — **explain** (full per-rule
+trace; today the emitted `DecisionRecord` carries the same information), a **historical
+scan** over past MRs feeding `stats` (backtesting a policy before trusting it — no database,
+just report artifacts; `assent compare` covers the corpus-replay case today), and a
+**webhook service** for orgs that prefer event-driven operation (E12, ADR-0009). See the
+[walkthrough](usage/walkthrough.md) for what each of them does and does not do yet.
## What makes it different
| Capability | Typical bespoke bot | assent |
| --- | --- | --- |
| Change understanding | regex on diff lines | canonical field-level change model for JSON / YAML / HCL-tfvars |
-| Rule language | imperative script | Rego or declarative YAML, versioned in the governed repo |
+| Rule language | imperative script | declarative YAML + CEL assertions, versioned in the governed repo (Rego backend *planned* — E11) |
| Permission checks | hard-coded HTTP calls | pluggable providers: Keycloak, LDAP, GitLab/GitHub groups, ownership files, custom plugins |
| Review UX | pipeline pass/fail | resolvable review threads, comments, approve/deny, auto-merge |
| Testing | none | fixture-based policy tests, required by lint |
-| Platform | one forge | GitLab + GitHub behind one forge-neutral port |
+| Platform | one forge | one forge-neutral port — GitLab adapter shipped, GitHub adapter *planned* (E10) |
## Personas