@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +106 -2
  2. package/dist/agentcore/embed.d.ts +189 -0
  3. package/dist/agentcore/embed.d.ts.map +1 -0
  4. package/dist/agentcore/enforcement.d.ts +76 -0
  5. package/dist/agentcore/enforcement.d.ts.map +1 -0
  6. package/dist/agentcore/scan.d.ts +46 -0
  7. package/dist/agentcore/scan.d.ts.map +1 -0
  8. package/dist/codegen/docs-dogwood.d.ts +21 -0
  9. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts.map +1 -1
  11. package/dist/codegen/package.d.ts.map +1 -1
  12. package/dist/config.d.ts +25 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/dogwood/cli.d.ts +247 -0
  15. package/dist/dogwood/cli.d.ts.map +1 -0
  16. package/dist/dogwood/event-schema.d.ts +161 -0
  17. package/dist/dogwood/event-schema.d.ts.map +1 -0
  18. package/dist/dogwood/index.d.ts +39 -0
  19. package/dist/dogwood/index.d.ts.map +1 -0
  20. package/dist/dogwood/macros.d.ts +96 -0
  21. package/dist/dogwood/macros.d.ts.map +1 -0
  22. package/dist/dogwood/policy.d.ts +120 -0
  23. package/dist/dogwood/policy.d.ts.map +1 -0
  24. package/dist/dogwood/replay-activity.d.ts +196 -0
  25. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  26. package/dist/dogwood/replay-op.d.ts +165 -0
  27. package/dist/dogwood/replay-op.d.ts.map +1 -0
  28. package/dist/dogwood/scan.d.ts +109 -0
  29. package/dist/dogwood/scan.d.ts.map +1 -0
  30. package/dist/dogwood/serialize.d.ts +66 -0
  31. package/dist/dogwood/serialize.d.ts.map +1 -0
  32. package/dist/dogwood/temporal.d.ts +259 -0
  33. package/dist/dogwood/temporal.d.ts.map +1 -0
  34. package/dist/dogwood/trace.d.ts +215 -0
  35. package/dist/dogwood/trace.d.ts.map +1 -0
  36. package/dist/dogwood/upstream.d.ts +41 -0
  37. package/dist/dogwood/upstream.d.ts.map +1 -0
  38. package/dist/dogwood/window.d.ts +73 -0
  39. package/dist/dogwood/window.d.ts.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/integrity.json +15 -3
  43. package/dist/lint/audit-catalog.d.ts.map +1 -1
  44. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  45. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  46. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  47. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  48. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  49. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  50. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  51. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  52. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  53. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  54. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  55. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  56. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  57. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  58. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  59. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  60. package/dist/lint/post-synth/index.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +7 -0
  63. package/dist/okf/rules/DWDC010.md +11 -0
  64. package/dist/okf/rules/DWDC011.md +11 -0
  65. package/dist/okf/rules/DWDC012.md +11 -0
  66. package/dist/okf/rules/DWDC013.md +15 -0
  67. package/dist/okf/rules/DWDE010.md +11 -0
  68. package/dist/okf/rules/DWDE011.md +11 -0
  69. package/dist/okf/rules/DWDS010.md +11 -0
  70. package/dist/okf/types/Policy.md +1 -0
  71. package/dist/op/activities/index.d.ts +18 -0
  72. package/dist/op/activities/index.d.ts.map +1 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/policy-text.d.ts +53 -0
  75. package/dist/policy-text.d.ts.map +1 -0
  76. package/dist/rules/dogwood-helpers.ts +139 -0
  77. package/dist/rules/dwdc010.ts +62 -0
  78. package/dist/rules/dwdc011.ts +61 -0
  79. package/dist/rules/dwdc012.ts +46 -0
  80. package/dist/rules/dwdc013.ts +64 -0
  81. package/dist/rules/dwde010.ts +130 -0
  82. package/dist/rules/dwde011.ts +108 -0
  83. package/dist/rules/dwds010.ts +46 -0
  84. package/dist/serializer.d.ts +10 -18
  85. package/dist/serializer.d.ts.map +1 -1
  86. package/dist/skills/chant-cedar-authoring.md +180 -0
  87. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  88. package/dist/skills/chant-cedar-dogwood.md +327 -0
  89. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  90. package/package.json +7 -2
  91. package/src/agentcore/embed.test.ts +254 -0
  92. package/src/agentcore/embed.ts +399 -0
  93. package/src/agentcore/enforcement.test.ts +43 -0
  94. package/src/agentcore/enforcement.ts +92 -0
  95. package/src/agentcore/scan.ts +119 -0
  96. package/src/codegen/docs-dogwood.ts +1119 -0
  97. package/src/codegen/docs.ts +66 -1
  98. package/src/codegen/package.ts +3 -2
  99. package/src/config.test.ts +12 -0
  100. package/src/config.ts +28 -0
  101. package/src/dogwood/cli.test.ts +513 -0
  102. package/src/dogwood/cli.ts +666 -0
  103. package/src/dogwood/event-schema.test.ts +218 -0
  104. package/src/dogwood/event-schema.ts +318 -0
  105. package/src/dogwood/index.ts +271 -0
  106. package/src/dogwood/macros.test.ts +104 -0
  107. package/src/dogwood/macros.ts +229 -0
  108. package/src/dogwood/policy.test.ts +94 -0
  109. package/src/dogwood/policy.ts +141 -0
  110. package/src/dogwood/replay-activity.test.ts +481 -0
  111. package/src/dogwood/replay-activity.ts +506 -0
  112. package/src/dogwood/replay-op.ts +242 -0
  113. package/src/dogwood/scan.ts +287 -0
  114. package/src/dogwood/serialize.test.ts +331 -0
  115. package/src/dogwood/serialize.ts +246 -0
  116. package/src/dogwood/temporal.test.ts +272 -0
  117. package/src/dogwood/temporal.ts +592 -0
  118. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  119. package/src/dogwood/testdata/default-macros.dw +23 -0
  120. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  121. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  122. package/src/dogwood/testdata/pinned.dwschema +31 -0
  123. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  124. package/src/dogwood/testdata/read-after-login.dw +17 -0
  125. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  126. package/src/dogwood/trace.test.ts +231 -0
  127. package/src/dogwood/trace.ts +471 -0
  128. package/src/dogwood/upstream.ts +41 -0
  129. package/src/dogwood/window.ts +124 -0
  130. package/src/index.ts +76 -0
  131. package/src/lint/audit-catalog.ts +64 -0
  132. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  133. package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
  134. package/src/lint/post-synth/dwdc010.ts +62 -0
  135. package/src/lint/post-synth/dwdc011.ts +61 -0
  136. package/src/lint/post-synth/dwdc012.ts +46 -0
  137. package/src/lint/post-synth/dwdc013.ts +64 -0
  138. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  139. package/src/lint/post-synth/dwde010.ts +130 -0
  140. package/src/lint/post-synth/dwde011.ts +108 -0
  141. package/src/lint/post-synth/dwds010.ts +46 -0
  142. package/src/lint/post-synth/index.ts +14 -0
  143. package/src/lint/post-synth/post-synth.test.ts +7 -3
  144. package/src/op/activities/index.ts +27 -0
  145. package/src/plugin.test.ts +3 -2
  146. package/src/plugin.ts +30 -0
  147. package/src/policy-text.ts +128 -0
  148. package/src/serializer.ts +71 -109
  149. package/src/skills/chant-cedar-dogwood.md +327 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * `PolicyReplayOp` — replay a declared `.dw` set against a recorded event
