@intentius/chant-lexicon-cedar 0.44.13 → 0.45.0
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.
- package/README.md +13 -4
- package/dist/avp/client.d.ts +18 -0
- package/dist/avp/client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +5 -9
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/generate.d.ts +35 -8
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/commands.d.ts +14 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts +38 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts +1 -1
- package/dist/integrity.json +4 -4
- package/dist/lint/post-synth/cede010.d.ts +5 -4
- package/dist/lint/post-synth/cede010.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/cede010.ts +5 -4
- package/dist/schema-artifact.d.ts +50 -0
- package/dist/schema-artifact.d.ts.map +1 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-cedar-authoring.md +9 -3
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/avp/OWNERSHIP.md +4 -1
- package/src/avp/client.test.ts +35 -0
- package/src/avp/client.ts +29 -2
- package/src/codegen/docs.ts +5 -799
- package/src/codegen/generate-cli.ts +8 -6
- package/src/codegen/generate.ts +63 -14
- package/src/codegen/output-dir.test.ts +137 -0
- package/src/commands.test.ts +93 -0
- package/src/commands.ts +95 -0
- package/src/config.ts +50 -4
- package/src/index.ts +10 -2
- package/src/init-templates.test.ts +22 -4
- package/src/init-templates.ts +16 -16
- package/src/lint/post-synth/cede010.ts +5 -4
- package/src/plugin.ts +33 -8
- package/src/schema-artifact.test.ts +160 -0
- package/src/schema-artifact.ts +87 -0
- package/src/serializer.ts +17 -1
- package/src/skills/chant-cedar-authoring.md +9 -3
- package/src/spec/fetch.ts +23 -2
- package/dist/codegen/docs-dogwood.d.ts +0 -21
- package/dist/codegen/docs-dogwood.d.ts.map +0 -1
- package/src/codegen/docs-dogwood.ts +0 -1119
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The dogwood dialect's docs pages (#1662, epic #1646).
|
|
3
|
-
*
|
|
4
|
-
* Separate from `./docs.ts` because the dialect is a surface inside this
|
|
5
|
-
* lexicon rather than a lexicon of its own: its pages sit in one sidebar group
|
|
6
|
-
* and get written and reviewed together, while `docs.ts` stays the cedar
|
|
7
|
-
* site's own shape. They are wired in as `extraPages` with `sidebar: false`
|
|
8
|
-
* plus a `sidebarExtra` group, so every page is reachable — Starlight does not
|
|
9
|
-
* auto-discover, and a page no sidebar entry points at exists only for whoever
|
|
10
|
-
* types the URL (#1312).
|
|
11
|
-
*
|
|
12
|
-
* The facts here come from the #1657 upstream verification and from this
|
|
13
|
-
* lexicon's own `src/dogwood/`. Where the two ever disagree the code wins: the
|
|
14
|
-
* builders are what a reader will actually run.
|
|
15
|
-
*/
|
|
16
|
-
export declare const dogwoodOverview = "[Dogwood](https://github.com/dogwood-policy/dogwood) is Cedar with temporal\noperators. A policy can ask what already happened in a session \u2014 was there a\nlogin in the last hour, how much has been transferred in the last fifteen\nminutes, has anything touched a classified document since the session started \u2014\nso approval-before-action, rate limits and budgets become policy instead of\napplication code.\n\nA `.dw` file is a Cedar policy with extra clause forms. Its head is Cedar's,\nbyte for byte, and its action schema is an ordinary `.cedarschema`.\n\n```\n@id(\"read_after_login\")\npermit (\n principal,\n action == Drupe::Action::\"Read\",\n resource\n)\nwhen temporal {\n formerly within 1h Drupe::Action::\"Login\"::response{ input.user: context.input.user }\n};\n```\n\n## Pre-release, and what that means here\n\nUpstream calls itself a reference interpreter and says, in bold on its own\nREADME, that it is **not intended for production use**. The gaps it enumerates:\nno event timestamp validation, no event authentication, no trace durability,\nunsandboxed Rhai in providers, no audit logging, no multi-tenancy isolation,\nand an `http_get` provider with no SSRF protection.\n\nMost of those are a runtime consumer's problem rather than chant's \u2014 chant's\nhalf is authoring, serialization and the walls, and evaluation stays with\nBedrock AgentCore Policy or whatever engine reads the emitted files. The part\nthat *is* chant's problem is that the language surface can move underneath the\ntyped builders, which is the next section.\n\n## How upstream is governed\n\nThere is no versioning story, and that is a finding rather than a complaint.\n\n| Question | Answer at the pinned revision |\n|---|---|\n| Tags | None |\n| GitHub releases | None |\n| Changelog | None |\n| Crate version | `1.0.0`, declared `publish = [\"brazil\"]` \u2014 an Amazon-internal registry, not crates.io |\n| Contributions | CONTRIBUTING declares the repo a read-only mirror, not accepting external PRs, not using GitHub issues |\n| Stability statement | Nowhere in README, CONTRIBUTING, SECURITY or the guide |\n\nEvery content change arrives as one squashed `Sync from internal source`\ncommit from a publish bot, authored against a repository nobody outside Amazon\ncan see. Over the repo's public life the cadence has been roughly one sync\nevery three days. A sync is a wholesale tree replacement, so it can retune the\ngrammar, rename a JSON field or swap the default macro library in a single\ncommit, and the crate will report `1.0.0` either way.\n\nSo a chant version gate cannot key off anything upstream publishes. What\n`src/dogwood/upstream.ts` records instead is a git SHA plus the blob hashes of\nseven files \u2014 three `.pest` grammars, the default macro library, and the three\n`dogwood-cli/src` files whose report structs are the JSON contract. The whole\ntree hash moves on docs-only syncs, which makes it too noisy to gate on.\n\nThree consequences run through everything else on these pages:\n\n- The typed builders target the **parser primitives**, never the named\n aggregates, because the aggregates live in a file a sync can edit and a\n caller can replace. See [Temporal Policies](../dogwood-temporal-policies/).\n- The CLI's **JSON report structs** are the integration surface, never its\n human text, because the human renderer is the likelier thing to get\n cosmetically retuned. See [Validation](../dogwood-validation/).\n- **Nothing in gating CI runs the binary.** Full `.dw` validation is a\n CLI-gated check that says out loud when it did not run.\n\n## A dialect, not a sibling lexicon\n\nk3s beside k3d and forgejo beside github are parallel peers with separate\nupstreams. Dogwood is not a peer: it embeds Cedar, a `.dw` file stripped of\nCedar semantics is meaningless, and the expensive machinery \u2014 schema codegen,\ntyped entity and action classes, meta-policy lint \u2014 is shared verbatim. It\nships as a surface inside this lexicon, with its checks under the `DWD` id\nfamily declared on the serializer's `extraRulePrefixes`.\n\n## Quick start\n\n```typescript\nimport { TemporalPolicy, TemporalEventSchema, dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nexport const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema() });\n\nexport const readAfterLogin = new TemporalPolicy({\n annotations: { id: \"read_after_login\" },\n action: { eq: 'Drupe::Action::\"Read\"' },\n whenTemporal: [\n dogwood.formerly(\n \"1h\",\n dogwood.predicate('Drupe::Action::\"Login\"', \"response\", {\n \"input.user\": dogwood.ctx(\"input.user\"),\n }),\n ),\n ],\n});\n```\n\n## What comes out\n\n| File | What reads it |\n|---|---|\n| `policies.dw` | `dogwood validate` / `lower` / `replay` |\n| `events.dwschema` | `dogwood --event-schema` \u2014 the service half of the schema |\n| `macros.dw` | `dogwood --macros` \u2014 a macro library, when one is declared non-inline |\n| `<name>.cedar`, `policies.cedar.json` | The plain-Cedar half of the same policy set, unchanged |\n\nA build with no temporal policies emits none of the first three and behaves\nexactly as it did before. A build with both halves emits both from one pass,\nwith policy ids derived the same way on each leg.\n\n## Deploying it: AgentCore\n\n`AWS::BedrockAgentCore::Policy` \u2014 generated by the\n[aws lexicon](/chant/lexicons/aws/) \u2014 is where a temporal policy is actually\ndeployed. Its `Definition` is a two-arm `oneOf`: `Cedar.Statement` for plain\nCedar, `Policy.Statement` for anything else. The second arm is what a `.dw`\npolicy travels in, and it is why the epic picked AgentCore as the target.\n\n```typescript\nimport { agentCoreStagedPolicy } from \"@intentius/chant-lexicon-cedar\";\n\nnew BedrockAgentCorePolicy({\n PolicyEngineId: engine.ref(),\n ...agentCoreStagedPolicy(\"writeNeedsApproval\", writeNeedsApproval, \"log-only\"),\n});\n```\n\n`agentCorePolicyDefinition(name, policy)` picks the arm from the policy itself:\na `TemporalPolicy`, or any props carrying a temporal clause, goes to `Policy`;\nplain Cedar goes to `Cedar`. Nothing in the cedar lexicon imports the aws one \u2014\nthe seam is the data shape, the same rule the AVP embedding follows.\n\n`EnforcementMode` is the staging dial, and a temporal rule is the case that\nneeds it most: `LOG_ONLY` is evaluated on every request with its decision\nobserved rather than returned, so a policy whose behaviour depends on unreplayed\ntraffic can be watched before it binds. Promotion is one token, `\"log-only\"` to\n`\"enforce\"`.\n\nThe resource carries a statement and nothing else, so the event schema has\nnowhere to live in it and is registered with the engine separately. DWDC013\nwarns when a build embeds temporal text and emits no `.dwschema` beside it,\nbecause a deployed statement whose event kinds nobody registered matches\nnothing and stops doing its job without failing.\n\nThe worked example is `lexicons/cedar/examples/agentcore-policy`.\n\n## What chant does not do\n\nchant does not lower. `dogwood lower` compiles a `.dw` set to plain Cedar with\nthe temporal conditions hoisted into `context.*` slots; that is upstream's\nsemantics to own, and a reimplementation would drift the first time a sync\nchanged it. Where the lowered form is wanted, chant shells to the binary.\n\nchant does not evaluate at request time. Temporal decisions are made by the\npolicy engine in front of the traffic, and chant has no seat there. What chant\ndoes have is the offline half: `PolicyReplayOp` replays a declared set against\nrecorded history through upstream's own interpreter and reports where the\nverdicts diverged from what the policy set was supposed to decide.\n\n## The pages\n\n- [Temporal Policies](../dogwood-temporal-policies/) \u2014 the builders, the\n operators, and which of them are macros\n- [Event Schemas](../dogwood-event-schemas/) \u2014 the `.dwschema` surface and the\n `callerPrincipal` pin\n- [Validation](../dogwood-validation/) \u2014 which checks always run and which need\n the binary\n- [Replay](../dogwood-replay/) \u2014 typed traces, `PolicyReplayOp`, and the trap\n that makes half a trace pass silently\n";
|
|
17
|
-
export declare const dogwoodTemporalPolicies = "A `Dogwood::TemporalPolicy` is a `Cedar::Policy` with three extra clause\nforms. Upstream's policy grammar differs from Cedar's in exactly one rule:\n\n```\ncond = { cond_kw ~ (extension_marker | guardrails_tag? ~ \"{\" ~ expr ~ \"}\") }\n```\n\n| Prop | Emits | What it is |\n|---|---|---|\n| `when` / `unless` | `when { \u2026 }` | Ordinary Cedar expression strings, same as `Cedar::Policy` |\n| `whenGuardrails` / `unlessGuardrails` | `when guardrails { \u2026 }` | A Cedar expression with a tag upstream discards when lowering. It marks a clause for a reader; it does not change what the policy means |\n| `whenTemporal` / `unlessTemporal` | `when temporal { \u2026 }` | The temporal sub-language, dispatched to a different parser |\n\nClause order in the emitted file is fixed \u2014 every `when` form, then every\n`unless` form \u2014 rather than taken from the author. Conditions are a\nconjunction, so order carries no meaning, and fixing it means a policy that\ngains a temporal clause does not reshuffle the clauses already there.\n\n## The primitives\n\nThese seven are the whole temporal keyword set in upstream's\n`extension/temporal/grammar.pest`. Everything else you will read about dogwood\nis built out of them.\n\n| Builder | Renders | Notes |\n|---|---|---|\n| `formerly(w, \u03C6)` | `formerly within 1h \u03C6` | \u03C6 held at some point in the window |\n| `previous(w, \u03C6)` | `previous within 30s \u03C6` | \u03C6 held at the immediately preceding timepoint in the window |\n| `since(\u03C6, w, \u03C8)` | `\u03C6 since within 1h \u03C8` | Infix; \u03C6 has held continuously since \u03C8 |\n| `exists(binder, \u03C6)` | `exists (total: Long). \u03C6` | Binds a value for the body to compare |\n| `tp(binder)` | `tp(t)` | Binds the timepoint under evaluation |\n| `count(binders, \u03C6)` | `count for (t: Timepoint). where \u03C6` | The aggregation domain is mandatory |\n| `sum(over, binders, \u03C6)` | `sum a for (a: Long), (t: Timepoint). where \u03C6` | `over` names the summed variable |\n\nPlus `and`, `not`, `compare`, and `predicate` for an event head.\n\nTwo properties of the operator set are worth stating plainly. All of them are\n**past-only** \u2014 there is no future operator, and no way to write one. And\n`formerly`, `previous` and `since` all carry a **mandatory window**: an\ninteger and one of `s`, `m`, `h`, `d`, and nothing else. The builders take the\nwindow as an argument, so a windowless operator has nowhere to live; DWDC012\ncatches the ones that arrive by other routes.\n\n## Four policies\n\nApproval before action:\n\n```typescript\nimport { TemporalPolicy, dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nexport const readAfterLogin = new TemporalPolicy({\n annotations: { id: \"read_after_login\" },\n action: { eq: 'Drupe::Action::\"Read\"' },\n whenTemporal: [\n dogwood.formerly(\n \"1h\",\n dogwood.predicate('Drupe::Action::\"Login\"', \"response\", {\n \"input.user\": dogwood.ctx(\"input.user\"),\n }),\n ),\n ],\n});\n```\n\nA rate limit, from the `count` primitive:\n\n```typescript\nexport const rateLimited = new TemporalPolicy({\n annotations: { id: \"rate_limited\" },\n action: { eq: 'Drupe::Action::\"Transfer\"' },\n whenTemporal: [\n dogwood.compare(\n dogwood.count(\n [dogwood.typedBinder(\"t\", \"Timepoint\")],\n dogwood.formerly(\n \"15m\",\n dogwood.and(dogwood.predicate('Drupe::Action::\"Transfer\"', \"request\"), dogwood.tp(\"t\")),\n ),\n ),\n \"<\",\n 5,\n ),\n ],\n});\n```\n\nA budget, from `sum` behind a macro, with `exists` naming the total:\n\n```typescript\nimport { TemporalMacroLibrary, TemporalPolicy, dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nconst sumFormerly = dogwood.defTemporalMacro(\n \"sum_formerly\",\n [\"?a\", \"?w\", \"?body\"],\n dogwood.sum(\n \"?a\",\n [dogwood.typedBinder(\"?a\", \"Long\"), dogwood.typedBinder(\"$t\", \"Timepoint\")],\n dogwood.formerly(\n dogwood.macroWindow(\"?w\"),\n dogwood.and(dogwood.macroCondition(\"?body\"), dogwood.tp(\"$t\")),\n ),\n ),\n \"Sums the numeric value `?a` over occurrences of `?body` within window `?w`.\",\n);\n\nexport const library = new TemporalMacroLibrary({ macros: [sumFormerly], inline: true });\n\nexport const transferBudget = new TemporalPolicy({\n annotations: { id: \"transfer_sum_over_100\" },\n action: { eq: 'Drupe::Action::\"Alert\"' },\n whenTemporal: [\n dogwood.exists(\n dogwood.typedBinder(\"total\", \"Long\"),\n dogwood.and(\n dogwood.compare(\n dogwood.call(\"sum_formerly\", [\n dogwood.varRef(\"a\"),\n dogwood.interval(\"1h\"),\n dogwood.predicate('Drupe::Action::\"Transfer\"', \"request\", {\n \"input.user\": dogwood.varRef(\"_\"),\n \"input.amount\": dogwood.varRef(\"a\"),\n }),\n ]),\n \"==\",\n dogwood.varRef(\"total\"),\n ),\n dogwood.compare(dogwood.varRef(\"total\"), \">\", 100),\n ),\n ),\n ],\n});\n```\n\nSequencing, with a guardrail and a break-glass exemption:\n\n```typescript\nexport const noToolAfterSensitiveRead = new TemporalPolicy({\n effect: \"forbid\",\n annotations: { id: \"no_tool_after_sensitive_read\" },\n action: { eq: 'Drupe::Action::\"Invoke\"' },\n whenGuardrails: ['context.input.tool != \"audit\"'],\n whenTemporal: [\n dogwood.since(\n dogwood.predicate('Drupe::Action::\"Read\"', \"response\", {\n \"output.classification\": dogwood.varRef(\"c\"),\n }),\n \"30m\",\n dogwood.predicate('Drupe::Action::\"Login\"', \"request\"),\n ),\n ],\n unless: ['principal in Drupe::Group::\"breakglass\"'],\n});\n```\n\nThose four emit this, and the golden test in `src/dogwood/serialize.test.ts`\npins it byte for byte:\n\n```\n// Sums the numeric value `?a` over occurrences of `?body` within window `?w`.\ndef temporal sum_formerly(?a, ?w, ?body) {\n sum ?a for (?a: Long), ($t: Timepoint). where formerly within ?w (?body && tp($t))\n};\n\n@id(\"read_after_login\")\npermit (\n principal,\n action == Drupe::Action::\"Read\",\n resource\n)\nwhen temporal {\n formerly within 1h Drupe::Action::\"Login\"::response{ input.user: context.input.user }\n};\n\n@id(\"transfer_sum_over_100\")\npermit (\n principal,\n action == Drupe::Action::\"Alert\",\n resource\n)\nwhen temporal {\n exists (total: Long). (sum_formerly(a, 1h, Drupe::Action::\"Transfer\"::request{ input.user: _, input.amount: a })) == total && total > 100\n};\n\n@id(\"no_tool_after_sensitive_read\")\nforbid (\n principal,\n action == Drupe::Action::\"Invoke\",\n resource\n)\nwhen guardrails { context.input.tool != \"audit\" }\nwhen temporal {\n Drupe::Action::\"Read\"::response{ output.classification: c } since within 30m Drupe::Action::\"Login\"::request{}\n}\nunless { principal in Drupe::Group::\"breakglass\" };\n\n@id(\"rate_limited\")\npermit (\n principal,\n action == Drupe::Action::\"Transfer\",\n resource\n)\nwhen temporal {\n (count for (t: Timepoint). where formerly within 15m (Drupe::Action::\"Transfer\"::request{} && tp(t))) < 5\n};\n```\n\n## Primitives versus macros\n\nThis is the distinction to get right, and the reason the builder list above is\nshorter than most write-ups of dogwood.\n\n`count_within`, `sum_within` and `count_distinct_within` are **not**\noperators. They are macros defined in\n`dogwood-language/configuration/default_macros.dw`, alongside `bind`:\n\n```\ndef temporal count_within(?w, ?s) {\n count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))\n};\n```\n\n`once` is not even that. It appears in upstream's examples as an ordinary\nuser-defined macro and ships in no library at all. (The grammar rule behind\n`formerly` is internally named `once_op`, which is where the confusion\nstarts.) If you want `once`, define it \u2014 chant will not pretend it exists.\n\nA caller who passes `--macros` replaces the entire default library, so a\npolicy built on `count_within` is a policy built on an assumption about the\nfar end. chant therefore exposes the four as **calls**:\n\n```typescript\ndogwood.countWithin(\"15m\", dogwood.predicate('Drupe::Action::\"Transfer\"', \"request\"));\n// count_within(15m, Drupe::Action::\"Transfer\"::request{})\n```\n\nA call that resolves to nothing at the other end is a missing-macro error,\nwhich is comprehensible. A first-class builder emitting a name the callee's\nlibrary does not define would be a mystery.\n\nThe way to stop assuming is to emit the definitions yourself:\n\n```typescript\nimport { TemporalMacroLibrary, dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nexport const macros = new TemporalMacroLibrary({ macros: dogwood.defaultMacroLibrary() });\n```\n\nThat writes `macros.dw` with upstream's four definitions verbatim. With\n`inline: true` they go at the top of `policies.dw` instead, and a policy set's\nown `def` shadows a same-named library macro \u2014 which makes inlining the\nstrongest form: the definitions travel with the policies and win over whatever\n`--macros` the caller supplies.\n\n## Writing macros\n\n```typescript\ndogwood.defTemporalMacro(\"once\", [\"?w\", \"?s\"], dogwood.formerly(dogwood.macroWindow(\"?w\"), dogwood.macroCondition(\"?s\")));\ndogwood.defCedarMacro(\"is_small\", [\"?n\"], \"?n < 100\");\n```\n\nTwo sigils, and they are not interchangeable:\n\n- `?p` splices the call-site argument literally. Build one with\n `macroWindow(\"?w\")` in window position, `macroCondition(\"?s\")` in condition\n position, `macroTerm(\"?a\")` in term position.\n- `$t` is a fresh binder the macro introduces, gensym'd at every expansion.\n\nBoth are legal only inside a macro body; upstream's well-formedness pass\nrejects them anywhere else, so the builders validate them at definition time.\n\nA call site supplies a window as a **bare interval** \u2014 `once(1h, \u2026)`, no\n`within` \u2014 because the keyword belongs to the operator and stays in the body.\nThat is `dogwood.interval(\"1h\")`.\n\n## Terms\n\nNumbers and booleans lift to literals. A bare string does not, and that is\ndeliberate: `\"alice\"` is a Cedar string literal and `alice` is a binder\nreference, and guessing which one was meant is how a policy silently stops\nmatching.\n\n| Builder | Renders |\n|---|---|\n| `str(\"alice\")` | `\"alice\"` |\n| `varRef(\"a\")` | `a` |\n| `ctx(\"input.user\")` | `context.input.user` |\n| `scopeRef(\"principal\", \"dept\")` | `principal.dept` |\n| `entityUid('Drupe::OAuthUser::\"alice\"')` | `Drupe::OAuthUser::\"alice\"` |\n| `decimalOf(\"1.50\")` | `decimal(\"1.50\")` |\n| `arrayOf(1, 2)` | `[1, 2]` |\n| `wildcard()` | `*` |\n\n## Precedence, and who handles it\n\nThe renderer parenthesises rather than relying on the reader knowing the\ngrammar. `!` binds tighter than `since` and `&&`, so `!a since within W b`\nnegates only `a`; an aggregate's `where` body is greedy, so\n`count for (\u2026). where \u03C6 == 3` would read `== 3` as part of \u03C6. Aggregates and\nmacro calls in comparison position are wrapped on both sides, and `exists`\nbinds maximally to the right so it is wrapped inside an `&&` chain.\n\n## The escape hatch\n\n`dogwood.raw(\"formerly within 1h \u2026\")` emits temporal source verbatim. It is\nthe one builder that can produce something the walls exist to catch, which is\nwhy the walls read the serialized text rather than the in-memory tree \u2014 see\n[Validation](../dogwood-validation/).\n\n## Next\n\n- [Event Schemas](../dogwood-event-schemas/) \u2014 what the `request`/`response`\n kinds in those predicates come from\n- [Validation](../dogwood-validation/) \u2014 what checks a policy set before it\n leaves the build\n";
|
|
18
|
-
export declare const dogwoodEventSchemas = "A dogwood policy set is checked against two schemas, and only one of them is\nrequired.\n\n| Half | Format | Flag | Required |\n|---|---|---|---|\n| Action schema | Cedar `.cedarschema` \u2014 entities, actions, each action's `context` | `--policy-schema` | Yes, for `validate`, `lower` and `replay` |\n| Service schema | `.dwschema` event DSL, a `providers.json`, a `.dw` macro library | `--event-schema`, `--providers`, `--macros` | No |\n\nThe action schema is the one the rest of this lexicon already generates from \u2014\nsee [Schema](../schema/). This page is about the other half.\n\nWith all three service flags omitted, upstream falls back to\n`ServiceSchema::defaults()`: `request` (deciding), `response` and `error`\nkinds, a universal `pin callerPrincipal = principal`, a 24h `max_window` cap,\nno providers, and the embedded default macro library.\n\n## The `.dwschema` surface\n\nThe grammar is 136 lines of pest and purely syntactic: an optional\n`max_window` directive, then a sequence of event declarations.\n\n```\nmax_window = 30d\n\ndecision event <A>::request {\n ...inputs(A),\n pin callerPrincipal: principalType(A) = principal,\n callerResource: resourceType(A),\n requestId: String,\n}\n```\n\n`A` is a symbolic action binder, not an action. **The file names no actions at\nall** \u2014 it says what shape an event of each kind has, for whichever action it\nis derived against. That is why DWDC010 can check a predicate's event *kind*\nagainst the emitted schema but not its action: the action half of that check\nlives in the `.cedarschema`, and it is the CLI's to make.\n\nEvent kind names are author-defined. `request`, `response` and `error` are\nconventional, not fixed.\n\n| Builder | Emits |\n|---|---|\n| `spreadInputs()` / `spreadOutputs()` | `...inputs(A)` / `...outputs(A)` |\n| `field(\"requestId\", concrete(\"String\"))` | `requestId: String` |\n| `field(\"callerResource\", resourceType())` | `callerResource: resourceType(A)` |\n| `field(\"meta\", record([\u2026]))` | a nested record, addressed as `meta.member` |\n| `pinnedField(name, type, pinPrincipal())` | `pin name: \u2026 = principal` |\n| `pinnedField(name, type, pinContext(\"input.user\"))` | `pin name: \u2026 = context.input.user` |\n\nA pinned field must be a leaf; upstream requires the `pin` prefix and the\n`= \u2026` clause together, and the builder enforces both rather than deferring to\nthe parser.\n\n## The default, and the pin\n\n```typescript\nimport { TemporalEventSchema, dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nexport const events = new TemporalEventSchema({ schema: dogwood.defaultEventSchema() });\n```\n\nThat reproduces upstream's `pinned.dwschema` \u2014 the shape\n`ServiceSchema::defaults()` uses \u2014 and emits `events.dwschema`:\n\n```\n// The default event-schema shape: request/response/error, each correlated to\n// the deciding request's principal.\n\ndecision event <A>::request {\n ...inputs(A),\n pin callerPrincipal: principalType(A) = principal,\n callerResource: resourceType(A),\n requestId: String,\n sessionId: String,\n}\n\nevent <A>::response {\n ...inputs(A),\n ...outputs(A),\n pin callerPrincipal: principalType(A) = principal,\n callerResource: resourceType(A),\n requestId: String,\n sessionId: String,\n}\n\nevent <A>::error {\n ...inputs(A),\n pin callerPrincipal: principalType(A) = principal,\n callerResource: resourceType(A),\n requestId: String,\n sessionId: String,\n}\n```\n\n**The pin is the thing to understand before writing your own schema.**\n`pin callerPrincipal = principal` correlates every temporal predicate to the\ndeciding request's principal: events logged by other principals are invisible\nto `formerly`, `since` and every aggregate over them.\n\nSupplying *any* event schema opts out of upstream's default wholesale. So a\nschema emitted without a pin does not merely fail to add a correlation \u2014 it\nremoves one the policy author very likely assumed, and every predicate in the\nset starts matching other principals' events.\n\nThat is a legitimate design; cross-principal correlation is a reason to write\nyour own schema. It is also a decision, so chant makes it a named argument:\n\n```typescript\nexport const events = new TemporalEventSchema({\n schema: dogwood.defaultEventSchema({ pinCallerPrincipal: false }),\n});\n```\n\nwhich stamps the reasoning into the emitted file as a comment, and which\nDWDS010 reports as a warning in the build. Neither stops you. Both make the\nchoice visible in a diff.\n\n## `max_window`\n\nThe directive caps how far back any operator in the set may look. Absent, the\ncap is upstream's 24h default \u2014 the same 24h that applies when no schema is\nsupplied at all.\n\n```typescript\ndogwood.defaultEventSchema({ maxWindow: \"30d\" }); // max_window = 30d\n```\n\nDWDC011 does the arithmetic in TypeScript and fails the build on a window past\nthe cap, with no binary involved. Where several schemas are emitted the\ntightest cap wins, and macro-call intervals count: `once(48h, \u2026)` expands\nthrough `within ?w` and looks back exactly as far as `formerly within 48h`.\n\n## Several schemas\n\nOne `.dwschema` per file, because `max_window` is a single directive at the\ntop and concatenating two schemas would emit something upstream rejects. A\nbuild with more than one gives each an explicit filename:\n\n```typescript\nexport const gateway = new TemporalEventSchema({\n schema: dogwood.defaultEventSchema({ maxWindow: \"30d\" }),\n filename: \"gateway.dwschema\",\n});\n```\n\nTwo schemas targeting one filename is a serializer warning and only the first\nis written \u2014 a silent merge would produce a file that parses as neither.\n\n## Providers\n\nThe third service flag, `--providers`, takes a `providers.json` whose entries\ncarry `argumentTypes`, an `outputType` and an `implementation`. chant has no\ntyped builder for it today; the CLI adapter's bundle type accepts provider text\nif you assemble it, and the build's planner does not emit one.\n\nOne upstream trap worth recording even so: the CLI reads `--providers` as raw\ntext and **never resolves `scriptFile`**. Rhai has to be inlined under\n`implementation.script`, or `replay` fails per-evaluation with \"rhai\nimplementation has no script\" while `validate` and `lower` still pass.\n\n## Next\n\n- [Validation](../dogwood-validation/) \u2014 DWDC010, DWDC011 and DWDS010 in full\n- [Replay](../dogwood-replay/) \u2014 where the events these schemas describe\n actually come from\n";
|
|
19
|
-
export declare const dogwoodValidation = "Validation splits by what needs a binary, and the split is the point.\n\nEverything answerable in TypeScript runs on every build and gates. Full `.dw`\nvalidation needs upstream's own frontend, which ships as a Rust CLI and nothing\nelse \u2014 no npm package, no wasm build, no bindings \u2014 so it runs when the binary\nis there and says so out loud when it is not.\n\n| Check | Severity | Needs the binary |\n|---|---|---|\n| DWDC010 \u2014 a temporal predicate names a declared event kind | error | no |\n| DWDC011 \u2014 a window fits inside `max_window` | error | no |\n| DWDC012 \u2014 `formerly`/`previous`/`since` carries its window | error | no |\n| DWDC013 \u2014 an embedded AgentCore temporal statement has its event schema emitted | warning | no |\n| DWDS010 \u2014 an emitted event schema pins something | warning | no |\n| DWDE010 \u2014 the set validates clean under `dogwood validate` | error | yes |\n| DWDE011 \u2014 the lowered Cedar validates under `cedar-wasm` | error | yes |\n\nThe DWD family is an ordinary set of\n[post-synth checks](/chant/guide/organizational-policy/) under the prefix the\ncedar serializer declares in `extraRulePrefixes`. There is no second policy\nengine here; dogwood is a target, the same as Cedar.\n\nEvery one of them reads the **emitted text**, not the in-memory model, for the\nsame reason the CED checks read `policies.cedar.json`: `chant audit` runs over\na checked-in artifact chant did not write, and a wall that only fires on\nchant's own output is not a wall. It also keeps the builders and the walls\nindependent \u2014 DWDC012 catches a windowless `formerly` even though the builders\ncannot construct one, because `raw()` and a hand-written `.dw` both can.\n\n## The TypeScript walls\n\n**DWDC010** compares every predicate head in the temporal regions of a `.dw`\nfile against the event kinds the emitted `.dwschema` declares. Upstream rejects\nthe same thing with code `extension`. The check is silent when no `.dwschema`\nwas emitted: with none supplied, `ServiceSchema::defaults()` decides the kinds\nat the far end, and guessing that a project's out-of-band schema matches\nupstream's default would fail builds for a policy set that is fine.\n\n**DWDC011** fires with or without an emitted schema, because the cap applies\neither way \u2014 24h by default. See\n[Event Schemas](../dogwood-event-schemas/#max_window).\n\n**DWDC012** is the wall behind the typed builders. `formerly(w, body)` has\nnowhere to put a missing window, so the builders make it unrepresentable; the\ncheck is what covers `raw()`, hand-written files, and audits of trees chant\nnever wrote.\n\n**DWDC013** asks the question prior to DWDC010's, and only of statements that\nleft the `.dw` file behind. A policy embedded in `Definition.Policy.Statement`\ntravels as one string; the engine at the other end cannot match a temporal\npredicate until it knows what an event is. A build that embeds temporal text\nand emits no `.dwschema` has shipped half a policy \u2014 the statement deploys, the\npredicates match nothing, and a `formerly`-guarded forbid stops denying.\nWarning rather than error, because a project may register the service schema\nthrough a separate pipeline, and failing that build would be chant asserting a\nfact it cannot check.\n\n**DWDS010** is report-only. chant does not know whether cross-principal\ncorrelation was wanted, only that an unpinned schema should not slip through a\nreview unremarked.\n\nScanning is confined to the temporal regions of a file \u2014 every\n`temporal { \u2026 }` body and every `def temporal` body \u2014 so a Cedar attribute\nnamed `since` is not mistaken for the operator, and `context.retryWindow == 3`\nis not mistaken for a window.\n\n## The CLI-gated half\n\n**DWDE010** runs `dogwood validate --format json` over each emitted policy set\nand reports every finding. What it catches that the walls cannot: macro\nexpansion, the temporal type checker, and the Cedar body checked against the\naction schema through upstream's own frontend.\n\n**DWDE011** takes the `dogwood lower` output \u2014 plain Cedar with the temporal\nconditions hoisted into `context.*` slots, plus an augmented schema declaring\nthem \u2014 and runs the published `@cedar-policy/cedar-wasm` over it. That is a\ndifferent validator from the one vendored inside upstream, which makes a\nfinding here meaningful: it is drift between the Cedar upstream pins and the\nCedar the rest of chant validates against. The #1657 verification put all 86\nupstream example bundles through this exact path and every one validated clean\nin strict mode.\n\n### When the binary is absent\n\nOne `info` finding, naming the binary, where chant looked, and the issue. Not\nsilence. A check that quietly passes when it could not run is claiming a\nguarantee it never made.\n\n## Pointing chant at a binary\n\nThere is no published build. You build it from the pinned revision:\n\n```bash\ngit clone https://github.com/dogwood-policy/dogwood\ncd dogwood && git checkout 5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c\ncargo build --release\n```\n\nResolution order:\n\n1. An explicit `configureDogwoodCli({ binary })` call \u2014 taken as given, since\n its caller knows.\n2. `$CHANT_DOGWOOD_BINARY`.\n3. `cedar.dogwood.binary` in a `chant.config.json`, resolved by walking up\n from the working directory.\n4. `dogwood` on `PATH`.\n\n```bash\nexport CHANT_DOGWOOD_BINARY=/path/to/dogwood/target/release/dogwood\n```\n\n```json\n{ \"cedar\": { \"dogwood\": { \"binary\": \"./vendor/dogwood\" } } }\n```\n\nThe config knob reads `chant.config.json` only, and that is a real limitation\nrather than an oversight: a post-synth check's `check()` is synchronous, while\nchant's config loader is async and, under `chant build --sandbox`, evaluates a\n`chant.config.ts` in a child process. JSON is data, so reading it executes\nnothing. A project on `chant.config.ts` uses the environment variable or the\nprogrammatic override.\n\nA path from the environment or the config that is not executable is resolved\npast rather than returned to fail later, and the advisory names where chant\nlooked.\n\n## Why exit codes decide nothing\n\nThree properties of the CLI shape the adapter, all verified against the pinned\nsources.\n\n**Exit 2 is ambiguous.** It covers a rejected policy set *and* clap's own usage\nerror for an unknown flag \u2014 and upstream's published guide claims exit 1 for\nthe latter. Reading a bare non-zero exit as \"your policy is bad\" would fail a\nbuild over a flag rename in a sync nobody outside Amazon can review. So the\nadapter branches on the JSON on stdout, in both directions: a `passed: false`\nwith a zero exit is still a rejection, and the reverse is still a pass.\n\n**There are two JSON shapes.** A type-check finding arrives in a report \u2014\n`passed`, `passed_without_warnings`, `errors[]`, `warnings[]`. A fatal parse,\nmacro or lowering error replaces the whole report with a bare error object\ncarrying the same diagnostic fields at the top level plus `related[]`. Both\nnormalize into one diagnostic type, so nothing downstream has to know which\narrived.\n\n**A run that produced no usable JSON is neither a pass nor a rejection.** It is\nreported at `warning` severity as \"could not be validated\", and the policy set\nis explicitly described as neither accepted nor rejected.\n\nTwo smaller contract facts the adapter encodes: `--format json` writes to\nstdout for success and fatal alike, and `--emit` is ignored under\n`--format json` (the JSON always carries all three lowered artifacts), so it is\nnot passed.\n\nDiagnostic labels are **byte offsets** into the `.dw` source, and findings\nreport them as byte ranges. Converting to line and column would mean\nre-deriving line breaks over a file the adapter does not hold, and a wrong line\nnumber is worse than an honest offset.\n\n## Nothing in gating CI runs it\n\nBy design, from the epic: upstream instability is priced, not absorbed. The\nCLI-gated checks are for a developer with the binary and for on-demand\nharnesses in the `forgejo-runtime-e2e` shape. `PolicyReplayOp` shares that\nrule and the same binary discovery \u2014 see [Replay](../dogwood-replay/) \u2014 with\none difference: a replay step with no binary **fails**, where a build check\nwith no binary reports and moves on. A check that could not run should not\nblock a build; a replay that could not run has produced no answer at all.\n\n## Next\n\n- [Replay](../dogwood-replay/) \u2014 the third verb, wrapped as an activity and an\n Op rather than as a build check\n- [Lint Rules](../lint-rules/) \u2014 the cedar half of the same check set\n";
|
|
20
|
-
export declare const dogwoodReplay = "`dogwood replay` evaluates a policy set against a recorded event trace and\nreturns a verdict per decision point. It is how a temporal policy gets tested\nat all.\n\nA plain Cedar policy is decidable from its source: given the schema, a build\ncan say whether it parses, whether it type-checks, and what it applies to. The\nDWDC and CEDC checks already do that. A temporal policy is not decidable that\nway \u2014 whether `formerly within 1h Login::response{ \u2026 }` fires depends on a\nhistory nobody has replayed. So the check is a replay against recorded decision\nhistory, and the answer moves as the history does. That puts it on the observe\nend of the lifecycle dial, beside `WorkflowAuditOp`, with a finding mode as the\nreconcile step.\n\nThree pieces ship: a typed trace builder, a `dogwoodReplay` activity, and the\n`PolicyReplayOp` composite that pairs them. The worked example is\n`lexicons/cedar/examples/policy-replay`.\n\n## The Op\n\n```typescript\n// ops/policy-replay.op.ts\nimport { PolicyReplayOp } from \"@intentius/chant-lexicon-cedar\";\nimport { readAfterLoginExpectations } from \"../trace/read-after-login\";\n\nexport const { op } = PolicyReplayOp({\n name: \"policy-replay\",\n policiesPath: \"dist/policies.dw\",\n policySchemaPath: \"schema.cedarschema\",\n eventSchemaPath: \"dist/events.dwschema\",\n tracePath: \"trace/read-after-login.log\",\n expect: readAfterLoginExpectations,\n onFinding: \"report\",\n});\n\nexport default op;\n```\n\n```bash\nnpx chant run policy-replay\n```\n\nThree phases:\n\n| Phase | Step | What it does |\n|---|---|---|\n| Artifacts | `chantBuild` | Emits `policies.dw`, the `.cedarschema` and the `.dwschema` the replay reads. Pass `buildScript: false` when they are checked in and the phase is dropped rather than run empty |\n| Replay | `dogwoodReplay` | Runs `dogwood replay --format json` over the bundle and the trace, writes the divergence report |\n| Report | `dogwoodReplayReport` | Reads that report and acts on the finding mode |\n\nThe report file (`dist/dogwood-replay.json` by default) is the seam between the\nlast two phases, for the same reason `dist/fly.json` is the seam between\n`build:fly` and `flyApply`: Op steps do not hand return values to one another,\nso a phase boundary needs an artifact to be a real boundary.\n\nThe Replay step carries `outcomeAttribute: { name: \"Divergences\", from: \"findings\" }`,\nso \"show me the replays that found something\" is one filter rather than a log\nread. `onFinding` takes `report | issue | pull-request`; `report` prints the\nmarkdown, and the other two hand back a title and body for whatever opens them\n\u2014 the cedar lexicon has no forge client and does not grow one, the same\ndivision `workflowSupplyChainAudit` draws. `failOnDivergence` defaults to\nfalse: an observe-dial Op reports, and a red run is the caller's decision.\n\nThe composite ships from cedar, not from temporal, because it hands back an Op\nand nothing else. It imports `@intentius/chant/op` and carries no dependency on\nthe temporal lexicon. A project that wants it scheduled pairs it with a\n`TemporalSchedule` of its own \u2014 two lines, project-side, rather than a config\nflag that would drag the dependency in for everyone.\n\n## Typed traces\n\n`traceEvent()` builds one line. `renderTrace()` renders a list.\n`traceFixture()` does both and refuses to hand back a trace that would weaken\nits own replay.\n\n```typescript\nimport { dogwood } from \"@intentius/chant-lexicon-cedar\";\n\nconst { entityRef, traceEvent } = dogwood;\n\nconst ALICE = 'Drupe::OAuthUser::\"alice\"';\nconst GATEWAY = 'Drupe::Gateway::\"gw1\"';\n\nconst session = {\n scope: { principal: ALICE, resource: GATEWAY },\n context: { input: { user: \"alice\" } },\n} as const;\n\nconst injected = (requestId: string) => ({\n callerPrincipal: entityRef(ALICE),\n callerResource: entityRef(GATEWAY),\n requestId,\n});\n\nexport const trace = [\n traceEvent({ ...session, timestamp: 0, action: 'Drupe::Action::\"Login\"', record: injected(\"u1\") }),\n traceEvent({ ...session, timestamp: 0, action: 'Drupe::Action::\"Login\"', kind: \"response\", record: injected(\"u1\") }),\n traceEvent({ ...session, timestamp: 10, action: 'Drupe::Action::\"Read\"', record: injected(\"u2\") }),\n traceEvent({ ...session, timestamp: 7200, action: 'Drupe::Action::\"Read\"', record: injected(\"u3\") }),\n];\n```\n\n```\n@0 scope(principal: Drupe::OAuthUser::\"alice\", resource: Drupe::Gateway::\"gw1\") request_context(input: { user: \"alice\" }) Drupe::Action::\"Login\"::request(input: { user: \"alice\" }, callerPrincipal: Drupe::OAuthUser::\"alice\", callerResource: Drupe::Gateway::\"gw1\", requestId: \"u1\")\n```\n\nCompare the input to the output: `input` was written once, under `context`,\nand comes out in the `request_context` envelope **and** in the logged record.\n`kind` defaults to `request`. Values render in Cedar surface\nforms: strings quote themselves, `entityRef()` renders a uid bare,\n`decimalValue(\"1.50\")` keeps a scale a JS number would lose, and a non-integer\n`number` throws rather than emitting something the parser reads differently.\n\n## The both-bags trap\n\nEach line carries two field bags and they are not the same bag.\n\n```\n@10 \u2026 request_context(input: { user: \"alice\" }) Drupe::Action::\"Read\"::request(input: { user: \"alice\" }, callerPrincipal: \u2026)\n \u2514\u2500\u2500 the Cedar request is built from this \u2514\u2500\u2500 temporal predicates match against this\n```\n\n`formerly within 1h Drupe::Action::\"Login\"::response{ input.user: context.input.user }`\ncompares the *past* login's `input.user`, out of the logged record, against the\n*current* request's `context.input.user`, out of the `request_context`\nenvelope. Fill one bag and not the other and nothing errors: the replay exits 0\nwith a verdict that tested half of what it claims.\n\nSo the default is both bags, and the weaker trace takes an explicit opt-out:\n\n| `bags` | Where a `context` group lands |\n|---|---|\n| `\"both\"` (default) | `request_context` and the logged record |\n| `\"record-only\"` | The logged record alone \u2014 `context.*` is absent from the Cedar request |\n| `\"context-only\"` | The envelope alone \u2014 no temporal predicate can match the group |\n\n`record` is the other half of the input, and it is deliberately separate: the\nevent schema's own injections (`callerPrincipal`, `callerResource`,\n`requestId`, `sessionId`) belong to the logged record and are never part of the\nCedar request.\n\n## The action-naming trap\n\nAction names must be fully qualified \u2014 `Drupe::Action::\"Read\"`, never `Read`. A\nshort name leaves every temporal predicate unmatched while Cedar still\nauthorizes. `traceEvent()` rejects one at construction, as do `entityRef()` and\n`traceEntity()`.\n\n## Auditing a trace chant did not build\n\nA trace fetched from somewhere else \u2014 an AgentCore session history, a `.log`\nrecorded by hand \u2014 normalizes into the same `TraceEvent` list and takes the\nsame audit:\n\n```typescript\nconst issues = dogwood.auditTrace(events);\n```\n\n| Kind | What it means |\n|---|---|\n| `single-bag` | A group is in one bag and not the other, so one side of the check silently misses |\n| `no-request-context` | A deciding event has no envelope at all, so every `context.*` test misses |\n| `empty-record` | An event logs no fields, so no temporal predicate can match it |\n| `out-of-order` | A timestamp goes backwards; history accumulates in file order, so a window sees something different |\n\nEvery one of those makes a replay *weaker* rather than making it fail, which is\nthe class a green run hides. `decisionKinds` defaults to `[\"request\"]` \u2014 a\nhistory-only event never becomes a Cedar request, so a missing envelope on one\nis not a weakening and is not reported. The truth is whichever kinds the\nproject's `.dwschema` marks `decision`, and that file is not visible from the\naudit.\n\n`traceFixture(events)` runs the same audit and **throws** on any finding,\nnaming the `allow` list that would let it through. A fixture that weakens its\nown replay fails at build time instead of producing a green run that proves\nnothing.\n\n## Expectations\n\n```typescript\nexport const expectations = [\n { timestamp: 0, verdict: \"deny\", note: \"the login request itself is not permitted\" },\n { timestamp: 10, verdict: \"allow\", determiningRules: [0], note: \"the login is ten seconds old\" },\n { timestamp: 7200, verdict: \"deny\", note: \"the login is two hours stale\" },\n];\n```\n\nThree expectations for four trace lines, and that is the point of writing them\nagainst `timestamp` rather than `index`: `Login::response` is history-only\nunder the default event schema, so it contributes to the window and produces no\nverdict. `index` is the position in the *decision* stream, not the trace line\nnumber, and it shifts whenever a trace gains a history-only event.\n\n`determiningRules` is the second half of the assertion. A decision that comes\nout right for the wrong reason \u2014 the correct verdict carried by a different\nrule \u2014 is drift the verdict alone cannot show.\n\nWhat `compareVerdicts` reports: a verdict that differs from the expectation; a\nverdict that matches but was determined by different rules; a decision point an\nexpectation named that never occurred; a decision point that occurred and\nnothing expected. Per-evaluation errors are reported even when the expectation\nmatched, and \u2014 when no expectations were written at all \u2014 an errored evaluation\nis still a finding, because a provider with no inlined Rhai script would\notherwise replay \"clean\".\n\n## The trace format\n\nOne event per line. Blank lines are skipped, a leading BOM is stripped, and\nthere is **no comment syntax** \u2014 a `//` is an ordinary part of a value, so URLs\nsurvive and an attribution header would be parsed as an event and rejected with\n\"timepoint must start with `@`\".\n\n```\n@<timestamp> [scope(...)] [entities(...)] [request_context(...)] <Ns>::Action::\"<Name>\"::<kind>(<field>: <value>, ...)\n```\n\nThe timestamp is an `i64` after `@`. The three envelopes are optional and must\nappear in that order. Values use Cedar surface forms: entity refs, quoted\nstrings, integers, decimals like `1.50`, booleans, arrays, nested records.\n\n## Reading the run\n\nHuman output is one line per decision point:\n\n```\n@0 (time point 0): DENY\n@10 (time point 1): ALLOW [rules: 0]\n@7200 (time point 2): DENY\n```\n\nJSON gives `{verdicts: [{index, timestamp, verdict, determining_rules, errors}]}`.\nHistory-only events produce no line.\n\n**Replay exits 0 even when every verdict is DENY.** A non-zero exit means the\ntrace or the policy set failed to load, never that a policy denied. The adapter\nin `src/dogwood/cli.ts` reads the JSON and not the exit code, here as\neverywhere; an unrecognised verdict string is read as a deny rather than\ndropped, because dropping an entry would shift every later index.\n\n## It needs the binary, and does not pretend otherwise\n\nThe Replay phase shells to upstream's CLI. There is no npm package and no wasm\nbuild \u2014 see [Validation](../dogwood-validation/) for how chant finds a binary\nand how to build one.\n\nWithout one the step **fails** and says where chant looked. It does not degrade\nto a pass, and an unusable invocation or a fatal (a malformed trace line, an\nunparseable policy set) throws rather than reporting zero divergences. A replay\nthat did not happen is not a replay that found nothing \u2014 which is also why\nnothing in gating CI executes the binary.\n\n## Next\n\n- [Validation](../dogwood-validation/) \u2014 the two verbs that run inside a build,\n and the binary knobs replay shares\n- [The Dogwood Dialect](../dogwood/) \u2014 what pre-release means for all of this\n";
|
|
21
|
-
//# sourceMappingURL=docs-dogwood.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docs-dogwood.d.ts","sourceRoot":"","sources":["../../src/codegen/docs-dogwood.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,eAAO,MAAM,eAAe,oiQA+K3B,CAAC;AAIF,eAAO,MAAM,uBAAuB,ghXA0TnC,CAAC;AAIF,eAAO,MAAM,mBAAmB,q6MAsK/B,CAAC;AAIF,eAAO,MAAM,iBAAiB,49QA8K7B,CAAC;AAIF,eAAO,MAAM,aAAa,inXA+PzB,CAAC"}
|