Skip to content

Requirements

This is the aggregate requirement list docs/PRD.md links to. It merges the three per-milestone REQUIREMENTS.md files that used to live under .planning/ (archived at docs/archive/planning/ as of 2026-08-08) with the requirement IDs already summarised in docs/PRD.md's own Functional Requirements section.

The "Verified by" column is the point of this document. It names a real test file or CI job, or says Unverified with a pointer to what would close the gap. Nothing here names a test that doesn't exist — every path below was confirmed present with ls/grep on 2026-08-08. Several rows below turned up requirements the original per-milestone documents marked "Complete" that have no automated test behind them; those are called out explicitly rather than carried forward as clean.

This document is the canonical source for requirement IDs going forward. docs/PRD.md's own inline requirement table (below its "Full requirement list" link) predates this consolidation and uses different, non-canonical numbers for some v2.0 items (its OBS-04 and TLS-03/TLS-04 rows) — see ID renumbering at the bottom of this page for how those map to the IDs used here.


v1.0 MVP (shipped 2026-03-03)

CEPA Protocol

ID Requirement Status Verified by
CEPA-01 Listener completes the RegisterRequest handshake with a <RegisterResponse> carrying an identity CEE will accept Delivered pkg/server/server_test.go::TestServeHTTP_RegisterRequest_ReturnsRegisterResponse asserts the four attributes CEndPoint::Init() requires and a non-zero event filter; TestServeHTTP_RegisterResponse_IsUTF16WhenAsked pins the encoding mirroring. Both mutation-tested. Corrected 2026-08-22 — this row said "HTTP 200 OK and strictly empty body" and that was the inverse of the truth. An empty body fails with Top node is not RegisterResponse, no partner is registered, and CEE answers the array 0x16 CEPP_NOT_FOUND while every observable stays green. The identity must also appear in CEE's compiled-in CGuidStore allowlist — see [cepa] in config.toml.example. Verified end to end against a live PowerStore on 2026-08-22.
CEPA-02 Listener responds to heartbeat PUT requests within 3 seconds Unverified Nothing measures response latency. The ACK-before-processing ordering is correct by inspection (pkg/server/server.go:94-116) but untested.
CEPA-03 Listener parses single-event CEE XML payloads into CEPAEvent structs Delivered pkg/parser/parser_test.go::TestParse ("single_event" case)
CEPA-04 Listener parses VCAPS bulk batch XML payloads (EventBatch) Delivered pkg/parser/parser_test.go::TestParse ("vcaps_batch_two_events" case)
CEPA-05 HTTP handler ACKs immediately and delegates event processing to an async queue Delivered pkg/server/server_test.go::TestServeHTTP_ACKsBeforeQueueWork (added in 77aec66) proves ServeHTTP returns a written 200 while the queued write is provably still in progress, via a channel-enforced happens-before chain — not the strict "WriteHeader precedes the enqueue loop" ordering, which the test's own doc comment explains is not observable from outside ServeHTTP (see docs/PROMISES.md for the full wording). TestServeHTTP_LargeBatchACKsWellUnder3s additionally covers a 2000-event VCAPS batch. Updated 2026-08-08 — this row previously said no test asserts the response is written before Enqueue runs; that stopped being true when these tests landed.
CEPA-06 Listener registers under an identity present in CEE's CGuidStore allowlist, configurable via [cepa] Delivered pkg/server/register.go. CEE refuses any other identity (unknown or invalid GUID / GUID mismatch) and then answers the array 0x16 CEPP_NOT_FOUND, so this is the difference between receiving every event and receiving none. Defaults are deliberately values CEE refuses — see ADR-017. TestServeHTTP_RegisterRequest_ReturnsRegisterResponse covers the document; the identity itself is operator configuration, verified end to end against PowerStore diabps01 on 2026-08-22.
CEPA-07 Listener answers CEE's <HeartBeatRequest /> with hbStatus=0 so the partner is reported ONLINE Delivered pkg/server/heartbeat.go; pkg/server/checkevent_test.go::TestServeHTTP_HeartBeatRequest_ReportsOnline. Without it registration succeeds but the array receives 0x12 OFFLINE and still publishes nothing. The CEPP_SERVICE_* values are read from an indexed pointer table in the CEE binary, not inferred from string order.
CEPA-08 Listener parses Dell CEE's <CheckEventRequest> event delivery Delivered pkg/parser/checkevent.go; pkg/parser/checkevent_test.go, pkg/server/checkevent_test.go. This is a third dialect, distinct from the VCAPS <CEEEvent> shapes in CEPA-03/04 and from OneFS's <CheckFileRequest>. Event codes are a 21-bit mask; one bit is measured directly, six more are corroborated by the independent OneFS table (TestEventTablesCorroborate), the remaining fourteen are documented-only and marked provisional in the source.
CEPA-09 Dispatcher transcodes each request body once Delivered pkg/parser/classify.go::Classify. Each Is* predicate decodes the whole body to read its root element, so dispatching through four of them and then parsing decoded the same payload five times — 62% of request time and 92% of allocations on a 1000-event batch, against a ~3 s CEPA deadline and a 64 MiB body cap.

