sf-decomposer 6.17.0 → 6.19.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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@
5
5
 
6
6
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
7
7
 
8
+ ## [6.19.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.18.0...v6.19.0) (2026-05-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * **metadata:** expand uniqueIdElements coverage from audit pass ([#439](https://github.com/mcarvin8/sf-decomposer/issues/439)) ([a4c6080](https://github.com/mcarvin8/sf-decomposer/commit/a4c6080fbafa0840ff9eb797351dd92ab1b0ecae))
14
+
15
+ ## [6.18.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.17.0...v6.18.0) (2026-05-05)
16
+
17
+
18
+ ### Features
19
+
20
+ * **decompose:** bump config-disassembler to 1.2.1 and use compound keys for app ([#433](https://github.com/mcarvin8/sf-decomposer/issues/433)) ([c087664](https://github.com/mcarvin8/sf-decomposer/commit/c087664a44d73116bc375333275cb6f722662013))
21
+ * **deps:** bump config-disassembler to 1.3.0 (sanitize + collision detection) ([#436](https://github.com/mcarvin8/sf-decomposer/issues/436)) ([d53878b](https://github.com/mcarvin8/sf-decomposer/commit/d53878b8c67b4ca42a35695b605b2c489bed5f6c))
22
+
8
23
  ## [6.17.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.16.0...v6.17.0) (2026-05-04)
9
24
 
10
25
 
package/HANDBOOK.md CHANGED
@@ -126,9 +126,9 @@ Each dialog still gets its own folder, but steps live as flat `*.botSteps-meta.x
126
126
  >
127
127
  > The default still applies to every other bot.
128
128
 
129
- > **Agentforce vs Einstein.** Both share the `bot` suffix, so a single override entry covers both bot families. The structural difference (Agentforce uses richer `botFlowInvocation` and `genAi*` blocks where Einstein uses `botNavigation` and `mlIntents`) is invisible to this recipe — `multiLevel` rules only target the repeating sections that exist on each side. The plugin ships an `Internal_Copilot_Sample` fixture that exercises the Agentforce-style shape and an Einstein-style `Sample_Chat_Bot` fixture; both round-trip with the same recipe.
130
-
131
- > **Sibling order inside `botSteps`.** Recompose orders `<botSteps>` siblings alphabetically by their on-disk filename, not by their original document position. For bot deploys this is a no-op (Salesforce doesn't rely on step order at the XML level), but a freshly-recomposed file may show step entries shuffled compared to the source you originally pulled. The committed fixtures in this repo are baked from the recompose output for that reason `sf decomposer verify` will treat the baked output as the source of truth.
129
+ > **Agentforce vs Einstein.** Both share the `bot` suffix and are covered by a single override. Their structural differences (Agentforce: `botFlowInvocation`, `genAi*`; Einstein: `botNavigation`, `mlIntents`) are invisible to the recipe — `multiLevel` only targets the repeating sections that exist on each side.
130
+ >
131
+ > **Sibling order inside `botSteps`.** Recompose orders `<botSteps>` siblings alphabetically by on-disk filename, not by document position. Salesforce ignores step order at the XML level, so deploys are unaffected — but a freshly-recomposed file may show steps shuffled compared to the originally-retrieved source. The committed fixtures in this repo are baked from the recompose output for that reason; `sf decomposer verify` treats the baked output as the source of truth.
132
132
 
133
133
  ## Flexipages (Lightning App / Record / Home pages)
134
134
 
@@ -280,7 +280,7 @@ sf decomposer verify -t bot --config
280
280
  You probably ran two `decompose` invocations back-to-back, one rule each. Don't. The disassembler rewrites `.multi_level.json` on every run, so each call replaces the prior one. Pass every rule for a given component in **one** override entry, in array form.
281
281
 
282
282
  **2. "My `multiLevel` rule is correct but recompose produces a smaller file."**
283
- Almost always a unique-id collision. Two array items resolved to the same filename and one overwrote the other on disk. Add a tiebreaker to `unique_id_elements` (e.g. `developerName,id`) and re-decompose with `prePurge: true`.
283
+ On `config-disassembler` Rust 0.5.0 / Node 1.3.0 this should not happen sibling collisions are written to per-element SHA-256 shards and surfaced as a `WARN` (see pitfall #5), not silently overwritten. If you do see a shrunken recomposed file on a current build, treat it as a regression worth capturing as a fixture.
284
284
 
285
285
  **3. "Component-scope override fields look ignored."**
286
286
  Component-scope wins over type-scope, but only for fields **the component override explicitly sets**. Fields it leaves out fall through to the type-scope value, then to the run-wide default. If you set `decomposedFormat: "yaml"` on a type and `strategy: "grouped-by-tag"` on the component, the component still gets `decomposedFormat: "yaml"` from the type override.
@@ -289,7 +289,10 @@ Component-scope wins over type-scope, but only for fields **the component overri
289
289
  That's by design for `multiLevel` types only. Multi-level recompose has to clean up inner-level directories so the next level can merge their reassembled XML. If you want the decomposed tree preserved for inspection, copy it before running `recompose`.
290
290
 
291
291
  **5. "Decompose succeeded but my decomposed files all have hash names."**
292
- Your `unique_id_elements` (or the rule's third part) didn't resolve to a non-empty value on those items. Check the source XML for the elements you listed — names are case-sensitive and live at the immediate child level of each repeating item. The plugin only walks one level deep when picking a UID.
292
+ There are two distinct causes; run the decompose under `RUST_LOG=warn` to tell them apart:
293
+
294
+ - **No `WARN` line.** Your `unique_id_elements` (or the rule's third part) didn't resolve to a non-empty value on those items. Check the source XML for the elements you listed — names are case-sensitive and live at the immediate child level of each repeating item. The plugin only walks one level deep when picking a UID.
295
+ - **A `WARN` line of the form `uniqueIdElements collision: <parentTag> id "X" matched N sibling elements`.** The configured key is too narrow — multiple siblings legitimately share the same value, so the collision detector falls back to per-element SHA-256 hashes for that group rather than overwrite. Add a tiebreaker to `unique_id_elements` (e.g. a compound like `name+recordType`) and re-decompose with `prePurge: true`.
293
296
 
294
297
  ---
295
298
 
package/README.md CHANGED
@@ -14,29 +14,17 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
14
14
  <summary>Table of Contents</summary>
15
15
 
16
16
  - [Quick Start](#quick-start)
17
+ - [Requirements](#requirements)
17
18
  - [Why sf-decomposer?](#why-sf-decomposer)
18
19
  - [Commands](#commands)
19
- - [sf decomposer decompose](#sf-decomposer-decompose)
20
- - [sf decomposer recompose](#sf-decomposer-recompose)
21
- - [sf decomposer verify](#sf-decomposer-verify)
22
20
  - [Manifest-scoped runs](#manifest-scoped-runs)
23
21
  - [Decompose Strategies](#decompose-strategies)
24
- - [Custom Labels](#custom-labels-decomposition)
25
- - [Permission Sets (grouped-by-tag)](#additional-permission-set-decomposition)
26
- - [Loyalty Program Setup](#loyalty-program-setup-decomposition)
27
22
  - [Supported Metadata](#supported-metadata)
28
- - [Exceptions](#exceptions)
29
23
  - [Troubleshooting](#troubleshooting)
30
24
  - [Hooks](#hooks)
31
25
  - [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
32
- - [splitTags grammar](#splittags-grammar)
33
- - [multiLevel grammar](#multilevel-grammar)
34
26
  - [Ignore Files](#ignore-files)
35
- - [.forceignore](#forceignore)
36
- - [.sfdecomposerignore](#sfdecomposerignore)
37
- - [.gitignore](#gitignore)
38
27
  - [Issues](#issues)
39
- - [Requirements](#requirements)
40
28
  - [Built With](#built-with)
41
29
  - [Contributing](#contributing)
42
30
  - [License](#license)
@@ -74,14 +62,7 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
74
62
  sf project deploy start
75
63
  ```
76
64
 
77
- Or scope the recompose to just the components in your deploy manifest:
78
-
79
- ```bash
80
- sf decomposer recompose -x "manifest/package.xml"
81
- sf project deploy start -x "manifest/package.xml"
82
- ```
83
-
84
- Or run the deploy command directly after configuring the [hooks](#hooks) to run the recompose automatically before deploying.
65
+ Pass `-x manifest/package.xml` to both commands to scope the run to the components in a deploy manifest. Configuring the [hooks](#hooks) automates this step entirely.
85
66
 
86
67
  ---
87
68
 
@@ -101,21 +82,14 @@ If other platforms or architectures require support, please open an issue in [co
101
82
 
102
83
  ## Why sf-decomposer?
103
84
 
104
- Salesforces built-in decomposition is limited. sf-decomposer gives admins and developers more control, flexibility, and better versioning.
105
-
106
- ### Benefits
85
+ Salesforce's built-in decomposition is limited. sf-decomposer gives admins and developers more control, flexibility, and better versioning.
107
86
 
108
- - **Broader metadata support** Works with most Metadata API types, not just the subset Salesforce decomposes.
109
- - **Selective decomposition** Decompose only what you need; use [.sfdecomposerignore](#sfdecomposerignore) to skip specific files.
110
- - **Manifest-scoped runs** Pass `-x package.xml` to decompose or recompose only the components listed in a Salesforce manifest, mirroring `sf project deploy start -x`. Ideal for CI/CD pipelines that only ship a subset of metadata per deployment.
111
- - **Two [strategies](#decompose-strategies)**:
112
- - **unique-id** (default): one file per nested element, named by content or hash.
113
- - **grouped-by-tag**: one file per tag (e.g. all `fieldPermissions` in a permission set in `fieldPermissions.xml`). Use `--decompose-nested-permissions` for deeper permission set and muting permission set decomposition.
114
- - **Full decomposition** – Fully decompose types that Salesforce only partially supports (e.g. permission sets).
115
- - **Stable ordering** – Elements are sorted consistently to reduce noisy diffs.
116
- - **Multiple formats** – Output as XML, JSON, JSON5, or YAML.
117
- - **CI/CD hooks** – Auto decompose after retrieve and recompose before deploy via [.sfdecomposer.config.json](#hooks).
118
- - **Better reviews** – Smaller, structured files mean clearer pull requests and fewer merge conflicts.
87
+ - **Broader metadata support** works with most Metadata API types, not just the subset Salesforce decomposes.
88
+ - **Two [strategies](#decompose-strategies)** `unique-id` (one file per nested element) or `grouped-by-tag` (one file per tag).
89
+ - **Multiple formats** XML, JSON, JSON5, or YAML.
90
+ - **Manifest-scoped runs** — pass `-x package.xml` to scope a run to just the components in a deploy manifest, the same way `sf project deploy start -x` does.
91
+ - **CI/CD hooks** auto-decompose after retrieve and auto-recompose before deploy via [.sfdecomposer.config.json](#hooks).
92
+ - **Stable ordering and smaller files** clearer pull requests, fewer merge conflicts.
119
93
 
120
94
  ---
121
95
 
@@ -238,22 +212,18 @@ sf decomposer verify -m "permissionset" -s "grouped-by-tag" -p
238
212
  sf decomposer verify -x "manifest/package.xml" --config
239
213
  ```
240
214
 
241
- Files where the **only** delta is sibling or attribute ordering are surfaced separately as informational notices ("Note: N file(s) round-tripped semantically but with sibling/attribute reordering") rather than as drift. This is safe — Salesforce treats metadata as order-agnostic and `config-disassembler` does not preserve original sibling order but it tells you up front that committing the post-recompose output will produce a diff in git even though the metadata is functionally identical.
215
+ Files whose **only** delta is sibling or attribute ordering are reported as informational notices, not drift. Salesforce treats metadata as order-agnostic, so the deploy is safethe notice just warns that committing the post-recompose output will show a git diff even though the metadata is functionally identical.
242
216
 
243
217
  ---
244
218
 
245
219
  ## Manifest-scoped runs
246
220
 
247
- The `-x` / `--manifest` flag is supported by every `sf decomposer` command (`decompose`, `recompose`, `verify`) and accepts any standard Salesforce `package.xml`, limiting the work to just the components it lists. This is especially useful for CI/CD pipelines that deploy a subset of metadata per change.
221
+ `-x` / `--manifest` is supported by every `sf decomposer` command and accepts the same `package.xml` you pass to `sf project deploy start -x`. Only the listed components are decomposed/recomposed; everything else is left alone.
248
222
 
249
- How it works:
250
-
251
- - The manifest is parsed with `@salesforce/source-deploy-retrieve`'s `ManifestResolver`, so the same XML you pass to `sf project deploy start -x` is honored here.
252
- - For each entry, the plugin resolves the matching parent metadata files in your local package directories (using each metadata type's `directoryName`, `suffix`, `strictDirectoryName`, and `folderType` from the SDR registry).
253
- - Only those files are decomposed/recomposed; everything else on disk is left untouched.
254
- - Wildcards (`<members>*</members>`) expand against your local source. Folder-typed members (e.g. `MyFolder/MyReport`) are resolved by walking the folder.
255
- - Types in the manifest that the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning instead of failing the run, so a single manifest can drive both deploys and decomposer runs.
256
- - If both `--metadata-type` and `--manifest` are provided, the run is scoped to the intersection (only types present in both).
223
+ - Wildcards (`<members>*</members>`) expand against your local source.
224
+ - Folder members (e.g. `MyFolder/MyReport`) resolve by walking the folder.
225
+ - Types the plugin does not support (e.g. `CustomObject`, `ApexClass`) are skipped with a warning, so the same manifest can drive both deploys and decomposer runs.
226
+ - If both `--metadata-type` and `--manifest` are supplied, the run is scoped to the intersection.
257
227
 
258
228
  Example manifest:
259
229
 
@@ -329,9 +299,18 @@ permissionsets/
329
299
  └── userPermissions.xml
330
300
  ```
331
301
 
302
+ ### Filename safety (unique-id)
303
+
304
+ Two safety nets apply automatically to every shard filename emitted by the **unique-id** strategy. Neither requires configuration:
305
+
306
+ - **Path-segment sanitization (silent).** Characters illegal or reserved on at least one supported filesystem — path separators (`/`, `\`), Windows-reserved chars (`:`, `*`, `?`, `"`, `<`, `>`, `|`), and ASCII control bytes — are replaced with `_`; trailing `.` and spaces are stripped. Sanitized filenames are byte-stable across platforms.
307
+ - **Sibling-collision fallback (emits `WARN`).** When two or more siblings of the same parent tag would resolve to the same filename (the configured unique-id elements are too narrow, or sanitization folded two distinct values together), every sibling in the colliding group is written to its own per-element SHA-256 shard instead. No row is silently overwritten.
308
+
309
+ If you see a hash-named shard and want to know whether it came from a collision (vs. simply a missing UID), set `RUST_LOG=warn` and rerun — see [Rust crate logging](#xml-disassemble-output-rust-crate).
310
+
332
311
  ### Custom Labels Decomposition
333
312
 
334
- Custom labels use only the **unique-id** strategy. If you pass `grouped-by-tag`, the plugin overrides to `unique-id` and continues. Grouping labels by tag would produce no difference from the original file since all elements share the same tag. Each label is written to its own file.
313
+ Custom labels are always decomposed with `unique-id` (grouped-by-tag would be a no-op since every element shares the same tag). Each label is written to its own file:
335
314
 
336
315
  ```
337
316
  labels/
@@ -373,12 +352,9 @@ permissionsets/
373
352
 
374
353
  ### Loyalty Program Setup Decomposition
375
354
 
376
- `loyaltyProgramSetup` supports only the **unique-id** strategy. If you pass `grouped-by-tag`, the plugin overrides to `unique-id` and continues. The metadata is automatically decomposed further under unique-id:
377
-
378
- - Each `<programProcesses>` element → its own file.
379
- - Each `<parameters>` and `<rules>` child → its own file.
355
+ `loyaltyProgramSetup` is always decomposed with `unique-id`, with a built-in `multiLevel` default that splits `<programProcesses>` into per-process folders containing per-`<parameters>` / per-`<rules>` files.
380
356
 
381
- > Recomposition for loyalty program setup removes decomposed files even without `--postpurge`. Use version control or CI to keep them if needed.
357
+ > Recompose for `loyaltyProgramSetup` always removes the decomposed tree, with or without `--postpurge`. Rely on version control if you need to inspect it after a deploy.
382
358
 
383
359
  ```
384
360
  loyaltyProgramSetups/
@@ -461,32 +437,31 @@ For example, if you attempt to decompose Custom Labels but none of your package
461
437
 
462
438
  ### XML disassemble output (Rust crate)
463
439
 
464
- The config-disassembler Node plugin uses a **Rust crate** for XML decomposing and recomposing. Disassemble errors and messages are shown in the terminal.
440
+ The underlying Rust crate logs through [env_logger](https://docs.rs/env_logger). Set `RUST_LOG` to opt into more verbosity:
465
441
 
466
- Control verbosity with the `RUST_LOG` environment variable (e.g. `RUST_LOG=debug` for detailed output).
442
+ | Level | What it covers |
443
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
444
+ | `RUST_LOG=error` | Default. Parse errors and skipped files (leaf-only XML — primitives only, nothing to decompose). |
445
+ | `RUST_LOG=warn` | Adds [sibling-collision fallback](#filename-safety-unique-id) signals — one line per colliding group (parent tag, collided id, sibling count). **Recommended in CI** when shipping overrides. |
467
446
 
468
- Example output in the terminal (Rust log format):
447
+ Example `WARN` (CustomApplication where four `actionOverrides` siblings shared the action name `View`):
469
448
 
470
449
  ```
471
- [2026-04-30T12:34:38Z ERROR config_disassembler::xml::builders::build_disassembled_files] The XML file C:\Users\matthew.carvin\Documents\sf-decomposer\fixtures\package-dir-1\permissionsets\only_leafs.permissionset-meta.xml only has leaf elements. This file will not be disassembled.
450
+ [2026-05-04T15:21:09Z WARN config_disassembler::xml::builders::build_disassembled_files]
451
+ uniqueIdElements collision: <actionOverrides> id "View" matched 4 sibling elements;
452
+ falling back to SHA-256 content hashes for the colliding group.
453
+ Consider adding more discriminating fields to uniqueIdElements for this metadata type.
472
454
  ```
473
455
 
474
- ### Files with only leaf elements
475
-
476
- If a metadata file has only leaf elements (primitives, no nested structure), there is nothing to decompose. The Rust crate skips the file and logs an ERROR like the example above.
477
-
478
456
  ---
479
457
 
480
458
  ## Hooks
481
459
 
482
- > Configure [.forceignore](#forceignore) so the Salesforce CLI ignores decomposed files; otherwise `sf` commands can fail.
460
+ Put **.sfdecomposer.config.json** in the project root to auto-decompose after `sf project retrieve start` and auto-recompose before `sf project deploy start` / `validate`.
483
461
 
484
- Put **.sfdecomposer.config.json** in the project root to run:
462
+ > Configure [.forceignore](#forceignore) first the Salesforce CLI must ignore decomposed files or `sf` commands can fail.
485
463
 
486
- - **After** `sf project retrieve start`: decompose.
487
- - **Before** `sf project deploy start` / `sf project deploy validate`: recompose.
488
-
489
- Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample config with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy/etc. by metadata type or by individual component.
464
+ Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample with overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy per metadata type or component.
490
465
 
491
466
  | Option | Required | Description |
492
467
  | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -584,7 +559,7 @@ For each component, each option is resolved independently in this order (highest
584
559
 
585
560
  Each `<tag>` may appear at most once in a spec. The plugin validates the grammar at config-load time. Deeper checks (e.g. unknown tag names for the metadata type) are surfaced by the underlying disassembler crate at runtime.
586
561
 
587
- #### splitTags cookbook
562
+ **Examples:**
588
563
 
589
564
  ```json
590
565
  "overrides": [
@@ -597,21 +572,13 @@ Each `<tag>` may appear at most once in a spec. The plugin validates the grammar
597
572
  "metadataTypes": ["profile"],
598
573
  "strategy": "grouped-by-tag",
599
574
  "splitTags": "objectPermissions:split:object,fieldPermissions:group:field,layoutAssignments:group:layout"
600
- },
601
- {
602
- "metadataTypes": ["flow"],
603
- "strategy": "grouped-by-tag",
604
- "splitTags": "actionCalls:split:name,decisions:split:name,assignments:split:name"
605
- },
606
- {
607
- "metadataTypes": ["workflow"],
608
- "strategy": "grouped-by-tag",
609
- "splitTags": "rules:split:fullName,alerts:split:fullName,fieldUpdates:split:fullName,tasks:split:fullName"
610
575
  }
611
576
  ]
612
577
  ```
613
578
 
614
- > **Caveat:** When using `mode: split`, the chosen `<field>` must produce a unique value for every array item — otherwise two items would map to the same filename. If two items share a field value, prefer `mode: group` instead, which is designed for that case.
579
+ > **Caveat:** With `mode: split`, the chosen `<field>` must produce a unique value across every array item — otherwise two items map to the same filename. If items can share a field value, use `mode: group` instead.
580
+
581
+ See the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for additional `splitTags` and `multiLevel` recipes (flows, workflows, layouts, flexipages, bots).
615
582
 
616
583
  ### multiLevel grammar
617
584
 
@@ -651,13 +618,9 @@ Within one scope, the `(file_pattern, root_to_strip)` pair must be unique across
651
618
  ]
652
619
  ```
653
620
 
654
- > **`bot` and `loyaltyProgramSetup` ship with built-in `multiLevel` defaults**, so you don't need to add an override for either type to get the canonical decomposed layout — supply your own only if you want to replace the default. The full registry lives in [`src/metadata/multiLevelDefaults.ts`](https://github.com/mcarvin8/sf-decomposer/blob/main/src/metadata/multiLevelDefaults.ts).
655
-
656
- > **Why one call:** Pass every rule for a given component in a single override. Sequential single-rule decompositions rewrite the on-disk `.multi_level.json` and only the last rule survives — so multi-rule scenarios must travel together.
657
-
658
- > **Tip:** Use [`sf decomposer verify`](#sf-decomposer-verify) to non-destructively confirm a new override config still round-trips before committing it.
659
-
660
- > **Tip:** See the [admin handbook](https://github.com/mcarvin8/sf-decomposer/blob/main/HANDBOOK.md) for end-to-end recipes for Bots, Flexipages, Layouts, and other deeply-nested metadata.
621
+ > **Built-in defaults.** `bot` and `loyaltyProgramSetup` ship with built-in `multiLevel` rules, so you do not need an override to get the canonical layout — supply your own only to replace the default. Full registry: [`src/metadata/multiLevelDefaults.ts`](https://github.com/mcarvin8/sf-decomposer/blob/main/src/metadata/multiLevelDefaults.ts).
622
+ >
623
+ > **Pass all rules at once.** Sequential single-rule decomposes rewrite `.multi_level.json` and only the last rule survives — bundle every rule for a given component into one override. Use [`sf decomposer verify`](#sf-decomposer-verify) to confirm a new config round-trips before committing it.
661
624
 
662
625
  ### Opting in from the CLI
663
626
 
@@ -62,6 +62,15 @@ declare const _default: {
62
62
  reportType: {
63
63
  uniqueIdElements: string[];
64
64
  };
65
+ serviceChannel: {
66
+ uniqueIdElements: string[];
67
+ };
68
+ genAiPlugin: {
69
+ uniqueIdElements: string[];
70
+ };
71
+ app: {
72
+ uniqueIdElements: string[];
73
+ };
65
74
  mutingpermissionset: {
66
75
  uniqueIdElements: string[];
67
76
  };
@@ -144,9 +144,53 @@ export default [
144
144
  uniqueIdElements: ['sobjectType'],
145
145
  },
146
146
  reportType: {
147
- // `<sections>` items use `<masterLabel>` as their natural key. Same
147
+ // `<sections>` items use `<masterLabel>` as their natural key — same
148
148
  // pattern as `globalValueSetTranslation`/`standardValueSetTranslation`.
149
- uniqueIdElements: ['masterLabel'],
149
+ // The singleton `<join>` element is keyed by `<relationship>` purely for
150
+ // readability: without it every reportType that joins a child object
151
+ // produces a hash-named shard.
152
+ uniqueIdElements: ['masterLabel', 'relationship'],
153
+ },
154
+ serviceChannel: {
155
+ // Each `<serviceChannelStatusFieldMappings>` row carries `<type>` (a
156
+ // status category like `COMPLETED` / `IN_PROGRESS`) and `<value>` (the
157
+ // human-readable status name). `<type>` collides massively (most rows
158
+ // are `COMPLETED`) and `<value>` alone is usually unique within a
159
+ // channel, but we observed status names repeated across distinct types
160
+ // in production data — the compound `type+value` is the only fully
161
+ // stable id, with `value` as a single-field fallback for any row that
162
+ // happens to lack `<type>`.
163
+ uniqueIdElements: ['type+value', 'value'],
164
+ },
165
+ genAiPlugin: {
166
+ // `<genAiFunctions>` items are keyed by `<functionName>`;
167
+ // `<genAiPluginInstructions>` items by `<developerName>`. Each
168
+ // repeating child only carries one of these fields, so first-match
169
+ // wins picks the right one without a compound.
170
+ uniqueIdElements: ['functionName', 'developerName'],
171
+ },
172
+ app: {
173
+ // CustomApplication's `<profileActionOverrides>` and `<actionOverrides>`
174
+ // have a *compound* natural unique key: any single field (e.g. just
175
+ // `<actionName>`) collides for hundreds of siblings sharing
176
+ // `<actionName>View</actionName>`, silently merging on disassembly.
177
+ // Compound keys (config-disassembler >= 0.4.5) join the resolved values
178
+ // with `__` to form a stable, readable, collision-free filename.
179
+ //
180
+ // Fallback chain, widest first:
181
+ // 1. profileActionOverrides with recordType
182
+ // 2. profileActionOverrides without recordType
183
+ // 3. actionOverrides with recordType (no profile)
184
+ // 4. actionOverrides without recordType (no profile)
185
+ // Items missing `pageOrSobjectType` or `formFactor` (very rare) fall
186
+ // through to the SHA-256 outer-element hash, which is correct since
187
+ // we can't safely name them without those keys.
188
+ uniqueIdElements: [
189
+ 'actionName+pageOrSobjectType+formFactor+profile+recordType',
190
+ 'actionName+pageOrSobjectType+formFactor+profile',
191
+ 'actionName+pageOrSobjectType+formFactor+recordType',
192
+ 'actionName+pageOrSobjectType+formFactor',
193
+ ],
150
194
  },
151
195
  mutingpermissionset: {
152
196
  uniqueIdElements: [
@@ -1 +1 @@
1
- {"version":3,"file":"uniqueIdElements.js","sourceRoot":"","sources":["../../src/metadata/uniqueIdElements.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"uniqueIdElements.js","sourceRoot":"","sources":["../../src/metadata/uniqueIdElements.ts"],"names":[],"mappings":"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"}
@@ -342,5 +342,5 @@
342
342
  ]
343
343
  }
344
344
  },
345
- "version": "6.17.0"
345
+ "version": "6.19.0"
346
346
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "sf-decomposer",
3
3
  "description": "Split large Salesforce metadata files into version-control-friendly pieces and rebuild deployment-ready files.",
4
- "version": "6.17.0",
4
+ "version": "6.19.0",
5
5
  "dependencies": {
6
6
  "@oclif/core": "^4",
7
7
  "@salesforce/core": "^8.26.3",
8
8
  "@salesforce/sf-plugins-core": "^12.2.6",
9
9
  "@salesforce/source-deploy-retrieve": "^12.35.0",
10
- "config-disassembler": "^1.1.3",
10
+ "config-disassembler": "^1.3.0",
11
11
  "fast-xml-parser": "^5.7.2",
12
12
  "p-limit": "^7.3.0"
13
13
  },
@@ -95,6 +95,9 @@
95
95
  "test:only": "wireit",
96
96
  "test:perf": "vitest run --config ./vitest.perf.config.ts",
97
97
  "test:perf:gen": "node --import ts-node/esm scripts/gen-perf-fixtures.ts",
98
+ "audit": "node --loader ts-node/esm --no-warnings=ExperimentalWarning scripts/audit/audit.ts",
99
+ "audit:sweep": "node --loader ts-node/esm --no-warnings=ExperimentalWarning scripts/audit/sweep.ts",
100
+ "audit:roundtrip": "node --loader ts-node/esm --no-warnings=ExperimentalWarning scripts/audit/roundtrip.ts",
98
101
  "version": "oclif readme"
99
102
  },
100
103
  "publishConfig": {