Module: Jekyll::AgentAudit::Rules::Publication
- Defined in:
- lib/jekyll/agent_audit/rules/publication.rb
Constant Summary collapse
- IDS =
%w[ architecture.output_collision links.target_missing links.fragment_missing architecture.orphan identity.canonical_invalid identity.canonical_multiple identity.canonical_target_missing structure.title_missing structure.h1_absent structure.heading_empty structure.heading_jump structure.id_duplicate structure.link_unnamed ].freeze
- SEVERITIES =
{ "architecture.output_collision" => :error, "links.target_missing" => :error, "links.fragment_missing" => :error, "architecture.orphan" => :warning, "identity.canonical_invalid" => :error, "identity.canonical_multiple" => :error, "identity.canonical_target_missing" => :error, "structure.title_missing" => :warning, "structure.h1_absent" => :info, "structure.heading_empty" => :warning, "structure.heading_jump" => :info, "structure.id_duplicate" => :error, "structure.link_unnamed" => :warning }.freeze
Class Method Summary collapse
- .architecture_orphan(document, context) ⇒ Object
- .architecture_output_collision(_document, context) ⇒ Object
- .asset_target?(entry) ⇒ Boolean
- .collision(context) ⇒ Object
- .evaluate(id, document, context) ⇒ Object
- .finding(rule, document, message, observation, line = nil, related_locations = []) ⇒ Object
- .graph(context) ⇒ Object
- .identity_canonical_invalid(document, _context) ⇒ Object
- .identity_canonical_multiple(document, context) ⇒ Object
- .identity_canonical_target_missing(document, context) ⇒ Object
- .links(document, context) ⇒ Object
- .links_fragment_missing(document, context) ⇒ Object
- .links_target_missing(document, context) ⇒ Object
- .root?(document, context) ⇒ Boolean
- .structure_h1_absent(document, _context) ⇒ Object
- .structure_heading_empty(document, _context) ⇒ Object
- .structure_heading_jump(document, _context) ⇒ Object
- .structure_id_duplicate(document, _context) ⇒ Object
- .structure_link_unnamed(document, _context) ⇒ Object
- .structure_title_missing(document, _context) ⇒ Object
Class Method Details
.architecture_orphan(document, context) ⇒ Object
63 64 65 66 67 68 69 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 63 def architecture_orphan(document, context) return [] if root?(document, context) || document[:redirect] return [] unless document[:content_confident] return [] unless graph(context) return [] unless graph(context).incoming(document).empty? [finding("architecture.orphan", document, "No incoming links in this build.", {incoming: 0})] end |
.architecture_output_collision(_document, context) ⇒ Object
40 41 42 43 44 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 40 def architecture_output_collision(_document, context) Array(context[:inventory]&.collisions).map do |collision| finding("architecture.output_collision", collision[:primary], "Multiple owners claim #{collision[:output_path]}.", {output_path: collision[:output_path], owners: collision[:owners].map { |o| o[:identity] }}) end end |
.asset_target?(entry) ⇒ Boolean
156 157 158 159 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 156 def asset_target?(entry) path = entry[:output_path].to_s !path.empty? && !path.end_with?(".html", "/") end |
.collision(context) ⇒ Object
146 147 148 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 146 def collision(context) {status: :evaluated, findings: architecture_output_collision(nil, context)} end |
.evaluate(id, document, context) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 28 def evaluate(id, document, context) return {status: :skipped, findings: [], reason: :unknown_rule} unless IDS.include?(id) return collision(context) if id == "architecture.output_collision" if document && document[:redirect] && %w[architecture.orphan structure.title_missing structure.h1_absent structure.heading_empty structure.heading_jump structure.id_duplicate structure.link_unnamed].include?(id) return {status: :not_applicable, findings: [], reason: :redirect} end if document && !document[:content_confident] && %w[structure.h1_absent structure.heading_empty structure.heading_jump].include?(id) return {status: :skipped, findings: [], reason: :content_extraction_uncertain} end return {status: :evaluated, findings: public_send(id.tr(".", "_"), document, context)} end |
.finding(rule, document, message, observation, line = nil, related_locations = []) ⇒ Object
179 180 181 182 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 179 def finding(rule, document, , observation, line = nil, = []) {document: document, message: , observation: observation, line: line, related_locations: , severity: SEVERITIES[rule]} end |
.graph(context) ⇒ Object
161 162 163 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 161 def graph(context) context[:graph] end |
.identity_canonical_invalid(document, _context) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 71 def identity_canonical_invalid(document, _context) Array(document[:canonicals]).map do |canonical| href = canonical[:href].to_s next if canonical[:href] == "" if canonical[:href].nil? next finding("identity.canonical_invalid", document, "Canonical declaration has no href.", {href: nil}, canonical[:line]) end next if href.match?(/[^\x00-\x7F]/) begin URI.parse(href) nil rescue URI::InvalidURIError finding("identity.canonical_invalid", document, "Canonical declaration is not a valid URI reference.", {href: href}, canonical[:line]) end end.compact end |
.identity_canonical_multiple(document, context) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 88 def identity_canonical_multiple(document, context) declarations = Array(document[:canonicals]).map do |canonical| href = canonical[:href].to_s next if href.empty? [context[:resolver].resolve(href, document)[:url], canonical] rescue [href, canonical] end.compact values = declarations.map(&:first).uniq return [] unless values.length > 1 locations = declarations.map { |value, canonical| {line: canonical[:line], href: canonical[:href]} } [finding("identity.canonical_multiple", document, "Multiple distinct canonical URLs are declared.", {canonicals: values}, locations.first[:line], locations.drop(1))] end |
.identity_canonical_target_missing(document, context) ⇒ Object
100 101 102 103 104 105 106 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 100 def identity_canonical_target_missing(document, context) Array(document[:canonicals]).map do |canonical| resolution = context[:resolver].resolve(canonical[:href], document) next unless resolution[:status] == :missing finding("identity.canonical_target_missing", document, "Canonical points to a missing local target.", {href: canonical[:href], path: resolution[:path]}, canonical[:line]) end.compact end |
.links(document, context) ⇒ Object
150 151 152 153 154 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 150 def links(document, context) graph = context[:graph] return graph.edges_for(document) if graph && graph.respond_to?(:edges_for) Array(graph&.edges).select { |edge| edge[:source][:identity] == document[:identity] } end |
.links_fragment_missing(document, context) ⇒ Object
53 54 55 56 57 58 59 60 61 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 53 def links_fragment_missing(document, context) links(document, context).map do |edge| resolution, occurrence = edge[:resolution], edge[:occurrence] next unless resolution[:status] == :local && resolution[:fragment] && resolution[:target] next if asset_target?(resolution[:target]) next if resolution[:target][:ids]&.include?(resolution[:fragment]) finding("links.fragment_missing", document, "Local target does not contain the requested fragment.", {href: occurrence[:href], fragment: resolution[:fragment], target: resolution[:target][:identity]}, occurrence[:line]) end.compact end |
.links_target_missing(document, context) ⇒ Object
46 47 48 49 50 51 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 46 def links_target_missing(document, context) links(document, context).map do |edge| next unless edge[:resolution][:status] == :missing finding("links.target_missing", document, "Link points to a missing local target.", {href: edge[:occurrence][:href], path: edge[:resolution][:path]}, edge[:occurrence][:line]) end.compact end |
.root?(document, context) ⇒ Boolean
165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 165 def root?(document, context) config = context[:configuration].respond_to?(:to_h) ? context[:configuration].to_h : context[:configuration] roots = config.dig(:graph, :roots) || config.dig("graph", "roots") || ["/"] base = context.fetch(:site_config, {}).fetch("baseurl", "").to_s.sub(%r{/\z}, "") roots.map(&:to_s).any? do |root| route = root == "/" && !base.empty? ? "#{base}/" : root document[:route].to_s == route || (root == "/" && document[:route].to_s == "/index.html") || begin URI.parse(document[:url].to_s).path == route rescue URI::InvalidURIError false end end end |
.structure_h1_absent(document, _context) ⇒ Object
112 113 114 115 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 112 def structure_h1_absent(document, _context) return [] unless document[:content_confident] document[:headings].none? { |h| h[:level] == 1 && h[:name_supported] != false && !h[:hidden] && !h[:name].to_s.empty? } ? [finding("structure.h1_absent", document, "No level-one heading detected within the extracted content.", {})] : [] end |
.structure_heading_empty(document, _context) ⇒ Object
117 118 119 120 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 117 def structure_heading_empty(document, _context) return [] unless document[:content_confident] document[:headings].map { |heading| finding("structure.heading_empty", document, "Heading has no supported accessible name.", {level: heading[:level]}, heading[:line]) if heading[:name_supported] != false && !heading[:hidden] && heading[:name].to_s.empty? }.compact end |
.structure_heading_jump(document, _context) ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 122 def structure_heading_jump(document, _context) return [] unless document[:content_confident] previous = nil document[:headings].map do |heading| result = if previous && heading[:level] > previous[:level] + 1 && !heading[:hidden] && heading[:name_supported] && previous[:name_supported] finding("structure.heading_jump", document, "Heading level jumps from h#{previous[:level]} to h#{heading[:level]}.", {from: previous[:level], to: heading[:level]}, heading[:line], [{line: previous[:line]}]) end previous = heading unless heading[:hidden] result end.compact end |
.structure_id_duplicate(document, _context) ⇒ Object
134 135 136 137 138 139 140 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 134 def structure_id_duplicate(document, _context) locations = document[:duplicate_id_locations] || {} Array(document[:duplicate_ids]).map do |id| lines = Array(locations[id]) finding("structure.id_duplicate", document, "ID is declared more than once.", {id: id}, lines.first, lines.drop(1).map { |line| {line: line, id: id} }) end end |
.structure_link_unnamed(document, _context) ⇒ Object
142 143 144 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 142 def structure_link_unnamed(document, _context) document[:links].map { |link| finding("structure.link_unnamed", document, "Navigation link has no supported accessible name.", {href: link[:href]}, link[:line]) if link[:kind] == :navigation && !link[:hidden] && link[:name_supported] && link[:name].to_s.empty? }.compact end |
.structure_title_missing(document, _context) ⇒ Object
108 109 110 |
# File 'lib/jekyll/agent_audit/rules/publication.rb', line 108 def structure_title_missing(document, _context) document[:title].to_s.strip.empty? ? [finding("structure.title_missing", document, "Rendered HTML has no nonempty title.", {})] : [] end |