Semantic Mapping

ID Requirement Status Verified by
MAP-01 CEPP_CREATE_FILE / CEPP_CREATE_DIRECTORY → EventID 4663, WriteData mask Delivered pkg/mapper/mapper_test.go::TestMapEventID
MAP-02 CEPP_FILE_READ → EventID 4663, ReadData mask Delivered pkg/mapper/mapper_test.go::TestMapEventID
MAP-03 CEPP_FILE_WRITE → EventID 4663, WriteData mask Delivered pkg/mapper/mapper_test.go::TestMapEventID
MAP-04 CEPP_DELETE_FILE / CEPP_DELETE_DIRECTORY → EventID 4660, DELETE mask Delivered pkg/mapper/mapper_test.go::TestMapEventID
MAP-05 CEPP_SETACL_FILE / CEPP_SETACL_DIRECTORY → EventID 4670, WRITE_DAC mask Delivered pkg/mapper/mapper_test.go::TestMapEventID
MAP-06 CEPP_CLOSE_MODIFIED → EventID 4663 with I/O statistics preserved Delivered pkg/mapper/mapper_test.go::TestMapEventID + TestMapFieldPropagation

Output — GELF (cross-platform)

ID Requirement Status Verified by
GELF-01 Emits valid GELF 1.1 JSON over UDP to a configurable host:port Delivered pkg/evtx/writer_gelf_test.go::TestBuildGELFValidJSON (payload construction only; no test dials a real UDP socket)
GELF-02 Supports TCP transport Unverified Nothing dials a real TCP listener for GELF specifically. Unlike syslog (TestSyslogTCPFraming, via net.Pipe), the GELF TCP dial/write path (pkg/evtx/writer_gelf.go:87-92) has no dedicated test.
GELF-03 Payload includes _event_id, _object_name, _account_name, _account_domain, _client_address, _access_mask, _cepa_event_type Delivered pkg/evtx/writer_gelf_test.go::TestBuildGELF, TestBuildGELFBytesFields
GELF-04 Reconnects automatically after a lost TCP connection Delivered pkg/evtx/writer_helpers_test.go::TestSendWithRetry_RetrySucceeds (shared retry helper used by all network writers; not GELF-specific)

Output — Win32 (Windows)

ID Requirement Status Verified by
WIN-01 Writes events to the Windows Application log via ReportEvent API, rendering a real description for event IDs 4660/4663/4670 (not the former placeholder text) Delivered pkg/evtx/writer_windows_test.go::TestEnsureEventSource_PointsAtCurrentExecutable and ::TestEnsureEventSource_RepointsStaleSource — the first tests in this repo's history to execute writer_windows.go — run in the windows CI job (.github/workflows/ci.yml), confirmed green in run 31315601780. That job's "Assert Event Viewer renders real text, not the placeholder" step drives the real binary via -emit-test-events and, for each of 4660/4663/4670, confirmed in the raw log ("event 4660/4663/4670 renders correctly") that Get-WinEvent resolves a non-null message containing the payload and Get-EventLog shows no placeholder text. docs/windows-verification.md (manual protocol, 2026-08-09, Windows Server 2025) additionally records the actual before text (placeholder, via a source registered the real pre-v5.0 way) and after text (real description) side by side
WIN-02 Registers the "PowerStore-CEPA" event source against the exporter's own executable on first start, and re-registers a source left pointing at EventCreate.exe by a pre-v5.0 build Delivered Same CI tests as WIN-01. TestEnsureEventSource_RepointsStaleSource covers the upgrade case specifically (seeded stale EventMessageFile, asserts it gets corrected). docs/windows-verification.md's "upgrade in place" step observed the real path fire against a source this run did not itself create: win32_source_repointing logged with from: "%SystemRoot%\\System32\\EventCreate.exe", to the real executable path, on Windows Server 2025, 2026-08-09

