releasehx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +2915 -0
  3. data/bin/releasehx +7 -0
  4. data/bin/rhx +7 -0
  5. data/bin/rhx-mcp +7 -0
  6. data/bin/sourcerer +32 -0
  7. data/build/docs/CNAME +1 -0
  8. data/build/docs/Gemfile.lock +95 -0
  9. data/build/docs/_config.yml +36 -0
  10. data/build/docs/config-reference.adoc +4104 -0
  11. data/build/docs/config-reference.json +1546 -0
  12. data/build/docs/index.adoc +2915 -0
  13. data/build/docs/landing.adoc +21 -0
  14. data/build/docs/manpage.adoc +68 -0
  15. data/build/docs/releasehx.1 +281 -0
  16. data/build/docs/releasehx_readme.html +367 -0
  17. data/build/docs/sample-config.adoc +9 -0
  18. data/build/docs/sample-config.yml +251 -0
  19. data/build/docs/schemagraphy_readme.html +0 -0
  20. data/build/docs/sourcerer_readme.html +46 -0
  21. data/build/snippets/helpscreen.txt +29 -0
  22. data/lib/docopslab/mcp/asset_packager.rb +30 -0
  23. data/lib/docopslab/mcp/manifest.rb +67 -0
  24. data/lib/docopslab/mcp/resource_pack.rb +46 -0
  25. data/lib/docopslab/mcp/server.rb +92 -0
  26. data/lib/docopslab/mcp.rb +6 -0
  27. data/lib/releasehx/cli.rb +937 -0
  28. data/lib/releasehx/configuration.rb +215 -0
  29. data/lib/releasehx/generated.rb +17 -0
  30. data/lib/releasehx/helpers.rb +58 -0
  31. data/lib/releasehx/mcp/asset_packager.rb +21 -0
  32. data/lib/releasehx/mcp/assets/agent-config-guide.md +178 -0
  33. data/lib/releasehx/mcp/assets/config-def.yml +1426 -0
  34. data/lib/releasehx/mcp/assets/config-reference.adoc +4104 -0
  35. data/lib/releasehx/mcp/assets/config-reference.json +1546 -0
  36. data/lib/releasehx/mcp/assets/sample-config.yml +251 -0
  37. data/lib/releasehx/mcp/manifest.rb +18 -0
  38. data/lib/releasehx/mcp/resource_pack.rb +26 -0
  39. data/lib/releasehx/mcp/server.rb +57 -0
  40. data/lib/releasehx/mcp.rb +7 -0
  41. data/lib/releasehx/ops/check_ops.rb +136 -0
  42. data/lib/releasehx/ops/draft_ops.rb +173 -0
  43. data/lib/releasehx/ops/enrich_ops.rb +221 -0
  44. data/lib/releasehx/ops/template_ops.rb +61 -0
  45. data/lib/releasehx/ops/write_ops.rb +124 -0
  46. data/lib/releasehx/rest/clients/github.yml +46 -0
  47. data/lib/releasehx/rest/clients/gitlab.yml +31 -0
  48. data/lib/releasehx/rest/clients/jira.yml +31 -0
  49. data/lib/releasehx/rest/yaml_client.rb +418 -0
  50. data/lib/releasehx/rhyml/adapter.rb +740 -0
  51. data/lib/releasehx/rhyml/change.rb +167 -0
  52. data/lib/releasehx/rhyml/liquid.rb +13 -0
  53. data/lib/releasehx/rhyml/loaders.rb +37 -0
  54. data/lib/releasehx/rhyml/mappings/github.yaml +60 -0
  55. data/lib/releasehx/rhyml/mappings/gitlab.yaml +73 -0
  56. data/lib/releasehx/rhyml/mappings/jira.yaml +29 -0
  57. data/lib/releasehx/rhyml/mappings/verb_past_tenses.yml +98 -0
  58. data/lib/releasehx/rhyml/release.rb +144 -0
  59. data/lib/releasehx/rhyml.rb +15 -0
  60. data/lib/releasehx/sgyml/helpers.rb +45 -0
  61. data/lib/releasehx/transforms/adf_to_markdown.rb +307 -0
  62. data/lib/releasehx/version.rb +7 -0
  63. data/lib/releasehx.rb +69 -0
  64. data/lib/schemagraphy/attribute_resolver.rb +48 -0
  65. data/lib/schemagraphy/cfgyml/definition.rb +90 -0
  66. data/lib/schemagraphy/cfgyml/doc_builder.rb +52 -0
  67. data/lib/schemagraphy/cfgyml/path_reference.rb +24 -0
  68. data/lib/schemagraphy/data_query/json_pointer.rb +42 -0
  69. data/lib/schemagraphy/loader.rb +59 -0
  70. data/lib/schemagraphy/regexp_utils.rb +215 -0
  71. data/lib/schemagraphy/safe_expression.rb +189 -0
  72. data/lib/schemagraphy/schema_utils.rb +124 -0
  73. data/lib/schemagraphy/tag_utils.rb +32 -0
  74. data/lib/schemagraphy/templating.rb +104 -0
  75. data/lib/schemagraphy.rb +17 -0
  76. data/lib/sourcerer/builder.rb +120 -0
  77. data/lib/sourcerer/jekyll/bootstrapper.rb +78 -0
  78. data/lib/sourcerer/jekyll/liquid/file_system.rb +74 -0
  79. data/lib/sourcerer/jekyll/liquid/filters.rb +215 -0
  80. data/lib/sourcerer/jekyll/liquid/tags.rb +44 -0
  81. data/lib/sourcerer/jekyll/monkeypatches.rb +73 -0
  82. data/lib/sourcerer/jekyll.rb +26 -0
  83. data/lib/sourcerer/plaintext_converter.rb +75 -0
  84. data/lib/sourcerer/templating.rb +190 -0
  85. data/lib/sourcerer.rb +322 -0
  86. data/specs/data/api-client-schema.yaml +160 -0
  87. data/specs/data/config-def.yml +1426 -0
  88. data/specs/data/mcp-manifest.yml +50 -0
  89. data/specs/data/rhyml-mapping-schema.yaml +410 -0
  90. data/specs/data/rhyml-schema.yaml +152 -0
  91. metadata +376 -0
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ module RHYML
5
+ # Represents a single Change within a Release, such as a bug fix, feature, or enhancement.
6
+ #
7
+ # The Change class encapsulates all metadata associated with an individual modification,
8
+ # including its classification (type), descriptive content (summary, notes),
9
+ # organizational data (tags, parts), and contributor information.
10
+ # Changes are always associated with a parent Release object.
11
+ class Change
12
+ attr_accessor :release, :version
13
+ attr_reader :vrsn, :chid, :tick, :hash, :type, :parts, :summ, :head, :note, :tags, :lead, :auths, :links
14
+
15
+ # Initializes a new Change object from attribute hash and parent Release.
16
+ #
17
+ # Processes the provided attributes to populate Change properties, handling
18
+ # multiple possible field names (e.g., 'summ', 'summary', 'title') and
19
+ # normalizing complex attributes like authors and links.
20
+ #
21
+ # @param attrs [Hash] A hash of attributes for the Change.
22
+ # @param release [Release] The required parent Release object.
23
+ # @raise [ArgumentError] If attrs is not a Hash or if both 'part' and 'parts' are provided.
24
+ def initialize attrs = {}, release:
25
+ raise ArgumentError, 'attrs must be a Hash' unless attrs.is_a? Hash
26
+
27
+ @release = release
28
+ @vrsn = @release.code
29
+ @chid = attrs['chid']
30
+ @tick = attrs_value(attrs, %w[tick ticketid])
31
+ @hash = attrs['hash']
32
+ @type = attrs['type']
33
+ @summ = attrs_value(attrs, %w[summ summary title])
34
+ @head = attrs['head']
35
+ @note = attrs['note']
36
+ @tags = attrs['tags'] || []
37
+ @lead = attrs_value(attrs, %w[lead contributor auth])
38
+ @auths = normalize_auths(attrs['auths'])
39
+ @links = normalize_links(attrs['links'])
40
+
41
+ # Handle 'part' vs 'parts'; mutually exclusive attributes
42
+ part = attrs['part']
43
+ parts = attrs['parts']
44
+ raise ArgumentError, "Change cannot have both 'part' and 'parts'" if part && parts
45
+
46
+ @parts = if parts
47
+ Array(parts).map(&:to_s)
48
+ elsif part
49
+ [part.to_s]
50
+ else
51
+ []
52
+ end
53
+
54
+ ReleaseHx.logger.debug "Initialized Change: #{@tick} – #{@summ}"
55
+ end
56
+
57
+ # Produces a comprehensive hash representation of the Change.
58
+ #
59
+ # Includes all public attributes plus computed boolean properties
60
+ # for common Change classifications (highlight, breaking, etc.).
61
+ #
62
+ # @return [Hash] A hash containing all public attributes of the Change.
63
+ def to_h
64
+ {
65
+ 'vrsn' => vrsn,
66
+ 'chid' => chid,
67
+ 'tick' => tick,
68
+ 'hash' => hash,
69
+ 'type' => type,
70
+ 'parts' => parts,
71
+ 'summ' => summ,
72
+ 'head' => head,
73
+ 'note' => note,
74
+ 'tags' => tags,
75
+ 'lead' => lead,
76
+ 'auths' => auths,
77
+ 'links' => links,
78
+ 'deprecation' => deprecation?,
79
+ 'removal' => removal?,
80
+ 'highlight' => highlight?,
81
+ 'breaking' => breaking?,
82
+ 'experimental' => experimental?
83
+ }
84
+ end
85
+
86
+ # @return [Boolean] True if the Change is tagged as a highlight.
87
+ def highlight? = tags.include?('highlight')
88
+ # @return [Boolean] True if the Change is a breaking change.
89
+ def breaking? = tags.include?('breaking')
90
+ # @return [Boolean] True if the Change is experimental.
91
+ def experimental? = tags.include?('experimental')
92
+ # @return [Boolean] True if the Change includes a deprecation.
93
+ def deprecation? = tags.include?('deprecation')
94
+ # @return [Boolean] True if the Change includes a removal.
95
+ def removal? = tags.include?('removal')
96
+
97
+ # Checks if a given tag is associated with the Change.
98
+ #
99
+ # Performs flexible tag matching, checking for the tag as provided,
100
+ # as a string, and as a symbol to handle different input types.
101
+ #
102
+ # @param tag_name [String, Symbol] The name of the tag to check.
103
+ # @return [Boolean] True if the tag exists in any form.
104
+ def tag? tag_name
105
+ tags.include?(tag_name) || tags.include?(tag_name.to_s) || tags.include?(tag_name.to_sym)
106
+ end
107
+
108
+ private
109
+
110
+ # Retrieves the first available attribute value from a prioritized list of keys.
111
+ #
112
+ # Used for handling multiple possible field names in source data
113
+ # (example: 'tick' or 'ticketid', 'summ' or 'summary' or 'title').
114
+ #
115
+ # @param attrs [Hash] The attributes hash to search.
116
+ # @param keys [Array<String>] Ordered list of keys to try.
117
+ # @return [Object, nil] The first found value or nil if none exist.
118
+ def attrs_value attrs, keys
119
+ keys.find { |key| return attrs[key] if attrs.key?(key) }
120
+ nil
121
+ end
122
+
123
+ # Normalizes the 'auths' attribute to ensure consistent structure.
124
+ #
125
+ # Converts various input formats (strings, hashes, arrays) into a
126
+ # standardized array of hashes with 'user' and optional 'memo' keys.
127
+ #
128
+ # @param val [String, Hash, Array, nil] The authors data to normalize.
129
+ # @return [Array<Hash>] Normalized array of author hashes.
130
+ def normalize_auths val
131
+ return [] if val.nil?
132
+
133
+ Array(val).map do |a|
134
+ if a.is_a?(String)
135
+ { 'user' => a }
136
+ elsif a.is_a?(Hash)
137
+ {
138
+ 'user' => a['user'] || a[:user],
139
+ 'memo' => a['memo'] || a[:memo]
140
+ }.compact
141
+ else
142
+ { 'user' => a.to_s }
143
+ end
144
+ end
145
+ end
146
+
147
+ # Normalizes the 'links' attribute to ensure consistent structure.
148
+ #
149
+ # Ensures all links have standardized 'text', 'xref', and 'href' keys,
150
+ # converting symbol keys to string keys and filtering out nil values.
151
+ #
152
+ # @param val [Array<Hash>, nil] The links data to normalize.
153
+ # @return [Array<Hash>] Normalized array of link hashes.
154
+ def normalize_links val
155
+ return [] if val.nil?
156
+
157
+ val.map do |l|
158
+ {
159
+ 'text' => l['text'] || l[:text],
160
+ 'xref' => l['xref'] || l[:xref],
161
+ 'href' => l['href'] || l[:href]
162
+ }.compact
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ module RHYML
5
+ module RHYMLFilters
6
+ def pasterize input
7
+ return input unless input.is_a? String
8
+
9
+ RHYML.pasterize(input)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ module RHYML
5
+ class Loader
6
+ require 'yaml'
7
+ require 'json'
8
+
9
+ def self.load_file path
10
+ ext = File.extname(path)
11
+ case ext
12
+ when '.yml', '.yaml'
13
+ SchemaGraphy::Loader.load_yaml_with_tags(path)
14
+ when '.json'
15
+ JSON.parse(File.read(path))
16
+ else
17
+ raise "Unsupported format: #{ext}"
18
+ end
19
+ end
20
+ end
21
+
22
+ class MappingLoader < Loader
23
+ end
24
+
25
+ class ReleaseLoader < Loader
26
+ def self.load path
27
+ hash = load_file(path)
28
+ Release.new(
29
+ code: hash['code'],
30
+ date: hash['date'],
31
+ hash: hash['hash'],
32
+ memo: hash['memo'],
33
+ changes: hash['changes'] || hash['work'] || [])
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+ # GitHub Issues API to RHYML mapping configuration
2
+ # Maps GitHub Issues API response to RHYML Change entries
3
+
4
+ $config:
5
+ desc: "GitHub Issues API to RHYML mapping"
6
+ path_lang: jmespath
7
+ tplt_lang: liquid
8
+
9
+ # GitHub Issues API returns an array of issues directly
10
+ # But some payloads may have an {issues: [...]} wrapper
11
+ changes_array_path: "issues || @"
12
+
13
+ # Map each issue to RHYML Change properties
14
+ tick:
15
+ path: "number"
16
+
17
+ # hash:
18
+ # path: $nil # GitHub Issues API doesn't include git commit hashes
19
+
20
+ summ:
21
+ path: "title"
22
+
23
+ note:
24
+ path: "body"
25
+
26
+ # Extract type from the native GitHub Issues type field (modern approach)
27
+ type:
28
+ path: "issue_type.name"
29
+
30
+ # Derive `parts` from labels using direct key matching or slug override
31
+ parts:
32
+ path: "labels[].name"
33
+ ruby: |
34
+ labels = path.is_a?(Array) ? path : [path]
35
+ parts_defs = config['parts'] || {}
36
+ label_prefix = parts_defs['label_prefix'] || ''
37
+
38
+ label_to_part = parts_defs.each_with_object({}) do |(part_key, part_config), memo|
39
+ if part_config.is_a?(Hash)
40
+ slug = part_config['slug']
41
+ memo[part_key.to_s.downcase] = part_key
42
+ memo[slug.to_s.downcase] = part_key if slug
43
+ end
44
+ end
45
+
46
+ found_parts = labels.map do |label|
47
+ l = label.to_s.downcase
48
+ if !label_prefix.to_s.empty? && l.start_with?(label_prefix.to_s.downcase)
49
+ l = l.sub(/^#{Regexp.escape(label_prefix)}/i, '')
50
+ end
51
+ label_to_part[l]
52
+ end
53
+
54
+ found_parts.compact.uniq
55
+
56
+ tags:
57
+ path: "labels[].name"
58
+
59
+ lead:
60
+ path: "assignee.login"
@@ -0,0 +1,73 @@
1
+ # GitLab Issues API to RHYML mapping configuration
2
+ # Maps GitLab Issues API response to RHYML Change entries
3
+
4
+ $config:
5
+ desc: "GitLab Issues API to RHYML mapping"
6
+ path_lang: jmespath
7
+ tplt_lang: liquid
8
+
9
+ # GitLab Issues API returns an array of issues in "issues" key
10
+ changes_array_path: "issues"
11
+
12
+ # Map each issue to RHYML Change properties
13
+ tick:
14
+ path: "iid"
15
+
16
+ summ:
17
+ path: "title"
18
+
19
+ note:
20
+ path: "description"
21
+
22
+ # Derive `type` from labels using direct key matching or slug override
23
+ type:
24
+ path: "labels"
25
+ ruby: |
26
+ labels = path.is_a?(Array) ? path : [path]
27
+ type_defs = config['types'] || {}
28
+
29
+ label_to_type = type_defs.each_with_object({}) do |(type_key, type_config), memo|
30
+ if type_config.is_a?(Hash)
31
+ slug = type_config['slug']
32
+ memo[type_key.to_s.downcase] = type_key
33
+ memo[slug.to_s.downcase] = type_key if slug
34
+ end
35
+ end
36
+
37
+ found_type = labels.find do |label|
38
+ label_to_type[label.to_s.downcase]
39
+ end
40
+
41
+ label_to_type[found_type.to_s.downcase] if found_type
42
+
43
+ # Derive `parts` from labels using direct key matching or slug override
44
+ parts:
45
+ path: "labels"
46
+ ruby: |
47
+ labels = path.is_a?(Array) ? path : [path]
48
+ parts_defs = config['parts'] || {}
49
+ label_prefix = parts_defs['label_prefix'] || ''
50
+
51
+ label_to_part = parts_defs.each_with_object({}) do |(part_key, part_config), memo|
52
+ if part_config.is_a?(Hash)
53
+ slug = part_config['slug']
54
+ memo[part_key.to_s.downcase] = part_key
55
+ memo[slug.to_s.downcase] = part_key if slug
56
+ end
57
+ end
58
+
59
+ found_parts = labels.map do |label|
60
+ l = label.to_s.downcase
61
+ if !label_prefix.to_s.empty? && l.start_with?(label_prefix.to_s.downcase)
62
+ l = l.sub(/^#{Regexp.escape(label_prefix)}/i, '')
63
+ end
64
+ label_to_part[l]
65
+ end
66
+
67
+ found_parts.compact.uniq
68
+
69
+ tags:
70
+ path: "labels"
71
+
72
+ lead:
73
+ path: "assignees[0].username"
@@ -0,0 +1,29 @@
1
+ # Jira Issues API to RHYML mapping configuration
2
+ $config:
3
+ desc: "Jira Issues API to RHYML mapping"
4
+ path_lang: jmespath
5
+ tplt_lang: liquid
6
+
7
+ changes_array_path: "@"
8
+
9
+ tick:
10
+ path: "key"
11
+
12
+ summ:
13
+ path: "fields.summary"
14
+
15
+ note:
16
+ path: "fields.description"
17
+
18
+ type:
19
+ path: "fields.issuetype.name"
20
+ tplt: '{{ path | downcase }}'
21
+
22
+ parts:
23
+ path: "fields.components[].name"
24
+
25
+ tags:
26
+ path: "fields.labels"
27
+
28
+ lead:
29
+ path: "fields.assignee.displayName"
@@ -0,0 +1,98 @@
1
+ # Verb past tense mappings for the pasterize feature
2
+ # Maps present tense verbs to their past tense equivalents
3
+ add: added
4
+ adds: added
5
+ address: addressed
6
+ addresses: addressed
7
+ change: changed
8
+ changes: changed
9
+ clarify: clarified
10
+ clarifies: clarified
11
+ clean: cleaned
12
+ cleans: cleaned
13
+ configure: configured
14
+ configures: configured
15
+ correct: corrected
16
+ corrects: corrected
17
+ create: created
18
+ creates: created
19
+ deprecate: deprecated
20
+ deprecates: deprecated
21
+ document: documented
22
+ documents: documented
23
+ downgrade: downgraded
24
+ downgrades: downgraded
25
+ enhance: enhanced
26
+ enhances: enhanced
27
+ enable: enabled
28
+ enables: enabled
29
+ evaluate: evaluated
30
+ evaluates: evaluated
31
+ expand: expanded
32
+ expands: expanded
33
+ fix: fixed
34
+ fixes: fixed
35
+ improve: improved
36
+ improves: improved
37
+ handle: handled
38
+ handles: handled
39
+ implement: implemented
40
+ implements: implemented
41
+ initialize: initialized
42
+ initializes: initialized
43
+ integrate: integrated
44
+ integrates: integrated
45
+ investigate: investigated
46
+ investigates: investigated
47
+ launch: launched
48
+ launches: launched
49
+ make: made
50
+ makes: made
51
+ merge: merged
52
+ merges: merged
53
+ migrate: migrated
54
+ migrates: migrated
55
+ optimize: optimized
56
+ optimizes: optimized
57
+ patch: patched
58
+ patches: patched
59
+ refactor: refactored
60
+ refactors: refactored
61
+ refine: refined
62
+ refines: refined
63
+ remove: removed
64
+ removes: removed
65
+ rename: renamed
66
+ renames: renamed
67
+ revert: reverted
68
+ reverts: reverted
69
+ review: reviewed
70
+ reviews: reviewed
71
+ resolve: resolved
72
+ resolves: resolved
73
+ restructure: restructured
74
+ restructures: restructured
75
+ rework: reworked
76
+ reworks: reworked
77
+ secure: secured
78
+ secures: secured
79
+ simplify: simplified
80
+ simplifies: simplified
81
+ speedup: "sped up"
82
+ speedups: "sped up"
83
+ standardize: standardized
84
+ standardizes: standardized
85
+ streamline: streamlined
86
+ streamlines: streamlined
87
+ support: supported
88
+ supports: supported
89
+ test: tested
90
+ tests: tested
91
+ tweak: tweaked
92
+ tweaks: tweaked
93
+ update: updated
94
+ updates: updated
95
+ upgrade: upgraded
96
+ upgrades: upgraded
97
+ verify: verified
98
+ verifies: verified
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReleaseHx
4
+ module RHYML
5
+ # Represents a single versioned release containing metadata and associated changes.
6
+ #
7
+ # The Release class serves as a container for release metadata (version code, date, etc.)
8
+ # and manages a collection of Change objects that comprise the Release content.
9
+ class Release
10
+ attr_reader :code, :date, :hash, :memo, :changes
11
+
12
+ # Initializes a new Release object.
13
+ #
14
+ # @param code [String] The version code for the release (e.g., '1.2.0').
15
+ # @param date [Date, String] The release date.
16
+ # @param hash [String] The Git commit hash associated with the release.
17
+ # @param memo [String] A descriptive memo for the release.
18
+ # @param changes [Array<Change, Hash>] An array of Change objects or Hashes
19
+ # to be converted into Change objects.
20
+ def initialize code:, date: nil, hash: nil, memo: nil, changes: []
21
+ @code = code
22
+ @date = date
23
+ @hash = hash
24
+ @memo = memo
25
+ @changes = Array(changes).map { |ch| init_change(ch) }.compact
26
+
27
+ ReleaseHx.logger.debug 'Release initialized with changes (post-compact):'
28
+ @changes.each_with_index do |ch, i|
29
+ ReleaseHx.logger.debug " changes[#{i}]: #{ch.class}" unless ch.nil?
30
+ end
31
+ raise 'Unexpected nil in changes' if @changes.any?(&:nil?)
32
+ end
33
+
34
+ # Adds a Change object to the Release.
35
+ #
36
+ # @param change [Change] The Change object to add.
37
+ # @return [Array<Change>] The updated array of Changes.
38
+ def add_change change
39
+ attach_release(change)
40
+ @changes << change
41
+ end
42
+
43
+ # Returns the number of Changes in the Release.
44
+ #
45
+ # @return [Integer] The count of Changes.
46
+ def change_count
47
+ changes.size
48
+ end
49
+
50
+ # Retrieves a unique, sorted list of contributor logins for the Release.
51
+ #
52
+ # @return [Array<String>] An array of unique contributor names.
53
+ def contributors
54
+ changes.map(&:lead).compact.uniq
55
+ end
56
+
57
+ # Calculates a hash with the count of each tag used in the Release.
58
+ #
59
+ # @return [Hash{String => Integer}] A hash where keys are tag names and
60
+ # values are their counts.
61
+ def tag_stats
62
+ changes.compact.flat_map { |c| c.tags || [] }.tally
63
+ end
64
+
65
+ # Converts the Release metadata to a hash representation.
66
+ #
67
+ # @note This method excludes the Changes array from the output.
68
+ # @return [Hash] A hash containing the Release's metadata fields.
69
+ def to_h
70
+ {
71
+ 'code' => code,
72
+ 'version' => code, # alias for backward compatibility
73
+ 'date' => date,
74
+ 'hash' => hash,
75
+ 'memo' => memo,
76
+ 'tag_stats' => tag_stats,
77
+ 'contributors' => contributors
78
+ }.compact
79
+ end
80
+
81
+ private
82
+
83
+ # Initializes a Change object from various input types.
84
+ # Handles both existing Change objects and raw Hash data.
85
+ #
86
+ # @param change [Hash, Change] The item to process into a Change object.
87
+ # @return [Change, nil] A valid Change object or nil if input is invalid.
88
+ def init_change change
89
+ return nil unless change
90
+
91
+ if change.is_a?(Change)
92
+ change.release = self
93
+ change
94
+ elsif change.is_a?(Hash)
95
+ begin
96
+ obj = Change.new(change, release: self)
97
+ obj.release = self
98
+ obj
99
+ rescue StandardError => e
100
+ ReleaseHx.logger.warn "Skipping malformed change: #{e.message}"
101
+ nil
102
+ end
103
+ else
104
+ ReleaseHx.logger.warn "Unknown change type: #{ch.class}"
105
+ nil
106
+ end
107
+ end
108
+
109
+ # Associates a Change object with this Release by setting its release property.
110
+ #
111
+ # @param change [Change] The Change to associate with this release.
112
+ # @return [Change] The associated Change object.
113
+ def attach_release change
114
+ change.release = self
115
+ change
116
+ end
117
+ end
118
+
119
+ # Manages a collection of Release objects for historical tracking.
120
+ #
121
+ # @note This class is currently unused but maintained as part of the core
122
+ # RHYML data model for future functionality like cross-release analytics.
123
+ class History
124
+ attr_reader :releases
125
+
126
+ # Initializes a new, empty History object.
127
+ def initialize
128
+ @releases = []
129
+ end
130
+
131
+ # Adds a Release to the history.
132
+ #
133
+ # @param release [Release] The Release to add.
134
+ # @return [Release] The Release that was added.
135
+ def add_release release
136
+ raise ArgumentError, 'Release must be a Release object' unless release.is_a? Release
137
+
138
+ @releases << release
139
+ ReleaseHx.logger.debug "Added Release: #{release.code} (#{release.date})"
140
+ release
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rhyml/adapter'
4
+ require_relative 'rhyml/change'
5
+ require_relative 'rhyml/loaders'
6
+ require_relative 'rhyml/release'
7
+ require_relative 'rhyml/liquid'
8
+
9
+ module ReleaseHx
10
+ # RHYML (Release History YAML) is the core data modeling language for ReleaseHx.
11
+ # This module provides the components for loading, validating, and transforming
12
+ # RHYML data.
13
+ module RHYML
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../schemagraphy/templating'
4
+
5
+ module ReleaseHx
6
+ module SgymlHelpers
7
+ # Precompiles a schema into a set of templates, using the provided data and schema.
8
+ def self.precompile_from_schema! data, schema, base_path = '', scope: {}
9
+ SchemaGraphy::Templating.precompile_from_schema!(data, schema, base_path, scope: scope)
10
+ end
11
+
12
+ # Renders all templated fields in a given Hash if they've previously been parsed
13
+ def self.render_stage_fields! data, stage
14
+ data.each do |key, value|
15
+ next unless value.is_a?(Sourcerer::Templating::TemplatedField)
16
+
17
+ tmpl_context = value.context
18
+ next unless tmpl_context.respond_to?(:stage)
19
+ next unless tmpl_context.stage.to_sym == stage.to_sym
20
+
21
+ data[key] = value.render
22
+ end
23
+ end
24
+
25
+ # Recursively converts all keys in a Hash or Array to strings,
26
+ # safely handling non-stringifiable objects
27
+ def self.deep_stringify_safe obj
28
+ case obj
29
+ when Hash
30
+ obj.each_with_object({}) do |(k, v), h|
31
+ h[k.to_s] = deep_stringify_safe(v)
32
+ end
33
+ when Array
34
+ obj.map { |v| deep_stringify_safe(v) }
35
+ else
36
+ begin
37
+ obj.to_yaml
38
+ obj
39
+ rescue TypeError
40
+ obj.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end