Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
198 changes: 157 additions & 41 deletions docs/architecture/c4-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>GitLab HTTP adapter"]
forgePort["internal/forge<br/>Forge port: Snapshot / Resolve /<br/>Reconcile / PublicationReceipt"]
forgeGithub["GitHub adapter<br/>PLANNED — E10"]
change["internal/change<br/>value tree · structural differ<br/>JSON · YAML · HCL/tfvars<br/>positions + resource limits"]
evaldecode["internal/evaldecode<br/>strict decode of<br/>evaluation inputs"]
policy["internal/core/policy<br/>policy envelope types<br/>+ loader (target ref, ADR-0015)"]
catalogue["internal/catalogue<br/>catalogue load/combine<br/>profile to pack activation"]
remotepacks["Remote packs<br/>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<br/>CEL assert trees (cel-go, ADR-0013)<br/>obligations · coverage · tri-state"]
classifyPkg["internal/core/classify<br/>change-set classification"]
decision["internal/core/decision<br/>Decision · Findings · Pins<br/>APPROVE · REVIEW · BLOCK<br/>+ report emission"]
hash["internal/core/hash<br/>canonical JSON digests<br/>(ADR-0017)"]
glob["internal/glob<br/>path globs for selectors"]
purity["internal/core<br/>purity guard (test-only):<br/>core must not import I/O"]
rego["rego predicate backend<br/>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<br/>host: HTTP + exec transports<br/>digest pinning · sensitive tier<br/>negotiation · max-age"]
builtin["internal/provider/builtin<br/>builtin/gitlab-groups (forge-groups)<br/>repo-file · resource-owner"]
grpcw["gRPC (tier 3) + WASM (tier 4)<br/>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<br/>Markdown renderer · redaction<br/>finding lifecycle · summary"]
locale["internal/render/locale<br/>locale string catalog"]
serve["serve (HTTP API)<br/>PLANNED — E12"]
end

subgraph quality["Authoring + quality surfaces"]
lint["internal/lint<br/>assent lint hard errors"]
adoptertest["internal/adoptertest<br/>assent test harness (ADR-0014)"]
compare["internal/compare<br/>assent compare suite"]
schemadrift["internal/schemadrift<br/>presentation/config drift"]
forgeFake["internal/forge/fake<br/>in-memory forge fake"]
forgeConf["internal/forge/conformance<br/>SHA-guarded reconcile suite"]
end

schemas["schemas (root package)<br/>embedded JSON schemas —<br/>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<n>` / `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`.
43 changes: 33 additions & 10 deletions docs/architecture/c4-context.md
Original file line number Diff line number Diff line change
@@ -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 questionsvia 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<n>)` | **Planned** — designed seam, **no code**. Deferred under [D-012](../decisions/decisions.md); unlocks when a named consumer commits. `E<n>` 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).
2 changes: 1 addition & 1 deletion docs/architecture/policy-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading