moose-inventory 2.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +6 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +638 -9
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +1 -1
  8. data/README.md +315 -39
  9. data/Rakefile +2 -0
  10. data/bin/moose-inventory +2 -1
  11. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  12. data/docs/compatibility/cli-output-compatibility.md +76 -0
  13. data/docs/governance/approval-register.md +37 -0
  14. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  15. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  16. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  17. data/docs/product/product-brief.md +161 -0
  18. data/docs/product/requirements-baseline.md +477 -0
  19. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  20. data/docs/release/package-provenance-hardening.md +126 -0
  21. data/docs/release/publishing.md +11 -3
  22. data/docs/release/release-environment-protection.md +78 -0
  23. data/docs/release/release-readiness.md +23 -4
  24. data/docs/security/accepted-risk-register.md +84 -0
  25. data/docs/security/security-privacy-process.md +287 -0
  26. data/docs/security-audit-2026-05-26-rerun.md +2 -2
  27. data/docs/security-audit-2026-05-29-snapshot-import-fuzz.md +58 -0
  28. data/docs/ux/cli-workflow-notes.md +287 -0
  29. data/examples/ansible/ansible.cfg +3 -0
  30. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  31. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  32. data/examples/ci/README.md +16 -0
  33. data/examples/ci/github-actions/inventory-review.yml +38 -0
  34. data/examples/ci/inventory/example-snapshot.yml +19 -0
  35. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  36. data/lib/moose_inventory/cli/application.rb +135 -5
  37. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  38. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  39. data/lib/moose_inventory/cli/audit.rb +62 -0
  40. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  41. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  42. data/lib/moose_inventory/cli/console.rb +135 -0
  43. data/lib/moose_inventory/cli/db.rb +64 -0
  44. data/lib/moose_inventory/cli/factory.rb +28 -0
  45. data/lib/moose_inventory/cli/formatter.rb +8 -12
  46. data/lib/moose_inventory/cli/group.rb +5 -2
  47. data/lib/moose_inventory/cli/group_add.rb +11 -9
  48. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  49. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  50. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  51. data/lib/moose_inventory/cli/group_get.rb +8 -42
  52. data/lib/moose_inventory/cli/group_list.rb +7 -40
  53. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  54. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  55. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  56. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  57. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  58. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  59. data/lib/moose_inventory/cli/helpers.rb +68 -1
  60. data/lib/moose_inventory/cli/host.rb +6 -3
  61. data/lib/moose_inventory/cli/host_add.rb +69 -29
  62. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  63. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  64. data/lib/moose_inventory/cli/host_get.rb +9 -37
  65. data/lib/moose_inventory/cli/host_list.rb +24 -21
  66. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  67. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  68. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  69. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  70. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  71. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  72. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  73. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  74. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  75. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  76. data/lib/moose_inventory/config/config.rb +185 -108
  77. data/lib/moose_inventory/db/db.rb +170 -195
  78. data/lib/moose_inventory/db/exceptions.rb +6 -3
  79. data/lib/moose_inventory/db/models.rb +16 -0
  80. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  81. data/lib/moose_inventory/inventory_context.rb +68 -2
  82. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  83. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  84. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  85. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  86. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  87. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  88. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  89. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  90. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +174 -0
  95. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  96. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  97. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  98. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  99. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  100. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  101. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  102. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  103. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  104. data/lib/moose_inventory/runtime_options.rb +31 -0
  105. data/lib/moose_inventory/version.rb +3 -1
  106. data/lib/moose_inventory.rb +10 -7
  107. data/moose-inventory.gemspec +19 -35
  108. data/scripts/check.sh +1 -0
  109. data/scripts/ci/check_generated_artifacts.sh +41 -0
  110. data/scripts/ci/check_permissions.sh +2 -0
  111. data/scripts/ci/check_rubocop.sh +30 -25
  112. data/scripts/ci/check_security.sh +4 -1
  113. data/scripts/files.rb +5 -4
  114. data/spec/examples/ci_examples_spec.rb +37 -0
  115. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  116. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  117. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +132 -0
  118. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  119. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  120. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  121. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  122. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  123. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  124. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  125. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  126. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  127. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  128. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  129. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  130. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  131. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  132. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  133. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  134. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  135. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  136. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  137. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  138. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  139. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  140. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  141. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  142. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  143. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  144. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  145. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  146. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  147. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  148. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  149. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  150. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  151. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  152. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  153. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  154. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  155. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  156. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  157. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  158. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  159. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +239 -0
  160. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  161. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  162. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  163. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  164. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  165. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  166. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  167. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  168. data/spec/shared/shared_config_setup.rb +4 -3
  169. data/spec/spec_helper.rb +50 -40
  170. data/spec/support/cli_harness.rb +33 -0
  171. metadata +81 -41
