helm-env-delta 1.11.1 → 1.12.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.
package/README.md CHANGED
@@ -52,11 +52,11 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
52
52
 
53
53
  🛡️ **Safety Rules** - Block major version upgrades, scaling violations, and forbidden patterns. Load validation rules from external files. Scan globally or target specific fields.
54
54
 
55
- 🎨 **Format Enforcement** - Standardize YAML across all environments: key ordering, indentation, quoting, array sorting.
55
+ 🎨 **Format Enforcement** - Standardize YAML across all environments: key ordering, alphabetical key sorting, indentation, quoting, array sorting.
56
56
 
57
57
  📦 **Config Inheritance** - Reuse base configurations with environment-specific overrides.
58
58
 
59
- 📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, synchronized side-by-side scrolling, copy diff buttons, file search, and collapse/expand controls. Empty categories are automatically hidden.
59
+ 📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, stop rule violations table (dry-run only), synchronized side-by-side scrolling, copy diff buttons, file search, and collapse/expand controls. Empty categories are automatically hidden.
60
60
 
61
61
  🔍 **Discovery Tools** - Preview files (`-l`), inspect config (`--show-config`), filter by filename/content (`-f`), filter by change type (`-m`), validate with comprehensive warnings including unused pattern detection.
62
62
 
@@ -119,7 +119,7 @@ hed -c config.yaml
119
119
  hed -c config.yaml -H
