M1-02: Action catalogue as the single data source (ADR 0049)¶
Status: ready | Depends on: none | Milestone: M1
Goal¶
After this ticket docs/spec/actions.yaml is the only place where actions, rate-limit classes,
event types with their receive action and WebSocket ops are written. The tables of chapter 05 and
the action and class lists in docs/api/redocly.yaml are generated from it, and the gate
catalog-check fails on any drift or inconsistency. Later tickets read the same file: M1-03 for
the API contract checks, M1-06 for the module register, M1-25 for the Go constants of authz.
ADR 0049 records the decision.
Read first¶
docs/PLAN.md: section "Way of working per ticket"; section "M1 Foundation" (Block A, row M1-02; "Prerequisites from the core contract", row "machine-readable action catalogue"); P-11, P-12 in "Decisions of this plan".docs/spec/05-actions.md, the whole chapter, especially "Binding rules" (rules 3, 4, 10), "Scopes for apps" (2.6), "Rate-limit classes", "Actions" (4.1 to 4.14), "Durable event types and receive action", "WebSocket ops and action", "Tests that hold this catalogue", "Configuration values of this chapter".docs/spec/00-glossary.md(terms),docs/spec/04-sync-protocol.md"Ops" (6.2) and "Two kinds" (2.1).docs/spec/02-architecture.md: "Alignment with the core contract" (row "machine-readable action catalogue"), "Actions per module", "Event types and WebSocket ops per module" (read only; M1-06 checks them).docs/api/redocly.yaml(rulesauthz-action-known,rate-limit-class-known,receive-action-known-message,receive-action-known-webhook),docs/api/asyncapi.yaml(operations,components.messageswithx-event-kind,x-receive-action,x-audience),docs/api/README.md("Change").- ADRs:
docs/adr/0017-one-authorization-function-404-instead-of-403-mandatory-registration.md,docs/adr/0013-spec-first-openapi-31-and-asyncapi-30-normative.md,docs/adr/0001-adrs-in-madr-format-decisions-as-ruling-log.md,docs/adr/0000-template-madr-4.md,docs/adr/0049-machine-readable-action-catalogue-as-the-single-source.md(accepted; it already records the decision and the rulings of this ticket, see "Design notes"). docs/quality/gates.md,docs/spec/23-quality.md"Gates",tools/README.md,tools/pyproject.toml,Makefile(targetsgates,lint,api-lint),CHANGELOG.md,docs/DECISIONS.md.
Action ids: all of chapter 05 section "Actions". Event types and ops: all of sections 6 and 7. No API operation and no table of chapter 03 changes.
Fixed requirements that apply¶
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
- Test first: every behavior gets a test that is red before the change (show the red output in the report), then green.
- No en dash (U+2013) and no em dash (U+2014) in any file or commit message. Check every changed
file with
uv run --project tools --locked --quiet python tools/hooks/text_rules.py <file>(exit code 0; plaingrep -Pfails on macOS). - White label: no company, person or host names, no real domains (only example.com and example.org), no real IP addresses, no absolute paths into a home directory, no internal system names, in any file including tests and fixtures.
- Limits and thresholds are named configuration values from
docs/operations/configuration.md; prose uses the name, never the number. This ticket adds no new value. - The catalogue is closed: an unknown action is a program error (05 rule 5); every REST operation names exactly one known action and its class (05 rule 3); every op and every event type names its action or receive action (05 rule 4); system actions are not reachable over API or gateway (05 rule 10, test 7 in "Tests that hold this catalogue").
- Chapter 05 is core contract and normative (
docs/PLAN.md"Principles" item 3). The content of every table row stays exactly as it is; only its source moves. If a row cannot be expressed in the schema below without changing its text, stop withVerdict: BLOCKEDand name the row (P-11). - One source per fact: actions, classes, event types and ops exist in
actions.yamlonly; every copy is generated and checked (ADR 0049).
Scope¶
docs/spec/actions.yaml(new): the catalogue in the schema of "Design notes", first line# SPDX-License-Identifier: Apache-2.0, then a short English header comment (purpose, ADR 0049, "edit here, then runmake catalog").tools/catalog/action_catalog.py(new; Python 3.12, standard library plus PyYAML). CLI:action_catalog.py check [--root <repo root>](exit 0 clean, 1 findings) andaction_catalog.py generate [--root <repo root>](rewrites the generated blocks, exit 0, or 1 if the catalogue itself is invalid; never writes a block from an invalid catalogue). Default root: the repository containing the script. Public interface used by M1-03, M1-06 and M1-25, each with a docstring:class CatalogError(Exception)carryingfindings: list[str].- Frozen dataclasses
RateLimitClass,Action,Event,WsOp,Catalogwith the fields of "Design notes";Action.systemis a property (right == "system").Catalogoffersactions_by_id,events_by_type,ws_ops_by_name,classes_by_idas read-only mappings. def load_catalog(path: Path) -> Catalog(validates the schema and every rule that needs only the YAML; raisesCatalogErrorwith all findings).def generated_blocks(text: str) -> dict[str, str]anddef replace_generated_block(text: str, name: str, body: str) -> strfor Markdown markers<!-- BEGIN GENERATED: <name> -->and<!-- END GENERATED: <name> -->(the form already used indocs/spec/06-api.md);bodyis everything between the two marker lines and ends with a newline. Unbalanced, nested or duplicate markers raiseCatalogError.def check(root: Path) -> list[str]anddef generate(root: Path) -> list[str](returns the paths it rewrote).
tools/catalog/tests/test_action_catalog.py,tools/catalog/tests/table_import.py(test helper, see "Design notes"),tools/catalog/tests/fixtures/05-actions-before.md.txt(byte copy ofdocs/spec/05-actions.mdfrom the base commit of this ticket; suffix.txtso thatmake linkcheckdoes not check its relative links).tools/catalog/README.md(English, short: purpose, commands, schema pointer).docs/spec/05-actions.md: wrap each generated table in markers (names in "Design notes"); the rows themselves are produced bymake catalog. Rewrite rule 3 of "Binding rules": the sentence "A machine-readable version ... is decided (ADR 0049); until it exists, a reconciliation ..." becomes: the single source is[actions.yaml](actions.yaml)(ADR 0049); the tables of sections 3, 4, 6 and 7 and the lists indocs/api/redocly.yamlare generated from it (make catalog) andmake catalog-checkholds them in the gate. Rule 4: add that sections 6 and 7 are generated from the same file. No other text of the chapter changes.docs/api/redocly.yaml: replace the three hand lists by generated blocks (YAML comment markers, see "Design notes"); update the header comment (sourcedocs/spec/actions.yaml, regenerate withmake catalog, checked bymake catalog-check).make api-lintmust stay green.Makefile: targetscatalog(## Regenerate the tables of chapter 05 and the lists in docs/api/redocly.yaml from docs/spec/actions.yaml) andcatalog-check(## Action catalogue valid and its generated copies current (ADR 0049));catalog-checkgoes intogatesdirectly beforeapi-lint; addtools/catalogto the bandit directory list oflint; add both to.PHONY.tools/pyproject.toml: addcatalogtotestpaths,pythonpath,[tool.coverage.run] sourceand[tool.mypy] files.- ADR 0049 exists. Change it only where the implementation differs from it (then set
date:to the day of the merge and runmake adr-index); no new ADR. - Other documents:
docs/spec/02-architecture.md"Alignment with the core contract", row "machine-readable action catalogue": nowdocs/spec/actions.yaml(ADR 0049), checked bymake catalog-check.docs/api/README.md"Change" items 2 and 3: a missing action or a new event type entersdocs/spec/actions.yamlfirst, thenmake catalog.docs/quality/gates.md: rowcatalog-check.docs/spec/23-quality.md"Gates", group "Docs": addcatalog-check.tools/README.md: row forcatalog/.CHANGELOG.md[Unreleased].docs/DECISIONS.md: the rulings in "Design notes".
Out of scope¶
- Checks of
openapi.yaml,asyncapi.yamlandwebhooks.mdagainst the catalogue, and the generated tables of chapter 06: M1-03. - The tables of chapter 02 ("Actions per module", "Event types and WebSocket ops per module") and
module ownership of actions: M1-06 (module ownership lives in
architecture/modules.yaml, ADR 0050, never inactions.yaml). - Go constants and the closed catalogue in code: M1-25.
- The "Configuration values of this chapter" table of 05 stays hand-written (it feeds
docs/operations/configuration.md); this ticket only cross-checks it. - The scope table "Scopes for apps" (2.6) and the list "Effect (
effect)" (2.7) stay hand-written (prose); this ticket cross-checks both against the YAML.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M1-02-action-catalogue-source. - Conventional Commits with DCO sign-off (
git commit -s), for exampledocs(spec): action catalogue as the data source (ADR 0049)andbuild(catalog): .... - Never push, tag, open pull requests or create remote repositories.
- Do not modify files outside the scope above, except
docs/DECISIONS.mdandCHANGELOG.mdas named. - Follow the host rules supplied by the launcher. Stop only processes you started, by PID.
Temporary directories only via
mktemp -dor pytesttmp_path, removed at the end. - Never read or print secrets. The launcher provides the private deny list; do not set
DENYLIST_FLAGS=--allow-missingorSONAR_SKIP=1. - No network calls except package downloads for pinned tools.
- Do not lower a threshold, disable a gate or delete a test.
Design notes¶
Schema of docs/spec/actions.yaml¶
Top level, exactly these keys in this order; unknown keys anywhere are findings:
| Key | Type | Meaning |
|---|---|---|
version |
integer 1 |
schema version |
rate_limit_classes |
list of class | row order of 05 "Rate-limit classes" |
sections |
list of strings | keys of the action tables, in order: public (4.1), instance (4.2), identity (4.3), grants (4.4), structure (4.5), threads (4.6), messages (4.7), files (4.8), personal (4.9), agents (4.10), apps (4.11), operations (4.12), dev (4.13), terminal (4.14) |
actions |
list of action | row order of 05 section 4 |
events |
list of event | durable rows, then ephemeral rows, in 05 row order |
ws_ops |
list of op | row order of the first table of 05 "WebSocket ops and action" (gateway) |
channel_ops |
list of channel op | row order of the second table of 05 "WebSocket ops and action" ("Terminal channels") |
Class: id (rl.<name>), key (cell "Key"), purpose (cell "Purpose").
Action (05 column in brackets):
| Field | Type | Required | Renders as |
|---|---|---|---|
id |
string, pattern ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$, unique |
yes | `id` (ID) |
section |
one of sections |
yes | (which table) |
description |
string, verbatim Markdown | yes | Description |
resource |
string, verbatim | yes | Resource |
right |
string, verbatim; the value system marks a system action |
yes | Permission |
owner |
true, false or as_target |
yes | yes, no, as target action (Owner) |
step_up |
true, false, conditional or fresh |
yes | yes / no / conditional / fresh (Step-up; conditional and fresh per 05 "Columns Owner, Step-up, Audit, Permissions"; fresh is used by the terminal actions of 4.14) |
audit |
always, denials, never or conditional |
yes | yes, denials, no, or audit_text (Audit) |
audit_text |
string | only and always with conditional |
verbatim cell, for example yes for others' |
rights_change |
boolean | yes | yes / no (Permissions) |
rl_class |
a class id or none |
yes | `rl.x` or `none` (RL class) |
delivery_check |
boolean, default false |
no | marks the internal delivery checks (push.deliver, webhook_out.deliver) |
effect |
read or write |
yes | not rendered; the closed rule of 05 "Effect (effect)" (2.7): read exactly for the actions listed there, write for all others; the check derives the expected value from 2.7 and compares |
app_scope |
one of messages:read, messages:write, files:read, files:write, commands:write, components:write, users:read, events:subscribe, costs:reconcile, none |
no | not rendered; mirrors 2.6 (none: usable by apps without a scope; absent: 2.6 names no scope) |
Event:
| Field | Type | Required | Meaning |
|---|---|---|---|
type |
string, same pattern as action ids, unique | yes | Event type |
kind |
durable or ephemeral |
yes | which of the two tables |
resource |
string | yes | Resource |
receive_action |
a non-system action id or self |
yes | machine receive action (equals x-receive-action in asyncapi.yaml) |
audience |
self |
no | only the named principal receives it (equals x-audience: self); absent means every permitted recipient |
receivers |
principal, agent or agent_or_app |
only with audience: self, default principal |
who can be that principal; M1-03 uses it for the webhook table |
receive_text |
string | no | verbatim cell override when the cell is more than the action |
note |
string | no | Note (durable) or Additional condition (ephemeral); empty when absent |
resource_version |
boolean | yes for durable |
not rendered; whether the envelope carries resource.version (04 "Delivery guarantee" point 3: only when the table of the resource has a version column and the event reports its new value; the listed exceptions are false). M1-25 carries it into EventSpec, and the frame conformance test of M1-67 checks every dispatched event against it |
Receive cell: receive_text if set; else self (no backticks) for self; else `id`.
receive_action: self requires audience: self. When set, receive_text must contain
`<receive_action>` (or the word self for self).
Values for the current rows (from 05 section 6 and docs/api/asyncapi.yaml): every event whose
cell starts with self has audience: self. receivers: agent for agent.invocation,
agent.followup, run.cancel_requested, agent.config_changed; receivers: agent_or_app for
interaction.created, command.invoked, command.autocomplete_requested; all other self
events have principal. receive_action of the composite cells: agent.events.consume for
agent.invocation, agent.followup, run.cancel_requested, interaction.created,
command.invoked, command.autocomplete_requested; agent.config.read for
agent.config_changed; push.deliver for notification.created.
WebSocket op:
| Field | Type | Required | Meaning |
|---|---|---|---|
op |
string, unique | yes | Op |
direction |
client_to_server, agent_to_server, agent_or_app_to_server, server_to_client |
yes | renders client to server, agent to server, agent or app to server, server to client |
action |
non-system action id | yes | the action checked for the op (equals x-authz-action in asyncapi.yaml) |
also_actions |
list of action ids | no | further checks (heartbeat: agent.heartbeat for agents) |
action_text |
string | no | verbatim cell override |
row_group |
string | no | consecutive ops with the same group share one table row |
per_event_receive_action |
boolean | no, only on dispatch |
delivery checks the receive action of each event |
Current values: identify, resume, heartbeat, reauth, focus, typing.start,
typing.stop are client_to_server; ack and run.update are agent_to_server;
command.autocomplete_result is agent_or_app_to_server; hello, ready, resumed, cursor,
catchup.complete, resync_required, heartbeat_ack, reauth_required, error are
server_to_client with action gateway.connect, row_group: session and the action text
"none of its own; part of the session from gateway.connect"; dispatch is server_to_client
with action gateway.connect, per_event_receive_action: true and the action text "receive
action of the event type (section 6)"; heartbeat has also_actions: [agent.heartbeat] and the
action text "gateway.connect (session must be valid); for agents also agent.heartbeat";
focus has the action text "gateway.focus per thread"; stats.subscribe and
stats.unsubscribe are client_to_server with action stats.read, row_group: stats and the
action text "stats.read on the instance or space respectively". These actions equal x-authz-action in
docs/api/asyncapi.yaml today; M1-03 keeps them equal.
Channel op (one row of the table "Terminal channels" of 05 section 7; these channels are not the
gateway and have their own operations in docs/api/asyncapi.yaml):
| Field | Type | Required | Meaning |
|---|---|---|---|
channel |
terminal_stream, host_control, host_data |
yes | asyncapi channel terminalStream, hostControl, hostData |
channel_text |
string | only on the first row of a channel | verbatim cell "Channel"; continuation rows render an empty cell |
ops |
list of op names; the name noise_frame stands for binary Noise frames |
yes | cell "Op": backticked names joined by ,; noise_frame renders as binary frames (Noise) |
direction |
server_to_device, device_to_server, server_to_host, host_to_server, both |
yes | server to device, device to server, server to host, host to server, both |
action |
non-system action id | yes | equals x-authz-action of the matching asyncapi operations |
action_text |
string | no | verbatim cell override (for example "terminal.stream (the session must be valid)") |
Current values follow the six rows of that table: terminal_stream with hello (server to device),
identify (device to server) and noise_frame (both), action terminal.stream; host_control
with the server-to-host ops hello, ready, heartbeat_ack, error, session.open,
session.close and the host-to-server ops identify, heartbeat, report, session.started,
session.ended, action host.connect; host_data with hello, identify, noise_frame
(both), action host.connect.
Validation rules (load_catalog and check)¶
Each rule yields one finding line actions.yaml: <rule>: <detail> (or the path of the chapter or
redocly file for block rules):
- Schema: required keys, types, enums, no unknown keys,
version == 1. - Uniqueness: class ids, action ids, event types, ops.
- Every
sectionis listed; every listed section has at least one action. rl_classis a known class ornone;rl_class == noneexactly for actions withright: systemordelivery_check: true(05 "Rate-limit classes", last sentence); every class is used by at least one action.- Every class has the rows
ratelimit.<id>.per_minute/.burstin 05 "Configuration values of this chapter" (row form`ratelimit.rl.x.per_minute` / `.burst`), and that table names no other class. - Events:
receive_actionpresent ("event type without receive action"), known, not a system action; theaudience,receiversandreceive_textrules above. - Ops:
actionpresent ("op without action"), known, not a system action;also_actionsknown; members of arow_groupare consecutive and sharedirection,actionandaction_text. Channel ops:actionknown and not a system action; rows of one channel are consecutive and only the first carrieschannel_text; an op name is unique within its channel and direction. - 2.6 consistency: parse the table under "Scopes for apps". In column one every backticked token
is an action id or a pattern ending in
.*(prefix match); prose without backticks is ignored. In column two the first backticked token is the scope; a cell without one meansnone. Every action named there has thatapp_scope, and every action withapp_scopeis named in exactly one row. - Generated blocks: the set of block names in 05 equals the expected set; each block equals the
rendering ("generated block differs from docs/spec/actions.yaml; run make catalog"); the same
for the three YAML blocks in
docs/api/redocly.yaml.
Generated blocks¶
In docs/spec/05-actions.md, each block contains the whole table including its header line and
separator line:
| Block name | Table | Header |
|---|---|---|
rate_limit_classes |
"Rate-limit classes" | \| Class \| Key \| Purpose \| |
actions.<section key> (14 blocks) |
each table of section 4 | \| ID \| Description \| Resource \| Permission \| Owner \| Step-up \| Audit \| Permissions \| RL class \| |
events.durable |
first table of section 6 | \| Event type \| Resource \| Receive action \| Note \| |
events.ephemeral |
second table of section 6 | \| Event type \| Resource \| Receive action \| Additional condition \| |
ws_ops |
first table of section 7 | \| Op \| Direction \| Action \| |
channel_ops |
second table of section 7 ("Terminal channels") | \| Channel \| Op \| Direction \| Action \| |
Separator lines are |---|---|... with one --- per column. Row rendering in chapter 05:
"|" + "".join(f" {cell} |" if cell else " |" for cell in cells); an empty cell renders as a
single space (| |), as in the current chapter. All prose between the tables (for example
"Ephemeral events:", the paragraph after 4.4, the intros of 4.13 and 4.14, the paragraph after
4.14, the line "Terminal channels ..." before the second table of section 7) stays outside the markers
and hand-written. Marker lines stand directly before the header line and directly after the last
row, without blank lines, like in chapter 06.
In docs/api/redocly.yaml the blocks use YAML comment markers at the indentation of the list
items, for example:
enum:
# BEGIN GENERATED: authz_actions (make catalog)
- access.explain
# END GENERATED: authz_actions
| Block | Rule | Content |
|---|---|---|
authz_actions |
rule/authz-action-known |
every non-system action id, sorted |
rate_limit_classes |
rule/rate-limit-class-known |
none, then every class id, sorted |
receive_actions |
rule/receive-action-known-message (anchor &receive_actions, reused by the webhook rule) |
every non-system action id plus self, sorted |
Excluding system actions from both lists is deliberate: 05 test 7 forbids a route or op with a
system action, and no event has one as receive action; the lint now enforces it. Replace the old
comment line # catalogue (docs/spec/05-actions.md section 4) by the markers. The generator
rewrites only lines between markers and keeps every other byte of the file.
Migration without content change (proof)¶
tools/catalog/tests/table_import.py is a test helper with
def import_chapter(chapter: str) -> dict: it parses the pre-migration tables of chapter 05
(located by their headings in the fixture) into the YAML structure above. Cell mapping:
yes/no to booleans, Step-up conditional to conditional and fresh to fresh, as target action to as_target, Audit yes/denials/no to
always/denials/never and any other text to conditional plus audit_text, backticked
none to none, backticked rl.x to rl.x, delivery_check: true where the class is none and the
right is not system; app_scope from 2.6; for event cells that are a single backticked action
or self the obvious values, for the composite cells the receive_action, audience and
receivers listed above (a constant table in the helper, so the test depends on nothing but the
frozen fixture); ops as listed above. Cross-check the result once against x-receive-action and
x-audience in docs/api/asyncapi.yaml before committing. Use it once to write
docs/spec/actions.yaml (yaml.safe_dump(..., sort_keys=False, allow_unicode=True, width=100)
behind the header comment), review the file by hand, and keep the helper only for the fixture
test. The proof that no row changed is twofold: the fixture test below, and
git diff <base> -- docs/spec/05-actions.md shows only added marker lines plus the edited
sentences of rules 3 and 4 (put that diff summary into the report).
ADR 0049¶
ADR 0049 (docs/adr/0049-machine-readable-action-catalogue-as-the-single-source.md) is accepted
and names the file, the generated copies, the hand-written tables that are only cross-checked
(2.6 and the configuration values of 05), the machine value next to prose cells of the receive
action, and the confirmation (make catalog-check in gates, the tests in tools/catalog/tests/).
M1-03, M1-06 and M1-25 read the same file.
Rulings for docs/DECISIONS.md¶
- The redocly lists of known actions exclude system actions. Reason: 05 test 7; the lint enforces what a route test would find later.
- The scope table 2.6 and the configuration table of 05 stay hand-written and are cross-checked
by
catalog-check. Reason: their rows contain prose and the configuration table feeds the configuration reference. actions.yamlcarries no module ownership. Reason: ownership belongs to the module register (ADR 0050, M1-06); one fact, one source.
Tests (write them first)¶
tools/catalog/tests/test_action_catalog.py. Negative cases copy docs/spec/actions.yaml,
docs/spec/05-actions.md and docs/api/redocly.yaml into tmp_path with the same relative
paths, apply one mutation, run check(tmp_root) and assert a finding naming the rule:
test_repository_catalog_loadsandtest_repository_blocks_are_current(real repository, no findings).test_render_reproduces_pre_migration_tables: import the fixture withtable_import, render every block and compare each with the corresponding table of the fixture, byte for byte. The test depends only on the frozen fixture, so it stays valid when the catalogue grows later.test_generate_is_idempotent:generateon a copy twice; the second run rewrites nothing; on the repositorygeneraterewrites nothing.test_duplicate_action_id_fails,test_unknown_rate_limit_class_fails,test_unused_rate_limit_class_fails,test_none_class_only_for_system_or_delivery_check.test_event_without_receive_action_fails,test_event_receive_system_action_fails,test_self_receive_requires_self_audience,test_receive_text_must_name_receive_action.test_op_without_action_fails,test_op_with_system_action_fails,test_row_group_must_be_consecutive.test_unknown_key_fails,test_wrong_version_fails.test_app_scope_mismatch_with_scope_table_fails(change oneapp_scope), andtest_scope_table_names_unknown_action_fails.test_missing_ratelimit_config_row_fails.test_effect_mismatch_with_effect_list_fails(flip oneeffect) andtest_durable_event_without_resource_version_flag_fails.test_edited_chapter_block_fails(change one cell inactions.identity),test_edited_redocly_list_fails,test_unknown_block_name_fails,test_unbalanced_markers_fail.test_redocly_lists_exclude_system_actions.test_cli_exit_codes(check0 and 1,generateon an invalid catalogue exits 1 and writes nothing).
Commands that must pass:
uv run --project tools --locked pytest tools/catalog/tests
make catalog && git diff --exit-code # regeneration gives no diff
make catalog-check
make api-lint
make check
Acceptance criteria¶
-
docs/spec/actions.yamlexists and is the only source of actions, classes, event types and ops; 05 tables and redocly lists sit in generated blocks. -
pytest tools/catalog/tests: duplicate action id, unknown rate-limit class, event type without receive action, op without action and an edited table block fail. - A test proves that the 05 tables before and after the change have the same rows
(
test_render_reproduces_pre_migration_tables), and the report shows the 05 diff contains only markers and the edited sentences of rules 3 and 4. -
make catalog-checkandmake api-lintare green;make cataloggives no diff. - ADR 0049 matches the implementation (changed only where the implementation differs).
- Docs updated: 02 row,
docs/api/README.md,docs/quality/gates.md, chapter 23,tools/README.md,tools/catalog/README.md,CHANGELOG.md,docs/DECISIONS.md. -
make checkgreen; no file of the diff contains U+2013 or U+2014.
Definition of done¶
Code, tests and docs as above: docstrings on every public symbol of action_catalog.py; README of
the tool; ADR 0049 current; CHANGELOG entry under [Unreleased]; make check green. Then make review
and an independent code review over the diff, then the red team gate
(docs/quality/red-team-gate.md) over the diff with verdict recorded. Fix every finding with a
test and re-check the delta. Merge locally into the base branch only if the review is clean, the
red team says JA and make check is green; then git worktree remove.
Report¶
Write to {{REPORT_PATH}}: first line Verdict: DONE or Verdict: BLOCKED <reason>; then the
commits, the files with line counts, the test results with counts (including the red runs), the
05 diff summary, what is not proven (for example: consumers M1-03, M1-06 and M1-25 do not exist
yet), and follow-ups that could not be done in scope, each with its reason. Then write
{{DONE_PATH}} with exit=0 sha=<merged sha> or exit=1 <reason>.