Output — Multi

ID Requirement Status Verified by
MULTI-01 Fans events to all configured backends; one backend's failure doesn't block delivery to others Delivered pkg/evtx/writer_multi_test.go::TestMultiWriter_WriteEvent_AllCalledJoinedErrors

Transport Security

ID Requirement Status Verified by
TLS-01 Listener supports HTTPS with a configurable x509 certificate and key Delivered (partial) cmd/cee-exporter/tls_test.go::TestMigrateListenConfig ("legacy_tls_true_with_cert" case) covers config recognition; no test drives an actual TLS handshake against the listener.
TLS-02 TLS certificate expiry is logged at startup; WARN logged when < 30 days remain Delivered (partial) pkg/server/health_test.go::TestHealth_TLSCertExpiryPopulated confirms expiry is computed and exposed via /health; the startup WARN-log-under-30-days path itself has no test.

Observability

ID Requirement Status Verified by
OBS-01 GET /health returns JSON with uptime, queue depth, events received/written/dropped, writer type/target Delivered pkg/server/health_test.go::TestHealth_OKWithBaseline, TestHealth_DegradedWhenDropped
OBS-02 Structured JSON logs (slog) include event_type, file_path, client_ip, queue_depth, latency_ms per batch Unverified Nothing captures/parses slog output to confirm field presence. Correct by inspection (pkg/server/server.go:87-92,108-113).
OBS-03 Dropped events (queue overflow) are logged at WARN with running total Delivered (partial) pkg/queue/queue_test.go::TestDropOnFull verifies the counter increments; the WARN log line itself is not asserted by any test.

Quality

ID Requirement Status Verified by
QUAL-01 Unit tests cover the CEE XML parser (single event, VCAPS batch, malformed, RegisterRequest detection) Delivered pkg/parser/parser_test.go
QUAL-02 Unit tests cover the CEPA → WindowsEvent mapper (all 6 event types, field propagation) Delivered pkg/mapper/mapper_test.go
QUAL-03 Unit tests cover the queue (enqueue, drop on full, drain on stop) Delivered pkg/queue/queue_test.go
QUAL-04 Unit tests cover GELFWriter payload construction (field presence, GELF 1.1 compliance) Delivered pkg/evtx/writer_gelf_test.go
QUAL-05 Fix readBody nil ResponseWriter bug (panic on oversized payload) Delivered pkg/server/server_test.go::TestReadBodyOversized
QUAL-06 go build ./... and go vet/golangci-lint pass with zero warnings on Linux and Windows Delivered make lint (golangci-lint run) + make build/make build-windows, both run in CI (.github/workflows/ci.yml)

Build & Distribution

ID Requirement Status Verified by
BUILD-01 Makefile with build, build-windows, test, lint, clean targets Delivered Makefile
BUILD-02 Cross-compiled Windows binary (GOOS=windows GOARCH=amd64) via make build-windows Delivered Makefile:build-windows; cross-built in CI (.github/workflows/ci.yml)

Documentation

ID Requirement Status Verified by
DOC-01 README covers installation, prerequisites, quick-start Delivered README.md, docs/operator-guide.md ("Installation")
DOC-02 README documents all config.toml fields with examples Delivered docs/operator-guide.md ("Configuration reference")
DOC-03 README covers TLS setup Delivered docs/operator-guide.md ("TLS / HTTPS setup")
DOC-04 README covers CEPA registration Delivered docs/operator-guide.md ("Registering with PowerStore CEPA")

