standard_health 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bc8cdc021df7485cb1443cf6902b6fee31627ad6135d9eef6f284cd5eed47e0
4
- data.tar.gz: bfd4e8a50e602685430481b85a0ebf604d1e930db71ce47492036d772f4aa13c
3
+ metadata.gz: 73c1743f46e03601e44970d282ca02dd195b22e5f22fe502e20e8a45281e0d69
4
+ data.tar.gz: 507f8e5fda5420690b7146b0f6a9136561c0a1b61d6c567c5be3dcd5c250be61
5
5
  SHA512:
6
- metadata.gz: 32ec724c45eb9899e4d241d5681d296e1ac5745c3ff1852b5a187dea1fcc2a6b7ff7a922c08f925f0b59189ca8523d11fd034a5743f394186d91c4c1f2d8596d
7
- data.tar.gz: 4711ceae81d4b9d0c19b440cc62fe8b2677ce569661adf741775ecc7933ee93c68402c8141ca830e76a6a75aa023c3f7759ef4d68a175d38280a2d971e83b964
6
+ metadata.gz: 38b527cf8973dc2557b1b8c13e40ace60f73efb78f1aab34100b4a51cf4f53069700c7ac0047f038a9bcf5fdcb9a1f3223fa92ddc653e1293f6c5d4b51f320ed
7
+ data.tar.gz: 3662477381f1149d26394ba63fc2380c2104d1cb78a65bf3d943681a907b591ad0e7e9a6f990c868cf48087306cf6cb845c399fd976a346858ba083647b6bc3e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-05-05
11
+
12
+ ### Added
13
+
14
+ - Consumer-presence detection for `consumed_by:` paths. `audit()` accepts an optional `root:` keyword (host-app root). When given, each entry whose `consumed_by:` is set is checked against the host app's tree, producing a new `consumer:` field on the audit row: `:present` (file exists and references the var via `ENV[...]` or `ENV.fetch(...)`), `:file_missing` (path missing on disk), or `:not_referenced` (file exists but no `ENV` reference). Catches renamed/deleted consumer files, `consumed_by:` typos, and vars declared in env-spec but never actually `ENV.fetch`'d.
15
+ - `DiagnosticsController#env` now passes `Rails.root` automatically, so host apps get the new `consumer:` field with no host-side change.
16
+ - Deprecation metadata on `required` / `recommended`: `deprecated: true`, `sunset_on:` (target removal date), `replacement:` (what to use instead). Surfaced verbatim in audit rows. Lets vars be staged for removal with audit trail.
17
+
18
+ ### Changed
19
+
20
+ - `Entry` struct extended with `deprecated`, `sunset_on`, `replacement`. Backward-compatible — every 0.3.0 spec produces identical audit output when the new opts aren't used and `root:` isn't passed.
21
+
10
22
  ## [0.3.0] - 2026-04-29
11
23
 
12
24
  ### Added
@@ -19,8 +19,9 @@ module StandardHealth
19
19
  def env
20
20
  spec = StandardHealth.config.env_spec
21
21
  mode = ENV["APP_ENVIRONMENT"].to_s
22
+ root = defined?(Rails) ? Rails.root : nil
22
23
 
23
- audit = spec ? spec.audit(ENV.to_h, mode: mode) : []
24
+ audit = spec ? spec.audit(ENV.to_h, mode: mode, root: root) : []
24
25
 
25
26
  render json: {
26
27
  mode: mode,
@@ -31,12 +31,22 @@ module StandardHealth
31
31
  # - `description:` (String, optional) — human-readable hint surfaced
32
32
  # verbatim by `/diagnostics/env`.
33
33
  # - `consumed_by:` (String or Array<String>, optional) — pointer(s) to
34
- # where the value is read in the host app. Surfaced verbatim.
34
+ # where the value is read in the host app. Surfaced verbatim. When
35
+ # `audit` is called with `root:`, each path is checked for an
36
+ # `ENV[...]` / `ENV.fetch(...)` reference to the var; the result is
37
+ # reported as `consumer:` in the audit row
38
+ # (`:present`, `:file_missing`, or `:not_referenced`).
35
39
  # - `if:` / `unless:` (Proc, optional) — predicates evaluated at audit
36
40
  # time. When `unless:` returns truthy or `if:` returns falsy the entry
37
41
  # is reported with `status: :not_applicable`.
38
42
  # - `group` (String, optional) — set implicitly by enclosing `group`
39
43
  # block; surfaced verbatim.
44
+ # - `deprecated:` (Boolean, optional) — flag for staged removal. When
45
+ # true, the audit row includes `deprecated: true`.
46
+ # - `sunset_on:` (String/Date, optional) — target removal date.
47
+ # Surfaced verbatim in audit rows.
48
+ # - `replacement:` (String, optional) — what to use instead. Surfaced
49
+ # in audit rows.
40
50
  class EnvSpec
41
51
  # Raised when `in:` references a Symbol that hasn't been declared via
42
52
  # `mode_alias`.
@@ -53,6 +63,9 @@ module StandardHealth
53
63
  :if_predicate,
54
64
  :unless_predicate,
55
65
  :group,
66
+ :deprecated,
67
+ :sunset_on,
68
+ :replacement,
56
69
  keyword_init: true
57
70
  )
