@intentius/chant-lexicon-cedar 0.44.9 → 0.44.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +48 -2
  2. package/dist/agentcore/embed.d.ts +189 -0
  3. package/dist/agentcore/embed.d.ts.map +1 -0
  4. package/dist/agentcore/enforcement.d.ts +76 -0
  5. package/dist/agentcore/enforcement.d.ts.map +1 -0
  6. package/dist/agentcore/scan.d.ts +46 -0
  7. package/dist/agentcore/scan.d.ts.map +1 -0
  8. package/dist/avp/client.d.ts +85 -8
  9. package/dist/avp/client.d.ts.map +1 -1
  10. package/dist/codegen/docs-dogwood.d.ts +21 -0
  11. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  12. package/dist/codegen/docs.d.ts.map +1 -1
  13. package/dist/dogwood/cli.d.ts +41 -0
  14. package/dist/dogwood/cli.d.ts.map +1 -1
  15. package/dist/dogwood/index.d.ts +10 -4
  16. package/dist/dogwood/index.d.ts.map +1 -1
  17. package/dist/dogwood/replay-activity.d.ts +196 -0
  18. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  19. package/dist/dogwood/replay-op.d.ts +165 -0
  20. package/dist/dogwood/replay-op.d.ts.map +1 -0
  21. package/dist/dogwood/serialize.d.ts +20 -0
  22. package/dist/dogwood/serialize.d.ts.map +1 -1
  23. package/dist/dogwood/trace.d.ts +215 -0
  24. package/dist/dogwood/trace.d.ts.map +1 -0
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +5 -3
  28. package/dist/lint/audit-catalog.d.ts.map +1 -1
  29. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  30. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  31. package/dist/lint/post-synth/index.d.ts.map +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/okf/index.md +1 -0
  34. package/dist/okf/rules/DWDC013.md +15 -0
  35. package/dist/okf/types/Policy.md +1 -0
  36. package/dist/op/activities/index.d.ts +18 -0
  37. package/dist/op/activities/index.d.ts.map +1 -0
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/rules/dwdc013.ts +64 -0
  40. package/dist/skills/chant-cedar-dogwood.md +327 -0
  41. package/package.json +7 -2
  42. package/src/agentcore/embed.test.ts +254 -0
  43. package/src/agentcore/embed.ts +399 -0
  44. package/src/agentcore/enforcement.test.ts +43 -0
  45. package/src/agentcore/enforcement.ts +92 -0
  46. package/src/agentcore/scan.ts +119 -0
  47. package/src/avp/OWNERSHIP.md +38 -0
  48. package/src/avp/client.test.ts +271 -0
  49. package/src/avp/client.ts +150 -16
  50. package/src/codegen/docs-dogwood.ts +1119 -0
  51. package/src/codegen/docs.ts +66 -1
  52. package/src/dogwood/cli.test.ts +122 -1
  53. package/src/dogwood/cli.ts +122 -1
  54. package/src/dogwood/index.ts +74 -1
  55. package/src/dogwood/replay-activity.test.ts +481 -0
  56. package/src/dogwood/replay-activity.ts +506 -0
  57. package/src/dogwood/replay-op.ts +242 -0
  58. package/src/dogwood/serialize.ts +37 -0
  59. package/src/dogwood/trace.test.ts +231 -0
  60. package/src/dogwood/trace.ts +471 -0
  61. package/src/index.ts +52 -0
  62. package/src/lint/audit-catalog.ts +8 -0
  63. package/src/lint/post-synth/dwd-post-synth.test.ts +119 -1
  64. package/src/lint/post-synth/dwdc013.ts +64 -0
  65. package/src/lint/post-synth/dwde-post-synth.test.ts +1 -1
  66. package/src/lint/post-synth/index.ts +2 -0
  67. package/src/op/activities/index.ts +27 -0
  68. package/src/plugin.test.ts +3 -2
  69. package/src/plugin.ts +30 -0
  70. package/src/skills/chant-cedar-dogwood.md +327 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * DWDC013: a temporal statement embedded in AgentCore needs its event schema
