Sprint SPRINT_20260408_004 AUDIT-004 documentation criterion.
docs/modules/timeline/audit-retention.md covers:
- Four-rung classification ladder and the "narrowest wins" rule
- Retention table structure, platform defaults, per-tenant overrides,
and legal holds via compliance_hold
- AuditRetentionPurgeService config + operator recommendations
- Right-to-erasure endpoint contract, hash-chain integrity guarantees,
and the idempotency semantics via pii_redacted_at
- Sequence-chain gap behaviour after purge and how chain verification
should window its checks
- Compliance checklist for operators
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004. AUDIT-004 flipped TODO → DOING with the
first three completion criteria checked. Migration 005, classifier,
retention purge host, and right-to-erasure endpoint all shipped across
commits 44c0e2b34..AUDIT-004 (migration + store + endpoint) and the
purge background host. Docs dossier + Doctor check deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004 AUDIT-004 (retention enforcement).
AuditRetentionPurgeService BackgroundService enumerates tenants that
have any rows in timeline.unified_audit_events, then calls
timeline.purge_expired_audit_events(tenantId, dryRun) per tenant.
The SQL function honours per-classification retention windows and the
compliance_hold flag (legal holds pass through unaffected).
AuditRetentionPurgeOptions bound from the AuditRetentionPurge config
section: Enabled (default true), DryRun (default false), InitialDelay
(default 5 min), Interval (default 6h). Failures on a single tenant
are logged and do not stop the rest of the cycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004 AUDIT-004.
Schema (migration 005_audit_data_classification_retention.sql):
- ALTER timeline.unified_audit_events adds data_classification
(none|personal|sensitive|restricted, default 'none'), compliance_hold
(default false, exempts from purge), pii_redacted_at (null until
right-to-erasure redaction).
- New timeline.audit_retention_policies table holds per-tenant /
per-classification retention windows; seed row tenant_id='*' = platform
default (none/personal=365d, sensitive=730d, restricted=2555d ≈ 7y).
- Function resolve_audit_retention_days falls back tenant→platform→365d.
- Function purge_expired_audit_events iterates classes, honours
compliance_hold, supports dry-run counting.
- Function redact_actor_pii replaces actor_email/actor_ip/actor_user_agent
(+actor_name for personal/sensitive rows) with '[REDACTED]', preserves
actor_id so the content_hash chain stays intact.
Code:
- AuditDataClassifier implements the none/personal/sensitive/restricted
ladder: restricted (signer+attestor key/ceremony ops, cross-module
key_escrow actions) > sensitive (authority auth-protocol events) >
personal (actor email/ip/user_agent present). 16/16 unit tests pass.
- PostgresUnifiedAuditEventStore inserts include data_classification,
defaulting to AuditDataClassifier.Classify() when the payload's
precomputed value is absent. New RedactActorPiiAsync delegates to the
SQL function.
- UnifiedAuditEvent record gets an optional DataClassification property
so services that already classify events can bypass auto-classification.
- DELETE /api/v1/audit/actors/{actorId}/pii endpoint exposes
right-to-erasure, scoped to the new Timeline.Admin policy backed by
the timeline:admin scope (StellaOpsScopes.TimelineAdmin).
Remaining AUDIT-004 work: scheduled background purge host, Doctor
AuditReadinessCheck update to verify retention config, UI badges.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_005 DEPRECATE-001 (JobEngine/ReleaseOrchestrator,
fifth service).
PostgresAuditRepository.AppendAsync now fans out to Timeline via the
optional IAuditEventEmitter after the local transaction commits. The
hash chain (content_hash, previous_entry_hash, sequence_number) stays
in the local audit_entries table as service-level chain-of-custody
evidence; Timeline receives only the summary event for cross-service
correlation, with the content hash surfaced as a detail field.
Same pattern as Authority/Policy/Notify/Scheduler dual-write:
fire-and-forget, optional DI, local write stays authoritative.
Remaining: Attestor dual-write (existing audit is already decorated
with .Audited() on endpoints — verifying the attestor audit log insert
path needs separate review).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_005 DEPRECATE-001 (Scheduler, fourth service).
PostgresSchedulerAuditService.WriteAsync now fans out to Timeline
via the optional IAuditEventEmitter after the local scheduler.audit
row insert. Fire-and-forget, same pattern as Authority/Policy/Notify.
AuditActor is mapped to actor.{id, name, type} with kind -> type,
ActorId -> id, DisplayName -> name. Metadata tuples flatten as
`metadata.{key}` fields in Details. ScheduleId/RunId included.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_005 DEPRECATE-001 (Notify, third service).
Same pattern as Authority + Policy dual-write: NotifyAuditRepository
now fans out to Timeline via the optional IAuditEventEmitter.
Fire-and-forget; local write stays authoritative.
Remaining DEPRECATE-001 services: Scheduler (ISchedulerAuditService),
JobEngine/ReleaseOrchestrator (PostgresAuditRepository.AppendAsync),
Attestor (audit log inserts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_005 DEPRECATE-001 (Policy, second service).
PolicyAuditRepository.CreateAsync now fans out to Timeline's unified
audit store via the optional IAuditEventEmitter (injected via
AddAuditEmission in Policy.Engine / Policy.Gateway Program.cs).
Same pattern as Authority dual-write in commit a947c8df6:
- Optional constructor dependency (default null) for DI compatibility
- Fire-and-forget emission wrapped in try/catch
- MapToTimelinePayload builds a UnifiedAuditEvent-compatible payload
with actor (UserId or "policy-system"), resource (audit.ResourceType,
audit.ResourceId), severity "info", and details including old/new
value strings plus the local audit id.
Remaining DEPRECATE-001 services: Notify, Scheduler, JobEngine,
Attestor dual-write on the same pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_005 DEPRECATE-001 (Authority, first service).
AuthorityAuditSink.WriteAsync now fans out to Timeline's unified audit
store via the optional IAuditEventEmitter (injected via AddAuditEmission
in Program.cs). The local authority.audit table write remains the
authoritative path; the Timeline emission is strictly fire-and-forget:
- Optional constructor dependency (default null) keeps existing tests
that construct the sink without the emitter working unchanged.
- Emission is wrapped in try/catch so any Timeline-side failure (DNS,
timeout, auth) is logged as a warning and never impacts the local
write or calling endpoint.
- MapToTimelinePayload builds a UnifiedAuditEvent-compatible payload
with actor (subject id/name/IP/UA), resource (authority_session
keyed by correlationId), severity derived from outcome, and event
details including client, reason, and event type.
Existing AuthorityAuditSinkTests (2/2) still pass — backward compat
verified via direct xUnit run.
Remaining DEPRECATE-001 work: Policy, Notify, Scheduler, JobEngine,
Attestor dual-write wiring on the same pattern. Tracked as follow-ups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004. After AUDIT-002 wired Emission in all 14+
priority services, the original AUDIT-003 scope of "add more polling
targets" is no longer load-bearing. The remaining candidate modules
(Scanner, Scheduler, Integrations, Attestor) do not expose HTTP audit
endpoints — they rely on Emission. SbomService's ledger audit endpoint
is artifact-specific and does not fit the unified polling contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004 execution log entry for the 26+ new
.Audited() decorations across Graph, SbomService, Policy.Gateway,
Notifier, Concelier, Excititor (commits 4cbe58fc8 + 6c3ebff9d).
Combined with pre-existing decoration in Authority/Scanner/Policy.Engine/
Notify/JobEngine/Integrations/AdvisoryAI/EvidenceLocker/Attestor, the
codebase now has ~240 .Audited() call sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004. First completion criterion of AUDIT-002
("AddAuditEmission() called in all 14+ service Program.cs files") is
now DONE after waves A (commit b2b0c905b) + B (commit 981f4459a).
Remaining: endpoint-level AuditActionAttribute decoration, runtime
verification at /api/v1/audit/events, startup-time regression check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004_Timeline_unified_audit_sink AUDIT-002.
Second wave wiring — services outside the original 14-priority table that
own production audit-relevant surfaces:
- Router Gateway.WebService (ingress, claim mapping)
- Registry.TokenService (token issuance, plan admin)
- PacksRegistry.WebService (packs lifecycle)
- IssuerDirectory.WebService (issuer/subject identity, PII)
- ExportCenter.WebService (compliance bundle origin)
Same pattern: ProjectReference to StellaOps.Audit.Emission,
`using StellaOps.Audit.Emission;`, and `builder.Services
.AddAuditEmission(builder.Configuration);` placed after auth wiring.
All 5 projects build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260408_004_Timeline_unified_audit_sink AUDIT-002.
Wire the unified audit emitter in 7 webservices that were listed in the
AUDIT-002 priority table but missing the call:
- Concelier.WebService (priority 6a)
- Excititor.WebService (priority 6b)
- SbomService (priority 8)
- Graph.Api (priority 12)
- BinaryIndex.WebService (priority 14)
- Policy.Gateway (priority 3b)
- Notifier.WebService (priority 4b)
Each adds:
- ProjectReference to __Libraries/StellaOps.Audit.Emission
- using StellaOps.Audit.Emission;
- builder.Services.AddAuditEmission(builder.Configuration) in Program.cs
placed after auth wiring, before authorization policies.
AUDIT-002 completion criterion "AddAuditEmission() called in all 14+
service Program.cs files" is now met for the listed priority services.
Endpoint-level AuditActionAttribute decoration is a separate wave.
All 7 projects build clean against the existing Audit.Emission lib.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/test-targeted-xunit.ps1: replace @(x).Count checks with
[bool] coercion in Assert-FilterShape; StrictMode 'Latest' rejects
.Count on null even when wrapped in @().
- ConcelierInfrastructureRegistrationTests.AddConcelierPostgresStorage_
RegistersDurableObservationAndAffectedSymbolServices: wrap provider
in try/finally with DisposeAsync — ConcelierDataSource is
IAsyncDisposable only, so sync Dispose at `using` scope end throws.
Follow-up to SPRINT_20260419_027/028.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test-targeted-xunit.ps1 verified end-to-end against
SchedulerStorageConfigurationTests (3/3 pass via direct DLL exec).
QA flow + testing-practices docs updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SchedulerStorageConfiguration ResolveConnectionString supports flat,
compose-nested, and legacy keys; web host fails fast with a clear
message listing all three. Targeted xUnit run of
SchedulerStorageConfigurationTests passes 3/3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three tasks are now DONE per the 2026-04-19 execution log:
- ADV-SETUP-006 setup source probe/apply reflects real connectivity
- ADV-SETUP-007 enabled-but-broken advisory warning + auth helper
- ADV-SETUP-008 targeted Platform + Concelier xUnit verification
passed via the direct xUnit runner workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to SPRINT_20260419_028 TEST-RUNNER-001.
- scripts/test-targeted-xunit.ps1: refinements to the helper.
- docs/code-of-conduct/TESTING_PRACTICES.md: default targeted xUnit v3
verification to the new helper.
- docs/qa/feature-checks/FLOW.md: call out Microsoft Testing Platform
filter-ignore behaviour and point to the helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260419_028_Tools_targeted_xunit_runner_workflow
(TEST-RUNNER-001 DOING — sprint remains active).
- scripts/test-targeted-xunit.ps1: rebuild-and-invoke xUnit v3 in-process
runner directly so targeted filters work under Microsoft Testing Platform
(dotnet test --filter is ignored there).
- Register sprint file in implplan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to SPRINT_20260419_027_Concelier_durable_affected_symbol_runtime.
PostgresAdvisoryObservationStoreTests + PostgresAffectedSymbolStoreTests
covering the durable store behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to SPRINT_20260419_027_Concelier_durable_affected_symbol_runtime
(REALPLAN-007-F still DOING).
Postgres-backed IAdvisoryObservationStore + IAffectedSymbolStore
implementations for the durable observation + affected-symbol
persistence path introduced by migration 008.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to SPRINT_20260419_027_Concelier_durable_affected_symbol_runtime.
UnsupportedRuntimeWiringTests updated for the removed non-testing
UnsupportedAffectedSymbol registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to SPRINT_20260419_027_Concelier_durable_affected_symbol_runtime
(REALPLAN-007-F still DOING).
- Program.cs: register AddConcelierObservationPipeline and drop the
non-testing UnsupportedAffectedSymbolStore/Provider overrides — the
durable stores will replace them as REALPLAN-007-F lands.
- ConcelierInfrastructureRegistrationTests: contract test asserting
AddConcelierPostgresStorage registers durable observation lookup/sink
and affected-symbol store services.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprints SPRINT_20260418_025_Concelier_durable_mirror_bundle_import_runtime
and SPRINT_20260419_026_Concelier_mirror_import_allowlisted_root.
- MirrorBundleImportRuntimeService: durable PostgreSQL-backed live mirror
bundle importer replacing the testing-only in-memory path.
- Enforces an allowlisted import root (Mirror.ImportRoot) and rejects
bundle/trust-root paths that resolve outside it; relative paths resolve
against the configured root, not the process cwd.
_025 and _026 ship together because _026 extends the same live importer
introduced in _025; splitting leaves an unguarded filesystem reach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260417_023_Concelier_truthful_affected_symbol_runtime.
UnsupportedAffectedSymbolServices shim returning a clear
501/unsupported response until the durable affected-symbol backend ships.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260417_022_AdvisoryAI_truthful_testing_only_runtime_fallback.
AdvisoryAiRuntimeStartupContractTests documenting the testing-only
in-memory fallback and its boundary versus the durable runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260417_002_JobEngine_scheduler_storage_compose_compatibility
(SCHEDULER-COMPAT-001 still DOING — sprint remains active).
Adds scheduler storage configuration adapter layer so the web host
accepts the compose-shaped storage configuration without manual remapping,
plus SchedulerStorageConfigurationTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint SPRINT_20260416_013_Authority_issuerdirectory_truthful_persistence_runtime.
IssuerDirectory.WebService Postgres persistence, options,
program wiring, tests. Sample config under etc/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>