@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
@@ -12,17 +12,38 @@ import { join } from "path";
12
12
  import { fileURLToPath } from "url";
13
13
 
14
14
  import { expandFileMarkers } from "./docs-file-markers";
15
+ import { readAuthoredPages } from "./docs-pages";
15
16
  import { scanRules, generateRules } from "./docs-rule-scanning";
16
17
  import { buildSidebar } from "./docs-sidebar";
17
18
  import { generateOverview, generateIntrinsics, generatePseudoParameters, generateSerialization } from "./docs-sections";
18
- import type { DocsConfig, DocsResult, ManifestJSON, MetaEntry } from "./docs-types";
19
+ import type { DocsConfig, DocsResult, ManifestJSON, MetaEntry, SidebarPage } from "./docs-types";
19
20
 
20
21
  // Re-export all public types and functions so existing importers continue to work.
21
22
  export { expandFileMarkers } from "./docs-file-markers";
22
- export type { DocsConfig, DocsResult } from "./docs-types";
23
+ export type { DocsConfig, DocsResult, Quadrant, SidebarPage } from "./docs-types";
24
+ export { QUADRANTS, QUADRANT_LABELS, readAuthoredPages } from "./docs-pages";
23
25
 
24
26
  // ── Pipeline ───────────────────────────────────────────────────────
25
27
 
28
+ /**
29
+ * The version rendered into the docs (chant #1377).
30
+ *
31
+ * `dist/manifest.json` is written by `npm run bundle`, which runs only under
32
+ * `prepack`, so its `version` is whatever was last bundled on this machine
33
+ * and can trail `package.json` by several releases. `package.json` is the
34
+ * source of truth and is never stale, so it wins whenever it is present.
35
+ * The manifest's own version is the fallback for a dist with no package
36
+ * beside it (test fixtures, relocated dist directories).
37
+ */
38
+ export function lexiconVersion(config: DocsConfig, manifest: ManifestJSON): string {
39
+ const pkgPath = config.packageJsonPath ?? join(config.distDir, "..", "package.json");
40
+ if (existsSync(pkgPath)) {
41
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: unknown };
42
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
43
+ }
44
+ return manifest.version;
45
+ }
46
+
26
47
  /**
27
48
  * Run the documentation pipeline with the supplied config.
28
49
  */
