@intentius/chant-lexicon-cedar 0.44.8 → 0.44.9

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 (108) hide show
  1. package/README.md +58 -0
  2. package/dist/codegen/package.d.ts.map +1 -1
  3. package/dist/config.d.ts +25 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/dogwood/cli.d.ts +206 -0
  6. package/dist/dogwood/cli.d.ts.map +1 -0
  7. package/dist/dogwood/event-schema.d.ts +161 -0
  8. package/dist/dogwood/event-schema.d.ts.map +1 -0
  9. package/dist/dogwood/index.d.ts +33 -0
  10. package/dist/dogwood/index.d.ts.map +1 -0
  11. package/dist/dogwood/macros.d.ts +96 -0
  12. package/dist/dogwood/macros.d.ts.map +1 -0
  13. package/dist/dogwood/policy.d.ts +120 -0
  14. package/dist/dogwood/policy.d.ts.map +1 -0
  15. package/dist/dogwood/scan.d.ts +109 -0
  16. package/dist/dogwood/scan.d.ts.map +1 -0
  17. package/dist/dogwood/serialize.d.ts +46 -0
  18. package/dist/dogwood/serialize.d.ts.map +1 -0
  19. package/dist/dogwood/temporal.d.ts +259 -0
  20. package/dist/dogwood/temporal.d.ts.map +1 -0
  21. package/dist/dogwood/upstream.d.ts +41 -0
  22. package/dist/dogwood/upstream.d.ts.map +1 -0
  23. package/dist/dogwood/window.d.ts +73 -0
  24. package/dist/dogwood/window.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +13 -3
  28. package/dist/lint/audit-catalog.d.ts.map +1 -1
  29. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  30. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  31. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  32. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  33. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  34. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  35. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  36. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  37. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  38. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  39. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  40. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  41. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  42. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  43. package/dist/lint/post-synth/index.d.ts.map +1 -1
  44. package/dist/manifest.json +1 -1
  45. package/dist/okf/index.md +6 -0
  46. package/dist/okf/rules/DWDC010.md +11 -0
  47. package/dist/okf/rules/DWDC011.md +11 -0
  48. package/dist/okf/rules/DWDC012.md +11 -0
  49. package/dist/okf/rules/DWDE010.md +11 -0
  50. package/dist/okf/rules/DWDE011.md +11 -0
  51. package/dist/okf/rules/DWDS010.md +11 -0
  52. package/dist/policy-text.d.ts +53 -0
  53. package/dist/policy-text.d.ts.map +1 -0
  54. package/dist/rules/dogwood-helpers.ts +139 -0
  55. package/dist/rules/dwdc010.ts +62 -0
  56. package/dist/rules/dwdc011.ts +61 -0
  57. package/dist/rules/dwdc012.ts +46 -0
  58. package/dist/rules/dwde010.ts +130 -0
  59. package/dist/rules/dwde011.ts +108 -0
  60. package/dist/rules/dwds010.ts +46 -0
  61. package/dist/serializer.d.ts +10 -18
  62. package/dist/serializer.d.ts.map +1 -1
  63. package/dist/skills/chant-cedar-authoring.md +180 -0
  64. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  65. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  66. package/package.json +2 -2
  67. package/src/codegen/package.ts +3 -2
  68. package/src/config.test.ts +12 -0
  69. package/src/config.ts +28 -0
  70. package/src/dogwood/cli.test.ts +392 -0
  71. package/src/dogwood/cli.ts +545 -0
  72. package/src/dogwood/event-schema.test.ts +218 -0
  73. package/src/dogwood/event-schema.ts +318 -0
  74. package/src/dogwood/index.ts +198 -0
  75. package/src/dogwood/macros.test.ts +104 -0
  76. package/src/dogwood/macros.ts +229 -0
  77. package/src/dogwood/policy.test.ts +94 -0
  78. package/src/dogwood/policy.ts +141 -0
  79. package/src/dogwood/scan.ts +287 -0
  80. package/src/dogwood/serialize.test.ts +331 -0
  81. package/src/dogwood/serialize.ts +209 -0
  82. package/src/dogwood/temporal.test.ts +272 -0
  83. package/src/dogwood/temporal.ts +592 -0
  84. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  85. package/src/dogwood/testdata/default-macros.dw +23 -0
  86. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  87. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  88. package/src/dogwood/testdata/pinned.dwschema +31 -0
  89. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  90. package/src/dogwood/testdata/read-after-login.dw +17 -0
  91. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  92. package/src/dogwood/upstream.ts +41 -0
  93. package/src/dogwood/window.ts +124 -0
  94. package/src/index.ts +24 -0
  95. package/src/lint/audit-catalog.ts +56 -0
  96. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  97. package/src/lint/post-synth/dwd-post-synth.test.ts +256 -0
  98. package/src/lint/post-synth/dwdc010.ts +62 -0
  99. package/src/lint/post-synth/dwdc011.ts +61 -0
  100. package/src/lint/post-synth/dwdc012.ts +46 -0
  101. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  102. package/src/lint/post-synth/dwde010.ts +130 -0
  103. package/src/lint/post-synth/dwde011.ts +108 -0
  104. package/src/lint/post-synth/dwds010.ts +46 -0
  105. package/src/lint/post-synth/index.ts +12 -0
  106. package/src/lint/post-synth/post-synth.test.ts +7 -3
  107. package/src/policy-text.ts +128 -0
  108. package/src/serializer.ts +71 -109
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The dogwood temporal dialect (#1658, epic #1646).
3
+ *
4
+ * A dialect of the cedar lexicon, not a lexicon of its own: `.dw` policies are
5
+ * Cedar policies with temporal clauses, their entity and action schemas are
6
+ * Cedar schemas, and the rendering is shared with the `.cedar` leg. Checks
7
+ * ship under the `DWD` id family, declared on the cedar serializer's
8
+ * `extraRulePrefixes`.
9
+ *
10
+ * Pre-release. Upstream (dogwood-policy/dogwood, Apache-2.0) is a read-only
11
+ * squash-sync mirror with no tags, no releases and no stability statement, and
12
+ * calls itself "NOT intended for production use". The surface here is built
13
+ * against the revision recorded in `./upstream.ts` and re-verified per
14
+ * sub-issue.
15
+ */
16
+
17
+ export {
18
+ DEFAULT_MAX_WINDOW,
19
+ isWindowParam,
20
+ renderWindow,
21
+ renderWindowValue,
22
+ window,
23
+ windowParam,
24
+ windowSeconds,
25
+ windowValue,
26
+ } from "./window";
27
+ export type { TemporalWindow, TimeUnit, WindowLike, WindowParam, WindowValue } from "./window";
28
+
29
+ export {
30
+ and,
31
+ arrayOf,
32
+ binderName,
33
+ bool,
34
+ call,
35
+ compare,
36
+ count,
37
+ ctx,
38
+ decimalOf,
39
+ entityUid,
40
+ exists,
41
+ formerly,
42
+ int,
43
+ interval,
44
+ not,
45
+ predicate,
46
+ previous,
47
+ raw,
48
+ renderCondition,
49
+ renderTerm,
50
+ scopeRef,
51
+ sigilCondition,
52
+ since,
53
+ str,
54
+ sum,
55
+ temporalMarker,
56
+ term,
57
+ tp,
58
+ typedBinder,
59
+ varRef,
60
+ wildcard,
61
+ } from "./temporal";
62
+ export type {
63
+ AndNode,
64
+ ArrayTerm,
65
+ CallNode,
66
+ ComparisonNode,
67
+ ComparisonOperator,
68
+ CountTerm,
69
+ ExistsNode,
70
+ FormerlyNode,
71
+ IntervalArg,
72
+ MacroArg,
73
+ NotNode,
74
+ PredicateNode,
75
+ PreviousNode,
76
+ RawCondition,
77
+ SigilCondition,
78
+ SinceNode,
79
+ SumTerm,
80
+ TemporalCondition,
81
+ TemporalTerm,
82
+ TermInput,
83
+ TermText,
84
+ TpNode,
85
+ TypedBinder,
86
+ WindowArgument,
87
+ } from "./temporal";
88
+
89
+ export {
90
+ DEFAULT_MACRO_NAMES,
91
+ bind,
92
+ countDistinctWithin,
93
+ countWithin,
94
+ defCedarMacro,
95
+ defTemporalMacro,
96
+ defaultMacroLibrary,
97
+ macroCondition,
98
+ macroTerm,
99
+ macroWindow,
100
+ renderMacroDefinition,
101
+ renderMacroLibrary,
102
+ sumWithin,
103
+ } from "./macros";
104
+ export type { MacroDefinition, MacroKind } from "./macros";
105
+
106
+ export {
107
+ concrete,
108
+ declaredEventKinds,
109
+ defaultEventSchema,
110
+ eventDeclaration,
111
+ eventSchema,
112
+ field,
113
+ pinContext,
114
+ pinPrincipal,
115
+ pinResource,
116
+ pinnedField,
117
+ principalType,
118
+ record,
119
+ renderEventSchema,
120
+ resourceType,
121
+ spreadInputs,
122
+ spreadOutputs,
123
+ } from "./event-schema";
124
+ export type {
125
+ EventDeclaration,
126
+ EventField,
127
+ EventFieldType,
128
+ EventSchema,
129
+ EventSelector,
130
+ NamedField,
131
+ PinTarget,
132
+ SpreadField,
133
+ } from "./event-schema";
134
+
135
+ export {
136
+ DOGWOOD_EVENT_SCHEMA_FILENAME,
137
+ DOGWOOD_EVENT_SCHEMA_TYPE,
138
+ DOGWOOD_LEXICON,
139
+ DOGWOOD_MACRO_FILENAME,
140
+ DOGWOOD_MACRO_LIBRARY_TYPE,
141
+ DOGWOOD_POLICY_FILENAME,
142
+ DOGWOOD_POLICY_TYPE,
143
+ TemporalEventSchema,
144
+ TemporalMacroLibrary,
145
+ TemporalPolicy,
146
+ } from "./policy";
147
+ export type { EventSchemaProps, MacroLibraryProps, TemporalPolicyProps } from "./policy";
148
+
149
+ export {
150
+ DOGWOOD_EVENT_SCHEMA_SUFFIX,
151
+ DOGWOOD_POLICY_SUFFIX,
152
+ renderTemporalPolicyText,
153
+ serializeDogwood,
154
+ } from "./serialize";
155
+ export type { DogwoodSerializeResult } from "./serialize";
156
+
157
+ export {
158
+ blankComments,
159
+ dogwoodPolicyFiles,
160
+ dogwoodSchemaFiles,
161
+ effectiveMaxWindowSeconds,
162
+ readEventSchema,
163
+ scanPredicates,
164
+ scanWindowlessOperators,
165
+ scanWindows,
166
+ temporalRegions,
167
+ } from "./scan";
168
+ export type { DogwoodArtifact, EventSchemaFacts, TemporalPredicateRef, WindowRef } from "./scan";
169
+
170
+ export { DOGWOOD_UPSTREAM } from "./upstream";
171
+
172
+ export {
173
+ DOGWOOD_BINARY_ENV,
174
+ DOGWOOD_BINARY_NAME,
175
+ DOGWOOD_SEARCH_ORDER,
176
+ configureDogwoodCli,
177
+ findDogwoodBinary,
178
+ formatDogwoodDiagnostic,
179
+ parseLowerOutput,
180
+ parseValidateOutput,
181
+ resetDogwoodCli,
182
+ runDogwoodLower,
183
+ runDogwoodValidate,
184
+ } from "./cli";
185
+ export type {
186
+ DogwoodBinary,
187
+ DogwoodBinarySource,
188
+ DogwoodBundle,
189
+ DogwoodDiagnostic,
190
+ DogwoodFatal,
191
+ DogwoodLabel,
192
+ DogwoodLowerResult,
193
+ DogwoodLowered,
194
+ DogwoodRun,
195
+ DogwoodRunner,
196
+ DogwoodUnusable,
197
+ DogwoodValidateResult,
198
+ } from "./cli";
@@ -0,0 +1,104 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ DEFAULT_MACRO_NAMES,
4
+ bind,
5
+ defCedarMacro,
6
+ defTemporalMacro,
7
+ defaultMacroLibrary,
8
+ macroCondition,
9
+ macroTerm,
10
+ macroWindow,
11
+ renderMacroDefinition,
12
+ renderMacroLibrary,
13
+ } from "./macros";
14
+ import { and, count, formerly, renderCondition, tp, typedBinder } from "./temporal";
15
+
16
+ describe("def-macro authoring", () => {
17
+ test("a temporal macro renders as upstream's def_decl", () => {
18
+ const def = defTemporalMacro("once", ["?w", "?s"], formerly(macroWindow("?w"), macroCondition("?s")));
19
+ expect(renderMacroDefinition(def)).toBe(
20
+ "def temporal once(?w, ?s) {\n formerly within ?w ?s\n};",
21
+ );
22
+ });
23
+
24
+ test("a temporal macro body may be a bare aggregation value", () => {
25
+ const def = defTemporalMacro(
26
+ "count_within",
27
+ ["?w", "?s"],
28
+ count(
29
+ [typedBinder("$t", "Timepoint")],
30
+ formerly(macroWindow("?w"), and(macroCondition("?s"), tp("$t"))),
31
+ ),
32
+ );
33
+ expect(renderMacroDefinition(def)).toContain("count for ($t: Timepoint). where formerly within ?w (?s && tp($t))");
34
+ });
35
+
36
+ test("a cedar macro keeps its body as Cedar expression text", () => {
37
+ const def = defCedarMacro("is_small", ["?n"], "?n < 100");
38
+ expect(renderMacroDefinition(def)).toBe("def cedar is_small(?n) {\n ?n < 100\n};");
39
+ });
40
+
41
+ test("a comment is emitted above the definition", () => {
42
+ const def = defCedarMacro("is_small", ["?n"], "?n < 100", "Under a hundred.\nNothing more.");
43
+ expect(renderMacroDefinition(def).split("\n").slice(0, 2)).toEqual([
44
+ "// Under a hundred.",
45
+ "// Nothing more.",
46
+ ]);
47
+ });
48
+
49
+ test("a parameter without the ? sigil is refused, and the message says which to write", () => {
50
+ expect(() => defCedarMacro("is_small", ["n"], "n < 100")).toThrow(/carries the "\?" sigil.*"\?n"/s);
51
+ });
52
+
53
+ test("a macro name that is not an identifier is refused", () => {
54
+ expect(() => defCedarMacro("is small", [], "true")).toThrow(/must be an identifier/);
55
+ });
56
+ });
57
+
58
+ describe("the two sigils", () => {
59
+ test("?p splices a call-site value; $t is a macro-introduced fresh binder", () => {
60
+ expect(renderCondition(macroCondition("?s"))).toBe("?s");
61
+ expect(macroTerm("$t").text).toBe("$t");
62
+ expect(macroTerm("?a").text).toBe("?a");
63
+ });
64
+
65
+ test("a $ sigil is not a window parameter — the expander resolves only ?p there", () => {
66
+ expect(() => macroWindow("$w")).toThrow(/carries the "\?" sigil/);
67
+ });
68
+
69
+ test("a bare identifier is not a sigil", () => {
70
+ expect(() => macroCondition("s")).toThrow(/carries the "\?" sigil/);
71
+ expect(() => macroTerm("a")).toThrow(/carries a "\?" or "\$" sigil/);
72
+ });
73
+ });
74
+
75
+ describe("the default library", () => {
76
+ test("names exactly what upstream's default_macros.dw ships", () => {
77
+ expect([...DEFAULT_MACRO_NAMES]).toEqual([
78
+ "count_within",
79
+ "sum_within",
80
+ "count_distinct_within",
81
+ "bind",
82
+ ]);
83
+ expect(defaultMacroLibrary().map((d) => d.name)).toEqual([...DEFAULT_MACRO_NAMES]);
84
+ });
85
+
86
+ test("every definition is a temporal macro whose params all carry the sigil", () => {
87
+ for (const def of defaultMacroLibrary()) {
88
+ expect(def.kind).toBe("temporal");
89
+ for (const param of def.params) expect(param.startsWith("?")).toBe(true);
90
+ }
91
+ });
92
+
93
+ test("bind() is a call, so a policy using it depends on the callee's --macros", () => {
94
+ const node = bind("n", macroCondition("?A"), macroCondition("?B"));
95
+ expect(node.op).toBe("call");
96
+ expect(renderCondition(node)).toBe("bind(n, ?A, ?B)");
97
+ });
98
+
99
+ test("the library renders as a file a --macros flag can point at", () => {
100
+ const text = renderMacroLibrary(defaultMacroLibrary());
101
+ expect(text.endsWith("};\n")).toBe(true);
102
+ expect(text.split("\n\n")).toHaveLength(4);
103
+ });
104
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Macro authoring, and the default library as calls rather than operators.
3
+ *
4
+ * Upstream's policy grammar carries the macro system in the parser, not in the
5
+ * docs:
6
+ *
7
+ * ```
8
+ * def_decl = { kw_def ~ def_kind ~ ident ~ "(" ~ param_list? ~ ")" ~ "{" ~ block_inner ~ "}" ~ ";" }
9
+ * def_kind = { kw_def_cedar | kw_def_temporal }
10
+ * ```
11
+ *
12
+ * A `cedar` macro's body is a Cedar expression; a `temporal` macro's body is a
13
+ * temporal condition *or* a bare aggregation value (upstream's
14
+ * `temporal_macro_body_entry` tries `agg_expr` first, which is why
15
+ * `count_within` can expand into a `count …` that the call site compares).
16
+ *
17
+ * Two sigils, and they are not interchangeable. `?p` splices the call-site
18
+ * argument literally. `$t` is a fresh binder the macro introduces, gensym'd at
19
+ * every expansion. Both are legal *only* inside a macro body — upstream's
20
+ * well-formedness pass rejects them anywhere else — which is why they are
21
+ * validated here at definition time rather than at render time.
22
+ *
23
+ * The four wrappers at the bottom are the default library
24
+ * (`configuration/default_macros.dw`). They are calls, not operators. A caller
25
+ * who passes `--macros` replaces that library entirely, so a policy built on
26
+ * them is built on an assumption about the other end; {@link defaultMacroLibrary}
27
+ * exists so a project can emit the same definitions itself and stop assuming.
28
+ */
29
+
30
+ import {
31
+ call,
32
+ interval,
33
+ renderCondition,
34
+ renderTerm,
35
+ sigilCondition,
36
+ varRef,
37
+ type CallNode,
38
+ type SigilCondition,
39
+ type TemporalCondition,
40
+ type TemporalTerm,
41
+ type TermText,
42
+ } from "./temporal";
43
+ import { windowParam, type WindowLike, type WindowParam } from "./window";
44
+
45
+ /** `?w` — a value parameter, spliced from the call site. */
46
+ const PARAM = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
47
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
48
+
49
+ /** Which sub-parser reads the body: Cedar's, or the temporal sub-language's. */
50
+ export type MacroKind = "cedar" | "temporal";
51
+
52
+ /** One `def <kind> <name>(<params>) { <body> };` declaration. */
53
+ export interface MacroDefinition {
54
+ readonly kind: MacroKind;
55
+ readonly name: string;
56
+ /** Parameter names, each carrying the `?` sigil. */
57
+ readonly params: readonly string[];
58
+ /** Rendered body text. */
59
+ readonly body: string;
60
+ /** Emitted as `// …` lines above the definition. */
61
+ readonly comment?: string;
62
+ }
63
+
64
+ function assertMacroName(name: string): string {
65
+ if (!IDENT.test(name)) throw new Error(`dogwood: a macro name must be an identifier — got "${name}"`);
66
+ return name;
67
+ }
68
+
69
+ function assertParams(params: readonly string[]): readonly string[] {
70
+ for (const p of params) {
71
+ if (!PARAM.test(p)) {
72
+ throw new Error(`dogwood: a macro parameter carries the "?" sigil — got "${p}" (write "?${p.replace(/^\?/, "")}")`);
73
+ }
74
+ }
75
+ return params;
76
+ }
77
+
78
+ /**
79
+ * `def temporal name(?a, ?b) { … };`
80
+ *
81
+ * The body may be a condition or an aggregation term, matching upstream's
82
+ * `temporal_macro_body_entry`; `sum_within` is an aggregation-bodied macro and
83
+ * `once` is a condition-bodied one.
84
+ */
85
+ export function defTemporalMacro(
86
+ name: string,
87
+ params: string[],
88
+ body: TemporalCondition | TemporalTerm,
89
+ comment?: string,
90
+ ): MacroDefinition {
91
+ return {
92
+ kind: "temporal",
93
+ name: assertMacroName(name),
94
+ params: assertParams(params),
95
+ body: isTermBody(body) ? renderTerm(body) : renderCondition(body),
96
+ comment,
97
+ };
98
+ }
99
+
100
+ function isTermBody(body: TemporalCondition | TemporalTerm): body is TemporalTerm {
101
+ return body.op === "term" || body.op === "array" || body.op === "count" || body.op === "sum";
102
+ }
103
+
104
+ /**
105
+ * `def cedar name(?a) { … };` — the body is Cedar expression text.
106
+ *
107
+ * Cedar's expression grammar is the policy language itself, so it stays a
108
+ * string here for the same reason `Cedar::Policy`'s `when` does.
109
+ */
110
+ export function defCedarMacro(
111
+ name: string,
112
+ params: string[],
113
+ body: string,
114
+ comment?: string,
115
+ ): MacroDefinition {
116
+ return { kind: "cedar", name: assertMacroName(name), params: assertParams(params), body, comment };
117
+ }
118
+
119
+ // ── The sigils, in the three positions they appear ────────────────
120
+
121
+ /**
122
+ * `within ?w` — the window a macro body defers to its call site.
123
+ *
124
+ * The call site supplies a bare interval (`once(1h, …)`), not a `within`
125
+ * clause: the keyword belongs to the operator, so it stays in the body.
126
+ */
127
+ export function macroWindow(param: string): WindowParam {
128
+ return windowParam(param);
129
+ }
130
+
131
+ /** `?s` standing for an entire condition, spliced from the call site. */
132
+ export function macroCondition(param: string): SigilCondition {
133
+ if (!PARAM.test(param)) throw new Error(`dogwood: a condition parameter carries the "?" sigil — got "${param}"`);
134
+ return sigilCondition(param);
135
+ }
136
+
137
+ /** `?a` / `$t` in term or binder position. */
138
+ export function macroTerm(sigil: string): TermText {
139
+ if (!/^[?$][A-Za-z_][A-Za-z0-9_]*$/.test(sigil)) {
140
+ throw new Error(`dogwood: a macro term carries a "?" or "$" sigil — got "${sigil}"`);
141
+ }
142
+ return varRef(sigil);
143
+ }
144
+
145
+ /** One definition as `.dw` source. */
146
+ export function renderMacroDefinition(def: MacroDefinition): string {
147
+ const lines: string[] = [];
148
+ if (def.comment) {
149
+ for (const line of def.comment.split("\n")) lines.push(`// ${line}`.trimEnd());
150
+ }
151
+ lines.push(`def ${def.kind} ${def.name}(${def.params.join(", ")}) {`);
152
+ lines.push(` ${def.body}`);
153
+ lines.push("};");
154
+ return lines.join("\n");
155
+ }
156
+
157
+ /** A whole library — the file `--macros` points at, or a policy set's own prelude. */
158
+ export function renderMacroLibrary(defs: readonly MacroDefinition[]): string {
159
+ return defs.map(renderMacroDefinition).join("\n\n") + "\n";
160
+ }
161
+
162
+ // ── The default library, as calls ─────────────────────────────────
163
+
164
+ /** The macro names `configuration/default_macros.dw` ships at the pinned revision. */
165
+ export const DEFAULT_MACRO_NAMES = ["count_within", "sum_within", "count_distinct_within", "bind"] as const;
166
+
167
+ /**
168
+ * `count_within(?w, ?s)` — timepoints in the window at which `?s` held.
169
+ *
170
+ * A default-library call, not an operator. See this module's header.
171
+ */
172
+ export function countWithin(w: WindowLike, condition: TemporalCondition): CallNode {
173
+ return call("count_within", [interval(w), condition]);
174
+ }
175
+
176
+ /** `sum_within(?a, ?w, ?body)` — `over` names the sum's bound variable. */
177
+ export function sumWithin(over: string, w: WindowLike, body: TemporalCondition): CallNode {
178
+ return call("sum_within", [varRef(over), interval(w), body]);
179
+ }
180
+
181
+ /** `count_distinct_within(?k, ?w, ?s)` — distinct String keys `k` for which `s` held. */
182
+ export function countDistinctWithin(key: string, w: WindowLike, condition: TemporalCondition): CallNode {
183
+ return call("count_distinct_within", [varRef(key), interval(w), condition]);
184
+ }
185
+
186
+ /** `bind(?n, ?A, ?B)` — names an aggregate result so it can be compared. */
187
+ export function bind(name: string, aggregate: TemporalCondition, body: TemporalCondition): CallNode {
188
+ return call("bind", [varRef(name), aggregate, body]);
189
+ }
190
+
191
+ /**
192
+ * The default library's four definitions, verbatim from upstream's
193
+ * `dogwood-language/configuration/default_macros.dw` at the pinned revision.
194
+ *
195
+ * Emitting these into a project's own `macros.dw` turns an assumption about
196
+ * the callee's `--macros` into a file the project controls.
197
+ */
198
+ export function defaultMacroLibrary(): MacroDefinition[] {
199
+ return [
200
+ {
201
+ kind: "temporal",
202
+ name: "count_within",
203
+ params: ["?w", "?s"],
204
+ body: "count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))",
205
+ comment: "Counts the timepoints within window `?w` at which condition `?s` held.",
206
+ },
207
+ {
208
+ kind: "temporal",
209
+ name: "sum_within",
210
+ params: ["?a", "?w", "?body"],
211
+ body: "sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))",
212
+ comment: "Sums the numeric value `?a` over occurrences of `?body` within window `?w`.",
213
+ },
214
+ {
215
+ kind: "temporal",
216
+ name: "count_distinct_within",
217
+ params: ["?k", "?w", "?s"],
218
+ body: "count for (?k: String). where (formerly within ?w ?s)",
219
+ comment: "Counts the distinct key values `?k` for which `?s` held within window `?w`.",
220
+ },
221
+ {
222
+ kind: "temporal",
223
+ name: "bind",
224
+ params: ["?n", "?A", "?B"],
225
+ body: "exists (?n: Long). (?A == ?n && ?B)",
226
+ comment: 'A "let"-style binder: names an aggregate result `?n` and uses it in `?B`.',
227
+ },
228
+ ];
229
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The entity classes, driven the way a policy file drives them.
3
+ *
4
+ * The golden serializer test builds its entities by hand so the rendering is
5
+ * pinned independently of the runtime factory. This one closes the other half:
6
+ * that `new TemporalPolicy({ … })` produces a Declarable the cedar serializer
7
+ * recognises, with the props it was given.
8
+ */
9
+ import { describe, test, expect } from "vitest";
10
+ import type { Declarable } from "@intentius/chant/declarable";
11
+ import { cedarSerializer } from "../serializer";
12
+ import {
13
+ DOGWOOD_EVENT_SCHEMA_FILENAME,
14
+ DOGWOOD_EVENT_SCHEMA_TYPE,
15
+ DOGWOOD_LEXICON,
16
+ DOGWOOD_MACRO_FILENAME,
17
+ DOGWOOD_POLICY_FILENAME,
18
+ DOGWOOD_POLICY_TYPE,
19
+ TemporalEventSchema,
20
+ TemporalMacroLibrary,
21
+ TemporalPolicy,
22
+ } from "./policy";
23
+ import { ctx, formerly, predicate } from "./temporal";
24
+ import { defaultEventSchema } from "./event-schema";
25
+ import { defaultMacroLibrary } from "./macros";
26
+
27
+ function files(entities: Map<string, Declarable>): Record<string, string> {
28
+ const out = cedarSerializer.serialize(entities);
29
+ return typeof out === "string" ? {} : (out.files ?? {});
30
+ }
31
+
32
+ describe("the dogwood entity classes", () => {
33
+ test("a TemporalPolicy is a cedar-lexicon Declarable of its own entity type", () => {
34
+ const policy = new TemporalPolicy({ whenTemporal: [] });
35
+ expect(policy.entityType).toBe(DOGWOOD_POLICY_TYPE);
36
+ expect(policy.lexicon).toBe(DOGWOOD_LEXICON);
37
+ expect(policy.kind).toBe("resource");
38
+ });
39
+
40
+ test("the dialect's entity types are namespaced apart from Cedar::Policy", () => {
41
+ expect(DOGWOOD_POLICY_TYPE).toBe("Dogwood::TemporalPolicy");
42
+ expect(DOGWOOD_EVENT_SCHEMA_TYPE).toBe("Dogwood::EventSchema");
43
+ });
44
+
45
+ test("a declared policy reaches the .dw output through the serializer", () => {
46
+ const entities = new Map<string, Declarable>([
47
+ [
48
+ "readAfterLogin",
49
+ new TemporalPolicy({
50
+ action: { eq: 'Drupe::Action::"Read"' },
51
+ principal: { is: "Drupe::OAuthUser" },
52
+ annotations: { owner: "platform" },
53
+ whenTemporal: [
54
+ formerly("1h", predicate('Drupe::Action::"Login"', "response", { "input.user": ctx("input.user") })),
55
+ ],
56
+ }),
57
+ ],
58
+ ]);
59
+
60
+ const text = files(entities)[DOGWOOD_POLICY_FILENAME];
61
+ expect(text).toContain('@id("read-after-login")');
62
+ expect(text).toContain('@owner("platform")');
63
+ expect(text).toContain(" principal is Drupe::OAuthUser,");
64
+ expect(text).toContain(' action == Drupe::Action::"Read",');
65
+ expect(text).toContain("formerly within 1h");
66
+ });
67
+
68
+ test("an event schema and a macro library each reach their own file", () => {
69
+ const entities = new Map<string, Declarable>([
70
+ ["events", new TemporalEventSchema({ schema: defaultEventSchema({ maxWindow: "12h" }) })],
71
+ ["macros", new TemporalMacroLibrary({ macros: defaultMacroLibrary() })],
72
+ ]);
73
+
74
+ const written = files(entities);
75
+ expect(written[DOGWOOD_EVENT_SCHEMA_FILENAME]).toContain("max_window = 12h");
76
+ expect(written[DOGWOOD_MACRO_FILENAME]).toContain("def temporal count_within(?w, ?s) {");
77
+ });
78
+
79
+ test("an inline macro library lands in the policy set, not a separate file", () => {
80
+ const entities = new Map<string, Declarable>([
81
+ ["macros", new TemporalMacroLibrary({ macros: defaultMacroLibrary(), inline: true })],
82
+ [
83
+ "p",
84
+ new TemporalPolicy({
85
+ whenTemporal: [formerly("1h", predicate('Ns::Action::"A"', "request"))],
86
+ }),
87
+ ],
88
+ ]);
89
+
90
+ const written = files(entities);
91
+ expect(written[DOGWOOD_MACRO_FILENAME]).toBeUndefined();
92
+ expect(written[DOGWOOD_POLICY_FILENAME]).toContain("def temporal count_within(?w, ?s) {");
93
+ });
94
+ });