Walkthrough — adopting assent on a topic registry¶
Mixed status — read the per-step banner. This page began life as a UX sketch written before any code existed. Most of it now describes the shipped v0.1.0 binary; the steps that still describe unbuilt commands are labelled Planned, and each one names what you can do today instead. The authority for what exists is the CLI reference, which is pinned byte-for-byte to
assent --help. Config semantics: ADR-0010; effects: ADR-0007.
The repo¶
topic-registry — one YAML file per Kafka topic:
# topics/prod/orders.yaml
name: orders
owner: team-orders
partitions: 12
retentionMs: 604800000
Today every MR waits for a platform engineer. Goal: routine changes merge themselves.
Step 1 — scaffold a policy tree¶
Planned —
assent initdoes not exist. There is no scaffolding subcommand in the shipped binary. Copy a starter pack instead; the shipped ones are complete, linted, and covered by their own tests.
git clone https://github.com/PlatformRelay/assent
cp -R assent/examples/packs/topic-registry/.assent .assent
assent lint .
That gives you .assent/config.yaml (environments prod/dev by path, class
kafka-topic), .assent/bindings.yaml, .assent/packs/topics/ (ownership,
bounded-change, non-destructive, schema-valid) and .assent/tests/topics/ with a passing
fixture for every rule — assent lint rejects a rule with no test case, so the two ship
together by construction.
Committed starter packs: examples/packs/
(topic-registry, service-catalog, infra-vars).
Step 2 — make a rule yours¶
Shipped. Policy authoring is the frozen
assent.dev/v1alpha1surface;assent lintis the gate over it.
Edit the starter pack (.assent/packs/topics/rules/bounded-change.yaml), e.g. cap
partitions via your quota provider and challenge retention shrinks — see the full rule
file example in ADR-0010. Wire your company's permission source in config.yaml:
providers:
author: { type: builtin/gitlab-groups, failure: closed } # http/exec too — ADR-0004
Re-run assent lint . after every edit: unknown fields, unknown enums, duplicate
collection IDs, a rule without a test, and an unsupported match.fileEvents kind are all
hard errors, not warnings.
Step 3 — test the policy like code¶
Shipped —
assent test <repo>runs.assent/tests/**against the real engine.
$ assent test .
PASS topics/bounded-change (APPROVE)
PASS topics/bounded-change/negative (REVIEW)
PASS topics/ownership (APPROVE)
PASS topics/ownership/negative (REVIEW)
PASS topics/schema-valid (APPROVE)
PASS topics/schema-valid/negative (BLOCK)
PASS topics/non-destructive (APPROVE)
PASS topics/non-destructive-delete (REVIEW)
That is real output from the shipped starter pack. Each case is a directory with
base/, head/, facts.yaml and expect.yaml (or an inline cases.yaml entry); a
failing case prints the expected and actual decision plus the findings that differ.
--update rewrites expectations from the produced actuals (refused when CI is set),
--coverage is the read-only both-polarity completeness gate. Exit 0 every case
matched; 1 a mismatch, write or load error; 2 usage, discovery, or the CI guard
refusing --update.
Step 4 — backtest before trusting it¶
Planned —
assent scanandassent statsdo not exist. There is no historical backtest over closed MRs in v0.1.0, and no "would-have-automerged %" report.
What ships instead is assent compare: replay a comparison suite of a baseline
versus a candidate policy over a fixed corpus and apply the promotion gates, so you can
see what a policy change would do before you enforce it.
assent compare --suite compare-suite/
Use it the way you would use a backtest for policy edits. For first-adoption evidence
on live MRs, set the pack's rollout phase: observe (ADR-0018) — rules evaluate and land
in findings.observed, structurally excluded from the decision — and read the emitted
DecisionRecords rather than expecting a scan report.
Step 5 — wire CI (GitLab first)¶
Shipped —
assent runis the CI entry point. One caveat below: no container image is published, so the job installs the binary.
# included from a PROTECTED source (compliance pipeline / protected include) — the assent
# job definition must not be editable from the MR branch (ADR-0015 §4); `assent doctor`
# verifies this and the required forge settings (all-threads-resolved merge gate) at setup.
# One-publisher-per-MR (ADR-0019 §3): resource_group keyed per MR IID serializes concurrent
# assent jobs for the same MR. Without it, duplicates converge only on the next reconcile.
assent:
image: golang:1.25 # no ghcr.io/…/assent image is published yet — install in-job
rules: [{ if: $CI_MERGE_REQUEST_IID }]
resource_group: assent-mr-$CI_MERGE_REQUEST_IID
before_script:
# Simplest route, shown here to keep the example self-contained. It stamps the
# binary `0.0.0-dev`, so every DecisionRecord this job emits carries that in
# `pins.toolVersion` (`pins.toolDigest` still identifies the build, D-120).
# For a record that names the real tag, install the checksum-verified release
# archive instead — the full URL pattern is in the install guide.
- go install github.com/PlatformRelay/assent/cmd/assent@v0.1.0
script:
- assent run --project "$CI_PROJECT_ID" --mr "$CI_MERGE_REQUEST_IID"
--subject "file:$SUBJECT" --bot-author "$ASSENT_BOT"
GITLAB_TOKEN comes from a masked CI variable and is never a flag. Run assent doctor
once during setup: it reports whether this environment can arm auto-merge and why not
when it cannot. See Install for the checksum-verified archive route, and
CLI reference for every assent run flag.
Step 6 — the contributor experience¶
Shipped, except the
assent explainblock at the end of this section.
A dev bumps partitions: 12 -> 24 on their own topic in dev: pipeline runs, the MR gets a
summary comment ("APPROVE — 1 obligation proved, score 1/10"), approval, and merges. Nobody was
interrupted.
The same dev shrinks retention on a prod topic: assent opens a resolvable thread —
headline message, then collapsible "Why this check exists & how to fix" (with the rule's
docs.url) and "Evaluation details" sections (ADR-0012).
They resolve the thread ("intentional, ticket TOPIC-123"). assent had already armed the
forge's auto-merge, pinned to the evaluated commit — so the moment the last thread is
resolved, GitLab itself merges (ADR-0009 amendment). Any new push cancels that and
re-evaluates from scratch; the policies that judged this MR came from the target branch,
so nobody can weaken the rules in the MR they gate (ADR-0015).
Planned —
assent explaindoes not exist. Today the same information is in theDecisionRecordthat every run emits (assent run --emit record.json): matched and unmatched rules, obligation results, and the aggregation that produced the decision. The sketch below is the intended local ergonomics, not a shipped command.
$ assent explain --mr 481
change topics/prod/orders.yaml /retentionMs modify 604800000 -> 86400000
class kafka-topic · env prod · binding -> packs [topics, topics-strict] threshold 4
✓ matched retention-shrink-challenge
prove: {obligation: bounded-change, when: "new < old"} = true
onFailure: {effect: challenge, code: bounded-change.out-of-band}
✗ not matched partition-increase
(valueChanges pointer /partitions did not match this change)
aggregation: no block · 1 unresolved challenge -> REVIEW
Status summary¶
| Step | Command | Status |
|---|---|---|
| 1 — scaffold | assent init |
Planned — copy examples/packs/<sample>/.assent |
| 2 — author | assent lint <repo> |
Shipped |
| 3 — test | assent test <repo> |
Shipped |
| 4 — backtest | assent scan / assent stats |
Planned — assent compare covers policy-change replay |
| 5 — CI | assent run · assent doctor |
Shipped (no published container image) |
| 6 — explain | assent explain |
Planned — read the emitted DecisionRecord |
The full dispatched command set — including catalogue, render, eval-input and
version — is in the CLI reference.