v2.0 Operations & Output Expansion (shipped 2026-03-04)

Observability (Prometheus)

IDs renumbered OBS-04–OBS-08 — see ID renumbering.

ID Requirement Status Verified by
OBS-04 Operator can scrape cee_events_received_total from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_AllRequiredMetrics
OBS-05 Operator can scrape cee_events_dropped_total from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_AllRequiredMetrics
OBS-06 Operator can scrape cee_queue_depth from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_AllRequiredMetrics
OBS-07 Operator can scrape cee_writer_errors_total from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_AllRequiredMetrics
OBS-08 /metrics is served on a configurable dedicated port (default 9228) Delivered (partial) cmd/cee-exporter/main.go ([metrics] addr, default 0.0.0.0:9228); no test exercises a non-default value end-to-end.
OBS-10 Operator can scrape per-publisher CEPA liveness (cee_cepa_last_request_unix_seconds{remote}) from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_CEPAPeerMetrics, pkg/server/server_test.go::TestServeHTTP_StampsPeerOnEveryPath
OBS-11 Operator can scrape per-publisher handshake counts (cee_cepa_registrations_total{remote}) from /metrics Delivered pkg/prometheus/handler_test.go::TestMetricsHandler_CEPAPeerMetrics, pkg/server/server_test.go::TestServeHTTP_CountsRegistrationsOnly
OBS-12 The remote label is bounded — port stripped, publishers capped at 64, overflow counted in cee_cepa_peers_dropped_total Delivered pkg/metrics/metrics_test.go::TestStore_PeerCap, pkg/server/server_test.go::TestServeHTTP_StampsPeerWithoutPort, pkg/server/server_test.go::TestPeerHost

Service Deployment

ID Requirement Status Verified by
DEPLOY-01 Linux operator is provided a hardened systemd unit file Delivered deploy/systemd/cee-exporter.service
DEPLOY-02 systemctl enable --now cee-exporter auto-starts at boot with auto-restart on failure Delivered deploy/systemd/cee-exporter.service (Restart=on-failure, RestartSec=5s, WantedBy=multi-user.target); no live-systemd integration test.
DEPLOY-03 cee-exporter.exe install registers the daemon with the Service Control Manager Delivered The windows CI job's "Service install/uninstall round-trip" step, confirmed in run 31318610291's raw log: sc.exe query cee-exporter immediately after install returned STATE: 1 STOPPED, and — since the I-3/S-1 fix round — sc.exe start followed by polling Get-Service reached Running before sc.exe stop reached Stopped. This exercises svcProgram.Start/Stop in service_windows.go for the first time, not just kardianos's SCM registration helpers, and closes the earlier caveat: every native .exe/sc.exe call in the step is now gated on $LASTEXITCODE, so a silent non-zero exit without a terminating PowerShell error would fail the step
DEPLOY-04 cee-exporter.exe uninstall removes the daemon from the Service Control Manager Delivered (partial) Same step, same run: .\cee-exporter.exe uninstall printed Service "cee-exporter" uninstalled., now gated on $LASTEXITCODE -eq 0 rather than inferred from $ErrorActionPreference='Stop' alone. The job still does not re-run sc.exe query after uninstall to independently confirm the service is actually gone from the SCM, so removal is confirmed by the tool's own success message and exit code, not by re-querying the SCM afterward
DEPLOY-05 Windows Service auto-restarts after an unexpected crash Unverified The windows CI job now exercises svcProgram.Start/Stop via sc.exe start/stop (see DEPLOY-03) — the SCM can start and cleanly stop the service — but nothing simulates a crash or observes the OnFailure: "restart" recovery action in service_windows.go's svcConfig actually firing. Recovery-action configuration remains correct by inspection only.

Output Targets

