@intentius/chant-lexicon-cedar 0.124.31 → 0.124.33

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 (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Typed builders for dogwood's temporal sub-language.
3
+ *
4
+ * These target the **parser primitives**, not the named aggregates. The #1657
5
+ * verification (and the correction comment on epic #1646) is explicit about
6
+ * why: `formerly`, `previous`, `since`, `exists`, `tp()`, `count for … where`
7
+ * and `sum … for … where` are the only temporal keywords in upstream's
8
+ * `extension/temporal/grammar.pest`. `count_within`, `sum_within`,
9
+ * `count_distinct_within` and `bind` are macros in `default_macros.dw` — a
10
+ * swappable standard library that a caller passing `--macros` replaces
11
+ * wholesale — and `once` is not even in that library, only in the examples.
12
+ *
13
+ * So the aggregates are expressible here as macro *calls* ({@link call}, and
14
+ * the convenience wrappers in `./macros.ts`), never as first-class operators.
15
+ * A call that resolves to nothing at the other end is a missing macro, which
16
+ * is a comprehensible failure; a first-class builder that silently emits a
17
+ * name the callee's library does not define is not.
18
+ *
19
+ * Everything below renders to the surface syntax the pest grammar accepts.
20
+ * Where the grammar's precedence would bind differently from the tree the
21
+ * author built, the renderer parenthesises — it never relies on the reader
22
+ * knowing that `!` binds tighter than `since`, or that an aggregate's `where`
23
+ * body is greedy.
24
+ */
25
+ import { escapeCedarString } from "../policy-text.js";
26
+ import { renderWindow, renderWindowValue, window, windowValue, } from "./window.js";
27
+ // ── Names ─────────────────────────────────────────────────────────
28
+ /** A bare binder or field name. */
29
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+ /** `?p` (call-site value splice) or `$t` (macro-introduced fresh binder). */
31
+ const SIGIL = /^[?$][A-Za-z_][A-Za-z0-9_]*$/;
32
+ /** `input.user`, `output.result` — the dotted path a predicate argument names. */
33
+ const FIELD_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
34
+ /** `Drupe::Action::"Read"` — a qualified action with a quoted id. */
35
+ const QUALIFIED_ACTION = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*::"[^"]*"$/;
36
+ /** `String`, `Long`, `Timepoint`, `MyApp::OAuthUser`. */
37
+ const TYPE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$/;
38
+ function assertName(pattern, value, what) {
39
+ if (!pattern.test(value))
40
+ throw new Error(`dogwood: ${what} — got "${value}"`);
41
+ return value;
42
+ }
43
+ /**
44
+ * A binder position: a plain identifier, or a macro sigil.
45
+ *
46
+ * The sigils are legal only inside a macro body; upstream's well-formedness
47
+ * pass rejects them elsewhere. `./macros.ts` is where they belong, and it is
48
+ * what carries that warning to the author.
49
+ */
50
+ export function binderName(value) {
51
+ if (IDENT.test(value) || SIGIL.test(value))
52
+ return value;
53
+ throw new Error(`dogwood: a binder must be an identifier, or a "?param"/"$binder" sigil inside a macro body — got "${value}"`);
54
+ }
55
+ /** `(t: Timepoint)`, `(amount: Long)`, `($t: Timepoint)` inside a macro. */
56
+ export function typedBinder(name, type) {
57
+ return { name: binderName(name), type: assertName(TYPE_NAME, type, "a binder type must be a Cedar type name") };
58
+ }
59
+ function liftTerm(value) {
60
+ if (typeof value === "number")
61
+ return int(value);
62
+ if (typeof value === "boolean")
63
+ return bool(value);
64
+ return value;
65
+ }
66
+ /** A raw term, emitted verbatim. The escape hatch for a shape not modelled here. */
67
+ export function term(text) {
68
+ return { op: "term", text };
69
+ }
70
+ /** A Cedar string literal: `"alice"`. */
71
+ export function str(value) {
72
+ return term(`"${escapeCedarString(value)}"`);
73
+ }
74
+ /** An integer literal. Upstream's grammar admits a leading `-`. */
75
+ export function int(value) {
76
+ if (!Number.isInteger(value)) {
77
+ throw new Error(`dogwood: an integer term must be a whole number — use decimalOf() for ${value}`);
78
+ }
79
+ return term(String(value));
80
+ }
81
+ /** `true` / `false`. */
82
+ export function bool(value) {
83
+ return term(value ? "true" : "false");
84
+ }
85
+ /** `decimal("1.50")` — Cedar's decimal constructor, which dogwood admits as a term. */
86
+ export function decimalOf(value) {
87
+ return term(`decimal("${escapeCedarString(value)}")`);
88
+ }
89
+ /** `context.input.user` — a field of the request context record. */
90
+ export function ctx(path) {
91
+ assertName(FIELD_PATH, path, "a context path must be dotted identifiers");
92
+ return term(`context.${path}`);
93
+ }
94
+ /** `principal`, `resource`, `principal.dept` — the request scope entities. */
95
+ export function scopeRef(root, ...attrs) {
96
+ for (const attr of attrs)
97
+ assertName(IDENT, attr, "a scope attribute must be an identifier");
98
+ return term([root, ...attrs].join("."));
99
+ }
100
+ /** `Drupe::OAuthUser::"alice"` — an entity UID literal. */
101
+ export function entityUid(uid) {
102
+ assertName(QUALIFIED_ACTION, uid, 'an entity UID looks like Ns::Type::"id"');
103
+ return term(uid);
104
+ }
105
+ /** A bare binder reference — a `for`-declared variable, or a macro sigil. */
106
+ export function varRef(name) {
107
+ return term(binderName(name));
108
+ }
109
+ /** `*` — the grammar's wildcard term. */
110
+ export function wildcard() {
111
+ return term("*");
112
+ }
113
+ /** `[a, b]`. */
114
+ export function arrayOf(...items) {
115
+ return { op: "array", items: items.map(liftTerm) };
116
+ }
117
+ /**
118
+ * `count for (t: Timepoint). where φ` — the primitive behind `count_within`.
119
+ *
120
+ * The `for` list is the aggregation domain and is mandatory in the grammar,
121
+ * so it is a required argument here too.
122
+ */
123
+ export function count(binders, where) {
124
+ if (binders.length === 0)
125
+ throw new Error("dogwood: count needs at least one typed binder in its for-list");
126
+ return { op: "count", binders, where };
127
+ }
128
+ /** `sum a for (a: Long), (t: Timepoint). where φ` — the primitive behind `sum_within`. */
129
+ export function sum(over, binders, where) {
130
+ if (binders.length === 0)
131
+ throw new Error("dogwood: sum needs at least one typed binder in its for-list");
132
+ return { op: "sum", over: binderName(over), binders, where };
133
+ }
134
+ // ── Condition builders ────────────────────────────────────────────
135
+ /**
136
+ * `Ns::Action::"Name"::kind{ field: term, … }`.
137
+ *
138
+ * The event kind is mandatory in the grammar and author-defined in practice —
139
+ * `request`/`response`/`error` are the conventional kinds the default event
140
+ * schema declares, not a fixed set. Which kinds are legal for a given policy
141
+ * set is the emitted `.dwschema`'s business, and DWDC010's.
142
+ */
143
+ export function predicate(action, kind, args = {}) {
144
+ assertName(QUALIFIED_ACTION, action, 'a predicate action looks like Ns::Action::"Name"');
145
+ assertName(IDENT, kind, "an event kind must be an identifier");
146
+ return {
147
+ op: "predicate",
148
+ action,
149
+ kind,
150
+ args: Object.entries(args).map(([path, value]) => ({
151
+ path: assertName(FIELD_PATH, path, "a predicate field path must be dotted identifiers"),
152
+ value: liftTerm(value),
153
+ })),
154
+ };
155
+ }
156
+ /** `formerly within <w> φ` — held at some point in the window. */
157
+ export function formerly(w, body) {
158
+ return { op: "formerly", window: windowValue(w), body };
159
+ }
160
+ /** `previous within <w> φ` — held at the immediately preceding timepoint in the window. */
161
+ export function previous(w, body) {
162
+ return { op: "previous", window: windowValue(w), body };
163
+ }
164
+ /** `φ since within <w> ψ` — φ has held continuously since ψ, inside the window. */
165
+ export function since(left, w, right) {
166
+ return { op: "since", left, window: windowValue(w), right };
167
+ }
168
+ /** `exists (name: Type). φ`. */
169
+ export function exists(binder, body) {
170
+ return { op: "exists", binder, body };
171
+ }
172
+ /** `tp(t)`. */
173
+ export function tp(binder) {
174
+ return { op: "tp", binder: binderName(binder) };
175
+ }
176
+ /** `φ && ψ && …`. A single operand collapses to itself. */
177
+ export function and(...operands) {
178
+ if (operands.length === 0)
179
+ throw new Error("dogwood: and() needs at least one operand");
180
+ if (operands.length === 1)
181
+ return operands[0];
182
+ return { op: "and", operands };
183
+ }
184
+ /** `!φ`. */
185
+ export function not(body) {
186
+ return { op: "not", body };
187
+ }
188
+ /** `a <op> b`. */
189
+ export function compare(left, operator, right) {
190
+ return { op: "compare", left: liftTerm(left), operator, right: liftTerm(right) };
191
+ }
192
+ /**
193
+ * A macro invocation.
194
+ *
195
+ * Nothing here checks that the macro exists: the library is `--macros`, chosen
196
+ * by whoever runs `dogwood validate`, and chant is not that. `./macros.ts`
197
+ * carries typed wrappers for the four the default library ships.
198
+ */
199
+ export function call(name, args = []) {
200
+ assertName(IDENT, name, "a macro name must be an identifier");
201
+ return { op: "call", name, args };
202
+ }
203
+ /** `1h` in macro-call argument position — the interval without the `within`. */
204
+ export function interval(w) {
205
+ return { op: "interval", window: window(w) };
206
+ }
207
+ /** Temporal source text, passed through untouched. See {@link RawCondition}. */
208
+ export function raw(text) {
209
+ return { op: "raw", text };
210
+ }
211
+ /** `?s` in condition position. See {@link SigilCondition}. */
212
+ export function sigilCondition(param) {
213
+ if (!SIGIL.test(param))
214
+ throw new Error(`dogwood: a condition sigil looks like "?s" — got "${param}"`);
215
+ return { op: "sigil", param };
216
+ }
217
+ // ── Rendering ─────────────────────────────────────────────────────
218
+ function isTerm(node) {
219
+ return node.op === "term" || node.op === "array" || node.op === "count" || node.op === "sum";
220
+ }
221
+ function isAggregate(t) {
222
+ return t.op === "count" || t.op === "sum";
223
+ }
224
+ /** Render a term. */
225
+ export function renderTerm(t) {
226
+ switch (t.op) {
227
+ case "term":
228
+ return t.text;
229
+ case "array":
230
+ return `[${t.items.map(renderTerm).join(", ")}]`;
231
+ case "count":
232
+ return `count ${renderForBinders(t.binders)} where ${renderCondition(t.where)}`;
233
+ case "sum":
234
+ return `sum ${t.over} ${renderForBinders(t.binders)} where ${renderCondition(t.where)}`;
235
+ case "call":
236
+ return renderCall(t);
237
+ }
238
+ }
239
+ function renderForBinders(binders) {
240
+ return `for ${binders.map((b) => `(${b.name}: ${b.type})`).join(", ")}.`;
241
+ }
242
+ /**
243
+ * A comparison operand.
244
+ *
245
+ * An aggregate's `where` body is greedy, so `count for (…). where φ == 3`
246
+ * reads `== 3` as part of φ. Upstream's `paren_agg` rule exists for exactly
247
+ * this and is always legal, so aggregates and macro calls are parenthesised on
248
+ * both sides rather than only where the greed would actually bite — the
249
+ * emitted text should not depend on which side of the operator it landed on.
250
+ */
251
+ function renderComparisonOperand(t) {
252
+ return isAggregate(t) || t.op === "call" ? `(${renderTerm(t)})` : renderTerm(t);
253
+ }
254
+ function renderCall(node) {
255
+ const args = node.args.map((arg) => {
256
+ if (arg.op === "interval")
257
+ return renderWindow(arg.window);
258
+ if (isTerm(arg))
259
+ return renderTerm(arg);
260
+ return renderCondition(arg);
261
+ });
262
+ return `${node.name}(${args.join(", ")})`;
263
+ }
264
+ /**
265
+ * An `atom` — the operand of `formerly`, `previous` and the right of `since`.
266
+ *
267
+ * The grammar's `atom` is `"(" condition ")" | tp_op | call | refinable |
268
+ * comparison`; everything else has to be parenthesised.
269
+ */
270
+ function renderAtom(node) {
271
+ switch (node.op) {
272
+ case "predicate":
273
+ case "sigil":
274
+ case "tp":
275
+ case "call":
276
+ case "compare":
277
+ return renderCondition(node);
278
+ default:
279
+ return `(${renderCondition(node)})`;
280
+ }
281
+ }
282
+ /**
283
+ * A `neg_conjunct` — what `!` negates, and what sits left of `since`.
284
+ *
285
+ * `!` binds tighter than `since` and `&&`, so `!a since within W b` negates
286
+ * only `a`. Anything looser gets parentheses.
287
+ */
288
+ function renderNegConjunct(node) {
289
+ switch (node.op) {
290
+ case "and":
291
+ case "since":
292
+ case "exists":
293
+ return `(${renderCondition(node)})`;
294
+ default:
295
+ return renderCondition(node);
296
+ }
297
+ }
298
+ /**
299
+ * A `conjunct_or_since` — one operand of an `&&` chain.
300
+ *
301
+ * `since` is allowed here bare (it is part of the same rule). `exists` binds
302
+ * maximally to the right, so an unparenthesised one would swallow every
303
+ * operand after it.
304
+ */
305
+ function renderConjunct(node) {
306
+ switch (node.op) {
307
+ case "and":
308
+ case "exists":
309
+ return `(${renderCondition(node)})`;
310
+ default:
311
+ return renderCondition(node);
312
+ }
313
+ }
314
+ /** Render a temporal condition to the surface syntax the pest grammar accepts. */
315
+ export function renderCondition(node) {
316
+ switch (node.op) {
317
+ case "raw":
318
+ return node.text;
319
+ case "sigil":
320
+ return node.param;
321
+ case "predicate": {
322
+ const args = node.args.map((a) => `${a.path}: ${renderTerm(a.value)}`).join(", ");
323
+ return `${node.action}::${node.kind}{${args.length > 0 ? ` ${args} ` : ""}}`;
324
+ }
325
+ case "formerly":
326
+ return `formerly within ${renderWindowValue(node.window)} ${renderAtom(node.body)}`;
327
+ case "previous":
328
+ return `previous within ${renderWindowValue(node.window)} ${renderAtom(node.body)}`;
329
+ case "since":
330
+ return `${renderNegConjunct(node.left)} since within ${renderWindowValue(node.window)} ${renderAtom(node.right)}`;
331
+ case "exists":
332
+ return `exists (${node.binder.name}: ${node.binder.type}). ${renderCondition(node.body)}`;
333
+ case "tp":
334
+ return `tp(${node.binder})`;
335
+ case "and":
336
+ return node.operands.map(renderConjunct).join(" && ");
337
+ case "not":
338
+ return `!${renderNegConjunct(node.body)}`;
339
+ case "compare":
340
+ return `${renderComparisonOperand(node.left)} ${node.operator} ${renderComparisonOperand(node.right)}`;
341
+ case "call":
342
+ return renderCall(node);
343
+ }
344
+ }
345
+ /**
346
+ * `temporal { … }` — the extension marker.
347
+ *
348
+ * It is a `primary` in the Cedar expression grammar as well as a clause tag,
349
+ * so this is also how a temporal condition is embedded mid-expression:
350
+ * `when { is_small(context.input.shares) && temporal { once(1h, …) } }`.
351
+ */
352
+ export function temporalMarker(condition) {
353
+ return `temporal { ${renderCondition(condition)} }`;
354
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Typed construction of dogwood replay traces (#1661, epic #1646).
3
+ *
4
+ * A trace is one event per line, and the line grammar recorded by the #1657
5
+ * verification (§6, read from `dogwood-language/src/interpreter/log_parse.rs`
6
+ * at the pinned SHA) is:
7
+ *
8
+ * ```
9
+ * @<timestamp> [scope(principal: <uid>, resource: <uid>)]
10
+ * [entities(<uid>: { <attrs> } [in [<uid>, …]], …)]
11
+ * [request_context(<group>: { … }, …)]
12
+ * <Ns>::Action::"<Name>"::<kind>(<field>: <value>, …)
13
+ * ```
14
+ *
15
+ * The three envelopes are optional and must appear in that order; the trailing
16
+ * group is the logged record. There is no comment syntax — a `//` is an
17
+ * ordinary part of a value, so URLs survive — blank lines are skipped, and a
18
+ * leading BOM is stripped.
19
+ *
20
+ * Two things in that format silently weaken a replay rather than failing it,
21
+ * and both are what this module exists to make hard:
22
+ *
23
+ * 1. **`request_context(…)` and the logged record are different bags.** The
24
+ * Cedar request is built from the first; temporal predicates match against
25
+ * the second. A field needed by both and supplied to only one weakens
26
+ * either the Cedar check or the temporal check, and the replay still exits
27
+ * 0 with a verdict that looks authoritative. So {@link traceEvent} writes a
28
+ * `context` group into *both* bags by default, and dropping one is an
29
+ * explicit per-event `bags` opt-out that has to be typed out.
30
+ * 2. **Actions must be fully qualified.** `Drupe::Action::"Transfer"`, never
31
+ * `Transfer` — a short name leaves the temporal predicate unmatched while
32
+ * Cedar still authorizes. {@link traceEvent} rejects anything that is not
33
+ * `Ns::…::"Name"` at construction.
34
+ *
35
+ * {@link auditTrace} is the same two checks applied to a trace this module did
36
+ * not build — the AgentCore session history #1661 leaves to the aws lexicon,
37
+ * or a `.log` recorded by hand. A trace is a trace, wherever it came from.
38
+ */
39
+ import { escapeCedarString } from "../policy-text.js";
40
+ // ── Names ─────────────────────────────────────────────────────────
41
+ /** `Drupe::Action::"Read"`, `Drupe::OAuthUser::"alice"` — a qualified uid. */
42
+ const QUALIFIED_UID = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)+::"[^"]*"$/;
43
+ /** A bare field, group or event-kind name. */
44
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
45
+ function assertUid(value, what) {
46
+ if (!QUALIFIED_UID.test(value)) {
47
+ throw new Error(`dogwood: ${what} must be fully qualified, like Ns::Type::"id" — got "${value}"`);
48
+ }
49
+ return value;
50
+ }
51
+ function assertIdent(value, what) {
52
+ if (!IDENT.test(value))
53
+ throw new Error(`dogwood: ${what} must be an identifier — got "${value}"`);
54
+ return value;
55
+ }
56
+ /** `Ns::Type::"id"`. Validated at construction, so a short name cannot slip through. */
57
+ export function entityRef(uid) {
58
+ return { traceValue: "entity", uid: assertUid(uid, "an entity reference") };
59
+ }
60
+ /** A Cedar decimal, written as it should appear (`"1.50"`). */
61
+ export function decimalValue(text) {
62
+ if (!/^-?\d+\.\d+$/.test(text)) {
63
+ throw new Error(`dogwood: a trace decimal looks like "1.50" — got "${text}"`);
64
+ }
65
+ return { traceValue: "decimal", text };
66
+ }
67
+ /** Surface text, rendered verbatim. For values this module has no shape for. */
68
+ export function rawValue(text) {
69
+ return { traceValue: "raw", text };
70
+ }
71
+ function isTagged(value) {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "traceValue" in value;
73
+ }
74
+ /** Render one value in the Cedar surface form the trace parser reads. */
75
+ export function renderTraceValue(value) {
76
+ if (typeof value === "string")
77
+ return `"${escapeCedarString(value)}"`;
78
+ if (typeof value === "boolean")
79
+ return value ? "true" : "false";
80
+ if (typeof value === "number") {
81
+ if (!Number.isInteger(value)) {
82
+ throw new Error(`dogwood: ${String(value)} is not an integer — a trace decimal must be written with decimalValue("1.50") so its scale survives`);
83
+ }
84
+ return String(value);
85
+ }
86
+ if (Array.isArray(value))
87
+ return `[${value.map(renderTraceValue).join(", ")}]`;
88
+ if (isTagged(value)) {
89
+ if (value.traceValue === "entity")
90
+ return value.uid;
91
+ if (value.traceValue === "decimal")
92
+ return value.text;
93
+ return value.text;
94
+ }
95
+ return renderTraceFields(value);
96
+ }
97
+ /** `{ a: 1, b: "x" }` — a record body, braces included. */
98
+ export function renderTraceFields(fields) {
99
+ const parts = Object.entries(fields).map(([key, value]) => `${assertIdent(key, "a trace field name")}: ${renderTraceValue(value)}`);
100
+ return `{ ${parts.join(", ")} }`;
101
+ }
102
+ /** `Ns::Type::"id": { … } in [ … ]`. */
103
+ export function traceEntity(uid, attrs, parents) {
104
+ return {
105
+ uid: assertUid(uid, "an entity declaration"),
106
+ ...(attrs ? { attrs } : {}),
107
+ ...(parents ? { parents: parents.map((p) => assertUid(p, "an entity parent")) } : {}),
108
+ };
109
+ }
110
+ /**
111
+ * Build one trace event with both bags populated.
112
+ *
113
+ * Every `context` group is written to the `request_context` envelope *and*
114
+ * merged into the logged record, because that is the only combination in which
115
+ * the Cedar check and the temporal check both see the field. Dropping one is a
116
+ * typed decision (`bags`), not an omission.
117
+ */
118
+ export function traceEvent(input) {
119
+ const action = assertUid(input.action, "a trace action");
120
+ const kind = assertIdent(input.kind ?? "request", "an event kind");
121
+ const bags = input.bags ?? "both";
122
+ const groups = input.context ?? {};
123
+ for (const group of Object.keys(groups))
124
+ assertIdent(group, "a request-context group");
125
+ const record = {};
126
+ if (bags !== "context-only")
127
+ Object.assign(record, groups);
128
+ Object.assign(record, input.record ?? {});
129
+ const context = {};
130
+ if (bags !== "record-only")
131
+ Object.assign(context, groups);
132
+ return {
133
+ timestamp: input.timestamp,
134
+ action,
135
+ kind,
136
+ record,
137
+ ...(Object.keys(context).length > 0 ? { requestContext: context } : {}),
138
+ ...(input.scope
139
+ ? {
140
+ scope: {
141
+ principal: assertUid(input.scope.principal, "a scope principal"),
142
+ resource: assertUid(input.scope.resource, "a scope resource"),
143
+ },
144
+ }
145
+ : {}),
146
+ ...(input.entities && input.entities.length > 0 ? { entities: input.entities } : {}),
147
+ };
148
+ }
149
+ // ── Rendering ─────────────────────────────────────────────────────
150
+ function renderScope(scope) {
151
+ return `scope(principal: ${scope.principal}, resource: ${scope.resource})`;
152
+ }
153
+ function renderEntities(entities) {
154
+ const parts = entities.map((e) => {
155
+ const attrs = renderTraceFields(e.attrs ?? {});
156
+ const parents = e.parents && e.parents.length > 0 ? ` in [${e.parents.join(", ")}]` : "";
157
+ return `${e.uid}: ${attrs}${parents}`;
158
+ });
159
+ return `entities(${parts.join(", ")})`;
160
+ }
161
+ function renderGroups(fields) {
162
+ const parts = Object.entries(fields).map(([group, value]) => `${assertIdent(group, "a request-context group")}: ${renderTraceValue(value)}`);
163
+ return `request_context(${parts.join(", ")})`;
164
+ }
165
+ /** One event as the single line the trace parser reads. Never ends in a newline. */
166
+ export function renderTraceLine(event) {
167
+ if (!Number.isInteger(event.timestamp)) {
168
+ throw new Error(`dogwood: a trace timestamp is an i64 — got ${String(event.timestamp)}`);
169
+ }
170
+ const parts = [`@${event.timestamp}`];
171
+ if (event.scope)
172
+ parts.push(renderScope(event.scope));
173
+ if (event.entities && event.entities.length > 0)
174
+ parts.push(renderEntities(event.entities));
175
+ if (event.requestContext && Object.keys(event.requestContext).length > 0) {
176
+ parts.push(renderGroups(event.requestContext));
177
+ }
178
+ const record = Object.entries(event.record).map(([key, value]) => `${assertIdent(key, "a trace field name")}: ${renderTraceValue(value)}`);
179
+ parts.push(`${event.action}::${event.kind}(${record.join(", ")})`);
180
+ return parts.join(" ");
181
+ }
182
+ /** A whole trace, one event per line, newline-terminated. */
183
+ export function renderTrace(events) {
184
+ return events.map(renderTraceLine).join("\n") + "\n";
185
+ }
186
+ /**
187
+ * The both-bags and ordering checks, applied to any trace.
188
+ *
189
+ * Every finding here is something that makes a replay *weaker* rather than
190
+ * something that makes it fail — which is exactly the class a green replay run
191
+ * hides. A trace built by {@link traceEvent} produces none of them unless a
192
+ * `bags` opt-out was taken deliberately.
193
+ */
194
+ export function auditTrace(events, options = {}) {
195
+ const issues = [];
196
+ const decisionKinds = new Set(options.decisionKinds ?? ["request"]);
197
+ let previous;
198
+ events.forEach((event, index) => {
199
+ const at = { index, timestamp: event.timestamp };
200
+ const context = event.requestContext ?? {};
201
+ const contextKeys = Object.keys(context);
202
+ const decides = decisionKinds.has(event.kind);
203
+ if (decides && contextKeys.length === 0) {
204
+ issues.push({
205
+ ...at,
206
+ kind: "no-request-context",
207
+ message: `${event.action}::${event.kind} has no request_context envelope, so the Cedar request it is evaluated against carries no context — every context.* test in a policy silently misses`,
208
+ });
209
+ }
210
+ for (const group of contextKeys) {
211
+ if (!(group in event.record)) {
212
+ issues.push({
213
+ ...at,
214
+ kind: "single-bag",
215
+ message: `${event.action}::${event.kind} puts "${group}" in request_context but not in the logged record, so temporal predicates over ${group}.* silently miss`,
216
+ });
217
+ }
218
+ }
219
+ for (const group of Object.keys(event.record)) {
220
+ // Only groups (records) are candidates for the Cedar side; the event
221
+ // schema's own scalar injections (callerPrincipal, requestId, …) belong
222
+ // to the logged record alone and are not a single-bag finding.
223
+ const value = event.record[group];
224
+ const isGroup = typeof value === "object" && value !== null && !Array.isArray(value) && !isTagged(value);
225
+ if (decides && isGroup && contextKeys.length > 0 && !(group in context)) {
226
+ issues.push({
227
+ ...at,
228
+ kind: "single-bag",
229
+ message: `${event.action}::${event.kind} puts "${group}" in the logged record but not in request_context, so context.${group}.* is absent from the Cedar request`,
230
+ });
231
+ }
232
+ }
233
+ if (Object.keys(event.record).length === 0) {
234
+ issues.push({
235
+ ...at,
236
+ kind: "empty-record",
237
+ message: `${event.action}::${event.kind} logs no fields at all, so no temporal predicate can match it`,
238
+ });
239
+ }
240
+ if (previous !== undefined && event.timestamp < previous) {
241
+ issues.push({
242
+ ...at,
243
+ kind: "out-of-order",
244
+ message: `timestamp ${event.timestamp} follows ${previous} — the interpreter accumulates history in file order, so an out-of-order line changes what a window sees`,
245
+ });
246
+ }
247
+ previous = event.timestamp;
248
+ });
249
+ return issues;
250
+ }
251
+ /**
252
+ * Assemble events into trace text, refusing to produce a weakened one.
253
+ *
254
+ * The inversion of the trap: the default is a trace that populates both bags,
255
+ * and every way of getting a weaker one — a `bags` opt-out on an event, a
256
+ * hand-normalized event with an empty record — has to be named in `allow`
257
+ * before this will hand it back.
258
+ */
259
+ export function traceFixture(events, options = {}) {
260
+ const allow = new Set(options.allow ?? []);
261
+ const issues = auditTrace(events, options);
262
+ const blocking = issues.filter((i) => !allow.has(i.kind));
263
+ if (blocking.length > 0) {
264
+ const detail = blocking.map((i) => ` [${i.kind}] @${i.timestamp}: ${i.message}`).join("\n");
265
+ throw new Error(`dogwood: this trace would weaken its own replay rather than fail it:\n${detail}\n` +
266
+ `Fix the events, or pass { allow: [${[...new Set(blocking.map((i) => `"${i.kind}"`))].join(", ")}] } to say the weakening is the point.`);
267
+ }
268
+ return { events, text: renderTrace(events), issues };
269
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The dogwood upstream pin.
3
+ *
4
+ * Not a `upstreamPin` on the plugin: that slot belongs to the cedar grammar
5
+ * (`@cedar-policy/cedar-wasm`, see `../spec/pin.ts`), and self-upgrade tooling
6
+ * bumps exactly one thing per lexicon. Dogwood's pin is a git SHA recorded by
7
+ * #1657, because upstream has no version to key off — zero tags, zero
8
+ * releases, and a workspace version of `1.0.0` that is an Amazon-internal
9
+ * package artifact every future sync will also report.
10
+ *
11
+ * The blob hashes are the narrow content pin the #1657 report recommends. The
12
+ * whole tree hash moves on every sync including docs-only ones, which makes it
13
+ * too noisy to gate on; these seven files are the surface chant actually
14
+ * depends on. The three `.pest` grammars pin the language, `default_macros.dw`
15
+ * pins the standard aggregate library (which is a file a sync can edit and a
16
+ * caller can replace with `--macros`), and the three `dogwood-cli/src` files
17
+ * pin the JSON report structs that are #1659's integration surface.
18
+ *
19
+ * Movement risk is structural, not incidental: content arrives as squashed
20
+ * "Sync from internal source" commits from a publish bot, against a repository
21
+ * nobody outside Amazon can see, with no changelog and no version bump.
22
+ */
23
+ /** The pinned upstream revision and the files chant's surface is built against. */
24
+ export const DOGWOOD_UPSTREAM = {
25
+ owner: "dogwood-policy",
26
+ repo: "dogwood",
27
+ /** Default-branch SHA verified in #1657 (committed 2026-08-06). */
28
+ revision: "5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c",
29
+ license: "Apache-2.0",
30
+ /** Git blob hashes at {@link revision} — the narrow content pin. */
31
+ contents: {
32
+ "dogwood-language/src/parser/grammar.pest": "adb0801303ee1b735585fe22a5d85175ecf6772c",
33
+ "dogwood-language/src/extension/temporal/grammar.pest": "dc31fe1634b5c37173f0a999619e9834069b7a22",
34
+ "dogwood-language/src/event_schema/grammar.pest": "bd3b064447dc0d09cc2f8eb28a7c657e746cc8a2",
35
+ "dogwood-language/configuration/default_macros.dw": "ecd67e5edcd566aa66b05ad51bc472afbaf22a7a",
36
+ "dogwood-cli/src/ops.rs": "2924c7eb0e569a745ff19c2d84cbba782fc56502",
37
+ "dogwood-cli/src/main.rs": "bfd68c9ff05883ed48e5738f658b5925542d817d",
38
+ "dogwood-cli/src/error.rs": "43f12afbbf25ad94e25130c83227eefd86f8d516",
39
+ },
40
+ };