@intentius/chant 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 (170) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  20. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  21. package/dist/cli/commands/onboard.d.ts +28 -0
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/handlers/build.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  26. package/dist/cli/handlers/run.d.ts.map +1 -1
  27. package/dist/cli/handlers/search.d.ts +27 -0
  28. package/dist/cli/handlers/search.d.ts.map +1 -1
  29. package/dist/cli/main.d.ts.map +1 -1
  30. package/dist/codegen/docs-pages.d.ts +39 -0
  31. package/dist/codegen/docs-pages.d.ts.map +1 -0
  32. package/dist/codegen/docs-sections.d.ts.map +1 -1
  33. package/dist/codegen/docs-sidebar.d.ts +13 -2
  34. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  35. package/dist/codegen/docs-types.d.ts +30 -10
  36. package/dist/codegen/docs-types.d.ts.map +1 -1
  37. package/dist/codegen/docs.d.ts +14 -2
  38. package/dist/codegen/docs.d.ts.map +1 -1
  39. package/dist/codegen/surface-snapshot.d.ts +27 -3
  40. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  41. package/dist/codegen/validate.d.ts +11 -3
  42. package/dist/codegen/validate.d.ts.map +1 -1
  43. package/dist/components/capability-plugin.d.ts +13 -2
  44. package/dist/components/capability-plugin.d.ts.map +1 -1
  45. package/dist/components/driver.d.ts +38 -0
  46. package/dist/components/driver.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +1 -1
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/starter-plugin.d.ts.map +1 -1
  50. package/dist/config.d.ts +57 -5
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/discovery/fold-import.d.ts.map +1 -1
  53. package/dist/fold/fold.d.ts +100 -1
  54. package/dist/fold/fold.d.ts.map +1 -1
  55. package/dist/graph-ir.d.ts +12 -0
  56. package/dist/graph-ir.d.ts.map +1 -1
  57. package/dist/graph-ops.d.ts +17 -0
  58. package/dist/graph-ops.d.ts.map +1 -0
  59. package/dist/lifecycle/change-set.d.ts +23 -2
  60. package/dist/lifecycle/change-set.d.ts.map +1 -1
  61. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  62. package/dist/lifecycle/observe.d.ts +8 -0
  63. package/dist/lifecycle/observe.d.ts.map +1 -1
  64. package/dist/lifecycle/replay.d.ts.map +1 -1
  65. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  66. package/dist/lifecycle/types.d.ts +7 -0
  67. package/dist/lifecycle/types.d.ts.map +1 -1
  68. package/dist/lint/policy.d.ts.map +1 -1
  69. package/dist/managed-fields.d.ts +33 -21
  70. package/dist/managed-fields.d.ts.map +1 -1
  71. package/dist/observation.d.ts +23 -1
  72. package/dist/observation.d.ts.map +1 -1
  73. package/dist/yaml.d.ts +0 -8
  74. package/dist/yaml.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/__snapshots__/okf.test.ts.snap +20 -0
  77. package/src/audit/discover.test.ts +55 -1
  78. package/src/audit/discover.ts +70 -2
  79. package/src/audit/edge-init-safety.test.ts +149 -18
  80. package/src/audit/report-model.ts +12 -1
  81. package/src/audit/rules-doc.ts +1 -0
  82. package/src/build-params.test.ts +52 -0
  83. package/src/cli/build-params-cli.test.ts +25 -8
  84. package/src/cli/build-params-cli.ts +37 -23
  85. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  86. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  87. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  88. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  89. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  90. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  91. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  92. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  93. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  94. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  95. package/src/cli/commands/audit.test.ts +83 -1
  96. package/src/cli/commands/audit.ts +121 -28
  97. package/src/cli/commands/build.test.ts +144 -5
  98. package/src/cli/commands/build.ts +66 -14
  99. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  100. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  101. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  102. package/src/cli/commands/check-lexicon.test.ts +1 -1
  103. package/src/cli/commands/check-lexicon.ts +19 -4
  104. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  105. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  106. package/src/cli/commands/init-lexicon.test.ts +42 -1
  107. package/src/cli/commands/init-lexicon.ts +3 -1
  108. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  109. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  110. package/src/cli/commands/lexicon-surface-diff.ts +1 -1
  111. package/src/cli/commands/onboard.test.ts +319 -225
  112. package/src/cli/commands/onboard.ts +144 -58
  113. package/src/cli/handlers/build.test.ts +3 -3
  114. package/src/cli/handlers/build.ts +2 -0
  115. package/src/cli/handlers/components.ts +2 -2
  116. package/src/cli/handlers/graph.test.ts +44 -0
  117. package/src/cli/handlers/graph.ts +22 -2
  118. package/src/cli/handlers/lifecycle.test.ts +117 -0
  119. package/src/cli/handlers/lifecycle.ts +16 -0
  120. package/src/cli/handlers/run.test.ts +5 -5
  121. package/src/cli/handlers/run.ts +1 -0
  122. package/src/cli/handlers/search-live.test.ts +217 -0
  123. package/src/cli/handlers/search.test.ts +41 -1
  124. package/src/cli/handlers/search.ts +55 -6
  125. package/src/cli/main.ts +5 -3
  126. package/src/cli/security-boundary.test.ts +2 -1
  127. package/src/codegen/docs-pages.test.ts +194 -0
  128. package/src/codegen/docs-pages.ts +138 -0
  129. package/src/codegen/docs-sections.ts +0 -8
  130. package/src/codegen/docs-sidebar.ts +50 -37
  131. package/src/codegen/docs-types.ts +32 -4
  132. package/src/codegen/docs.ts +78 -40
  133. package/src/codegen/lexicon-regen.ts +3 -1
  134. package/src/codegen/rolling-upgrade.test.ts +1 -1
  135. package/src/codegen/rolling-upgrade.ts +1 -1
  136. package/src/codegen/surface-snapshot.test.ts +139 -0
  137. package/src/codegen/surface-snapshot.ts +297 -122
  138. package/src/codegen/validate.test.ts +16 -3
  139. package/src/codegen/validate.ts +17 -7
  140. package/src/components/auto-release.ts +1 -1
  141. package/src/components/capability-plugin.ts +23 -2
  142. package/src/components/driver.test.ts +45 -0
  143. package/src/components/driver.ts +64 -27
  144. package/src/components/index.ts +2 -0
  145. package/src/components/starter-plugin.ts +5 -2
  146. package/src/config.ts +101 -7
  147. package/src/discovery/fold-import.test.ts +427 -2
  148. package/src/discovery/fold-import.ts +163 -7
  149. package/src/discovery/sandbox/driver.test.ts +11 -0
  150. package/src/fold/fold.test.ts +41 -0
  151. package/src/fold/fold.ts +372 -2
  152. package/src/graph-ir-live.test.ts +42 -0
  153. package/src/graph-ir.ts +42 -2
  154. package/src/graph-ops.test.ts +69 -0
  155. package/src/graph-ops.ts +42 -0
  156. package/src/lifecycle/change-set.test.ts +41 -0
  157. package/src/lifecycle/change-set.ts +30 -2
  158. package/src/lifecycle/deep-observe.ts +4 -0
  159. package/src/lifecycle/observe.ts +29 -2
  160. package/src/lifecycle/replay.ts +3 -0
  161. package/src/lifecycle/snapshot.ts +3 -0
  162. package/src/lifecycle/types.ts +7 -0
  163. package/src/lint/policy.ts +14 -3
  164. package/src/managed-fields.test.ts +15 -42
  165. package/src/managed-fields.ts +43 -26
  166. package/src/observation.test.ts +24 -8
  167. package/src/observation.ts +48 -4
  168. package/src/ownership.test.ts +63 -1
  169. package/src/yaml.test.ts +15 -0
  170. package/src/yaml.ts +19 -2
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { checkExamplesBuild } from "./check-lexicon-examples";
@@ -15,7 +15,10 @@ import { checkExamplesBuild } from "./check-lexicon-examples";
15
15
  * exactly what each example's own `npm run build` does.
16
16
  *
17
17
  * These fixtures import the real, already-installed `@intentius/chant-lexicon-aws`
18
- * package (workspace-linked), so no lexicon plugin needs to be faked.
18
+ * package (workspace-linked), so no lexicon plugin needs to be faked. The
19
+ * temp lexicon dir gets the repo's node_modules linked in so that bare
20
+ * specifier resolves from the fixture the way it does from a real lexicon
21
+ * (Vitest 4's module runner resolves from the importing file, not the root).
19
22
  */
20
23
 
21
24
  function writeLexiconDirWithExample(
@@ -35,18 +38,43 @@ describe("checkExamplesBuild", () => {
35
38
 
36
39
  beforeAll(() => {
37
40
  dir = mkdtempSync(join(tmpdir(), "chant-check-lexicon-examples-"));
41
+ symlinkSync(join(__dirname, "..", "..", "..", "..", "..", "node_modules"), join(dir, "node_modules"), "dir");
38
42
  });
39
43
  afterAll(() => rmSync(dir, { recursive: true, force: true }));
40
44
 
41
- test("a clean example builds", async () => {
42
- writeLexiconDirWithExample(dir, "clean", {
43
- "bucket.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
45
+ // A bucket that satisfies the aws lexicon's error-severity checks (#1400):
46
+ // public access blocked (WAW018) and a TLS-only bucket policy (WAW042).
47
+ const cleanBucketSource = `import { Bucket, PublicAccessBlockConfiguration, S3BucketPolicy, Ref } from "@intentius/chant-lexicon-aws";
44
48
 
45
49
  export const appBucket = new Bucket({
46
50
  BucketName: "my-app-bucket",
51
+ PublicAccessBlockConfiguration: new PublicAccessBlockConfiguration({
52
+ BlockPublicAcls: true,
53
+ BlockPublicPolicy: true,
54
+ IgnorePublicAcls: true,
55
+ RestrictPublicBuckets: true,
56
+ }),
47
57
  });
48
- `,
49
- });
58
+
59
+ export const appBucketPolicy = new S3BucketPolicy({
60
+ Bucket: Ref(appBucket),
61
+ PolicyDocument: {
62
+ Version: "2012-10-17",
63
+ Statement: [
64
+ {
65
+ Effect: "Deny",
66
+ Principal: "*",
67
+ Action: "s3:*",
68
+ Resource: [appBucket.Arn],
69
+ Condition: { Bool: { "aws:SecureTransport": "false" } },
70
+ },
71
+ ],
72
+ },
73
+ });
74
+ `;
75
+
76
+ test("a clean example builds", async () => {
77
+ writeLexiconDirWithExample(dir, "clean", { "bucket.ts": cleanBucketSource });
50
78
 
51
79
  const results = await checkExamplesBuild(dir);
52
80
  const clean = results.find((r) => r.example === "clean");
@@ -75,6 +103,44 @@ export const dataBucket = new Bucket({
75
103
  expect(broken?.detail).toMatch(/Duplicate export name "dataBucket" found/);
76
104
  });
77
105
 
106
+ // chant #1400 — a bare bucket serializes fine, which is all #1067 asked,
107
+ // but fails the aws lexicon's own WAW018/WAW042 at error severity. This is
108
+ // the exact shape lambda-api and lambda-s3 shipped in.
109
+ test("an example that fails its own lexicon's post-synth checks at error severity fails (#1400)", async () => {
110
+ writeLexiconDirWithExample(dir, "insecure", {
111
+ "bucket.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
112
+
113
+ export const plainBucket = new Bucket({
114
+ BucketName: "plain-bucket",
115
+ });
116
+ `,
117
+ });
118
+
119
+ const results = await checkExamplesBuild(dir);
120
+ const insecure = results.find((r) => r.example === "insecure");
121
+ expect(insecure?.ok).toBe(false);
122
+ expect(insecure?.detail).toMatch(/post-synth error\(s\) from the lexicon's own checks/);
123
+ expect(insecure?.detail).toMatch(/WAW042: \[plainBucket\]/);
124
+ });
125
+
126
+ test("an example's own lint.rules severity config applies to post-synth checks (#1400)", async () => {
127
+ writeLexiconDirWithExample(dir, "suppressed", {
128
+ "bucket.ts": `import { Bucket } from "@intentius/chant-lexicon-aws";
129
+
130
+ export const plainBucket = new Bucket({
131
+ BucketName: "plain-bucket",
132
+ });
133
+ `,
134
+ });
135
+ writeFileSync(
136
+ join(dir, "examples", "suppressed", "chant.config.json"),
137
+ JSON.stringify({ lint: { rules: { WAW018: "warning", WAW042: "off" } } }),
138
+ );
139
+
140
+ const results = await checkExamplesBuild(dir);
141
+ expect(results.find((r) => r.example === "suppressed")).toMatchObject({ ok: true });
142
+ });
143
+
78
144
  test("an empty src/ directory is skipped, not reported as a failure", async () => {
79
145
  writeLexiconDirWithExample(dir, "empty", {});
80
146
  const results = await checkExamplesBuild(dir);
@@ -1,5 +1,6 @@
1
1
  /**
2
- * "Every shipped example builds" (chant #1067).
2
+ * "Every shipped example builds" (chant #1067) and "every shipped example
3
+ * passes its own lexicon's post-synth checks" (chant #1400).
3
4
  *
4
5
  * `check-lexicon.ts`'s existing example checks only count directories
5
6
  * ("At least 1 example", "At least 3 examples", "At least 5 examples with
@@ -12,28 +13,29 @@
12
13
  * pipeline `chant build` runs and report whether it produced output with no
13
14
  * structural error.
14
15
  *
15
- * Scope, deliberately: "builds" here means discovery + serialization
16
- * succeed (no `DiscoveryError`/`BuildError`, real output produced) — the
17
- * same thing a `Duplicate export name` failure blocks. It does not
18
- * additionally require the output to pass every post-synth/lint check
19
- * (WAW0xx and friends). Those are a separate, already-gated contract
20
- * (`chant lint`, the post-synth pipeline) with their own severity model and
21
- * their own CI step; several existing AWS examples (docs-snippets,
22
- * lambda-api, lambda-s3, shared-alb) currently fail one or more post-synth
23
- * checks for reasons unrelated to this issue (e.g. WAW042, a TLS-only
24
- * bucket policy check added after those examples were written). Folding
25
- * that axis into "does it build" would fail this new check for all of them
26
- * on its very first run, for defects this issue never set out to fix.
27
- * Tracked separately; not silently absorbed here.
16
+ * #1067 scoped "builds" to discovery + serialization on purpose, and said
17
+ * so here: three aws examples (lambda-api, lambda-s3, shared-alb) failed
18
+ * WAW042/WAW054 at error severity at the time, for defects that issue never
19
+ * set out to fix. #1400 fixed those three and added the second axis: after
20
+ * serialization, the post-synth checks each loaded plugin ships run against
21
+ * that plugin's own output, exactly as `chant build` runs them (scoped per
22
+ * plugin, `lint.rules` from the example's chant.config applied), and any
23
+ * diagnostic left at `error` severity fails the example. Warnings do not.
24
+ * An example is what a user copies wholesale; it must not teach a pattern
25
+ * the lexicon it demonstrates flags as an error.
28
26
  */
29
27
 
30
28
  import { existsSync, readdirSync } from "fs";
31
29
  import { join } from "path";
32
- import { build } from "../../build";
30
+ import { build, type BuildResult } from "../../build";
33
31
  import { findInfraFiles } from "../../discovery/files";
34
32
  import { detectLexicons } from "../../detectLexicon";
35
33
  import { loadChantConfig } from "../../config";
36
34
  import { loadPlugins } from "../plugins";
35
+ import { runPostSynthChecks, type PostSynthDiagnostic } from "../../lint/post-synth";
36
+ import { applyConfiguredSeverity } from "../../lint/config";
37
+ import type { LexiconPlugin } from "../../lexicon";
38
+ import type { SerializerResult } from "../../serializer";
37
39
 
38
40
  export interface ExampleBuildResult {
39
41
  example: string;
@@ -41,6 +43,43 @@ export interface ExampleBuildResult {
41
43
  detail?: string;
42
44
  }
43
45
 
46
+ /**
47
+ * Run each plugin's own post-synth checks against that plugin's output —
48
+ * the same per-plugin scoping and `lint.rules` severity resolution
49
+ * `cli/commands/build.ts` applies — and return what is left at `error`
50
+ * severity. Project `lint.policies` are not run here: those are the
51
+ * example author's organizational policy, not the lexicon's contract.
52
+ */
53
+ export function postSynthErrors(
54
+ plugins: LexiconPlugin[],
55
+ result: BuildResult,
56
+ lintRules: Parameters<typeof applyConfiguredSeverity>[1],
57
+ ): PostSynthDiagnostic[] {
58
+ const errors: PostSynthDiagnostic[] = [];
59
+ for (const plugin of plugins) {
60
+ if (!plugin.postSynthChecks) continue;
61
+ const checks = plugin.postSynthChecks();
62
+ if (checks.length === 0) continue;
63
+
64
+ const outputKey = plugin.serializer.name;
65
+ const scopedOutputs = new Map<string, string | SerializerResult>();
66
+ const pluginOutput = result.outputs.get(outputKey);
67
+ if (pluginOutput !== undefined) scopedOutputs.set(outputKey, pluginOutput);
68
+
69
+ const diags = runPostSynthChecks(checks, { ...result, outputs: scopedOutputs });
70
+ const { diagnostics } = applyConfiguredSeverity(diags, lintRules);
71
+ for (const diag of diagnostics) {
72
+ if (diag.severity === "error") errors.push(diag);
73
+ }
74
+ }
75
+ return errors;
76
+ }
77
+
78
+ function formatPostSynthError(diag: PostSynthDiagnostic): string {
79
+ const prefix = diag.entity ? `[${diag.entity}] ` : "";
80
+ return `${diag.checkId}: ${prefix}${diag.message}`;
81
+ }
82
+
44
83
  /**
45
84
  * Build every non-empty example under `<lexiconDir>/examples/*\/src` and
46
85
  * report per-example pass/fail. Returns `[]` when the lexicon has no
@@ -94,7 +133,15 @@ export async function checkExamplesBuild(lexiconDir: string): Promise<ExampleBui
94
133
 
95
134
  const structuralErrors = result.errors.map((e) => e.message);
96
135
  const producedOutput = result.outputs.size > 0;
97
- const ok = structuralErrors.length === 0 && producedOutput;
136
+ const structurallyOk = structuralErrors.length === 0 && producedOutput;
137
+
138
+ // #1400 — only once the build is structurally sound. A discovery error
139
+ // already explains the failure, and partial output is not the output
140
+ // the checks are meant to see.
141
+ const postSynth = structurallyOk
142
+ ? postSynthErrors(plugins, result, exampleConfig.lint?.rules)
143
+ : [];
144
+ const ok = structurallyOk && postSynth.length === 0;
98
145
 
99
146
  results.push({
100
147
  example: entry.name,
@@ -103,7 +150,9 @@ export async function checkExamplesBuild(lexiconDir: string): Promise<ExampleBui
103
150
  ? undefined
104
151
  : structuralErrors.length > 0
105
152
  ? structuralErrors.join("; ")
106
- : "discovered source but produced no output",
153
+ : !producedOutput
154
+ ? "discovered source but produced no output"
155
+ : `post-synth error(s) from the lexicon's own checks: ${postSynth.map(formatPostSynthError).join("; ")}`,
107
156
  });
108
157
  } catch (err) {
109
158
  results.push({
@@ -17,7 +17,7 @@ describe("checkLexicon", () => {
17
17
  test("includes the new #1067 tier-1 checks by name", async () => {
18
18
  const result = await checkLexicon(gitlabDir);
19
19
  const names = result.items.map((i) => i.name);
20
- expect(names).toContain("Every shipped example builds");
20
+ expect(names).toContain("Every shipped example builds and passes its own post-synth checks");
21
21
  expect(names).toContain("Registered intrinsics are exported by the package");
22
22
  expect(names).toContain("Registered intrinsics' isTag matches how they're authored");
23
23
  expect(names).toContain("dist/manifest.json declares a chantVersion");
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
2
2
  import { join, basename } from "path";
3
3
  import { auditIntrinsics } from "./check-lexicon-intrinsics";
4
4
  import { checkExamplesBuild } from "./check-lexicon-examples";
5
- import { auditDocsReachability } from "./check-lexicon-docs";
5
+ import { auditDocsClassification, auditDocsReachability } from "./check-lexicon-docs";
6
6
  import { auditMcpNames, lexiconNameFor } from "./check-lexicon-mcp";
7
7
  import { loadLexiconFromDir, registers, safeList } from "./check-lexicon-plugin";
8
8
  import { RULE_CATALOG } from "../../audit/catalog";
@@ -254,12 +254,14 @@ export async function checkLexicon(dir: string): Promise<CheckResult> {
254
254
  // checks only ever counted example directories; none tried to build one,
255
255
  // so `lexicons/aws/examples/core-concepts` shipped with a discovery-time
256
256
  // "Duplicate export name" error while this tool reported "All tier-1
257
- // checks passed." See ./check-lexicon-examples.ts for exactly what
258
- // "builds" means here (discovery + serialization, not post-synth/lint).
257
+ // checks passed." chant #1400 and the built output must pass the
258
+ // lexicon's own post-synth checks at error severity; three aws examples
259
+ // taught patterns (no TLS-only bucket policy, mutable ECR tags) the
260
+ // lexicon flags as errors. See ./check-lexicon-examples.ts.
259
261
  const exampleBuilds = await checkExamplesBuild(dir);
260
262
  const brokenExamples = exampleBuilds.filter((e) => !e.ok);
261
263
  items.push({
262
- name: "Every shipped example builds",
264
+ name: "Every shipped example builds and passes its own post-synth checks",
263
265
  tier: 1,
264
266
  pass: brokenExamples.length === 0,
265
267
  detail:
@@ -374,6 +376,19 @@ export async function checkLexicon(dir: string): Promise<CheckResult> {
374
376
  : undefined,
375
377
  });
376
378
 
379
+ // Authored pages name their Diátaxis quadrant; the sidebar is built from
380
+ // it, so an untagged page cannot be placed (#1731).
381
+ const docsClass = auditDocsClassification(dir);
382
+ items.push({
383
+ name: "Every authored doc page has a diataxis quadrant",
384
+ tier: 1,
385
+ pass: !docsClass.hasPages || docsClass.unclassified.length === 0,
386
+ detail:
387
+ docsClass.unclassified.length > 0
388
+ ? `${docsClass.unclassified.length} unclassified: ${docsClass.unclassified.join(", ")}`
389
+ : undefined,
390
+ });
391
+
377
392
  // ── Tier 2: Recommended ────────────────────────────────────────
378
393
 
379
394
  const pluginContent = readOr(join(dir, "src/plugin.ts"));
@@ -67,7 +67,8 @@ export async function generate(options?: { verbose?: boolean; force?: boolean })
67
67
  * Write generated files to the package directory.
68
68
  */
69
69
  export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
70
- const dir = pkgDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
70
+ // This file lives in src/codegen/, so the package root is three levels up.
71
+ const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
71
72
  writeGeneratedArtifacts({
72
73
  baseDir: dir,
73
74
  files: {
@@ -86,7 +87,9 @@ import { generate, writeGeneratedFiles } from "./generate";
86
87
  import { dirname } from "path";
87
88
  import { fileURLToPath } from "url";
88
89
 
89
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
90
+ // src/codegen/generate-cli.ts -> src/codegen -> src -> package root.
91
+ // Two dirnames land in src/ and the first generate writes src/src/generated/ (#1614).
92
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
90
93
  const result = await generate({ verbose: true });
91
94
  writeGeneratedFiles(result, pkgDir);
92
95
  `;
@@ -57,16 +57,49 @@ export default defineConfig({
57
57
  }
58
58
 
59
59
  export function generateDocsContentConfig(): string {
60
- return `import { defineCollection } from 'astro:content';
60
+ return `import { defineCollection, z } from 'astro:content';
61
61
  import { docsLoader } from '@astrojs/starlight/loaders';
62
62
  import { docsSchema } from '@astrojs/starlight/schema';
63
63
 
64
64
  export const collections = {
65
- docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
65
+ docs: defineCollection({
66
+ loader: docsLoader(),
67
+ schema: docsSchema({
68
+ extend: z.object({
69
+ // Diátaxis quadrant (https://diataxis.fr), chant #1731.
70
+ diataxis: z.enum(['tutorial', 'how-to', 'reference', 'explanation']).optional(),
71
+ }),
72
+ }),
73
+ }),
66
74
  };
67
75
  `;
68
76
  }
69
77
 
78
+ /**
79
+ * First authored page. Lives under docs/pages/; the docs pipeline copies it
80
+ * into src/content/docs/ and builds the sidebar from its `diataxis` field.
81
+ */
82
+ export function generateDocsGettingStartedMdx(name: string): string {
83
+ const displayName = name.charAt(0).toUpperCase() + name.slice(1);
84
+ return `---
85
+ title: Getting Started
86
+ description: First project with the ${displayName} lexicon
87
+ diataxis: tutorial
88
+ ---
89
+
90
+ Install the lexicon, declare one resource, and build it.
91
+
92
+ \`\`\`bash
93
+ npm install --save-dev @intentius/chant-lexicon-${name}
94
+ chant build
95
+ \`\`\`
96
+
97
+ Replace this with a walkthrough a new user can follow start to finish. Other
98
+ pages go beside this file in \`docs/pages/\`, each with a \`diataxis\` field
99
+ (\`tutorial\`, \`how-to\`, \`reference\` or \`explanation\`).
100
+ `;
101
+ }
102
+
70
103
  export function generateDocsIndexMdx(name: string): string {
71
104
  const displayName = name.charAt(0).toUpperCase() + name.slice(1);
72
105
  return `---
@@ -1,5 +1,6 @@
1
1
  import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
- import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, readdirSync } from "fs";
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync, readdirSync, realpathSync } from "fs";
3
+ import { execFileSync } from "child_process";
3
4
  import { join, dirname } from "path";
4
5
  import { tmpdir } from "os";
5
6
  import { fileURLToPath } from "url";
@@ -63,6 +64,7 @@ describe("initLexiconCommand", () => {
63
64
  "docs/astro.config.mjs",
64
65
  "docs/src/content.config.ts",
65
66
  "docs/src/content/docs/index.mdx",
67
+ "docs/pages/getting-started.mdx",
66
68
  "examples/getting-started/package.json",
67
69
  "examples/getting-started/src/infra.ts",
68
70
  "src/generated/.gitkeep",
@@ -265,6 +267,45 @@ describe("scaffold content validation", () => {
265
267
  const content = readFileSync(join(targetDir, "src/lint/rules/index.ts"), "utf-8");
266
268
  expect(content).toContain("export const rules");
267
269
  });
270
+
271
+ // #1614: generate-cli.ts lives in src/codegen/, so the package root is three
272
+ // dirnames up. Two dirnames is src/, and the first `npm run generate` then
273
+ // writes src/src/generated/. Run the scaffolded CLI for real with generate.ts
274
+ // replaced by a stub that reports where it was told to write.
275
+ test("generate-cli.ts resolves pkgDir to the package root, so generate writes src/generated (#1614)", () => {
276
+ const cliPath = join(targetDir, "src/codegen/generate-cli.ts");
277
+ expect(readFileSync(cliPath, "utf-8")).toContain(
278
+ "dirname(dirname(dirname(fileURLToPath(import.meta.url))))",
279
+ );
280
+
281
+ writeFileSync(
282
+ join(targetDir, "src/codegen/generate.ts"),
283
+ [
284
+ 'import { mkdirSync, writeFileSync } from "fs";',
285
+ 'import { join } from "path";',
286
+ "export async function generate() { return {}; }",
287
+ "export function writeGeneratedFiles(_result: unknown, pkgDir: string) {",
288
+ ' const dir = join(pkgDir, "src/generated");',
289
+ " mkdirSync(dir, { recursive: true });",
290
+ ' writeFileSync(join(dir, "lexicon.json"), "{}");',
291
+ " process.stdout.write(pkgDir);",
292
+ "}",
293
+ "",
294
+ ].join("\n"),
295
+ );
296
+
297
+ const tsx = join(__dirname, "../../../../../node_modules/.bin/tsx");
298
+ const out = execFileSync(tsx, [cliPath], { cwd: targetDir, encoding: "utf-8" });
299
+
300
+ expect(realpathSync(out.trim())).toBe(realpathSync(targetDir));
301
+ expect(existsSync(join(targetDir, "src/generated/lexicon.json"))).toBe(true);
302
+ expect(existsSync(join(targetDir, "src/src"))).toBe(false);
303
+ });
304
+
305
+ test("generate.ts default pkgDir is also the package root (#1614)", () => {
306
+ const content = readFileSync(join(targetDir, "src/codegen/generate.ts"), "utf-8");
307
+ expect(content).toContain("pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))))");
308
+ });
268
309
  });
269
310
 
270
311
  // ── Fixture snapshot tests ──────────────────────────────────────────
@@ -10,7 +10,7 @@ import { generateSampleRuleTs, generateLintRulesIndexTs } from "./init-lexicon/t
10
10
  import { generateLspCompletionsTs, generateLspHoverTs } from "./init-lexicon/templates/lsp";
11
11
  import { generatePackageJson, generateTsConfig, generateTsConfigBuild, generateJustfile, generateGitignore, generateReadme, generateSerializerTs, generateValidateTs, generateValidateCliTs } from "./init-lexicon/templates/project";
12
12
  import { generatePluginTestTs, generateSerializerTestTs, generateCompletionsTestTs, generateHoverTestTs } from "./init-lexicon/templates/tests";
13
- import { generateDocsPackageJson, generateDocsTsConfig, generateDocsAstroConfig, generateDocsContentConfig, generateDocsIndexMdx } from "./init-lexicon/templates/docs";
13
+ import { generateDocsPackageJson, generateDocsTsConfig, generateDocsAstroConfig, generateDocsContentConfig, generateDocsIndexMdx, generateDocsGettingStartedMdx } from "./init-lexicon/templates/docs";
14
14
  import { generateExamplePackageJson, generateExampleInfraTs } from "./init-lexicon/templates/examples";
15
15
 
16
16
  /**
@@ -115,6 +115,7 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
115
115
  "docs/src",
116
116
  "docs/src/content",
117
117
  "docs/src/content/docs",
118
+ "docs/pages",
118
119
  "examples/getting-started",
119
120
  "examples/getting-started/src",
120
121
  ];
@@ -159,6 +160,7 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
159
160
  "docs/astro.config.mjs": generateDocsAstroConfig(name),
160
161
  "docs/src/content.config.ts": generateDocsContentConfig(),
161
162
  "docs/src/content/docs/index.mdx": generateDocsIndexMdx(name),
163
+ "docs/pages/getting-started.mdx": generateDocsGettingStartedMdx(name),
162
164
  "examples/getting-started/package.json": generateExamplePackageJson(name),
163
165
  "examples/getting-started/src/infra.ts": generateExampleInfraTs(name, names),
164
166
  };
@@ -46,7 +46,7 @@ export async function runRollingUpgrade(
46
46
  lexicon: "aws",
47
47
  hasUpgrade: false,
48
48
  severity: "none",
49
- delta: { added: [], changed: [], removed: [], severity: "none" },
49
+ delta: { added: [], changed: [], removed: [], renamed: [], severity: "none" },
50
50
  deltaText: "",
51
51
  validationOk: false,
52
52
  failures: [{ step: "setup", output: `Lexicon directory not found: ${dir}` }],
@@ -21,7 +21,7 @@ function result(over: Partial<RegenResult>): RegenResult {
21
21
  ok: true,
22
22
  changed: true,
23
23
  severity: "additive",
24
- delta: { added: [], changed: [], removed: [], severity: "additive" },
24
+ delta: { added: [], changed: [], removed: [], renamed: [], severity: "additive" },
25
25
  deltaText: "",
26
26
  failures: [],
27
27
  freshSnapshot: { surface: "x" } as unknown as RegenResult["freshSnapshot"],
@@ -62,7 +62,7 @@ export async function runSurfaceDiff(opts: SurfaceDiffOptions): Promise<RegenRes
62
62
  ok: false,
63
63
  changed: false,
64
64
  severity: "none",
65
- delta: { added: [], changed: [], removed: [], severity: "none" },
65
+ delta: { added: [], changed: [], removed: [], renamed: [], severity: "none" },
66
66
  deltaText: "",
67
67
  failures: [failure],
68
68
  freshSnapshot: null,