ID Requirement Status Verified by
OUT-01 BeatsWriter forwards events via Lumberjack v2 to Logstash/Graylog Delivered pkg/evtx/writer_beats_test.go::TestBuildBeatsEvent
OUT-02 BeatsWriter supports TLS Delivered (partial) pkg/evtx/writer_beats_test.go::TestBeatsWriterDialerInjection exercises the TLS dial path only for an unreachable-address failure; no test confirms a successful TLS handshake.
OUT-03 SyslogWriter forwards RFC 5424 over UDP Delivered pkg/evtx/writer_syslog_test.go::TestBuildSyslog5424
OUT-04 SyslogWriter forwards RFC 5424 over TCP (octet-counting) Delivered pkg/evtx/writer_syslog_test.go::TestSyslogTCPFraming (via net.Pipe)
OUT-05 BinaryEvtxWriter writes native .evtx files on Linux Delivered pkg/evtx/writer_evtx_notwindows_test.go (TestBinaryEvtxWriter_WriteClose, TestBinaryEvtxWriter_ChunkLayout, and others)
OUT-06 Generated .evtx files open correctly in Windows Event Viewer and parse with forensics tools Verified evtx-readback job (Get-WinEvent -Path on windows-latest, reading the artifact from evtx-oracle) covers opening the file, 3 records, the event ID set {4660, 4663, 4670}, per-record ToXml(), and ObjectName in the rendered XML; evtx-oracle job (python-evtx via tools/evtx-debug/verify_evtx.py) is the independent-parser half, and since the Channel fix below it also asserts System/Channel == "Security" in the generated XML on every push. Description rendering from a saved log, LogName resolving to Security, and the Event Viewer GUI open/placement question — none CI-gated, since the first two need a host with the event source registered and the third additionally needs an interactive desktop session — are covered by the dated manual protocol in docs/windows-verification.md section 5: an initial 2026-08-10 reading on winvm found descriptions not rendering, traced to this project's own -emit-test-events fixture leaving ProviderName empty rather than a go-evtx or saved-log defect (see CHANGELOG.md's [5.1.0] entry); re-measured 2026-08-10 with ProviderName set, all three descriptions render correctly. A second, separate defect surfaced the same day: windowsEventToFields dropped WindowsEvent.Channel (which pkg/mapper has set to "Security" on every event since v2) before it ever reached go-evtx, so LogName resolved empty on every record this exporter has ever written. Fixed by passing Channel through with evtx.DefaultChannel = "Security" as the fallback (pkg/evtx/writer_evtx_notwindows_test.go::TestWindowsEventToFields_Channel); the same saved log now measures LogName=[Security] where it measured LogName=[] before. The Event Viewer GUI open/placement question was run on 2026-08-10 over an RDP session to winvm (the earlier SSH-only connection had no interactive desktop) against the released v5.1.0 binary downloaded from GitHub, not a local build: the saved log opens via Action → Open Saved Log…, lists under Saved Logs → released-v5.1.0 with all three IDs present and Log Name: Security in the General pane. A pre-run prediction that the Level/Task Category/Keywords columns would render blank — based on Get-WinEvent's *DisplayName properties returning empty strings headlessly — was falsified: Event Viewer supplies its own defaults (Information/None/Info/None) for those zero values, because the empty PowerShell properties reflect missing provider metadata to resolve against, not an absent GUI value.

v3.0 TLS Certificate Automation (Phase 8, shipped 2026-03-04)

IDs renumbered TLS-03–TLS-07 — see ID renumbering.

ID Requirement Status Verified by
TLS-03 tls_mode="acme" auto-obtains/renews a certificate from Let's Encrypt via TLS-ALPN-01 Unverified Nothing. Requires a live ACME server. Config recognition only (cmd/cee-exporter/tls_test.go::TestMigrateListenConfig, "already_set_acme" case).
TLS-04 tls_mode="self-signed" generates a runtime ECDSA certificate — no files, no network Delivered cmd/cee-exporter/tls_test.go::TestBuildSelfSignedTLS
TLS-05 tls_mode="manual" (or legacy tls=true + cert_file/key_file) loads credentials from files Delivered cmd/cee-exporter/tls_test.go::TestMigrateListenConfig
TLS-06 Existing tls=true + cert_file/key_file configs are auto-migrated to tls_mode="manual" Delivered cmd/cee-exporter/tls_test.go::TestMigrateListenConfig ("legacy_tls_true_with_cert" case)
TLS-07 config.toml.example documents all four TLS modes with explanatory comments Delivered config.toml.example (tls_mode block) + docs/operator-guide.md ("TLS / HTTPS setup")

