Event Schemas
A dogwood policy set is checked against two schemas, and only one of them is required.
| Half | Format | Flag | Required |
|---|---|---|---|
| Action schema | Cedar .cedarschema — entities, actions, each action’s context | --policy-schema | Yes, for validate, lower and replay |
| Service schema | .dwschema event DSL, a providers.json, a .dw macro library | --event-schema, --providers, --macros | No |
The action schema is the one the rest of this lexicon already generates from; see Schema. This page is about the other half.
With all three service flags omitted, upstream falls back to
ServiceSchema::defaults(): request (deciding), response and error
kinds, a universal pin callerPrincipal = principal, a 24h max_window cap,
no providers, and the embedded default macro library.
The .dwschema surface
Section titled “The .dwschema surface”The grammar is 136 lines of pest and purely syntactic: an optional
max_window directive, then a sequence of event declarations.
max_window = 30d
decision event <A>::request { ...inputs(A), pin callerPrincipal: principalType(A) = principal, callerResource: resourceType(A), requestId: String,}A is a symbolic action binder, not an action. The file names no actions at
all. It says what shape an event of each kind has, for whichever action it
is derived against. That is why DWDC010 can check a predicate’s event kind
against the emitted schema but not its action: the action half of that check
lives in the .cedarschema, and it is the CLI’s to make.
Event kind names are author-defined. request, response and error are
conventional, not fixed.
| Builder | Emits |
|---|---|
spreadInputs() / spreadOutputs() | ...inputs(A) / ...outputs(A) |
field("requestId", concrete("String")) | requestId: String |
field("callerResource", resourceType()) | callerResource: resourceType(A) |
field("meta", record([…])) | a nested record, addressed as meta.member |
pinnedField(name, type, pinPrincipal()) | pin name: … = principal |
pinnedField(name, type, pinContext("input.user")) | pin name: … = context.input.user |
A pinned field must be a leaf; upstream requires the pin prefix and the
= … clause together, and the builder enforces both rather than deferring to
the parser.
The default, and the pin
Section titled “The default, and the pin”import { TemporalEventSchema, dogwood } from "@intentius/chant-lexicon-cedar";
export const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema() });That reproduces upstream’s pinned.dwschema (the shape
ServiceSchema::defaults() uses) and emits events.dwschema:
// The default event-schema shape: request/response/error, each correlated to// the deciding request's principal.
decision event <A>::request { ...inputs(A), pin callerPrincipal: principalType(A) = principal, callerResource: resourceType(A), requestId: String, sessionId: String,}
event <A>::response { ...inputs(A), ...outputs(A), pin callerPrincipal: principalType(A) = principal, callerResource: resourceType(A), requestId: String, sessionId: String,}
event <A>::error { ...inputs(A), pin callerPrincipal: principalType(A) = principal, callerResource: resourceType(A), requestId: String, sessionId: String,}The pin is the thing to understand before writing your own schema.
pin callerPrincipal = principal correlates every temporal predicate to the
deciding request’s principal: events logged by other principals are invisible
to formerly, since and every aggregate over them.
Supplying any event schema opts out of upstream’s default wholesale. So a schema emitted without a pin removes a correlation the policy author very likely assumed, and every predicate in the set starts matching other principals’ events.
That is a legitimate design; cross-principal correlation is a reason to write your own schema. It is also a decision, so chant makes it a named argument:
export const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema({ pinCallerPrincipal: false }),});which stamps the reasoning into the emitted file as a comment, and which DWDS010 reports as a warning in the build. Neither stops you. Both make the choice visible in a diff.
max_window
Section titled “max_window”The directive caps how far back any operator in the set may look. Absent, the cap is upstream’s 24h default, the same 24h that applies when no schema is supplied at all.
dogwood.defaultEventSchema({ maxWindow: "30d" }); // max_window = 30dDWDC011 does the arithmetic in TypeScript and fails the build on a window past
the cap, with no binary involved. Where several schemas are emitted the
tightest cap wins, and macro-call intervals count: once(48h, …) expands
through within ?w and looks back exactly as far as formerly within 48h.
Several schemas
Section titled “Several schemas”One .dwschema per file, because max_window is a single directive at the
top and concatenating two schemas would emit something upstream rejects. A
build with more than one gives each an explicit filename:
export const gateway = new TemporalEventSchema({ schema: dogwood.defaultEventSchema({ maxWindow: "30d" }), filename: "gateway.dwschema",});Two schemas targeting one filename is a serializer warning and only the first is written; a silent merge would produce a file that parses as neither.
Providers
Section titled “Providers”The third service flag, --providers, takes a providers.json whose entries
carry argumentTypes, an outputType and an implementation. chant has no
typed builder for it today; the CLI adapter’s bundle type accepts provider text
if you assemble it, and the build’s planner does not emit one.
One upstream trap: the CLI reads --providers as raw
text and never resolves scriptFile. Rhai has to be inlined under
implementation.script, or replay fails per-evaluation with “rhai
implementation has no script” while validate and lower still pass.
- Validation — DWDC010, DWDC011 and DWDS010 in full
- Replay — where the events these schemas describe actually come from