3
+ * registered
4
+ *
5
+ * `AWS::BedrockAgentCore::Policy` carries one string. When that string is `.dw`
6
+ * text — `Definition.Policy.Statement`, the language-agnostic arm — the policy
7
+ * engine at the other end has to know what an event *is* before any temporal
8
+ * predicate in it can match: which kinds exist, what fields they carry, and
9
+ * what is pinned to the deciding request. That is the `.dwschema` half of the
10
+ * schema, and the resource has no property to put it in.
11
+ *
12
+ * So a build that embeds temporal text and emits no event schema has shipped
13
+ * half a policy. Nothing fails: the statement deploys, the predicates match
14
+ * nothing, and a `formerly`-guarded permit stops granting — or a
15
+ * `formerly`-guarded forbid stops denying, which is the direction that
16
+ * matters. That is the silent trap this check exists for — the emitted schema
17
+ * is the artifact that makes the other half reviewable, whether it is
18
+ * registered with the engine out of band or shipped to a `dogwood validate`
19
+ * run.
20
+ *
21
+ * Warning rather than error, because "not emitted here" is not the same as
22
+ * "does not exist": a project may register the service schema through a
23
+ * separate pipeline, and failing that build would be chant asserting a fact it
24
+ * cannot check. Emitting one with `TemporalEventSchema` silences it and makes
25
+ * the assumption visible in the diff.
26
+ *
27
+ * The complement of DWDC010, which asks whether the kinds a `.dw` file names
28
+ * are declared. This one asks the prior question, and only of statements that
29
+ * left the `.dw` file behind.
30
+ */
31
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
32
+ import { embeddedAgentCorePolicyStatements } from "../../agentcore/scan";
33
+ import { dogwoodSchemaFiles, temporalRegions } from "../../dogwood/scan";
34
+
35
+ export const dwdc013: PostSynthCheck = {
36
+ id: "DWDC013",
37
+ description: "An embedded AgentCore temporal statement has its event schema emitted beside it",
38
+
39
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
40
+ if (dogwoodSchemaFiles(ctx).length > 0) return [];
41
+
42
+ const diagnostics: PostSynthDiagnostic[] = [];
43
+ const seen = new Set<string>();
44
+
45
+ for (const embedded of embeddedAgentCorePolicyStatements(ctx)) {
46
+ if (temporalRegions(embedded.statement).length === 0) continue;
47
+
48
+ const named = embedded.logicalId ?? embedded.source;
49
+ const key = `${embedded.lexicon}:${embedded.source}:${named}`;
50
+ if (seen.has(key)) continue;
51
+ seen.add(key);
52
+
53
+ diagnostics.push({
54
+ checkId: "DWDC013",
55
+ severity: "warning",
56
+ message: `AgentCore policy "${named}" in "${embedded.source}" carries temporal text in Definition.Policy.Statement, but the build emitted no .dwschema event schema. The policy engine needs the event kinds and their pins registered before any temporal predicate can match; without them the clause never fires and the policy silently stops doing its job. Declare a TemporalEventSchema, or record where the service schema is registered.`,
57
+ entity: named,
58
+ lexicon: embedded.lexicon,
59
+ });
60
+ }
61
+
62
+ return diagnostics;
63
+ },
64
+ };
@@ -341,7 +341,7 @@ describe("registration", () => {
341
341
  test("the helper module contributes no check of its own", () => {
342
342
  // `dogwood-helpers.ts` is excluded from discovery by the "helper" filename
343
343
  // filter, the same way `wasm-helpers.ts` is.
344
- expect(postSynthChecks.filter((c) => c.id.startsWith("DWD"))).toHaveLength(6);
344
+ expect(postSynthChecks.filter((c) => c.id.startsWith("DWD"))).toHaveLength(7);
345
345
  });
346
346
  });
347
347
 
@@ -13,6 +13,7 @@ import { ceds012 } from "./ceds012";
13
13
  import { dwdc010 } from "./dwdc010";
14
14
  import { dwdc011 } from "./dwdc011";
15
15
  import { dwdc012 } from "./dwdc012";
16
+ import { dwdc013 } from "./dwdc013";
16
17
  import { dwde010 } from "./dwde010";