v4.0 Industrialisation (shipped 2026-03-05)

OSS Module Extraction (EXT)

ID Requirement Status Verified by
EXT-01 github.com/fjacquet/go-evtx exists as an independent Go module with its own repo, CI, README Delivered (partial) go.mod/go.sum (github.com/fjacquet/go-evtx v0.8.2). The module's own repository, CI, and README are external and not verifiable from this repo.
EXT-02 go-evtx exposes a low-level WriteRaw(chunk []byte) error API Unverified from this repo cee-exporter does not call WriteRaw; go-evtx's own test suite is external, not observable here.
EXT-03 go-evtx exposes a high-level WriteRecord(eventID int, fields map[string]string) error API Delivered pkg/evtx/writer_evtx_notwindows.go:49 calls b.w.WriteRecord(...)
EXT-04 Existing pkg/evtx EVTX tests are ported to go-evtx and pass in its CI Unverified from this repo External repository/CI, not observable from here.
EXT-05 cee-exporter's BinaryEvtxWriter becomes a thin adapter over go-evtx Delivered pkg/evtx/writer_evtx_notwindows.go — a translation-layer adapter delegating to goevtx.New/WriteRecord/Close/Rotate, plus the defensive field-truncation and idempotent-Close guards Task 4 added on top. (A line count was cited here before this correction; it read 49 at extraction, then went stale at 228 once Task 4 landed — a measurement that rots the moment the file is next edited. Describing the shape instead of counting lines is the fix, not a fresher number.)

Durability (FLUSH)

ID Requirement Status Verified by
FLUSH-01 flush_interval_s (default 15) triggers periodic f.Sync() Delivered (partial) cmd/cee-exporter/validate_test.go::TestValidateOutputConfig covers config validation; the fsync ticker itself now lives in go-evtx (external, post-EXT-05), not testable from this repo.
FLUSH-02 All buffered events are flushed and fsynced before process exit on graceful shutdown Delivered pkg/queue/queue_test.go::TestDrainOnStop + pkg/evtx/writer_evtx_notwindows_test.go::TestBinaryEvtxWriter_WriteClose
FLUSH-03 /metrics exposes cee_last_fsync_unix_seconds Delivered pkg/metrics/metrics_test.go::TestStore_LastFsyncUnix + pkg/prometheus/handler_test.go::TestMetricsHandler_AllRequiredMetrics

EVTX Correctness (EVTX)

ID Requirement Status Verified by
EVTX-01 BinaryEvtxWriter writes all events regardless of session length (fix flushChunkLocked() stub silently dropping events beyond ~2,400/session) Moved The fix and its implementation now live in go-evtx (external, post-EXT-05 extraction). Nothing to verify in this repo today.

File Rotation (ROT)

ID Requirement Status Verified by
ROT-01 max_file_size_mb rotates the active .evtx file when it reaches that size Delivered (partial) Config validated by cmd/cee-exporter/validate_test.go::TestValidateOutputConfig; the rotation algorithm itself is now implemented in go-evtx (external, unverified from here).
ROT-02 max_file_count keeps only the N most recent archive files Delivered (partial) Same as ROT-01.
ROT-03 rotation_interval_h rotates on a fixed schedule Delivered (partial) Same as ROT-01.
ROT-04 SIGHUP triggers an immediate rotation without restart Delivered (partial) pkg/evtx/writer_multi_test.go::TestMultiWriter_Rotate_ForwardsAndSkips verifies Rotate() forwarding; the SIGHUP signal registration itself (cmd/cee-exporter/sighup_notwindows.go) has no test.

Configuration (CFG)

ID Requirement Status Verified by
CFG-01 All flush/rotation parameters configurable in [output] with documented zero-value semantics Delivered config.toml.example
CFG-02 Invalid configuration (e.g. flush_interval_s = 0) is rejected at startup with a clear error Delivered cmd/cee-exporter/validate_test.go::TestValidateOutputConfig
CFG-03 config.toml.example documents all four new [output] fields Delivered config.toml.example

Architecture & Documentation (ADR)

