@intentius/chant 0.44.14 → 0.46.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 (220) 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 +14 -0
  17. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  20. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  21. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  22. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  23. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  24. package/dist/cli/commands/lint.d.ts.map +1 -1
  25. package/dist/cli/commands/onboard.d.ts +28 -0
  26. package/dist/cli/commands/onboard.d.ts.map +1 -1
  27. package/dist/cli/handlers/build.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.d.ts.map +1 -1
  29. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  30. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts.map +1 -1
  32. package/dist/cli/handlers/search.d.ts +27 -0
  33. package/dist/cli/handlers/search.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/registry.d.ts +7 -0
  36. package/dist/cli/registry.d.ts.map +1 -1
  37. package/dist/codegen/docs-pages.d.ts +39 -0
  38. package/dist/codegen/docs-pages.d.ts.map +1 -0
  39. package/dist/codegen/docs-sections.d.ts.map +1 -1
  40. package/dist/codegen/docs-sidebar.d.ts +13 -2
  41. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  42. package/dist/codegen/docs-types.d.ts +30 -10
  43. package/dist/codegen/docs-types.d.ts.map +1 -1
  44. package/dist/codegen/docs.d.ts +14 -2
  45. package/dist/codegen/docs.d.ts.map +1 -1
  46. package/dist/codegen/lexicon-regen.d.ts +11 -0
  47. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  48. package/dist/codegen/surface-snapshot.d.ts +27 -3
  49. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  50. package/dist/codegen/validate.d.ts +21 -3
  51. package/dist/codegen/validate.d.ts.map +1 -1
  52. package/dist/components/capability-plugin.d.ts +13 -2
  53. package/dist/components/capability-plugin.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts +38 -0
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/index.d.ts +1 -1
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/config.d.ts +85 -5
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/discovery/fold-import.d.ts.map +1 -1
  62. package/dist/env.d.ts +12 -1
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/fold/fold.d.ts +100 -1
  65. package/dist/fold/fold.d.ts.map +1 -1
  66. package/dist/graph-ir.d.ts +12 -0
  67. package/dist/graph-ir.d.ts.map +1 -1
  68. package/dist/graph-ops.d.ts +17 -0
  69. package/dist/graph-ops.d.ts.map +1 -0
  70. package/dist/lexicon.d.ts +182 -2
  71. package/dist/lexicon.d.ts.map +1 -1
  72. package/dist/lifecycle/change-set.d.ts +23 -2
  73. package/dist/lifecycle/change-set.d.ts.map +1 -1
  74. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  75. package/dist/lifecycle/index.d.ts +1 -0
  76. package/dist/lifecycle/index.d.ts.map +1 -1
  77. package/dist/lifecycle/observe.d.ts +8 -0
  78. package/dist/lifecycle/observe.d.ts.map +1 -1
  79. package/dist/lifecycle/replay.d.ts.map +1 -1
  80. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  81. package/dist/lifecycle/teardown.d.ts +130 -0
  82. package/dist/lifecycle/teardown.d.ts.map +1 -0
  83. package/dist/lifecycle/types.d.ts +7 -0
  84. package/dist/lifecycle/types.d.ts.map +1 -1
  85. package/dist/lint/engine.d.ts +6 -2
  86. package/dist/lint/engine.d.ts.map +1 -1
  87. package/dist/lint/policy.d.ts.map +1 -1
  88. package/dist/lint/rule.d.ts +31 -0
  89. package/dist/lint/rule.d.ts.map +1 -1
  90. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  91. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  92. package/dist/lint/rules/index.d.ts +2 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/managed-fields.d.ts +33 -21
  95. package/dist/managed-fields.d.ts.map +1 -1
  96. package/dist/observation.d.ts +23 -1
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/op/builders.d.ts +36 -7
  99. package/dist/op/builders.d.ts.map +1 -1
  100. package/dist/op/index.d.ts +1 -1
  101. package/dist/op/index.d.ts.map +1 -1
  102. package/dist/testing.d.ts +136 -0
  103. package/dist/testing.d.ts.map +1 -0
  104. package/dist/yaml.d.ts +0 -8
  105. package/dist/yaml.d.ts.map +1 -1
  106. package/package.json +6 -1
  107. package/src/__snapshots__/okf.test.ts.snap +20 -0
  108. package/src/audit/discover.test.ts +55 -1
  109. package/src/audit/discover.ts +70 -2
  110. package/src/audit/edge-init-safety.test.ts +149 -18
  111. package/src/audit/report-model.ts +12 -1
  112. package/src/audit/rules-doc.ts +1 -0
  113. package/src/build-params.test.ts +52 -0
  114. package/src/cli/build-params-cli.test.ts +25 -8
  115. package/src/cli/build-params-cli.ts +37 -23
  116. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  117. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  118. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  119. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  120. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  121. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  122. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  123. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  124. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  125. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  126. package/src/cli/commands/audit.test.ts +83 -1
  127. package/src/cli/commands/audit.ts +121 -28
  128. package/src/cli/commands/build.test.ts +275 -5
  129. package/src/cli/commands/build.ts +86 -14
  130. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  131. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  132. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  133. package/src/cli/commands/check-lexicon.test.ts +46 -2
  134. package/src/cli/commands/check-lexicon.ts +64 -4
  135. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  136. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  137. package/src/cli/commands/init-lexicon.test.ts +42 -1
  138. package/src/cli/commands/init-lexicon.ts +3 -1
  139. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  140. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  141. package/src/cli/commands/lexicon-surface-diff.ts +10 -1
  142. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  143. package/src/cli/commands/lint.ts +19 -6
  144. package/src/cli/commands/onboard.test.ts +319 -225
  145. package/src/cli/commands/onboard.ts +144 -58
  146. package/src/cli/handlers/build.test.ts +3 -3
  147. package/src/cli/handlers/build.ts +2 -0
  148. package/src/cli/handlers/components.ts +2 -2
  149. package/src/cli/handlers/graph.test.ts +44 -0
  150. package/src/cli/handlers/graph.ts +24 -4
  151. package/src/cli/handlers/lifecycle.test.ts +348 -1
  152. package/src/cli/handlers/lifecycle.ts +235 -3
  153. package/src/cli/handlers/run.test.ts +5 -5
  154. package/src/cli/handlers/run.ts +1 -0
  155. package/src/cli/handlers/search-live.test.ts +217 -0
  156. package/src/cli/handlers/search.test.ts +41 -1
  157. package/src/cli/handlers/search.ts +60 -8
  158. package/src/cli/main.ts +17 -4
  159. package/src/cli/registry.ts +7 -0
  160. package/src/cli/security-boundary.test.ts +2 -1
  161. package/src/codegen/docs-pages.test.ts +194 -0
  162. package/src/codegen/docs-pages.ts +138 -0
  163. package/src/codegen/docs-sections.ts +0 -8
  164. package/src/codegen/docs-sidebar.ts +50 -37
  165. package/src/codegen/docs-types.ts +32 -4
  166. package/src/codegen/docs.ts +78 -40
  167. package/src/codegen/lexicon-regen.ts +22 -2
  168. package/src/codegen/rolling-upgrade.test.ts +1 -1
  169. package/src/codegen/rolling-upgrade.ts +1 -1
  170. package/src/codegen/surface-snapshot.test.ts +139 -0
  171. package/src/codegen/surface-snapshot.ts +297 -122
  172. package/src/codegen/validate.test.ts +49 -3
  173. package/src/codegen/validate.ts +36 -7
  174. package/src/components/auto-release.ts +1 -1
  175. package/src/components/capability-plugin.ts +23 -2
  176. package/src/components/driver.test.ts +45 -0
  177. package/src/components/driver.ts +64 -27
  178. package/src/components/index.ts +2 -0
  179. package/src/components/starter-plugin.ts +5 -2
  180. package/src/config.test.ts +40 -0
  181. package/src/config.ts +159 -8
  182. package/src/discovery/fold-import.test.ts +427 -2
  183. package/src/discovery/fold-import.ts +163 -7
  184. package/src/discovery/sandbox/driver.test.ts +11 -0
  185. package/src/env.test.ts +35 -1
  186. package/src/env.ts +17 -3
  187. package/src/fold/fold.test.ts +41 -0
  188. package/src/fold/fold.ts +372 -2
  189. package/src/graph-ir-live.test.ts +42 -0
  190. package/src/graph-ir.ts +42 -2
  191. package/src/graph-ops.test.ts +69 -0
  192. package/src/graph-ops.ts +42 -0
  193. package/src/lexicon.ts +182 -2
  194. package/src/lifecycle/change-set.test.ts +41 -0
  195. package/src/lifecycle/change-set.ts +30 -2
  196. package/src/lifecycle/deep-observe.ts +4 -0
  197. package/src/lifecycle/index.ts +1 -0
  198. package/src/lifecycle/observe.ts +29 -2
  199. package/src/lifecycle/replay.ts +3 -0
  200. package/src/lifecycle/snapshot.ts +3 -0
  201. package/src/lifecycle/teardown.test.ts +537 -0
  202. package/src/lifecycle/teardown.ts +357 -0
  203. package/src/lifecycle/types.ts +7 -0
  204. package/src/lint/engine.ts +7 -1
  205. package/src/lint/policy.ts +14 -3
  206. package/src/lint/rule.ts +23 -0
  207. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  208. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  209. package/src/lint/rules/index.ts +4 -1
  210. package/src/managed-fields.test.ts +15 -42
  211. package/src/managed-fields.ts +43 -26
  212. package/src/observation.test.ts +24 -8
  213. package/src/observation.ts +48 -4
  214. package/src/op/builders.ts +40 -7
  215. package/src/op/index.ts +1 -1
  216. package/src/ownership.test.ts +63 -1
  217. package/src/testing.test.ts +261 -0
  218. package/src/testing.ts +338 -0
  219. package/src/yaml.test.ts +15 -0
  220. package/src/yaml.ts +19 -2
