Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-11
123 changes: 123 additions & 0 deletions openspec/changes/migrate-parafering-to-or-audit/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Design: migrate-parafering-to-or-audit

## Context

The procest parafering audit trail currently flows through a parallel pipeline:

```
ParafeerTransitionEvent
→ ParaferingAuditListener
→ ObjectService::saveObject(register, paraferingAuditEntry_schema, entry)
→ OR stores the record as a regular OR object (not an audit-trail entry)
```

The `ParaferingAuditAppendOnlyValidator` then blocks UPDATE/DELETE on those objects via
OR's object lifecycle events to simulate immutability.

After migration, the flow MUST be:

```
ParafeerTransitionEvent
→ ParaferingAuditListener (rewritten)
→ AuditTrailMapper::createAuditTrailEntry(ObjectEntity $object, string $action, array $context)
→ OR audit trail (hash-chained, natively immutable)
```

## File-by-File Migration Plan

### lib/Listener/ParaferingAuditListener.php — REWRITE

**Current**: injects `ObjectService` and `IAppConfig`; reads `paraferingAuditEntry_register`
and `paraferingAuditEntry_schema` from config; calls `objectService->saveObject()`.

**After**: inject `OCA\OpenRegister\Db\AuditTrailMapper`. On each `ParafeerTransitionEvent`:

1. Determine the transition name from the event (e.g. `approved`, `returned`, `skipped`).
2. Build the action type: `procest.parafering.{transitionName}`.
3. Build the `$context` array (persisted in the `changed` JSON column):
```php
$context = [
'parafeerrouteId' => $event->getParafeerrouteId(),
'paraffeerstapId' => $event->getParaffeerstapId(),
'fromState' => $event->getFromState(),
'toState' => $event->getToState(),
'actorUuid' => $event->getActorUuid(),
'comment' => $event->getComment() ?? null,
];
```
4. Call `AuditTrailMapper::createAuditTrailEntry($object, $actionType, $context)` where
`$object` is the `ObjectEntity` for the parafeerroute.

App-specific context is carried in the `$context` array argument to
`AuditTrailMapper::createAuditTrailEntry()`, which is persisted in the existing `changed`
JSON column on the `openregister_audit_trails` table. No OR schema change is required.

### lib/Validator/ParaferingAuditAppendOnlyValidator.php — REMOVE

This file is deleted entirely. OR's audit trail is append-only by construction (HTTP 405
on PUT/DELETE on `/api/audit-trails/{id}`). No replacement is needed.

### lib/AppInfo/Application.php lines 127–147 — UPDATE

Remove the registrations for `ObjectCreatingEvent`, `ObjectUpdatingEvent`, and
`ObjectDeletingEvent` that target `ParaferingAuditAppendOnlyValidator`. The
`ParaferingAuditListener` registration (for `ParafeerTransitionEvent`) is retained and
updated to use the new listener implementation.

### lib/Settings/procest_register.json — MARK DEPRECATED

The `paraferingAuditEntry` schema entry remains in the JSON to keep existing rows readable.
Add a `deprecated: true` flag and a `deprecationNote` string:

```json
"paraferingAuditEntry": {
"deprecated": true,
"deprecationNote": "Superseded by OR audit trail via migrate-parafering-to-or-audit. Sunset: one major release after spec acceptance. No new writes after migration.",
...existing schema fields...
}
```

Do not remove the schema object — existing rows must remain readable until sunset.

### openspec/specs/parafering-audit-trail/spec.md — UPDATE (apply phase)

During the apply phase (not now), update the existing `parafering-audit-trail` spec to
reference the new consumer contract: "audit trail is accessible via
`GET /api/audit-trails?objectUuid={parafeerrouteId}`". The existing spec's requirements for
immutability, delegation display, and export remain valid but the discovery mechanism changes.

## Backwards Compatibility

- Existing `paraferingAuditEntry` rows in OR remain queryable via the deprecated schema
endpoint for one major release (sunset date in proposal.md).
- New parafering transition events ONLY emit via OR audit trail after this spec ships.
- The consumer contract (callers asking "what happened to parafeerroute X") is preserved
through the new OR audit-trail discovery endpoint.

## Event Type Naming Convention

Transition names follow the parafeerroute state machine. The listener maps transition names
from `ParafeerTransitionEvent` to action type strings:

| Transition | Action type |
|---|---|
| approved | `procest.parafering.approved` |
| returned | `procest.parafering.returned` |
| skipped | `procest.parafering.skipped` |
| delegated | `procest.parafering.delegated` |
| advised | `procest.parafering.advised` |
| accorded | `procest.parafering.accorded` |

If the event carries a transition name not in this table, the listener MUST use the raw name
as `procest.parafering.{rawName}` (no exception, no fallback to `unknown`).

## Seed Data

No new schemas are added. The `paraferingAuditEntry` schema is deprecated in-place. No
new registers or register definitions are created by this migration.

## Related ADRs

- **ADR-022** (primary) — mandate for this migration.
- **ADR-008** — testing contract; hash-chain verification test required.
- **ADR-001** — data layer; no new entities or mappers introduced.
101 changes: 101 additions & 0 deletions openspec/changes/migrate-parafering-to-or-audit/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Proposal: migrate-parafering-to-or-audit

