@intentius/chant-lexicon-cedar 0.44.14 → 0.45.0

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 (49) hide show
  1. package/README.md +13 -4
  2. package/dist/avp/client.d.ts +18 -0
  3. package/dist/avp/client.d.ts.map +1 -1
  4. package/dist/codegen/docs.d.ts +5 -9
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/codegen/generate.d.ts +35 -8
  7. package/dist/codegen/generate.d.ts.map +1 -1
  8. package/dist/commands.d.ts +14 -0
  9. package/dist/commands.d.ts.map +1 -0
  10. package/dist/config.d.ts +38 -2
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/index.d.ts +5 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/init-templates.d.ts +1 -1
  15. package/dist/integrity.json +4 -4
  16. package/dist/lint/post-synth/cede010.d.ts +5 -4
  17. package/dist/lint/post-synth/cede010.d.ts.map +1 -1
  18. package/dist/manifest.json +1 -1
  19. package/dist/plugin.d.ts.map +1 -1
  20. package/dist/rules/cede010.ts +5 -4
  21. package/dist/schema-artifact.d.ts +50 -0
  22. package/dist/schema-artifact.d.ts.map +1 -0
  23. package/dist/serializer.d.ts.map +1 -1
  24. package/dist/skills/chant-cedar-authoring.md +9 -3
  25. package/dist/spec/fetch.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/avp/OWNERSHIP.md +4 -1
  28. package/src/avp/client.test.ts +35 -0
  29. package/src/avp/client.ts +29 -2
  30. package/src/codegen/docs.ts +5 -799
  31. package/src/codegen/generate-cli.ts +8 -6
  32. package/src/codegen/generate.ts +63 -14
  33. package/src/codegen/output-dir.test.ts +137 -0
  34. package/src/commands.test.ts +93 -0
  35. package/src/commands.ts +95 -0
  36. package/src/config.ts +50 -4
  37. package/src/index.ts +10 -2
  38. package/src/init-templates.test.ts +22 -4
  39. package/src/init-templates.ts +16 -16
  40. package/src/lint/post-synth/cede010.ts +5 -4
  41. package/src/plugin.ts +33 -8
  42. package/src/schema-artifact.test.ts +160 -0
  43. package/src/schema-artifact.ts +87 -0
  44. package/src/serializer.ts +17 -1
  45. package/src/skills/chant-cedar-authoring.md +9 -3
  46. package/src/spec/fetch.ts +23 -2
  47. package/dist/codegen/docs-dogwood.d.ts +0 -21
  48. package/dist/codegen/docs-dogwood.d.ts.map +0 -1
  49. package/src/codegen/docs-dogwood.ts +0 -1119
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * The policy files use `entityType`-string scopes (`{ is: "App::Document" }`)
19
19
  * and generated action constants, so a scaffolded project compiles the moment