17
18
  import { dwde011 } from "./dwde011";
18
19
  import { dwds010 } from "./dwds010";
@@ -31,6 +32,7 @@ export const postSynthChecks: PostSynthCheck[] = [
31
32
  dwdc010,
32
33
  dwdc011,
33
34
  dwdc012,
35
+ dwdc013,
34
36
  dwde010,
35
37
  dwde011,
36
38
  dwds010,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * cedar Op activities — resolved by the core activity registry when a
3
+ * project's `chant.config.ts` lists the `cedar` lexicon.
4
+ *
5
+ * The registry keys every exported *function* in this module by its name
6
+ * (`loadActivities` → `collectActivities`), which is why only the activities
7
+ * themselves are exported here. `dogwoodReplay`'s helpers — the input
8
+ * resolver, the verdict comparison, the summary renderer — stay importable
9
+ * from `@intentius/chant-lexicon-cedar/dogwood/replay-activity` rather than
10
+ * being registered as activities nobody would ever name in a step.
11
+ *
12
+ * Contributed the `flyApply` way: a plain async function taking one args
13
+ * object, with no Temporal import anywhere beneath it, so the local executor
14
+ * runs it unchanged and a Temporal worker registers the same function.
15
+ */
16
+
17
+ export { dogwoodReplay, dogwoodReplayReport } from "../../dogwood/replay-activity";
18
+ export type {
19
+ DogwoodReplayArgs,
20
+ DogwoodReplayReportArgs,
21
+ ExpectedVerdict,
22
+ PolicyReplayDispatch,
23
+ PolicyReplayMode,
24
+ PolicyReplayReport,
25
+ ReplayDivergence,
26
+ ReplayExpectation,
27
+ } from "../../dogwood/replay-activity";
@@ -43,9 +43,9 @@ describe("cedar plugin", () => {
43
43
  }
44
44
  });
45
45
 