## Why

ADR-022 (Apps Consume OpenRegister Abstractions) explicitly prohibits "home-grown audit
trails — an app writing to a private events table instead of OR's audit trail for actions on
OR-owned objects."

Procest currently violates this rule:

- `lib/Listener/ParaferingAuditListener.php` listens to `ParafeerTransitionEvent` and writes
records to a `paraferingAuditEntry` schema in OR, creating a parallel audit store instead
of using OR's built-in audit trail.
- `lib/Validator/ParaferingAuditAppendOnlyValidator.php` registers on
`ObjectCreatingEvent` / `ObjectUpdatingEvent` / `ObjectDeletingEvent` for
`paraferingAuditEntry` objects, re-implementing immutability guards that OR's hash-chained
audit trail already provides natively.
- `lib/AppInfo/Application.php` wires both listeners (lines 127–147).
- `lib/Settings/procest_register.json` defines the `paraferingAuditEntry` schema.

The umbrella spec `consume-or-audit-trail-fleet-wide` (hydra) mandates per-app migration specs
for each violating app within 90 days of umbrella acceptance.

## What

Replace the parallel audit pipeline with direct OR audit-trail emissions:

1. Rewrite `ParaferingAuditListener` to inject `OCA\OpenRegister\Db\AuditTrailMapper`
and emit OR audit events with namespaced action type
`procest.parafering.{transitionName}` and domain context in the `$context` payload
(persisted in the `changed` JSON column).
2. Remove `ParaferingAuditAppendOnlyValidator` (OR audit trail is append-only by construction).
3. Remove the validator's event listener registrations from `Application.php`.
4. Mark `paraferingAuditEntry` schema deprecated in `procest_register.json` with a sunset
date (one major procest release after this spec ships).
5. Preserve the consumer contract: callers asking "what happened to parafeerroute X" get the
same event history via OR's audit-trail API.

## Capabilities

### New Capabilities

- `parafering-audit-via-or`: Parafering transition events are discoverable via
`GET /api/audit-trails?objectUuid={parafeerrouteId}` with action types matching
`procest.parafering.*`.

### Modified Capabilities

- `parafering-audit-trail` (existing spec) — consumer contract updated to reference OR
audit-trail API as the discovery endpoint. The spec body update happens during apply phase.

### Removed Capabilities

- In-app `paraferingAuditEntry` write path — removed. Existing records remain readable via
deprecated endpoint until sunset date. No new records are written after this spec ships.

## Affected Projects

- [x] Project: `procest` — all implementation work is in this repo
- Reference: `hydra/openspec/changes/consume-or-audit-trail-fleet-wide/` (umbrella policy)
- Reference: `openregister/openspec/specs/audit-trail-immutable/spec.md` (OR contract)

## Scope

### In Scope

- Rewriting `ParaferingAuditListener` to emit via OR audit trail
- Removing `ParaferingAuditAppendOnlyValidator`
- Removing listener registrations from `Application.php`
- Marking `paraferingAuditEntry` deprecated in the register JSON
- Tests verifying discoverability via OR audit trail API

### Out of Scope

- The umbrella policy itself (separate spec)
- Modifying OR's audit-trail API (already shipped; consumed, not changed)
- Backfilling historical `paraferingAuditEntry` rows into OR audit trail (out of scope —
see rationale below)
- Changing the parafering domain logic or transition rules

## Sunset Date

The `paraferingAuditEntry` schema deprecation sunset date is one major procest release
after this spec is accepted. Existing rows remain queryable (read-only) until that date.

## Historical Backfill: Out of Scope

Per ADR-022 + Archiefwet retention, historical rows remain in the deprecated
`paraferingAuditEntry` store in read-only mode; new events emit via OR. Backfilling into
OR's hash chain would risk integrity since chronological ordering of legacy rows is not
guaranteed, and determining the correct `objectUuid` mapping for pre-existing rows is
error-prone. Historical records remain queryable via the deprecated schema endpoint until
the sunset date.

## Success Criteria

- `openspec validate --strict migrate-parafering-to-or-audit` exits 0.
- `ParaferingAuditAppendOnlyValidator.php` is removed.
- `ParaferingAuditListener.php` emits via OR audit trail (no direct `paraferingAuditEntry` writes).
- `GET /api/audit-trails?objectUuid={parafeerrouteId}` returns parafering transition events.
- `composer check:strict` passes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# parafering-audit-via-or Specification

---
status: proposed
---

## Purpose

Replace the parallel `paraferingAuditEntry` write path with OR audit-trail emissions.
Every parafering transition event on an OR-owned `parafeerroute` object MUST be discoverable
via OR's audit-trail-immutable API. References `consume-or-audit-trail-fleet-wide` umbrella
and ADR-022.

## ADDED Requirements

### Requirement: Parafeer Transition Emits OR Audit Event

Every `ParafeerTransitionEvent` SHALL trigger an OR audit trail entry with action type
`procest.parafering.{transitionName}` (e.g. `procest.parafering.approved`,
`procest.parafering.returned`, `procest.parafering.skipped`).

