@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,101 @@
1
+ import { watch } from "fs";
2
+ import { relative } from "path";
3
+
4
+ /**
5
+ * Watch options
6
+ */
7
+ export interface WatchOptions {
8
+ /** Debounce delay in milliseconds (default: 300) */
9
+ debounceMs?: number;
10
+ /** File extensions to include (default: [".ts"]) */
11
+ extensions?: string[];
12
+ /** Directory names to ignore */
13
+ ignoreDirs?: string[];
14
+ }
15
+
16
+ const DEFAULT_OPTIONS: Required<WatchOptions> = {
17
+ debounceMs: 300,
18
+ extensions: [".ts"],
19
+ ignoreDirs: ["node_modules", ".chant"],
20
+ };
21
+
22
+ /**
23
+ * Check if a file path should be watched
24
+ */
25
+ export function shouldWatch(filePath: string, options: Required<WatchOptions>): boolean {
26
+ // Ignore dotdirs
27
+ const parts = filePath.split("/");
28
+ for (const part of parts) {
29
+ if (part.startsWith(".") && part.length > 1) return false;
30
+ if (options.ignoreDirs.includes(part)) return false;
31
+ }
32
+
33
+ // Ignore test/spec files
34
+ if (filePath.endsWith(".test.ts") || filePath.endsWith(".spec.ts")) return false;
35
+
36
+ // Check extension
37
+ return options.extensions.some((ext) => filePath.endsWith(ext));
38
+ }
39
+
40
+ /**
41
+ * Format a timestamp as HH:MM:SS
42
+ */
43
+ export function formatTimestamp(date: Date = new Date()): string {
44
+ const h = String(date.getHours()).padStart(2, "0");
45
+ const m = String(date.getMinutes()).padStart(2, "0");
46
+ const s = String(date.getSeconds()).padStart(2, "0");
47
+ return `${h}:${m}:${s}`;
48
+ }
49
+
50
+ /**
51
+ * Format changed files for display (relative paths, truncated if many)
52
+ */
53
+ export function formatChangedFiles(files: string[], basePath: string, maxShow = 3): string {
54
+ const relative_paths = files.map((f) => relative(basePath, f));
55
+
56
+ if (relative_paths.length <= maxShow) {
57
+ return relative_paths.join(", ");
58
+ }
59
+
60
+ const shown = relative_paths.slice(0, maxShow);
61
+ return `${shown.join(", ")} (+${relative_paths.length - maxShow} more)`;
62
+ }
63
+
64
+ /**
65
+ * Watch a directory for file changes with debouncing.
66
+ * Returns a cleanup function to stop watching.
67
+ */
68
+ export function watchDirectory(
69
+ path: string,
70
+ callback: (changedFiles: string[]) => void,
71
+ options?: WatchOptions,
72
+ ): () => void {
73
+ const opts: Required<WatchOptions> = { ...DEFAULT_OPTIONS, ...options };
74
+ let pendingFiles = new Set<string>();
75
+ let timer: ReturnType<typeof setTimeout> | null = null;
76
+
77
+ const watcher = watch(path, { recursive: true }, (_event, filename) => {
78
+ if (!filename) return;
79
+ if (!shouldWatch(filename, opts)) return;
80
+
81
+ // Collect the full path
82
+ const fullPath = `${path}/${filename}`;
83
+ pendingFiles.add(fullPath);
84
+
85
+ // Reset the debounce timer
86
+ if (timer) clearTimeout(timer);
87
+ timer = setTimeout(() => {
88
+ const files = [...pendingFiles];
89
+ pendingFiles = new Set();
90
+ timer = null;
91
+ if (files.length > 0) {
92
+ callback(files);
93
+ }
94
+ }, opts.debounceMs);
95
+ });
96
+
97
+ return () => {
98
+ if (timer) clearTimeout(timer);
99
+ watcher.close();
100
+ };
101
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { toCamelCase, toPascalCase } from "./case";
3
+
4
+ describe("toCamelCase", () => {
5
+ test("lowercases first character", () => {
6
+ expect(toCamelCase("BucketName")).toBe("bucketName");
7
+ });
8
+
9
+ test("preserves already-camelCase", () => {
10
+ expect(toCamelCase("already")).toBe("already");
11
+ });
12
+
13
+ test("handles single character", () => {
14
+ expect(toCamelCase("A")).toBe("a");
15
+ });
16
+ });
17
+
18
+ describe("toPascalCase", () => {
19
+ test("uppercases first character", () => {
20
+ expect(toPascalCase("bucketName")).toBe("BucketName");
21
+ });
22
+
23
+ test("preserves already-PascalCase", () => {
24
+ expect(toPascalCase("Already")).toBe("Already");
25
+ });
26
+
27
+ test("handles single character", () => {
28
+ expect(toPascalCase("a")).toBe("A");
29
+ });
30
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Case conversion utilities for codegen.
3
+ */
4
+
5
+ export function toCamelCase(name: string): string {
6
+ return name.charAt(0).toLowerCase() + name.slice(1);
7
+ }
8
+
9
+ export function toPascalCase(name: string): string {
10
+ return name.charAt(0).toUpperCase() + name.slice(1);
11
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Coverage analysis for lexicon artifacts.
3
+ *
4
+ * Analyzes a generated lexicon JSON to measure coverage across dimensions:
5
+ * property constraints, lifecycle attributes, return attributes, and extension constraints.
6
+ */
7
+
8
+ import type { LexiconEntryParsed } from "../lexicon-schema";
9
+
10
+ export interface ResourceCoverage {
11
+ name: string;
12
+ resourceType: string;
13
+ hasPropertyConstraints: boolean;
14
+ hasLifecycle: boolean;
15
+ hasAttrs: boolean;
16
+ hasConstraints: boolean;
17
+ }
18
+
19
+ export interface CoverageReport {
20
+ resourceCount: number;
21
+ propertyPct: number;
22
+ lifecyclePct: number;
23
+ attrPct: number;
24
+ constraintPct: number;
25
+ resources: ResourceCoverage[];
26
+ }
27
+
28
+ /**
29
+ * Compute coverage from lexicon JSON content.
30
+ */
31
+ export function computeCoverage(lexiconJSON: string): CoverageReport {
32
+ const entries: Record<string, LexiconEntryParsed> = JSON.parse(lexiconJSON);
33
+
34
+ // Deduplicate: multiple TS names may map to the same resource type
35
+ const seen = new Set<string>();
36
+ const resources: ResourceCoverage[] = [];
37
+
38
+ for (const [name, entry] of Object.entries(entries)) {
39
+ if (entry.kind !== "resource") continue;
40
+ if (seen.has(entry.resourceType)) continue;
41
+ seen.add(entry.resourceType);
42
+
43
+ resources.push({
44
+ name,
45
+ resourceType: entry.resourceType,
46
+ hasPropertyConstraints: !!(entry.propertyConstraints && Object.keys(entry.propertyConstraints).length > 0),
47
+ hasLifecycle: !!(entry.createOnly?.length || entry.writeOnly?.length),
48
+ hasAttrs: !!(entry.attrs && Object.keys(entry.attrs).length > 0),
49
+ hasConstraints: !!(entry.constraints && entry.constraints.length > 0),
50
+ });
51
+ }
52
+
53
+ const count = resources.length;
54
+ if (count === 0) {
55
+ return {
56
+ resourceCount: 0,
57
+ propertyPct: 0,
58
+ lifecyclePct: 0,
59
+ attrPct: 0,
60
+ constraintPct: 0,
61
+ resources: [],
62
+ };
63
+ }
64
+
65
+ const propertyCount = resources.filter((r) => r.hasPropertyConstraints).length;
66
+ const lifecycleCount = resources.filter((r) => r.hasLifecycle).length;
67
+ const attrCount = resources.filter((r) => r.hasAttrs).length;
68
+ const constraintCount = resources.filter((r) => r.hasConstraints).length;
69
+
70
+ return {
71
+ resourceCount: count,
72
+ propertyPct: Math.round((propertyCount / count) * 100),
73
+ lifecyclePct: Math.round((lifecycleCount / count) * 100),
74
+ attrPct: Math.round((attrCount / count) * 100),
75
+ constraintPct: Math.round((constraintCount / count) * 100),
76
+ resources,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Overall coverage percentage (average of all dimensions).
82
+ */
83
+ export function overallPct(report: CoverageReport): number {
84
+ return Math.round(
85
+ (report.propertyPct + report.lifecyclePct + report.attrPct + report.constraintPct) / 4,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Format a human-readable coverage summary.
91
+ */
92
+ export function formatSummary(report: CoverageReport): string {
93
+ const lines: string[] = [];
94
+ lines.push(`Coverage Report: ${report.resourceCount} resources`);
95
+ lines.push(` Property constraints: ${report.propertyPct}%`);
96
+ lines.push(` Lifecycle (createOnly/writeOnly): ${report.lifecyclePct}%`);
97
+ lines.push(` Return attributes: ${report.attrPct}%`);
98
+ lines.push(` Extension constraints: ${report.constraintPct}%`);
99
+ lines.push(` Overall: ${overallPct(report)}%`);
100
+ return lines.join("\n");
101
+ }
102
+
103
+ /**
104
+ * Threshold configuration for coverage enforcement.
105
+ */
106
+ export interface CoverageThresholds {
107
+ minPropertyPct?: number;
108
+ minLifecyclePct?: number;
109
+ minAttrPct?: number;
110
+ minConstraintPct?: number;
111
+ minOverallPct?: number;
112
+ }
113
+
114
+ export interface ThresholdResult {
115
+ ok: boolean;
116
+ violations: string[];
117
+ }
118
+
119
+ /**
120
+ * Check coverage report against thresholds.
121
+ */
122
+ export function checkThresholds(report: CoverageReport, thresholds: CoverageThresholds): ThresholdResult {
123
+ const violations: string[] = [];
124
+
125
+ if (thresholds.minPropertyPct !== undefined && report.propertyPct < thresholds.minPropertyPct) {
126
+ violations.push(`Property constraints: ${report.propertyPct}% < ${thresholds.minPropertyPct}% minimum`);
127
+ }
128
+ if (thresholds.minLifecyclePct !== undefined && report.lifecyclePct < thresholds.minLifecyclePct) {
129
+ violations.push(`Lifecycle: ${report.lifecyclePct}% < ${thresholds.minLifecyclePct}% minimum`);
130
+ }
131
+ if (thresholds.minAttrPct !== undefined && report.attrPct < thresholds.minAttrPct) {
132
+ violations.push(`Return attributes: ${report.attrPct}% < ${thresholds.minAttrPct}% minimum`);
133
+ }
134
+ if (thresholds.minConstraintPct !== undefined && report.constraintPct < thresholds.minConstraintPct) {
135
+ violations.push(`Extension constraints: ${report.constraintPct}% < ${thresholds.minConstraintPct}% minimum`);
136
+ }
137
+ if (thresholds.minOverallPct !== undefined && overallPct(report) < thresholds.minOverallPct) {
138
+ violations.push(`Overall: ${overallPct(report)}% < ${thresholds.minOverallPct}% minimum`);
139
+ }
140
+
141
+ return { ok: violations.length === 0, violations };
142
+ }
143
+
144
+ /**
145
+ * Format verbose per-resource coverage details.
146
+ */
147
+ export function formatVerbose(report: CoverageReport): string {
148
+ const lines: string[] = [formatSummary(report), ""];
149
+
150
+ const sorted = [...report.resources].sort((a, b) => a.name.localeCompare(b.name));
151
+
152
+ for (const r of sorted) {
153
+ const missing: string[] = [];
154
+ if (!r.hasPropertyConstraints) missing.push("property-constraints");
155
+ if (!r.hasLifecycle) missing.push("lifecycle");
156
+ if (!r.hasAttrs) missing.push("attrs");
157
+ if (!r.hasConstraints) missing.push("constraints");
158
+
159
+ if (missing.length === 0) {
160
+ lines.push(` ${r.name} (${r.resourceType}): full coverage`);
161
+ } else {
162
+ lines.push(` ${r.name} (${r.resourceType}): missing ${missing.join(", ")}`);
163
+ }
164
+ }
165
+
166
+ return lines.join("\n");
167
+ }