20
- * `chant generate` has run against its own schema.
20
+ * `chant cedar generate` has run against its own schema (#1696).
21
21
  */
22
22
 
23
23
  import type { InitTemplateSet } from "@intentius/chant/lexicon";
@@ -30,12 +30,16 @@ function readme(title: string, body: string): string {
30
30
 
31
31
  \`\`\`bash
32
32
  npm install
33
- npx chant generate --lexicon cedar # schema.cedarschema -> typed classes
34
- npx chant build # emits .cedar text + policies.cedar.json
33
+ npx chant cedar generate # schema.cedarschema -> src/generated/cedar/
34
+ npx chant build # emits .cedar text, policies.cedar.json, and the schema
35
35
  \`\`\`
36
36
 
37
- Generate first. The classes and action constants \`src/policies.ts\` imports do
38
- not exist until \`chant generate\` has read \`${SCHEMA_FILE}\`.
37
+ Generate first. The classes and action constants \`src/policies.ts\` imports
38
+ from \`./generated/cedar\` do not exist until \`chant cedar generate\` has read
39
+ \`${SCHEMA_FILE}\`. The output is yours: it lives in this project, not in
40
+ \`node_modules\`, so commit it (or regenerate it in CI before \`chant build\`)
41
+ and re-run generate whenever the schema changes. \`cedar.outDir\` in
42
+ \`chant.config.ts\` moves it.
39
43
 
40
44
  ${body}
41
45
 
@@ -63,6 +67,7 @@ this project never declared.
63
67
  |------|--------------|
64
68
  | \`dist/*.cedar\` | Every Cedar evaluator — AVP, cedar-agent, an embedded cedar-wasm |
65
69
  | \`dist/policies.cedar.json\` | The Cedar JSON policy format; also the parse source for import |
70
+ | \`dist/${SCHEMA_FILE}\` | Your schema, beside the policies, so the build validates against it and a bundle can carry it |
66
71
  `;
67
72
  }
68
73
 
@@ -70,7 +75,7 @@ this project never declared.
70
75
 
71
76
  const DEFAULT_SCHEMA = `// The entity model your policies are typed against.
72
77
  //
73
- // \`chant generate --lexicon cedar\` turns every declaration below into a
78
+ // \`chant cedar generate\` turns every declaration below into a
74
79
  // TypeScript class or constant, so a renamed entity type becomes a
75
80
  // compiler-guided refactor rather than a runtime validation failure.
76
81
 
@@ -96,7 +101,7 @@ namespace App {
96
101
  }
97
102
  `;
98
103
 
99
- const DEFAULT_POLICIES = `import { Policy, ReadAction, WriteAction } from "@intentius/chant-lexicon-cedar";
104
+ const DEFAULT_POLICIES = `import { Policy, ReadAction, WriteAction } from "./generated/cedar";
100
105
 
101
106
  /**
102
107
  * A permit and a forbid — the smallest policy set worth deploying.
@@ -160,7 +165,7 @@ namespace Store {
160
165
  }
161
166
  `;
162
167
 
163
- const AVP_POLICIES = `import { Policy, EditAction, ViewAction } from "@intentius/chant-lexicon-cedar";
168
+ const AVP_POLICIES = `import { Policy, EditAction, ViewAction } from "./generated/cedar";
164
169
 
165
170
  /**
166
171
  * Policies destined for an AVP policy store.
@@ -239,13 +244,8 @@ namespace Gateway {
239
244
  }
240
245
  `;
241
246
 
242
- const GATEWAY_POLICIES = `import {
243
- DeleteAction,
244
- DenyByDefaultSet,
245
- GetAction,
246
- Policy,
247
- PostAction,
248
- } from "@intentius/chant-lexicon-cedar";
247
+ const GATEWAY_POLICIES = `import { DenyByDefaultSet } from "@intentius/chant-lexicon-cedar";
248
+ import { DeleteAction, GetAction, Policy, PostAction } from "./generated/cedar";
249
249
 
250
250
  /**
251
251
  * A gateway policy set with an explicit deny floor.
@@ -298,7 +298,7 @@ export const authenticatedWriteGrant = guarded.members[1];
298
298
  */
299
299
  export function cedarInitTemplates(template?: string): InitTemplateSet {
300
300
  const scripts = {
301
- generate: "chant generate --lexicon cedar",
301
+ generate: "chant cedar generate",
302
302
  build: "chant build",
303
303
  };
304
304
 
@@ -24,10 +24,11 @@
24
24
  * When the build emits no schema there is nothing to validate against, and
25
25
  * `validate()` cannot be called at all — it has no optional-schema mode, and an
26
26
  * empty schema is legal Cedar that reports every policy as inapplicable, which
27
- * would be worse than useless. Schema-driven codegen is #1650; until a schema
28
- * is emitted beside the policies, this check says so once, as an advisory,
29
- * rather than passing silently and claiming a guarantee it did not check.
30
- * CEDC010's parse gate still applies in that state.
27
+ * would be worse than useless. A project schema is emitted beside the policies
28
+ * by the plugin's `buildRoots` hook (#1697, `src/schema-artifact.ts`), so the
29
+ * no-schema state is now the project with no schema of its own; there this
30
+ * check says so once, as an advisory, rather than passing silently and
31
+ * claiming a guarantee it did not check. CEDC010's parse gate still applies.
31
32
  */
32
33
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
33
34
  import { parsedPolicySets } from "./cedar-helpers";
package/src/plugin.ts CHANGED
@@ -20,6 +20,7 @@ import { CedarTemplateGenerator, CedarTemplateParser } from "./import/adapter";
20
20
  import type { ResourceMetadata } from "@intentius/chant/lexicon";
21
21
  import { AVP_OWNERSHIP_KEYS } from "./avp/ownership";
22
22
  import { AVP_AMBIENT_KINDS } from "./avp/ambient";
23
+ import { cedarCommandGroup } from "./commands";
23
24
 
24
25
  /**
25
26
  * cedar lexicon plugin.
@@ -35,11 +36,17 @@ export const cedarPlugin: LexiconPlugin = {
35
36
  async generate(options?: { verbose?: boolean }): Promise<void> {
36
37
  // Cedar's codegen input is the project's own schema, so the `cedar` config
37
38
  // namespace is read here rather than being decoration (#1650).
38
- const { generate, writeGeneratedFiles } = await import("./codegen/generate");
39
- const { loadCedarConfig } = await import("./config");
40
- const projectRoot = process.cwd();
41
- const result = await generate({ ...options, projectRoot, config: await loadCedarConfig(projectRoot) });
42
- writeGeneratedFiles(result);
39
+ //
40
+ // The output goes into the *project* (#1696): `cedar.outDir`, else
41
+ // `src/generated/cedar` under the config's directory. Only when the project
42
+ // is this package itself does it land in `src/generated/`.
43
+ const { generate, resolveGeneratedDir, writeGeneratedFiles } = await import("./codegen/generate");
44
+ const { loadCedarProject } = await import("./config");
45
+ const { projectRoot, config } = await loadCedarProject(process.cwd());
46
+ const result = await generate({ ...options, projectRoot, config });
47
+ const outDir = resolveGeneratedDir({ projectRoot, config });
48
+ writeGeneratedFiles(result, outDir);
49
+ if (options?.verbose) console.error(`cedar: wrote generated classes to ${outDir}`);
43
50
  },
44
51
 
45
52
  async validate(options?: { verbose?: boolean }): Promise<void> {
@@ -51,11 +58,15 @@ export const cedarPlugin: LexiconPlugin = {
51
58
 
52
59
  async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
53
60
  const { analyzeCedarCoverage } = await import("./coverage");
54
- const { loadCedarConfig } = await import("./config");
55
- const projectRoot = process.cwd();
61
+ const { resolveGeneratedDir } = await import("./codegen/generate");
62
+ const { loadCedarProject } = await import("./config");
63
+ const { projectRoot, config } = await loadCedarProject(process.cwd());
56
64
  analyzeCedarCoverage({
57
65
  projectRoot,
58
- config: await loadCedarConfig(projectRoot),
66
+ config,
67
+ // Coverage compares the project's schema with the project's generated
68
+ // artifacts, which live where generate put them (#1696).
69
+ generatedDir: resolveGeneratedDir({ projectRoot, config }),
59
70
  verbose: options?.verbose,
60
71
  minOverall: options?.minOverall,
61
72
  });
@@ -76,6 +87,20 @@ export const cedarPlugin: LexiconPlugin = {
76
87
 
77
88
  // ── Optional extensions ────────────────────────────────────
78
89
 
90
+ // #1697 — the project's `.cedarschema` joins the build as a `Cedar::Schema`
91
+ // entity, so it is emitted beside the policies and CEDE010 validates
92
+ // against it rather than reporting that it could not. Nothing is
93
+ // contributed when only the bundled default schema applies.
94
+ async buildRoots(ctx) {
95
+ const { schemaBuildRoot } = await import("./schema-artifact");
96
+ return schemaBuildRoot(ctx);
97
+ },
98
+
99
+ // `chant cedar generate` and `chant cedar coverage` (#1696, via #1078).
100
+ commands() {
101
+ return cedarCommandGroup();
102
+ },
103
+
79
104
  lintRules() {
80
105
  return rules;
81
106
  },
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The project schema as a build artifact (#1697).
3
+ *
4
+ * End to end: a project schema goes in through the plugin's `buildRoots`
5
+ * hook, comes out of the serializer as `schema.cedarschema` beside the
6
+ * policies, and CEDE010 validates against it instead of saying it could not.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+ import type { Declarable } from "@intentius/chant/declarable";
14
+ import type { SerializerResult } from "@intentius/chant/serializer";
15
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
16
+ import { cedarPlugin } from "./plugin";
17
+ import { cedarSerializer, type CedarPolicyProps } from "./serializer";
18
+ import { Policy, type PolicyProps } from "./generated/index";
19
+ import { CEDAR_SCHEMA_ENTITY_NAME, CEDAR_SCHEMA_FILENAME, Schema, schemaBuildRoot } from "./schema-artifact";
20
+ import { cede010 } from "./lint/post-synth/cede010";
21
+
22
+ /**
23
+ * The package's `PolicyProps` narrows scopes to the bundled sample schema's
24
+ * names. These tests use the `Shop` schema above, so they build props as the
25
+ * string-typed `CedarPolicyProps` and widen at the constructor.
26
+ */
27
+ function shopPolicy(props: CedarPolicyProps): InstanceType<typeof Policy> {
28
+ return new Policy(props as unknown as PolicyProps);
29
+ }
30
+
31
+ const SCHEMA = `namespace Shop {
32
+ entity Customer = { "email": String };
33
+ entity Order = { "owner": Customer };
34
+ action view appliesTo {
35
+ principal: [Customer],
36
+ resource: [Order],
37
+ context: { "mfa": Bool }
38
+ };
39
+ }
40
+ `;
41
+
42
+ function serialize(entities: Map<string, Declarable>): SerializerResult {
43
+ const out = cedarSerializer.serialize(entities);
44
+ if (typeof out === "string") return { primary: out };
45
+ return out;
46
+ }
47
+
48
+ function postSynthCtx(result: SerializerResult): PostSynthContext {
49
+ return {
50
+ outputs: new Map([["cedar", result]]),
51
+ entities: new Map(),
52
+ } as unknown as PostSynthContext;
53
+ }
54
+
55
+ describe("Schema declarable", () => {
56
+ it("is emitted as a .cedarschema beside the policies", () => {
57
+ const entities = new Map<string, Declarable>([
58
+ ["authz", new Schema({ text: SCHEMA })],
59
+ [
60
+ "viewOwn",
61
+ shopPolicy({
62
+ effect: "permit",
63
+ principal: { is: "Shop::Customer" },
64
+ action: { in: ['Shop::Action::"view"'] },
65
+ resource: { is: "Shop::Order" },
66
+ when: ["resource.owner == principal"],
67
+ }),
68
+ ],
69
+ ]);
70
+
71
+ const result = serialize(entities);
72
+ expect(result.files?.[CEDAR_SCHEMA_FILENAME]).toBe(SCHEMA);
73
+ expect(result.primary).toContain("permit");
74
+ });
75
+
76
+ it("honours an explicit filename and is emitted even with no policies", () => {
77
+ const entities = new Map<string, Declarable>([["authz", new Schema({ text: SCHEMA, filename: "shop.cedarschema" })]]);
78
+ const result = serialize(entities);
79
+ expect(Object.keys(result.files ?? {})).toEqual(["shop.cedarschema"]);
80
+ });
81
+
82
+ it("warns rather than silently overwriting when two schemas share a filename", () => {
83
+ const entities = new Map<string, Declarable>([
84
+ ["a", new Schema({ text: "namespace A { entity X; }\n" })],
85
+ ["b", new Schema({ text: "namespace B { entity Y; }\n" })],
86
+ ]);
87
+ const result = serialize(entities);
88
+ expect(result.files?.[CEDAR_SCHEMA_FILENAME]).toBe("namespace A { entity X; }\n");
89
+ expect(result.warnings?.join("\n")).toMatch(/"b" targets schema\.cedarschema/);
90
+ });
91
+
92
+ it("turns CEDE010 from an advisory into validation", () => {
93
+ const badPolicy = shopPolicy({
94
+ effect: "permit",
95
+ principal: { is: "Shop::Customer" },
96
+ action: { in: ['Shop::Action::"view"'] },
97
+ resource: { is: "Shop::Nope" },
98
+ });
99
+
100
+ const without = cede010.check(postSynthCtx(serialize(new Map([["p", badPolicy]]))));
101
+ expect(without.map((d) => d.severity)).toEqual(["info"]);
102
+
103
+ const withSchema = cede010.check(
104
+ postSynthCtx(
105
+ serialize(
106
+ new Map<string, Declarable>([
107
+ ["p", badPolicy],
108
+ ["authz", new Schema({ text: SCHEMA })],
109
+ ]),
110
+ ),
111
+ ),
112
+ );
113
+ expect(withSchema.some((d) => d.severity === "error" && /Shop::Nope/.test(d.message))).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe("schemaBuildRoot", () => {
118
+ let root: string;
119
+
120
+ beforeEach(() => {
121
+ root = mkdtempSync(join(tmpdir(), "cedar-schema-root-"));
122
+ });
123
+
124
+ afterEach(() => {
125
+ rmSync(root, { recursive: true, force: true });
126
+ });
127
+
128
+ it("contributes the project schema when the project has one", async () => {
129
+ writeFileSync(join(root, "authz.cedarschema"), SCHEMA);
130
+ const contribution = await schemaBuildRoot({
131
+ projectRoot: root,
132
+ config: { cedar: { schema: "authz.cedarschema" } },
133
+ });
134
+
135
+ const entity = contribution.entities.get(CEDAR_SCHEMA_ENTITY_NAME);
136
+ expect(entity).toBeDefined();
137
+
138
+ const result = serialize(contribution.entities);
139
+ expect(result.files?.["authz.cedarschema"]).toBe(SCHEMA);
140
+ });
141
+
142
+ it("finds schema.cedarschema in the project root with no config at all", async () => {
143
+ writeFileSync(join(root, CEDAR_SCHEMA_FILENAME), SCHEMA);
144
+ const contribution = await schemaBuildRoot({ projectRoot: root, config: {} });
145
+ expect(contribution.entities.size).toBe(1);
146
+ });
147
+
148
+ it("contributes nothing when only the bundled default schema applies", async () => {
149
+ // Emitting the default would validate a project's policies against an
150
+ // entity model it never wrote; the CEDE010 advisory is the honest outcome.
151
+ const contribution = await schemaBuildRoot({ projectRoot: root, config: {} });
152
+ expect(contribution.entities.size).toBe(0);
153
+ });
154
+
155
+ it("is wired into the plugin", async () => {
156
+ writeFileSync(join(root, CEDAR_SCHEMA_FILENAME), SCHEMA);
157
+ const contribution = await cedarPlugin.buildRoots!({ projectRoot: root, config: {} });
158
+ expect(contribution.entities.has(CEDAR_SCHEMA_ENTITY_NAME)).toBe(true);
159
+ });
160
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The project schema as a build artifact (#1697).
3
+ *
4
+ * CEDE010 can only validate a policy set against a schema the build emitted
5
+ * beside it, and until now nothing emitted one: the `.cedarschema` drove
6
+ * codegen and then stayed in the source tree, so every consumer build got the
7
+ * parse-only advisory. This module closes that gap from two directions.
8
+ *
9
+ * `Schema` is a declarable a project can author by hand when it wants a
10
+ * particular filename or a schema that is not the one in `chant.config.ts`.
11
+ * `schemaBuildRoot` is the automatic path: the plugin's `buildRoots` hook
12
+ * contributes one `Schema` entity per build, holding the resolved project
13
+ * schema, so a project with a `cedar.schema` gets `dist/schema.cedarschema`
14
+ * and full validation without declaring anything.
15
+ *
16
+ * The bundled default schema is never emitted. A project without its own
17
+ * schema has policies the default may well reject, and CEDE010 failing a
18
+ * build against a schema the project never wrote would be a worse surprise
19
+ * than the advisory it replaces.
20
+ */
21
+
22
+ import type { Declarable } from "@intentius/chant/declarable";
23
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
24
+ import type { BuildRootContext, BuildRootContribution } from "@intentius/chant/lexicon";
25
+ import { createResource } from "@intentius/chant/runtime";
26
+ import { getProps } from "./policy-text";
27
+ import { resolveSchemaPath } from "./spec/fetch";
28
+ import type { CedarGenerateConfig } from "./codegen/generate";
29
+ import { readFileSync } from "fs";
30
+ import { basename } from "path";
31
+
32
+ /** The entity type of a {@link Schema}. */
33
+ export const CEDAR_SCHEMA_TYPE = "Cedar::Schema";
34
+
35
+ /** The logical name the build-root contribution uses. */
36
+ export const CEDAR_SCHEMA_ENTITY_NAME = "cedarSchema";
37
+
38
+ /** Filename the schema is emitted under when `filename` is unset. */
39
+ export const CEDAR_SCHEMA_FILENAME = "schema.cedarschema";
40
+
41
+ export interface SchemaProps {
42
+ /** Human-readable Cedar schema text, emitted verbatim. */
43
+ text: string;
44
+ /** Defaults to {@link CEDAR_SCHEMA_FILENAME}. Must end in `.cedarschema`. */
45
+ filename?: string;
46
+ }
47
+
48
+ /** A `.cedarschema` emitted with the policy set, so post-synth validation has something to validate against. */
49
+ export const Schema = createResource(CEDAR_SCHEMA_TYPE, "cedar", {}) as unknown as new (props: SchemaProps) => Declarable;
50
+
51
+ /** One schema file the serializer should write. */
52
+ export interface SchemaFile {
53
+ name: string;
54
+ filename: string;
55
+ text: string;
56
+ }
57
+
58
+ /** Every `Cedar::Schema` in the entity map, with its props read. */
59
+ export function schemaEntities(entities: Map<string, Declarable>): SchemaFile[] {
60
+ const files: SchemaFile[] = [];
61
+ for (const [name, entity] of entities) {
62
+ if (isPropertyDeclarable(entity)) continue;
63
+ if (entity.entityType !== CEDAR_SCHEMA_TYPE) continue;
64
+ const props = getProps(entity) as Partial<SchemaProps>;
65
+ if (typeof props.text !== "string") continue;
66
+ files.push({
67
+ name,
68
+ filename: typeof props.filename === "string" ? props.filename : CEDAR_SCHEMA_FILENAME,
69
+ text: props.text.endsWith("\n") ? props.text : props.text + "\n",
70
+ });
71
+ }
72
+ return files;
73
+ }
74
+
75
+ /**
76
+ * The `buildRoots` contribution: the project's resolved schema as one
77
+ * {@link Schema} entity, or nothing when only the bundled default applies.
78
+ */
79
+ export async function schemaBuildRoot(ctx: BuildRootContext): Promise<BuildRootContribution> {
80
+ const config = (ctx.config as { cedar?: CedarGenerateConfig }).cedar ?? {};
81
+ const source = resolveSchemaPath({ projectRoot: ctx.projectRoot, config });
82
+ if (source.isDefault) return { entities: new Map() };
83
+
84
+ const text = readFileSync(source.path, "utf-8");
85
+ const entity = new Schema({ text, filename: basename(source.path) });
86
+ return { entities: new Map([[CEDAR_SCHEMA_ENTITY_NAME, entity]]) };
87
+ }
package/src/serializer.ts CHANGED
@@ -34,6 +34,7 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
34
34
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
35
35
  import { policyToJson, splitPolicySet, templateToJson, type PolicyJson } from "./spec/wasm";
36
36
  import { serializeDogwood } from "./dogwood/serialize";
37
+ import { schemaEntities } from "./schema-artifact";
37
38
  import {
38
39
  conditionStrings,
39
40
  getProps,
@@ -263,10 +264,25 @@ export const cedarSerializer: Serializer = {
263
264
  // and a project holding only temporal policies still gets its files.
264
265
  const dogwood = serializeDogwood(entities);
265
266
 
266
- if (policyText.length === 0 && !dogwood) return "";
267
+ // The project schema, as a file beside the policies (#1697). This is what
268
+ // turns CEDE010's parse-only advisory into validation: `findSchema` in
269
+ // lint/post-synth/wasm-helpers.ts reads it straight back out of `files`.
270
+ const schemas = schemaEntities(entities);
271
+
272
+ if (policyText.length === 0 && !dogwood && schemas.length === 0) return "";
267
273
 
268
274
  const files: Record<string, string> = {};
269
275
  const warnings: string[] = [...(dogwood?.warnings ?? [])];
276
+
277
+ for (const schema of schemas) {
278
+ if (files[schema.filename] !== undefined) {
279
+ warnings.push(
280
+ `cedar: schema "${schema.name}" targets ${schema.filename}, which another schema already wrote; give one of them an explicit \`filename\``,
281
+ );
282
+ continue;
283
+ }
284
+ files[schema.filename] = schema.text;
285
+ }
270
286
  let primary = "";
271
287
 
272
288
  if (policyText.length > 0) {
@@ -51,9 +51,15 @@ between "your entity types" and "somebody else's" is invisible.
51
51
  ### 2. Run generate
52
52
 
53
53
  ```bash
54
- npx chant generate --lexicon cedar
54
+ npx chant cedar generate
55
55
  ```
56
56
 
57
+ The output is written into the project, at `src/generated/cedar/` (or
58
+ `cedar.outDir`), never into `node_modules`. Commit it or regenerate it in CI;
59
+ re-run it whenever the schema changes. Policies import from that directory,
60
+ not from `@intentius/chant-lexicon-cedar`, whose own `Policy` and classes
61
+ describe the package's bundled sample schema.
62
+
57
63
  That produces, per declaration in the schema:
58
64
 
59
65
  | Schema declaration | Generated |
@@ -68,7 +74,7 @@ compile error, not a validation error hours later.
68
74
  ### 3. Write policies
69
75
 
70
76
  ```typescript
71
- import { Policy, ReadAction, WriteAction, type UserUid } from "@intentius/chant-lexicon-cedar";
77
+ import { Policy, ReadAction, WriteAction, type UserUid } from "./generated/cedar";
72
78
 
73
79
  const archivist: UserUid = 'App::User::"archivist"';
74
80
 
@@ -159,7 +165,7 @@ wider grant.
159
165
 
160
166
  ```bash
161
167
  npx chant build # emits both artifacts, runs the CED rules
162
- npx chant coverage --lexicon cedar # is every schema declaration generated?
168
+ npx chant cedar coverage # is every schema declaration generated?
163
169
  ```
164
170
 
165
171
  The MCP tool `cedar:coverage` answers the other question: which schema entity
package/src/spec/fetch.ts CHANGED
@@ -32,11 +32,32 @@ export const DEFAULT_SCHEMA_FILENAME = "default-schema.cedarschema";
32
32
  /** Map key used for the single schema entry, so `parseSchema` can tell them apart. */
33
33
  export const DEFAULT_SCHEMA_KEY = "<bundled-default>";
34
34
 
35
- const specDir = dirname(fileURLToPath(import.meta.url));
35
+ let specDir: string | undefined;
36
+
37
+ /**
38
+ * Directory this module lives in, resolved on first use.
39
+ *
40
+ * Kept lazy so importing the module never touches `import.meta.url`; edge
41
+ * bundles that pull this file in through the lint barrel have no such URL and
42
+ * would crash at module init otherwise.
43
+ */
44
+ function resolveSpecDir(): string | undefined {
45
+ if (specDir !== undefined) return specDir;
46
+ try {
47
+ specDir = dirname(fileURLToPath(import.meta.url));
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ return specDir;
52
+ }
36
53
 
37
54
  /** Absolute path to the schema bundled with this package. */
38
55
  export function defaultSchemaPath(): string {
39
- return join(specDir, DEFAULT_SCHEMA_FILENAME);
56
+ const dir = resolveSpecDir();
57
+ if (dir === undefined) {
58
+ throw new Error("cedar: the bundled default schema is not reachable from this runtime (no module URL).");
59
+ }
60
+ return join(dir, DEFAULT_SCHEMA_FILENAME);
40
61
  }
41
62
 
42
63
  export interface SchemaSource {