#### Scenario: Approved transition creates OR audit entry

- GIVEN a parafeerroute with UUID `route-001` stored in OR
- WHEN a ParafeerTransitionEvent fires with transition `approved` on `route-001`
- THEN an OR audit entry SHALL be created with `objectUuid = route-001`
- AND the entry's `action` field SHALL equal `procest.parafering.approved`
- AND the entry SHALL be retrievable via `GET /api/audit-trails?objectUuid=route-001`

#### Scenario: Returned transition creates OR audit entry

- GIVEN a parafeerroute with UUID `route-002` stored in OR
- WHEN a ParafeerTransitionEvent fires with transition `returned` on `route-002`
- THEN an OR audit entry SHALL be created with action `procest.parafering.returned`
- AND the hash chain SHALL remain intact across all entries for `route-002`

---

### Requirement: Audit Event Carries Parafeer Context

The OR audit event `$context` payload (stored in the `changed` JSON column) MUST include
the following fields for every parafering transition: `parafeerrouteId`, `paraffeerstapId`
(if applicable), `fromState`, `toState`, `actorUuid`, `comment` (if present on the event).

#### Scenario: Changed column contains route and actor context

- GIVEN an OR audit entry for `procest.parafering.approved` on `route-001`
- WHEN the entry is retrieved via the audit trail API
- THEN the `changed` field MUST contain `parafeerrouteId` equal to `route-001`
- AND the `changed` field MUST contain `fromState` and `toState` strings
- AND the `changed` field MUST contain `actorUuid` identifying the approving user

#### Scenario: Comment field carried in context when present

- GIVEN a transition event with a non-empty `comment` field
- WHEN the OR audit entry is created
- THEN the `changed` field's `comment` key MUST equal the comment string from the transition event

---

### Requirement: No Direct Writes To paraferingAuditEntry Schema

Application code MUST NOT write new `paraferingAuditEntry` objects after this spec ships.
The `ParaferingAuditListener` MUST route all new events through OR's audit trail instead.

#### Scenario: No new paraferingAuditEntry objects created after migration

- GIVEN the migration is applied
- WHEN a ParafeerTransitionEvent fires
- THEN the count of `paraferingAuditEntry` objects in OR SHALL NOT increase
- AND a new OR audit trail entry SHALL exist for the transition

#### Scenario: Existing paraferingAuditEntry objects remain

- GIVEN `paraferingAuditEntry` objects exist from before the migration
- WHEN an administrator queries `paraferingAuditEntry` objects via the OR API
- THEN those objects SHALL remain readable (the schema is deprecated, not deleted)

---

### Requirement: Existing paraferingAuditEntry Records Remain Readable

Until the sunset date documented in `proposal.md`, existing `paraferingAuditEntry` rows MUST
remain queryable via the deprecated schema endpoint. The sunset date is defined as one major
procest release after this spec's acceptance.

#### Scenario: Historical audit records readable after migration

- GIVEN `paraferingAuditEntry` objects exist from before the migration was applied
- WHEN an administrator queries `GET /api/registers/{register}/schemas/paraferingAuditEntry/objects`
- THEN the response SHALL return the existing historical records
- AND the response SHALL NOT include a 404 or schema-not-found error

---

### Requirement: Test Audit Discoverable Via OR

Given a parafeerroute UUID, querying OR's audit-trail-immutable API SHALL return all
parafering transitions in chronological order, including hash-chain integrity.

#### Scenario: Full parafering history discoverable via OR

- GIVEN a parafeerroute `route-003` that has gone through three transitions: submitted → under_review → approved
- WHEN `GET /api/audit-trails?objectUuid=route-003` is called
- THEN the response SHALL include three entries in chronological order
- AND each entry SHALL have an `action` field matching `procest.parafering.*`
- AND `GET /api/audit-trails/verify` SHALL return a passing integrity check for the chain

#### Scenario: Cross-actor delegation audit is preserved

- GIVEN a transition delegated by user A on behalf of user B
- WHEN the OR audit entry is created
- THEN the `changed` field MUST contain both the delegate actor UUID and the principal UUID (onBehalfOf)

---

### Requirement: Append-Only Validator Removed

The `ParaferingAuditAppendOnlyValidator` class MUST be removed from the codebase. OR's
audit trail provides immutability natively via HTTP 405 on PUT/DELETE. No replacement
validator is needed.

#### Scenario: Validator file absent after migration

- GIVEN the migration is applied
- THEN `lib/Validator/ParaferingAuditAppendOnlyValidator.php` SHALL NOT exist in the repo
- AND no `ObjectCreatingEvent` / `ObjectUpdatingEvent` / `ObjectDeletingEvent` registrations
for `paraferingAuditEntry` objects SHALL remain in `Application.php`

#### Scenario: OR enforces immutability natively

- GIVEN an OR audit trail entry for a parafering transition
- WHEN an API call attempts `PUT /api/audit-trails/{id}` with modified data
- THEN OR SHALL return HTTP 405 Method Not Allowed
- AND no procest-specific validator SHALL be required to enforce this
Loading
Loading