120
120
  ```
121
121
 
122
- Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, synchronized side-by-side scrolling, copy buttons, sidebar search, and collapse/expand controls. Empty categories are automatically hidden.
122
+ Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, stop rule violations table (shown in dry-run mode), synchronized side-by-side scrolling, copy buttons, sidebar search, and collapse/expand controls. Empty categories are automatically hidden.
123
123
 
124
124
  ### 5️⃣ Get Smart Suggestions (Optional)
125
125
 
@@ -208,6 +208,9 @@ outputFormat:
208
208
  - 'kind'
209
209
  - 'metadata'
210
210
  - 'spec'
211
+ keySort:
212
+ '**/*.yaml':
213
+ - path: 'spec.template.metadata.labels'
211
214
  arraySort:
212
215
  '**/*.yaml':
213
216
  - path: 'env'
@@ -664,6 +667,8 @@ stopRules:
664
667
 
665
668
  **Override:** Use `--force` to bypass stop rules when needed.
666
669
 
670
+ **Visibility:** Stop rule violations appear in console output, JSON reports, and HTML reports (dry-run mode only, as a collapsible table in the header area).
671
+
667
672
  ---
668
673
 
669
674
  ### 🎨 Output Formatting
@@ -675,13 +680,18 @@ outputFormat:
675
680
  indent: 2 # Indentation size
676
681
  keySeparator: true # Blank line between top-level keys (or second-level keys when single top-level key)
677
682
 
678
- keyOrders: # Custom key ordering
683
+ keyOrders: # Custom key ordering (pin specific keys to top)
679
684
  'apps/*.yaml':
680
685
  - 'apiVersion'
681
686
  - 'kind'
682
687
  - 'metadata'
683
688
  - 'spec'
684
689
 
690
+ keySort: # Sort all keys alphabetically at path
691
+ '**/*.yaml':
692
+ - path: 'spec.template.metadata.labels'
693
+ - path: 'env.vars'
694
+
685
695
  arraySort: # Sort arrays
686
696
  'services/**/values.yaml':
687
697
  - path: 'env'
@@ -77,6 +77,9 @@ declare const arraySortRuleSchema: z.ZodObject<{
77
77
  desc: "desc";
78
78
  }>>;
79
79
  }, z.core.$strip>;
80
+ declare const keySortRuleSchema: z.ZodObject<{
81
+ path: z.ZodString;
82
+ }, z.core.$strip>;
80
83
  declare const fixedValueRuleSchema: z.ZodObject<{
81
84
  path: z.ZodString;
82
85
  value: z.ZodUnknown;
@@ -111,6 +114,9 @@ declare const baseConfigSchema: z.ZodObject<{
111
114
  keySeparator: z.ZodOptional<z.ZodBoolean>;
112
115
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
113
116
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
117
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
118
+ path: z.ZodString;
119
+ }, z.core.$strip>>>>;
114
120
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
115
121
  path: z.ZodString;
116
122
  sortBy: z.ZodString;
@@ -230,6 +236,9 @@ declare const finalConfigSchema: z.ZodObject<{
230
236
  keySeparator: z.ZodDefault<z.ZodBoolean>;
231
237
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
232
238
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
239
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
240
+ path: z.ZodString;
241
+ }, z.core.$strip>>>>;
233
242
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
234
243
  path: z.ZodString;
235
244
  sortBy: z.ZodString;
@@ -301,6 +310,9 @@ declare const formatOnlyConfigSchema: z.ZodObject<{
301
310
  keySeparator: z.ZodDefault<z.ZodBoolean>;
302
311
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
303
312
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
313
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
314
+ path: z.ZodString;
315
+ }, z.core.$strip>>>>;
304
316
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
305
317
  path: z.ZodString;
306
318
  sortBy: z.ZodString;
@@ -324,6 +336,7 @@ export type RegexFileRule = z.infer<typeof regexFileRuleSchema>;
324
336
  export type RegexFileKeyRule = z.infer<typeof regexFileKeyRuleSchema>;
325
337
  export type VersionFormatRule = z.infer<typeof versionFormatRuleSchema>;
326
338
  export type ArraySortRule = z.infer<typeof arraySortRuleSchema>;
339
+ export type KeySortRule = z.infer<typeof keySortRuleSchema>;
327
340
  export type TransformRule = z.infer<typeof transformRuleSchema>;
328
341
  export type TransformRules = z.infer<typeof transformRulesSchema>;
329
342
  export type TransformConfig = Record<string, TransformRules>;
@@ -79,6 +79,7 @@ const arraySortRuleSchema = zod_1.z.object({
79
79
  sortBy: zod_1.z.string().min(1),
80
80
  order: zod_1.z.enum(['asc', 'desc']).default('asc')
81
81
  });
82
+ const keySortRuleSchema = zod_1.z.object({ path: zod_1.z.string().min(1) });
82
83
  const fixedValueRuleSchema = zod_1.z.object({
83
84
  path: zod_1.z.string().min(1).describe('JSONPath to the value to set'),
84
85
  value: zod_1.z.unknown().describe('The constant value to set (any type: string, number, boolean, null, object, array)')
@@ -128,6 +129,7 @@ const baseConfigSchema = zod_1.z.object({
128
129
  keySeparator: zod_1.z.boolean().optional(),
129
130
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
130
131
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
132
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
131
133
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
132
134
  })
133
135
  .optional(),
@@ -149,6 +151,7 @@ const finalConfigSchema = baseConfigSchema
149
151
  keySeparator: zod_1.z.boolean().default(false),
150
152
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
151
153
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
154
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
152
155
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
153
156
  })
154
157
  .optional()
@@ -176,6 +179,7 @@ const formatOnlyConfigSchema = baseConfigSchema
176
179
  keySeparator: zod_1.z.boolean().default(false),
177
180
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
178
181
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
182
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
179
183
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
180
184
  })
181
185
  .optional()
@@ -1,5 +1,6 @@
1
1
  import { Config } from './configFile';
2
2
  import { FileDiffResult } from './fileDiff';
3
+ import type { ValidationResult } from './stopRulesValidator';
3
4
  export type { DiffStats, ReportMetadata } from './reporters/htmlTemplate';
4
5
  declare const HtmlReporterErrorClass: {
5
6
  new (message: string, options?: import("./utils/errors").ErrorOptions): {
@@ -19,4 +20,4 @@ declare const HtmlReporterErrorClass: {
19
20
  export declare class HtmlReporterError extends HtmlReporterErrorClass {
20
21
  }
21
22
  export declare const isHtmlReporterError: (error: unknown) => error is HtmlReporterError;
22
- export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean, logger?: import("./logger").Logger) => Promise<void>;
23
+ export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean, logger?: import("./logger").Logger, validationResult?: ValidationResult) => Promise<void>;
@@ -118,7 +118,7 @@ const writeHtmlFile = async (htmlContent, outputPath) => {
118
118
  });
119
119
  }
120
120
  };
121
- const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, logger) => {
121
+ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, logger, validationResult) => {
122
122
  const reportPath = generateTemporaryFilePath();
123
123
  const metadata = {
124
124
  timestamp: new Date().toISOString(),
@@ -151,7 +151,17 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, lo
151
151
  }
152
152
  statsArray.sort((a, b) => b.added + b.removed - (a.added + a.removed));
153
153
  const diffStats = { totalAdded, totalRemoved, fileStats: statsArray };
154
- const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds, diffStats);
154
+ const stopRuleViolations = validationResult && validationResult.violations.length > 0
155
+ ? validationResult.violations.map((violation) => ({
156
+ file: violation.file,
157
+ rule: { type: violation.rule.type, path: violation.rule.path },
158
+ path: violation.path,
159
+ oldValue: violation.oldValue,
160
+ updatedValue: violation.updatedValue,
161
+ message: violation.message
162
+ }))
163
+ : undefined;
164
+ const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds, diffStats, stopRuleViolations);
155
165
  await writeHtmlFile(htmlContent, reportPath);
156
166
  logger?.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
157
167
  try {
package/dist/index.js CHANGED
@@ -326,7 +326,7 @@ const main = async () => {
326
326
  }
327
327
  const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, syncConfig, command.dryRun, command.skipFormat, logger);
328
328
  if (command.diffHtml && !command.quiet)
329
- await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger);
329
+ await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger, command.dryRun ? validationResult : undefined);
330
330
  if (command.diffJson)
331
331
  (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, syncConfig, command.dryRun, package_json_1.default.version);
332
332
  };
@@ -1,3 +1,3 @@
1
1
  export declare const DIFF2HTML_STYLES: string;
2
- export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .file-section[open] > summary {\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n /* Treeview styles */\n .tree-root {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n }\n\n .tree-root ul {\n list-style: none;\n padding-left: 20px;\n margin: 0;\n }\n\n .tree-folder,\n .tree-file {\n padding: 4px 8px;\n border-radius: 4px;\n cursor: default;\n }\n\n .tree-folder:hover,\n .tree-file:hover {\n background: #f6f8fa;\n }\n\n .tree-toggle {\n display: inline-block;\n width: 16px;\n cursor: pointer;\n color: #586069;\n font-size: 10px;\n user-select: none;\n }\n\n .tree-folder.collapsed > .tree-toggle {\n transform: rotate(-90deg);\n }\n\n .tree-folder.collapsed > .tree-children {\n display: none;\n }\n\n .tree-folder-name {\n color: #0969da;\n font-weight: 500;\n }\n\n .tree-file-name {\n color: #586069;\n padding-left: 16px;\n }\n\n /* Sidebar styles */\n .sidebar-container {\n display: flex;\n gap: 0;\n }\n\n .sidebar {\n width: 280px;\n min-width: 280px;\n border-right: 1px solid #d0d7de;\n background: #f6f8fa;\n transition: width 0.2s, min-width 0.2s, padding 0.2s, opacity 0.2s;\n }\n\n .sidebar.collapsed {\n width: 0;\n min-width: 0;\n padding: 0;\n overflow: hidden;\n border-right: none;\n }\n\n .sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #fff;\n font-weight: 600;\n font-size: 14px;\n color: #24292e;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .sidebar-toggle {\n background: none;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n cursor: pointer;\n padding: 4px 8px;\n color: #586069;\n font-size: 12px;\n }\n\n .sidebar-toggle:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .sidebar-content {\n padding: 8px;\n }\n\n .sidebar-tree .tree-file-link {\n color: #586069;\n text-decoration: none;\n padding-left: 16px;\n display: block;\n }\n\n .sidebar-tree .tree-file-link:hover {\n color: #0969da;\n }\n\n .sidebar-tree .tree-file.active .tree-file-link {\n color: #0969da;\n font-weight: 600;\n }\n\n .changed-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n .sidebar-expand-btn {\n display: none;\n position: fixed;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n background: #f6f8fa;\n border: 1px solid #d0d7de;\n border-left: none;\n border-radius: 0 4px 4px 0;\n padding: 8px 4px;\n cursor: pointer;\n color: #586069;\n z-index: 100;\n }\n\n .sidebar-expand-btn:hover {\n background: #eaeef2;\n color: #24292e;\n }\n\n .sidebar.collapsed ~ .sidebar-expand-btn {\n display: block;\n }\n\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n\n /* Scroll-to-top button */\n .scroll-to-top {\n display: none;\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 50%;\n background: #0969da;\n color: white;\n font-size: 18px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n transition: opacity 0.2s, background 0.2s;\n z-index: 200;\n line-height: 40px;\n text-align: center;\n padding: 0;\n }\n\n .scroll-to-top:hover {\n background: #0550ae;\n }\n\n .scroll-to-top.visible {\n display: block;\n }\n\n /* Content toolbar (collapse/expand buttons) */\n .content-toolbar {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-bottom: 12px;\n }\n\n .collapse-all-btn,\n .expand-all-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .collapse-all-btn:hover,\n .expand-all-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Line change count badges */\n .summary-badges {\n float: right;\n display: inline-flex;\n gap: 6px;\n margin-left: 12px;\n }\n\n .line-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n line-height: 18px;\n }\n\n .line-added {\n background: #d4edda;\n color: #155724;\n }\n\n .line-removed {\n background: #f8d7da;\n color: #721c24;\n }\n\n /* Diff toolbar */\n .diff-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 8px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #f6f8fa;\n }\n\n .copy-diff-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-diff-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-diff-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n /* Sidebar search */\n .sidebar-search {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n margin-bottom: 8px;\n box-sizing: border-box;\n position: sticky;\n top: 0;\n z-index: 1;\n background: #fff;\n }\n\n .sidebar-search:focus {\n outline: none;\n border-color: #0969da;\n box-shadow: 0 0 0 3px rgba(9,105,218,0.15);\n }\n\n /* Stats toggle button */\n .stats-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Statistics dashboard */\n .stats-dashboard {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .stats-summary {\n display: flex;\n gap: 16px;\n align-items: center;\n margin-bottom: 10px;\n }\n\n .stats-summary .total-added {\n font-weight: 700;\n color: #155724;\n font-size: 16px;\n }\n\n .stats-summary .total-removed {\n font-weight: 700;\n color: #721c24;\n font-size: 16px;\n }\n\n .stats-bar {\n display: flex;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: #e1e4e8;\n margin-bottom: 10px;\n }\n\n .stats-segment {\n height: 100%;\n min-width: 2px;\n }\n\n .stats-segment.added-segment {\n background: #28a745;\n }\n\n .stats-segment.removed-segment {\n background: #d73a49;\n }\n\n .top-changed-files {\n list-style: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n }\n\n .top-changed-files li {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 3px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n color: #586069;\n }\n\n .top-changed-files .file-path {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n margin-right: 8px;\n }\n\n .top-changed-files .file-stats {\n white-space: nowrap;\n flex-shrink: 0;\n }\n";
2
+ export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .file-section[open] > summary {\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n /* Treeview styles */\n .tree-root {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n }\n\n .tree-root ul {\n list-style: none;\n padding-left: 20px;\n margin: 0;\n }\n\n .tree-folder,\n .tree-file {\n padding: 4px 8px;\n border-radius: 4px;\n cursor: default;\n }\n\n .tree-folder:hover,\n .tree-file:hover {\n background: #f6f8fa;\n }\n\n .tree-toggle {\n display: inline-block;\n width: 16px;\n cursor: pointer;\n color: #586069;\n font-size: 10px;\n user-select: none;\n }\n\n .tree-folder.collapsed > .tree-toggle {\n transform: rotate(-90deg);\n }\n\n .tree-folder.collapsed > .tree-children {\n display: none;\n }\n\n .tree-folder-name {\n color: #0969da;\n font-weight: 500;\n }\n\n .tree-file-name {\n color: #586069;\n padding-left: 16px;\n }\n\n /* Sidebar styles */\n .sidebar-container {\n display: flex;\n gap: 0;\n }\n\n .sidebar {\n width: 280px;\n min-width: 280px;\n border-right: 1px solid #d0d7de;\n background: #f6f8fa;\n transition: width 0.2s, min-width 0.2s, padding 0.2s, opacity 0.2s;\n }\n\n .sidebar.collapsed {\n width: 0;\n min-width: 0;\n padding: 0;\n overflow: hidden;\n border-right: none;\n }\n\n .sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #fff;\n font-weight: 600;\n font-size: 14px;\n color: #24292e;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .sidebar-toggle {\n background: none;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n cursor: pointer;\n padding: 4px 8px;\n color: #586069;\n font-size: 12px;\n }\n\n .sidebar-toggle:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .sidebar-content {\n padding: 8px;\n }\n\n .sidebar-tree .tree-file-link {\n color: #586069;\n text-decoration: none;\n padding-left: 16px;\n display: block;\n }\n\n .sidebar-tree .tree-file-link:hover {\n color: #0969da;\n }\n\n .sidebar-tree .tree-file.active .tree-file-link {\n color: #0969da;\n font-weight: 600;\n }\n\n .changed-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n .sidebar-expand-btn {\n display: none;\n position: fixed;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n background: #f6f8fa;\n border: 1px solid #d0d7de;\n border-left: none;\n border-radius: 0 4px 4px 0;\n padding: 8px 4px;\n cursor: pointer;\n color: #586069;\n z-index: 100;\n }\n\n .sidebar-expand-btn:hover {\n background: #eaeef2;\n color: #24292e;\n }\n\n .sidebar.collapsed ~ .sidebar-expand-btn {\n display: block;\n }\n\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n\n /* Scroll-to-top button */\n .scroll-to-top {\n display: none;\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 50%;\n background: #0969da;\n color: white;\n font-size: 18px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n transition: opacity 0.2s, background 0.2s;\n z-index: 200;\n line-height: 40px;\n text-align: center;\n padding: 0;\n }\n\n .scroll-to-top:hover {\n background: #0550ae;\n }\n\n .scroll-to-top.visible {\n display: block;\n }\n\n /* Content toolbar (collapse/expand buttons) */\n .content-toolbar {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-bottom: 12px;\n }\n\n .collapse-all-btn,\n .expand-all-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .collapse-all-btn:hover,\n .expand-all-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Line change count badges */\n .summary-badges {\n float: right;\n display: inline-flex;\n gap: 6px;\n margin-left: 12px;\n }\n\n .line-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n line-height: 18px;\n }\n\n .line-added {\n background: #d4edda;\n color: #155724;\n }\n\n .line-removed {\n background: #f8d7da;\n color: #721c24;\n }\n\n /* Diff toolbar */\n .diff-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 8px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #f6f8fa;\n }\n\n .copy-diff-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-diff-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-diff-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n /* Sidebar search */\n .sidebar-search {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n margin-bottom: 8px;\n box-sizing: border-box;\n position: sticky;\n top: 0;\n z-index: 1;\n background: #fff;\n }\n\n .sidebar-search:focus {\n outline: none;\n border-color: #0969da;\n box-shadow: 0 0 0 3px rgba(9,105,218,0.15);\n }\n\n /* Stats toggle button */\n .stats-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Statistics dashboard */\n .stats-dashboard {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .stats-summary {\n display: flex;\n gap: 16px;\n align-items: center;\n margin-bottom: 10px;\n }\n\n .stats-summary .total-added {\n font-weight: 700;\n color: #155724;\n font-size: 16px;\n }\n\n .stats-summary .total-removed {\n font-weight: 700;\n color: #721c24;\n font-size: 16px;\n }\n\n .stats-bar {\n display: flex;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: #e1e4e8;\n margin-bottom: 10px;\n }\n\n .stats-segment {\n height: 100%;\n min-width: 2px;\n }\n\n .stats-segment.added-segment {\n background: #28a745;\n }\n\n .stats-segment.removed-segment {\n background: #d73a49;\n }\n\n .top-changed-files {\n list-style: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n }\n\n .top-changed-files li {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 3px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n color: #586069;\n }\n\n .top-changed-files .file-path {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n margin-right: 8px;\n }\n\n .top-changed-files .file-stats {\n white-space: nowrap;\n flex-shrink: 0;\n }\n\n /* Stop rules violations badge */\n .stat.violations { background: #f8d7da; color: #721c24; }\n\n /* Violations section */\n .violations-section {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .violations-toggle-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .violations-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .violations-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n margin-top: 10px;\n }\n\n .violations-table th {\n background: #f6f8fa;\n text-align: left;\n padding: 8px 12px;\n border-bottom: 2px solid #d0d7de;\n font-weight: 600;\n color: #24292e;\n }\n\n .violations-table td {\n padding: 8px 12px;\n border-bottom: 1px solid #e1e4e8;\n color: #24292e;\n vertical-align: top;\n }\n\n .violations-table tr:hover td {\n background: #f6f8fa;\n }\n\n .violation-rule-badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n background: #fff3cd;\n color: #856404;\n white-space: nowrap;\n }\n\n .violation-value {\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n background: #f6f8fa;\n padding: 2px 6px;\n border-radius: 3px;\n border: 1px solid #e1e4e8;\n }\n";
3
3
  export declare const TAB_SCRIPT: string;
@@ -617,6 +617,79 @@ exports.HTML_STYLES = `
617
617
  white-space: nowrap;
