Class: Clickwrap::Linter
- Inherits:
-
Object
- Object
- Clickwrap::Linter
- Defined in:
- lib/clickwrap/linter.rb
Overview
Development and test heuristics for the presentation layer.
============================================================================ THESE ARE HEURISTICS. They warn; they never raise, never block a render, and never certify anything. A clean run means "none of the specific hazards below were detected in what was inspected" and nothing else.
This class must never print, return, or imply "compliant", "enforceable", "legally binding", "accessible", or "approved". Courts assess the complete page in context and accessibility applies to the whole experience, so no library that sees one fragment of one template can make any of those calls. What it can do is notice objectively checkable mistakes — a submit button above the controls, a consent box that arrives already ticked, a document nobody can open — and say so in a full sentence.
findings = Clickwrap::Linter.review_policy(Clickwrap.policy!(:signup))
findings.map(&:code) # => [:consent_statement_bundles_purposes]
findings.first.explanation # => a plain-English sentence
Every finding carries a stable symbol, so a test can assert on it without matching English, and an explanation, so a person reading the log knows what to do about it.
Defined Under Namespace
Classes: Finding
Constant Summary collapse
- ANSWER_FIELD_PATTERN =
Rendered-fragment scans, kept to string work so they are cheap enough to run on every development render.
/name=["']clickwrap_submission\[answers\]\[([^\]"']+)\]["']/- TOKEN_FIELD_PATTERN =
/name=["']clickwrap_submission\[presentation_token\]["']/- SUBMIT_CONTROL_PATTERN =
/<(?:button|input)\b[^>]*type=["']submit["']|<button(?![^>]*type=)/i- CHECKED_ATTRIBUTE_PATTERN =
/\bchecked\b/- BUNDLED_PURPOSE_PATTERN =
A rough test for a consent sentence that is carrying more than one purpose. It is deliberately generous: the cost of a false positive is one log line suggesting a split, and the cost of a false negative is a consent record whose meaning nobody can reconstruct.
%r{\b(and|and/or|as well as|plus)\b}i
Class Method Summary collapse
-
.enabled? ⇒ Boolean
On in development and test, off everywhere else — a production request should not be scanning its own HTML — and
config.lint_presentationsanswers for a host that disagrees with either half. -
.review_manifest(manifest, policy:) ⇒ Object
Whether the manifest that came back still says what the policy says.
-
.review_policy(policy, locale: nil) ⇒ Object
What a compiled policy alone can be checked for, without rendering anything: blank copy, bundled consent purposes, and an optional consent that another statement has quietly made mandatory.
-
.review_presentation(presentation) ⇒ Object
What one presentation can be checked for: a statement the person is asked to accept with no way to read what they are accepting.
-
.review_rendered_fields(html, presentation:) ⇒ Object
Called by the form-builder helper on every development render.
-
.review_rendered_html(html, presentation: nil) ⇒ Object
What the rendered HTML can be checked for.
-
.warn_about(findings, source: nil) ⇒ Object
Warnings go to the Rails log, or to stderr when there is no log.
Class Method Details
.enabled? ⇒ Boolean
On in development and test, off everywhere else — a production request
should not be scanning its own HTML — and config.lint_presentations
answers for a host that disagrees with either half.
Every finding is a log line, never an exception: a lint finding is a thing to look at, not a reason to stop a developer's page from rendering. Which is exactly why it needs a switch — a warning nobody can turn off is one people learn to scroll past.
54 55 56 57 58 59 60 61 |
# File 'lib/clickwrap/linter.rb', line 54 def enabled? configured = Clickwrap.config.lint_presentations return configured unless configured.nil? return false unless defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env ::Rails.env.development? || ::Rails.env.test? end |
.review_manifest(manifest, policy:) ⇒ Object
Whether the manifest that came back still says what the policy says. A difference here means the evidence and the current server-owned offer have diverged — usually a deploy between render and submit, sometimes a hand-built form that stopped tracking the policy.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/clickwrap/linter.rb', line 99 def review_manifest(manifest, policy:) snapshot = manifest.respond_to?(:to_h) ? manifest.to_h : manifest rendered = Array(snapshot["statements"] || snapshot[:statements]) differences = manifest_differences(rendered, policy: policy, locale: snapshot["locale"]) return [] if differences.empty? [Finding.new( code: :rendered_manifest_differs_from_policy, explanation: "The submitted presentation manifest does not match the current policy " \ "#{policy.key}: #{differences.join("; ")}. Clickwrap verifies this at " \ "capture; the warning is here so the drift is visible while you can still " \ "explain it.", context: { policy: policy.key, differences: differences } )] end |
.review_policy(policy, locale: nil) ⇒ Object
What a compiled policy alone can be checked for, without rendering anything: blank copy, bundled consent purposes, and an optional consent that another statement has quietly made mandatory.
68 69 70 71 72 73 74 75 76 |
# File 'lib/clickwrap/linter.rb', line 68 def review_policy(policy, locale: nil) locale = (locale || (defined?(::I18n) ? ::I18n.locale : :en)).to_s findings = [] findings.concat(blank_assertion_findings(policy)) findings.concat((policy, locale)) findings.concat((policy)) findings end |
.review_presentation(presentation) ⇒ Object
What one presentation can be checked for: a statement the person is asked to accept with no way to read what they are accepting.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/clickwrap/linter.rb', line 80 def review_presentation(presentation) presentation.statements.filter_map do |statement| next if statement.documents.any? next unless %w[agreement acknowledgment consent].include?(statement.kind) Finding.new( code: :document_link_missing, explanation: "The #{statement.kind} #{statement.key.inspect} presents no document, so " \ "the person is asked to accept something the page never shows them. Give " \ "the statement a `document:` and publish it.", context: { statement: statement.key, policy: presentation.policy_key } ) end end |
.review_rendered_fields(html, presentation:) ⇒ Object
Called by the form-builder helper on every development render. Scans only the fragment it produced, so it deliberately skips the CTA-ordering check (a submit button earlier in the host's page is invisible from here — that one belongs in a system test over the whole page).
135 136 137 138 139 140 141 142 |
# File 'lib/clickwrap/linter.rb', line 135 def review_rendered_fields(html, presentation:) findings = preselected_control_findings(html.to_s) findings.concat(rendered_document_link_findings(html.to_s, presentation)) findings.concat(combined_control_findings(html.to_s, presentation)) findings.concat(review_presentation(presentation)) warn_about(findings, source: "policy #{presentation.policy_key}") findings end |
.review_rendered_html(html, presentation: nil) ⇒ Object
What the rendered HTML can be checked for. Pass the whole page when you have it — the CTA-ordering check needs everything before the block, not just the block.
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/clickwrap/linter.rb', line 118 def review_rendered_html(html, presentation: nil) text = html.to_s findings = [] findings.concat(submit_ordering_findings(text)) findings.concat((text, presentation)) if presentation findings.concat(preselected_control_findings(text)) if presentation findings.concat(rendered_document_link_findings(text, presentation)) findings.concat(combined_control_findings(text, presentation)) end findings end |
.warn_about(findings, source: nil) ⇒ Object
Warnings go to the Rails log, or to stderr when there is no log. Never an exception: a lint finding is a thing to look at, not a reason to stop a developer's page from rendering.
147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/clickwrap/linter.rb', line 147 def warn_about(findings, source: nil) Array(findings).each do |finding| = ["[clickwrap] lint", source, "#{finding.code}: #{finding.explanation}"] .compact.join(" — ") logger = Clickwrap.logger logger ? logger.warn() : Kernel.warn() end findings end |