Skip to content

feat: workflow audit records at the framework level - #3

Merged
Nshai merged 3 commits into
masterfrom
feat/framework-audit
Aug 11, 2026
Merged

feat: workflow audit records at the framework level#3
Nshai merged 3 commits into
masterfrom
feat/framework-audit

Conversation

@Nshai

@Nshai Nshai commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Adds the framework-level audit hook, the API that reads a record back, and a worked example in the host. Two commits, reviewable in order.

Why

Events answer what did the runtime do. They do not answer why is this result defensible — the plan a node formed, the input it worked from, the output it produced. Those are workflow-specific questions, so this adds the hook and the storage without the framework ever owning a schema.

What changed

1. 9d439d0 — the framework hook

  • A definition opts in by implementing IAuditedWorkflowDefinition, declaring a root kind plus its sections. WorkflowRunner reads that at build time and hands every node an IWorkflowAuditRecorder bound to it, reachable via HostExecutorRuntime.Audit or WorkflowBuildContext.Audit. A workflow that declares nothing gets null and pays nothing.
  • Storage stays workflow-agnostic: IAuditRecordStore holds a root row plus string-typed entries with opaque JSON payloads, so a new workflow needs no schema change.
  • GET /workflows/{name}/instances/{id}/state returns lifecycle status plus the record, projected back through the declared sections. ?section= narrows it. The route is scoped by workflow name because the response shape comes from that workflow's declaration — a mismatched name is a wrong URL, so 404.
  • InMemoryAuditRecordStore is the framework default; Abacus.Run.Service displaces it with an EF Core SQLite store under Abacus:AuditRecords.

2. 454b4aa — the worked example

Workflow example-order in src/Abacus.Run.Service/Workflows/ExampleOrder. The work is deliberately dull — plan, price, total — because the point is the auditing around it. Two details are why it exists rather than something shorter:

  • Line entries are keyed by SKU, so a retried attempt corrects the record instead of appending a contradictory second line.
  • The failure path records the outcome before throwing. A recorder failure is swallowed by design, so an explanation filed after the throw is an explanation lost.

Design notes worth a reviewer's attention

  • Recording is best-effort by contract. A store failure is logged and swallowed, never rethrown. Failing work because its explanation could not be filed trades a correct result for a missing one. The consequence is that storage problems surface in the host log, not the response — called out in the troubleshooting docs.
  • The host may now define workflows. ArchitectureBoundaryTests previously forbade any workflow definition in the host assembly. It now carves out Abacus.Run.Service.Workflows.<Name>; extension points outside that namespace still fail the test.
  • Test isolation fix. Because the host substitutes a SQLite store, every fixture had been writing into the deployed database file and inheriting records across runs — 602 KB had accumulated in the integration bin. HostFixture and ChaosFixture now take a temp file each and delete it on dispose, and runtime .db files are gitignored.

Testing

534 unit / 97 integration / 7 chaos, 0 failures.

New coverage: 9 unit tests for the recorder's guarantees (undeclared sections dropped, re-record replaces, sequence monotonic, store failure swallowed, close-before-open writes nothing), 5 integration tests for the state route against a fixture-local workflow, and 7 for the example workflow end to end.

Also verified against a running host, not only the suite — both documented curl invocations return the records shown in the docs, including the DeadStopped failure path with its pre-failure line still recorded.

Docs

README.md gains an "Audit records" section with a runnable example; docs/wiki.md gains "Workflow audit records" with the declaration walkthrough, the recorder's guarantees, the example, the API route, the config section, the custom-store extension point, and a troubleshooting entry.

Scope

Framework audit only. The QA workflow realization, RAG/ingestion, and Abacus.Data.Service are deliberately excluded, as is the tenant gate-configuration work on feat/tenant-executor-gate-configuration — this branch does not depend on it.

@Nshai
Nshai force-pushed the feat/framework-audit branch from 454b4aa to fea9680 Compare August 11, 2026 00:02
A workflow definition can now declare the shape of its own audit record via
IAuditedWorkflowDefinition, and the runtime hands every node an
IWorkflowAuditRecorder bound to that declaration. Storage stays
workflow-agnostic — a root plus string-typed entries with JSON payloads — so a
new workflow needs no schema change. Recording is best-effort by contract: an
audit write must never fail the work it describes, so store failures are logged
and swallowed.

GET /workflows/{name}/instances/{id}/state returns an instance's lifecycle
status alongside whatever record its workflow declared, projected back through
the declared sections so a run in progress shows what is still outstanding as
readily as what is done. ?section= narrows the response.

The framework default is InMemoryAuditRecordStore. Abacus.Run.Service displaces
it with an EF Core SQLite store under Abacus:AuditRecords, whose connection
string is resolved from IOptions inside the context factory rather than read at
wire time, so a test host's configuration override actually applies.

README.md and docs/wiki.md document the declaration, the recorder's guarantees,
the state route, the configuration section, and the custom-store extension point.
The audit hook had no worked example outside the test fixtures, so the answer to
"what does a definition actually have to do" lived only in prose. Workflow
example-order is that answer: it declares four sections, opens the record with
attributes, files a plan, records one entry per line, and settles both terminal
paths. The work it does is deliberately dull — plan, price, total — because the
point is the auditing around it, not the domain.

Two details are the reason it exists rather than a shorter sample. Line entries
are keyed by SKU, so a retried attempt corrects the record instead of appending
a contradictory second line. The failure path records the outcome before letting
the exception propagate, because a recorder failure is swallowed by design and
an explanation filed after the throw is an explanation lost.

The host may now define workflows. ArchitectureBoundaryTests carves out the
Abacus.Run.Service.Workflows.<Name> namespace: extension points outside it still
mean the shell has grown behaviour of its own, but a workflow that ships inside
the host assembly by convention is a documented consumer of the framework.

Test isolation. The host substitutes a SQLite audit store, so every fixture was
writing into the deployed database file and inheriting records from previous
runs — 600 KB of them had accumulated in the integration bin. Both fixtures now
take a temp file each and delete it on dispose, and runtime database files are
ignored rather than left to be committed by accident.

Verified against a running host, not only the suite: both documented curl
invocations return the records shown in the docs.
@Nshai
Nshai force-pushed the feat/framework-audit branch from fea9680 to 03b1726 Compare August 11, 2026 00:05
Commits here are authored by the repository owner. The tooling used to produce
a change is not part of the change's history, so no co-author trailer, no
generated-with footer on pull requests, and no tool attribution in code or docs.
@Nshai
Nshai merged commit a6562fe into master Aug 11, 2026
2 checks passed
@Nshai
Nshai deleted the feat/framework-audit branch August 11, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants