PR #297 · pramaan-io/pramaan-functions · merged 2026-06-10
Eighty-five writers, twenty-five files, zero shared vocabulary.
When the audit trail is the only witness, the words must agree.
Constraining the schema solves today. Tomorrow's vocabulary still costs a migration.
The fix — One word book, one filing desk — extending vocabulary is a dict edit, never a migration.
sequenceDiagram autonumber participant caller as Caller participant registry as Registry (word book) participant emitter as Emitter (filing desk) participant db as Audit log participant ci as CI gate caller->>registry: resolve(action) registry->>registry: returns ActionSpec — verb, category, area, snapshot_kind, excluded_fie caller->>emitter: emit(action, subject, scope, snapshot, request) emitter->>emitter: validates snapshot, truncates values, builds row emitter->>db: single row written ci->>ci: scans for direct AuditService.log() outside the audit module build fa
| Method | in → out | what it does |
|---|---|---|
resolve | (action) → ActionSpec | looks up an action key in the word book and returns its full stamped attributes |
emit | (action, subject, scope, snapshot, secondary_subjects, request) → AuditLog | the single filing desk that validates the snapshot and writes one audit row |
emit_legacy | async def emit_legacy( session: AsyncSession, *, action: str, resource_kind: str, resource_id: UUID | None = None, grant_id: UUID | None = None, event_metadata: dict[str, Any] | None = None, firm_id_override: UUID | None = None, ip_address: | adapter for the eighty-five legacy call sites, rows stay byte-for-byte unchanged |
DiffSnapshot | (class) | typed snapshot form with 512-char value caps and a truncated flag |
Data-only registry of verbs, subjects, categories, areas, and snapshot kinds — extending vocabulary is a dict edit, not a migration.
| new class | signature |
|---|---|
VerbSpec | (attributes, category, snapshot_kind) |
ActionSpec | (action, subject_kind, verb, category, area, snapshot_kind, scope_field, excluded_fields) |
resolve | (action) → ActionSpec |
CATEGORIES : Final[frozenset[str]]AREAS : Final[frozenset[str]]SNAPSHOT_KINDS : Final[frozenset[str]]label : strdefault_area : strscope_field : str | Nonesrc/shared/audit/emitter.py+144 −63Provides the single typed entry point plus a shim the eighty-five legacy call sites use, keeping written rows byte-for-byte unchanged.
| new method | signature |
|---|---|
emit | (action, subject, scope, snapshot, secondary_subjects, request) → AuditLog |
emit_legacy | async def emit_legacy( session: AsyncSession, *, action: str, resource_kind: str, resource_id: UUID | None = None, grant_id: UUID | None = None, event_metadata: dict[str, Any] | None = None, firm_id_override: UUID | None = None, ip_address: |
Defines the Pydantic typed snapshot union with 512-char value caps, fifty fields max, and a truncated flag for call-site validation.
| new class | signature |
|---|---|
_truncate | (value) → tuple[str | None, bool] |
_SnapshotBase | (class) |
DiffField | (class) |
_cap | () → FactField |
FactField | (class) |
DiffSnapshot | (class) |
SummarySnapshot | (class) |
TombstoneSnapshot | (class) |
FileSnapshot | (class) |
RunSnapshot | (class) |
SecondarySubject | (class) |
VALUE_CAP : intLABEL_CAP : intMAX_FIELDS : intMAX_SECONDARY_SUBJECTS : intv : intfield : strlabel : strold : str | Nonenew : str | Nonetruncated : boolvalue : str | Nonefilename : strsize_bytes : int | Nonecontent_type : str | Noneduration_ms : int | Nonekind : strid : UUID | Nonerole : strDemoted the writer to a private path and added new audit columns (category, area, scope ids) for forward-compatible agent identity.
subject_label : str | Nonecategory : str | Nonearea : str | Nonematter_id : UUID | Noneproceeding_id : UUID | Nonefiling_id : UUID | Nonedocument_id : UUID | NoneTyped Subject, Scope, and RequestContext models so callers can pass structured request data into the new emit entry point.
| new class | signature |
|---|---|
Subject | (class) |
Scope | (class) |
RequestContext | (class) |
kind : strid : UUID | Nonelabel : strmatter_id : UUID | Noneproceeding_id : UUID | Nonefiling_id : UUID | Nonedocument_id : UUID | Noneip_address : str | Noneuser_agent : str | None