@@ -0,0 +1,287 @@
1
+ # Moose Inventory CLI UX and Workflow Notes
2
+
3
+ ## Approval status
4
+
5
+ Status: **Approved CLI UX/workflow baseline**
6
+
7
+ Approval reference: `GOV-UX-001` in `docs/governance/approval-register.md` approves this document as the CLI UX/workflow baseline for Moose Inventory.
8
+
9
+ Approved references:
10
+
11
+ - `GOV-TAILOR-001`: Moose Inventory is approved as Class 4 with target profile Software Library / Package.
12
+ - `GOV-PRODUCT-001`: `docs/product/product-brief.md` is approved as the product-framing baseline.
13
+ - `GOV-REQ-001`: `docs/product/requirements-baseline.md` is approved as the requirements and acceptance criteria baseline.
14
+
15
+ Scope limit: this approval covers command-line workflows and interaction conventions only. It is not architecture approval, security/privacy design approval, release approval, accepted-risk approval, public/compliance-claim approval, RubyGems publishing approval, or implementation approval for the UX follow-up backlog items.
16
+
17
+ ## UX posture
18
+
19
+ Moose Inventory is a command-line tool and RubyGem, so its UX baseline is not wireframes or visual mockups. The appropriate UX artifact is a workflow and interaction convention baseline for CLI users, automation users, reviewers, and maintainers.
20
+
21
+ Primary UX priorities:
22
+
23
+ 1. Make inventory-changing actions explicit, reviewable, and hard to mistake for read-only actions.
24
+ 2. Preserve existing command names, output shape, and wording where users or tests may depend on them.
25
+ 3. Keep machine-readable output parseable and stable enough for automation.
26
+ 4. Give clear, actionable error messages before any write occurs when inputs/options are invalid.
27
+ 5. Keep safety controls visible: transactions, dry-run, plan output, validation, doctor checks, and audit history.
28
+
29
+ ## User personas and interaction modes
30
+
31
+ ### CLI operator
32
+
33
+ A human operator uses `moose-inventory` directly to inspect and change inventory state.
34
+
35
+ UX needs:
36
+
37
+ - discoverable help
38
+ - predictable command families
39
+ - readable progress output
40
+ - clear success/failure states
41
+ - safe review before mutation
42
+
43
+ ### Automation/review consumer
44
+
45
+ A script, CI job, or reviewer consumes machine-readable output from list/get/doctor/export/dry-run plan commands.
46
+
47
+ UX needs:
48
+
49
+ - parseable YAML/JSON/pjson
50
+ - non-zero status for failed checks/findings where documented
51
+ - stable keys and event structure
52
+ - no secret leakage in examples/output
53
+
54
+ ### Maintainer/release reviewer
55
+
56
+ A maintainer checks behavior, documentation, release evidence, and compatibility.
57
+
58
+ UX needs:
59
+
60
+ - regression tests for output contracts
61
+ - README examples matching supported behavior
62
+ - clear approval boundaries for breaking changes
63
+ - process evidence distinguishing checks from approval
64
+
65
+ ## Core workflows
66
+
67
+ ### 1. Discover commands
68
+
69
+ Workflow:
70
+
71
+ 1. User runs top-level or nested help.
72
+ 2. CLI displays available commands/options.
73
+ 3. User chooses a command family such as `host`, `group`, `db`, `audit`, `doctor`, `import`, or `export`.
74
+
75
+ UX expectations:
76
+
77
+ - Help must be available at top-level and command-family levels.
78
+ - Help examples should be accurate enough to copy/adapt.
79
+ - New command families should follow existing README/help style.
80
+
81
+ ### 2. Select configuration and environment
82
+
83
+ Workflow:
84
+
85
+ 1. User relies on config discovery or passes `--config <FILE>`.
86
+ 2. User relies on `general.defaultenv` or passes `--env <SECTION>`.
87
+ 3. CLI initializes the configured database context.
88
+
89
+ UX expectations:
90
+
91
+ - Missing config, missing environment, and invalid DB config errors should fail before mutation.
92
+ - Error messages should name the missing/invalid element when practical.
93
+ - Docs should steer users toward `password_env` rather than plaintext `password`.
94
+
95
+ ### 3. Inspect inventory
96
+
97
+ Workflow:
98
+
99
+ 1. User runs list/get commands for hosts/groups/variables/tags.
100
+ 2. User optionally passes `--format yaml|json|pjson`.
101
+ 3. CLI returns current state without mutation.
102
+
103
+ UX expectations:
104
+
105
+ - Read-only commands should not create, repair, or migrate data implicitly unless explicitly documented.
106
+ - Machine-readable formats should remain parseable and consistent.
107
+ - Empty results should be explicit rather than misleading.
108
+
109
+ ### 4. Mutate inventory safely
110
+
111
+ Workflow:
112
+
113
+ 1. User chooses a mutating command: add/remove hosts/groups, variables, tags, associations, or child relationships.
114
+ 2. User optionally previews with `--dry-run` or `--dry-run --plan-format`.
115
+ 3. CLI validates inputs/options before writes.
116
+ 4. Destructive removal commands require `--yes` before writing unless the user selected `--dry-run`.
117
+ 5. CLI applies changes transactionally when not dry-run.
118
+ 5. CLI reports progress, warnings, success/failure, and audit evidence where applicable.
119
+
120
+ UX expectations:
121
+
122
+ - Mutating commands must be visually distinguishable from read-only workflows through command naming, progress output, and documentation.
123
+ - Invalid option combinations, such as `--plan-format` without `--dry-run`, fail before mutation.
124
+ - Removal commands should fail closed without `--yes`, while still allowing dry-run preview without confirmation.
125
+ - Dry-run output must not imply changes were applied.
126
+ - Real mutation output should preserve existing output contracts unless a breaking change is approved.
127
+
128
+ ### 5. Review planned changes
129
+
130
+ Workflow:
131
+
132
+ 1. User runs a mutating command with `--dry-run`.
133
+ 2. CLI renders planned progress and ends with `Dry run complete. No changes applied.`
134
+ 3. User optionally requests `--plan-format yaml|json|pjson` for automation review.
135
+
136
+ UX expectations:
137
+
138
+ - Dry-run should use the same conceptual progress path as a real mutation.
139
+ - Dry-run should not write inventory, audit records, schema state, or automatic cleanup associations.
140
+ - Plan output should include ordered event types and payloads suitable for review tools.
141
+ - Human-readable dry-run output should remain easy to compare with real command output.
142
+
143
+ ### 6. Validate health
144
+
145
+ Workflow:
146
+
147
+ 1. User runs `moose-inventory doctor`.
148
+ 2. CLI performs read-only checks.
149
+ 3. CLI reports no issues or lists findings with severity/check identifiers.
150
+ 4. CLI exits non-zero when findings are present.
151
+
152
+ UX expectations:
153
+
154
+ - Findings should be actionable and identify affected subject when practical.
155
+ - Machine-readable doctor output should be suitable for CI gates.
156
+ - Doctor should not silently fix inventory; repairs should remain explicit user actions.
157
+
158
+ ### 7. Import/export snapshots
159
+
160
+ Workflow:
161
+
162
+ 1. User exports inventory for review, backup, migration, or automation.
163
+ 2. User previews a YAML/JSON snapshot with `import FILE --preview` or `--preview --preview-format yaml|json|pjson` when reviewing a proposed change.
164
+ 3. User imports a YAML/JSON snapshot.
165
+ 4. CLI validates before writing and applies additive/update-oriented changes only.
166
+
167
+ UX expectations:
168
+
169
+ - Export should be read-only.
170
+ - Import preview should be read-only and distinct from command-level dry-run planning.
171
+ - Preview output should show creates, variable updates, association additions, unchanged items, ignored existing records, and destructive-change count.
172
+ - Import validation failures should avoid partial writes.
173
+ - Additive import semantics must be documented clearly.
174
+ - Destructive sync/restore semantics must not be introduced without separate requirements, UX, recovery, and approval records.
175
+
176
+ ### 8. Review audit history
177
+
178
+ Workflow:
179
+
180
+ 1. User runs `audit list` with optional format/limit.
181
+ 2. CLI returns append-only evidence for successful mutating commands.
182
+
183
+ UX expectations:
184
+
185
+ - Audit output should help explain what changed, when, by whom/tooling, and against what target.
186
+ - Audit output is accountability/debug evidence, not rollback by itself.
187
+ - Dry-runs should not appear as mutation audit records.
188
+
189
+ ## Destructive and high-risk operations
190
+
191
+ High-risk CLI areas include:
192
+
193
+ - host/group removal
194
+ - recursive group deletion
195
+ - child-group cleanup with orphan deletion
196
+ - variable removal
197
+ - snapshot import into populated databases
198
+ - schema migration
199
+ - backup/restore-adjacent workflows
200
+ - future destructive snapshot sync/restore features, if ever approved
201
+
202
+ UX requirements for high-risk operations:
203
+
204
+ 1. The command name/options must make destructive intent visible.
205
+ 2. Documentation must describe mutation scope and notable cleanup behavior.
206
+ 3. `--dry-run` should be available for documented mutating workflows where supported.
207
+ 4. Invalid inputs/options must fail before writes.
208
+ 5. Real writes should be transactional where practical.
209
+ 6. Future destructive restore/sync behavior requires a separate UX design and approval record.
210
+
211
+ ## Error states and messaging
212
+
213
+ Expected error behavior:
214
+
215
+ - fail early before mutation when arguments/options/config are invalid
216
+ - name the invalid option/value where practical
217
+ - distinguish usage errors from database/runtime failures
218
+ - preserve existing exact error strings where tests or downstream users rely on them
219
+ - avoid exposing secrets in errors
220
+
221
+ Known UX improvement area:
222
+
223
+ - Read-only console parsing now uses shell-style quoting and command-specific validation for the current browsing commands. Future console changes should preserve the read-only boundary unless mutation flows add confirmation, dry-run, and audit behavior.
224
+
225
+ ## Accessibility and readability expectations
226
+
227
+ For a CLI, accessibility/readability means:
228
+
229
+ - plain text that works in ordinary terminals
230
+ - no required color-only signal
231
+ - stable indentation for progress output
232
+ - concise severity/check IDs for doctor findings
233
+ - machine-readable alternatives for automation and assistive tooling
234
+ - examples that can be copied without hidden state or secrets
235
+ - readable failure messages rather than stack traces for ordinary user errors
236
+
237
+ ## Machine-readable output conventions
238
+
239
+ Machine-readable output is automation-facing UX and is governed by `CLI-OUTPUT-v1` in `docs/compatibility/cli-output-compatibility.md`.
240
+
241
+ Expectations:
242
+
243
+ - JSON/YAML/pjson structures should remain parseable.
244
+ - Event/check keys should not be renamed casually.
245
+ - New fields should prefer additive compatibility.
246
+ - Breaking output changes require approval and migration/release notes.
247
+ - Pretty JSON is for humans reviewing structured data; normal JSON/YAML are for scripts and CI.
248
+
249
+ ## Compatibility conventions
250
+
251
+ Human-readable output is also a versioned compatibility surface under `CLI-OUTPUT-v1` when tests, docs, or scripts rely on exact wording.
252
+
253
+ Expectations:
254
+
255
+ - Preserve existing wording/newline behavior during refactors unless an intentional change is approved.
256
+ - Tests should cover known legacy output contracts, especially around warnings and cleanup behavior.
257
+ - README examples should not promise output that the CLI no longer emits.
258
+ - Breaking human-readable output changes need backlog/approval evidence and release notes just like machine-readable breaking changes.
259
+
260
+ ## UX acceptance checklist
261
+
262
+ A new or changed CLI workflow is UX-ready when:
263
+
264
+ - The command path and option names are consistent with existing command families.
265
+ - Read vs write behavior is obvious from command naming and docs.
266
+ - Invalid inputs/options fail before mutation.
267
+ - Mutating behavior is transactional where practical.
268
+ - Dry-run/plan behavior exists where required by the requirements baseline.
269
+ - Human-readable output is clear and compatible unless an approved breaking change exists.
270
+ - Machine-readable output is parseable and compatibility-reviewed.
271
+ - README/help examples are updated where user-facing behavior changed.
272
+ - Tests cover success, failure, and safety boundaries.
273
+ - Any unresolved UX risk is recorded as a backlog item or accepted risk.
274
+
275
+ ## UX decisions recorded from review
276
+
277
+ These decisions were provided by Russ during review on 2026-05-28. They are captured here as product/UX direction for future implementation, but this draft UX baseline still requires explicit approval through `GOV-UX-001` before it becomes approved UX evidence.
278
+
279
+ 1. Destructive commands should eventually require explicit confirmation unless `--yes` or an equivalent non-interactive acknowledgement is provided.
280
+ 2. Human-readable output compatibility is formally versioned through `CLI-OUTPUT-v1`, alongside machine-readable output compatibility.
281
+ 3. Read-only console support for quoted names and richer validation through `Shellwords.split` should be prioritized before adding more CLI features.
282
+ 4. Audit history should remain evidence only; future rollback/change-set UX should not be introduced through audit history.
283
+ 5. Snapshot import should eventually offer a formal preview/diff mode distinct from command-level dry-run planning, but this is future work and does not block the current UX baseline.
284
+
285
+ ## Open UX questions
286
+
287
+ No open UX questions remain in this draft. Future questions should be added here only when they are not already represented as a decision, backlog item, or accepted scope limit.
@@ -0,0 +1,3 @@
1
+ [defaults]
2
+ inventory = inventory/moose_inventory.yml
3
+ inventory_plugins = inventory_plugins
@@ -0,0 +1,5 @@
1
+ ---
2
+ plugin: moose_inventory
3
+ executable: moose-inventory
4
+ config: ./example.conf
5
+ env: dev
@@ -0,0 +1,100 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright: (c) Russell Davies
3
+ # MIT License
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import subprocess
9
+
10
+ from ansible.errors import AnsibleParserError
11
+ from ansible.plugins.inventory import BaseInventoryPlugin
12
+
13
+ DOCUMENTATION = r'''
14
+ name: moose_inventory
15
+ plugin_type: inventory
16
+ short_description: Moose Inventory plugin
17
+ description:
18
+ - Loads inventory from the C(moose-inventory) command line tool.
19
+ - Keeps Moose Inventory configuration and environment selection in YAML instead of a shell shim.
20
+ options:
21
+ plugin:
22
+ description: Token that ensures this is a source file for this plugin.
23
+ required: true
24
+ choices: ['moose_inventory']
25
+ executable:
26
+ description: Moose Inventory executable path.
27
+ required: false
28
+ default: moose-inventory
29
+ config:
30
+ description: Moose Inventory config file passed to C(--config).
31
+ required: false
32
+ env:
33
+ description: Moose Inventory environment section passed to C(--env).
34
+ required: false
35
+ '''
36
+
37
+ EXAMPLES = r'''
38
+ plugin: moose_inventory
39
+ executable: moose-inventory
40
+ config: ./example.conf
41
+ env: dev
42
+ '''
43
+
44
+
45
+ class InventoryModule(BaseInventoryPlugin):
46
+ NAME = 'moose_inventory'
47
+
48
+ def verify_file(self, path):
49
+ return super().verify_file(path) and path.endswith(('moose_inventory.yml', 'moose_inventory.yaml'))
50
+
51
+ def parse(self, inventory, loader, path, cache=True):
52
+ super().parse(inventory, loader, path, cache=cache)
53
+ config = self._read_config_data(path)
54
+ executable = config.get('executable', 'moose-inventory')
55
+ moose_config = config.get('config')
56
+ env = config.get('env')
57
+
58
+ groups = self._run_moose(executable, moose_config, env, ['--ansible', 'group', 'list'])
59
+ hosts = self._run_moose(executable, moose_config, env, ['host', 'list'])
60
+
61
+ self._apply_groups(groups)
62
+ self._apply_hosts(hosts)
63
+
64
+ def _run_moose(self, executable, config, env, args):
65
+ command = [executable]
66
+ if config:
67
+ command.extend(['--config', config])
68
+ if env:
69
+ command.extend(['--env', env])
70
+ command.extend(args)
71
+
72
+ try:
73
+ completed = subprocess.run(command, check=True, capture_output=True, text=True)
74
+ except (OSError, subprocess.CalledProcessError) as error:
75
+ raise AnsibleParserError('moose-inventory command failed: %s' % error) from error
76
+
77
+ try:
78
+ return json.loads(completed.stdout or '{}')
79
+ except json.JSONDecodeError as error:
80
+ raise AnsibleParserError('moose-inventory returned invalid JSON: %s' % error) from error
81
+
82
+ def _apply_groups(self, groups):
83
+ for group_name, payload in groups.items():
84
+ self.inventory.add_group(group_name)
85
+ for host_name in payload.get('hosts', []):
86
+ self.inventory.add_host(host_name, group=group_name)
87
+ for child_name in payload.get('children', []):
88
+ self.inventory.add_group(child_name)
89
+ self.inventory.add_child(group_name, child_name)
90
+ for key, value in payload.get('vars', {}).items():
91
+ self.inventory.set_variable(group_name, key, value)
92
+
93
+ def _apply_hosts(self, hosts):
94
+ for host_name, payload in hosts.items():
95
+ self.inventory.add_host(host_name)
96
+ for group_name in payload.get('groups', []):
97
+ self.inventory.add_group(group_name)
98
+ self.inventory.add_host(host_name, group=group_name)
99
+ for key, value in payload.get('hostvars', {}).items():
100
+ self.inventory.set_variable(host_name, key, value)
@@ -0,0 +1,16 @@
1
+ # Moose Inventory CI/CD Examples
2
+
3
+ These examples show how to validate an inventory snapshot in CI without using production database credentials.
4
+
5
+ - `inventory/example-snapshot.yml` is a small review snapshot fixture.
6
+ - `scripts/validate-inventory-snapshot.sh` imports a snapshot into a temporary SQLite database, runs `doctor`, exports a canonical snapshot, lists hosts, and produces an Ansible-compatible inventory artifact.
7
+ - `github-actions/inventory-review.yml` is a copy/paste GitHub Actions workflow example. It is intentionally stored under `examples/` rather than `.github/workflows/` so projects can adapt it before enabling it.
8
+
9
+ The example writes artifacts to `tmp/inventory-ci-artifacts` by default:
10
+
11
+ - `doctor.txt`
12
+ - `inventory.yml`
13
+ - `hosts.json`
14
+ - `ansible-inventory.json`
15
+
16
+ Use this pattern for pull-request review gates before applying inventory changes to a shared or production Moose Inventory database.
@@ -0,0 +1,38 @@
1
+ name: Inventory review example
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ paths:
7
+ - 'inventory/**'
8
+ - 'examples/ci/inventory/**'
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ validate-inventory:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Check out repository
18
+ uses: actions/checkout@v5
19
+
20
+ - name: Set up Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: '3.4'
24
+ bundler-cache: true
25
+
26
+ - name: Validate proposed inventory snapshot
27
+ env:
28
+ MOOSE_INVENTORY_CMD: bundle exec ruby -Ilib bin/moose-inventory
29
+ run: |
30
+ examples/ci/scripts/validate-inventory-snapshot.sh \
31
+ examples/ci/inventory/example-snapshot.yml \
32
+ tmp/inventory-ci-artifacts
33
+
34
+ - name: Upload inventory review artifacts
35
+ uses: actions/upload-artifact@v4
36
+ with:
37
+ name: inventory-review
38
+ path: tmp/inventory-ci-artifacts
@@ -0,0 +1,19 @@
1
+ ---
2
+ version: 1
3
+ hosts:
4
+ web01:
5
+ groups:
6
+ - web
7
+ tags:
8
+ - prod
9
+ - public-edge
10
+ vars:
11
+ os: fedora
12
+ owner: platform
13
+ groups:
14
+ web:
15
+ children: []
16
+ tags:
17
+ - frontend
18
+ vars:
19
+ role: frontend
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ snapshot="${1:-examples/ci/inventory/example-snapshot.yml}"
5
+ artifact_dir="${2:-tmp/inventory-ci-artifacts}"
6
+ moose_cmd="${MOOSE_INVENTORY_CMD:-moose-inventory}"
7
+ work_dir="$(mktemp -d)"
8
+ trap 'rm -rf "$work_dir"' EXIT
9
+
10
+ mkdir -p "$artifact_dir"
11
+ config_file="$work_dir/moose-inventory-ci.yml"
12
+ db_file="$work_dir/inventory.db"
13
+
14
+ cat > "$config_file" <<YAML
15
+ ---
16
+ general:
17
+ defaultenv: ci
18
+ ci:
19
+ db:
20
+ adapter: sqlite3
21
+ file: "$db_file"
22
+ YAML
23
+
24
+ $moose_cmd --config "$config_file" --env ci import "$snapshot"
25
+ $moose_cmd --config "$config_file" --env ci doctor > "$artifact_dir/doctor.txt"
26
+ $moose_cmd --config "$config_file" --env ci --format yaml export "$artifact_dir/inventory.yml"
27
+ $moose_cmd --config "$config_file" --env ci --format pjson host list > "$artifact_dir/hosts.json"
28
+ $moose_cmd --config "$config_file" --env ci --ansible group list > "$artifact_dir/ansible-inventory.json"
29
+
30
+ echo "Inventory CI artifacts written to $artifact_dir"
@@ -1,20 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
1
4
  require 'thor'