618
618
  flex-shrink: 0;
619
619
  }
620
+
621
+ /* Stop rules violations badge */
622
+ .stat.violations { background: #f8d7da; color: #721c24; }
623
+
624
+ /* Violations section */
625
+ .violations-section {
626
+ margin: 15px 0 0;
627
+ padding: 12px 0 0;
628
+ border-top: 1px solid #e1e4e8;
629
+ }
630
+
631
+ .violations-toggle-btn {
632
+ padding: 4px 12px;
633
+ border: 1px solid #d0d7de;
634
+ border-radius: 4px;
635
+ background: none;
636
+ cursor: pointer;
637
+ font-size: 12px;
638
+ color: #586069;
639
+ transition: all 0.2s;
640
+ }
641
+
642
+ .violations-toggle-btn:hover {
643
+ background: #f6f8fa;
644
+ color: #24292e;
645
+ }
646
+
647
+ .violations-table {
648
+ width: 100%;
649
+ border-collapse: collapse;
650
+ font-size: 13px;
651
+ margin-top: 10px;
652
+ }
653
+
654
+ .violations-table th {
655
+ background: #f6f8fa;
656
+ text-align: left;
657
+ padding: 8px 12px;
658
+ border-bottom: 2px solid #d0d7de;
659
+ font-weight: 600;
660
+ color: #24292e;
661
+ }
662
+
663
+ .violations-table td {
664
+ padding: 8px 12px;
665
+ border-bottom: 1px solid #e1e4e8;
666
+ color: #24292e;
667
+ vertical-align: top;
668
+ }
669
+
670
+ .violations-table tr:hover td {
671
+ background: #f6f8fa;
672
+ }
673
+
674
+ .violation-rule-badge {
675
+ display: inline-block;
676
+ padding: 2px 8px;
677
+ border-radius: 10px;
678
+ font-size: 11px;
679
+ font-weight: 600;
680
+ background: #fff3cd;
681
+ color: #856404;
682
+ white-space: nowrap;
683
+ }
684
+
685
+ .violation-value {
686
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
687
+ font-size: 12px;
688
+ background: #f6f8fa;
689
+ padding: 2px 6px;
690
+ border-radius: 3px;
691
+ border: 1px solid #e1e4e8;
692
+ }
620
693
  `;
621
694
  exports.TAB_SCRIPT = String.raw `
622
695
  // Tab switching
@@ -915,6 +988,18 @@ exports.TAB_SCRIPT = String.raw `
915
988
  });
916
989
  }
917
990
 
991
+ // Violations section toggle
992
+ const violationsToggleBtn = document.getElementById('violations-toggle-btn');
993
+ const violationsContent = document.getElementById('violations-content');
994
+ if (violationsToggleBtn && violationsContent) {
995
+ violationsToggleBtn.addEventListener('click', () => {
996
+ const isHidden = violationsContent.style.display === 'none';
997
+ violationsContent.style.display = isHidden ? 'block' : 'none';
998
+ const count = violationsToggleBtn.textContent.match(/\d+/)?.[0] || '0';
999
+ violationsToggleBtn.textContent = isHidden ? 'Hide Violations (' + count + ')' : 'Show Violations (' + count + ')';
1000
+ });
1001
+ }
1002
+
918
1003
  // Synchronized horizontal scrolling for side-by-side diff panels
919
1004
  document.querySelectorAll('.d2h-files-diff').forEach(container => {
920
1005
  const panels = container.querySelectorAll('.d2h-file-side-diff');
@@ -1,4 +1,15 @@
1
1
  import { FileDiffResult } from '../fileDiff';
2
+ export interface HtmlStopRuleViolation {
3
+ file: string;
4
+ rule: {
5
+ type: string;
6
+ path?: string;
7
+ };
8
+ path: string;
9
+ oldValue: unknown;
10
+ updatedValue: unknown;
11
+ message: string;
12
+ }
2
13
  export interface ReportMetadata {
3
14
  timestamp: string;
4
15
  source: string;
@@ -14,4 +25,4 @@ export interface DiffStats {
14
25
  removed: number;
15
26
  }>;
16
27
  }
17
- export declare const generateHtmlTemplate: (diffResult: FileDiffResult, formattedFiles: string[], trulyUnchangedFiles: string[], metadata: ReportMetadata, changedSections: string[], changedFileIds?: Map<string, string>, addedSections?: string[], addedFileIds?: Map<string, string>, diffStats?: DiffStats) => string;
28
+ export declare const generateHtmlTemplate: (diffResult: FileDiffResult, formattedFiles: string[], trulyUnchangedFiles: string[], metadata: ReportMetadata, changedSections: string[], changedFileIds?: Map<string, string>, addedSections?: string[], addedFileIds?: Map<string, string>, diffStats?: DiffStats, stopRuleViolations?: HtmlStopRuleViolation[]) => string;
@@ -4,6 +4,46 @@ exports.generateHtmlTemplate = void 0;
4
4
  const htmlStyles_1 = require("./htmlStyles");
5
5
  const treeBuilder_1 = require("./treeBuilder");
6
6
  const treeRenderer_1 = require("./treeRenderer");
7
+ const formatViolationValue = (value) => {
8
+ if (value === undefined || value === null)
9
+ return '<span class="violation-value">-</span>';
10
+ return `<span class="violation-value">${(0, treeRenderer_1.escapeHtml)(String(value))}</span>`;
11
+ };
12
+ const renderStopRulesSection = (violations) => {
13
+ if (violations.length === 0)
14
+ return '';
15
+ const rows = violations
16
+ .map((v) => `<tr>
17
+ <td>${(0, treeRenderer_1.escapeHtml)(v.file)}</td>
18
+ <td><span class="violation-rule-badge">${(0, treeRenderer_1.escapeHtml)(v.rule.type)}</span></td>
19
+ <td>${(0, treeRenderer_1.escapeHtml)(v.path)}</td>
20
+ <td>${formatViolationValue(v.oldValue)}</td>
21
+ <td>${formatViolationValue(v.updatedValue)}</td>
22
+ <td>${(0, treeRenderer_1.escapeHtml)(v.message)}</td>
23
+ </tr>`)
24
+ .join('');
25
+ return `
26
+ <div class="violations-section">
27
+ <button class="violations-toggle-btn" id="violations-toggle-btn">Show Violations (${violations.length})</button>
28
+ <div id="violations-content" style="display: none">
29
+ <table class="violations-table">
30
+ <thead>
31
+ <tr>
32
+ <th>File</th>
33
+ <th>Rule</th>
34
+ <th>Path</th>
35
+ <th>Old Value</th>
36
+ <th>New Value</th>
37
+ <th>Message</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ ${rows}
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ </div>`;
46
+ };
7
47
  const renderStatsDashboard = (diffStats) => {
8
48
  const total = diffStats.totalAdded + diffStats.totalRemoved;
9
49
  if (total === 0)
@@ -31,7 +71,7 @@ const renderStatsDashboard = (diffStats) => {
31
71
  </div>
32
72
  </div>`;
33
73
  };