@@ -30,6 +51,7 @@ export function docsPipeline(config: DocsConfig): DocsResult {
30
51
  const manifest = JSON.parse(
31
52
  readFileSync(join(config.distDir, "manifest.json"), "utf-8"),
32
53
  ) as ManifestJSON;
54
+ manifest.version = lexiconVersion(config, manifest);
33
55
 
34
56
  const meta = JSON.parse(
35
57
  readFileSync(join(config.distDir, "meta.json"), "utf-8"),
@@ -71,50 +93,47 @@ export function docsPipeline(config: DocsConfig): DocsResult {
71
93
  }
72
94
  pages.set("index.mdx", overviewContent);
73
95
  const suppress = new Set(config.suppressPages ?? []);
74
- const extraSlugs = new Set((config.extraPages ?? []).map((p) => p.slug));
75
-
76
- // Extra pages from lexicon config
77
- if (config.extraPages) {
78
- for (const page of config.extraPages) {
79
- let content = page.content;
80
- if (config.examplesDir) {
81
- content = expandFileMarkers(content, config.examplesDir);
82
- }
83
- pages.set(
84
- `${page.slug}.mdx`,
85
- [
86
- "---",
87
- `title: "${page.title}"`,
88
- page.description ? `description: "${page.description}"` : "",
89
- "---",
90
- "",
91
- content,
92
- "",
93
- ]
94
- .filter(Boolean)
95
- .join("\n"),
96
- );
97
- }
96
+ const sidebarPages: SidebarPage[] = [];
97
+
98
+ // Authored pages under docs/pages/ (#1733). Each names its Diátaxis
99
+ // quadrant; the sidebar is grouped from that. Written with a provenance
100
+ // marker that points back at the source file.
101
+ const authored = readAuthoredPages(config);
102
+ const authoredSources = new Map<string, string>();
103
+ for (const page of authored) {
104
+ pages.set(`${page.slug}.mdx`, page.content);
105
+ authoredSources.set(`${page.slug}.mdx`, page.file);
106
+ if (!page.hidden) sidebarPages.push(page);
98
107
  }
99
108
 
100
- // A generated page must not overwrite one the lexicon explicitly declared.
101
- // The extraPages above are written into `pages` first, so an unguarded
102
- // `pages.set` below silently discards them: helm declared its own
103
- // "Intrinsics Reference" and pre-synth rules pages and shipped neither for
104
- // as long as both slugs collided (#1312). Explicit authorship wins, and the
105
- // collision is reported rather than resolved in silence.
109
+ const extraSlugs = new Set(authored.map((p) => p.slug));
110
+
111
+ // A generated page must not overwrite one the lexicon authored. Authored
112
+ // pages are written into `pages` first, so an unguarded `pages.set` below
113
+ // would silently discard them: helm once declared its own "Intrinsics
114
+ // Reference" and pre-synth rules pages and shipped neither for as long as
115
+ // both slugs collided (#1312). Authorship wins, and the collision is
116
+ // reported rather than resolved in silence.
106
117
  const claimed = (slug: string): boolean => {
107
118
  if (suppress.has(slug)) return true;
108
119
  if (!extraSlugs.has(slug)) return false;
109
120
  console.warn(
110
- `[docs:${config.name}] extraPages declares "${slug}", which is also a generated page — keeping the declared one.\n` +
111
- ` Add "${slug}" to suppressPages to make that explicit, or rename the extraPage if both are wanted.`,
121
+ `[docs:${config.name}] docs/pages/${slug}.mdx has the slug of a generated page — keeping the authored one.\n` +
122
+ ` Add "${slug}" to suppressPages to make that explicit, or rename the authored page if both are wanted.`,
112
123
  );
113
124
  return true;
114
125
  };
115
126
 
127
+ // Generated reference tables sort after authored reference pages, in the
128
+ // order the old flat sidebar listed them.
129
+ let generatedOrder = 1000;
130
+ const generated = (slug: string, label: string): void => {
131
+ sidebarPages.push({ slug, label, quadrant: "reference", order: generatedOrder++ });
132
+ };
133
+
116
134
  if (!claimed("intrinsics") && manifest.intrinsics && manifest.intrinsics.length > 0) {
117
135
  pages.set("intrinsics.mdx", generateIntrinsics(config, manifest));
136
+ generated("intrinsics", "Intrinsics");
118
137
  }
119
138
 
120
139
  if (
@@ -126,14 +145,21 @@ export function docsPipeline(config: DocsConfig): DocsResult {
126
145
  "pseudo-parameters.mdx",
127
146
  generatePseudoParameters(config, manifest),
128
147
  );
148
+ generated("pseudo-parameters", "Pseudo-Parameters");
129
149
  }
130
150
 
151
+ // Every lexicon links its generated rules table, whether or not it also
152
+ // ships a prose `lint-rules` page. Skipping it when one existed was how gcp
153
+ // ended up emitting a page nothing pointed at (#1312). The label
154
+ // distinguishes the generated table from a prose page.
131
155
  if (!claimed("rules") && rules.length > 0) {
132
156
  pages.set("rules.mdx", generateRules(config, rules));
157
+ generated("rules", "All Rules");
133
158
  }
134
159
 
135
160
  if (!claimed("serialization")) {
136
161
  pages.set("serialization.mdx", generateSerialization(config));
162
+ generated("serialization", "Serialization");
137
163
  }
138
164
 
139
165
  // Stamp every emitted page with its provenance. These files look exactly
@@ -142,11 +168,12 @@ export function docsPipeline(config: DocsConfig): DocsResult {
142
168
  // intrinsics guide's #1044 claim were both fixed in the emitted `.mdx` and
143
169
  // silently reverted by the next regen (#1312).
144
170
  for (const [filename, content] of pages) {
145
- pages.set(filename, withGeneratedMarker(config, content));
171
+ pages.set(filename, withGeneratedMarker(config, content, authoredSources.get(filename)));
146
172
  }
147
173
 
148
174
  return {
149
175
  pages,
176
+ sidebarPages,
150
177
  stats: {
151
178
  resources: resources.size,
152
179
  properties: properties.size,
@@ -163,8 +190,11 @@ export function docsPipeline(config: DocsConfig): DocsResult {
163
190
  * MDX parses `<!-- -->` as JSX rather than a comment, so this uses the
164
191
  * `{/* … *\/}` form the rest of the docs already use for generated markers.
165
192
  */
166
- function withGeneratedMarker(config: DocsConfig, content: string): string {
167
- const marker = `{/* ${GENERATED_MARKER_TAG} by \`npm run docs -w @intentius/chant-lexicon-${config.name}\` — DO NOT EDIT.\n Edit lexicons/${config.name}/src/codegen/docs.ts instead; changes here are overwritten. */}`;
193
+ function withGeneratedMarker(config: DocsConfig, content: string, source?: string): string {
194
+ const edit = source
195
+ ? `Edit lexicons/${config.name}/docs/pages/${source} instead`
196
+ : `Edit lexicons/${config.name}/src/codegen/docs.ts instead`;
197
+ const marker = `{/* ${GENERATED_MARKER_TAG} by \`npm run docs -w @intentius/chant-lexicon-${config.name}\` — DO NOT EDIT.\n ${edit}; changes here are overwritten. */}`;
168
198
  const lines = content.split("\n");
169
199
  // Frontmatter is the leading `---` … `---` block; the marker goes after it.
170
200
  if (lines[0] === "---") {
@@ -302,7 +332,7 @@ export function writeDocsSite(config: DocsConfig, result: DocsResult): void {
302
332
  if (unreachable.length > 0) {
303
333
  console.warn(
304
334
  `[docs:${config.name}] ${unreachable.length} page(s) in no sidebar entry — reachable only by direct URL: ${unreachable.join(", ")}.\n` +
305
- ` Declare them in this lexicon's DocsConfig \`sidebarExtra\` (or \`extraPages\`) to surface them.`,
335
+ ` Move them to docs/pages/ with a \`diataxis\` field to surface them (chant #1731).`,
306
336
  );
307
337
  }
308
338
 
@@ -349,12 +379,20 @@ export function writeDocsSite(config: DocsConfig, result: DocsResult): void {
349
379
  mkdirSync(join(outDir, "src"), { recursive: true });
350
380
  writeFileSync(
351
381
  join(outDir, "src", "content.config.ts"),
352
- `import { defineCollection } from 'astro:content';
382
+ `import { defineCollection, z } from 'astro:content';
353
383
  import { docsLoader } from '@astrojs/starlight/loaders';
354
384
  import { docsSchema } from '@astrojs/starlight/schema';
355
385
 
356
386
  export const collections = {
357
- docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
387
+ docs: defineCollection({
388
+ loader: docsLoader(),
389
+ schema: docsSchema({
390
+ extend: z.object({
391
+ // Diátaxis quadrant (https://diataxis.fr), chant #1731.
392
+ diataxis: z.enum(['tutorial', 'how-to', 'reference', 'explanation']).optional(),
393
+ }),
394
+ }),
395
+ }),
358
396
  };
359
397
  `,
360
398
  );
@@ -524,13 +524,15 @@ function makeResult(
524
524
  added: [],
525
525
  changed: [],
526
526
  removed: [],
527
+ renamed: [],
527
528
  severity: "none",
528
529
  };
529
530
  const effectiveDelta = delta ?? emptyDelta;
530
531
  const changed =
531
532
  effectiveDelta.added.length > 0 ||
532
533
  effectiveDelta.changed.length > 0 ||
533
- effectiveDelta.removed.length > 0;
534
+ effectiveDelta.removed.length > 0 ||
535
+ effectiveDelta.renamed.length > 0;
534
536
  const deltaText = changed ? formatDelta(effectiveDelta) : "";
535
537
 
536
538
  return {
@@ -93,7 +93,7 @@ function makeRegen(
93
93
  return async () => {
94
94
  const delta = fresh
95
95
  ? diffSurface(baseline, fresh)
96
- : { added: [], changed: [], removed: [], severity: "none" as const };
96
+ : { added: [], changed: [], removed: [], renamed: [], severity: "none" as const };
97
97
  const changed =
98
98
  delta.added.length > 0 || delta.changed.length > 0 || delta.removed.length > 0;
99
99
  return {
@@ -293,7 +293,7 @@ export function classifyDelta(delta: SurfaceDelta): {
293
293
  severity: ChangeSeverity;
294
294
  } {
295
295
  const hasUpgrade =
296
- delta.added.length > 0 || delta.changed.length > 0 || delta.removed.length > 0;
296
+ delta.added.length > 0 || delta.changed.length > 0 || delta.removed.length > 0 || delta.renamed.length > 0;
297
297
  return { hasUpgrade, severity: hasUpgrade ? delta.severity : "none" };
298
298
  }
299
299
 
@@ -428,6 +428,133 @@ describe("serializeSnapshot / parseSnapshot", () => {
428
428
 
429
429
  // ── formatDelta ───────────────────────────────────────────────────────
430
430
 
431
+ describe("diffSurface — renames (#1460)", () => {
432
+ test("a removal and an addition with the same resourceType pair as one rename", () => {
433
+ const baseline = makeSnapshot({
434
+ MacieSession: { kind: "resource", resourceType: "AWS::Macie::Session", props: ["Status:false"] },
435
+ });
436
+ const fresh = makeSnapshot({
437
+ Session: { kind: "resource", resourceType: "AWS::Macie::Session", props: ["Status:false"] },
438
+ });
439
+ const delta = diffSurface(baseline, fresh);
440
+ expect(delta.added).toEqual([]);
441
+ expect(delta.removed).toEqual([]);
442
+ expect(delta.renamed).toEqual([
443
+ { from: "MacieSession", to: "Session", entry: fresh.entries.Session },
444
+ ]);
445
+ // The old import still breaks, so a rename stays breaking for 1.x.
446
+ expect(delta.severity).toBe("breaking");
447
+ });
448
+
449
+ test("property types rename the same way, and a same-kind match is required", () => {
450
+ const baseline = makeSnapshot({
451
+ BCMDataExportsExport_DataQuery: { kind: "property", resourceType: "AWS::BCMDataExports::Export.DataQuery" },
452
+ Thing: { kind: "resource", resourceType: "X::Thing" },
453
+ });
454
+ const fresh = makeSnapshot({
455
+ Export_DataQuery: { kind: "property", resourceType: "AWS::BCMDataExports::Export.DataQuery" },
456
+ Thing_Prop: { kind: "property", resourceType: "X::Thing" },
457
+ });
458
+ const delta = diffSurface(baseline, fresh);
459
+ expect(delta.renamed.map((r) => `${r.from} -> ${r.to}`)).toEqual([
460
+ "BCMDataExportsExport_DataQuery -> Export_DataQuery",
461
+ ]);
462
+ // A resource and a property under the same type string are not a rename.
463
+ expect(delta.removed.map((r) => r.name)).toEqual(["Thing"]);
464
+ expect(delta.added.map((a) => a.name)).toEqual(["Thing_Prop"]);
465
+ expect(delta.severity).toBe("breaking");
466
+ });
467
+
468
+ test("a rename that also changed shape carries the structural diff", () => {
469
+ const baseline = makeSnapshot({
470
+ PanoramaPackage: { kind: "resource", resourceType: "AWS::Panorama::Package", props: ["PackageName:true", "Tags:false"] },
471
+ });
472
+ const fresh = makeSnapshot({
473
+ Package: { kind: "resource", resourceType: "AWS::Panorama::Package", props: ["PackageName:true"], attrs: ["Arn"] },
474
+ });
475
+ const delta = diffSurface(baseline, fresh);
476
+ expect(delta.renamed).toHaveLength(1);
477
+ expect(delta.renamed[0].changes).toEqual({ removedProps: ["Tags"], addedAttrs: ["Arn"] });
478
+ expect(delta.changed).toEqual([]);
479
+ });
480
+
481
+ test("several old and new names under one resourceType pair by similarity; the rest stay add/remove", () => {
482
+ const rt = "AWS::SageMaker::Space";
483
+ const baseline = makeSnapshot({
484
+ Space_OwnershipSettings: { kind: "property", resourceType: rt },
485
+ Space_SpaceSettings: { kind: "property", resourceType: rt },
486
+ Space_Gone: { kind: "property", resourceType: rt },
487
+ });
488
+ const fresh = makeSnapshot({
489
+ SageMakerSpace_SpaceSettings: { kind: "property", resourceType: rt },
490
+ SageMakerSpace_OwnershipSettings: { kind: "property", resourceType: rt },
491
+ });
492
+ const delta = diffSurface(baseline, fresh);
493
+ expect(delta.renamed.map((r) => `${r.from} -> ${r.to}`)).toEqual([
494
+ "Space_OwnershipSettings -> SageMakerSpace_OwnershipSettings",
495
+ "Space_SpaceSettings -> SageMakerSpace_SpaceSettings",
496
+ ]);
497
+ expect(delta.removed.map((r) => r.name)).toEqual(["Space_Gone"]);
498
+ expect(delta.added).toEqual([]);
499
+ });
500
+
501
+ test("entries with an empty resourceType never pair", () => {
502
+ const baseline = makeSnapshot({ A: { kind: "property", resourceType: "" } });
503
+ const fresh = makeSnapshot({ B: { kind: "property", resourceType: "" } });
504
+ const delta = diffSurface(baseline, fresh);
505
+ expect(delta.renamed).toEqual([]);
506
+ expect(delta.removed).toHaveLength(1);
507
+ expect(delta.added).toHaveLength(1);
508
+ });
509
+
510
+ test("the #1423 shape: 28 renames among 148 removals and 83 additions", () => {
511
+ // 3 resources renamed, each dragging 25/3-ish property types with them —
512
+ // 28 renames in total, 120 plain removals, 55 plain additions.
513
+ const baseEntries: SurfaceSnapshot["entries"] = {};
514
+ const freshEntries: SurfaceSnapshot["entries"] = {};
515
+
516
+ const renamedResources: Array<[string, string, string]> = [
517
+ ["MacieSession", "Session", "AWS::Macie::Session"],
518
+ ["PanoramaPackage", "Package", "AWS::Panorama::Package"],
519
+ ["Space", "SageMakerSpace", "AWS::SageMaker::Space"],
520
+ ];
521
+ let renames = 0;
522
+ for (const [oldName, newName, rt] of renamedResources) {
523
+ baseEntries[oldName] = { kind: "resource", resourceType: rt };
524
+ freshEntries[newName] = { kind: "resource", resourceType: rt };
525
+ renames++;
526
+ }
527
+ // 25 property renames spread across the three parents
528
+ for (let i = 0; i < 25; i++) {
529
+ const [oldName, newName, rt] = renamedResources[i % 3];
530
+ baseEntries[`${oldName}_Prop${i}`] = { kind: "property", resourceType: `${rt}.Prop${i}` };
531
+ freshEntries[`${newName}_Prop${i}`] = { kind: "property", resourceType: `${rt}.Prop${i}` };
532
+ renames++;
533
+ }
534
+ expect(renames).toBe(28);
535
+
536
+ for (let i = 0; i < 120; i++) {
537
+ baseEntries[`Gone${i}`] = { kind: "resource", resourceType: `AWS::Gone::Type${i}` };
538
+ }
539
+ for (let i = 0; i < 55; i++) {
540
+ freshEntries[`Fresh${i}`] = { kind: "resource", resourceType: `AWS::Fresh::Type${i}` };
541
+ }
542
+
543
+ const delta = diffSurface(makeSnapshot(baseEntries), makeSnapshot(freshEntries));
544
+ expect(delta.renamed).toHaveLength(28);
545
+ expect(delta.removed).toHaveLength(120);
546
+ expect(delta.added).toHaveLength(55);
547
+ expect(delta.severity).toBe("breaking");
548
+
549
+ const text = formatDelta(delta);
550
+ expect(text).toContain("Renamed (28):");
551
+ expect(text).toContain("Removed (120):");
552
+ expect(text).toContain("Added (55):");
553
+ expect(text).toContain(" ~ MacieSession -> Session [resource] (AWS::Macie::Session)");
554
+ expect(text).toContain("Severity: breaking");
555
+ });
556
+ });
557
+
431
558
  describe("formatDelta", () => {
432
559
  test("includes severity in output", () => {
433
560
  const delta = diffSurface(
@@ -457,5 +584,17 @@ describe("formatDelta", () => {
457
584
  expect(text).toContain("Added (1):");
458
585
  expect(text).toContain("Removed (1):");
459
586
  expect(text).toContain("Changed (1):");
587
+ expect(text).not.toContain("Renamed");
588
+ });
589
+
590
+ test("renames print as Old -> New with the shared resourceType", () => {
591
+ const baseline = makeSnapshot({ Old: { kind: "resource", resourceType: "X" } });
592
+ const fresh = makeSnapshot({ New: { kind: "resource", resourceType: "X" } });
593
+ const text = formatDelta(diffSurface(baseline, fresh));
594
+ expect(text).toContain("Renamed (1):");
595
+ expect(text).toContain(" ~ Old -> New [resource] (X)");
596
+ expect(text).not.toContain("Added (");
597
+ expect(text).not.toContain("Removed (");
598
+ expect(text).toContain("Severity: breaking");
460
599
  });
461
600
  });