sf-decomposer 6.12.0 → 6.14.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,23 @@
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.14.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.13.0...v6.14.0) (2026-04-30)
9
+
10
+
11
+ ### Features
12
+
13
+ * add multiLevel override and round-trip verify command ([#418](https://github.com/mcarvin8/sf-decomposer/issues/418)) ([5f06265](https://github.com/mcarvin8/sf-decomposer/commit/5f06265333e9a0523154d0c745e83e0571938f98))
14
+ * **decompose:** expose splitTags as an override field ([#416](https://github.com/mcarvin8/sf-decomposer/issues/416)) ([eedf0f2](https://github.com/mcarvin8/sf-decomposer/commit/eedf0f23d248fdd028a097435aa9493449269954))
15
+
16
+ ## [6.13.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.12.0...v6.13.0) (2026-04-30)
17
+
18
+
19
+ ### Features
20
+
21
+ * **decompose:** add per-component overrides ([#415](https://github.com/mcarvin8/sf-decomposer/issues/415)) ([47d7c79](https://github.com/mcarvin8/sf-decomposer/commit/47d7c794db17ccc59e832309a0a4980d736ab89c))
22
+ * **deps:** bump dependabot/fetch-metadata from 2 to 3 ([#412](https://github.com/mcarvin8/sf-decomposer/issues/412)) ([6b0a24b](https://github.com/mcarvin8/sf-decomposer/commit/6b0a24b9c83dfb6fb9ef1d8dc8e2dfbec23200bf))
23
+ * **metadata:** bump @salesforce/source-deploy-retrieve ([#413](https://github.com/mcarvin8/sf-decomposer/issues/413)) ([e261049](https://github.com/mcarvin8/sf-decomposer/commit/e261049cc07a94642c1e80cf29dbd1e96a7183bd))
24
+
8
25
  ## [6.12.0](https://github.com/mcarvin8/sf-decomposer/compare/v6.11.0...v6.12.0) (2026-04-27)
9
26
 
10
27
 
package/README.md CHANGED
@@ -17,7 +17,8 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
17
17
  - [Commands](#commands)
18
18
  - [sf decomposer decompose](#sf-decomposer-decompose)
19
19
  - [sf decomposer recompose](#sf-decomposer-recompose)
20
- - [Manifest-scoped runs](#manifest-scoped-runs)
20
+ - [sf decomposer verify](#sf-decomposer-verify)
21
+ - [Manifest-scoped runs](#manifest-scoped-runs)
21
22
  - [Decompose Strategies](#decompose-strategies)
22
23
  - [Custom Labels](#custom-labels-decomposition)
23
24
  - [Permission Sets (grouped-by-tag)](#additional-permission-set-decomposition)
@@ -26,7 +27,9 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
26
27
  - [Exceptions](#exceptions)
27
28
  - [Troubleshooting](#troubleshooting)
28
29
  - [Hooks](#hooks)
29
- - [Per-Type Overrides](#per-type-overrides)
30
+ - [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)
31
+ - [splitTags grammar](#splittags-grammar)
32
+ - [multiLevel grammar](#multilevel-grammar)
30
33
  - [Ignore Files](#ignore-files)
31
34
  - [.forceignore](#forceignore)
32
35
  - [.sfdecomposerignore](#sfdecomposerignore)
@@ -79,7 +82,7 @@ A Salesforce CLI plugin that **decomposes** large metadata XML files into smalle
79
82
 
80
83
  ## Requirements
81
84
 
82
- The [xml-disassembler-node](https://github.com/mcarvin8/xml-disassembler-node) package, which depends on a Rust crate, ships with native binaries for these platforms:
85
+ The [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) package, which depends on a Rust crate, ships with native binaries for these platforms:
83
86
 
84
87
  | Platform | Architectures |
85
88
  | ----------- | ---------------------------------- |
@@ -87,7 +90,7 @@ The [xml-disassembler-node](https://github.com/mcarvin8/xml-disassembler-node) p
87
90
  | **Linux** | x64, arm64, ia32 |
88
91
  | **Windows** | x64 |
89
92
 
90
- If other platforms or architectures require support, please open an issue in [xml-disassembler-node](https://github.com/mcarvin8/xml-disassembler-node/issues).
93
+ If other platforms or architectures require support, please open an issue in [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node/issues).
91
94
 
92
95
  ---
93
96
 
@@ -113,10 +116,11 @@ Salesforce’s built-in decomposition is limited. sf-decomposer gives admins and
113
116
 
114
117
  ## Commands
115
118
 
116
- | Command | Description |
117
- | ------------------------- | --------------------------------------------------------------- |
118
- | `sf decomposer decompose` | Decompose metadata in package directories into smaller files. |
119
- | `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata. |
119
+ | Command | Description |
120
+ | ------------------------- | ----------------------------------------------------------------------------------- |
121
+ | `sf decomposer decompose` | Decompose metadata in package directories into smaller files. |
122
+ | `sf decomposer recompose` | Recompose decomposed files back into deployment-ready metadata. |
123
+ | `sf decomposer verify` | Round-trip check: decompose + recompose in a temp directory and diff the originals. |
120
124
 
121
125
  ### sf decomposer decompose
122
126
 
@@ -135,7 +139,7 @@ FLAGS
135
139
  --prepurge Remove existing decomposed files before decomposing [default: false]
136
140
  --postpurge Remove original metadata files after decomposing [default: false]
137
141
  -p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions
138
- -c, --config Load per-metadata-type overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. See Per-Type Overrides. [default: false]
142
+ -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root. Only the "overrides" array is consumed. See Per-Type & Per-Component Overrides. [default: false]
139
143
 
140
144
  GLOBAL FLAGS
141
145
  --json Output as JSON.
@@ -193,9 +197,49 @@ sf decomposer recompose -x "manifest/package.xml"
193
197
  sf project deploy start -x "manifest/package.xml"
194
198
  ```
195
199
 
196
- ### Manifest-scoped runs
200
+ ### sf decomposer verify
197
201
 
198
- The `-x` / `--manifest` flag accepts any standard Salesforce `package.xml` and limits the work to just the components it lists. This is especially useful for CI/CD pipelines that deploy a subset of metadata per change.
202
+ Non-destructive round-trip check: copies your package directories into a temp directory under your OS's `tmpdir()`, runs decompose then recompose there, and diffs the rebuilt parents against the originals using **structural XML equality** (sibling and attribute order are ignored). Exits non-zero on any drift; your working tree is never modified.
203
+
204
+ ```
205
+ USAGE
206
+ $ sf decomposer verify [-m <value>] [-x <value>] [-f <value>] [-i <value>] [-s <value>] [-p -c --json]
207
+
208
+ FLAGS
209
+ -m, --metadata-type=<value> Metadata suffix to verify (e.g. flow, labels). Repeatable. Optional when --manifest is provided.
210
+ -x, --manifest=<value> Path to a package.xml manifest. When provided, only the components listed in the manifest are verified.
211
+ -f, --format=<value> Output format used for the round-trip decompose: xml | yaml | json | json5 [default: xml]
212
+ -i, --ignore-package-directory=<value> Package directory to skip. Repeatable.
213
+ -s, --strategy=<value> unique-id | grouped-by-tag [default: unique-id]
214
+ -p, --decompose-nested-permissions With grouped-by-tag, further decompose permission set and muting permission set object/field permissions.
215
+ -c, --config Load per-type and per-component overrides from .sfdecomposer.config.json in the repo root, the same as `decompose --config`. [default: false]
216
+
217
+ GLOBAL FLAGS
218
+ --json Output as JSON.
219
+ ```
220
+
221
+ > At least one of `--metadata-type` or `--manifest` is required. When both are supplied, the run is scoped to the intersection of the two.
222
+
223
+ **Examples**
224
+
225
+ ```bash
226
+ # Verify two metadata types round-trip cleanly with defaults
227
+ sf decomposer verify -m "permissionset" -m "profile"
228
+
229
+ # Verify a different strategy + nested-perms split before committing the change
230
+ sf decomposer verify -m "permissionset" -s "grouped-by-tag" -p
231
+
232
+ # CI gate: verify just the components in a deploy manifest, using the repo-root config
233
+ sf decomposer verify -x "manifest/package.xml" --config
234
+ ```
235
+
236
+ 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.
237
+
238
+ ---
239
+
240
+ ## Manifest-scoped runs
241
+
242
+ 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.
199
243
 
200
244
  How it works:
201
245
 
@@ -232,7 +276,7 @@ sf project deploy start -x "manifest/package.xml"
232
276
 
233
277
  ## Decompose Strategies
234
278
 
235
- > **Important:** Use one strategy per metadata type. To switch from `unique-id` to `grouped-by-tag`, run decompose with `--prepurge` and `-s "grouped-by-tag"` to regenerate.
279
+ > **Tip:** A single decompose run can mix strategies and formats across metadata types — and even across components within the same type through the `overrides` array (see [Per-Type & Per-Component Overrides](#per-type--per-component-overrides)). Recompose is deterministic from the on-disk sidecar, so any combination round-trips. When switching strategies for an existing component, pass `--prepurge` (or set `prePurge: true`) so leftover files from the previous strategy are removed before the new ones are written.
236
280
 
237
281
  - **unique-id** (default): Each nested element goes to its own file, named by unique-id fields or content hash. Leaf elements stay in a file named like the original XML.
238
282
  - **grouped-by-tag**: All elements with the same tag (e.g. `<fieldPermissions>`) go into one file named after the tag (e.g. `fieldPermissions.xml`). Leaf elements are still grouped in the original-named file.
@@ -334,14 +378,14 @@ For example, if you attempt to decompose Custom Labels but none of your package
334
378
 
335
379
  ### XML disassemble output (Rust crate)
336
380
 
337
- The xml-disassembler Node plugin uses a **Rust crate** for XML decomposing and recomposing. Disassemble errors and messages are shown in the terminal.
381
+ The config-disassembler Node plugin uses a **Rust crate** for XML decomposing and recomposing. Disassemble errors and messages are shown in the terminal.
338
382
 
339
383
  Control verbosity with the `RUST_LOG` environment variable (e.g. `RUST_LOG=debug` for detailed output).
340
384
 
341
385
  Example output in the terminal (Rust log format):
342
386
 
343
387
  ```
344
- [2026-02-11T22:52:32Z ERROR xml_disassembler::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.
388
+ [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.
345
389
  ```
346
390
 
347
391
  ### Files with only leaf elements
@@ -359,27 +403,27 @@ Put **.sfdecomposer.config.json** in the project root to run:
359
403
  - **After** `sf project retrieve start`: decompose.
360
404
  - **Before** `sf project deploy start` / `sf project deploy validate`: recompose.
361
405
 
362
- Copy and customize the [sample config](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.json), or the [sample config with per-type overrides](https://raw.githubusercontent.com/mcarvin8/sf-decomposer/main/examples/.sfdecomposer.config.overrides.json) to vary format/strategy/etc. by metadata type.
406
+ 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.
363
407
 
364
- | Option | Required | Description |
365
- | ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
366
- | `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, the run is scoped to the intersection. |
367
- | `manifest` | Conditional | Path (relative to the project root) to a `package.xml` manifest. When set, only the components listed in the manifest are decomposed/recomposed. See `-x` above. |
368
- | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
369
- | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
370
- | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
371
- | `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
372
- | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
373
- | `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
374
- | `overrides` | No | Array of per-metadata-type overrides for `decomposedFormat`, `strategy`, `decomposeNestedPermissions`, `prePurge`, and `postPurge`. See [Per-Type Overrides](#per-type-overrides). |
408
+ | Option | Required | Description |
409
+ | ---------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
410
+ | `metadataSuffixes` | Conditional | Comma-separated metadata suffixes to decompose/recompose. Required unless `manifest` is set; when both are set, the run is scoped to the intersection. |
411
+ | `manifest` | Conditional | Path (relative to the project root) to a `package.xml` manifest. When set, only the components listed in the manifest are decomposed/recomposed. See `-x` above. |
412
+ | `ignorePackageDirectories` | No | Comma-separated package directories to skip. |
413
+ | `prePurge` | No | Remove existing decomposed files before decomposing (default: false). |
414
+ | `postPurge` | No | After decompose: remove originals; after recompose: remove decomposed files (default: false). |
415
+ | `decomposedFormat` | No | xml, json, json5, or yaml (default: xml). |
416
+ | `strategy` | No | `unique-id` \| `grouped-by-tag` (default: unique-id). |
417
+ | `decomposeNestedPermissions` | No | With grouped-by-tag, set true to further decompose permission set and muting permission set object/field permissions. |
418
+ | `overrides` | No | Array of per-type and/or per-component overrides for `decomposedFormat`, `strategy`, `decomposeNestedPermissions`, `prePurge`, and `postPurge`. See [Per-Type & Per-Component Overrides](#per-type--per-component-overrides). |
375
419
 
376
420
  ---
377
421
 
378
- ## Per-Type Overrides
422
+ ## Per-Type & Per-Component Overrides
379
423
 
380
- Per-type overrides apply to **decompose only**. Recompose is a deterministic round-trip — it auto-detects format from the on-disk files and does not depend on strategy — so it ignores the `overrides` array.
424
+ Overrides apply to **decompose only**. Recompose is a deterministic round-trip — it auto-detects format from the on-disk files and does not depend on strategy — so it ignores the `overrides` array.
381
425
 
382
- By default, a single decompose run uses one format and one strategy across every metadata type. The optional `overrides` array in `.sfdecomposer.config.json` lets you vary a small set of options per metadata suffix without splitting the run into multiple invocations.
426
+ By default, a single decompose run uses one format and one strategy across every metadata type. The optional `overrides` array in `.sfdecomposer.config.json` lets you vary a small set of options per metadata suffix (**type-scope**) or per individual SDR component (**component-scope**) without splitting the run into multiple invocations.
383
427
 
384
428
  ```json
385
429
  {
@@ -395,6 +439,11 @@ By default, a single decompose run uses one format and one strategy across every
395
439
  "metadataTypes": ["permissionset", "mutingpermissionset"],
396
440
  "strategy": "grouped-by-tag",
397
441
  "decomposeNestedPermissions": true
442
+ },
443
+ {
444
+ "components": ["permissionset:HR_Admin", "permissionset:Big_PermSet"],
445
+ "strategy": "grouped-by-tag",
446
+ "decomposeNestedPermissions": true
398
447
  }
399
448
  ]
400
449
  }
@@ -402,24 +451,113 @@ By default, a single decompose run uses one format and one strategy across every
402
451
 
403
452
  ### What can be overridden
404
453
 
405
- | Field | Notes |
406
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
407
- | `metadataTypes` | Required. Array of metadata suffixes (same vocabulary as `--metadata-type` / `metadataSuffixes`). Each suffix may appear in at most one override. |
408
- | `decomposedFormat` | `xml` \| `json` \| `json5` \| `yaml`. |
409
- | `strategy` | `unique-id` \| `grouped-by-tag`. Hard rules still win — `labels` and `loyaltyProgramSetup` are always treated as `unique-id`. |
410
- | `decomposeNestedPermissions` | Only applies to `permissionset` / `mutingpermissionset` with `grouped-by-tag`. |
411
- | `prePurge` | Per-type prePurge (decompose). |
412
- | `postPurge` | Per-type postPurge (decompose: remove originals after decomposing). |
454
+ | Field | Notes |
455
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
456
+ | `metadataTypes` | Optional (required if `components` is omitted). Array of metadata suffixes (same vocabulary as `--metadata-type` / `metadataSuffixes`). Each suffix may appear in at most one override. |
457
+ | `components` | Optional (required if `metadataTypes` is omitted). Array of `<metadataSuffix>:<fullName>` keys (e.g. `permissionset:HR_Admin`, `report:MyFolder/MyReport`). Each component may appear in at most one override. |
458
+ | `decomposedFormat` | `xml` \| `json` \| `json5` \| `yaml`. |
459
+ | `strategy` | `unique-id` \| `grouped-by-tag`. Hard rules still win — `labels` and `loyaltyProgramSetup` are always treated as `unique-id`. |
460
+ | `decomposeNestedPermissions` | Only applies to `permissionset` / `mutingpermissionset` with `grouped-by-tag`. Sets a known-good `splitTags` default; ignored if `splitTags` is also set in the same scope. |
461
+ | `splitTags` | Custom `splitTags` spec for `grouped-by-tag` strategy. See [splitTags grammar](#splittags-grammar). Ignored when the resolved strategy is not `grouped-by-tag`. |
462
+ | `multiLevel` | Custom `multiLevel` spec for nested-array decomposition. See [multiLevel grammar](#multilevel-grammar). When set, replaces the hardcoded `loyaltyProgramSetup` default for the targeted scope. |
463
+ | `prePurge` | Per-scope prePurge (decompose). Component-scope `prePurge` only purges the named component's decomposed directory. |
464
+ | `postPurge` | Per-scope postPurge (decompose: remove originals after decomposing). |
413
465
 
414
466
  Run-scope options (`metadataSuffixes`, `manifest`, `ignorePackageDirectories`) are **not** valid inside an override; the plugin will throw if they are present.
415
467
 
468
+ #### Component key conventions
469
+
470
+ The `<fullName>` part of a component key is the SDR fullName for the component, matching the basename of the decomposed directory:
471
+
472
+ - **Plain types** (e.g. `permissionset`, `flow`, `profile`, `workflow`): use the file stem, e.g. `permissionset:HR_Admin` for `permissionsets/HR_Admin.permissionset-meta.xml`.
473
+ - **Strict-directory types** (e.g. `bot`): use the bot directory name, e.g. `bot:My_Bot` for `bots/My_Bot/My_Bot.bot-meta.xml`.
474
+ - **Folder-typed metadata** (e.g. `report`, `dashboard`, `email`, `document`): the unit of decomposition is the folder; use the folder name, e.g. `report:MyFolder` to scope every report inside `reports/MyFolder/`.
475
+ - **`labels`**: there is exactly one labels file per labels directory, so component-scope keys are not meaningful — use the type-scope `metadataTypes: ["labels"]` instead.
476
+
477
+ Component overrides are not a filter. If `--metadata` / `metadataSuffixes` includes `permissionset`, every permission set is still decomposed; the override only changes how the named ones are decomposed. Use `--manifest` / the hook's `manifest` field if you want to scope the run itself to a subset of components.
478
+
416
479
  ### Precedence
417
480
 
418
- For each metadata type, the effective value is resolved as:
481
+ For each component, each option is resolved independently in this order (highest first):
482
+
483
+ 1. The component-scope override value (matching `<suffix>:<fullName>` in `components`), if set.
484
+ 2. The type-scope override value (matching `<suffix>` in `metadataTypes`), if set.
485
+ 3. The run-wide value (CLI flag, hook config top-level field, or built-in default).
486
+ 4. Hard plugin rules (e.g. `labels` and `loyaltyProgramSetup` forced to `unique-id`) override all of the above.
487
+
488
+ ### splitTags grammar
489
+
490
+ `splitTags` lets you control how `grouped-by-tag` writes nested arrays for any metadata type. The plugin already applies a known-good default for permission sets when `decomposeNestedPermissions: true` is set; setting `splitTags` directly takes precedence and works for any metadata type.
491
+
492
+ **Spec:** Comma-separated rules. Each rule has 3 or 4 colon-separated parts:
493
+
494
+ - `<tag>:<mode>:<field>` — read array items from the top-level `<tag>`.
495
+ - `<tag>:<path>:<mode>:<field>` — read array items from the nested `<path>` (defaults to `<tag>`).
496
+
497
+ `<mode>` is one of:
498
+
499
+ - **`split`** — write one file per array item, named after the value of `<field>` on each item.
500
+ - **`group`** — group array items by the value of `<field>`, writing one file per group.
501
+
502
+ 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.
503
+
504
+ #### splitTags cookbook
505
+
506
+ ```json
507
+ "overrides": [
508
+ {
509
+ "metadataTypes": ["permissionset", "mutingpermissionset"],
510
+ "strategy": "grouped-by-tag",
511
+ "splitTags": "objectPermissions:split:object,fieldPermissions:group:field"
512
+ },
513
+ {
514
+ "metadataTypes": ["profile"],
515
+ "strategy": "grouped-by-tag",
516
+ "splitTags": "objectPermissions:split:object,fieldPermissions:group:field,layoutAssignments:group:layout"
517
+ },
518
+ {
519
+ "metadataTypes": ["flow"],
520
+ "strategy": "grouped-by-tag",
521
+ "splitTags": "actionCalls:split:name,decisions:split:name,assignments:split:name"
522
+ },
523
+ {
524
+ "metadataTypes": ["workflow"],
525
+ "strategy": "grouped-by-tag",
526
+ "splitTags": "rules:split:fullName,alerts:split:fullName,fieldUpdates:split:fullName,tasks:split:fullName"
527
+ }
528
+ ]
529
+ ```
530
+
531
+ > **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.
532
+
533
+ ### multiLevel grammar
534
+
535
+ `multiLevel` enables a second decomposition pass on inner-level files for metadata types whose XML has deeply nested repeatable blocks (e.g. `loyaltyProgramSetup`'s `programProcesses → parameters → ...`). The plugin already applies a known-good default for `loyaltyProgramSetup` when running the `unique-id` strategy; setting `multiLevel` directly takes precedence and works for any metadata type.
536
+
537
+ **Spec:** A single rule with exactly 3 colon-separated parts (the third part is itself a comma-separated list):
538
+
539
+ ```
540
+ <file_pattern>:<root_to_strip>:<unique_id_elements>
541
+ ```
542
+
543
+ - **`<file_pattern>`** — basename pattern that selects which inner-level files get the second decomposition pass (e.g. `programProcesses`).
544
+ - **`<root_to_strip>`** — XML root tag to strip from each matched file before splitting.
545
+ - **`<unique_id_elements>`** — comma-separated list of element names used to derive a stable filename for each inner-level item (e.g. `parameterName,ruleName`). The first element that resolves to a non-empty value wins.
546
+
547
+ The plugin validates the grammar at config-load time; deeper checks (whether the file pattern matches anything, whether the unique-id elements actually appear on the inner XML) are surfaced by the underlying disassembler crate at runtime.
548
+
549
+ ```json
550
+ "overrides": [
551
+ {
552
+ "metadataTypes": ["loyaltyProgramSetup"],
553
+ "multiLevel": "programProcesses:programProcesses:parameterName,ruleName"
554
+ }
555
+ ]
556
+ ```
557
+
558
+ > **Limit:** Only one rule per scope. The disassembler does not currently accept multiple comma-separated multiLevel specs, because the third part of the rule is itself a comma-separated list.
419
559
 
420
- 1. The per-type override value, if set.
421
- 2. Otherwise, the run-wide value (CLI flag, hook config top-level field, or built-in default).
422
- 3. Hard plugin rules (e.g. labels → `unique-id`) still override both.
560
+ > **Tip:** Use [`sf decomposer verify`](#sf-decomposer-verify) to non-destructively confirm a new override config still round-trips before committing it.
423
561
 
424
562
  ### Opting in from the CLI
425
563
 
@@ -463,7 +601,7 @@ Bugs and feature requests: [open an issue](https://github.com/mcarvin8/sf-decomp
463
601
 
464
602
  ## Built With
465
603
 
466
- - [xml-disassembler](https://github.com/mcarvin8/xml-disassembler) – Disassemble XML into smaller, manageable files and reassemble when needed. Node.js + Rust (Neon). Includes prebuilt binaries for macOS (x64, arm64), Linux (x64, arm64, ia32), and Windows (x64).
604
+ - [config-disassembler-node](https://github.com/mcarvin8/config-disassembler-node) – Disassemble XML (and other config formats) into smaller, manageable files and reassemble when needed. Node.js + Rust (Neon). See [Requirements](#requirements).
467
605
  - [@salesforce/source-deploy-retrieve](https://github.com/forcedotcom/source-deploy-retrieve) – JavaScript toolkit for working with Salesforce metadata.
468
606
 
469
607
  ---
@@ -0,0 +1,17 @@
1
+ import { SfCommand } from '@salesforce/sf-plugins-core';
2
+ import { VerifyResult } from '../../helpers/types.js';
3
+ export default class DecomposerVerify extends SfCommand<VerifyResult> {
4
+ static readonly summary: string;
5
+ static readonly description: string;
6
+ static readonly examples: string[];
7
+ static readonly flags: {
8
+ 'metadata-type': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ manifest: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'ignore-package-directory': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ strategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'decompose-nested-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ config: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<VerifyResult>;
17
+ }
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
3
+ import { Messages } from '@salesforce/core';
4
+ import { DECOMPOSED_FILE_TYPES, DECOMPOSED_STRATEGIES } from '../../helpers/constants.js';
5
+ import { verifyMetadataTypes } from '../../core/verifyMetadataTypes.js';
6
+ import { loadOverridesFromConfig, resolveDefaultConfigPath } from '../../helpers/configOverrides.js';
7
+ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
8
+ const messages = Messages.loadMessages('sf-decomposer', 'decomposer.verify');
9
+ export default class DecomposerVerify extends SfCommand {
10
+ static summary = messages.getMessage('summary');
11
+ static description = messages.getMessage('description');
12
+ static examples = messages.getMessages('examples');
13
+ static flags = {
14
+ 'metadata-type': Flags.string({
15
+ summary: messages.getMessage('flags.metadata-type.summary'),
16
+ char: 'm',
17
+ multiple: true,
18
+ required: false,
19
+ }),
20
+ manifest: Flags.file({
21
+ summary: messages.getMessage('flags.manifest.summary'),
22
+ char: 'x',
23
+ required: false,
24
+ exists: true,
25
+ }),
26
+ format: Flags.string({
27
+ summary: messages.getMessage('flags.format.summary'),
28
+ char: 'f',
29
+ required: true,
30
+ multiple: false,
31
+ default: 'xml',
32
+ options: DECOMPOSED_FILE_TYPES,
33
+ }),
34
+ 'ignore-package-directory': Flags.directory({
35
+ summary: messages.getMessage('flags.ignore-package-directory.summary'),
36
+ char: 'i',
37
+ required: false,
38
+ multiple: true,
39
+ }),
40
+ strategy: Flags.string({
41
+ summary: messages.getMessage('flags.strategy.summary'),
42
+ char: 's',
43
+ required: true,
44
+ multiple: false,
45
+ default: 'unique-id',
46
+ options: DECOMPOSED_STRATEGIES,
47
+ }),
48
+ 'decompose-nested-permissions': Flags.boolean({
49
+ summary: messages.getMessage('flags.decompose-nested-permissions.summary'),
50
+ char: 'p',
51
+ required: false,
52
+ default: false,
53
+ }),
54
+ config: Flags.boolean({
55
+ summary: messages.getMessage('flags.config.summary'),
56
+ char: 'c',
57
+ required: false,
58
+ default: false,
59
+ }),
60
+ };
61
+ async run() {
62
+ const { flags } = await this.parse(DecomposerVerify);
63
+ if (!flags['metadata-type'] && !flags['manifest']) {
64
+ throw messages.createError('error.missingMetadataOrManifest');
65
+ }
66
+ const overrides = flags['config'] ? await loadOverridesFromConfig(await resolveDefaultConfigPath()) : undefined;
67
+ const result = await verifyMetadataTypes({
68
+ metadataTypes: flags['metadata-type'],
69
+ format: flags['format'],
70
+ ignoreDirs: flags['ignore-package-directory'],
71
+ strategy: flags['strategy'],
72
+ decomposeNestedPerms: flags['decompose-nested-permissions'],
73
+ manifest: flags['manifest'],
74
+ overrides,
75
+ log: this.log.bind(this),
76
+ });
77
+ if (result.drift.length > 0) {
78
+ throw messages.createError('error.driftDetected', [String(result.drift.length)]);
79
+ }
80
+ return result;
81
+ }
82
+ }
83
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/commands/decomposer/verify.ts"],"names":[],"mappings":"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"}
@@ -48,15 +48,12 @@ export async function decomposeMetadataTypes(options) {
48
48
  log(`Skipping ${metadataType}: ${message}`);
49
49
  return;
50
50
  }
51
- const resolved = resolveDecomposeOptionsForType(metadataType, { format, strategy, decomposeNestedPerms, prepurge, postpurge }, overrides);
52
- let effectiveStrategy = resolved.strategy;
53
- if (metadataType === 'labels' && effectiveStrategy === 'grouped-by-tag') {
54
- effectiveStrategy = 'unique-id';
55
- }
56
- if (metadataType === 'loyaltyProgramSetup' && effectiveStrategy === 'grouped-by-tag') {
57
- effectiveStrategy = 'unique-id';
58
- }
59
- await decomposeFileHandler(metaAttributes, resolved.prepurge, resolved.postpurge, resolved.format, ignorePath, effectiveStrategy, resolved.decomposeNestedPerms, manifestXmlPaths);
51
+ // Type-scope resolved options serve as the base for component-scope resolution further
52
+ // down the call stack. Hard strategy rules (labels / loyaltyProgramSetup) are applied per
53
+ // file inside the disassembler so they remain in force even when a component-scope override
54
+ // tries to flip the strategy.
55
+ const typeResolved = resolveDecomposeOptionsForType(metadataType, { format, strategy, decomposeNestedPerms, prepurge, postpurge }, overrides);
56
+ await decomposeFileHandler(metaAttributes, typeResolved, ignorePath, overrides, manifestXmlPaths);
60
57
  processed.push(metadataType);
61
58
  log(`All metadata files have been decomposed for the metadata type: ${metadataType}`);
62
59
  }));
@@ -1 +1 @@
1
- {"version":3,"file":"decomposeMetadataTypes.js","sourceRoot":"","sources":["../../src/core/decomposeMetadataTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"decomposeMetadataTypes.js","sourceRoot":"","sources":["../../src/core/decomposeMetadataTypes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { VerifyOptions, VerifyResult } from '../helpers/types.js';
2
+ /**
3
+ * Run a non-destructive round-trip check: copy the user's package directories into a scratch
4
+ * directory under the OS temp folder, decompose + recompose them there, and diff the rebuilt
5
+ * parents against the originals.
6
+ *
7
+ * The temp directory is always removed before this function returns, and the user's working tree
8
+ * is never modified. The returned `drift` array is empty when every parent XML survived the round
9
+ * trip byte-identically; otherwise each entry names the offending file (relative to its package
10
+ * directory) and a short reason.
11
+ */
12
+ export declare function verifyMetadataTypes(options: VerifyOptions): Promise<VerifyResult>;