Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
032fadb
:bug: docs(readme): pass the repo root to lint/test, and execute the …
konih Aug 7, 2026
8e95b6e
:memo: docs(install): `go install` binaries report 0.0.0-dev, not a s…
konih Aug 7, 2026
a89defc
:bug: docs(readme): point the ADR-0014 link at the file that exists (…
konih Aug 7, 2026
af0d0cf
:memo: docs(contract): fileEvents ships add/delete — retire the "not …
konih Aug 7, 2026
5e6b623
:memo: docs(walkthrough): per-step Shipped/Planned banners replace th…
konih Aug 7, 2026
7d4e6a5
:memo: docs(meta-plan): renumber the Phase-5 epic table to the epics …
konih Aug 7, 2026
0c8afcd
:memo: docs(adr): ADR-0020 is Accepted — its contract shipped in AUD-S01
konih Aug 7, 2026
9ccb92f
:memo: docs(examples): drop the pre-alpha banner and the "once it exi…
konih Aug 7, 2026
47ad5c8
:memo: docs(usage): document the checkout-less enumeration contract o…
konih Aug 7, 2026
ad448fc
:memo: docs(release): mandate patch tags over in-place asset replacem…
konih Aug 7, 2026
8b19010
:white_check_mark: test(docs): pin the retired truth-lag claims so th…
konih Aug 7, 2026
7738ae5
:twisted_rightwards_arrows: chore(lane): merge origin/main into aud-s06
konih Aug 7, 2026
43e822c
:memo: docs(install): narrow the 0.0.0-dev consequence to the version…
konih Aug 7, 2026
d393efc
:bug: fix(docs-gates): the scripts claimed a wiring that does not exist
konih Aug 7, 2026
98831db
:memo: docs(decisions): record D-124 — the AUD-S06 docs gates are unw…
konih Aug 7, 2026
9e0946e
:bug: docs(examples): starter packs advertised a subcommand that does…
konih Aug 7, 2026
547d30a
:memo: docs(decisions): fold three unfixed residuals into D-124
konih Aug 7, 2026
382d4d4
:twisted_rightwards_arrows: chore(lane): merge origin/main (D-122 emi…
konih Aug 7, 2026
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
23 changes: 14 additions & 9 deletions API_STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ replace — the broader hard-error table in [`docs/planning/lint-hard-errors.md`
Executable guards: `go test ./schemas/... -run TestDoNotGeneralize`. Removing a schema guard
so one of these fixtures validates is a failing regression, not a silent policy expansion.

> **Match-domain implementation status (E1-S06).** The four match-domain *primitives* shipped in
> `internal/core/classify/matcher.go` are `files` / `values.pointers` / `valueChanges` /
> **`entryEvents`**. `entryEvents` matches collection-*entry* identity churn (a keyed map/list
> entry added/removed/renamed within one file, via the E1-S05 `EntryRef`); it is deliberately a
> distinct domain from ADR-0017 §5's **`fileEvents`**, which denotes ADR-0003's whole-file
> git-detected add/delete/rename. Whole-file `fileEvents` is **not yet implemented** (deferred to a
> fast-follow after E1-S08, which first enumerates the MR's full changed-file set). The frozen §5
> vocabulary above is unchanged; this note records that the shipped primitive set substitutes
> `entryEvents` for `fileEvents` until the latter lands.
> **Match-domain implementation status (E1-S06, E-FILEEVENTS).** Alongside the ADR-0017 §5
> vocabulary, `internal/core/classify/matcher.go` also ships **`entryEvents`** — collection-*entry*
> identity churn (a keyed map/list entry added/removed/renamed within one file, via the E1-S05
> `EntryRef`). It is deliberately a distinct domain from §5's **`fileEvents`**, which denotes
> ADR-0003's whole-file git-detected lifecycle.
>
> Whole-file `fileEvents` **is implemented, for a narrowed kind set**: `internal/change.FileEvent`
> mints whole-file lifecycle changes and the loader accepts `match.fileEvents.kinds` ⊆
> **`{add, delete}`**. `modify` and `rename` have no minting path, so a rule naming either is
> **rejected at load** with a located error (`internal/core/policy/loader.go`) rather than left to
> match nothing — closing a vacuous-cover fail-open. This loader-level narrowing sits on top of the
> frozen schema, which still accepts the full `add`/`modify`/`delete`/`rename` enum: widening the
> accepted kinds later is additive and needs no `apiVersion` bump; the frozen §5 vocabulary above is
> unchanged.

## Portability notes (validators)

Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ it destructive, which policy applies. assent encodes that reasoning as policy so
- **Semantic diffs** — JSON, YAML, and HCL/tfvars parse into field-level adds/modifies/deletes,
not line noise ([ADR-0003](docs/adr/0003-canonical-change-model.md)).
- **Testable policies** — fixture changes in, expected decision out; policies without tests
are a lint error ([ADR-0014](docs/adr/0014-policy-test-harness.md)).
are a lint error ([ADR-0014](docs/adr/0014-adopter-test-format.md)).

## How it works

Expand All @@ -65,23 +65,31 @@ See [system context](docs/architecture/c4-context.md) for the full C4 diagram.

## Quick start

Install a stamped binary ([install guide](docs/usage/install.md)):
Install from source ([install guide](docs/usage/install.md)):

```bash
go install github.com/PlatformRelay/assent/cmd/assent@latest
assent version
```

Or verify a release archive with the checksum script — details in
[docs/usage/install.md](https://platformrelay.github.io/assent/usage/install/).
`go install` compiles without link-time stamping, so the binary it produces reports
`assent 0.0.0-dev` — even when you pin a tag (`@v0.1.0`). For a **version-stamped**
binary take the Homebrew tap or a release archive: goreleaser injects the version
(`-X main.version`) and the archives are checksum- and signature-verifiable. Both
routes are in [docs/usage/install.md](https://platformrelay.github.io/assent/usage/install/).

Lint and test policies locally:
Lint and test policies locally. Both commands take the **repository root** — `assent`
appends `.assent` itself, so passing `.assent/` makes it look for `.assent/.assent`:

```bash
assent lint .assent/
assent test .assent/
assent lint .
assent test .
```

No repo of your own yet? A clone of this one ships runnable sample policy trees; run the
two commands above from `examples/packs/service-catalog` (that is the fixture
`hack/docs/readme_smoke_test.sh` executes this block against).

Developers: gates live in the [`Taskfile`](Taskfile.yml):

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| | |
| --- | --- |
| **Status** | Proposed |
| **Status** | Accepted (D-119 — implemented in P5-AUD-S01) |
| **Date** | 2026-08-06 |
| **Deciders** | Konrad Heimel |
| **Context links** | [ADR-0008](0008-change-classification-routing-scope.md) §4 · [ADR-0015](0015-trust-boundaries-merge-integrity.md) §1 · [ADR-0017](0017-contract-model-obligations.md) §1 (honest capability gaps) · D-042 · D-076 · D-077 · REL-07 (PROJECT-AUDIT-2026-08-06) |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ supersessions by ADR-0016/0017 are noted on each ADR's status line (not full
| [0017](0017-contract-model-obligations.md) | Contract model: governed subjects, required obligations, typed facts, preconditioned reconciliation | Accepted |
| [0018](0018-policy-lifecycle-phase-profile-comparison.md) | Policy lifecycle — phase, profiles, comparison | Accepted (D-030) |
| [0019](0019-publication-marker-reconciliation-protocol.md) | Publication marker + reconciliation protocol (database-free) | Accepted (D-030) |
| [0020](0020-forge-snapshot-changed-file-completeness.md) | Forge snapshot changed-file completeness contract | Proposed |
| [0020](0020-forge-snapshot-changed-file-completeness.md) | Forge snapshot changed-file completeness contract | Accepted (D-119) |
23 changes: 14 additions & 9 deletions docs/api-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ replace — the broader hard-error table in [`docs/planning/lint-hard-errors.md`
Executable guards: `go test ./schemas/... -run TestDoNotGeneralize`. Removing a schema guard
so one of these fixtures validates is a failing regression, not a silent policy expansion.

> **Match-domain implementation status (E1-S06).** The four match-domain *primitives* shipped in
> `internal/core/classify/matcher.go` are `files` / `values.pointers` / `valueChanges` /
> **`entryEvents`**. `entryEvents` matches collection-*entry* identity churn (a keyed map/list
> entry added/removed/renamed within one file, via the E1-S05 `EntryRef`); it is deliberately a
> distinct domain from ADR-0017 §5's **`fileEvents`**, which denotes ADR-0003's whole-file
> git-detected add/delete/rename. Whole-file `fileEvents` is **not yet implemented** (deferred to a
> fast-follow after E1-S08, which first enumerates the MR's full changed-file set). The frozen §5
> vocabulary above is unchanged; this note records that the shipped primitive set substitutes
> `entryEvents` for `fileEvents` until the latter lands.
> **Match-domain implementation status (E1-S06, E-FILEEVENTS).** Alongside the ADR-0017 §5
> vocabulary, `internal/core/classify/matcher.go` also ships **`entryEvents`** — collection-*entry*
> identity churn (a keyed map/list entry added/removed/renamed within one file, via the E1-S05
> `EntryRef`). It is deliberately a distinct domain from §5's **`fileEvents`**, which denotes
> ADR-0003's whole-file git-detected lifecycle.
>
> Whole-file `fileEvents` **is implemented, for a narrowed kind set**: `internal/change.FileEvent`
> mints whole-file lifecycle changes and the loader accepts `match.fileEvents.kinds` ⊆
> **`{add, delete}`**. `modify` and `rename` have no minting path, so a rule naming either is
> **rejected at load** with a located error (`internal/core/policy/loader.go`) rather than left to
> match nothing — closing a vacuous-cover fail-open. This loader-level narrowing sits on top of the
> frozen schema, which still accepts the full `add`/`modify`/`delete`/`rename` enum: widening the
> accepted kinds later is additive and needs no `apiVersion` bump; the frozen §5 vocabulary above is
> unchanged.

## Portability notes (validators)

Expand Down
1 change: 1 addition & 0 deletions docs/decisions/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@ project/process decisions.
| D-121 | 2026-08-06 | **ARCH-04 — canonical-hash split codified; `assent-jcs-v1` wired into the replay-bundle digest (corrects D-114 drift).** Rule: digests over BYTE artifacts (policySha over policy bytes, marker occurrence over judged head bytes, marker decision over emitted DecisionRecord bytes, toolDigest) are raw `sha256:<hex>` — byte identity is the point; digests over SCHEMA-OWNED JSON DOCUMENTS that consumers re-parse and re-verify use `internal/core/hash.Digest` (ADR-0017 §9 domain separation) with the schema `$id` as domain. Exactly one digest switches now: `compare.ReplayBundleDigest` → `hash.Digest("https://assent.dev/schemas/decision/v1alpha1/replay-bundle.schema.json", raw)` (D-114 claimed this; implementation used undomained `sha256(json.Marshal(decoded))`). Migration (pre-v1, same commit): regenerate every `replayBundleDigest` in `examples/comparison/*/suite.yaml`; caseIds/bundles unchanged (D-113 immutability preserved — algorithm versioned by this row). ADR-0019 marker grammar untouched. Revert: restore undomained digest + old corpus pins. |
| D-122 | 2026-08-06 | **REL-08 — DecisionRecord emit precedes Reconcile; emit is atomic and fail-closed.** New invariant: NO forge write without a schema-valid, durably-emitted DecisionRecord. Order in `orchestrate`: build → marshal → schema-validate → EMIT (`--emit`: write `<path>.tmp` same-dir + `os.Rename`; stdout: unchanged) → Reconcile → summary. Emit failure → hard error, zero forge writes. Pure reordering: `recordJSON` is fully determined pre-Reconcile (receipt lives in the summary line, not the record) — byte-identical records, marker digests and determinism gate unaffected. Rejected: post-reconcile record stamping (breaks record byte-stability vs marker `decision` digest). Revert: move emit back below Reconcile. |
| D-123 | 2026-08-06 | **ARCH-01 — boundary enforcement automated (depguard + extended purity walk); ADR-0011 Amendment 3 truths the "arch-lint enforced" claim.** Two layers: (1) golangci `depguard` deny-rules — `internal/core/**`, `internal/change/**`, `internal/glob`, `internal/lint`, `internal/catalogue`, `internal/evaldecode`, `internal/compare`, `schemas/**` may import none of `internal/forge/**`, `internal/render/**`, `cmd/**`, `net/**`; (2) `TestCorePurity` walk extends to `../evaldecode`, `../compare`, and `../../schemas` (call-level: `time.Now`/`os.Getenv`/`os.Environ`/rand/net, adversarial self-test retained). Scope note: this EXTENDS the AGENTS.md rule-7 pure tree — `internal/evaldecode` (engine input decode) and `internal/compare` (D-116/D-117 gate determinism) join the determinism guard; `schemas` is embedded compile-time authority. Acceptance: a synthetic violating import/call fails CI both ways. Revert: drop depguard rules + walk dirs and re-amend ADR-0011. |
| D-124 | 2026-08-07 | **AUD-S06 residual — the two docs truth-lag gates exist but are UNWIRED; wiring is assigned to Lane B.** `hack/docs/readme_smoke_test.sh` (REQ-AUD-S06-01, executes every README quick-start command) and `hack/docs/truthlag_pins_test.sh` (REQ-AUD-S06-02, 18 grep/diff pins over DOC-05/06/09/10/11 + the ADR status index) are green and mutation-proven, but nothing invokes them: `Taskfile.yml` and `.github/workflows/**` belong to **Lane B** (AUD-S02/S03/S09/S14), so AUD-S06 could not add them. **Consequence, stated plainly: until they are wired, a future README or docs edit reopens DOC-07 (a quick-start that exits 2) or any pinned claim with NOTHING going red** — the mechanism is a manual gate, which is the same class of defect AUD-S06 exists to close. Assignment: Lane B adds a `docs-gates` task (`bash hack/docs/readme_smoke_test.sh && bash hack/docs/truthlag_pins_test.sh`) as a `check:` dependency, following the `hack/compare/exitgate_test.sh` precedent (D-118), alongside its AUD-S09/S14 workflow work. **Extend when wiring** (known pin gaps, both accepted for now): (a) the DOC-09 walkthrough check asserts banner PRESENCE, not polarity — flipping a step's `Planned` to `Shipped` stays green; (b) the DOC-05 link-resolution loop is scoped to `README.md`, so the relative links in `examples/README.md` are unpinned; (c) AUD-S05's `TestNoStaleProductClaims` (`cmd/assent/main_help_test.go`, Lane A5's file) walks only `cmd/`, `internal/` and `docs/` — markdown under `hack/`, `.github/` and `test/` is grepped by no pin at all. **Known unfixed truth-lag, deliberately not corrected in AUD-S06 to keep the reviewed diff narrow — fix when next editing these files:** (i) `docs/planning/meta-plan.md` closes the Phase-5 epic table with "Ordering constraint: E7 starts early (alongside E1)", directly under the new heading asserting the table is the numbering that actually executed — E7 in fact landed after E6 (E6 tip `ec91226` is an ancestor of E7-S08 `f27457d`, both 2026-08-04); (ii) `docs/usage/install.md` credits a stamped version to "the Homebrew **bottle**", but `.goreleaser.yaml`'s `brews:` block publishes a **Formula** whose `url_template` points at the release archive — no bottle is built or hosted; the version claim is true, only the term is wrong. Revert: delete the two scripts and reopen DOC-05/06/07/09/10/11 as live findings. |
38 changes: 25 additions & 13 deletions docs/planning/meta-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,31 @@ minimal.

## Phase 5 — Epic execution

Spec-first, vertical slices per epic (proposed cut, refined in Phase 3):

| Epic | Slice |
| --- | --- |
| E1 | Canonical change model: JSON + YAML (+ HCL/tfvars) |
| E2 | Decision engine + Rego frontend |
| E3 | Declarative YAML frontend |
| E4 | Forge: GitLab adapter (threads, approve, merge) |
| E5 | Provider host: built-ins + HTTP/exec |
| E6 | Adopter test harness (`assent test`) + examples |
| E7 | E2E infra: kind GitLab, sample-repo generator, conformance suite |
| E8 | Forge: GitHub adapter + Actions entrypoint |
| E9 | Distribution: releases, container, CI templates, docs site |
Spec-first, vertical slices per epic. The E-numbering below is the one that actually
executed — each row names its spec under `openspec/specs/` — and it matches the README
feature-maturity table. (The Phase-2 proposed cut had E2 as a Rego frontend and E8 as the
GitHub adapter; both moved to the deferred tier, so the numbering shifted.)

| Epic | Slice | Spec | Status |
| --- | --- | --- | --- |
| E1 | Canonical change model: JSON + YAML (+ HCL/tfvars) | `p5-e1-canonical-change-model` | shipped |
| E2 | Decision engine + CEL predicate backend | `p5-e2-decision-engine` | shipped |
| E3 | Policy surface: `assent lint` hard errors + rule catalogue | `p5-e3-policy-surface` | shipped |
| E4 | GitLab forge adapter: Snapshot / Resolve / Reconcile | `p5-e4-gitlab-forge` | shipped |
| E5 | Provider host + builtins (HTTP/exec, gitlab-groups, ownership) | `p5-e5-provider-host` | shipped |
| E6 | Adopter test harness (`assent test`) + `assent compare` seed | `p5-e6-adopter-test` | shipped |
| E7 | E2E & conformance infra | `p5-e7-e2e-conformance` | shipped |
| E8 | Renderer & presentation (ADR-0016 tier 0) | `p5-e8-renderer` | shipped |
| E9 | Distribution & release (oss-playbook) | `p5-e9-distribution` | shipped (v0.1.0) |

Follow-on epics cut during Phase 5, outside the E1–E9 sequence: **EFE**
(`p5-e-fileevents`, whole-file `match.fileEvents`), **PCS**
(`p5-pcs-policy-comparison`, full comparison-suite runner), **AUD**
(`p5-aud-audit-remediation`, post-release audit remediation).

Deferred tiers keep their own numbers and unlock only with a named consumer (D-012):
**E10** GitHub adapter, **E11** Rego backend, **E12** `serve` (HTTP API), **E13** remote
packs — see the feature-maturity table in `README.md`.

Ordering constraint: E7 starts early (alongside E1) because every later epic's exit gate
depends on it.
Expand Down
18 changes: 17 additions & 1 deletion docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,30 @@ and is never a flag; without it the command exits `2` before contacting the forg
| `-binding` | `.assent/ruleset-binding.yaml` | RulesetBinding path, loaded from the target ref |
| `-config` | — | optional Config path; when set, provider posture is validated |
| `-pack` | — | optional Pack path; its `spec.phase` caps every rule's phase |
| `-checkout` | — | local checkout dir (`base/` + `head/` subtrees) used to enumerate the MR's full changed-file set; when unset, only the governed subject is diffed |
| `-checkout` | — | local checkout dir (`base/` + `head/` subtrees) used to enumerate the MR's full changed-file set; when unset, the forge snapshot is the sole enumerator (see below) |
| `-emit` | stdout | path to write the `DecisionRecord` JSON |
| `-arm` | off | sandbox arming override — approve and merge only when set **and** the decision is APPROVE |

Exit codes: `0` the run completed and produced a valid receipt (an advisory
REVIEW/BLOCK, or an APPROVE without `--arm`, is still a clean `0`); `1` a hard error
during orchestration; `2` a missing flag, a missing `GITLAB_TOKEN`, or `-h`.

### Checkout-less runs and enumeration completeness

Without `-checkout`, the forge snapshot's changed-file list is the only thing that can
see a `.assent/**` policy edit outside the governed subject — so an incomplete list
would silently starve the self-edit guard. Per [ADR-0020](../adr/0020-forge-snapshot-changed-file-completeness.md)
the adapter must therefore *prove* completeness (paginated `/diffs`, cross-checked
against the MR's `changes_count`, below a page ceiling). When it cannot, the run does
not guess and does not fail silently: the change set is marked opaque and the decision
degrades to **REVIEW** with finding code `changeset.undecidable`, carrying the gap
reason. A `DecisionRecord` is still emitted and a thread still posted; approve and
merge are impossible on that path. A `.assent/**` path that *is* visible in a partial
list still dominates to BLOCK.

With `-checkout` the local tree is the sole authority (D-077) and snapshot completeness
is not consulted.

## assent doctor

Report whether this environment can arm auto-merge, and why not when it cannot.
Expand Down
25 changes: 24 additions & 1 deletion docs/usage/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Assent ships as a single static Go binary. Prefer a checksum-verified install for
release artifacts; use `go install` when developing from source.

Only the **release archives and the Homebrew bottle carry a stamped version** —
goreleaser injects it at link time (`-s -w -X main.version={{.Version}}` in
`.goreleaser.yaml`). A `go install` build has no such injection; see the caveat below
before you rely on `assent version` for provenance.

## go install

Requires Go 1.25+ (see `go.mod`).
Expand All @@ -23,6 +28,24 @@ Confirm:
assent version
```

!!! warning "`go install` binaries report `0.0.0-dev`"

`go install` does not pass the release ldflags, so the version stays at its
compile-time default whatever ref you build:

```console
$ go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0
$ assent version
assent 0.0.0-dev
```

That is cosmetic for local policy authoring (`assent lint` / `assent test`). A
`DecisionRecord` from such a binary is still identifiable — `pins.toolDigest` is a
sha256 over the binary's Go build info (D-120), so different builds differ regardless
of the version string — but `pins.toolVersion` reads `0.0.0-dev` and cannot be mapped
back to a released tag. Use the [release archive](#github-release-url-pattern) or
[Homebrew](#homebrew) route when the version string itself has to be true.

## curl / local install script (checksum-verified)

[`hack/install.sh`](https://github.com/PlatformRelay/assent/blob/main/hack/install.sh) verifies the archive SHA256 against a
Expand All @@ -46,7 +69,7 @@ Pick the archive that matches your OS/arch if the glob expands to more than one

### GitHub release URL pattern

Once tagged releases publish (E9-S05/S06):
Tagged releases publish under this pattern (`v0.1.0` onwards):

```bash
VERSION=0.1.0
Expand Down
Loading