@intentius/chant-lexicon-cedar 0.124.30 → 0.124.32

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,32 @@
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
+ export { DEFAULT_MAX_WINDOW, isWindowParam, renderWindow, renderWindowValue, window, windowParam, windowSeconds, windowValue, } from "./window.js";
17
+ export { and, arrayOf, binderName, bool, call, compare, count, ctx, decimalOf, entityUid, exists, formerly, int, interval, not, predicate, previous, raw, renderCondition, renderTerm, scopeRef, sigilCondition, since, str, sum, temporalMarker, term, tp, typedBinder, varRef, wildcard, } from "./temporal.js";
18
+ export { DEFAULT_MACRO_NAMES, bind, countDistinctWithin, countWithin, defCedarMacro, defTemporalMacro, defaultMacroLibrary, macroCondition, macroTerm, macroWindow, renderMacroDefinition, renderMacroLibrary, sumWithin, } from "./macros.js";
19
+ export { concrete, declaredEventKinds, defaultEventSchema, eventDeclaration, eventSchema, field, pinContext, pinPrincipal, pinResource, pinnedField, principalType, record, renderEventSchema, resourceType, spreadInputs, spreadOutputs, } from "./event-schema.js";
20
+ export { DOGWOOD_EVENT_SCHEMA_FILENAME, DOGWOOD_EVENT_SCHEMA_TYPE, DOGWOOD_LEXICON, DOGWOOD_MACRO_FILENAME, DOGWOOD_MACRO_LIBRARY_TYPE, DOGWOOD_POLICY_FILENAME, DOGWOOD_POLICY_TYPE, TemporalEventSchema, TemporalMacroLibrary, TemporalPolicy, } from "./policy.js";
21
+ export { DOGWOOD_EVENT_SCHEMA_SUFFIX, DOGWOOD_POLICY_SUFFIX, dogwoodPolicyRecords, renderTemporalPolicyText, serializeDogwood, } from "./serialize.js";
22
+ export { blankComments, dogwoodPolicyFiles, dogwoodSchemaFiles, effectiveMaxWindowSeconds, readEventSchema, scanPredicates, scanWindowlessOperators, scanWindows, temporalRegions, } from "./scan.js";
23
+ export { DOGWOOD_UPSTREAM } from "./upstream.js";
24
+ export { DOGWOOD_BINARY_ENV, DOGWOOD_BINARY_NAME, DOGWOOD_SEARCH_ORDER, configureDogwoodCli, findDogwoodBinary, formatDogwoodDiagnostic, parseLowerOutput, parseReplayOutput, parseValidateOutput, resetDogwoodCli, runDogwoodLower, runDogwoodReplay, runDogwoodValidate, } from "./cli.js";
25
+ // Replay traces (#1661) — typed construction of the §6 line grammar, with
26
+ // both-bag population as the default and the weakening as an opt-out.
27
+ export { auditTrace, decimalValue, entityRef, rawValue, renderTrace, renderTraceFields, renderTraceLine, renderTraceValue, traceEntity, traceEvent, traceFixture, } from "./trace.js";
28
+ // The replay activities (#1661). Contributed by name through
29
+ // `src/op/activities/index.ts`; the helpers are here for direct use.
30
+ export { compareVerdicts, dogwoodReplay, dogwoodReplayReport, renderReplaySummary, resolveReplayInputs, } from "./replay-activity.js";
31
+ // The PolicyReplayOp composite and its typed step builders (#1661).
32
+ export { DEFAULT_REPLAY_REPORT_PATH, PolicyReplayOp, dogwoodReplayReportStep, dogwoodReplayStep, } from "./replay-op.js";
@@ -0,0 +1,175 @@
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
+ import { call, interval, renderCondition, renderTerm, sigilCondition, varRef, } from "./temporal.js";
30
+ import { windowParam } from "./window.js";
31
+ /** `?w` — a value parameter, spliced from the call site. */
32
+ const PARAM = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
33
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
34
+ function assertMacroName(name) {
35
+ if (!IDENT.test(name))
36
+ throw new Error(`dogwood: a macro name must be an identifier — got "${name}"`);
37
+ return name;
38
+ }
39
+ function assertParams(params) {
40
+ for (const p of params) {
41
+ if (!PARAM.test(p)) {
42
+ throw new Error(`dogwood: a macro parameter carries the "?" sigil — got "${p}" (write "?${p.replace(/^\?/, "")}")`);
43
+ }
44
+ }
45
+ return params;
46
+ }
47
+ /**
48
+ * `def temporal name(?a, ?b) { … };`
49
+ *
50
+ * The body may be a condition or an aggregation term, matching upstream's
51
+ * `temporal_macro_body_entry`; `sum_within` is an aggregation-bodied macro and
52
+ * `once` is a condition-bodied one.
53
+ */
54
+ export function defTemporalMacro(name, params, body, comment) {
55
+ return {
56
+ kind: "temporal",
57
+ name: assertMacroName(name),
58
+ params: assertParams(params),
59
+ body: isTermBody(body) ? renderTerm(body) : renderCondition(body),
60
+ comment,
61
+ };
62
+ }
63
+ function isTermBody(body) {
64
+ return body.op === "term" || body.op === "array" || body.op === "count" || body.op === "sum";
65
+ }
66
+ /**
67
+ * `def cedar name(?a) { … };` — the body is Cedar expression text.
68
+ *
69
+ * Cedar's expression grammar is the policy language itself, so it stays a
70
+ * string here for the same reason `Cedar::Policy`'s `when` does.
71
+ */
72
+ export function defCedarMacro(name, params, body, comment) {
73
+ return { kind: "cedar", name: assertMacroName(name), params: assertParams(params), body, comment };
74
+ }
75
+ // ── The sigils, in the three positions they appear ────────────────
76
+ /**
77
+ * `within ?w` — the window a macro body defers to its call site.
78
+ *
79
+ * The call site supplies a bare interval (`once(1h, …)`), not a `within`
80
+ * clause: the keyword belongs to the operator, so it stays in the body.
81
+ */
82
+ export function macroWindow(param) {
83
+ return windowParam(param);
84
+ }
85
+ /** `?s` standing for an entire condition, spliced from the call site. */
86
+ export function macroCondition(param) {
87
+ if (!PARAM.test(param))
88
+ throw new Error(`dogwood: a condition parameter carries the "?" sigil — got "${param}"`);
89
+ return sigilCondition(param);
90
+ }
91
+ /** `?a` / `$t` in term or binder position. */
92
+ export function macroTerm(sigil) {
93
+ if (!/^[?$][A-Za-z_][A-Za-z0-9_]*$/.test(sigil)) {
94
+ throw new Error(`dogwood: a macro term carries a "?" or "$" sigil — got "${sigil}"`);
95
+ }
96
+ return varRef(sigil);
97
+ }
98
+ /** One definition as `.dw` source. */
99
+ export function renderMacroDefinition(def) {
100
+ const lines = [];
101
+ if (def.comment) {
102
+ for (const line of def.comment.split("\n"))
103
+ lines.push(`// ${line}`.trimEnd());
104
+ }
105
+ lines.push(`def ${def.kind} ${def.name}(${def.params.join(", ")}) {`);
106
+ lines.push(` ${def.body}`);
107
+ lines.push("};");
108
+ return lines.join("\n");
109
+ }
110
+ /** A whole library — the file `--macros` points at, or a policy set's own prelude. */
111
+ export function renderMacroLibrary(defs) {
112
+ return defs.map(renderMacroDefinition).join("\n\n") + "\n";
113
+ }
114
+ // ── The default library, as calls ─────────────────────────────────
115
+ /** The macro names `configuration/default_macros.dw` ships at the pinned revision. */
116
+ export const DEFAULT_MACRO_NAMES = ["count_within", "sum_within", "count_distinct_within", "bind"];
117
+ /**
118
+ * `count_within(?w, ?s)` — timepoints in the window at which `?s` held.
119
+ *
120
+ * A default-library call, not an operator. See this module's header.
121
+ */
122
+ export function countWithin(w, condition) {
123
+ return call("count_within", [interval(w), condition]);
124
+ }
125
+ /** `sum_within(?a, ?w, ?body)` — `over` names the sum's bound variable. */
126
+ export function sumWithin(over, w, body) {
127
+ return call("sum_within", [varRef(over), interval(w), body]);
128
+ }
129
+ /** `count_distinct_within(?k, ?w, ?s)` — distinct String keys `k` for which `s` held. */
130
+ export function countDistinctWithin(key, w, condition) {
131
+ return call("count_distinct_within", [varRef(key), interval(w), condition]);
132
+ }
133
+ /** `bind(?n, ?A, ?B)` — names an aggregate result so it can be compared. */
134
+ export function bind(name, aggregate, body) {
135
+ return call("bind", [varRef(name), aggregate, body]);
136
+ }
137
+ /**
138
+ * The default library's four definitions, verbatim from upstream's
139
+ * `dogwood-language/configuration/default_macros.dw` at the pinned revision.
140
+ *
141
+ * Emitting these into a project's own `macros.dw` turns an assumption about
142
+ * the callee's `--macros` into a file the project controls.
143
+ */
144
+ export function defaultMacroLibrary() {
145
+ return [
146
+ {
147
+ kind: "temporal",
148
+ name: "count_within",
149
+ params: ["?w", "?s"],
150
+ body: "count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))",
151
+ comment: "Counts the timepoints within window `?w` at which condition `?s` held.",
152
+ },
153
+ {
154
+ kind: "temporal",
155
+ name: "sum_within",
156
+ params: ["?a", "?w", "?body"],
157
+ body: "sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))",
158
+ comment: "Sums the numeric value `?a` over occurrences of `?body` within window `?w`.",
159
+ },
160
+ {
161
+ kind: "temporal",
162
+ name: "count_distinct_within",
163
+ params: ["?k", "?w", "?s"],
164
+ body: "count for (?k: String). where (formerly within ?w ?s)",
165
+ comment: "Counts the distinct key values `?k` for which `?s` held within window `?w`.",
166
+ },
167
+ {
168
+ kind: "temporal",
169
+ name: "bind",
170
+ params: ["?n", "?A", "?B"],
171
+ body: "exists (?n: Long). (?A == ?n && ?B)",
172
+ comment: 'A "let"-style binder: names an aggregate result `?n` and uses it in `?B`.',
173
+ },
174
+ ];
175
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The dogwood entity model: a temporal policy, an event schema, a macro library.
3
+ *
4
+ * `Dogwood::TemporalPolicy` is `Cedar::Policy` plus the two clause forms
5
+ * upstream's policy grammar adds. Its head — annotations, effect, the three
6
+ * scope positions — is Cedar's, byte for byte, which is why `./serialize.ts`
7
+ * renders it with the cedar serializer's own `renderPolicyHead` rather than a
8
+ * copy of it. Only the `cond` rule differs:
9
+ *
10
+ * ```
11
+ * cond = { cond_kw ~ (extension_marker | guardrails_tag? ~ "{" ~ expr ~ "}") }
12
+ * ```
13
+ *
14
+ * — so a clause is `when { … }`, `when guardrails { … }`, or
15
+ * `when temporal { … }`, and the same three under `unless`. `guardrails` is
16
+ * transparent sugar for a bare Cedar expression (upstream discards the tag
17
+ * during lowering); `temporal { … }` is a genuine sub-language dispatched to a
18
+ * different parser.
19
+ *
20
+ * These are not schema-driven the way `Cedar::Policy`'s generated class is.
21
+ * Codegen's input is the project's `.cedarschema`, which says nothing about
22
+ * event kinds or temporal operators, so the dialect's classes are written here
23
+ * and typed against the builders in `./temporal.ts`.
24
+ */
25
+ import { createResource } from "@intentius/chant/runtime";
26
+ /** The lexicon these entities belong to — dogwood is a dialect of cedar, not a lexicon. */
27
+ export const DOGWOOD_LEXICON = "cedar";
28
+ /** The `entityType` of a temporal policy. */
29
+ export const DOGWOOD_POLICY_TYPE = "Dogwood::TemporalPolicy";
30
+ /** The `entityType` of an event schema. */
31
+ export const DOGWOOD_EVENT_SCHEMA_TYPE = "Dogwood::EventSchema";
32
+ /** The `entityType` of a macro library. */
33
+ export const DOGWOOD_MACRO_LIBRARY_TYPE = "Dogwood::MacroLibrary";
34
+ /** The `.dw` policy set written beside the cedar outputs. */
35
+ export const DOGWOOD_POLICY_FILENAME = "policies.dw";
36
+ /** The `.dwschema` event schema — what `dogwood validate --event-schema` reads. */
37
+ export const DOGWOOD_EVENT_SCHEMA_FILENAME = "events.dwschema";
38
+ /** The macro library — what `dogwood validate --macros` reads. */
39
+ export const DOGWOOD_MACRO_FILENAME = "macros.dw";
40
+ /**
41
+ * A temporal policy. Declare one per policy, the same way as `Cedar::Policy`:
42
+ *
43
+ * ```ts
44
+ * export const readAfterLogin = new TemporalPolicy({
45
+ * action: { eq: 'Drupe::Action::"Read"' },
46
+ * whenTemporal: [
47
+ * formerly("1h", predicate('Drupe::Action::"Login"', "response", {
48
+ * "input.user": ctx("input.user"),
49
+ * })),
50
+ * ],
51
+ * });
52
+ * ```
53
+ */
54
+ export const TemporalPolicy = createResource(DOGWOOD_POLICY_TYPE, DOGWOOD_LEXICON, {});
55
+ /** The service half of the schema, emitted as `.dwschema` text. */
56
+ export const TemporalEventSchema = createResource(DOGWOOD_EVENT_SCHEMA_TYPE, DOGWOOD_LEXICON, {});
57
+ /** A `def cedar` / `def temporal` library, emitted as `.dw` text. */
58
+ export const TemporalMacroLibrary = createResource(DOGWOOD_MACRO_LIBRARY_TYPE, DOGWOOD_LEXICON, {});
@@ -0,0 +1,313 @@
1
+ /**
2
+ * `dogwoodReplay` / `dogwoodReplayReport` — the replay half of PolicyReplayOp
3
+ * (#1661, epic #1646).
4
+ *
5
+ * Contributed the way the fly lexicon contributes `flyApply`: a plain exported
6
+ * async function taking one args object, re-exported from
7
+ * `src/op/activities/index.ts`, resolved **by name** by core's activity
8
+ * registry when a project lists the `cedar` lexicon. It imports no runtime of
9
+ * its own, so the local executor calls it as-is.
10
+ *
11
+ * What it does: takes a policy bundle (inline text or paths), an event trace
12
+ * (typed events, inline text, or a path) and a set of expectations, runs
13
+ * `dogwood replay --format json` through the existing CLI adapter in
14
+ * `./cli.ts`, and returns a typed divergence report — expected versus actual
15
+ * verdict per decision point, with the determining rules and per-evaluation
16
+ * errors carried through.
17
+ *
18
+ * Three contract details from the #1657 verification shape the code:
19
+ *
20
+ * - **Replay exits 0 even when every verdict is DENY.** A nonzero exit means
21
+ * the trace or the policy set failed to load. `./cli.ts` already refuses to
22
+ * read exit codes as verdicts; this module refuses to read a DENY as a
23
+ * failure.
24
+ * - **A run that could not happen is not a run that found nothing.** An
25
+ * unusable invocation or a fatal (a malformed trace line, an unparseable
26
+ * policy set) throws, so the step fails instead of reporting zero
27
+ * divergences.
28
+ * - **`index` is the decision-stream position, not the trace line number.** A
29
+ * history-only event contributes history and no verdict, so expectations
30
+ * written against trace lines would silently address the wrong decision.
31
+ * Expectations therefore match on `timestamp` by default, and on `index`
32
+ * only when the caller says so.
33
+ *
34
+ * The trace input is deliberately generic. An AgentCore session/decision
35
+ * history is the follow-on source (it needs the aws lexicon's activity
36
+ * surface, out of scope here) — a trace is a trace, wherever it came from.
37
+ */
38
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
39
+ import { dirname, isAbsolute, resolve } from "node:path";
40
+ import { DOGWOOD_SEARCH_ORDER, findDogwoodBinary, formatDogwoodDiagnostic, runDogwoodReplay, } from "./cli.js";
41
+ import { auditTrace, renderTrace } from "./trace.js";
42
+ async function textOf(inline, path, cwd, what) {
43
+ if (inline !== undefined)
44
+ return inline;
45
+ if (path === undefined)
46
+ return undefined;
47
+ const full = isAbsolute(path) ? path : resolve(cwd, path);
48
+ try {
49
+ return await readFile(full, "utf-8");
50
+ }
51
+ catch (err) {
52
+ throw new Error(`dogwood replay: could not read ${what} at ${full} — ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ }
55
+ /**
56
+ * Assemble the bundle and the trace from whichever form the caller supplied.
57
+ *
58
+ * Exported because the composite's Artifacts phase writes files and the Replay
59
+ * phase names them: resolving the same way in a test as in the Op is the point
60
+ * of having one function do it.
61
+ */
62
+ export async function resolveReplayInputs(args) {
63
+ const cwd = args.cwd ?? process.cwd();
64
+ const policies = await textOf(args.policies, args.policiesPath, cwd, "the .dw policy set");
65
+ if (policies === undefined) {
66
+ throw new Error("dogwood replay: no policy set — pass `policies` or `policiesPath`");
67
+ }
68
+ const policySchema = await textOf(args.policySchema, args.policySchemaPath, cwd, "the Cedar action schema");
69
+ if (policySchema === undefined) {
70
+ throw new Error("dogwood replay: no action schema — `--policy-schema` is not optional to the CLI, so pass `policySchema` or `policySchemaPath`");
71
+ }
72
+ const eventSchema = await textOf(args.eventSchema, args.eventSchemaPath, cwd, "the .dwschema event schema");
73
+ const macros = await textOf(args.macros, args.macrosPath, cwd, "the macro library");
74
+ const providers = await textOf(args.providers, args.providersPath, cwd, "providers.json");
75
+ let trace;
76
+ let traceIssues = [];
77
+ if (args.traceEvents && args.traceEvents.length > 0) {
78
+ // Only the typed form can be audited — there is no trace parser on this
79
+ // side, and guessing one from a regex would report confident nonsense
80
+ // about a format a sync can retune.
81
+ traceIssues = auditTrace(args.traceEvents, {
82
+ ...(args.traceDecisionKinds ? { decisionKinds: args.traceDecisionKinds } : {}),
83
+ });
84
+ trace = renderTrace(args.traceEvents);
85
+ }
86
+ else {
87
+ trace = await textOf(args.trace, args.tracePath, cwd, "the event trace");
88
+ }
89
+ if (trace === undefined) {
90
+ throw new Error("dogwood replay: no trace — pass `traceEvents`, `trace` or `tracePath`");
91
+ }
92
+ return {
93
+ bundle: {
94
+ policies,
95
+ policySchema,
96
+ ...(eventSchema !== undefined ? { eventSchema } : {}),
97
+ ...(macros !== undefined ? { macros } : {}),
98
+ ...(providers !== undefined ? { providers } : {}),
99
+ },
100
+ trace,
101
+ traceIssues,
102
+ };
103
+ }
104
+ /**
105
+ * Match expectations against the decision stream.
106
+ *
107
+ * Timestamp matching consumes verdicts left to right, so two expectations at
108
+ * the same `@n` address the first and second decision there rather than both
109
+ * addressing the first. A verdict nothing expected is reported too — a policy
110
+ * set that starts deciding somewhere new is drift, and the usual reason a
111
+ * replay is being run at all.
112
+ */
113
+ export function compareVerdicts(verdicts, expectations) {
114
+ const divergences = [];
115
+ const claimed = new Set();
116
+ for (const expectation of expectations) {
117
+ if (expectation.index === undefined && expectation.timestamp === undefined) {
118
+ throw new Error("dogwood replay: an expectation needs a `timestamp` or an `index`");
119
+ }
120
+ const position = expectation.index !== undefined
121
+ ? verdicts.findIndex((v) => v.index === expectation.index)
122
+ : verdicts.findIndex((v, i) => !claimed.has(i) && v.timestamp === expectation.timestamp);
123
+ if (position === -1) {
124
+ const where = expectation.index !== undefined ? `decision index ${expectation.index}` : `@${String(expectation.timestamp)}`;
125
+ divergences.push({
126
+ index: -1,
127
+ timestamp: expectation.timestamp ?? -1,
128
+ expected: expectation.verdict,
129
+ determiningRules: [],
130
+ errors: [],
131
+ detail: `no decision point at ${where} — the trace produced ${verdicts.length} decision(s), and a history-only event produces none`,
132
+ ...(expectation.note ? { note: expectation.note } : {}),
133
+ });
134
+ continue;
135
+ }
136
+ claimed.add(position);
137
+ const actual = verdicts[position];
138
+ if (actual.verdict !== expectation.verdict) {
139
+ divergences.push({
140
+ index: actual.index,
141
+ timestamp: actual.timestamp,
142
+ expected: expectation.verdict,
143
+ actual: actual.verdict,
144
+ determiningRules: actual.determiningRules,
145
+ errors: actual.errors,
146
+ detail: `expected ${expectation.verdict.toUpperCase()}, replayed ${actual.verdict.toUpperCase()}${actual.determiningRules.length > 0 ? ` (rules: ${actual.determiningRules.join(", ")})` : ""}`,
147
+ ...(expectation.note ? { note: expectation.note } : {}),
148
+ });
149
+ continue;
150
+ }
151
+ if (expectation.determiningRules !== undefined) {
152
+ const want = [...expectation.determiningRules].sort((a, b) => a - b).join(", ");
153
+ const got = [...actual.determiningRules].sort((a, b) => a - b).join(", ");
154
+ if (want !== got) {
155
+ divergences.push({
156
+ index: actual.index,
157
+ timestamp: actual.timestamp,
158
+ expected: expectation.verdict,
159
+ actual: actual.verdict,
160
+ determiningRules: actual.determiningRules,
161
+ errors: actual.errors,
162
+ detail: `${actual.verdict.toUpperCase()} as expected, but determined by rules [${got}] rather than [${want}] — the decision is right for a different reason`,
163
+ ...(expectation.note ? { note: expectation.note } : {}),
164
+ });
165
+ continue;
166
+ }
167
+ }
168
+ if (actual.errors.length > 0) {
169
+ divergences.push({
170
+ index: actual.index,
171
+ timestamp: actual.timestamp,
172
+ expected: expectation.verdict,
173
+ actual: actual.verdict,
174
+ determiningRules: actual.determiningRules,
175
+ errors: actual.errors,
176
+ detail: `${actual.verdict.toUpperCase()} as expected, but the evaluation errored: ${actual.errors.join("; ")}`,
177
+ ...(expectation.note ? { note: expectation.note } : {}),
178
+ });
179
+ }
180
+ }
181
+ verdicts.forEach((verdict, position) => {
182
+ if (claimed.has(position))
183
+ return;
184
+ if (expectations.length === 0)
185
+ return;
186
+ divergences.push({
187
+ index: verdict.index,
188
+ timestamp: verdict.timestamp,
189
+ actual: verdict.verdict,
190
+ determiningRules: verdict.determiningRules,
191
+ errors: verdict.errors,
192
+ detail: `an unexpected decision point: @${verdict.timestamp} replayed ${verdict.verdict.toUpperCase()} and nothing expected a decision there`,
193
+ });
194
+ });
195
+ // Errors on decision points nobody wrote an expectation for still matter —
196
+ // a provider with no inlined Rhai script errors on every evaluation, and a
197
+ // replay with no expectations at all would otherwise report itself clean.
198
+ if (expectations.length === 0) {
199
+ for (const verdict of verdicts) {
200
+ if (verdict.errors.length === 0)
201
+ continue;
202
+ divergences.push({
203
+ index: verdict.index,
204
+ timestamp: verdict.timestamp,
205
+ actual: verdict.verdict,
206
+ determiningRules: verdict.determiningRules,
207
+ errors: verdict.errors,
208
+ detail: `@${verdict.timestamp} evaluated with errors: ${verdict.errors.join("; ")}`,
209
+ });
210
+ }
211
+ }
212
+ return divergences.sort((a, b) => a.timestamp - b.timestamp || a.index - b.index);
213
+ }
214
+ /** The markdown body — printed in `report` mode, posted in the other two. */
215
+ export function renderReplaySummary(report) {
216
+ const lines = ["## Policy replay", ""];
217
+ lines.push(`${report.verdicts.length} decision point(s) replayed; ${report.divergences.length} divergence(s).`, "");
218
+ if (report.divergences.length > 0) {
219
+ lines.push("| @ | index | expected | replayed | detail |", "|---|---|---|---|---|");
220
+ for (const d of report.divergences) {
221
+ lines.push(`| ${d.timestamp === -1 ? "—" : `@${d.timestamp}`} | ${d.index === -1 ? "—" : d.index} | ${d.expected ?? "—"} | ${d.actual ?? "—"} | ${d.detail}${d.note ? ` — ${d.note}` : ""} |`);
222
+ }
223
+ lines.push("");
224
+ }
225
+ if (report.traceIssues.length > 0) {
226
+ lines.push("### Trace weaknesses", "", "These do not fail a replay — they make one report a verdict it did not really test.", "");
227
+ for (const issue of report.traceIssues) {
228
+ lines.push(`- \`${issue.kind}\` @${issue.timestamp}: ${issue.message}`);
229
+ }
230
+ lines.push("");
231
+ }
232
+ if (report.divergences.length === 0 && report.traceIssues.length === 0) {
233
+ lines.push("Every decision point replayed to the verdict it was expected to reach.", "");
234
+ }
235
+ return lines.join("\n");
236
+ }
237
+ /**
238
+ * Replay a policy bundle against a trace and report divergence.
239
+ *
240
+ * Throws when the CLI could not be used or the run was fatal. That is the
241
+ * distinction `./cli.ts` draws and this preserves: a replay that did not
242
+ * happen must fail the step, never report zero divergences.
243
+ */
244
+ export async function dogwoodReplay(args) {
245
+ const cwd = args.cwd ?? process.cwd();
246
+ const mode = args.mode ?? "report";
247
+ const binary = args.binary ?? findDogwoodBinary(cwd)?.path;
248
+ if (!binary) {
249
+ throw new Error(`dogwood replay: no \`dogwood\` binary. chant looked at ${DOGWOOD_SEARCH_ORDER}. Upstream ships only a Rust CLI — build it from dogwood-policy/dogwood and point chant at it.`);
250
+ }
251
+ const { bundle, trace, traceIssues } = await resolveReplayInputs(args);
252
+ const result = runDogwoodReplay(binary, bundle, trace);
253
+ if (result.kind === "unusable") {
254
+ throw new Error(`dogwood replay: ${result.reason}`);
255
+ }
256
+ if (result.kind === "fatal") {
257
+ const related = result.related.map((d) => `\n ${formatDogwoodDiagnostic(d)}`).join("");
258
+ throw new Error(`dogwood replay: ${formatDogwoodDiagnostic(result.error)}${related}`);
259
+ }
260
+ const divergences = compareVerdicts(result.verdicts, args.expect ?? []);
261
+ const partial = {
262
+ ok: divergences.length === 0,
263
+ mode,
264
+ verdicts: result.verdicts,
265
+ divergences,
266
+ findings: divergences.length,
267
+ traceIssues,
268
+ };
269
+ const report = { ...partial, summary: renderReplaySummary(partial) };
270
+ if (args.reportPath) {
271
+ const full = isAbsolute(args.reportPath) ? args.reportPath : resolve(cwd, args.reportPath);
272
+ await mkdir(dirname(full), { recursive: true });
273
+ await writeFile(full, JSON.stringify(report, null, 2) + "\n", "utf-8");
274
+ }
275
+ return report;
276
+ }
277
+ /**
278
+ * Turn a written replay report into the thing the finding mode calls for.
279
+ *
280
+ * It renders and returns; it does not open anything. Same as
281
+ * `workflowSupplyChainAudit`, and for the same reason — the cedar lexicon has
282
+ * no forge client and should not grow one to reach GitHub, GitLab or Forgejo.
283
+ * `report` mode prints the body; `issue` and `pull-request` hand back the
284
+ * title and body for whatever step opens them.
285
+ */
286
+ export async function dogwoodReplayReport(args) {
287
+ const cwd = args.cwd ?? process.cwd();
288
+ let report = args.report;
289
+ if (!report) {
290
+ if (!args.reportPath) {
291
+ throw new Error("dogwood replay report: pass `report` or `reportPath`");
292
+ }
293
+ const full = isAbsolute(args.reportPath) ? args.reportPath : resolve(cwd, args.reportPath);
294
+ try {
295
+ report = JSON.parse(await readFile(full, "utf-8"));
296
+ }
297
+ catch (err) {
298
+ throw new Error(`dogwood replay report: could not read the replay report at ${full} — ${err instanceof Error ? err.message : String(err)}. The Replay phase writes it; a Report phase that runs without one has nothing to report on.`);
299
+ }
300
+ }
301
+ const mode = args.mode ?? report.mode;
302
+ const findings = report.findings;
303
+ const actionable = findings > 0 || report.traceIssues.length > 0;
304
+ const title = args.title ?? `Policy replay: ${findings} divergence(s)`;
305
+ if (mode === "report") {
306
+ // eslint-disable-next-line no-console -- report mode's whole output is this.
307
+ console.log(report.summary);
308
+ }
309
+ if (args.failOnDivergence && findings > 0) {
310
+ throw new Error(`dogwood replay: ${findings} divergence(s) between the declared policy set and the replayed trace`);
311
+ }
312
+ return { mode, findings, title, body: report.summary, actionable };
313
+ }