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 (so foo.audit! and Something::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 an audit_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

Class Method Details

.catalogueObject

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.

Returns:

  • (Array<String>)


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.

Returns:

  • (Array<String>)


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).

Returns:

  • (nil)

    when the error is swallowed

Raises:

  • (StandardError)

    the original error when config.raise_on_audit_write_error is true



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.

Returns:

  • (Array<Class>)


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.

Parameters:

  • source (String, nil) (defaults to: nil)

    keep only classes whose defining file path contains this fragment, e.g. "/app/operations/". Recommended: it scopes the check to the host's own operations and drops anything defined in a spec file.



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.

Parameters:

  • within (Array<String>, nil) (defaults to: nil)

    check only this slice. Hosts whose catalogue also covers non-operation writers (a controller concern, a tool server, a notification-bus name) pass the operation-only slice here; the rest are written outside app/operations/ and would always look orphaned.

Returns:

  • (Array<String>)


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

.registeredObject

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.

Returns:

  • (Array<Class>)


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.

Returns:

  • (Array<Class>)


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.

Returns:

  • (Hash{Class => Array<String>})


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.

Returns:

  • (Boolean)


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