58
71
 
@@ -119,12 +132,20 @@ module StandardHealth
119
132
  #
120
133
  # @param env_hash [Hash{String, Symbol => String}] e.g. ENV.to_h
121
134
  # @param mode [String, Symbol] current APP_ENVIRONMENT value
135
+ # @param root [String, Pathname, nil] host app root for resolving
136
+ # `consumed_by` paths. When given, each row gains a `consumer:` field:
137
+ # `:present` (file exists and references the var),
138
+ # `:file_missing` (path does not exist),
139
+ # `:not_referenced` (file exists but no `ENV[...]` / `ENV.fetch(...)`
140
+ # match for the var). When the entry has no `consumed_by` or `root`
141
+ # is nil, the field is omitted.
122
142
  # @return [Array<Hash>] one row per applicable entry. Each row has at
123
143
  # least `name`, `level`, `status`, `mode`. When an entry is suppressed
124
144
  # by an `if:`/`unless:` predicate, `status` is `:not_applicable` and a
125
- # `reason` field explains why. `description`, `consumed_by`, and
126
- # `group` are included when set on the entry.
127
- def audit(env_hash, mode:)
145
+ # `reason` field explains why. `description`, `consumed_by`, `group`,
146
+ # `deprecated`, `sunset_on`, `replacement`, and `consumer` are
147
+ # included when set on the entry / computed during audit.
148
+ def audit(env_hash, mode:, root: nil)
128
149
  mode_str = mode.to_s
129
150
  env = stringify(env_hash)
130
151
 
@@ -142,6 +163,9 @@ module StandardHealth
142
163
  row[:status] = classify(entry, value)
143
164
  end
144
165
 
166
+ consumer_status = consumer_state(entry, root)
167
+ row[:consumer] = consumer_status if consumer_status
168
+
145
169
  out << row
146
170
  end
147
171
  end
@@ -158,7 +182,10 @@ module StandardHealth
158
182
  consumed_by: normalize_consumed_by(opts[:consumed_by]),
159
183
  if_predicate: opts[:if],
160
184
  unless_predicate: opts[:unless],
161
- group: @group_stack.last
185
+ group: @group_stack.last,
186
+ deprecated: opts[:deprecated] ? true : nil,
187
+ sunset_on: opts[:sunset_on],
188
+ replacement: opts[:replacement]
162
189
  )
163
190
  end
164
191
 
@@ -211,9 +238,47 @@ module StandardHealth
211
238
  row[:description] = entry.description if entry.description
212
239
  row[:consumed_by] = serialize_consumed_by(entry.consumed_by) if entry.consumed_by
213
240
  row[:group] = entry.group if entry.group
241
+ row[:deprecated] = true if entry.deprecated
242
+ row[:sunset_on] = entry.sunset_on.to_s if entry.sunset_on
243
+ row[:replacement] = entry.replacement if entry.replacement
214
244
  row
215
245
  end
216
246
 
247
+ # Resolve the consumer-presence state for an entry. Returns nil when
248
+ # we have nothing to check (no root, no consumed_by). Otherwise:
249
+ #
250
+ # :present — at least one consumed_by path exists AND mentions
251
+ # ENV["VAR"] or ENV.fetch("VAR", ...).
252
+ # :file_missing — every consumed_by path is missing on disk.
253
+ # :not_referenced — at least one path exists, but none reference the
254
+ # env var.
255
+ def consumer_state(entry, root)
256
+ return nil if root.nil? || entry.consumed_by.nil?
257
+
258
+ pattern = env_reference_pattern(entry.name)
259
+ any_file_present = false
260
+ any_referenced = false
261
+
262
+ entry.consumed_by.each do |relative|
263
+ path = File.join(root.to_s, relative)
264
+ next unless File.file?(path)
265
+
266
+ any_file_present = true
267
+ any_referenced = true if File.read(path).match?(pattern)
268
+ end
269
+
270
+ return :file_missing unless any_file_present
271
+ any_referenced ? :present : :not_referenced
272
+ end
273
+
274
+ # Match ENV["VAR"], ENV['VAR'], ENV[:VAR], ENV.fetch("VAR", ...),
275
+ # ENV.fetch('VAR', ...), ENV.fetch(:VAR, ...). Word-boundary on the
276
+ # right so VAR_PREFIX doesn't accidentally match VAR.
277
+ def env_reference_pattern(name)
278
+ escaped = Regexp.escape(name.to_s)
279
+ /ENV(?:\.fetch)?\s*[\[(]\s*[:'"]?#{escaped}\b/
280
+ end
281
+
217
282
  def serialize_consumed_by(value)
218
283
  value.length == 1 ? value.first : value
219
284
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StandardHealth
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_health
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim