generated from coulomb/repo-seed
ADR-001: activity-core as org-wide Event Bridge — boundaries, NATS as org infrastructure, state hub delegation, rules-core module-first, issue-core adapter interface, capabilities domain assignment. ADR-002: markdown-as-definition format for event types and ActivityDefinitions — co-located intent/schema/logic/debugging, publisher-declared governance with environment-configurable curator gate, attribute type system, task template files. ADR-003: Rule vs. Instruction model and expression DSL — sandboxed Python-like AST evaluator for Rules, trusted-fields prompt injection protection for Instructions, output schema enforcement, audit trail, testing strategy, rules-core module boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
357 lines
12 KiB
Markdown
357 lines
12 KiB
Markdown
---
|
||
id: ACT-ADR-002
|
||
type: architecture-decision-record
|
||
title: "Markdown-as-Definition Format for Event Types and ActivityDefinitions"
|
||
status: accepted
|
||
decided_by: Bernd Worsch
|
||
date: "2026-05-14"
|
||
scope: cross-repo
|
||
affects:
|
||
- activity-core
|
||
- any event publisher registering event types
|
||
tags: ["architecture", "format", "event-type", "activity-definition", "markdown", "documentation"]
|
||
---
|
||
|
||
# ACT-ADR-002: Markdown-as-Definition Format
|
||
|
||
## Status
|
||
|
||
Accepted.
|
||
|
||
## Context
|
||
|
||
Event type schemas and ActivityDefinition rules need to be understood and authored
|
||
by three distinct audiences simultaneously: humans reviewing and debugging automation,
|
||
agents creating and modifying definitions at runtime, and machines parsing and
|
||
evaluating them. Traditional approaches split these concerns — schemas go in JSON
|
||
Schema or YAML, documentation goes in a wiki, logic goes in code — and they drift
|
||
apart. A bug in a rule requires cross-referencing three places to understand intent,
|
||
check the schema, and read the condition.
|
||
|
||
The Custodian ecosystem already uses markdown files with YAML frontmatter as the
|
||
authoritative format for workplans, ADRs, SCOPE.md, and INTENT.md — all understood
|
||
by humans and agents without additional tooling. The same pattern should apply here.
|
||
|
||
## Decision
|
||
|
||
**Event type definitions and ActivityDefinitions are markdown files** where machine-
|
||
parseable structure (frontmatter YAML and fenced definition blocks) is embedded within
|
||
human-readable narrative. Intent, schema, logic, and debugging notes live in one file.
|
||
|
||
### Event Type Definition Files
|
||
|
||
**Location**: `event-types/{namespace}.{event-name}.md` within the activity-core repo
|
||
(or a registered event-types registry repo if volumes justify separation).
|
||
|
||
**Naming convention**: `{publisher-domain}.{noun}.{verb}.md`, e.g.:
|
||
- `org.repo.registered.md`
|
||
- `org.security.cve.published.md`
|
||
- `org.workstream.completed.md`
|
||
|
||
**Structure**:
|
||
|
||
```markdown
|
||
---
|
||
id: org.repo.registered
|
||
type: event-type
|
||
version: "1.0"
|
||
publisher: the-custodian/state-hub
|
||
governance: publisher-declared # publisher-declared | curated
|
||
status: active # active | deprecated | draft
|
||
introduced: "2026-05-14"
|
||
---
|
||
|
||
# Event: org.repo.registered
|
||
|
||
## Intent
|
||
|
||
One-paragraph statement of why this event exists and what it signals.
|
||
Written for an agent or human who has never seen it before.
|
||
|
||
## When Published
|
||
|
||
Bulleted list of the exact conditions under which the publisher fires this event.
|
||
Be precise — ambiguity here causes missed or duplicate activations.
|
||
|
||
## Attributes
|
||
|
||
| Attribute | Type | Required | Description |
|
||
|---|---|---|---|
|
||
| `repo_slug` | string | yes | URL-safe repository identifier |
|
||
| `domain` | string | yes | Domain slug the repo is assigned to |
|
||
| `tags` | string[] | no | Capability tags set at registration time |
|
||
| `registered_at` | datetime | yes | ISO 8601 UTC timestamp |
|
||
|
||
## Example Payload
|
||
|
||
```json
|
||
{
|
||
"id": "evt-7f3a1b2c",
|
||
"type": "org.repo.registered",
|
||
"version": "1.0",
|
||
"timestamp": "2026-05-14T10:00:00Z",
|
||
"publisher": "the-custodian/state-hub",
|
||
"attributes": {
|
||
"repo_slug": "new-python-service",
|
||
"domain": "railiance",
|
||
"tags": ["python-service", "fastapi"],
|
||
"registered_at": "2026-05-14T10:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
## Consumer Notes
|
||
|
||
Guidance for agents and humans writing rules against this event type:
|
||
- Which attributes are safe for instruction prompts (trusted fields)
|
||
- Common misuses or gotchas
|
||
- Related events that are often used together
|
||
|
||
## Debugging
|
||
|
||
What to check when an activity that subscribes to this event does not fire:
|
||
- How to verify the event was published (NATS subject, log entry)
|
||
- How to inspect the event payload in the registry
|
||
- Common schema validation failures
|
||
```
|
||
|
||
### Attribute Types
|
||
|
||
The type system for event attributes is intentionally small:
|
||
|
||
| Type | Notes |
|
||
|---|---|
|
||
| `string` | UTF-8 string |
|
||
| `integer` | 64-bit signed integer |
|
||
| `float` | 64-bit float |
|
||
| `boolean` | true / false |
|
||
| `datetime` | ISO 8601 UTC string in payload, parsed to datetime in evaluator |
|
||
| `uuid` | String in payload, validated as UUID v4 |
|
||
| `string[]` | JSON array of strings |
|
||
| `integer[]` | JSON array of integers |
|
||
| `object` | Freeform JSON object — cannot be used in rule conditions; instruction-only |
|
||
|
||
`object` type attributes are available to instructions but excluded from rule
|
||
conditions deliberately — rules must be deterministic and schema-validatable.
|
||
|
||
### ActivityDefinition Files
|
||
|
||
**Location**: `activity-definitions/{slug}.md` within the repo that owns the automation.
|
||
For org-wide automations: `activity-core/activity-definitions/`.
|
||
For domain-specific automations: `{domain-repo}/activity-definitions/`.
|
||
|
||
**Structure**:
|
||
|
||
```markdown
|
||
---
|
||
id: ACT-DEF-onboard-python-repo
|
||
type: activity-definition
|
||
version: "1.0"
|
||
status: active
|
||
trigger:
|
||
type: event # event | cron | scheduled
|
||
event_type: org.repo.registered # for type: event
|
||
# cron: "0 9 * * 1" # for type: cron (5-field, UTC)
|
||
# timezone: "Europe/Berlin" # optional, cron only
|
||
# misfire_policy: skip # skip | catchup | compress (cron only)
|
||
# at: "2026-06-01T09:00:00Z" # for type: scheduled (one-off)
|
||
context_sources:
|
||
- type: repo-scoping
|
||
query: repo_profile
|
||
bind_to: context.repo_profile
|
||
- type: state-hub
|
||
query: domain_summary
|
||
bind_to: context.domain_summary
|
||
governance: publisher-declared
|
||
owner: custodian-agent
|
||
created: "2026-05-14"
|
||
---
|
||
|
||
# ActivityDefinition: Onboard New Python Service
|
||
|
||
## Purpose
|
||
|
||
One paragraph. What does this automation do and why does it exist? What problem
|
||
would accumulate if this automation were turned off?
|
||
|
||
## Trigger
|
||
|
||
Which event type fires this activity, and under what conditions does it apply?
|
||
Cross-reference the event type definition file.
|
||
|
||
## Context Sources
|
||
|
||
What context is resolved before rules are evaluated? Explain what each source
|
||
provides and why it is needed.
|
||
|
||
## Rules
|
||
|
||
Each rule is a fenced block tagged `rule`. Rules are evaluated in order; all
|
||
matching rules fire (not first-match-only). See ACT-ADR-003 for the expression
|
||
language specification.
|
||
|
||
```rule
|
||
id: create-sbom-scan
|
||
condition: '"python-service" in event.attributes.tags'
|
||
action:
|
||
task_template: tasks/sbom-initial-scan.md
|
||
target_repo: event.attributes.repo_slug
|
||
priority: high
|
||
labels: ["onboarding", "security"]
|
||
```
|
||
|
||
```rule
|
||
id: create-scope-generation
|
||
condition: '"python-service" in event.attributes.tags and context.repo_profile.scope_md_exists == false'
|
||
action:
|
||
task_template: tasks/generate-scope-md.md
|
||
target_repo: event.attributes.repo_slug
|
||
priority: medium
|
||
labels: ["onboarding", "documentation"]
|
||
```
|
||
|
||
## Instructions
|
||
|
||
Instructions are evaluated after all rules. An instruction asks an LLM to decide
|
||
what additional tasks (if any) to create. See ACT-ADR-003 for safety requirements.
|
||
|
||
```instruction
|
||
id: domain-specific-onboarding
|
||
condition: 'event.attributes.domain != "test_domain_v2"'
|
||
trusted_fields:
|
||
- event.attributes.repo_slug
|
||
- event.attributes.domain
|
||
- event.attributes.tags
|
||
model: claude-sonnet-4-6
|
||
review_required: false
|
||
prompt: |
|
||
A new repository has been registered in the Coulomb organization.
|
||
|
||
Repository: {event.attributes.repo_slug}
|
||
Domain: {event.attributes.domain}
|
||
Tags: {event.attributes.tags}
|
||
|
||
Based on the domain's current standards and the repository profile above,
|
||
determine what additional domain-specific onboarding tasks should be created
|
||
beyond the standard SBOM scan and SCOPE.md generation. Return an empty list
|
||
if no additional tasks are warranted.
|
||
output_schema: tasks/task-template-list-schema.json
|
||
```
|
||
|
||
## Task Templates
|
||
|
||
References to task template files used in rule actions. Each template is a
|
||
separate markdown file under `tasks/` that defines the task title, description
|
||
template, default labels, and default assignee logic.
|
||
|
||
- `tasks/sbom-initial-scan.md`
|
||
- `tasks/generate-scope-md.md`
|
||
|
||
## Notes
|
||
|
||
Operational notes, edge cases, and context that does not fit elsewhere.
|
||
|
||
## Debugging
|
||
|
||
Checklist for when this ActivityDefinition fires but produces unexpected output:
|
||
|
||
1. Was the triggering event published with the correct type and attributes?
|
||
2. Do the rule conditions evaluate as expected? (Use `make eval-rule` with a fixture)
|
||
3. Is issue-core reachable and configured for the target domain?
|
||
4. For instructions: check the audit log for the model response and output validation result.
|
||
|
||
## Change History
|
||
|
||
- v1.0 (2026-05-14): Initial definition
|
||
```
|
||
|
||
### Governance model
|
||
|
||
The `governance` field on an event type definition determines how the registry
|
||
runtime handles it:
|
||
|
||
| Value | Behaviour |
|
||
|---|---|
|
||
| `publisher-declared` | Accepted immediately on publish; no review required |
|
||
| `curated` | Held in `pending` state until a curator approves via registry API |
|
||
|
||
The runtime checks the **environment's curator gate configuration** — not just the
|
||
file's governance field. An environment configured with `curator_gate: disabled`
|
||
treats all event types as `publisher-declared` regardless of the field value.
|
||
An environment with `curator_gate: required` treats all event types as `curated`
|
||
regardless of the field value. The field is the publisher's declared preference;
|
||
the environment config is the enforcement point.
|
||
|
||
This means:
|
||
- **Dev / integration**: `curator_gate: disabled` — developers and agents iterate
|
||
freely; new event types take effect immediately.
|
||
- **Staging / production**: `curator_gate: required` — all new event types queue
|
||
for curator review before the runtime accepts events of that type.
|
||
|
||
### File as source of truth
|
||
|
||
Following CUST-ADR-001 (Workplans as Repository Artefacts), definition files are
|
||
the canonical source of truth. The activity-core runtime indexes them into its
|
||
database on startup and via a sync command. The database is a queryable cache,
|
||
not the origin. A definition deleted from the filesystem is disabled at next sync.
|
||
|
||
### Task Templates
|
||
|
||
Task templates are separate markdown files (`tasks/{slug}.md`) referenced from
|
||
ActivityDefinition action blocks. They define:
|
||
|
||
```markdown
|
||
---
|
||
id: tasks/sbom-initial-scan
|
||
type: task-template
|
||
---
|
||
# Task: Run Initial SBOM Scan
|
||
|
||
## Title template
|
||
`Run SBOM scan — {target_repo}`
|
||
|
||
## Description template
|
||
Initial SBOM scan required for newly registered repository `{target_repo}`.
|
||
Run: `make ingest-sbom REPO={target_repo} SCAN=1`
|
||
|
||
## Default labels
|
||
["sbom", "security", "automated"]
|
||
|
||
## Default assignee
|
||
None (unassigned)
|
||
```
|
||
|
||
This keeps task content editable separately from the routing logic in
|
||
ActivityDefinitions.
|
||
|
||
## Consequences
|
||
|
||
- A new `event-types/` directory in activity-core (and eventually a shared registry)
|
||
holds all org event type definitions.
|
||
- A new `activity-definitions/` directory in activity-core holds org-wide automations.
|
||
- Domain repos may hold their own `activity-definitions/` for domain-specific
|
||
automations, scanned by activity-core at sync time.
|
||
- The runtime requires a parser for the `rule` and `instruction` fenced blocks.
|
||
- SCOPE.md for activity-core must be updated to list these directories.
|
||
|
||
## Alternatives Considered
|
||
|
||
**Pure JSON Schema for event types, separate wiki for docs**: rejected — documentation
|
||
and schema diverge immediately; agents must cross-reference two systems to author
|
||
a rule correctly.
|
||
|
||
**OpenAPI / AsyncAPI specification**: rejected — those formats are excellent for
|
||
API and broker documentation but not designed for co-locating operational intent
|
||
and debugging guidance. They are also less readable for non-specialists.
|
||
|
||
**Code-only (Python dataclasses for event schemas, Python functions for rules)**:
|
||
rejected — requires code deployment for any definition change; agents cannot modify
|
||
definitions without write access to the codebase; non-technical stakeholders cannot
|
||
review or understand automation policies.
|
||
|
||
## Related
|
||
|
||
- ACT-ADR-001 — Event Bridge Architecture
|
||
- ACT-ADR-003 — Rule vs. Instruction model and DSL
|
||
- CUST-ADR-001 — Workplans as repository artefacts
|