34
- const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map(), diffStats) => {
74
+ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map(), diffStats, stopRuleViolations) => {
35
75
  const changedFilePaths = diffResult.changedFiles.map((f) => f.path);
36
76
  const changedTree = (0, treeBuilder_1.buildFileTree)(changedFilePaths);
37
77
  const addedFilePaths = diffResult.addedFiles.map((f) => f.path);
@@ -48,9 +88,11 @@ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, m
48
88
  ];
49
89
  const activeCategories = categories.filter((c) => c.count > 0);
50
90
  const firstActiveTab = activeCategories[0]?.id ?? 'changed';
51
- const summaryBadges = activeCategories
52
- .map((c) => `<span class="stat ${c.id}">${c.count} ${c.label}</span>`)
53
- .join('\n ');
91
+ const violationsBadge = stopRuleViolations && stopRuleViolations.length > 0
92
+ ? `<span class="stat violations">${stopRuleViolations.length} Violation${stopRuleViolations.length === 1 ? '' : 's'}</span>`
93
+ : '';
94
+ const summaryBadges = activeCategories.map((c) => `<span class="stat ${c.id}">${c.count} ${c.label}</span>`).join('\n ') +
95
+ (violationsBadge ? `\n ${violationsBadge}` : '');
54
96
  const tabButtons = activeCategories
