glossarist 2.6.4 → 2.6.5
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 +4 -4
- data/.rubocop_todo.yml +11 -111
- data/Gemfile +0 -2
- data/README.adoc +207 -1
- data/glossarist.gemspec +1 -1
- data/lib/glossarist/asset_reference.rb +16 -0
- data/lib/glossarist/bibliographic_reference.rb +16 -0
- data/lib/glossarist/concept_enricher.rb +1 -0
- data/lib/glossarist/concept_reference.rb +4 -0
- data/lib/glossarist/concept_validator.rb +27 -56
- data/lib/glossarist/dataset_validator.rb +30 -34
- data/lib/glossarist/gcr_validator.rb +26 -101
- data/lib/glossarist/reference_extractor.rb +80 -10
- data/lib/glossarist/reference_resolver.rb +1 -0
- data/lib/glossarist/validation/asset_index.rb +113 -0
- data/lib/glossarist/validation/bibliography_index.rb +121 -0
- data/lib/glossarist/validation/rules/asciidoc_xref_rule.rb +60 -0
- data/lib/glossarist/validation/rules/authoritative_source_rule.rb +47 -0
- data/lib/glossarist/validation/rules/base.rb +46 -0
- data/lib/glossarist/validation/rules/bibliography_yaml_rule.rb +37 -0
- data/lib/glossarist/validation/rules/citation_completeness_rule.rb +63 -0
- data/lib/glossarist/validation/rules/concept_context.rb +45 -0
- data/lib/glossarist/validation/rules/concept_count_rule.rb +34 -0
- data/lib/glossarist/validation/rules/concept_id_rule.rb +29 -0
- data/lib/glossarist/validation/rules/concept_id_uniqueness_rule.rb +42 -0
- data/lib/glossarist/validation/rules/concept_mention_rule.rb +44 -0
- data/lib/glossarist/validation/rules/concept_status_rule.rb +36 -0
- data/lib/glossarist/validation/rules/concept_uri_rule.rb +30 -0
- data/lib/glossarist/validation/rules/dataset_context.rb +99 -0
- data/lib/glossarist/validation/rules/date_type_rule.rb +54 -0
- data/lib/glossarist/validation/rules/date_validity_rule.rb +66 -0
- data/lib/glossarist/validation/rules/definition_content_rule.rb +41 -0
- data/lib/glossarist/validation/rules/designation_status_rule.rb +45 -0
- data/lib/glossarist/validation/rules/designation_type_rule.rb +55 -0
- data/lib/glossarist/validation/rules/duplicate_term_rule.rb +63 -0
- data/lib/glossarist/validation/rules/entry_status_rule.rb +39 -0
- data/lib/glossarist/validation/rules/filename_id_rule.rb +35 -0
- data/lib/glossarist/validation/rules/gcr_context.rb +92 -0
- data/lib/glossarist/validation/rules/image_reference_rule.rb +73 -0
- data/lib/glossarist/validation/rules/l10n_uuid_integrity_rule.rb +40 -0
- data/lib/glossarist/validation/rules/language_code_format_rule.rb +39 -0
- data/lib/glossarist/validation/rules/language_coverage_rule.rb +37 -0
- data/lib/glossarist/validation/rules/language_list_rule.rb +46 -0
- data/lib/glossarist/validation/rules/localization_presence_rule.rb +25 -0
- data/lib/glossarist/validation/rules/orphaned_bibliography_rule.rb +64 -0
- data/lib/glossarist/validation/rules/orphaned_images_rule.rb +68 -0
- data/lib/glossarist/validation/rules/orphaned_l10n_files_rule.rb +39 -0
- data/lib/glossarist/validation/rules/preferred_term_rule.rb +41 -0
- data/lib/glossarist/validation/rules/registry.rb +42 -0
- data/lib/glossarist/validation/rules/related_concept_cycle_rule.rb +102 -0
- data/lib/glossarist/validation/rules/related_concept_rule.rb +40 -0
- data/lib/glossarist/validation/rules/related_concept_symmetry_rule.rb +87 -0
- data/lib/glossarist/validation/rules/source_type_rule.rb +63 -0
- data/lib/glossarist/validation/rules/terms_presence_rule.rb +39 -0
- data/lib/glossarist/validation/rules.rb +85 -0
- data/lib/glossarist/validation/validation_issue.rb +39 -0
- data/lib/glossarist/validation.rb +12 -0
- data/lib/glossarist/validation_result.rb +26 -9
- data/lib/glossarist/version.rb +1 -1
- data/lib/glossarist.rb +3 -0
- metadata +60 -15
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class OrphanedBibliographyRule < Base
|
|
7
|
+
def code = "GLS-020"
|
|
8
|
+
def category = :references
|
|
9
|
+
def severity = "warning"
|
|
10
|
+
def scope = :collection
|
|
11
|
+
|
|
12
|
+
def applicable?(context)
|
|
13
|
+
context.bibliography_index.entries.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check(context)
|
|
17
|
+
extractor = ReferenceExtractor.new
|
|
18
|
+
bib_index = context.bibliography_index
|
|
19
|
+
referenced_anchors = Set.new
|
|
20
|
+
|
|
21
|
+
context.concepts.each do |concept|
|
|
22
|
+
concept.localizations.each do |l10n|
|
|
23
|
+
texts = extract_texts(l10n)
|
|
24
|
+
texts.each do |text|
|
|
25
|
+
next unless text
|
|
26
|
+
extractor.extract_from_text(text).each do |ref|
|
|
27
|
+
if ref.is_a?(BibliographicReference)
|
|
28
|
+
referenced_anchors.add(ref.anchor)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
issues = []
|
|
36
|
+
bib_index.each_entry do |entry|
|
|
37
|
+
next if referenced_anchors.any? { |a| bib_index.resolve?(a) }
|
|
38
|
+
|
|
39
|
+
anchor = entry[:anchor]
|
|
40
|
+
issues << issue(
|
|
41
|
+
"Orphaned bibliography entry: '#{anchor}'",
|
|
42
|
+
code: code, severity: severity,
|
|
43
|
+
location: "bibliography.yaml",
|
|
44
|
+
suggestion: "Remove the entry or reference it from a concept",
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
issues
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def extract_texts(l10n)
|
|
54
|
+
texts = []
|
|
55
|
+
(l10n.data&.definition || []).each { |d| texts << d.content if d.content }
|
|
56
|
+
(l10n.data&.notes || []).each { |n| texts << n.content if n.content }
|
|
57
|
+
(l10n.data&.examples || []).each { |e| texts << e.content if e.content }
|
|
58
|
+
texts
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class OrphanedImagesRule < Base
|
|
7
|
+
def code = "GLS-021"
|
|
8
|
+
def category = :references
|
|
9
|
+
def severity = "warning"
|
|
10
|
+
def scope = :collection
|
|
11
|
+
|
|
12
|
+
def applicable?(context)
|
|
13
|
+
context.asset_index.paths.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check(context)
|
|
17
|
+
extractor = ReferenceExtractor.new
|
|
18
|
+
referenced_paths = Set.new
|
|
19
|
+
|
|
20
|
+
context.concepts.each do |concept|
|
|
21
|
+
# Text-embedded image refs
|
|
22
|
+
concept.localizations.each do |l10n|
|
|
23
|
+
texts = extract_texts(l10n)
|
|
24
|
+
texts.each do |text|
|
|
25
|
+
next unless text
|
|
26
|
+
extractor.extract_from_text(text).each do |ref|
|
|
27
|
+
if ref.is_a?(AssetReference)
|
|
28
|
+
referenced_paths.add(ref.path)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Model-level asset refs
|
|
35
|
+
extractor.extract_asset_refs_from_concept(concept).each do |ref|
|
|
36
|
+
referenced_paths.add(ref.path)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
issues = []
|
|
41
|
+
context.asset_index.each_path do |path|
|
|
42
|
+
next if referenced_paths.include?(path)
|
|
43
|
+
|
|
44
|
+
issues << issue(
|
|
45
|
+
"Orphaned image: #{path} (not referenced by any concept)",
|
|
46
|
+
code: code, severity: severity,
|
|
47
|
+
location: path,
|
|
48
|
+
suggestion: "Remove the image or reference it from a concept",
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
issues
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def extract_texts(l10n)
|
|
58
|
+
texts = []
|
|
59
|
+
(l10n.data&.definition || []).each { |d| texts << d.content if d.content }
|
|
60
|
+
(l10n.data&.notes || []).each { |n| texts << n.content if n.content }
|
|
61
|
+
(l10n.data&.examples || []).each { |e| texts << e.content if e.content }
|
|
62
|
+
texts
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class OrphanedL10nFilesRule < Base
|
|
7
|
+
def code = "GLS-019"
|
|
8
|
+
def category = :integrity
|
|
9
|
+
def severity = "warning"
|
|
10
|
+
def scope = :collection
|
|
11
|
+
|
|
12
|
+
def applicable?(context)
|
|
13
|
+
context.localization_index.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check(context)
|
|
17
|
+
lc_index = context.localization_index
|
|
18
|
+
referenced = context.referenced_l10n_uuids
|
|
19
|
+
issues = []
|
|
20
|
+
|
|
21
|
+
lc_index.each do |uuid, path|
|
|
22
|
+
next if referenced.include?(uuid)
|
|
23
|
+
|
|
24
|
+
issues << issue(
|
|
25
|
+
"Orphaned localization file: #{File.basename(path)} " \
|
|
26
|
+
"(not referenced by any concept)",
|
|
27
|
+
code: code, severity: severity,
|
|
28
|
+
location: File.basename(path),
|
|
29
|
+
suggestion: "Delete the file or add a reference from a managed concept",
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
issues
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class PreferredTermRule < Base
|
|
7
|
+
def code = "GLS-301"
|
|
8
|
+
def category = :quality
|
|
9
|
+
def severity = "warning"
|
|
10
|
+
def scope = :concept
|
|
11
|
+
|
|
12
|
+
def applicable?(context)
|
|
13
|
+
context.concept.localizations&.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check(context)
|
|
17
|
+
concept = context.concept
|
|
18
|
+
fname = context.file_name
|
|
19
|
+
issues = []
|
|
20
|
+
|
|
21
|
+
concept.localizations.each do |l10n|
|
|
22
|
+
lang = l10n.language_code || "unknown"
|
|
23
|
+
terms = l10n.data&.terms || []
|
|
24
|
+
next if terms.empty?
|
|
25
|
+
next if terms.any? { |t| t.normative_status == "preferred" }
|
|
26
|
+
|
|
27
|
+
issues << issue(
|
|
28
|
+
"has #{terms.size} term(s) but none are preferred",
|
|
29
|
+
code: code, severity: severity,
|
|
30
|
+
location: "#{fname}/#{lang}",
|
|
31
|
+
suggestion: "Set normative_status: preferred on the primary term",
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
issues
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
module Registry
|
|
7
|
+
@rules = []
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
|
|
10
|
+
def self.register(rule_class)
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
@rules << rule_class unless @rules.include?(rule_class)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.all
|
|
17
|
+
@rules.map(&:new)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.for_category(category)
|
|
21
|
+
all.select { |r| r.category == category }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.for_scope(scope)
|
|
25
|
+
all.select { |r| r.scope == scope }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.find(code)
|
|
29
|
+
all.find { |r| r.code == code }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.reset!
|
|
33
|
+
@mutex.synchronize { @rules.clear }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.rule_classes
|
|
37
|
+
@rules.dup
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class RelatedConceptCycleRule < Base
|
|
7
|
+
def code = "GLS-113"
|
|
8
|
+
def category = :references
|
|
9
|
+
def severity = "error"
|
|
10
|
+
def scope = :collection
|
|
11
|
+
|
|
12
|
+
DIRECTIONAL = %w[supersedes deprecates narrower].freeze
|
|
13
|
+
|
|
14
|
+
def applicable?(context)
|
|
15
|
+
context.concepts.any? { |c| c.related&.any? }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check(context)
|
|
19
|
+
graph = build_directed_graph(context.concepts)
|
|
20
|
+
issues = []
|
|
21
|
+
|
|
22
|
+
cycles = detect_cycles(graph)
|
|
23
|
+
cycles.each do |cycle|
|
|
24
|
+
path = cycle.join(" -> ")
|
|
25
|
+
issues << issue(
|
|
26
|
+
"Circular relation chain detected: #{path}",
|
|
27
|
+
location: cycle.first,
|
|
28
|
+
suggestion: "Break the cycle by removing or redirecting one of the relations",
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
issues
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_directed_graph(concepts)
|
|
38
|
+
graph = {}
|
|
39
|
+
concepts.each do |c|
|
|
40
|
+
next unless c.related&.any?
|
|
41
|
+
|
|
42
|
+
src_id = c.data&.id&.to_s
|
|
43
|
+
next unless src_id
|
|
44
|
+
|
|
45
|
+
c.related.each do |rel|
|
|
46
|
+
next unless DIRECTIONAL.include?(rel.type)
|
|
47
|
+
|
|
48
|
+
target_id = resolve_target_id(rel)
|
|
49
|
+
next unless target_id
|
|
50
|
+
|
|
51
|
+
(graph[src_id] ||= []) << target_id
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
graph
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_target_id(rel)
|
|
58
|
+
ref = rel.ref
|
|
59
|
+
return nil unless ref
|
|
60
|
+
|
|
61
|
+
if ref.is_a?(Glossarist::Citation)
|
|
62
|
+
ref.id || ref.text
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detect_cycles(graph)
|
|
67
|
+
visited = Set.new
|
|
68
|
+
stack = Set.new
|
|
69
|
+
cycles = []
|
|
70
|
+
|
|
71
|
+
graph.keys.each do |node|
|
|
72
|
+
next if visited.include?(node)
|
|
73
|
+
|
|
74
|
+
dfs(node, graph, visited, stack, [], cycles)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
cycles
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def dfs(node, graph, visited, stack, path, cycles)
|
|
81
|
+
return if visited.include?(node) && !stack.include?(node)
|
|
82
|
+
|
|
83
|
+
if stack.include?(node)
|
|
84
|
+
cycle_start = path.index(node)
|
|
85
|
+
cycles << path[cycle_start..] + [node] if cycle_start
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
visited.add(node)
|
|
90
|
+
stack.add(node)
|
|
91
|
+
path = path + [node]
|
|
92
|
+
|
|
93
|
+
(graph[node] || []).each do |neighbor|
|
|
94
|
+
dfs(neighbor, graph, visited, stack, path, cycles)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
stack.delete(node)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class RelatedConceptRule < Base
|
|
7
|
+
def code = "GLS-200"
|
|
8
|
+
def category = :schema
|
|
9
|
+
def severity = "error"
|
|
10
|
+
def scope = :concept
|
|
11
|
+
|
|
12
|
+
VALID_TYPES = Glossarist::GlossaryDefinition::RELATED_CONCEPT_TYPES
|
|
13
|
+
|
|
14
|
+
def applicable?(context)
|
|
15
|
+
context.concept.related&.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check(context)
|
|
19
|
+
concept = context.concept
|
|
20
|
+
fname = context.file_name
|
|
21
|
+
issues = []
|
|
22
|
+
|
|
23
|
+
(concept.related || []).each_with_index do |rel, idx|
|
|
24
|
+
unless VALID_TYPES.include?(rel.type)
|
|
25
|
+
issues << issue(
|
|
26
|
+
"related concept #{idx + 1} has invalid type '#{rel.type}'",
|
|
27
|
+
code: code, severity: severity,
|
|
28
|
+
location: fname,
|
|
29
|
+
suggestion: "Use one of: #{VALID_TYPES.join(', ')}",
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
issues
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class RelatedConceptSymmetryRule < Base
|
|
7
|
+
def code = "GLS-112"
|
|
8
|
+
def category = :references
|
|
9
|
+
def severity = "warning"
|
|
10
|
+
def scope = :collection
|
|
11
|
+
|
|
12
|
+
INVERSE = {
|
|
13
|
+
"supersedes" => "superseded_by",
|
|
14
|
+
"superseded_by" => "supersedes",
|
|
15
|
+
"narrower" => "broader",
|
|
16
|
+
"broader" => "narrower",
|
|
17
|
+
"deprecates" => "deprecated_by",
|
|
18
|
+
"deprecated_by" => "deprecates",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DIRECTIONAL = %w[supersedes deprecates narrower].freeze
|
|
22
|
+
|
|
23
|
+
def applicable?(context)
|
|
24
|
+
context.concepts.any? { |c| c.related&.any? }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check(context)
|
|
28
|
+
index = build_relation_index(context.concepts)
|
|
29
|
+
issues = []
|
|
30
|
+
|
|
31
|
+
context.concepts.each do |concept|
|
|
32
|
+
next unless concept.related&.any?
|
|
33
|
+
|
|
34
|
+
concept_id = concept.data&.id&.to_s || "unknown"
|
|
35
|
+
(concept.related || []).each do |rel|
|
|
36
|
+
inverse = INVERSE[rel.type]
|
|
37
|
+
next unless inverse
|
|
38
|
+
|
|
39
|
+
target_id = resolve_target_id(rel)
|
|
40
|
+
next unless target_id
|
|
41
|
+
|
|
42
|
+
targets = index[target_id]
|
|
43
|
+
next if targets && targets.any? { |r| r.type == inverse }
|
|
44
|
+
|
|
45
|
+
issues << issue(
|
|
46
|
+
"#{concept_id}: #{rel.type} #{target_id} but #{target_id} has no #{inverse} back-link",
|
|
47
|
+
location: concept_id,
|
|
48
|
+
suggestion: "Add a #{inverse} relation on #{target_id} pointing back to #{concept_id}",
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
issues
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def build_relation_index(concepts)
|
|
59
|
+
index = {}
|
|
60
|
+
concepts.each do |c|
|
|
61
|
+
next unless c.related&.any?
|
|
62
|
+
|
|
63
|
+
src_id = c.data&.id&.to_s
|
|
64
|
+
next unless src_id
|
|
65
|
+
|
|
66
|
+
c.related.each do |rel|
|
|
67
|
+
target_id = resolve_target_id(rel)
|
|
68
|
+
next unless target_id
|
|
69
|
+
|
|
70
|
+
(index[src_id] ||= []) << rel
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
index
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve_target_id(rel)
|
|
77
|
+
ref = rel.ref
|
|
78
|
+
return nil unless ref
|
|
79
|
+
|
|
80
|
+
if ref.is_a?(Glossarist::Citation)
|
|
81
|
+
ref.id || ref.text
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class SourceEnumRule < Base
|
|
7
|
+
def code = "GLS-202"
|
|
8
|
+
def category = :schema
|
|
9
|
+
def severity = "error"
|
|
10
|
+
def scope = :concept
|
|
11
|
+
|
|
12
|
+
VALID_TYPES = Glossarist::GlossaryDefinition::CONCEPT_SOURCE_TYPES
|
|
13
|
+
VALID_STATUSES = Glossarist::GlossaryDefinition::CONCEPT_SOURCE_STATUSES
|
|
14
|
+
|
|
15
|
+
def applicable?(context)
|
|
16
|
+
context.concept.localizations&.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def check(context)
|
|
20
|
+
concept = context.concept
|
|
21
|
+
fname = context.file_name
|
|
22
|
+
issues = []
|
|
23
|
+
|
|
24
|
+
gather_all_sources(concept).each_with_index do |source, idx|
|
|
25
|
+
unless VALID_TYPES.include?(source.type)
|
|
26
|
+
issues << issue(
|
|
27
|
+
"source #{idx + 1} has invalid type '#{source.type}'",
|
|
28
|
+
code: "GLS-202", severity: severity,
|
|
29
|
+
location: fname,
|
|
30
|
+
suggestion: "Use one of: #{VALID_TYPES.join(', ')}",
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
next unless source.status && !VALID_STATUSES.include?(source.status)
|
|
35
|
+
|
|
36
|
+
issues << issue(
|
|
37
|
+
"source #{idx + 1} has invalid status '#{source.status}'",
|
|
38
|
+
code: "GLS-203", severity: severity,
|
|
39
|
+
location: fname,
|
|
40
|
+
suggestion: "Use one of: #{VALID_STATUSES.join(', ')}",
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
issues
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def gather_all_sources(concept)
|
|
50
|
+
sources = []
|
|
51
|
+
concept.localizations.each do |l10n|
|
|
52
|
+
(l10n.data&.sources || []).each { |s| sources << s }
|
|
53
|
+
(l10n.data&.definition || []).each { |d| (d.sources || []).each { |s| sources << s } }
|
|
54
|
+
(l10n.data&.notes || []).each { |n| (n.sources || []).each { |s| sources << s } }
|
|
55
|
+
(l10n.data&.examples || []).each { |e| (e.sources || []).each { |s| sources << s } }
|
|
56
|
+
end
|
|
57
|
+
sources
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Glossarist
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
class TermsPresenceRule < Base
|
|
7
|
+
def code = "GLS-005"
|
|
8
|
+
def category = :structure
|
|
9
|
+
def severity = "error"
|
|
10
|
+
def scope = :concept
|
|
11
|
+
|
|
12
|
+
def applicable?(context)
|
|
13
|
+
context.concept.localizations&.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check(context)
|
|
17
|
+
concept = context.concept
|
|
18
|
+
fname = context.file_name
|
|
19
|
+
issues = []
|
|
20
|
+
|
|
21
|
+
concept.localizations.each do |l10n|
|
|
22
|
+
lang = l10n.language_code || "unknown"
|
|
23
|
+
terms = l10n.data&.terms || []
|
|
24
|
+
next unless terms.empty?
|
|
25
|
+
|
|
26
|
+
issues << issue(
|
|
27
|
+
"#{fname}/#{lang}: must have at least 1 term",
|
|
28
|
+
code: code, severity: severity,
|
|
29
|
+
location: "#{fname}/#{lang}",
|
|
30
|
+
suggestion: "Add at least one term/designation to this localization",
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
issues
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rules/base"
|
|
4
|
+
require_relative "rules/registry"
|
|
5
|
+
require_relative "rules/dataset_context"
|
|
6
|
+
require_relative "rules/gcr_context"
|
|
7
|
+
require_relative "rules/concept_context"
|
|
8
|
+
|
|
9
|
+
# Load all rule definitions
|
|
10
|
+
require_relative "rules/concept_id_rule"
|
|
11
|
+
require_relative "rules/concept_id_uniqueness_rule"
|
|
12
|
+
require_relative "rules/localization_presence_rule"
|
|
13
|
+
require_relative "rules/entry_status_rule"
|
|
14
|
+
require_relative "rules/asciidoc_xref_rule"
|
|
15
|
+
require_relative "rules/image_reference_rule"
|
|
16
|
+
require_relative "rules/concept_mention_rule"
|
|
17
|
+
require_relative "rules/concept_count_rule"
|
|
18
|
+
require_relative "rules/language_list_rule"
|
|
19
|
+
require_relative "rules/language_coverage_rule"
|
|
20
|
+
require_relative "rules/filename_id_rule"
|
|
21
|
+
require_relative "rules/l10n_uuid_integrity_rule"
|
|
22
|
+
require_relative "rules/orphaned_l10n_files_rule"
|
|
23
|
+
require_relative "rules/orphaned_bibliography_rule"
|
|
24
|
+
require_relative "rules/orphaned_images_rule"
|
|
25
|
+
require_relative "rules/definition_content_rule"
|
|
26
|
+
require_relative "rules/preferred_term_rule"
|
|
27
|
+
require_relative "rules/duplicate_term_rule"
|
|
28
|
+
require_relative "rules/citation_completeness_rule"
|
|
29
|
+
require_relative "rules/authoritative_source_rule"
|
|
30
|
+
require_relative "rules/related_concept_rule"
|
|
31
|
+
require_relative "rules/concept_status_rule"
|
|
32
|
+
require_relative "rules/source_type_rule"
|
|
33
|
+
require_relative "rules/terms_presence_rule"
|
|
34
|
+
require_relative "rules/bibliography_yaml_rule"
|
|
35
|
+
require_relative "rules/concept_uri_rule"
|
|
36
|
+
require_relative "rules/related_concept_symmetry_rule"
|
|
37
|
+
require_relative "rules/related_concept_cycle_rule"
|
|
38
|
+
require_relative "rules/designation_status_rule"
|
|
39
|
+
require_relative "rules/date_type_rule"
|
|
40
|
+
require_relative "rules/language_code_format_rule"
|
|
41
|
+
require_relative "rules/designation_type_rule"
|
|
42
|
+
require_relative "rules/date_validity_rule"
|
|
43
|
+
|
|
44
|
+
# Register all built-in rules
|
|
45
|
+
module Glossarist
|
|
46
|
+
module Validation
|
|
47
|
+
module Rules
|
|
48
|
+
R = Registry
|
|
49
|
+
|
|
50
|
+
R.register(ConceptIdRule)
|
|
51
|
+
R.register(ConceptIdUniquenessRule)
|
|
52
|
+
R.register(LocalizationPresenceRule)
|
|
53
|
+
R.register(EntryStatusRule)
|
|
54
|
+
R.register(AsciidocXrefRule)
|
|
55
|
+
R.register(ImageReferenceRule)
|
|
56
|
+
R.register(ConceptMentionRule)
|
|
57
|
+
R.register(ConceptCountRule)
|
|
58
|
+
R.register(LanguageListRule)
|
|
59
|
+
R.register(LanguageCoverageRule)
|
|
60
|
+
R.register(FilenameIdRule)
|
|
61
|
+
R.register(L10nUuidIntegrityRule)
|
|
62
|
+
R.register(OrphanedL10nFilesRule)
|
|
63
|
+
R.register(OrphanedBibliographyRule)
|
|
64
|
+
R.register(OrphanedImagesRule)
|
|
65
|
+
R.register(DefinitionContentRule)
|
|
66
|
+
R.register(PreferredTermRule)
|
|
67
|
+
R.register(DuplicateTermRule)
|
|
68
|
+
R.register(CitationCompletenessRule)
|
|
69
|
+
R.register(AuthoritativeSourceRule)
|
|
70
|
+
R.register(RelatedConceptRule)
|
|
71
|
+
R.register(ConceptStatusRule)
|
|
72
|
+
R.register(SourceEnumRule)
|
|
73
|
+
R.register(TermsPresenceRule)
|
|
74
|
+
R.register(BibliographyYamlRule)
|
|
75
|
+
R.register(ConceptUriRule)
|
|
76
|
+
R.register(RelatedConceptSymmetryRule)
|
|
77
|
+
R.register(RelatedConceptCycleRule)
|
|
78
|
+
R.register(DesignationStatusRule)
|
|
79
|
+
R.register(DateTypeRule)
|
|
80
|
+
R.register(LanguageCodeFormatRule)
|
|
81
|
+
R.register(DesignationTypeRule)
|
|
82
|
+
R.register(DateValidityRule)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|