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