55
97
  .map((c) => `<button class="tab${c.id === firstActiveTab ? ' active' : ''}" data-tab="${c.id}">${c.label} (${c.count})</button>`)
56
98
  .join('\n ');
@@ -140,6 +182,7 @@ ${htmlStyles_1.HTML_STYLES}
140
182
  ${summaryBadges}
141
183
  </div>
142
184
  ${diffStats ? renderStatsDashboard(diffStats) : ''}
185
+ ${stopRuleViolations && stopRuleViolations.length > 0 ? renderStopRulesSection(stopRuleViolations) : ''}
143
186
  </header>
144
187
 
145
188
  <nav class="tabs">
@@ -20,12 +20,16 @@ exports.YamlFormatterError = (0, errors_1.createErrorClass)('YAML Formatter Erro
20
20
  exports.isYamlFormatterError = (0, errors_1.createErrorTypeGuard)(exports.YamlFormatterError);
21
21
  const getFormattingRules = (filePath, outputFormat) => {
22
22
  const keyOrders = [];
23
+ const keySort = [];
23
24
  const arraySort = [];
24
25
  const quoteValues = [];
25
26
  const allPatterns = new Set();
26
27
  if (outputFormat?.keyOrders)
27
28
  for (const pattern of Object.keys(outputFormat.keyOrders))
28
29
  allPatterns.add(pattern);
30
+ if (outputFormat?.keySort)
31
+ for (const pattern of Object.keys(outputFormat.keySort))
32
+ allPatterns.add(pattern);
29
33
  if (outputFormat?.arraySort)
30
34
  for (const pattern of Object.keys(outputFormat.arraySort))
31
35
  allPatterns.add(pattern);
@@ -38,6 +42,9 @@ const getFormattingRules = (filePath, outputFormat) => {
38
42
  const keyOrder = outputFormat?.keyOrders?.[pattern];
39
43
  if (keyOrder)
40
44
  keyOrders.push(keyOrder);
45
+ const keySortRule = outputFormat?.keySort?.[pattern];
46
+ if (keySortRule)
47
+ keySort.push(keySortRule);
41
48
  const arrayRule = outputFormat?.arraySort?.[pattern];
42
49
  if (arrayRule)
43
50
  arraySort.push(arrayRule);
@@ -45,7 +52,7 @@ const getFormattingRules = (filePath, outputFormat) => {
45
52
  if (quoteValue)
46
53
  quoteValues.push(quoteValue);
47
54
  }
48
- return { keyOrders, arraySort, quoteValues };
55
+ return { keyOrders, keySort, arraySort, quoteValues };
49
56
  };
50
57
  const preserveMultilineStrings = (yamlDocument) => {
51
58
  if (!yamlDocument.contents)
@@ -81,6 +88,8 @@ const formatYaml = (content, filePath, outputFormat) => {
81
88
  const rules = getFormattingRules(filePath, outputFormat);
82
89
  if (rules.keyOrders.length > 0)
83
90
  applyKeyOrdering(yamlDocument, rules.keyOrders);
91
+ if (rules.keySort.length > 0)
92
+ applyKeySort(yamlDocument, rules.keySort);
84
93
  if (rules.arraySort.length > 0)
85
94
  applyArraySorting(yamlDocument, rules.arraySort);
86
95
  if (rules.quoteValues.length > 0)
@@ -171,6 +180,43 @@ const applyOrderingToMap = (map, currentPath, orderHierarchy) => {
171
180
  }
172
181
  }
173
182
  };
183
+ const applyKeySort = (yamlDocument, sortRules) => {
184
+ if (sortRules.length === 0)
185
+ return;
186
+ const allRules = sortRules.flat();
187
+ for (const rule of allRules) {
188
+ const pathParts = (0, jsonPath_1.parseJsonPath)(rule.path);
189
+ if (pathParts.length === 0)
190
+ continue;
191
+ if (yamlDocument.contents)
192
+ traverseAndSortKeys(yamlDocument.contents, [], pathParts);
193
+ }
194
+ };
195
+ const traverseAndSortKeys = (node, currentPath, targetPath) => {
196
+ if (!node || typeof node !== 'object')
197
+ return;
198
+ if (matchPath(currentPath, targetPath)) {
199
+ if ((0, yamlTypeGuards_1.isYamlMap)(node))
200
+ sortMapKeysAlphabetically(node);
201
+ return;
202
+ }
203
+ if ((0, yamlTypeGuards_1.isYamlMap)(node))
204
+ for (const item of node.items) {
205
+ const keyValue = (0, yamlTypeGuards_1.extractKeyValue)(item);
206
+ if (keyValue && item.value) {
207
+ const childPath = [...currentPath, keyValue];
208
+ if (isPotentialMatch(childPath, targetPath))
209
+ traverseAndSortKeys(item.value, childPath, targetPath);
210
+ }
211
+ }
212
+ };
213
+ const sortMapKeysAlphabetically = (map) => {
214
+ map.items.sort((a, b) => {
215
+ const aKey = a.key && typeof a.key === 'object' && 'value' in a.key ? String(a.key.value) : '';
216
+ const bKey = b.key && typeof b.key === 'object' && 'value' in b.key ? String(b.key.value) : '';
217
+ return aKey.localeCompare(bKey);
218
+ });
219
+ };
174
220
  const applyValueQuoting = (yamlDocument, quoteLists) => {
175
221
  if (quoteLists.length === 0)
176
222
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -68,15 +68,15 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/hogan.js": "^3.0.5",
71
- "@types/node": "^25.2.1",
71
+ "@types/node": "^25.2.3",
72
72
  "@types/picomatch": "^4.0.2",
73
- "@typescript-eslint/eslint-plugin": "^8.54.0",
74
- "@typescript-eslint/parser": "^8.54.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
74
+ "@typescript-eslint/parser": "^8.55.0",
75
75
  "@vitest/coverage-v8": "^4.0.18",
76
76
  "eslint": "^9.39.2",
77
77
  "eslint-config-prettier": "^10.1.8",
78
78
  "eslint-plugin-simple-import-sort": "^12.1.1",
79
- "eslint-plugin-unicorn": "^62.0.0",
79
+ "eslint-plugin-unicorn": "^63.0.0",
80
80
  "prettier": "^3.8.1",
81
81
  "tsx": "^4.21.0",
82
82
  "typescript": "^5.9.3",