2
- require_relative '../version.rb'
3
- require_relative '../config/config.rb'
4
- require_relative './group.rb'
5
- require_relative './host.rb'
5
+ require_relative '../version'
6
+ require 'yaml'
7
+
8
+ require_relative '../config/config'
9
+ require_relative '../operations/import_inventory_snapshot'
10
+ require_relative '../operations/inventory_doctor'
11
+ require_relative '../operations/inventory_snapshot'
12
+ require_relative 'formatter'
13
+ require_relative 'helpers'
14
+ require_relative 'audit'
15
+ require_relative 'console'
16
+ require_relative 'db'
17
+ require_relative 'group'
18
+ require_relative 'host'
6
19
 
7
20
  module Moose
8
21
  module Inventory
9
22
  module Cli
10
23
  ##
11
- # TODO: Documentation
24
+ # Top-level Thor application for moose-inventory.
25
+ # rubocop:disable Metrics/ClassLength
12
26
  class Application < Thor
27
+ include Moose::Inventory::Cli::Helpers
28
+
13
29
  desc 'version', 'Get the code version'
14
30
  def version
15
31
  puts "Version #{Moose::Inventory::VERSION}"
16
32
  end
17
33
 
34
+ desc 'doctor', 'Run inventory health checks'
35
+ option :format, type: :string, desc: 'Emit doctor report as yaml|json|pjson'
36
+ def doctor
37
+ report = build_operation(Moose::Inventory::Operations::InventoryDoctor).call
38
+ render_doctor_report(report)
39
+ exit(1) unless report[:ok]
40
+ end
41
+
42
+ desc 'export [FILE]', 'Export a canonical inventory snapshot'
43
+ def export(file = nil)
44
+ snapshot = build_operation(Moose::Inventory::Operations::InventorySnapshot).export
45
+ output = serialize_snapshot(snapshot)
46
+
47
+ if file.nil?
48
+ puts output
49
+ else
50
+ File.write(file, output)
51
+ puts "Exported inventory snapshot to #{file}."
52
+ end
53
+ end
54
+
55
+ desc 'import FILE', 'Import and validate an inventory snapshot'
56
+ option :preview, type: :boolean, desc: 'Preview snapshot import changes without writing'
57
+ option :preview_format, type: :string, desc: 'Emit preview as yaml|json|pjson'
58
+ def import(file)
59
+ snapshot = YAML.safe_load_file(file, aliases: false)
60
+ operation = build_operation(Moose::Inventory::Operations::ImportInventorySnapshot)
61
+ return render_import_preview(operation.preview(snapshot: snapshot)) if options[:preview]
62
+
63
+ abort('ERROR: --preview-format requires --preview.') if options[:preview_format]
64
+
65
+ result = operation.call(snapshot: snapshot)
66
+ record_audit({ command: 'import', action: 'import', entity_type: 'inventory',
67
+ entity_names: file }, result: result)
68
+ puts "Imported inventory snapshot from #{file}."
69
+ puts "Created hosts: #{result.created_hosts}"
70
+ puts "Created groups: #{result.created_groups}"
71
+ puts "Variables changed: #{result.updated_variables}"
72
+ puts "Associations added: #{result.associations}"
73
+ rescue Psych::SyntaxError => e
74
+ abort("ERROR: Could not parse inventory snapshot '#{file}': #{e.message}")
75
+ rescue Psych::Exception => e
76
+ abort("ERROR: Could not load inventory snapshot '#{file}': #{e.message}")
77
+ rescue db.exceptions[:moose] => e
78
+ abort("ERROR: #{e.message}")
79
+ end
80
+
81
+ map 'db' => :database
82
+ desc 'audit ACTION', 'Inspect append-only inventory change history'
83
+ subcommand 'audit', Moose::Inventory::Cli::Audit
84
+
85
+ desc 'database ACTION', 'Inspect and manage database lifecycle state'
86
+ subcommand 'database', Moose::Inventory::Cli::Db
87
+
88
+ desc 'console', 'Open a small read-only inventory browsing console'
89
+ def console
90
+ Moose::Inventory::Cli::Console.new(context: inventory_context).run
91
+ end
92
+
18
93
  desc 'group ACTION',
