@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10

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 (149) hide show
  1. package/README.md +106 -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/codegen/docs-dogwood.d.ts +21 -0
  9. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts.map +1 -1
  11. package/dist/codegen/package.d.ts.map +1 -1
  12. package/dist/config.d.ts +25 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/dogwood/cli.d.ts +247 -0
  15. package/dist/dogwood/cli.d.ts.map +1 -0
  16. package/dist/dogwood/event-schema.d.ts +161 -0
  17. package/dist/dogwood/event-schema.d.ts.map +1 -0
  18. package/dist/dogwood/index.d.ts +39 -0
  19. package/dist/dogwood/index.d.ts.map +1 -0
  20. package/dist/dogwood/macros.d.ts +96 -0
  21. package/dist/dogwood/macros.d.ts.map +1 -0
  22. package/dist/dogwood/policy.d.ts +120 -0
  23. package/dist/dogwood/policy.d.ts.map +1 -0
  24. package/dist/dogwood/replay-activity.d.ts +196 -0
  25. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  26. package/dist/dogwood/replay-op.d.ts +165 -0
  27. package/dist/dogwood/replay-op.d.ts.map +1 -0
  28. package/dist/dogwood/scan.d.ts +109 -0
  29. package/dist/dogwood/scan.d.ts.map +1 -0
  30. package/dist/dogwood/serialize.d.ts +66 -0
  31. package/dist/dogwood/serialize.d.ts.map +1 -0
  32. package/dist/dogwood/temporal.d.ts +259 -0
  33. package/dist/dogwood/temporal.d.ts.map +1 -0
  34. package/dist/dogwood/trace.d.ts +215 -0
  35. package/dist/dogwood/trace.d.ts.map +1 -0
  36. package/dist/dogwood/upstream.d.ts +41 -0
  37. package/dist/dogwood/upstream.d.ts.map +1 -0
  38. package/dist/dogwood/window.d.ts +73 -0
  39. package/dist/dogwood/window.d.ts.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/integrity.json +15 -3
  43. package/dist/lint/audit-catalog.d.ts.map +1 -1
  44. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  45. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  46. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  47. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  48. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  49. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  50. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  51. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  52. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  53. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  54. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  55. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  56. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  57. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  58. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  59. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  60. package/dist/lint/post-synth/index.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +7 -0
  63. package/dist/okf/rules/DWDC010.md +11 -0
  64. package/dist/okf/rules/DWDC011.md +11 -0
  65. package/dist/okf/rules/DWDC012.md +11 -0
  66. package/dist/okf/rules/DWDC013.md +15 -0
  67. package/dist/okf/rules/DWDE010.md +11 -0
  68. package/dist/okf/rules/DWDE011.md +11 -0
  69. package/dist/okf/rules/DWDS010.md +11 -0
  70. package/dist/okf/types/Policy.md +1 -0
  71. package/dist/op/activities/index.d.ts +18 -0
  72. package/dist/op/activities/index.d.ts.map +1 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/policy-text.d.ts +53 -0
  75. package/dist/policy-text.d.ts.map +1 -0
  76. package/dist/rules/dogwood-helpers.ts +139 -0
  77. package/dist/rules/dwdc010.ts +62 -0
  78. package/dist/rules/dwdc011.ts +61 -0
  79. package/dist/rules/dwdc012.ts +46 -0
  80. package/dist/rules/dwdc013.ts +64 -0
  81. package/dist/rules/dwde010.ts +130 -0
  82. package/dist/rules/dwde011.ts +108 -0
  83. package/dist/rules/dwds010.ts +46 -0
  84. package/dist/serializer.d.ts +10 -18
  85. package/dist/serializer.d.ts.map +1 -1
  86. package/dist/skills/chant-cedar-authoring.md +180 -0
  87. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  88. package/dist/skills/chant-cedar-dogwood.md +327 -0
  89. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  90. package/package.json +7 -2
  91. package/src/agentcore/embed.test.ts +254 -0
  92. package/src/agentcore/embed.ts +399 -0
  93. package/src/agentcore/enforcement.test.ts +43 -0
  94. package/src/agentcore/enforcement.ts +92 -0
  95. package/src/agentcore/scan.ts +119 -0
  96. package/src/codegen/docs-dogwood.ts +1119 -0
  97. package/src/codegen/docs.ts +66 -1
  98. package/src/codegen/package.ts +3 -2
  99. package/src/config.test.ts +12 -0
  100. package/src/config.ts +28 -0
  101. package/src/dogwood/cli.test.ts +513 -0
  102. package/src/dogwood/cli.ts +666 -0
  103. package/src/dogwood/event-schema.test.ts +218 -0
  104. package/src/dogwood/event-schema.ts +318 -0
  105. package/src/dogwood/index.ts +271 -0
  106. package/src/dogwood/macros.test.ts +104 -0
  107. package/src/dogwood/macros.ts +229 -0
  108. package/src/dogwood/policy.test.ts +94 -0
  109. package/src/dogwood/policy.ts +141 -0
  110. package/src/dogwood/replay-activity.test.ts +481 -0
  111. package/src/dogwood/replay-activity.ts +506 -0
  112. package/src/dogwood/replay-op.ts +242 -0
  113. package/src/dogwood/scan.ts +287 -0
  114. package/src/dogwood/serialize.test.ts +331 -0
  115. package/src/dogwood/serialize.ts +246 -0
  116. package/src/dogwood/temporal.test.ts +272 -0
  117. package/src/dogwood/temporal.ts +592 -0
  118. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  119. package/src/dogwood/testdata/default-macros.dw +23 -0
  120. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  121. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  122. package/src/dogwood/testdata/pinned.dwschema +31 -0
  123. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  124. package/src/dogwood/testdata/read-after-login.dw +17 -0
  125. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  126. package/src/dogwood/trace.test.ts +231 -0
  127. package/src/dogwood/trace.ts +471 -0
  128. package/src/dogwood/upstream.ts +41 -0
  129. package/src/dogwood/window.ts +124 -0
  130. package/src/index.ts +76 -0
  131. package/src/lint/audit-catalog.ts +64 -0
  132. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  133. package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
  134. package/src/lint/post-synth/dwdc010.ts +62 -0
  135. package/src/lint/post-synth/dwdc011.ts +61 -0
  136. package/src/lint/post-synth/dwdc012.ts +46 -0
  137. package/src/lint/post-synth/dwdc013.ts +64 -0
  138. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  139. package/src/lint/post-synth/dwde010.ts +130 -0
  140. package/src/lint/post-synth/dwde011.ts +108 -0
  141. package/src/lint/post-synth/dwds010.ts +46 -0
  142. package/src/lint/post-synth/index.ts +14 -0
  143. package/src/lint/post-synth/post-synth.test.ts +7 -3
  144. package/src/op/activities/index.ts +27 -0
  145. package/src/plugin.test.ts +3 -2
  146. package/src/plugin.ts +30 -0
  147. package/src/policy-text.ts +128 -0
  148. package/src/serializer.ts +71 -109
  149. package/src/skills/chant-cedar-dogwood.md +327 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * DWDE010: the emitted `.dw` set validates clean under `dogwood validate`