3
+ * trace and report divergence (#1661, epic #1646).
4
+ *
5
+ * Shape follows `WorkflowAuditOp`: an observe-dial Op whose last phase is a
6
+ * finding mode (`report | issue | pull-request`). What it observes is not a
7
+ * moving upstream but a moving *history* — a policy set that was correct
8
+ * against last week's traffic can decide differently against this week's, and
9
+ * the deterministic build cannot see that.
10
+ *
11
+ * ## Packaging: the composite ships from cedar, not from temporal
12
+ *
13
+ * `WorkflowAuditOp` and `PipelineAuditOp` live in the temporal lexicon because
14
+ * they hand back a `TemporalSchedule` alongside the Op, and that resource is
15
+ * temporal's. This composite hands back an Op and nothing else, so it has no
16
+ * reason to reach across — it imports `@intentius/chant/op` only, exactly as
17
+ * the fly lexicon's `flyDeploy` composite does. cedar therefore keeps zero
18
+ * runtime dependency on `@intentius/chant-lexicon-temporal`.
19
+ *
20
+ * The scheduled form is a two-line project-side pairing rather than a config
21
+ * flag, and that is the deliberate cost of the decision:
22
+ *
23
+ * ```ts
24
+ * import { TemporalSchedule } from "@intentius/chant-lexicon-temporal";
25
+ *
26
+ * export const { op } = PolicyReplayOp({ name: "policy-replay", … });
27
+ * export const schedule = new TemporalSchedule({
28
+ * scheduleId: "policy-replay-schedule",
29
+ * spec: { cronExpressions: ["0 6 * * *"] },
30
+ * action: { workflowType: "policyReplayWorkflow", taskQueue: "policy-replay" },
31
+ * });
32
+ * ```
33
+ *
34
+ * A project that wants that already installs the temporal lexicon; a project
35
+ * that only wants `chant run policy-replay` on the local executor should not
36
+ * have to. `examples/policy-replay` is the worked recipe for both.
37
+ *
38
+ * ## Phases
39
+ *
40
+ * ```
41
+ * Artifacts chantBuild — emit policies.dw, the .cedarschema and the
42
+ * .dwschema the replay reads (skippable when they are checked in)
43
+ * Replay dogwoodReplay — run `dogwood replay --format json` over the
44
+ * bundle and the trace, write the divergence report
45
+ * Report dogwoodReplayReport — read that report and act on the mode
46
+ * ```
47
+ *
48
+ * The report file is the seam between the last two phases, the same way
49
+ * `dist/fly.json` is the seam between `build:fly` and `flyApply`: Op steps do
50
+ * not pass return values to one another, so a phase boundary needs an artifact
51
+ * to be a real boundary rather than a cosmetic one.
52
+ */
53
+
54
+ import { Op, activity, build, phase, type ActivityStep, type OpResource } from "@intentius/chant/op";
55
+ import type { PolicyReplayMode, ReplayExpectation } from "./replay-activity";
56
+
57
+ /** Where the Replay phase writes its report by default. */
58
+ export const DEFAULT_REPLAY_REPORT_PATH = "dist/dogwood-replay.json";
59
+
60
+ /** Options for {@link dogwoodReplayStep}. Mirrors `DogwoodReplayArgs`. */
61
+ export interface DogwoodReplayStepOpts {
62
+ policies?: string;
63
+ policiesPath?: string;
64
+ policySchema?: string;
65
+ policySchemaPath?: string;
66
+ eventSchema?: string;
67
+ eventSchemaPath?: string;
68
+ macros?: string;
69
+ macrosPath?: string;
70
+ providers?: string;
71
+ providersPath?: string;
72
+ trace?: string;
73
+ tracePath?: string;
74
+ expect?: readonly ReplayExpectation[];
75
+ mode?: PolicyReplayMode;
76
+ binary?: string;
77
+ cwd?: string;
78
+ reportPath?: string;
79
+ profile?: ActivityStep["profile"];
80
+ }
81
+
82
+ /**
83
+ * Replay a policy bundle against a trace — the typed twin of `flyApplyStep`.
84
+ *
85
+ * Resolves to the `dogwoodReplay` activity by name, which `loadActivities`
86
+ * binds when the project lists the `cedar` lexicon. Defaults to the
87
+ * `policyCheck` profile.
88
+ */
89
+ export const dogwoodReplayStep = (opts: DogwoodReplayStepOpts): ActivityStep => {
90
+ const { profile, ...args } = opts;
91
+ return activity("dogwoodReplay", args as Record<string, unknown>, profile ?? "policyCheck");
92
+ };
93
+
94
+ /** Options for {@link dogwoodReplayReportStep}. */
95
+ export interface DogwoodReplayReportStepOpts {
96
+ reportPath?: string;
97
+ mode?: PolicyReplayMode;
98
+ title?: string;
99
+ cwd?: string;
100
+ failOnDivergence?: boolean;
101
+ profile?: ActivityStep["profile"];
102
+ }
103
+
104
+ /** Read a written replay report and act on its finding mode. */
105
+ export const dogwoodReplayReportStep = (opts: DogwoodReplayReportStepOpts = {}): ActivityStep => {
106
+ const { profile, ...args } = opts;
107
+ return activity("dogwoodReplayReport", args as Record<string, unknown>, profile ?? "fastIdempotent");
108
+ };
109
+
110
+ /** Options for {@link PolicyReplayOp}. */
111
+ export interface PolicyReplayOpConfig {
112
+ /** Op name (kebab-case) — the `chant run <name>` target and the task queue base. */
113
+ name: string;
114
+ overview?: string;
115
+ /** Defaults to {@link name}. */
116
+ taskQueue?: string;
117
+
118
+ /**
119
+ * Directory every relative path below resolves against, and the one the
120
+ * build script runs in. Default `.`.
121
+ */
122
+ path?: string;
123
+ /**
124
+ * npm script that emits the `.dw` bundle. Default `build`. Pass `false` when
125
+ * the artifacts are checked in — the Artifacts phase is then omitted rather
126
+ * than run as a no-op, so the Op's phase list says what it really does.
127
+ */
128
+ buildScript?: string | false;
129
+
130
+ /** Emitted `.dw` policy set. Default `dist/policies.dw`. */
131
+ policiesPath?: string;
132
+ /** Emitted Cedar action schema. Required by the CLI; default `dist/app.cedarschema`. */
133
+ policySchemaPath?: string;
134
+ /** Emitted `.dwschema`, when the project declares one. */
135
+ eventSchemaPath?: string;
136
+ /** Emitted macro library, when the project ships one out of line. */
137
+ macrosPath?: string;
138
+ /** `providers.json`, with the Rhai inlined under `implementation.script`. */
139
+ providersPath?: string;
140
+
141
+ /** The recorded trace to replay. Required — there is nothing to replay without one. */
142
+ tracePath: string;
143
+
144
+ /** What each decision point must decide. Empty replays and reports verdicts. */
145
+ expect?: readonly ReplayExpectation[];
146
+
147
+ /** What to produce on divergence. Default `report`. */
148
+ onFinding?: PolicyReplayMode;
149
+ /** Title for the issue or pull request the finding mode calls for. */
150
+ title?: string;
151
+ /**
152
+ * Fail the Op when the replay diverged. Default false — an observe-dial Op
153
+ * reports by default, and a red run is a decision the caller makes.
154
+ */
155
+ failOnDivergence?: boolean;
156
+
157
+ /** Where the Replay phase writes its report. Default {@link DEFAULT_REPLAY_REPORT_PATH}. */
158
+ reportPath?: string;
159
+ /** Explicit `dogwood` binary path, for a runner that knows where it built one. */
160
+ binary?: string;
161
+ }
162
+
163
+ /** What {@link PolicyReplayOp} hands back. */
164
+ export interface PolicyReplayOpResources {
165
+ /** The Op — discovered by `chant run <name>`, emitted by `chant build`. */
166
+ op: InstanceType<typeof OpResource>;
167
+ }
168
+
169
+ /**
170
+ * Assemble the replay Op: emit the artifacts, replay them, act on the finding.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * export const { op } = PolicyReplayOp({
175
+ * name: "policy-replay",
176
+ * tracePath: "trace/read-after-login.log",
177
+ * expect: [
178
+ * { timestamp: 10, verdict: "allow", determiningRules: [0] },
179
+ * { timestamp: 7200, verdict: "deny", note: "the login is two hours stale" },
180
+ * ],
181
+ * onFinding: "issue",
182
+ * });
183
+ * ```
184
+ */
185
+ export function PolicyReplayOp(config: PolicyReplayOpConfig): PolicyReplayOpResources {
186
+ const path = config.path ?? ".";
187
+ const mode = config.onFinding ?? "report";
188
+ const reportPath = config.reportPath ?? DEFAULT_REPLAY_REPORT_PATH;
189
+
190
+ const phases = [];
191
+
192
+ if (config.buildScript !== false) {
193
+ phases.push(phase("Artifacts", [build(path, { script: config.buildScript ?? "build" })]));
194
+ }
195
+
196
+ phases.push(
197
+ phase("Replay", [
198
+ {
199
+ ...dogwoodReplayStep({
200
+ cwd: path,
201
+ policiesPath: config.policiesPath ?? "dist/policies.dw",
202
+ policySchemaPath: config.policySchemaPath ?? "dist/app.cedarschema",
203
+ ...(config.eventSchemaPath ? { eventSchemaPath: config.eventSchemaPath } : {}),
204
+ ...(config.macrosPath ? { macrosPath: config.macrosPath } : {}),
205
+ ...(config.providersPath ? { providersPath: config.providersPath } : {}),
206
+ tracePath: config.tracePath,
207
+ ...(config.expect ? { expect: config.expect } : {}),
208
+ mode,
209
+ ...(config.binary ? { binary: config.binary } : {}),
210
+ reportPath,
211
+ }),
212
+ // The divergence count as a workflow search attribute, so "show me the
213
+ // replays that found something" is one filter rather than a log read.
214
+ outcomeAttribute: { name: "Divergences", from: "findings" },
215
+ },
216
+ ]),
217
+ phase("Report", [
218
+ dogwoodReplayReportStep({
219
+ cwd: path,
220
+ reportPath,
221
+ mode,
222
+ ...(config.title ? { title: config.title } : {}),
223
+ ...(config.failOnDivergence ? { failOnDivergence: true } : {}),
224
+ }),
225
+ ]),
226
+ );
227
+
228
+ return {
229
+ op: Op({
230
+ name: config.name,
231
+ overview:
232
+ config.overview ??
233
+ "Replay the declared dogwood policy set against a recorded event trace and report divergence",
234
+ taskQueue: config.taskQueue ?? config.name,
235
+ searchAttributes: {
236
+ Audit: "true",
237
+ Surface: "cedar-dogwood",
238
+ },
239
+ phases,
240
+ }),
241
+ };
242
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Reading the emitted dogwood artifacts back, for the DWD post-synth checks.
3
+ *
4
+ * The checks judge the *text*, not the in-memory model, for the same reason
5
+ * the CED checks judge `policies.cedar.json`: `chant audit` runs over a
6
+ * checked-in artifact chant did not write, and a wall that only fires on
7
+ * chant's own output is not a wall. It also means the typed builders and the
8
+ * walls are independent — DWDC012 catches a windowless `formerly` even though
9
+ * the builders cannot construct one, because `raw()` and a hand-written `.dw`
10
+ * both can.
11
+ *
12
+ * This is scanning, not parsing. Upstream owns the parser and #1659 owns
13
+ * shelling to it; what is here is the subset of the surface that answers the
14
+ * three questions the walls ask, plus enough comment handling not to be fooled
15
+ * by a commented-out clause.
16
+ */
17
+
18
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
19
+ import { DEFAULT_MAX_WINDOW, windowSeconds, type TimeUnit } from "./window";
20
+ import { DOGWOOD_EVENT_SCHEMA_SUFFIX, DOGWOOD_POLICY_SUFFIX } from "./serialize";
21
+
22
+ /** One emitted artifact, with everything needed to name it in a finding. */
23
+ export interface DogwoodArtifact {
24
+ /** The lexicon output it came from. */
25
+ lexicon: string;
26
+ /** The filename, which is what a finding points the reader at. */
27
+ source: string;
28
+ /** The file's text. */
29
+ text: string;
30
+ }
31
+
32
+ /** Every `.dw` policy file in the build output. */
33
+ export function dogwoodPolicyFiles(ctx: PostSynthContext): DogwoodArtifact[] {
34
+ return filesWithSuffix(ctx, DOGWOOD_POLICY_SUFFIX);
35
+ }
36
+
37
+ /** Every `.dwschema` event-schema file in the build output. */
38
+ export function dogwoodSchemaFiles(ctx: PostSynthContext): DogwoodArtifact[] {
39
+ return filesWithSuffix(ctx, DOGWOOD_EVENT_SCHEMA_SUFFIX);
40
+ }
41
+
42
+ function filesWithSuffix(ctx: PostSynthContext, suffix: string): DogwoodArtifact[] {
43
+ const found: DogwoodArtifact[] = [];
44
+ for (const [lexicon, output] of ctx.outputs) {
45
+ if (typeof output === "string") continue;
46
+ for (const [filename, content] of Object.entries(output.files ?? {})) {
47
+ if (typeof content !== "string") continue;
48
+ if (!filename.endsWith(suffix)) continue;
49
+ found.push({ lexicon, source: filename, text: content });
50
+ }
51
+ }
52
+ return found;
53
+ }
54
+
55
+ // ── Comments ──────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Blank out `//` comments, leaving string literals intact.
59
+ *
60
+ * String literals stay because a predicate's action id lives inside one
61
+ * (`Drupe::Action::"Read"`), and blanking those would blind every scan below.
62
+ * The walk is string-aware in the other direction too: a `//` inside a quoted
63
+ * value (a URL, say) is not a comment.
64
+ *
65
+ * Comment bytes become spaces so every offset in the result still lines up
66
+ * with the original file.
67
+ */
68
+ export function blankComments(text: string): string {
69
+ const out = text.split("");
70
+ let inString = false;
71
+ for (let i = 0; i < text.length; i++) {
72
+ const ch = text[i];
73
+ if (inString) {
74
+ if (ch === "\\") {
75
+ i++;
76
+ continue;
77
+ }
78
+ if (ch === '"') inString = false;
79
+ continue;
80
+ }
81
+ if (ch === '"') {
82
+ inString = true;
83
+ continue;
84
+ }
85
+ if (ch === "/" && text[i + 1] === "/") {
86
+ while (i < text.length && text[i] !== "\n") {
87
+ out[i] = " ";
88
+ i++;
89
+ }
90
+ }
91
+ }
92
+ return out.join("");
93
+ }
94
+
95
+ // ── Temporal regions ──────────────────────────────────────────────
96
+
97
+ /**
98
+ * The stretches of a `.dw` file that the temporal sub-parser reads: every
99
+ * `temporal { … }` marker body, and every `def temporal … { … }` macro body.
100
+ *
101
+ * Scanning only these is what keeps a Cedar `when { … }` clause from being
102
+ * mistaken for temporal source — `context.retryWindow == 3` should not read as
103
+ * a window, and a Cedar attribute named `since` is not the `since` operator.
104
+ */
105
+ export function temporalRegions(text: string): string[] {
106
+ const source = blankComments(text);
107
+ const regions: string[] = [];
108
+
109
+ const markers = /\btemporal\s*\{/g;
110
+ for (let m = markers.exec(source); m !== null; m = markers.exec(source)) {
111
+ const body = matchedBlock(source, m.index + m[0].length - 1);
112
+ if (body !== undefined) regions.push(body);
113
+ }
114
+
115
+ const macros = /\bdef\s+temporal\s+[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/g;
116
+ for (let m = macros.exec(source); m !== null; m = macros.exec(source)) {
117
+ const body = matchedBlock(source, m.index + m[0].length - 1);
118
+ if (body !== undefined) regions.push(body);
119
+ }
120
+
121
+ return regions;
122
+ }
123
+
124
+ /** The text between `text[open]` (a `{`) and its matching `}`, or undefined. */
125
+ function matchedBlock(text: string, open: number): string | undefined {
126
+ let depth = 0;
127
+ let inString = false;
128
+ for (let i = open; i < text.length; i++) {
129
+ const ch = text[i];
130
+ if (inString) {
131
+ if (ch === "\\") i++;
132
+ else if (ch === '"') inString = false;
133
+ continue;
134
+ }
135
+ if (ch === '"') inString = true;
136
+ else if (ch === "{") depth++;
137
+ else if (ch === "}") {
138
+ depth--;
139
+ if (depth === 0) return text.slice(open + 1, i);
140
+ }
141
+ }
142
+ return undefined;
143
+ }
144
+
145
+ // ── What the walls ask ────────────────────────────────────────────
146
+
147
+ /** One `Ns::Action::"Name"::kind` predicate head found in temporal source. */
148
+ export interface TemporalPredicateRef {
149
+ /** The qualified action, quoted id and all. */
150
+ action: string;
151
+ /** The event kind segment after the quoted id. */
152
+ kind: string;
153
+ }
154
+
155
+ const PREDICATE = /([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*::"[^"]*")::([A-Za-z_][A-Za-z0-9_]*)/g;
156
+
157
+ /** Every temporal predicate head in a `.dw` file, in source order. */
158
+ export function scanPredicates(text: string): TemporalPredicateRef[] {
159
+ const refs: TemporalPredicateRef[] = [];
160
+ for (const region of temporalRegions(text)) {
161
+ for (let m = PREDICATE.exec(region); m !== null; m = PREDICATE.exec(region)) {
162
+ refs.push({ action: m[1], kind: m[2] });
163
+ }
164
+ PREDICATE.lastIndex = 0;
165
+ }
166
+ return refs;
167
+ }
168
+
169
+ /** One window literal found in temporal source. */
170
+ export interface WindowRef {
171
+ /** As written: `48h`. */
172
+ text: string;
173
+ seconds: number;
174
+ /** `within` for an operator's window, `argument` for a bare macro-call interval. */
175
+ position: "within" | "argument";
176
+ }
177
+
178
+ const WITHIN_WINDOW = /\bwithin\s+(\d+)([smhd])\b/g;
179
+ /** A bare interval in macro-call argument position: `once(1h, …)`, `f(a, 30m)`. */
180
+ const ARGUMENT_WINDOW = /[(,]\s*(\d+)([smhd])\s*(?=[,)])/g;
181
+
182
+ /**
183
+ * Every window literal in a `.dw` file's temporal source.
184
+ *
185
+ * Both shapes count against `max_window`: a macro-call interval is the window
186
+ * a `within ?w` in the macro body resolves to, so `once(48h, …)` looks back
187
+ * exactly as far as `formerly within 48h` does.
188
+ */
189
+ export function scanWindows(text: string): WindowRef[] {
190
+ const windows: WindowRef[] = [];
191
+ for (const region of temporalRegions(text)) {
192
+ for (let m = WITHIN_WINDOW.exec(region); m !== null; m = WITHIN_WINDOW.exec(region)) {
193
+ windows.push(windowRef(m[1], m[2], "within"));
194
+ }
195
+ WITHIN_WINDOW.lastIndex = 0;
196
+ for (let m = ARGUMENT_WINDOW.exec(region); m !== null; m = ARGUMENT_WINDOW.exec(region)) {
197
+ windows.push(windowRef(m[1], m[2], "argument"));
198
+ }
199
+ ARGUMENT_WINDOW.lastIndex = 0;
200
+ }
201
+ return windows;
202
+ }
203
+
204
+ function windowRef(value: string, unit: string, position: WindowRef["position"]): WindowRef {
205
+ const w = { value: Number(value), unit: unit as TimeUnit };
206
+ return { text: `${w.value}${w.unit}`, seconds: windowSeconds(w), position };
207
+ }
208
+
209
+ /**
210
+ * Every `formerly` / `previous` / `since` that is not followed by a `within`.
211
+ *
212
+ * Unrepresentable through the builders — {@link formerly} and its siblings all
213
+ * take the window as an argument — but reachable through `raw()`, through a
214
+ * hand-written `.dw`, and through an audit of a file chant never wrote.
215
+ */
216
+ export function scanWindowlessOperators(text: string): string[] {
217
+ const found: string[] = [];
218
+ const pattern = /\b(formerly|previous|since)\b(?!\s+within\b)/g;
219
+ for (const region of temporalRegions(text)) {
220
+ for (let m = pattern.exec(region); m !== null; m = pattern.exec(region)) {
221
+ found.push(m[1]);
222
+ }
223
+ pattern.lastIndex = 0;
224
+ }
225
+ return found;
226
+ }
227
+
228
+ // ── The event schema side ─────────────────────────────────────────
229
+
230
+ /** What a `.dwschema` file says, as far as the walls need to know. */
231
+ export interface EventSchemaFacts {
232
+ /** Every declared event kind. */
233
+ kinds: string[];
234
+ /** The `max_window` directive in seconds, or upstream's 24h default when absent. */
235
+ maxWindowSeconds: number;
236
+ /** As written (`30d`), or undefined when the directive is absent. */
237
+ maxWindowText?: string;
238
+ /** True when at least one field carries a `pin … = …`. */
239
+ hasPin: boolean;
240
+ }
241
+
242
+ const EVENT_DECL = /\bevent\s*<\s*[A-Za-z_][A-Za-z0-9_]*\s*>\s*::\s*([A-Za-z_][A-Za-z0-9_]*)/g;
243
+ const MAX_WINDOW = /\bmax_window\s*=\s*(\d+)([smhd])\b/;
244
+ const PINNED_FIELD = /\bpin\s+[A-Za-z_][A-Za-z0-9_]*\s*:/;
245
+
246
+ /** Read a `.dwschema` file's declarations. */
247
+ export function readEventSchema(text: string): EventSchemaFacts {
248
+ const source = blankComments(text);
249
+
250
+ const kinds: string[] = [];
251
+ for (let m = EVENT_DECL.exec(source); m !== null; m = EVENT_DECL.exec(source)) {
252
+ if (!kinds.includes(m[1])) kinds.push(m[1]);
253
+ }
254
+ EVENT_DECL.lastIndex = 0;
255
+
256
+ const max = MAX_WINDOW.exec(source);
257
+ return {
258
+ kinds,
259
+ maxWindowSeconds: max
260
+ ? windowSeconds({ value: Number(max[1]), unit: max[2] as TimeUnit })
261
+ : windowSeconds(DEFAULT_MAX_WINDOW),
262
+ maxWindowText: max ? `${max[1]}${max[2]}` : undefined,
263
+ hasPin: PINNED_FIELD.test(source),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * The look-back cap in force for a build.
269
+ *
270
+ * With no emitted schema, `ServiceSchema::defaults()` applies and the cap is
271
+ * 24h. With several, the tightest one wins — a window legal under one schema
272
+ * and not another is a window some consumer will reject.
273
+ */
274
+ export function effectiveMaxWindowSeconds(schemas: EventSchemaFacts[]): { seconds: number; text: string } {
275
+ if (schemas.length === 0) {
276
+ return {
277
+ seconds: windowSeconds(DEFAULT_MAX_WINDOW),
278
+ text: `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
279
+ };
280
+ }
281
+ let tightest = schemas[0];
282
+ for (const s of schemas) if (s.maxWindowSeconds < tightest.maxWindowSeconds) tightest = s;
283
+ return {
284
+ seconds: tightest.maxWindowSeconds,
285
+ text: tightest.maxWindowText ?? `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
286
+ };
287
+ }