46
- it("ships three skills whose frontmatter matches their registered name", () => {
46
+ it("ships four skills whose frontmatter matches their registered name", () => {
47
47
  const skills = cedarPlugin.skills?.() ?? [];
48
- expect(skills).toHaveLength(3);
48
+ expect(skills).toHaveLength(4);
49
49
 
50
50
  for (const skill of skills) {
51
51
  // An empty body means the loader could not read the file — the skill
@@ -59,6 +59,7 @@ describe("cedar plugin", () => {
59
59
  expect(skills.map((s) => s.name).sort()).toEqual([
60
60
  "chant-cedar-authoring",
61
61
  "chant-cedar-avp-embedding",
62
+ "chant-cedar-dogwood",
62
63
  "chant-cedar-meta-policy",
63
64
  ]);
64
65
  });
package/src/plugin.ts CHANGED
@@ -153,6 +153,36 @@ export const cedarPlugin: LexiconPlugin = {
153
153
  },
154
154
  ],
155
155
  },
156
+ {
157
+ file: "chant-cedar-dogwood.md",
158
+ name: "chant-cedar-dogwood",
159
+ description:
160
+ "Author dogwood temporal policies with the typed builders — parser primitives versus default-library macros, the callerPrincipal pin, the CLI-gated validation split, AgentCore embedding and trace replay",
161
+ triggers: [
162
+ { type: "file-pattern", value: "**/*.dw" },
163
+ { type: "file-pattern", value: "**/*.dwschema" },
164
+ { type: "context", value: "dogwood" },
165
+ { type: "context", value: "temporal policy" },
166
+ { type: "context", value: "agentcore policy" },
167
+ { type: "context", value: "rate limit policy" },
168
+ { type: "context", value: "policy replay" },
169
+ ],
170
+ parameters: [],
171
+ examples: [
172
+ {
173
+ title: "Approval before action",
174
+ description: "Allow a read only if a login happened inside the window",
175
+ input: "Only let them read if they logged in within the last hour",
176
+ output: `whenTemporal: [dogwood.formerly("1h", dogwood.predicate('Drupe::Action::"Login"', "response"))]`,
177
+ },
178
+ {
179
+ title: "The macro caveat",
180
+ input: "Use count_within for a rate limit",
181
+ output:
182
+ "count_within is a default-library macro a --macros caller replaces — emit the call and ship the definition, or build it from `count for … where`",
183
+ },
184
+ ],
185
+ },
156
186
  ]),
157
187
 
158
188
  initTemplates(template?: string): InitTemplateSet {
@@ -0,0 +1,327 @@
1
+ ---
2
+ skill: chant-cedar-dogwood
3
+ description: Author dogwood temporal policies with the cedar lexicon's typed builders — the parser primitives, the macro caveat, the validation split, AgentCore embedding, and replay
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Temporal Policies with the Dogwood Dialect
8
+
9
+ ## Read this part first
10
+
11
+ Dogwood is pre-release, and the framing matters more than usual because it
12
+ changes what you should write.
13
+
14
+ Upstream (`dogwood-policy/dogwood`, Apache-2.0) says on its own README, in
15
+ bold, that it is **not intended for production use**. It is a reference
16
+ interpreter with enumerated gaps: no event timestamp validation, no event
17
+ authentication, no trace durability, unsandboxed Rhai in providers, no audit
18
+ logging, no multi-tenancy isolation, and an `http_get` provider with no SSRF
19
+ protection.
20
+
21
+ It also has no versioning story at all. Zero tags, zero releases, no changelog.
22
+ The crate reports `1.0.0` because it is published to an Amazon-internal
23
+ registry, and every future sync will also report `1.0.0`. The repository is a
24
+ read-only mirror: content arrives as squashed "Sync from internal source"
25
+ commits from a publish bot, against a repository nobody outside Amazon can see,
26
+ at a cadence of roughly one sync every three days over its public life.
27
+
28
+ chant pins a git SHA plus the blob hashes of seven files in
29
+ `src/dogwood/upstream.ts`. Do not tell a user this surface is stable. Do tell
30
+ them what is pinned and what a sync can move.
31
+
32
+ ## The one distinction to get right
33
+
34
+ **`formerly`, `previous`, `since`, `exists`, `tp()`, `count for … where` and
35
+ `sum … for … where` are the parser primitives.** They are the entire temporal
36
+ keyword set in upstream's grammar, and they are what the typed builders target.
37
+
38
+ **`count_within`, `sum_within`, `count_distinct_within` and `bind` are macros**
39
+ in a swappable default library (`configuration/default_macros.dw`). A caller
40
+ who passes `--macros` replaces that library wholesale, so a policy built on
41
+ them is built on an assumption about the far end.
42
+
43
+ **`once` does not ship at all.** It appears in upstream's examples as an
44
+ ordinary user-defined macro and is in no library. (The grammar rule behind
45
+ `formerly` is internally named `once_op`, which is where the confusion comes
46
+ from.) If a user asks for `once`, define it as a macro or use `formerly`
47
+ directly — do not emit a bare `once(…)` call and hope.
48
+
49
+ So: the four aggregates are expressible as **calls**
50
+ (`dogwood.countWithin(…)`), never as operators. A missing macro at the far end
51
+ is a comprehensible error. A builder that emits a name the callee's library
52
+ does not define is a mystery.
53
+
54
+ The way to stop assuming is to ship the definitions:
55
+
56
+ ```typescript
57
+ import { TemporalMacroLibrary, dogwood } from "@intentius/chant-lexicon-cedar";
58
+
59
+ // Upstream's four definitions, verbatim, into the project's own macros.dw.
60
+ export const macros = new TemporalMacroLibrary({ macros: dogwood.defaultMacroLibrary() });
61
+ ```
62
+
63
+ With `inline: true` they go at the top of `policies.dw` instead, and a policy
64
+ set's own `def` shadows a same-named library macro — the strongest form,
65
+ because the definitions travel with the policies.
66
+
67
+ ## Authoring
68
+
69
+ ```typescript
70
+ import { TemporalPolicy, TemporalEventSchema, dogwood } from "@intentius/chant-lexicon-cedar";
71
+
72
+ export const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema() });
73
+
74
+ export const readAfterLogin = new TemporalPolicy({
75
+ annotations: { id: "read_after_login" },
76
+ action: { eq: 'Drupe::Action::"Read"' },
77
+ whenTemporal: [
78
+ dogwood.formerly(
79
+ "1h",
80
+ dogwood.predicate('Drupe::Action::"Login"', "response", {
81
+ "input.user": dogwood.ctx("input.user"),
82
+ }),
83
+ ),
84
+ ],
85
+ });
86
+ ```
87
+
88
+ Emits:
89
+
90
+ ```
91
+ @id("read_after_login")
92
+ permit (
93
+ principal,
94
+ action == Drupe::Action::"Read",
95
+ resource
96
+ )
97
+ when temporal {
98
+ formerly within 1h Drupe::Action::"Login"::response{ input.user: context.input.user }
99
+ };
100
+ ```
101
+
102
+ ### Clause forms
103
+
104
+ | Prop | Emits |
105
+ |---|---|
106
+ | `when` / `unless` | `when { … }` — Cedar expression strings, same as `Cedar::Policy` |
107
+ | `whenGuardrails` / `unlessGuardrails` | `when guardrails { … }` — a Cedar expression with a tag upstream discards when lowering |
108
+ | `whenTemporal` / `unlessTemporal` | `when temporal { … }` — the temporal sub-language |
109
+
110
+ ### Windows
111
+
112
+ `formerly`, `previous` and `since` all take a mandatory window: an integer and
113
+ one of `s`, `m`, `h`, `d`. Nothing else parses. The builders take it as an
114
+ argument, so a windowless operator is unrepresentable — do not reach for
115
+ `dogwood.raw()` to work around a window you do not want, because there is no
116
+ such form.
117
+
118
+ Every operator is past-only. There is no future operator and no way to write
119
+ one. A user asking for "deny if X happens later" needs a different design.
120
+
121
+ ### Terms
122
+
123
+ Numbers and booleans lift. A bare string does not, on purpose: `"alice"` is a
124
+ Cedar string literal and `alice` is a binder reference, and guessing is how a
125
+ policy silently stops matching. Say which:
126
+
127
+ | Builder | Renders |
128
+ |---|---|
129
+ | `dogwood.str("alice")` | `"alice"` |
130
+ | `dogwood.varRef("a")` | `a` |
131
+ | `dogwood.ctx("input.user")` | `context.input.user` |
132
+ | `dogwood.scopeRef("principal", "dept")` | `principal.dept` |
133
+ | `dogwood.entityUid('Drupe::OAuthUser::"alice"')` | `Drupe::OAuthUser::"alice"` |
134
+
135
+ ### An aggregate, from the primitive
136
+
137
+ ```typescript
138
+ export const rateLimited = new TemporalPolicy({
139
+ annotations: { id: "rate_limited" },
140
+ action: { eq: 'Drupe::Action::"Transfer"' },
141
+ whenTemporal: [
142
+ dogwood.compare(
143
+ dogwood.count(
144
+ [dogwood.typedBinder("t", "Timepoint")],
145
+ dogwood.formerly(
146
+ "15m",
147
+ dogwood.and(dogwood.predicate('Drupe::Action::"Transfer"', "request"), dogwood.tp("t")),
148
+ ),
149
+ ),
150
+ "<",
151
+ 5,
152
+ ),
153
+ ],
154
+ });
155
+ ```
156
+
157
+ That is what `count_within` expands to. Writing it directly costs four lines
158
+ and depends on nothing at the far end.
159
+
160
+ ## The event schema, and the pin
161
+
162
+ `.dwschema` is the optional service half of the schema; the required half is
163
+ the Cedar `.cedarschema` the rest of the lexicon already generates from. They
164
+ are two halves of one schema, not competing formats.
165
+
166
+ The default event schema pins `callerPrincipal = principal` on every kind, so
167
+ every temporal predicate is correlated to the deciding request's principal and
168
+ other principals' events are invisible.
169
+
170
+ **Supplying any event schema opts out of upstream's default wholesale.** A
171
+ schema emitted without a pin does not merely fail to add a correlation — it
172
+ removes one the author probably assumed, and every predicate in the set starts
173
+ matching across principals. `dogwood.defaultEventSchema()` carries the pin;
174
+ dropping it is a named argument that stamps a comment into the emitted file:
175
+
176
+ ```typescript
177
+ dogwood.defaultEventSchema({ pinCallerPrincipal: false });
178
+ ```
179
+
180
+ DWDS010 warns about an unpinned emitted schema. That is report-only — chant
181
+ does not know which the author wanted, only that the choice should be visible
182
+ in a diff.
183
+
184
+ `max_window` caps look-back for the whole set; absent, it is 24h — the same 24h
185
+ that applies when no schema is supplied at all.
186
+
187
+ ## Deploying to AgentCore
188
+
189
+ `AWS::BedrockAgentCore::Policy` is the vehicle. Its `Definition` is a two-arm
190
+ `oneOf` — `Cedar.Statement` for plain Cedar, `Policy.Statement` for anything
191
+ else — and a `.dw` policy travels in the second arm.
192
+
193
+ ```typescript
194
+ import { agentCoreStagedPolicy } from "@intentius/chant-lexicon-cedar";
195
+
196
+ new BedrockAgentCorePolicy({
197
+ PolicyEngineId: engine.ref(),
198
+ ...agentCoreStagedPolicy("writeNeedsApproval", writeNeedsApproval, "log-only"),
199
+ });
200
+ ```
201
+
202
+ `agentCorePolicyDefinition(name, policy)` picks the arm from the policy itself.
203
+ Templates are refused — AgentCore's union has no template-linked arm, so a
204
+ statement carrying `?principal` or `?resource` throws at authoring time rather
205
+ than failing at deploy.
206
+
207
+ `EnforcementMode` stages the rollout: `"log-only"` is evaluated on every request
208
+ with its decision observed rather than returned, `"enforce"` binds. Start a
209
+ temporal policy log-only — whether it fires depends on traffic nobody has
210
+ replayed. Promotion is a one-token diff.
211
+
212
+ The resource carries a statement and nothing else, so the event schema is
213
+ registered with the engine separately; DWDC013 warns when temporal text is
214
+ embedded and no `.dwschema` was emitted beside it.
215
+
216
+ ## The validation split
217
+
218
+ Explain this split when a user asks why something did or did not fail.
219
+
220
+ Always, gating, no binary anywhere:
221
+
222
+ | Check | Catches |
223
+ |---|---|
224
+ | DWDC010 | A temporal predicate naming an event kind the emitted `.dwschema` never declares |
225
+ | DWDC011 | A window past `max_window` (or upstream's 24h default) — macro-call intervals count |
226
+ | DWDC012 | A `formerly`/`previous`/`since` with no `within` window |
227
+ | DWDC013 | An AgentCore-embedded temporal statement with no `.dwschema` emitted beside it (warning) |
228
+ | DWDS010 | An emitted event schema that pins nothing (warning) |
229
+
230
+ Only when the `dogwood` binary is present:
231
+
232
+ | Check | Catches |
233
+ |---|---|
234
+ | DWDE010 | Everything `dogwood validate` catches — macro expansion, the temporal type check, the Cedar body against the action schema |
235
+ | DWDE011 | The `dogwood lower` output failing Cedar's own validator via `@cedar-policy/cedar-wasm` |
236
+
237
+ Binary resolution order: an explicit `configureDogwoodCli({ binary })`, then
238
+ `$CHANT_DOGWOOD_BINARY`, then `cedar.dogwood.binary` in a `chant.config.json`,
239
+ then `dogwood` on `PATH`. There is no published build — it is `cargo build
240
+ --release` from the pinned revision.
241
+
242
+ When it is absent, DWDE010 emits exactly one `info` finding naming the binary,
243
+ where chant looked, and the issue. It never silently passes: a check that
244
+ quietly succeeds when it could not run is claiming a guarantee it did not make.
245
+
246
+ Nothing in gating CI runs the binary, by design.
247
+
248
+ ### Do not read exit codes
249
+
250
+ If you write anything that shells to the CLI: exit 2 means both "policy set
251
+ rejected" and clap's usage error for an unknown flag, and upstream's own guide
252
+ claims 1 for the latter. The JSON on stdout decides, in both directions. There
253
+ are two JSON shapes — a validate report (`passed`, `errors[]`, `warnings[]`)
254
+ and a bare fatal error object with the same diagnostic fields flattened plus
255
+ `related[]`. A run that produced neither is "could not be used", never "your
256
+ policy is bad".
257
+
258
+ ## Replay
259
+
260
+ A temporal policy is not decidable from its source — whether `formerly within
261
+ 1h …` fires depends on history nobody has replayed — so the check is a replay
262
+ against recorded traffic. That ships as `PolicyReplayOp`:
263
+
264
+ ```typescript
265
+ import { PolicyReplayOp } from "@intentius/chant-lexicon-cedar";
266
+
267
+ export const { op } = PolicyReplayOp({
268
+ name: "policy-replay",
269
+ policiesPath: "dist/policies.dw",
270
+ policySchemaPath: "schema.cedarschema",
271
+ eventSchemaPath: "dist/events.dwschema",
272
+ tracePath: "trace/read-after-login.log",
273
+ expect: [
274
+ { timestamp: 10, verdict: "allow", determiningRules: [0] },
275
+ { timestamp: 7200, verdict: "deny", note: "the login is two hours stale" },
276
+ ],
277
+ onFinding: "report",
278
+ });
279
+ ```
280
+
281
+ Three phases — Artifacts (`chantBuild`, skippable with `buildScript: false`),
282
+ Replay (`dogwoodReplay`, writes `dist/dogwood-replay.json`), Report
283
+ (`dogwoodReplayReport`, acts on `report | issue | pull-request`).
284
+ `failOnDivergence` defaults to false: an observe-dial Op reports. The composite
285
+ ships from cedar and carries no dependency on the temporal lexicon; a scheduled
286
+ form is a project-side `TemporalSchedule` pairing.
287
+
288
+ Build traces with `dogwood.traceEvent()` rather than by hand. Two traps it
289
+ exists to close, and both are worth naming whenever a user assembles a fixture:
290
+
291
+ 1. **Both bags.** A trace line carries a `request_context(...)` envelope, which
292
+ the Cedar request is built from, and a trailing logged record, which temporal
293
+ predicates match against. A field both halves need must be in both.
294
+ `traceEvent()` writes a `context` group into both by default; getting the
295
+ weaker trace takes an explicit `bags: "record-only"` / `"context-only"`.
296
+ Populate one bag only and nothing errors — the other check silently weakens.
297
+ 2. **Fully-qualified action names.** `Drupe::Action::"Transfer"`, never
298
+ `Transfer`. A bare name still authorizes through Cedar while every temporal
299
+ predicate quietly fails to match. `traceEvent()` rejects one at construction.
300
+
301
+ `dogwood.auditTrace(events)` applies the same checks to a trace chant did not
302
+ build (`single-bag`, `no-request-context`, `empty-record`, `out-of-order`), and
303
+ `dogwood.traceFixture(events)` throws rather than handing back a weakened one.
304
+
305
+ Write expectations against `timestamp`, not `index`: `index` is the position in
306
+ the decision stream, and a history-only event (any kind the schema does not
307
+ mark `decision`) contributes history and no verdict, so the two do not line up.
308
+ `determiningRules` catches a decision that is right for the wrong reason.
309
+
310
+ Replay exits 0 even when every verdict is DENY, so the activity reads the JSON
311
+ rather than the exit status — and a run that could not happen throws instead of
312
+ reporting zero divergences.
313
+
314
+ ## What chant does not do
315
+
316
+ - **No lowering.** `dogwood lower` owns that; a reimplementation would drift on
317
+ the next sync. Where the lowered form is wanted, chant shells to the binary.
318
+ - **No request-time evaluation.** The policy engine in front of the traffic
319
+ decides; chant has no seat there. The offline half is `PolicyReplayOp`, which
320
+ replays recorded history through upstream's interpreter.
321
+ - **No gate.** Dogwood is a target, the same as Cedar. Organizational policy in
322
+ chant is TypeScript post-synth checks.
323
+
324
+ ## Reference
325
+
326
+ Full pages: `/chant/lexicons/cedar/dogwood/`, and from there temporal policies,
327
+ event schemas, validation and replay.