3
+ *
4
+ * The CLI-gated half of the epic's validation split. Everything the DWDC walls
5
+ * ask is answerable in TypeScript and gates unconditionally; everything else —
6
+ * macro expansion, the temporal type checker, the Cedar body checked against
7
+ * the action schema *through upstream's own frontend* — needs upstream's Rust
8
+ * frontend, which ships as a binary and nothing else. No npm package, no wasm
9
+ * build, no bindings.
10
+ *
11
+ * So this check has two modes and says which one it is in:
12
+ *
13
+ * - **Binary present.** Runs `dogwood validate --format json` over each
14
+ * emitted policy set and reports every finding as an error. Byte-offset
15
+ * labels come through as byte offsets — see `../../dogwood/cli.ts` for why
16
+ * they are not converted to line/column.
17
+ * - **Binary absent.** Exactly one `info` finding naming the binary, where
18
+ * chant looked, and this issue. The epic's words are "an explicit,
19
+ * issue-linked exception, not a silent one" — a check that quietly passes
20
+ * when it could not run is claiming a guarantee it did not make, which is
21
+ * the same failure CEDE010's no-schema advisory exists to avoid.
22
+ *
23
+ * A run that could not be made at all — a spawn failure, an unknown flag, JSON
24
+ * in a shape this adapter does not know — is `warning`, not `error`. Upstream
25
+ * is a read-only squash-sync mirror with no tags and no changelog, and a flag
26
+ * rename in a sync would otherwise fail every build that has the binary
27
+ * installed. The adapter never reads exit 2 as "rejected" on its own for the
28
+ * same reason: clap spends that code on usage errors too.
29
+ */
30
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
31
+ import {
32
+ DOGWOOD_BINARY_NAME,
33
+ DOGWOOD_SEARCH_ORDER,
34
+ findDogwoodBinary,
35
+ formatDogwoodDiagnostic,
36
+ runDogwoodValidate,
37
+ } from "../../dogwood/cli";
38
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
39
+
40
+ export const dwde010: PostSynthCheck = {
41
+ id: "DWDE010",
42
+ description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
43
+
44
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
+ const plan = planDogwoodRuns(ctx);
46
+ if (!plan.hasPolicies) return [];
47
+
48
+ const binary = findDogwoodBinary();
49
+ if (!binary) {
50
+ return [
51
+ {
52
+ checkId: "DWDE010",
53
+ severity: "info",
54
+ message: `This build emitted dogwood .dw policies, but no \`${DOGWOOD_BINARY_NAME}\` binary was found, so full .dw validation did not run — the DWDC walls checked what TypeScript can answer and nothing checked macro expansion, the temporal type check, or the Cedar body against the action schema. chant looked at ${DOGWOOD_SEARCH_ORDER}. See chant #1659.`,
55
+ lexicon: plan.lexicon,
56
+ },
57
+ ];
58
+ }
59
+
60
+ if (plan.blocked) {
61
+ return [
62
+ {
63
+ checkId: "DWDE010",
64
+ severity: "info",
65
+ message: `This build emitted dogwood .dw policies and \`${DOGWOOD_BINARY_NAME}\` is available, but ${plan.blocked}, so full .dw validation did not run. See chant #1659.`,
66
+ lexicon: plan.lexicon,
67
+ },
68
+ ];
69
+ }
70
+
71
+ const diagnostics: PostSynthDiagnostic[] = [];
72
+
73
+ for (const prepared of plan.bundles) {
74
+ const where = describeBundle(prepared);
75
+ const result = runDogwoodValidate(binary.path, prepared.bundle);
76
+
77
+ if (result.kind === "unusable") {
78
+ diagnostics.push({
79
+ checkId: "DWDE010",
80
+ severity: "warning",
81
+ message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
82
+ entity: prepared.source,
83
+ lexicon: prepared.lexicon,
84
+ });
85
+ continue;
86
+ }
87
+
88
+ if (result.kind === "fatal") {
89
+ for (const finding of [result.error, ...result.related]) {
90
+ diagnostics.push({
91
+ checkId: "DWDE010",
92
+ severity: "error",
93
+ message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
94
+ entity: prepared.source,
95
+ lexicon: prepared.lexicon,
96
+ });
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (result.kind === "rejected") {
102
+ for (const finding of result.errors) {
103
+ diagnostics.push({
104
+ checkId: "DWDE010",
105
+ severity: "error",
106
+ message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
107
+ entity: prepared.source,
108
+ lexicon: prepared.lexicon,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Upstream's own warnings ride along at warning severity. They do not
114
+ // fail `dogwood validate` (`passed` ignores them), and they should not
115
+ // fail a build either — but dropping them would throw away the half of
116
+ // the report that says a policy is legal and pointless.
117
+ for (const finding of result.warnings) {
118
+ diagnostics.push({
119
+ checkId: "DWDE010",
120
+ severity: "warning",
121
+ message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
122
+ entity: prepared.source,
123
+ lexicon: prepared.lexicon,
124
+ });
125
+ }
126
+ }
127
+
128
+ return diagnostics;
129
+ },
130
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * DWDE011: the lowered Cedar body validates clean under Cedar's own validator
3
+ *
4
+ * The epic's stated design for the non-temporal half: chant does not
5
+ * re-implement lowering — `dogwood lower` produces the analyzable plain-Cedar
6
+ * form, and a reimplementation would drift — but once that form exists, Cedar's
7
+ * own validator is the thing that judges it. The #1657 verification put all 86
8
+ * upstream example bundles through this exact path and every one parsed and
9
+ * validated clean in strict mode, which is what makes a finding here mean
10
+ * something: the lowered output is genuinely plain Cedar, so a validation error
11
+ * is the policy's, not the pipeline's.
12
+ *
13
+ * What this catches that DWDE010 does not is narrow but real. `dogwood
14
+ * validate` type-checks through upstream's vendored Cedar; this runs the
15
+ * *published* `@cedar-policy/cedar-wasm` over the same artifacts. A body that
16
+ * upstream's pinned Cedar accepts and the Cedar the rest of chant validates
17
+ * against rejects is exactly the drift the pin exists to make visible — and the
18
+ * augmented schema, with its hoisted `context.*` temporal slots, is the form a
19
+ * downstream Cedar policy store will actually receive.
20
+ *
21
+ * The cedar-wasm traps from #1648 all apply and are handled in
22
+ * `./wasm-helpers.ts`: `type: "success"` means validation *ran*, not that it
23
+ * passed; `validationErrors` comes back in a different order almost every call
24
+ * and is sorted before anything reads it; a malformed call throws rather than
25
+ * returning a failure answer.
26
+ *
27
+ * Silent when `lower` did not run — the binary is absent, or no action schema
28
+ * was emitted. Both cases are DWDE010's advisory to report, once, rather than
29
+ * twice in different words.
30
+ */
31
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
32
+ import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli";
33
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
34
+ import { loadWasm, validatePolicySet } from "./wasm-helpers";
35
+
36
+ export const dwde011: PostSynthCheck = {
37
+ id: "DWDE011",
38
+ description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
39
+
40
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
41
+ const plan = planDogwoodRuns(ctx);
42
+ if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0) return [];
43
+
44
+ const binary = findDogwoodBinary();
45
+ if (!binary) return [];
46
+
47
+ // A missing validator is not this check's finding to report; the cedar leg
48
+ // says so already through CEDC010.
49
+ const wasm = loadWasm();
50
+ if (!wasm) return [];
51
+
52
+ const diagnostics: PostSynthDiagnostic[] = [];
53
+
54
+ for (const prepared of plan.bundles) {
55
+ const where = describeBundle(prepared);
56
+ const lowered = runDogwoodLower(binary.path, prepared.bundle);
57
+
58
+ if (lowered.kind === "unusable") {
59
+ diagnostics.push({
60
+ checkId: "DWDE011",
61
+ severity: "warning",
62
+ message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
63
+ entity: prepared.source,
64
+ lexicon: prepared.lexicon,
65
+ });
66
+ continue;
67
+ }
68
+
69
+ if (lowered.kind === "fatal") {
70
+ // The same fatal DWDE010 reports from its own run. Saying it twice
71
+ // helps nobody, so this arm stays quiet and lets the validate leg own
72
+ // the parse/lower channel.
73
+ continue;
74
+ }
75
+
76
+ const { cedarPolicies, cedarSchema } = lowered.value;
77
+
78
+ // The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
79
+ // (`policy0`, `policy1`) for a bare-string set and does not read `@id`
80
+ // annotations as ids (#1648), so a finding names the .dw file it came
81
+ // from as well as the id the validator used.
82
+ const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
83
+
84
+ if (outcome.failure) {
85
+ diagnostics.push({
86
+ checkId: "DWDE011",
87
+ severity: "error",
88
+ message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
89
+ entity: prepared.source,
90
+ lexicon: prepared.lexicon,
91
+ });
92
+ continue;
93
+ }
94
+
95
+ for (const finding of outcome.errors) {
96
+ diagnostics.push({
97
+ checkId: "DWDE011",
98
+ severity: "error",
99
+ message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
100
+ entity: prepared.source,
101
+ lexicon: prepared.lexicon,
102
+ });
103
+ }
104
+ }
105
+
106
+ return diagnostics;
107
+ },
108
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DWDS010: an emitted event schema with no pinned field widens every predicate
3
+ *
4
+ * The #1657 verification is specific about this one. `ServiceSchema::defaults()`
5
+ * — what runs when nobody passes `--event-schema` — uses
6
+ * `configuration/event-schemas/pinned.dwschema`, which carries
7
+ * `pin callerPrincipal: principalType(A) = principal` on every kind. Every
8
+ * temporal predicate is therefore correlated to the deciding request's
9
+ * principal, and events from other principals are invisible.
10
+ *
11
+ * Supplying any event schema opts out of that default wholesale. So a schema
12
+ * emitted without a pin does not merely "not add" a correlation: it removes
13
+ * one the policy author very likely assumed, and every `formerly` in the set
14
+ * starts matching other principals' events. That is a legitimate design —
15
+ * cross-principal correlation is the reason to write your own schema — but it
16
+ * is a decision, and a decision nobody can see in a diff is one nobody made.
17
+ *
18
+ * Report-only: chant does not know which the author wanted, only that it
19
+ * should be said out loud. `defaultEventSchema({ pinCallerPrincipal: false })`
20
+ * also stamps the reasoning into the emitted file.
21
+ */
22
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
+ import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan";
24
+
25
+ export const dwds010: PostSynthCheck = {
26
+ id: "DWDS010",
27
+ description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
28
+
29
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
30
+ const diagnostics: PostSynthDiagnostic[] = [];
31
+
32
+ for (const schema of dogwoodSchemaFiles(ctx)) {
33
+ const facts = readEventSchema(schema.text);
34
+ if (facts.kinds.length === 0 || facts.hasPin) continue;
35
+ diagnostics.push({
36
+ checkId: "DWDS010",
37
+ severity: "warning",
38
+ message: `Dogwood event schema "${schema.source}" declares no pinned field, so temporal predicates correlate across every principal — wider than upstream's default, which pins callerPrincipal to the deciding request's principal. Add a pinned field, or record why cross-principal correlation is wanted.`,
39
+ entity: schema.source,
40
+ lexicon: schema.lexicon,
41
+ });
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -10,6 +10,13 @@ import { cede011 } from "./cede011";
10
10
  import { ceds010 } from "./ceds010";
11
11
  import { ceds011 } from "./ceds011";
12
12
  import { ceds012 } from "./ceds012";
13
+ import { dwdc010 } from "./dwdc010";
14
+ import { dwdc011 } from "./dwdc011";
15
+ import { dwdc012 } from "./dwdc012";
16
+ import { dwdc013 } from "./dwdc013";
17
+ import { dwde010 } from "./dwde010";
18
+ import { dwde011 } from "./dwde011";
19
+ import { dwds010 } from "./dwds010";
13
20
 
14
21
  export const postSynthChecks: PostSynthCheck[] = [
15
22
  cedc010,
@@ -22,4 +29,11 @@ export const postSynthChecks: PostSynthCheck[] = [
22
29
  ceds010,
23
30
  ceds011,
24
31
  ceds012,
32
+ dwdc010,
33
+ dwdc011,
34
+ dwdc012,
35
+ dwdc013,
36
+ dwde010,
37
+ dwde011,
38
+ dwds010,
25
39
  ];
@@ -12,7 +12,7 @@
12
12
  import { describe, test, expect } from "vitest";
13
13
  import { createPostSynthContext, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
14
14
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
15
- import { CEDAR_JSON_FILENAME } from "../../serializer";
15
+ import { CEDAR_JSON_FILENAME, cedarSerializer } from "../../serializer";
16
16
  import { postSynthChecks } from ".";
17
17
  import { cedc010 } from "./cedc010";
18
18
  import { cedc011 } from "./cedc011";
@@ -103,10 +103,14 @@ function ctxWithSchema(
103
103
  // ── Barrel ─────────────────────────────────────────────────────────
104
104
 
105
105
  describe("the cedar post-synth barrel", () => {
106
- test("ships every check exactly once, all under the CED prefix", () => {
106
+ test("ships every check exactly once, under a declared prefix", () => {
107
+ // Two id families, both declared on the serializer: CED for Cedar itself,
108
+ // DWD for the dogwood temporal dialect that ships inside this lexicon
109
+ // (#1658). An id outside both is what `chant dev check-lexicon` fails on.
110
+ const declared = [cedarSerializer.rulePrefix, ...(cedarSerializer.extraRulePrefixes ?? [])];
107
111
  const ids = postSynthChecks.map((c) => c.id);
108
112
  expect(new Set(ids).size).toBe(ids.length);
109
- expect(ids.every((id) => id.startsWith("CED"))).toBe(true);
113
+ expect(ids.filter((id) => !declared.some((p) => id.startsWith(p)))).toEqual([]);
110
114
  expect(ids.length).toBeGreaterThanOrEqual(10);
111
115
  });
112
116
 
@@ -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,128 @@
1
+ /**
2
+ * Cedar policy-text rendering, shared by the `.cedar` serializer, the AVP
3
+ * statement renderer, and the dogwood dialect's `.dw` leg.
4
+ *
5
+ * Upstream's dogwood policy grammar is Cedar's shape — `annotation* effect
6
+ * "(" scope ")" cond* ";"` — and only the `cond` rule differs, gaining
7
+ * `guardrails { … }` and `temporal { … }` forms. So everything above the first
8
+ * clause is common, and it lives here rather than being copied into
9
+ * `./dogwood/serialize.ts`, where a change to how a scope constraint is
10
+ * written would have had to be made twice.
11
+ *
12
+ * `resolvePolicyId` is here for the same reason it was extracted from
13
+ * `serialize()` by #1652: it is the only rule linking a chant entity to a
14
+ * policy id, the AVP observation matches against it, and the `.dw` leg has to
15
+ * derive ids the same way the `.cedar` leg does. One copy, three callers.
16
+ *
17
+ * This module is a leaf on purpose: `./serializer.ts` and `./dogwood/*` both
18
+ * import it, and neither imports the other's rendering.
19
+ */
20
+
21
+ import type { Declarable } from "@intentius/chant/declarable";
22
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
23
+
24
+ /** A plain object — not null, not an array. */
25
+ export function isRecord(value: unknown): value is Record<string, unknown> {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+
29
+ /** The `props` of a resource-kind Declarable, or `{}` for anything else. */
30
+ export function getProps(entity: Declarable): Record<string, unknown> {
31
+ if (isResourceDeclarable(entity) && isRecord(entity.props)) {
32
+ return entity.props;
33
+ }
34
+ return {};
35
+ }
36
+
37
+ /** Escape a string for a double-quoted Cedar literal. */
38
+ export function escapeCedarString(value: string): string {
39
+ return value
40
+ .replace(/\\/g, "\\\\")
41
+ .replace(/"/g, '\\"')
42
+ .replace(/\n/g, "\\n")
43
+ .replace(/\r/g, "\\r")
44
+ .replace(/\t/g, "\\t");
45
+ }
46
+
47
+ /**
48
+ * Derive a policy id from a logical name: `allowAdminRead` → `allow-admin-read`.
49
+ * Cedar ids are free-form strings; kebab-case keeps them readable in the
50
+ * `@id` annotation and stable across a rename-free refactor.
51
+ */
52
+ export function policyIdFromLogicalName(name: string): string {
53
+ return name
54
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
55
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
56
+ .replace(/[\s_]+/g, "-")
57
+ .toLowerCase();
58
+ }
59
+
60
+ /**
61
+ * The Cedar id for a policy: an explicit `annotations.id` when the author gave
62
+ * one, else derived from the logical name.
63
+ *
64
+ * This is the only rule that links a chant entity to a policy in a live AVP
65
+ * store (#1652) — the observation resolves the same id from the same props and
66
+ * matches it against the `@id` annotation the statement carries — and it is
67
+ * also what gives a `.dw` policy the id its `.cedar` sibling would have had.
68
+ * Two copies of this rule would be a mapping that drifts silently.
69
+ */
70
+ export function resolvePolicyId(logicalName: string, props: Record<string, unknown>): string {
71
+ const explicit = isRecord(props.annotations) ? props.annotations.id : undefined;
72
+ return typeof explicit === "string" && explicit.length > 0
73
+ ? explicit
74
+ : policyIdFromLogicalName(logicalName);
75
+ }
76
+
77
+ function refText(value: unknown): string {
78
+ return String(value);
79
+ }
80
+
81
+ /** `principal`, `principal == User::"alice"`, `action in [ … ]`, `resource is Photo`. */
82
+ export function renderScope(variable: string, scope: unknown): string {
83
+ if (!isRecord(scope)) return variable;
84
+
85
+ const parts = [variable];
86
+ if (typeof scope.is === "string") parts.push(`is ${scope.is}`);
87
+ if (scope.eq !== undefined) parts.push(`== ${refText(scope.eq)}`);
88
+ if (scope.in !== undefined) {
89
+ parts.push(
90
+ Array.isArray(scope.in)
91
+ ? `in [${scope.in.map(refText).join(", ")}]`
92
+ : `in ${refText(scope.in)}`,
93
+ );
94
+ }
95
+ return parts.join(" ");
96
+ }
97
+
98
+ /** A `when`/`unless` prop as a list of clause bodies — one string, or several. */
99
+ export function conditionStrings(value: unknown): string[] {
100
+ if (value === undefined || value === null) return [];
101
+ if (Array.isArray(value)) return value.map(refText);
102
+ return [refText(value)];
103
+ }
104
+
105
+ /**
106
+ * Everything above the first clause: the annotations, the effect, and the
107
+ * three scope positions.
108
+ */
109
+ export function renderPolicyHead(id: string, props: Record<string, unknown>): string[] {
110
+ const lines: string[] = [];
111
+
112
+ // @id first, then the author's own annotations in declaration order.
113
+ const annotations = isRecord(props.annotations) ? props.annotations : {};
114
+ lines.push(`@id("${escapeCedarString(id)}")`);
115
+ for (const [key, value] of Object.entries(annotations)) {
116
+ if (key === "id" || value === undefined || value === null) continue;
117
+ lines.push(`@${key}("${escapeCedarString(refText(value))}")`);
118
+ }
119
+
120
+ const effect = props.effect === "forbid" ? "forbid" : "permit";
121
+ lines.push(`${effect} (`);
122
+ lines.push(` ${renderScope("principal", props.principal)},`);
123
+ lines.push(` ${renderScope("action", props.action)},`);
124
+ lines.push(` ${renderScope("resource", props.resource)}`);
125
+ lines.push(")");
126
+
127
+ return lines;
128
+ }