@intentius/chant-lexicon-cedar 0.44.8 → 0.44.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +58 -0
  2. package/dist/codegen/package.d.ts.map +1 -1
  3. package/dist/config.d.ts +25 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/dogwood/cli.d.ts +206 -0
  6. package/dist/dogwood/cli.d.ts.map +1 -0
  7. package/dist/dogwood/event-schema.d.ts +161 -0
  8. package/dist/dogwood/event-schema.d.ts.map +1 -0
  9. package/dist/dogwood/index.d.ts +33 -0
  10. package/dist/dogwood/index.d.ts.map +1 -0
  11. package/dist/dogwood/macros.d.ts +96 -0
  12. package/dist/dogwood/macros.d.ts.map +1 -0
  13. package/dist/dogwood/policy.d.ts +120 -0
  14. package/dist/dogwood/policy.d.ts.map +1 -0
  15. package/dist/dogwood/scan.d.ts +109 -0
  16. package/dist/dogwood/scan.d.ts.map +1 -0
  17. package/dist/dogwood/serialize.d.ts +46 -0
  18. package/dist/dogwood/serialize.d.ts.map +1 -0
  19. package/dist/dogwood/temporal.d.ts +259 -0
  20. package/dist/dogwood/temporal.d.ts.map +1 -0
  21. package/dist/dogwood/upstream.d.ts +41 -0
  22. package/dist/dogwood/upstream.d.ts.map +1 -0
  23. package/dist/dogwood/window.d.ts +73 -0
  24. package/dist/dogwood/window.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +13 -3
  28. package/dist/lint/audit-catalog.d.ts.map +1 -1
  29. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  30. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  31. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  32. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  33. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  34. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  35. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  36. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  37. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  38. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  39. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  40. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  41. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  42. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  43. package/dist/lint/post-synth/index.d.ts.map +1 -1
  44. package/dist/manifest.json +1 -1
  45. package/dist/okf/index.md +6 -0
  46. package/dist/okf/rules/DWDC010.md +11 -0
  47. package/dist/okf/rules/DWDC011.md +11 -0
  48. package/dist/okf/rules/DWDC012.md +11 -0
  49. package/dist/okf/rules/DWDE010.md +11 -0
  50. package/dist/okf/rules/DWDE011.md +11 -0
  51. package/dist/okf/rules/DWDS010.md +11 -0
  52. package/dist/policy-text.d.ts +53 -0
  53. package/dist/policy-text.d.ts.map +1 -0
  54. package/dist/rules/dogwood-helpers.ts +139 -0
  55. package/dist/rules/dwdc010.ts +62 -0
  56. package/dist/rules/dwdc011.ts +61 -0
  57. package/dist/rules/dwdc012.ts +46 -0
  58. package/dist/rules/dwde010.ts +130 -0
  59. package/dist/rules/dwde011.ts +108 -0
  60. package/dist/rules/dwds010.ts +46 -0
  61. package/dist/serializer.d.ts +10 -18
  62. package/dist/serializer.d.ts.map +1 -1
  63. package/dist/skills/chant-cedar-authoring.md +180 -0
  64. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  65. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  66. package/package.json +2 -2
  67. package/src/codegen/package.ts +3 -2
  68. package/src/config.test.ts +12 -0
  69. package/src/config.ts +28 -0
  70. package/src/dogwood/cli.test.ts +392 -0
  71. package/src/dogwood/cli.ts +545 -0
  72. package/src/dogwood/event-schema.test.ts +218 -0
  73. package/src/dogwood/event-schema.ts +318 -0
  74. package/src/dogwood/index.ts +198 -0
  75. package/src/dogwood/macros.test.ts +104 -0
  76. package/src/dogwood/macros.ts +229 -0
  77. package/src/dogwood/policy.test.ts +94 -0
  78. package/src/dogwood/policy.ts +141 -0
  79. package/src/dogwood/scan.ts +287 -0
  80. package/src/dogwood/serialize.test.ts +331 -0
  81. package/src/dogwood/serialize.ts +209 -0
  82. package/src/dogwood/temporal.test.ts +272 -0
  83. package/src/dogwood/temporal.ts +592 -0
  84. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  85. package/src/dogwood/testdata/default-macros.dw +23 -0
  86. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  87. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  88. package/src/dogwood/testdata/pinned.dwschema +31 -0
  89. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  90. package/src/dogwood/testdata/read-after-login.dw +17 -0
  91. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  92. package/src/dogwood/upstream.ts +41 -0
  93. package/src/dogwood/window.ts +124 -0
  94. package/src/index.ts +24 -0
  95. package/src/lint/audit-catalog.ts +56 -0
  96. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  97. package/src/lint/post-synth/dwd-post-synth.test.ts +256 -0
  98. package/src/lint/post-synth/dwdc010.ts +62 -0
  99. package/src/lint/post-synth/dwdc011.ts +61 -0
  100. package/src/lint/post-synth/dwdc012.ts +46 -0
  101. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  102. package/src/lint/post-synth/dwde010.ts +130 -0
  103. package/src/lint/post-synth/dwde011.ts +108 -0
  104. package/src/lint/post-synth/dwds010.ts +46 -0
  105. package/src/lint/post-synth/index.ts +12 -0
  106. package/src/lint/post-synth/post-synth.test.ts +7 -3
  107. package/src/policy-text.ts +128 -0
  108. package/src/serializer.ts +71 -109
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Turning a build's emitted files into `dogwood` CLI invocations (#1659).
3
+ *
4
+ * The DWDE checks both need the same thing: which of the emitted `.dw` files
5
+ * is a policy set, which is a macro library, which `.cedarschema` backs
6
+ * `--policy-schema`, and which `.dwschema` backs `--event-schema`. Planning
7
+ * that once here keeps the two checks from disagreeing about what they ran —
8
+ * DWDE010 validates a bundle and DWDE011 lowers the *same* bundle, and a
9
+ * finding from one that names different inputs than the other would be a
10
+ * puzzle rather than a report.
11
+ *
12
+ * Excluded from check auto-discovery by the "helper" filename filter.
13
+ */
14
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
15
+ import { blankComments, dogwoodPolicyFiles, dogwoodSchemaFiles, type DogwoodArtifact } from "../../dogwood/scan";
16
+ import type { DogwoodBundle } from "../../dogwood/cli";
17
+
18
+ /** One planned invocation: the files, and the names to blame in a finding. */
19
+ export interface PreparedBundle {
20
+ lexicon: string;
21
+ /** The `.dw` policy set filename. */
22
+ source: string;
23
+ /** The `.cedarschema` filename passed as `--policy-schema`. */
24
+ policySchemaSource: string;
25
+ /** The `.dwschema` filename passed as `--event-schema`, when one was emitted. */
26
+ eventSchemaSource?: string;
27
+ bundle: DogwoodBundle;
28
+ }
29
+
30
+ /** What the CLI-gated checks should do with this build. */
31
+ export interface DogwoodRunPlan {
32
+ /** True when the build emitted a `.dw` policy set at all. */
33
+ hasPolicies: boolean;
34
+ /** Every invocation to make. Empty when {@link blocked} is set. */
35
+ bundles: PreparedBundle[];
36
+ /** Why nothing can be run, when policies exist but a required input does not. */
37
+ blocked?: string;
38
+ /** The lexicon the `.dw` output came from, for a finding that names no file. */
39
+ lexicon?: string;
40
+ }
41
+
42
+ /**
43
+ * A `.dw` file holding only `def` declarations is a macro library, not a
44
+ * policy set.
45
+ *
46
+ * By structure rather than by filename: the serializer's default is
47
+ * `macros.dw`, but `MacroLibraryProps.filename` overrides it, and `chant
48
+ * audit` runs over trees chant never wrote. A policy set is a file with a
49
+ * `permit` or `forbid` head in it — upstream's policy grammar has no third
50
+ * effect — and a set with inlined macros still has one.
51
+ */
52
+ export function looksLikeMacroLibrary(text: string): boolean {
53
+ return !/\b(permit|forbid)\s*\(/.test(blankComments(text));
54
+ }
55
+
56
+ function byName(a: DogwoodArtifact, b: DogwoodArtifact): number {
57
+ return a.source.localeCompare(b.source);
58
+ }
59
+
60
+ /**
61
+ * The Cedar action schema the CLI needs, as text.
62
+ *
63
+ * `--policy-schema` reads a human-readable `.cedarschema`; a `.cedarschema.json`
64
+ * is not a substitute, so it is not offered as one. Sorted by filename so a
65
+ * build emitting several picks the same one on every run.
66
+ */
67
+ function findPolicySchema(ctx: PostSynthContext): DogwoodArtifact | undefined {
68
+ const found: DogwoodArtifact[] = [];
69
+ for (const [lexicon, output] of ctx.outputs) {
70
+ if (typeof output === "string") continue;
71
+ for (const [source, text] of Object.entries(output.files ?? {})) {
72
+ if (typeof text !== "string") continue;
73
+ if (!source.endsWith(".cedarschema")) continue;
74
+ found.push({ lexicon, source, text });
75
+ }
76
+ }
77
+ return found.sort(byName)[0];
78
+ }
79
+
80
+ /**
81
+ * Plan every `dogwood` invocation this build calls for.
82
+ *
83
+ * One bundle per policy set per emitted event schema. A build with several
84
+ * `.dwschema` files means several services, and `--event-schema` takes exactly
85
+ * one file, so the set is run against each and every finding names the schema
86
+ * it was found under — the alternative is picking one silently and reporting
87
+ * against a schema the reader did not choose. Macro libraries do combine: a
88
+ * library is a sequence of `def`s, so several concatenate into the one file
89
+ * `--macros` accepts.
90
+ */
91
+ export function planDogwoodRuns(ctx: PostSynthContext): DogwoodRunPlan {
92
+ const files = dogwoodPolicyFiles(ctx).sort(byName);
93
+ const policySets = files.filter((f) => !looksLikeMacroLibrary(f.text));
94
+ if (policySets.length === 0) return { hasPolicies: false, bundles: [] };
95
+
96
+ const lexicon = policySets[0].lexicon;
97
+ const libraries = files.filter((f) => looksLikeMacroLibrary(f.text));
98
+ const macros = libraries.length > 0 ? libraries.map((f) => f.text).join("\n") : undefined;
99
+
100
+ const policySchema = findPolicySchema(ctx);
101
+ if (!policySchema) {
102
+ return {
103
+ hasPolicies: true,
104
+ bundles: [],
105
+ lexicon,
106
+ blocked:
107
+ "no Cedar action schema (.cedarschema) was emitted beside them, and `dogwood validate` requires one via --policy-schema",
108
+ };
109
+ }
110
+
111
+ const eventSchemas = dogwoodSchemaFiles(ctx).sort(byName);
112
+ const bundles: PreparedBundle[] = [];
113
+
114
+ for (const set of policySets) {
115
+ for (const events of eventSchemas.length > 0 ? eventSchemas : [undefined]) {
116
+ bundles.push({
117
+ lexicon: set.lexicon,
118
+ source: set.source,
119
+ policySchemaSource: policySchema.source,
120
+ ...(events ? { eventSchemaSource: events.source } : {}),
121
+ bundle: {
122
+ policies: set.text,
123
+ policySchema: policySchema.text,
124
+ ...(events ? { eventSchema: events.text } : {}),
125
+ ...(macros !== undefined ? { macros } : {}),
126
+ },
127
+ });
128
+ }
129
+ }
130
+
131
+ return { hasPolicies: true, bundles, lexicon };
132
+ }
133
+
134
+ /** How a finding names the inputs a run was made with. */
135
+ export function describeBundle(prepared: PreparedBundle): string {
136
+ const parts = [`"${prepared.source}"`, `against "${prepared.policySchemaSource}"`];
137
+ if (prepared.eventSchemaSource) parts.push(`with "${prepared.eventSchemaSource}"`);
138
+ return parts.join(" ");
139
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * DWDC010: a temporal predicate must name an event kind the schema declares
3
+ *
4
+ * Upstream rejects this outright — a predicate naming an undeclared kind fails
5
+ * `dogwood validate` with code `extension`: "predicate
6
+ * `Drupe::Action::\"Read\"::nosuchkind` does not name a declared event (no
7
+ * event kind `nosuchkind` derived for action `Read`)". Catching it in
8
+ * TypeScript is the point of the split epic #1646 drew: everything answerable
9
+ * without the binary runs always and gates, and this one is answerable.
10
+ *
11
+ * The check is on the event *kind*, not the action, and that is not a
12
+ * shortcut. Upstream's `.dwschema` grammar declares events against a symbolic
13
+ * action binder — `decision event <A>::request { … }` — so the file names no
14
+ * actions at all and cannot be asked whether one exists. The action half of
15
+ * that upstream error comes from the Cedar action schema
16
+ * (`--policy-schema`), which is #1659's CLI-gated leg.
17
+ *
18
+ * Silent when no `.dwschema` was emitted: with none supplied,
19
+ * `ServiceSchema::defaults()` decides the kinds at the far end, and guessing
20
+ * that a project's out-of-band schema matches upstream's default would fail
21
+ * builds for a policy set that is fine.
22
+ */
23
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
24
+ import { dogwoodPolicyFiles, dogwoodSchemaFiles, readEventSchema, scanPredicates } from "../../dogwood/scan";
25
+
26
+ export const dwdc010: PostSynthCheck = {
27
+ id: "DWDC010",
28
+ description: "A dogwood temporal predicate names an event kind the emitted .dwschema declares",
29
+
30
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
31
+ const schemas = dogwoodSchemaFiles(ctx);
32
+ if (schemas.length === 0) return [];
33
+
34
+ const declared = new Set<string>();
35
+ for (const schema of schemas) {
36
+ for (const kind of readEventSchema(schema.text).kinds) declared.add(kind);
37
+ }
38
+ if (declared.size === 0) return [];
39
+
40
+ const known = [...declared].sort().join(", ");
41
+ const diagnostics: PostSynthDiagnostic[] = [];
42
+
43
+ for (const policies of dogwoodPolicyFiles(ctx)) {
44
+ const seen = new Set<string>();
45
+ for (const ref of scanPredicates(policies.text)) {
46
+ if (declared.has(ref.kind)) continue;
47
+ const key = `${ref.action}::${ref.kind}`;
48
+ if (seen.has(key)) continue;
49
+ seen.add(key);
50
+ diagnostics.push({
51
+ checkId: "DWDC010",
52
+ severity: "error",
53
+ message: `Dogwood policy set "${policies.source}" has a temporal predicate on ${key}, but the emitted event schema declares no event kind "${ref.kind}" (declared: ${known}).`,
54
+ entity: policies.source,
55
+ lexicon: policies.lexicon,
56
+ });
57
+ }
58
+ }
59
+
60
+ return diagnostics;
61
+ },
62
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * DWDC011: a temporal window must fit inside the schema's `max_window`
3
+ *
4
+ * `formerly within 48h` under a 24h cap is an upstream validation error —
5
+ * "temporal window `48h` exceeds the maximum allowed window `24h` set by the
6
+ * event schema's `max_window`" — and it is arithmetic, so it needs no binary.
7
+ *
8
+ * Unlike DWDC010 this fires with no `.dwschema` emitted, because the cap
9
+ * applies either way: `ServiceSchema::defaults()` caps look-back at 24h, and
10
+ * so does an emitted schema that omits the directive. Where several schemas
11
+ * are emitted the tightest cap wins — a window one consumer accepts and
12
+ * another rejects is still a window that will be rejected.
13
+ *
14
+ * Macro-call intervals count. `once(48h, …)` expands to `formerly within 48h`
15
+ * through the default library's `within ?w`, so it looks back exactly as far.
16
+ */
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import {
19
+ dogwoodPolicyFiles,
20
+ dogwoodSchemaFiles,
21
+ effectiveMaxWindowSeconds,
22
+ readEventSchema,
23
+ scanWindows,
24
+ } from "../../dogwood/scan";
25
+
26
+ export const dwdc011: PostSynthCheck = {
27
+ id: "DWDC011",
28
+ description: "A dogwood temporal window stays inside the event schema's max_window cap",
29
+
30
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
31
+ const policyFiles = dogwoodPolicyFiles(ctx);
32
+ if (policyFiles.length === 0) return [];
33
+
34
+ const schemas = dogwoodSchemaFiles(ctx).map((s) => readEventSchema(s.text));
35
+ const cap = effectiveMaxWindowSeconds(schemas);
36
+ const source =
37
+ schemas.length === 0
38
+ ? "upstream's default cap (no event schema was emitted)"
39
+ : `the emitted event schema's max_window`;
40
+
41
+ const diagnostics: PostSynthDiagnostic[] = [];
42
+
43
+ for (const policies of policyFiles) {
44
+ const reported = new Set<string>();
45
+ for (const window of scanWindows(policies.text)) {
46
+ if (window.seconds <= cap.seconds) continue;
47
+ if (reported.has(window.text)) continue;
48
+ reported.add(window.text);
49
+ diagnostics.push({
50
+ checkId: "DWDC011",
51
+ severity: "error",
52
+ message: `Dogwood policy set "${policies.source}" looks back ${window.text}, past the ${cap.text} allowed by ${source}. Raise max_window in the event schema, or shorten the window.`,
53
+ entity: policies.source,
54
+ lexicon: policies.lexicon,
55
+ });
56
+ }
57
+ }
58
+
59
+ return diagnostics;
60
+ },
61
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DWDC012: `formerly`, `previous` and `since` carry a mandatory window
3
+ *
4
+ * Upstream's grammar makes `within` non-optional on all three:
5
+ *
6
+ * ```
7
+ * once_op = { "formerly" ~ within ~ atom }
8
+ * previous_op = { "previous" ~ within ~ atom }
9
+ * conjunct_or_since = { neg_conjunct ~ ("since" ~ within ~ atom)? }
10
+ * ```
11
+ *
12
+ * The typed builders make this unrepresentable — `formerly(window, body)` has
13
+ * nowhere to put a missing window — which is most of the fix. This check is
14
+ * the other part: `raw()` exists as an escape hatch, a hand-written `.dw` can
15
+ * be built by any other tool, and `chant audit` runs over files chant never
16
+ * emitted. A wall that only holds for the path that cannot breach it is not
17
+ * a wall.
18
+ */
19
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
20
+ import { dogwoodPolicyFiles, scanWindowlessOperators } from "../../dogwood/scan";
21
+
22
+ export const dwdc012: PostSynthCheck = {
23
+ id: "DWDC012",
24
+ description: "A dogwood formerly/previous/since operator carries its mandatory within window",
25
+
26
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
27
+ const diagnostics: PostSynthDiagnostic[] = [];
28
+
29
+ for (const policies of dogwoodPolicyFiles(ctx)) {
30
+ const seen = new Set<string>();
31
+ for (const operator of scanWindowlessOperators(policies.text)) {
32
+ if (seen.has(operator)) continue;
33
+ seen.add(operator);
34
+ diagnostics.push({
35
+ checkId: "DWDC012",
36
+ severity: "error",
37
+ message: `Dogwood policy set "${policies.source}" uses \`${operator}\` with no \`within\` window. All three past-only operators require one — write \`${operator} within <n><s|m|h|d> …\`.`,
38
+ entity: policies.source,
39
+ lexicon: policies.lexicon,
40
+ });
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * DWDE010: the emitted `.dw` set validates clean under `dogwood validate`
3
+ *
4
+ * The CLI-gated half of the epic's validation split. Everything the DWDC walls
5
+ * ask is answerable in TypeScript and gates unconditionally; everything else —
6
+ * macro expansion, the temporal type checker, the Cedar body checked against
7
+ * the action schema *through upstream's own frontend* — needs upstream's Rust
8
+ * frontend, which ships as a binary and nothing else. No npm package, no wasm
9
+ * build, no bindings.
10
+ *
11
+ * So this check has two modes and says which one it is in:
12
+ *
13
+ * - **Binary present.** Runs `dogwood validate --format json` over each
14
+ * emitted policy set and reports every finding as an error. Byte-offset
15
+ * labels come through as byte offsets — see `../../dogwood/cli.ts` for why
16
+ * they are not converted to line/column.
17
+ * - **Binary absent.** Exactly one `info` finding naming the binary, where
18
+ * chant looked, and this issue. The epic's words are "an explicit,
19
+ * issue-linked exception, not a silent one" — a check that quietly passes
20
+ * when it could not run is claiming a guarantee it did not make, which is
21
+ * the same failure CEDE010's no-schema advisory exists to avoid.
22
+ *
23
+ * A run that could not be made at all — a spawn failure, an unknown flag, JSON
24
+ * in a shape this adapter does not know — is `warning`, not `error`. Upstream
25
+ * is a read-only squash-sync mirror with no tags and no changelog, and a flag
26
+ * rename in a sync would otherwise fail every build that has the binary
27
+ * installed. The adapter never reads exit 2 as "rejected" on its own for the
28
+ * same reason: clap spends that code on usage errors too.
29
+ */
30
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
31
+ import {
32
+ DOGWOOD_BINARY_NAME,
33
+ DOGWOOD_SEARCH_ORDER,
34
+ findDogwoodBinary,
35
+ formatDogwoodDiagnostic,
36
+ runDogwoodValidate,
37
+ } from "../../dogwood/cli";
38
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
39
+
40
+ export const dwde010: PostSynthCheck = {
41
+ id: "DWDE010",
42
+ description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
43
+
44
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
+ const plan = planDogwoodRuns(ctx);
46
+ if (!plan.hasPolicies) return [];
47
+
48
+ const binary = findDogwoodBinary();
49
+ if (!binary) {
50
+ return [
51
+ {
52
+ checkId: "DWDE010",
53
+ severity: "info",
54
+ message: `This build emitted dogwood .dw policies, but no \`${DOGWOOD_BINARY_NAME}\` binary was found, so full .dw validation did not run — the DWDC walls checked what TypeScript can answer and nothing checked macro expansion, the temporal type check, or the Cedar body against the action schema. chant looked at ${DOGWOOD_SEARCH_ORDER}. See chant #1659.`,
55
+ lexicon: plan.lexicon,
56
+ },
57
+ ];
58
+ }
59
+
60
+ if (plan.blocked) {
61
+ return [
62
+ {
63
+ checkId: "DWDE010",
64
+ severity: "info",
65
+ message: `This build emitted dogwood .dw policies and \`${DOGWOOD_BINARY_NAME}\` is available, but ${plan.blocked}, so full .dw validation did not run. See chant #1659.`,
66
+ lexicon: plan.lexicon,
67
+ },
68
+ ];
69
+ }
70
+
71
+ const diagnostics: PostSynthDiagnostic[] = [];
72
+
73
+ for (const prepared of plan.bundles) {
74
+ const where = describeBundle(prepared);
75
+ const result = runDogwoodValidate(binary.path, prepared.bundle);
76
+
77
+ if (result.kind === "unusable") {
78
+ diagnostics.push({
79
+ checkId: "DWDE010",
80
+ severity: "warning",
81
+ message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
82
+ entity: prepared.source,
83
+ lexicon: prepared.lexicon,
84
+ });
85
+ continue;
86
+ }
87
+
88
+ if (result.kind === "fatal") {
89
+ for (const finding of [result.error, ...result.related]) {
90
+ diagnostics.push({
91
+ checkId: "DWDE010",
92
+ severity: "error",
93
+ message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
94
+ entity: prepared.source,
95
+ lexicon: prepared.lexicon,
96
+ });
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (result.kind === "rejected") {
102
+ for (const finding of result.errors) {
103
+ diagnostics.push({
104
+ checkId: "DWDE010",
105
+ severity: "error",
106
+ message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
107
+ entity: prepared.source,
108
+ lexicon: prepared.lexicon,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Upstream's own warnings ride along at warning severity. They do not
114
+ // fail `dogwood validate` (`passed` ignores them), and they should not
115
+ // fail a build either — but dropping them would throw away the half of
116
+ // the report that says a policy is legal and pointless.
117
+ for (const finding of result.warnings) {
118
+ diagnostics.push({
119
+ checkId: "DWDE010",
120
+ severity: "warning",
121
+ message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
122
+ entity: prepared.source,
123
+ lexicon: prepared.lexicon,
124
+ });
125
+ }
126
+ }
127
+
128
+ return diagnostics;
129
+ },
130
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * DWDE011: the lowered Cedar body validates clean under Cedar's own validator
3
+ *
4
+ * The epic's stated design for the non-temporal half: chant does not
5
+ * re-implement lowering — `dogwood lower` produces the analyzable plain-Cedar
6
+ * form, and a reimplementation would drift — but once that form exists, Cedar's
7
+ * own validator is the thing that judges it. The #1657 verification put all 86
8
+ * upstream example bundles through this exact path and every one parsed and
9
+ * validated clean in strict mode, which is what makes a finding here mean
10
+ * something: the lowered output is genuinely plain Cedar, so a validation error
11
+ * is the policy's, not the pipeline's.
12
+ *
13
+ * What this catches that DWDE010 does not is narrow but real. `dogwood
14
+ * validate` type-checks through upstream's vendored Cedar; this runs the
15
+ * *published* `@cedar-policy/cedar-wasm` over the same artifacts. A body that
16
+ * upstream's pinned Cedar accepts and the Cedar the rest of chant validates
17
+ * against rejects is exactly the drift the pin exists to make visible — and the
18
+ * augmented schema, with its hoisted `context.*` temporal slots, is the form a
19
+ * downstream Cedar policy store will actually receive.
20
+ *
21
+ * The cedar-wasm traps from #1648 all apply and are handled in
22
+ * `./wasm-helpers.ts`: `type: "success"` means validation *ran*, not that it
23
+ * passed; `validationErrors` comes back in a different order almost every call
24
+ * and is sorted before anything reads it; a malformed call throws rather than
25
+ * returning a failure answer.
26
+ *
27
+ * Silent when `lower` did not run — the binary is absent, or no action schema
28
+ * was emitted. Both cases are DWDE010's advisory to report, once, rather than
29
+ * twice in different words.
30
+ */
31
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
32
+ import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli";
33
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
34
+ import { loadWasm, validatePolicySet } from "./wasm-helpers";
35
+
36
+ export const dwde011: PostSynthCheck = {
37
+ id: "DWDE011",
38
+ description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
39
+
40
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
41
+ const plan = planDogwoodRuns(ctx);
42
+ if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0) return [];
43
+
44
+ const binary = findDogwoodBinary();
45
+ if (!binary) return [];
46
+
47
+ // A missing validator is not this check's finding to report; the cedar leg
48
+ // says so already through CEDC010.
49
+ const wasm = loadWasm();
50
+ if (!wasm) return [];
51
+
52
+ const diagnostics: PostSynthDiagnostic[] = [];
53
+
54
+ for (const prepared of plan.bundles) {
55
+ const where = describeBundle(prepared);
56
+ const lowered = runDogwoodLower(binary.path, prepared.bundle);
57
+
58
+ if (lowered.kind === "unusable") {
59
+ diagnostics.push({
60
+ checkId: "DWDE011",
61
+ severity: "warning",
62
+ message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
63
+ entity: prepared.source,
64
+ lexicon: prepared.lexicon,
65
+ });
66
+ continue;
67
+ }
68
+
69
+ if (lowered.kind === "fatal") {
70
+ // The same fatal DWDE010 reports from its own run. Saying it twice
71
+ // helps nobody, so this arm stays quiet and lets the validate leg own
72
+ // the parse/lower channel.
73
+ continue;
74
+ }
75
+
76
+ const { cedarPolicies, cedarSchema } = lowered.value;
77
+
78
+ // The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
79
+ // (`policy0`, `policy1`) for a bare-string set and does not read `@id`
80
+ // annotations as ids (#1648), so a finding names the .dw file it came
81
+ // from as well as the id the validator used.
82
+ const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
83
+
84
+ if (outcome.failure) {
85
+ diagnostics.push({
86
+ checkId: "DWDE011",
87
+ severity: "error",
88
+ message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
89
+ entity: prepared.source,
90
+ lexicon: prepared.lexicon,
91
+ });
92
+ continue;
93
+ }
94
+
95
+ for (const finding of outcome.errors) {
96
+ diagnostics.push({
97
+ checkId: "DWDE011",
98
+ severity: "error",
99
+ message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
100
+ entity: prepared.source,
101
+ lexicon: prepared.lexicon,
102
+ });
103
+ }
104
+ }
105
+
106
+ return diagnostics;
107
+ },
108
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DWDS010: an emitted event schema with no pinned field widens every predicate
3
+ *
4
+ * The #1657 verification is specific about this one. `ServiceSchema::defaults()`
5
+ * — what runs when nobody passes `--event-schema` — uses
6
+ * `configuration/event-schemas/pinned.dwschema`, which carries
7
+ * `pin callerPrincipal: principalType(A) = principal` on every kind. Every
8
+ * temporal predicate is therefore correlated to the deciding request's
9
+ * principal, and events from other principals are invisible.
10
+ *
11
+ * Supplying any event schema opts out of that default wholesale. So a schema
12
+ * emitted without a pin does not merely "not add" a correlation: it removes
13
+ * one the policy author very likely assumed, and every `formerly` in the set
14
+ * starts matching other principals' events. That is a legitimate design —
15
+ * cross-principal correlation is the reason to write your own schema — but it
16
+ * is a decision, and a decision nobody can see in a diff is one nobody made.
17
+ *
18
+ * Report-only: chant does not know which the author wanted, only that it
19
+ * should be said out loud. `defaultEventSchema({ pinCallerPrincipal: false })`
20
+ * also stamps the reasoning into the emitted file.
21
+ */
22
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
+ import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan";
24
+
25
+ export const dwds010: PostSynthCheck = {
26
+ id: "DWDS010",
27
+ description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
28
+
29
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
30
+ const diagnostics: PostSynthDiagnostic[] = [];
31
+
32
+ for (const schema of dogwoodSchemaFiles(ctx)) {
33
+ const facts = readEventSchema(schema.text);
34
+ if (facts.kinds.length === 0 || facts.hasPin) continue;
35
+ diagnostics.push({
36
+ checkId: "DWDS010",
37
+ severity: "warning",
38
+ message: `Dogwood event schema "${schema.source}" declares no pinned field, so temporal predicates correlate across every principal — wider than upstream's default, which pins callerPrincipal to the deciding request's principal. Add a pinned field, or record why cross-principal correlation is wanted.`,
39
+ entity: schema.source,
40
+ lexicon: schema.lexicon,
41
+ });
42
+ }
43
+
44
+ return diagnostics;
45
+ },
46
+ };
@@ -19,6 +19,12 @@
19
19
  * (#1650) generates typed classes onto exactly this shape, so nothing here
20
20
  * changes when it lands — the guards become typed expressions rather than
21
21
  * the opaque Cedar-expression strings they are today.
22
+ *
23
+ * A third view arrives when the build holds dogwood entities (#1658): `.dw`
24
+ * policy text and its `.dwschema`/`macros.dw` companions, rendered by
25
+ * `./dogwood/serialize.ts` and returned in the same result. Both legs share
26
+ * `./policy-text.ts` rather than a copy of it, because a `.dw` policy's head
27
+ * *is* a Cedar policy's head.
22
28
  */
23
29
  import type { Declarable } from "@intentius/chant/declarable";
24
30
  import type { Serializer } from "@intentius/chant/serializer";
@@ -69,14 +75,7 @@ export interface CedarPolicyProps {
69
75
  */
70
76
  annotations?: Record<string, string>;
71
77
  }
72
- /** Escape a string for a double-quoted Cedar literal. */
73
- export declare function escapeCedarString(value: string): string;
74
- /**
75
- * Derive a policy id from a logical name: `allowAdminRead` → `allow-admin-read`.
76
- * Cedar ids are free-form strings; kebab-case keeps them readable in the
77
- * `@id` annotation and stable across a rename-free refactor.
78
- */
79
- export declare function policyIdFromLogicalName(name: string): string;
78
+ export { conditionStrings, escapeCedarString, getProps, isRecord, policyIdFromLogicalName, renderPolicyHead, renderScope, resolvePolicyId, } from "./policy-text.js";
80
79
  /**
81
80
  * One policy, as `.cedar` text.
82
81
  *
@@ -84,6 +83,9 @@ export declare function policyIdFromLogicalName(name: string): string;
84
83
  * nothing else — `AWS::VerifiedPermissions::Policy` carries its policy as
85
84
  * `Definition.Static.Statement`, a single Cedar policy rather than a set. Two
86
85
  * renderers would be two dialects; there is one.
86
+ *
87
+ * The head comes from `renderPolicyHead`, which the dogwood dialect's `.dw`
88
+ * leg also calls: three surfaces, one rendering of an annotation and a scope.
87
89
  */
88
90
  export declare function renderPolicyText(id: string, props: Record<string, unknown>): string;
89
91
  /** The JSON policy-set envelope, exactly as `cedar-wasm` accepts it. */
@@ -117,16 +119,6 @@ export interface CedarPolicyRecord {
117
119
  /** Props with references resolved, ready for either renderer. */
118
120
  props: Record<string, unknown>;
119
121
  }
120
- /**
121
- * The Cedar id for a policy: an explicit `annotations.id` when the author gave
122
- * one, else derived from the logical name.
123
- *
124
- * This is the only rule that links a chant entity to a policy in a live AVP
125
- * store (#1652) — the observation resolves the same id from the same props and
126
- * matches it against the `@id` annotation the statement carries. Two copies of
127
- * this rule would be a mapping that drifts silently, so there is one.
128
- */
129
- export declare function resolvePolicyId(logicalName: string, props: Record<string, unknown>): string;
130
122
  /**
131
123
  * Every `Cedar::Policy` in a build, with references walked and ids resolved.
132
124
  *
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AAGhF,OAAO,EAAgD,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAY5F,8CAA8C;AAC9C,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAEzD,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACrB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,EAAE,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAEvD,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,qDAAqD;IACrD,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AASD,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,eAAe,GAChB,MAAM,eAAe,CAAC;AAwBvB;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAWnF;AAID,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC3C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,aAAa,EAAE,OAAO,EAAE,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,GAAG,CAAC,EAAE,kBAAkB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA4BxF;AAID,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,iBAAiB,EAAE,CAiBzF;AAID,eAAO,MAAM,eAAe,EAAE,UAqD7B,CAAC"}