ID Requirement Status Verified by
ADR-01 ADR documents owning the flush ticker inside BinaryEvtxWriter (not the queue layer) Delivered docs/adr/ADR-012-flush-ticker-ownership.md
ADR-02 ADR documents switching from write-on-close to open-handle incremental flush Unverified docs/adr/ADR-013-write-on-close-model.md exists but its own Status field still reads "Accepted (superseded by Phase 10 open-handle model when complete)" — it was never updated after Phase 10 shipped, and the whole area was later overtaken again by the go-evtx extraction (ADR-014). The decision was made and shipped; the record documenting it was not kept current. Documentation debt, not a code gap.

Deferred / Future Requirements (not yet built)

ID Requirement Status Verified by
OBS-F01 Prometheus alerting-threshold docs/examples for dropped events Deferred N/A — not implemented
OBS-F02 sd_notify READY=1 for Type=notify systemd units Deferred N/A — not implemented
OBS-09 cee_rotation_total counter for tracking rotation events Deferred N/A — not implemented (renumbered from v5 draft OBS-01; see ID renumbering)
OUT-F01 BinaryEvtxWriter cross-event template sharing for reduced file size Deferred N/A — not implemented
OUT-F02 BeatsWriter AsyncClient with batching Deferred N/A — not implemented
OUT-F03 Syslog TLS transport (RFC 5425) Deferred N/A — not implemented
EVTX-02 Full multi-chunk EVTX file support Deferred N/A — not implemented
EVTX-03 Startup repair pass for partial-chunk files left by a crash Deferred N/A — not implemented
ROT-F01 Compression of rotated .evtx files Deferred N/A — blocked on forensics tool support
TLS-F01 DNS-01 ACME challenge via go-acme/lego (air-gapped / private-network ACME) Deferred N/A — not implemented
TLS-F02 Let's Encrypt staging-URL support via a dedicated config flag Deferred N/A — not implemented. An earlier draft documented that flag by name as if it already existed; the 2026-08-08 audit found and removed that phantom-key claim (see the no-phantom-config job in .github/workflows/docs-lint.yml, and the correction banner in ADR-011).

ID renumbering notes

The three source REQUIREMENTS.md files reused some prefixes across milestones for unrelated requirements. This document keeps the original IDs where they don't collide, and renumbers the later, colliding set — continuing the same prefix's sequence rather than inventing a new one — so every ID in this document is unique. The mapping:

Prefix Original ID (source doc) Original meaning ID used here
OBS OBS-01..OBS-03 (.planning/milestones/v1.0-REQUIREMENTS.md) /health JSON, structured logs, dropped-event WARN OBS-01..OBS-03 (unchanged)
OBS OBS-01..OBS-05 (v2 section, .planning/milestones/v3.0-REQUIREMENTS.md) Prometheus /metrics counters/gauge, port config OBS-04..OBS-08
OBS OBS-01 (v5 section, .planning/milestones/v4.0-REQUIREMENTS.md) cee_rotation_total counter (deferred) OBS-09
TLS TLS-01, TLS-02 (.planning/milestones/v1.0-REQUIREMENTS.md) HTTPS support, cert expiry logging TLS-01, TLS-02 (unchanged)
TLS TLS-01..TLS-05 (v3 section, .planning/milestones/v3.0-REQUIREMENTS.md) ACME, self-signed, manual, migration, docs TLS-03..TLS-07

docs/PRD.md's own inline table (written before this consolidation) already worked around part of this collision ad hoc: it used OBS-04 for the metrics-port item (this document's OBS-08) and TLS-03/TLS-04 for the ACME/self-signed items (this document's TLS-03/TLS-04 — those two happen to agree). The PRD table was not rewritten as part of this consolidation (out of scope for the change that produced this document); this document is the canonical numbering going forward.

Separately, .planning/milestones/v1.0-REQUIREMENTS.md contains a draft "v2 Requirements" section (EVTX-01, EVTX-02, BEATS-01, OPS-01..OPS-03) written before Phase 4-8 planning happened. It was entirely superseded by the real v2.0 milestone requirements (OUT-01..OUT-06, DEPLOY-01..DEPLOY-05, OBS-04..OBS-08 above) under different names, and is not carried into this table — it is preserved only in the archived source file for provenance.