PQC203 — Events must be declared in YAML, not built by hand¶
Adapter code builds an event itself — with
EventBuilder.Event(...)or with a builder from the generatedPqEventcatalog.
Both forms are reported:
// reported — the event id is a bare string
var evt = EventBuilder.Event("pq.event.technical.fault.hardware", Severity.Warn)
.At(timestamp, panel)
.Build();
await panel.PublishEvent(evt);
// reported — the id is typed, but nothing ties it to the device type's YAML
await panel.PublishEvent(PqEvent.Technical.Fault.Hardware
.At(timestamp, panel)
.WithParameter("reason", "DeviceLicenseRemoved")
.Build());
PqEvent is the generator's own building material. The generated publication methods are built from it; adapter code calls those methods and never touches PqEvent or EventBuilder.
Why the build refuses this¶
A wrong id or parameter fails silently and permanently. EventBuilder accepts any string, so pq.event.technical.fault.hardwre compiles. WithParameter accepts any name, so "reason" compiles even though the event carries faultDescription. Nothing errors at runtime — the value lands where nobody reads it, and the failure surfaces as "that fault never reached us", months later.
The declaration is what generates everything else. Declaring the event under events: produces the typed publication method, its parameters from the taxonomy, the severity from the taxonomy, and the NATS subject routing. Hand-building skips all of it and reimplements the parts the author happened to remember.
YAML stops being the source of truth. "What does this adapter emit?" is answerable from adapter-registration.yaml — until a hand-built event hides in a routing class, and the only honest answer becomes "read every file".
How to fix it¶
1. Find the device type that emits the event¶
The event belongs to the Thing it happens on: a hardware fault of the panel belongs to the panel type, a forced door to the door type. Open the adapter's adapter-registration.yaml and find that type under device_types:.
2. Add the event id under events:¶
List the full event id from the taxonomy (src/Pq.Adapters.Framework/pq-events.yaml). If the type has no events: block yet, add one next to functions::
# adapter-registration.yaml
device_types:
- type_id: "Panel"
name: "Acme Panel"
category: panel
functions:
Comm:
Tamper:
events:
- pq.event.system.configuration.changed
- pq.event.technical.fault.hardware # ← the new line
properties:
ip_address:
type: host
required: true
If the id is not in the taxonomy, the build fails with PQA027 — a typo cannot slip through here.
3. Build, then call the generated method¶
The build generates a method on the Thing class. The name is the shortest unique tail of the event id; the parameters are the ones the taxonomy declares for the event:
// generated on Panel — do not write this yourself
public ValueTask<bool> HardwareFault(DeviceTimestamp timestamp, string faultDescription, string? faultCode = null)
Call it from the router or handler:
Required parameters are plain arguments; optional ones default to null. Pass every value the device reports — a dropped value leaves the audit record poorer than the taxonomy intends. A value the device reports only sometimes goes in as null when it is missing. A value the protocol implies goes in as a constant at the call site.
Not sure what the method is called? Type panel. and let the IDE list it, or look up the naming table in Declared Event Methods.
When a function already publishes the event¶
Some events belong to a function the type already has: Tamper publishes pq.event.intrusion.tamper, Door publishes pq.event.access.door.opened. Do not list those under events:. Call the function action instead — it publishes the event and updates the function state in the same call:
What is not reported¶
Generated code. The generator builds its methods from PqEvent and EventBuilder — that is where they belong, one layer below the adapter. Generated trees are excluded from analysis.
Framework code. The compliance analyzer runs on adapters only.
If you disagree with a report¶
Do not suppress it. A wrong report is a bug in the check — report it with the code that triggered it.