19
94
  'Manipulate groups in the inventory. ' \
20
95
  'ACTION can be add, rm, get, list, addhost, rmhost, addchild, rmchild, addvar, rmvar'
@@ -24,7 +99,62 @@ module Moose
24
99
  'Manipulate hosts in the inventory. ' \
25
100
  'ACTION can be add, rm, get, list, addgroup, rmgroup, addvar, rmvar'
26
101
  subcommand 'host', Moose::Inventory::Cli::Host
102
+
103
+ private
104
+
105
+ def render_doctor_report(report)
106
+ if options[:format]
107
+ puts serialize_data(report, options[:format].downcase)
108
+ else
109
+ render_human_doctor_report(report)
110
+ end
111
+ end
112
+
113
+ def render_human_doctor_report(report)
114
+ if report[:ok]
115
+ puts 'Inventory doctor found no issues.'
116
+ return
117
+ end
118
+
119
+ puts "Inventory doctor found #{report[:issue_count]} issue(s):"
120
+ report[:issues].each do |entry|
121
+ puts "- [#{entry[:severity]}] #{entry[:id]}: #{entry[:message]}"
122
+ end
123
+ end
124
+
125
+ def render_import_preview(preview)
126
+ if options[:preview_format]
127
+ puts serialize_data(preview, options[:preview_format].downcase)
128
+ else
129
+ render_human_import_preview(preview)
130
+ end
131
+ end
132
+
133
+ def render_human_import_preview(preview)
134
+ puts 'Snapshot import preview. No changes applied.'
135
+ preview.fetch('summary').each do |key, value|
136
+ puts "#{key.tr('_', ' ').capitalize}: #{value}"
137
+ end
138
+ end
139
+
140
+ def serialize_snapshot(snapshot)
141
+ serialize_data(snapshot, output_format)
142
+ end
143
+
144
+ def serialize_data(data, format)
145
+ case format
146
+ when 'yaml', 'y'
147
+ data.to_yaml
148
+ when 'prettyjson', 'pjson', 'p'
149
+ JSON.pretty_generate(data)
150
+ when 'json', 'j'
151
+ data.to_json
152
+ else
153
+ abort("Output format '#{format}' is not yet supported.")
154
+ end
155
+ end
27
156
  end
157
+ # rubocop:enable Metrics/ClassLength
28
158
  end
29
159
  end
30
160
  end