@@ -1,43 +1,174 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { readdirSync, readFileSync, existsSync } from "fs";
2
+ import { readdirSync, readFileSync, existsSync, statSync } from "fs";
3
3
  import { fileURLToPath } from "url";
4
- import { join } from "path";
4
+ import { join, dirname, resolve } from "path";
5
+ import ts from "typescript";
5
6
 
6
7
  /**
7
- * #428: the lexicon modules the hosted service imports on the edge — post-synth
8
- * checks (+ their local helpers) and the `detect` modules must not run Node-only
9
- * APIs at module load. `createRequire(import.meta.url)` at module scope throws on
10
- * Workers (import.meta.url is undefined), crashing the worker at startup. Anything
11
- * that genuinely needs it must do so lazily, inside a function, behind a try.
8
+ * #428 / #1621: the lexicon modules the hosted service imports on the edge —
9
+ * the post-synth barrel and the `detect` module, plus everything they reach
10
+ * through static imports must not build filesystem paths at module load.
11
+ * On workerd `import.meta.url` is undefined and `__dirname`, `require` and
12
+ * `process.cwd()` do not exist, so a module-scope
13
+ * `dirname(fileURLToPath(import.meta.url))` crashes the worker at startup.
14
+ * Anything that genuinely needs a path must compute it lazily, inside a
15
+ * function, behind a try.
16
+ *
17
+ * The old guard only grepped the post-synth files themselves for
18
+ * `createRequire`. azure's `deploy-scopes.ts` (reached from azr030) used
19
+ * `fileURLToPath` and slipped through. This version walks the transitive
20
+ * relative-import graph of every barrel with the TypeScript parser and flags
21
+ * the whole family of constructions at module scope.
12
22
  */
