Module: StandardAudit::Operation::Audit
- Defined in:
- lib/standard_audit/operation/audit.rb
Overview
The registry, the catalogue resolver, the write-error policy, and the analysis predicates behind the operation-audit meta-spec.
Everything here is PLAIN RUBY that returns data — no RSpec, no
assertions. A host can call these from a bespoke spec, a rake task, or a
CI script. standard_audit/rspec/operation is a thin shared-example
layer over exactly these methods and adds no logic of its own.
StandardAudit::Operation::Audit.undeclared # => [Class, ...]
StandardAudit::Operation::Audit.unknown_actions # => { Class => ["x.y"] }
StandardAudit::Operation::Audit.orphan_actions # => ["x.y"]
StandardAudit::Operation::Audit.missing_write_sites # => [Class, ...]
Constant Summary collapse
- WRITE_SITE_PATTERN =
Heuristic used by the source-scanning predicates: a call to the private
audit!helper, with or without parentheses, not preceded by a receiver (sofoo.audit!andSomething::audit!don't count).The scan is FILE-scoped, not class-scoped — two operations defined in one file share a verdict. Zeitwerk requires one class per file in a real app, so this only matters for fixtures.
Comments are stripped before scanning (see .strip_comments), so a documented or commented-out
audit!is not a write site. Until 0.9.1 it was, and the docstring here claimed the predicates "err towards a false pass rather than a false failure" — which was only ever true of missing_write_sites. In the unexpected_write_sites direction the same match produces a false FAILURE, so anaudit_none!class that explained itself in prose failed the check. The claim was wrong for half its own surface, which is worse than the bug: it told anyone hitting the failure not to suspect the scanner.What remains genuinely heuristic: a string literal containing
audit!still counts, and the runtime guard is still the authoritative check. /(?<![\w.:])audit!\s*(?:\(|["':@$\w])/
Class Method Summary collapse
-
.catalogue ⇒ Object
The host's action vocabulary as an Array of Strings, or nil when no catalogue is configured (in which case membership is not checked).
-
.declared_actions(operations: self.operations) ⇒ Array<String>
Every action declared across the given operations, de-duplicated.
-
.duplicate_catalogue_entries(catalogue: self.catalogue) ⇒ Array<String>
Catalogue entries listed more than once.
-
.handle_write_error(error, action:, operation:) ⇒ nil
Applies the configured policy to a failed audit write (never to a DeclarationError, which
audit!re-raises first). -
.missing_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare
auditsbut whose source contains noaudit!call — the declaration is aspirational and nothing writes the row. -
.operations(source: nil) ⇒ Object
The real operations a meta-spec should hold to the contract.
-
.orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) ⇒ Array<String>
Catalogue entries no operation declares — dead vocabulary, which makes the catalogue a wish rather than a record.
- .register(klass) ⇒ Object
-
.registered ⇒ Object
Every class that has gained the contract, in registration order.
-
.reset_registry! ⇒ Object
Empties the registry.
-
.source_path(klass) ⇒ Object
Absolute path of the file that defines
klass, or nil. -
.undeclared(operations: self.operations) ⇒ Array<Class>
Operations that declare neither
auditsnoraudit_none!. -
.unexpected_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare
audit_none!but whose source callsaudit!anyway. -
.unknown_actions(operations: self.operations, catalogue: self.catalogue) ⇒ Hash{Class => Array<String>}
Declared actions that are absent from the configured catalogue.
-
.verify? ⇒ Boolean
Whether
audit!verifies declarations before writing.
Class Method Details
.catalogue ⇒ Object
The host's action vocabulary as an Array of Strings, or nil when no catalogue is configured (in which case membership is not checked).
config.audit_catalogue is normally a callable — see the note in
StandardAudit::Operation on why an eagerly-referenced autoloadable
constant breaks Zeitwerk reloading. A plain Array is accepted for the
case where the vocabulary really is a frozen literal.
106 107 108 109 110 111 112 113 114 |
# File 'lib/standard_audit/operation/audit.rb', line 106 def catalogue raw = StandardAudit.config.audit_catalogue return nil if raw.nil? resolved = raw.respond_to?(:call) ? raw.call : raw return nil if resolved.nil? Array(resolved).map(&:to_s) end |
.declared_actions(operations: self.operations) ⇒ Array<String>
Every action declared across the given operations, de-duplicated.
197 198 199 |
# File 'lib/standard_audit/operation/audit.rb', line 197 def declared_actions(operations: self.operations) operations.flat_map { |klass| declared_for(klass) }.uniq end |
.duplicate_catalogue_entries(catalogue: self.catalogue) ⇒ Array<String>
Catalogue entries listed more than once.
204 205 206 207 208 |
# File 'lib/standard_audit/operation/audit.rb', line 204 def duplicate_catalogue_entries(catalogue: self.catalogue) return [] if catalogue.nil? catalogue.tally.select { |_action, count| count > 1 }.keys end |
.handle_write_error(error, action:, operation:) ⇒ nil
Applies the configured policy to a failed audit write (never to a
DeclarationError, which audit! re-raises first).
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/standard_audit/operation/audit.rb', line 132 def handle_write_error(error, action:, operation:) raising = StandardAudit.config.raise_on_audit_write_error handler = StandardAudit.config.audit_write_error_handler if handler handler.call(error, action: action, operation: operation) elsif !raising # Deliberately NOT reported when re-raising: the caller receives the # error and owns it from here. Reporting as well produced two events # for one failure in every host that both sets # `raise_on_audit_write_error` and reports on the error it catches — # which is most hosts that set the flag at all, since the flag exists # for hosts that treat an unaudited write as a failure worth # handling. And a host that does NOT catch it still gets the report, # via its framework's own unhandled-error path. report_write_error(error, action: action, operation: operation) end raise error if raising nil end |
.missing_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare audits but whose source contains no audit!
call — the declaration is aspirational and nothing writes the row.
Source-based, therefore a heuristic: a class whose defining file can't be read is skipped rather than reported.
217 218 219 220 221 222 223 224 225 |
# File 'lib/standard_audit/operation/audit.rb', line 217 def missing_write_sites(operations: self.operations) operations.select do |klass| spec = klass.audit_spec next false unless spec.is_a?(Array) src = source_for(klass) src && !src.match?(WRITE_SITE_PATTERN) end end |
.operations(source: nil) ⇒ Object
The real operations a meta-spec should hold to the contract.
Excluded:
- anonymous classes (no name) — test doubles and `Class.new` fixtures
- classes that declared `audit_abstract!`
- classes that declare NOTHING and have subclasses — i.e. a shared
base such as `ApplicationOperation`. This is what lets an app
adopt the whole contract with one `include` on its base class and
no further configuration. A class that DID declare is never
excluded by this rule, even if it is subclassed, so subclassing a
real operation cannot quietly drop it from the check.
77 78 79 80 81 82 83 84 85 86 |
# File 'lib/standard_audit/operation/audit.rb', line 77 def operations(source: nil) registered.select do |klass| next false if klass.name.nil? || klass.name.empty? next false if klass.audit_spec == :abstract next false if klass.audit_spec.nil? && subclassed?(klass) next true if source.nil? source_path(klass)&.include?(source) end end |
.orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) ⇒ Array<String>
Catalogue entries no operation declares — dead vocabulary, which makes the catalogue a wish rather than a record.
187 188 189 190 191 192 |
# File 'lib/standard_audit/operation/audit.rb', line 187 def orphan_actions(operations: self.operations, catalogue: self.catalogue, within: nil) pool = within ? Array(within).map(&:to_s) : catalogue return [] if pool.nil? pool - declared_actions(operations: operations) end |
.register(klass) ⇒ Object
50 51 52 53 |
# File 'lib/standard_audit/operation/audit.rb', line 50 def register(klass) registered << klass unless registered.include?(klass) klass end |
.registered ⇒ Object
Every class that has gained the contract, in registration order. Includes bases, intermediates, and anonymous classes — use operations for the filtered view a meta-spec should assert on.
46 47 48 |
# File 'lib/standard_audit/operation/audit.rb', line 46 def registered @registered ||= [] end |
.reset_registry! ⇒ Object
Empties the registry. For the gem's own specs and for hosts that rebuild the constant graph mid-suite; a normal suite never needs it.
57 58 59 |
# File 'lib/standard_audit/operation/audit.rb', line 57 def reset_registry! @registered = [] end |
.source_path(klass) ⇒ Object
Absolute path of the file that defines klass, or nil.
89 90 91 92 93 94 95 |
# File 'lib/standard_audit/operation/audit.rb', line 89 def source_path(klass) return nil if klass.name.nil? || klass.name.empty? Object.const_source_location(klass.name)&.first rescue NameError nil end |
.undeclared(operations: self.operations) ⇒ Array<Class>
Operations that declare neither audits nor audit_none!. A new
mutating operation shipping with no audit trail shows up here.
161 162 163 |
# File 'lib/standard_audit/operation/audit.rb', line 161 def undeclared(operations: self.operations) operations.select { |klass| klass.audit_spec.nil? } end |
.unexpected_write_sites(operations: self.operations) ⇒ Array<Class>
Operations that declare audit_none! but whose source calls audit!
anyway. The runtime guard catches this too, but only if that code path
is exercised; this catches it statically.
232 233 234 235 236 237 238 239 |
# File 'lib/standard_audit/operation/audit.rb', line 232 def unexpected_write_sites(operations: self.operations) operations.select do |klass| next false unless klass.audit_spec == :none src = source_for(klass) src&.match?(WRITE_SITE_PATTERN) end end |
.unknown_actions(operations: self.operations, catalogue: self.catalogue) ⇒ Hash{Class => Array<String>}
Declared actions that are absent from the configured catalogue. Empty when no catalogue is configured.
169 170 171 172 173 174 175 176 |
# File 'lib/standard_audit/operation/audit.rb', line 169 def unknown_actions(operations: self.operations, catalogue: self.catalogue) return {} if catalogue.nil? operations.each_with_object({}) do |klass, acc| unknown = declared_for(klass) - catalogue acc[klass] = unknown if unknown.any? end end |
.verify? ⇒ Boolean
Whether audit! verifies declarations before writing. Defaults to
"local environments only" — production writes silently, because a
developer's declaration mistake must not 500 a user.
121 122 123 124 |
# File 'lib/standard_audit/operation/audit.rb', line 121 def verify? resolver = StandardAudit.config.verify_audit_declarations resolver.respond_to?(:call) ? !!resolver.call : !!resolver end |