13
23
  const lexiconsDir = fileURLToPath(new URL("../../../../lexicons", import.meta.url));
14
24
 
15
- function edgeImportedFiles(): string[] {
25
+ function barrels(): string[] {
16
26
  const out: string[] = [];
17
27
  for (const lex of readdirSync(lexiconsDir)) {
18
28
  const detect = join(lexiconsDir, lex, "src", "detect.ts");
19
29
  if (existsSync(detect)) out.push(detect);
20
30
  const ps = join(lexiconsDir, lex, "src", "lint", "post-synth");
21
31
  if (existsSync(ps)) {
32
+ const index = join(ps, "index.ts");
33
+ if (existsSync(index)) out.push(index);
34
+ // Checks not re-exported from the barrel are still edge-imported
35
+ // individually by the hosted service, so keep them as roots too.
22
36
  for (const f of readdirSync(ps)) {
23
- if (f.endsWith(".ts") && !f.endsWith(".test.ts")) out.push(join(ps, f));
37
+ if (f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts") out.push(join(ps, f));
24
38
  }
25
39
  }
26
40
  }
27
41
  return out;
28
42
  }
29
43
 
30
- describe("edge-imported lexicon modules are init-safe", () => {
31
- test("no module-scope createRequire(import.meta.url)", () => {
32
- const offenders: string[] = [];
33
- for (const file of edgeImportedFiles()) {
34
- for (const line of readFileSync(file, "utf-8").split("\n")) {
35
- // Module scope = no leading whitespace. Inside a function it's indented.
36
- if (/^(const|let|var|\s*)?\S.*createRequire\(import\.meta\.url\)/.test(line) && !/^\s/.test(line)) {
37
- offenders.push(`${file.replace(lexiconsDir, "lexicons")}: ${line.trim()}`);
38
- }
44
+ function parse(file: string): ts.SourceFile {
45
+ return ts.createSourceFile(file, readFileSync(file, "utf-8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
46
+ }
47
+
48
+ /** Resolve a relative specifier to a .ts file the way the lexicon tsconfigs do. */
49
+ function resolveRelative(from: string, spec: string): string | undefined {
50
+ const base = resolve(dirname(from), spec.replace(/\.js$/, ""));
51
+ for (const candidate of [`${base}.ts`, `${base}.tsx`, join(base, "index.ts")]) {
52
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /** Static (non-type-only) imports and re-exports of a file, resolved to paths. */
58
+ function staticImports(sf: ts.SourceFile): string[] {
59
+ const out: string[] = [];
60
+ for (const stmt of sf.statements) {
61
+ let spec: string | undefined;
62
+ if (ts.isImportDeclaration(stmt)) {
63
+ if (stmt.importClause?.isTypeOnly) continue;
64
+ spec = (stmt.moduleSpecifier as ts.StringLiteral).text;
65
+ } else if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
66
+ if (stmt.isTypeOnly) continue;
67
+ spec = (stmt.moduleSpecifier as ts.StringLiteral).text;
68
+ }
69
+ if (!spec || !spec.startsWith(".")) continue;
70
+ const resolved = resolveRelative(sf.fileName, spec);
71
+ if (resolved) out.push(resolved);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** Every file reachable from the roots through static relative imports. */
77
+ function transitiveClosure(roots: string[]): Map<string, ts.SourceFile> {
78
+ const seen = new Map<string, ts.SourceFile>();
79
+ const queue = [...roots];
80
+ while (queue.length) {
81
+ const file = queue.pop()!;
82
+ if (seen.has(file)) continue;
83
+ const sf = parse(file);
84
+ seen.set(file, sf);
85
+ for (const dep of staticImports(sf)) if (!seen.has(dep)) queue.push(dep);
86
+ }
87
+ return seen;
88
+ }
89
+
90
+ function isImportMetaUrl(node: ts.Node): boolean {
91
+ return (
92
+ ts.isPropertyAccessExpression(node) &&
93
+ node.name.text === "url" &&
94
+ ts.isMetaProperty(node.expression) &&
95
+ node.expression.keywordToken === ts.SyntaxKind.ImportKeyword
96
+ );
97
+ }
98
+
99
+ function containsImportMetaUrl(node: ts.Node): boolean {
100
+ if (isImportMetaUrl(node)) return true;
101
+ return ts.forEachChild(node, containsImportMetaUrl) ?? false;
102
+ }
103
+
104
+ /** Describe a module-scope path construction, or undefined if `node` is benign. */
105
+ function offense(node: ts.Node): string | undefined {
106
+ if (ts.isIdentifier(node) && (node.text === "__dirname" || node.text === "__filename")) {
107
+ // `const __dirname_ = ...` is a declaration, not a use of the Node global.
108
+ if (ts.isVariableDeclaration(node.parent) && node.parent.name === node) return undefined;
109
+ return node.text;
110
+ }
111
+ if (ts.isCallExpression(node)) {
112
+ const callee = node.expression;
113
+ if (ts.isIdentifier(callee)) {
114
+ if (callee.text === "require") return "require(...)";
115
+ if ((callee.text === "fileURLToPath" || callee.text === "createRequire") && node.arguments.some(containsImportMetaUrl)) {
116
+ return `${callee.text}(import.meta.url)`;
39
117
  }
40
118
  }
119
+ if (
120
+ ts.isPropertyAccessExpression(callee) &&
121
+ ts.isIdentifier(callee.expression) &&
122
+ callee.expression.text === "process" &&
123
+ callee.name.text === "cwd"
124
+ ) {
125
+ return "process.cwd()";
126
+ }
127
+ }
128
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "URL" && node.arguments?.some(containsImportMetaUrl)) {
129
+ return "new URL(..., import.meta.url)";
130
+ }
131
+ return undefined;
132
+ }
133
+
134
+ /** Bodies that run later than module evaluation. */
135
+ function defersEvaluation(node: ts.Node): boolean {
136
+ return (
137
+ ts.isFunctionDeclaration(node) ||
138
+ ts.isFunctionExpression(node) ||
139
+ ts.isArrowFunction(node) ||
140
+ ts.isMethodDeclaration(node) ||
141
+ ts.isConstructorDeclaration(node) ||
142
+ ts.isGetAccessorDeclaration(node) ||
143
+ ts.isSetAccessorDeclaration(node) ||
144
+ ts.isPropertyDeclaration(node)
145
+ );
146
+ }
147
+
148
+ function moduleScopeOffenses(sf: ts.SourceFile): string[] {
149
+ const hits: string[] = [];
150
+ const visit = (node: ts.Node): void => {
151
+ if (defersEvaluation(node)) return;
152
+ const what = offense(node);
153
+ if (what) {
154
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
155
+ hits.push(`${sf.fileName.replace(lexiconsDir, "lexicons")}:${line + 1}: ${what}`);
156
+ }
157
+ ts.forEachChild(node, visit);
158
+ };
159
+ ts.forEachChild(sf, visit);
160
+ return hits;
161
+ }
162
+
163
+ describe("edge-imported lexicon modules are init-safe", () => {
164
+ test("reaches helpers through the barrel import graph", () => {
165
+ const files = [...transitiveClosure(barrels()).keys()];
166
+ expect(files).toContain(join(lexiconsDir, "azure", "src", "deploy-scopes.ts"));
167
+ });
168
+
169
+ test("no module-scope filesystem-path constructions in the transitive import graph", () => {
170
+ const offenders: string[] = [];
171
+ for (const sf of transitiveClosure(barrels()).values()) offenders.push(...moduleScopeOffenses(sf));
41
172
  expect(offenders, "move these into a function (lazy) — they crash edge bundles").toEqual([]);
42
173
  });
43
174
  });
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { AuditFinding } from "./core";
9
+ import type { UnclaimedFile } from "./discover";
9
10
  import { RULE_CATALOG, ruleDocUrl, type Authority, type Category, type FixKind, type RuleMeta, type Tier } from "./catalog";
10
11
  import { proveFix, unifiedDiff, type ProveOptions } from "./proof";
11
12
  import type { Severity } from "../lint/rule";
@@ -107,9 +108,17 @@ export interface SerializedFinding {
107
108
  export interface AuditReportJson {
108
109
  schemaVersion: string;
109
110
  tool: { name: string; version: string };
111
+ /**
112
+ * Whether the audit had lexicons to look with (#1623). `"ok"` when at least
113
+ * one audit lexicon resolved; `"no-lexicons"` means nothing was inspected and
114
+ * the report carries no findings by construction.
115
+ */
116
+ status: "ok" | "no-lexicons";
110
117
  snapshot?: AuditSnapshot;
111
118
  summary: ReportCounts;
112
119
  findings: SerializedFinding[];
120
+ /** Candidate files that looked like they wanted a lexicon that is not installed. */
121
+ unclaimed?: UnclaimedFile[];
113
122
  }
114
123
 
115
124
  export function metaFor(id: string, catalog: Record<string, RuleMeta> = RULE_CATALOG): RuleMeta {
@@ -270,13 +279,14 @@ export function buildReportModel(findings: AuditFinding[], opts: BuildModelOptio
270
279
  /** Build the versioned, machine-readable JSON report (stable contract). */
271
280
  export function buildReportJson(
272
281
  findings: AuditFinding[],
273
- opts: { snapshot?: AuditSnapshot; toolVersion?: string; catalog?: Record<string, RuleMeta> } = {},
282
+ opts: { snapshot?: AuditSnapshot; toolVersion?: string; catalog?: Record<string, RuleMeta>; unclaimed?: UnclaimedFile[] } = {},
274
283
  ): AuditReportJson {
275
284
  const model = buildReportModel(findings, { catalog: opts.catalog });
276
285
  const version = opts.toolVersion ?? opts.snapshot?.toolVersion ?? "0.0.0";
277
286
  return {
278
287
  schemaVersion: REPORT_SCHEMA_VERSION,
279
288
  tool: { name: "chant-audit", version },
289
+ status: "ok",
280
290
  snapshot: opts.snapshot,
281
291
  summary: model.counts,
282
292
  findings: model.findings.map((f) => ({
@@ -294,5 +304,6 @@ export function buildReportJson(
294
304
  authority: f.meta.authority ?? [],
295
305
  docUrl: ruleDocUrl(f.checkId),
296
306
  })),
307
+ unclaimed: opts.unclaimed && opts.unclaimed.length > 0 ? opts.unclaimed : undefined,
297
308
  };
298
309
  }
@@ -49,6 +49,7 @@ export async function renderRulesReference(): Promise<string> {
49
49
  return `---
50
50
  title: Audit rules reference
51
51
  description: Every rule chant audit can report, with its tier, fix kind, and remediation.
52
+ diataxis: reference
52
53
  ---
53
54
 
54
55
  This is the reference for every rule [\`chant audit\`](/chant/cli/audit/) can report. Each finding in a report links to its rule here.
@@ -129,6 +129,58 @@ describe("resolveBuildParams — validation", () => {
129
129
  });
130
130
  });
131
131
 
132
+ describe("resolveBuildParams — unset optional parameters (#1371)", () => {
133
+ // An unset optional parameter must read as `undefined` from `params.<name>`
134
+ // — never `null`. `null` is a value ("explicitly nothing"); leaving a
135
+ // parameter unset is not that statement, and a `null` that reaches a
136
+ // composite ships as `key: null` in the output.
137
+ test("unset optional with a declared default resolves to the default", () => {
138
+ const defs: BuildParamsConfig = {
139
+ baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN", default: "arn:base" },
140
+ };
141
+ const result = resolveBuildParams(defs, { env: {} });
142
+ expect(result.errors).toEqual([]);
143
+ expect(buildParamValues(result.provenance)).toEqual({ baseImageArn: "arn:base" });
144
+ });
145
+
146
+ test("unset optional without a default is absent — reads as undefined, never null", () => {
147
+ const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN" } };
148
+ const result = resolveBuildParams(defs, { env: {} });
149
+ expect(result.errors).toEqual([]);
150
+ const values = buildParamValues(result.provenance);
151
+ expect("baseImageArn" in values).toBe(false);
152
+ expect(values.baseImageArn).toBeUndefined();
153
+ expect(values.baseImageArn ?? "fallback").toBe("fallback");
154
+ });
155
+
156
+ test("an env var declared but not exported is the same as unset", () => {
157
+ const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, env: "KMV_BASE_IMAGE_ARN" } };
158
+ const result = resolveBuildParams(defs, { env: { KMV_BASE_IMAGE_ARN: undefined } });
159
+ expect(result.errors).toEqual([]);
160
+ expect(result.provenance).toEqual([]);
161
+ });
162
+
163
+ test("an explicit empty string is a supplied value, not unset", () => {
164
+ const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false, default: "arn:base" } };
165
+ const fromCli = resolveBuildParams(defs, { cli: { baseImageArn: "" } });
166
+ expect(fromCli.errors).toEqual([]);
167
+ expect(fromCli.provenance).toEqual([{ name: "baseImageArn", value: "", source: "cli" }]);
168
+
169
+ const fromEnv = resolveBuildParams({ baseImageArn: { type: "string", required: false, env: "X" } }, { env: { X: "" } });
170
+ expect(fromEnv.errors).toEqual([]);
171
+ expect(fromEnv.provenance).toEqual([{ name: "baseImageArn", value: "", source: "env" }]);
172
+ });
173
+
174
+ test("a null in --params-file is rejected, never passed through as a value", () => {
175
+ const defs: BuildParamsConfig = { baseImageArn: { type: "string", required: false } };
176
+ const result = resolveBuildParams(defs, { fromFile: { baseImageArn: null } });
177
+ expect(result.errors).toEqual([
178
+ 'build parameter "baseImageArn" (from --params-file) must be a string, number, or boolean',
179
+ ]);
180
+ expect(result.provenance).toEqual([]);
181
+ });
182
+ });
183
+
132
184
  describe("buildParamValues", () => {
133
185
  test("projects provenance records down to a plain value map", () => {
134
186
  const provenance = [
@@ -56,16 +56,16 @@ describe("resolveCliBuildParams", () => {
56
56
  }
57
57
  });
58
58
 
59
- test("a build with no overrides collapses to a single defaults count", () => {
59
+ test("a build with no overrides collapses to a single count line (#1424)", () => {
60
60
  // The provenance still records every value; the echo just stops naming
61
- // what nobody set. One line, so silence still never means "no parameters
62
- // reached us".
61
+ // them. One line, so silence still never means "no parameters reached us".
63
62
  const defs: BuildParamsConfig = { tier: { type: "string", default: "light" } };
64
63
  const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
65
64
  try {
66
65
  const result = resolveCliBuildParams(defs, {});
67
66
  const logged = errorSpy.mock.calls.map((call) => String(call[0]));
68
- expect(logged.some((line) => line.includes("1 parameters at their defaults"))).toBe(true);
67
+ expect(logged).toHaveLength(1);
68
+ expect(logged[0]).toContain("1 build parameter resolved (1 default); --verbose to list");
69
69
  expect(logged.some((line) => line.includes("[param] tier"))).toBe(false);
70
70
  expect(result.provenance).toEqual([{ name: "tier", value: "light", source: "default" }]);
71
71
  } finally {
@@ -73,7 +73,7 @@ describe("resolveCliBuildParams", () => {
73
73
  }
74
74
  });
75
75
 
76
- test("names the overrides and collapses the defaults into a count", () => {
76
+ test("the count line breaks down by source, so an override's arrival is still visible", () => {
77
77
  const defs: BuildParamsConfig = {
78
78
  tier: { type: "string", default: "light" },
79
79
  target: { type: "string", default: "k3d" },
@@ -83,9 +83,26 @@ describe("resolveCliBuildParams", () => {
83
83
  try {
84
84
  resolveCliBuildParams(defs, { cli: { tier: "ha" } });
85
85
  const logged = errorSpy.mock.calls.map((call) => String(call[0]));
86
+ expect(logged).toHaveLength(1);
87
+ expect(logged[0]).toContain("3 build parameters resolved (1 from cli, 2 default); --verbose to list");
88
+ expect(logged.some((line) => line.includes("[param]"))).toBe(false);
89
+ } finally {
90
+ errorSpy.mockRestore();
91
+ }
92
+ });
93
+
94
+ test("--verbose lists every parameter with its source", () => {
95
+ const defs: BuildParamsConfig = {
96
+ tier: { type: "string", default: "light" },
97
+ target: { type: "string", default: "k3d" },
98
+ };
99
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
100
+ try {
101
+ resolveCliBuildParams(defs, { cli: { tier: "ha" }, verbose: true });
102
+ const logged = errorSpy.mock.calls.map((call) => String(call[0]));
86
103
  expect(logged.some((line) => line.includes("[param] tier") && line.includes("ha") && line.includes("cli"))).toBe(true);
87
- expect(logged.some((line) => line.includes("2 more at their defaults"))).toBe(true);
88
- expect(logged.some((line) => line.includes("[param] target"))).toBe(false);
104
+ expect(logged.some((line) => line.includes("[param] target") && line.includes("k3d") && line.includes("default"))).toBe(true);
105
+ expect(logged.some((line) => line.includes("--verbose to list"))).toBe(false);
89
106
  } finally {
90
107
  errorSpy.mockRestore();
91
108
  }
@@ -104,7 +121,7 @@ describe("resolveCliBuildParams", () => {
104
121
  const logged = errorSpy.mock.calls.map((call) => String(call[0]));
105
122
  expect(logged.some((line) => line.includes("[param] tier") && line.includes("ha"))).toBe(true);
106
123
  expect(logged.some((line) => line.includes("[param] target") && line.includes("k3d") && line.includes("default"))).toBe(true);
107
- expect(logged.some((line) => line.includes("more at their defaults"))).toBe(false);
124
+ expect(logged.some((line) => line.includes("--verbose to list"))).toBe(false);
108
125
  } finally {
109
126
  if (prev === undefined) delete process.env.CHANT_PARAM_ECHO;
110
127
  else process.env.CHANT_PARAM_ECHO = prev;
@@ -38,6 +38,8 @@ export interface CliBuildParamsArgs {
38
38
  cli?: Record<string, string>;
39
39
  /** `--params-file <path>` — a JSON file of `{ "name": value }` values, read and parsed here. */
40
40
  paramsFile?: string;
41
+ /** `--verbose` — list every resolved parameter one per line instead of the one-line count (#1424). */
42
+ verbose?: boolean;
41
43
  }
42
44
 
43
45
  export interface CliBuildParamsResolution {
@@ -62,13 +64,12 @@ export interface CliBuildParamsResolution {
62
64
  * chant #1064's acceptance criterion that this never surfaces as a thrown
63
65
  * error from inside user source.
64
66
  *
65
- * On success, every explicitly-set parameter is logged via `console.error` as
66
- * `[param] <name> = <value> (<source>)`, and the ones still at their declared
67
- * defaults collapse to a single count line (`CHANT_PARAM_ECHO=all` lists them
68
- * individually). Overrides stay unconditionally visible not gated on
69
- * `--verbose` so a build's environment-varying inputs are always in the log,
70
- * the same way #1022's fold decisions are; the defaults are the part that
71
- * never varies, which is exactly why they can be a count.
67
+ * On success, the resolution is summarized on one `console.error` line —
68
+ * `N build parameters resolved (a default, b from cli, ...); --verbose to
69
+ * list` so silence never means "no parameters reached us", and the count
70
+ * by source says whether an override arrived. Under `--verbose` (or
71
+ * `CHANT_PARAM_ECHO=all`) every parameter is listed as
72
+ * `[param] <name> = <value> (<source>)` instead (#1424).
72
73
  */
73
74
  export function resolveCliBuildParams(
74
75
  buildParamsConfig: BuildParamsConfig | undefined,
@@ -102,27 +103,40 @@ export function resolveCliBuildParams(
102
103
  return { success: false, provenance: [], errors };
103
104
  }
104
105
 
105
- // Echo what the operator SET, one line each; collapse what they did not.
106
- // A project with two dozen declared parameters printed two dozen grey lines
107
- // on every build, and the ones that matter the overrides drowned in the
108
- // ones that never vary. Defaults still get named on request
109
- // (CHANT_PARAM_ECHO=all), and a build with no overrides still says how many
110
- // parameters resolved, so silence never means "no parameters reached us"
111
- // (the failure mode warnIfParamsCannotReachProject exists for).
112
- const explicit = resolution.provenance.filter((p) => p.source !== "default");
113
- const defaulted = resolution.provenance.length - explicit.length;
114
- const echoAll = process.env.CHANT_PARAM_ECHO === "all";
115
- for (const p of echoAll ? resolution.provenance : explicit) {
116
- console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
117
- }
118
- if (!echoAll && defaulted > 0) {
119
- const more = explicit.length > 0 ? `${defaulted} more` : `${defaulted} parameters`;
120
- console.error(formatInfo(`[param] ${more} at their defaults (CHANT_PARAM_ECHO=all lists them)`));
106
+ // #1424 one line by default. A project with two dozen declared
107
+ // parameters printed two dozen grey lines on every build, ahead of the
108
+ // warnings the reader actually needed. The count by source still answers
109
+ // "did my override reach the build" (the failure mode
110
+ // warnIfParamsCannotReachProject exists for); --verbose names every value.
111
+ const verbose = args.verbose === true || process.env.CHANT_PARAM_ECHO === "all";
112
+ if (verbose) {
113
+ for (const p of resolution.provenance) {
114
+ console.error(formatInfo(`[param] ${p.name} = ${JSON.stringify(p.value)} (${p.source})`));
115
+ }
116
+ } else if (resolution.provenance.length > 0) {
117
+ console.error(formatInfo(summarizeBuildParams(resolution.provenance)));
121
118
  }
122
119
 
123
120
  return { success: true, provenance: resolution.provenance, errors: [] };
124
121
  }
125
122
 
123
+ /**
124
+ * `N build parameters resolved (1 from cli, 3 default); --verbose to list` —
125
+ * the non-verbose echo of a successful resolution (#1424). Sources appear in
126
+ * precedence order; a source nobody used is left out.
127
+ */
128
+ export function summarizeBuildParams(provenance: readonly { source: string }[]): string {
129
+ const counts = new Map<string, number>();
130
+ for (const p of provenance) counts.set(p.source, (counts.get(p.source) ?? 0) + 1);
131
+ const order = ["cli", "params-file", "env", "default"];
132
+ const rank = (s: string) => (order.includes(s) ? order.indexOf(s) : order.length);
133
+ const parts = [...counts.keys()]
134
+ .sort((a, b) => rank(a) - rank(b))
135
+ .map((source) => (source === "default" ? `${counts.get(source)} default` : `${counts.get(source)} from ${source}`));
136
+ const n = provenance.length;
137
+ return `${n} build parameter${n === 1 ? "" : "s"} resolved (${parts.join(", ")}); --verbose to list`;
138
+ }
139
+
126
140
  /**
127
141
  * Resolve a command's declared build-time parameters, the same way `chant
128
142
  * build` does, so a `build()`/`discover()` on the declared side sees the values
@@ -0,0 +1,5 @@
1
+ on: push
2
+ permissions: write-all
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
@@ -0,0 +1,2 @@
1
+ FROM ubuntu
2
+ RUN apt-get update
@@ -0,0 +1,3 @@
1
+ resource "aws_s3_bucket" "b" {
2
+ bucket = "x"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Resources": {
4
+ "Bucket": { "Type": "AWS::S3::Bucket" }
5
+ }
6
+ }
@@ -0,0 +1,12 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: x
5
+ spec:
6
+ template:
7
+ spec:
8
+ containers:
9
+ - name: c
10
+ image: nginx:latest
11
+ securityContext:
12
+ privileged: true
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Getting Started
3
+ description: First project with the Fixture lexicon
4
+ diataxis: tutorial
5
+ ---
6
+
7
+ Install the lexicon, declare one resource, and build it.
8
+
9
+ ```bash
10
+ npm install --save-dev @intentius/chant-lexicon-fixture
11
+ chant build
12
+ ```
13
+
14
+ Replace this with a walkthrough a new user can follow start to finish. Other
15
+ pages go beside this file in `docs/pages/`, each with a `diataxis` field
16
+ (`tutorial`, `how-to`, `reference` or `explanation`).
@@ -1,7 +1,15 @@
1
- import { defineCollection } from 'astro:content';
1
+ import { defineCollection, z } from 'astro:content';
2
2
  import { docsLoader } from '@astrojs/starlight/loaders';
3
3
  import { docsSchema } from '@astrojs/starlight/schema';
4
4
 
5
5
  export const collections = {
6
- docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
6
+ docs: defineCollection({
7
+ loader: docsLoader(),
8
+ schema: docsSchema({
9
+ extend: z.object({
10
+ // Diátaxis quadrant (https://diataxis.fr), chant #1731.
11
+ diataxis: z.enum(['tutorial', 'how-to', 'reference', 'explanation']).optional(),
12
+ }),
13
+ }),
14
+ }),
7
15
  };
@@ -3,6 +3,8 @@ import { generate, writeGeneratedFiles } from "./generate";
3
3
  import { dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
 
6
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
6
+ // src/codegen/generate-cli.ts -> src/codegen -> src -> package root.
7
+ // Two dirnames land in src/ and the first generate writes src/src/generated/ (#1614).
8
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
7
9
  const result = await generate({ verbose: true });
8
10
  writeGeneratedFiles(result, pkgDir);
@@ -62,7 +62,8 @@ export async function generate(options?: { verbose?: boolean; force?: boolean })
62
62
  * Write generated files to the package directory.
63
63
  */
64
64
  export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
65
- const dir = pkgDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
65
+ // This file lives in src/codegen/, so the package root is three levels up.
66
+ const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
66
67
  writeGeneratedArtifacts({
67
68
  baseDir: dir,
68
69
  files: {
@@ -65,7 +65,8 @@ export async function generate(options?: { verbose?: boolean; force?: boolean })
65
65
  * Write generated files to the package directory.
66
66
  */
67
67
  export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
68
- const dir = pkgDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
68
+ // This file lives in src/codegen/, so the package root is three levels up.
69
+ const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
69
70
  writeGeneratedArtifacts({
70
71
  baseDir: dir,
71
72
  files: {