suma 0.5.7 → 0.5.8
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/.github/workflows/nightly-benchmark.yml +25 -0
- data/.github/workflows/release-provenance.yml +38 -0
- data/.rubocop_todo.yml +84 -0
- data/CLAUDE.md +44 -0
- data/Rakefile +13 -1
- data/bin/benchmark_corpus +138 -0
- data/bin/release_provenance +40 -0
- data/lib/suma/build_plan.rb +71 -0
- data/lib/suma/change_classification.rb +37 -0
- data/lib/suma/change_impact.rb +61 -0
- data/lib/suma/change_manifest.rb +147 -0
- data/lib/suma/cli/build.rb +23 -0
- data/lib/suma/cli/compare.rb +49 -2
- data/lib/suma/cli/core.rb +83 -0
- data/lib/suma/cli/export.rb +140 -10
- data/lib/suma/cli/extract_terms.rb +11 -0
- data/lib/suma/cli/generate_register.rb +12 -0
- data/lib/suma/cli/resolve.rb +45 -0
- data/lib/suma/cli/validate.rb +90 -0
- data/lib/suma/cli.rb +1 -0
- data/lib/suma/closure_validator.rb +317 -0
- data/lib/suma/corpus_graph.rb +141 -0
- data/lib/suma/express_schema.rb +22 -28
- data/lib/suma/null_cache.rb +4 -0
- data/lib/suma/owl_projection.rb +611 -0
- data/lib/suma/processor.rb +55 -6
- data/lib/suma/provenance.rb +159 -0
- data/lib/suma/publication.rb +108 -0
- data/lib/suma/register_manifest_generator.rb +76 -8
- data/lib/suma/schema_cache.rb +72 -13
- data/lib/suma/schema_category.rb +31 -2
- data/lib/suma/schema_collection.rb +58 -5
- data/lib/suma/schema_exporter.rb +5 -5
- data/lib/suma/schema_naming.rb +29 -33
- data/lib/suma/schema_profile.rb +134 -0
- data/lib/suma/term_classification.rb +9 -2
- data/lib/suma/term_extractor.rb +76 -35
- data/lib/suma/urn_resolver.rb +318 -0
- data/lib/suma/version.rb +1 -1
- data/lib/suma/worker_pool.rb +62 -0
- data/lib/suma.rb +12 -0
- metadata +19 -2
data/lib/suma/cli/export.rb
CHANGED
|
@@ -14,6 +14,11 @@ module Suma
|
|
|
14
14
|
# +Suma::SchemaCategory+, the single source of truth. The exporter
|
|
15
15
|
# itself never classifies — it consumes loaded ExpressSchema
|
|
16
16
|
# objects whose output paths were set by this adapter.
|
|
17
|
+
#
|
|
18
|
+
# With +--publication SCHEMA_ID+, exports an application-protocol
|
|
19
|
+
# publication bundle instead: the lead schema plus the transitive
|
|
20
|
+
# USE-FROM/REFERENCE closure (+Suma::Publication+), written flat
|
|
21
|
+
# under +<output>/<lead_id>/+.
|
|
17
22
|
class Export < Thor
|
|
18
23
|
desc "export *FILES",
|
|
19
24
|
"Export EXPRESS schemas from manifest files or " \
|
|
@@ -24,6 +29,22 @@ module Suma
|
|
|
24
29
|
desc: "Include annotations (remarks/comments)"
|
|
25
30
|
option :zip, type: :boolean, default: false,
|
|
26
31
|
desc: "Create ZIP archive of exported schemas"
|
|
32
|
+
option :publication, type: :string, banner: "SCHEMA_ID",
|
|
33
|
+
desc: "Export an AP publication bundle: the " \
|
|
34
|
+
"lead schema plus its transitive " \
|
|
35
|
+
"USE-FROM/REFERENCE closure (manifest " \
|
|
36
|
+
"input only)"
|
|
37
|
+
option :profile, type: :string, default: "iso10303",
|
|
38
|
+
desc: "Corpus profile (iso10303 or generic)"
|
|
39
|
+
option :format, type: :string, default: "express", banner: "FORMAT",
|
|
40
|
+
desc: "Output format: express (default) or owl"
|
|
41
|
+
option :corpus, type: :boolean, default: false,
|
|
42
|
+
desc: "OWL only: emit one ontology for the whole " \
|
|
43
|
+
"manifest instead of one per schema"
|
|
44
|
+
option :urn, type: :string, banner: "URN",
|
|
45
|
+
desc: "OWL only: base URN for IRIs, before the " \
|
|
46
|
+
":tech component " \
|
|
47
|
+
"(e.g. urn:iso:std:iso:10303:-2:ed-2:en)"
|
|
27
48
|
|
|
28
49
|
def export(*files)
|
|
29
50
|
require "expressir"
|
|
@@ -47,8 +68,87 @@ module Suma
|
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
def run(files, options)
|
|
50
|
-
|
|
71
|
+
format = options[:format].to_s.downcase
|
|
72
|
+
case format
|
|
73
|
+
when "express"
|
|
74
|
+
run_express(files, options)
|
|
75
|
+
when "owl"
|
|
76
|
+
run_owl(files, options)
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "Unsupported format: #{options[:format]}. " \
|
|
79
|
+
"Use express or owl."
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_express(files, options)
|
|
84
|
+
schemas = if options[:publication]
|
|
85
|
+
publication_bundle(files, options)
|
|
86
|
+
else
|
|
87
|
+
files.flat_map { |file| build_schemas(file, options) }
|
|
88
|
+
end
|
|
89
|
+
run_exporter(schemas, options)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# OWL projection: one Turtle file per schema (under its category
|
|
93
|
+
# directory, mirroring the EXPRESS export layout). With --corpus a
|
|
94
|
+
# top-level ontology importing every schema ontology is added
|
|
95
|
+
# alongside them.
|
|
96
|
+
def run_owl(files, options)
|
|
97
|
+
validate_owl_input!(files, options)
|
|
98
|
+
projection = OwlProjection.new(files.first, urn: options[:urn])
|
|
99
|
+
if options[:corpus]
|
|
100
|
+
write_schema_ontologies(projection, files.first)
|
|
101
|
+
write_ontology(projection, output_root.join("corpus.ttl"),
|
|
102
|
+
projection.to_turtle)
|
|
103
|
+
else
|
|
104
|
+
write_schema_ontologies(projection, files.first)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_owl_input!(files, options)
|
|
109
|
+
unless bundle_input?(files)
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"--format owl requires exactly one schema manifest file " \
|
|
112
|
+
"(.yml/.yaml) as input"
|
|
113
|
+
end
|
|
114
|
+
if options[:publication]
|
|
115
|
+
raise ArgumentError,
|
|
116
|
+
"--publication bundles EXPRESS files and cannot be " \
|
|
117
|
+
"combined with --format owl"
|
|
118
|
+
end
|
|
119
|
+
return if options[:urn]
|
|
120
|
+
|
|
121
|
+
raise ArgumentError, "--format owl requires --urn BASE_URN"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def write_schema_ontologies(projection, manifest_file)
|
|
125
|
+
manifest = Expressir::SchemaManifest.from_file(manifest_file)
|
|
126
|
+
manifest.schemas.each do |entry|
|
|
127
|
+
target = output_root
|
|
128
|
+
.join(SchemaCategory.for_schema(
|
|
129
|
+
id: entry.id, path: entry.path,
|
|
130
|
+
profile: options[:profile]
|
|
131
|
+
).directory)
|
|
132
|
+
.join("#{entry.id}.ttl")
|
|
133
|
+
write_ontology(projection, target,
|
|
134
|
+
projection.to_turtle(schema_id: entry.id),
|
|
135
|
+
schema_id: entry.id)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def write_ontology(projection, target, turtle, schema_id: nil)
|
|
140
|
+
problems = projection.validate(schema_id: schema_id)
|
|
141
|
+
unless problems.empty?
|
|
142
|
+
raise Error, "OWL projection of #{target} is invalid: " \
|
|
143
|
+
"#{problems.join('; ')}"
|
|
144
|
+
end
|
|
51
145
|
|
|
146
|
+
FileUtils.mkdir_p(target.dirname)
|
|
147
|
+
File.write(target, turtle)
|
|
148
|
+
Utils.log "Wrote OWL ontology: #{target}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_exporter(schemas, options)
|
|
52
152
|
exporter = SchemaExporter.new(
|
|
53
153
|
schemas: schemas,
|
|
54
154
|
output_path: options[:output],
|
|
@@ -57,16 +157,41 @@ module Suma
|
|
|
57
157
|
create_zip: options[:zip],
|
|
58
158
|
},
|
|
59
159
|
)
|
|
60
|
-
|
|
61
160
|
exporter.export
|
|
62
161
|
end
|
|
63
162
|
|
|
64
|
-
|
|
163
|
+
# Build the publication bundle schemas, rooted at
|
|
164
|
+
# <output>/<lead_id>/ so the AP volume lands in its own directory.
|
|
165
|
+
def publication_bundle(files, options)
|
|
166
|
+
validate_bundle_input!(files)
|
|
167
|
+
lead_id = options[:publication]
|
|
168
|
+
publication = Publication.new(
|
|
169
|
+
lead_id: lead_id,
|
|
170
|
+
schema_manifest_file: files.first,
|
|
171
|
+
profile: options[:profile],
|
|
172
|
+
)
|
|
173
|
+
publication.schemas(output_path: output_root.join(lead_id).to_s)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate_bundle_input!(files)
|
|
177
|
+
return if bundle_input?(files)
|
|
178
|
+
|
|
179
|
+
raise ArgumentError,
|
|
180
|
+
"--publication requires exactly one schema manifest file " \
|
|
181
|
+
"(.yml/.yaml) as input"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def bundle_input?(files)
|
|
185
|
+
files.length == 1 &&
|
|
186
|
+
%w[.yml .yaml].include?(File.extname(files.first).downcase)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_schemas(file, options)
|
|
65
190
|
case File.extname(file).downcase
|
|
66
191
|
when ".yml", ".yaml"
|
|
67
|
-
build_from_manifest(file)
|
|
192
|
+
build_from_manifest(file, options)
|
|
68
193
|
when ".exp"
|
|
69
|
-
[build_standalone(file)]
|
|
194
|
+
[build_standalone(file, options)]
|
|
70
195
|
else
|
|
71
196
|
raise ArgumentError, "Unsupported file type: #{file}. " \
|
|
72
197
|
"Only .yml, .yaml, and .exp files are " \
|
|
@@ -74,27 +199,32 @@ module Suma
|
|
|
74
199
|
end
|
|
75
200
|
end
|
|
76
201
|
|
|
77
|
-
def build_from_manifest(file)
|
|
202
|
+
def build_from_manifest(file, options)
|
|
78
203
|
manifest = Expressir::SchemaManifest.from_file(file)
|
|
79
|
-
manifest.schemas.map
|
|
204
|
+
manifest.schemas.map do |entry|
|
|
205
|
+
build_from_manifest_entry(entry, options)
|
|
206
|
+
end
|
|
80
207
|
end
|
|
81
208
|
|
|
82
|
-
def build_from_manifest_entry(entry)
|
|
83
|
-
category = SchemaCategory.for_schema(id: entry.id, path: entry.path
|
|
209
|
+
def build_from_manifest_entry(entry, options)
|
|
210
|
+
category = SchemaCategory.for_schema(id: entry.id, path: entry.path,
|
|
211
|
+
profile: options[:profile])
|
|
84
212
|
ExpressSchema.new(
|
|
85
213
|
id: entry.id,
|
|
86
214
|
path: entry.path.to_s,
|
|
87
215
|
output_path: output_root.join(category.directory).to_s,
|
|
88
216
|
is_standalone_file: false,
|
|
217
|
+
profile: options[:profile],
|
|
89
218
|
)
|
|
90
219
|
end
|
|
91
220
|
|
|
92
|
-
def build_standalone(exp_file)
|
|
221
|
+
def build_standalone(exp_file, options)
|
|
93
222
|
ExpressSchema.new(
|
|
94
223
|
id: nil,
|
|
95
224
|
path: File.expand_path(exp_file),
|
|
96
225
|
output_path: output_root.to_s,
|
|
97
226
|
is_standalone_file: true,
|
|
227
|
+
profile: options[:profile],
|
|
98
228
|
)
|
|
99
229
|
end
|
|
100
230
|
|
|
@@ -13,6 +13,14 @@ module Suma
|
|
|
13
13
|
option :urn, type: :string, required: true, aliases: "-u",
|
|
14
14
|
desc: "URN for the dataset source " \
|
|
15
15
|
"(used for section references)"
|
|
16
|
+
option :profile, type: :string, default: "iso10303",
|
|
17
|
+
desc: "Corpus profile (iso10303 or generic)"
|
|
18
|
+
option :edition, type: :numeric,
|
|
19
|
+
desc: "Owning edition recorded on each concept"
|
|
20
|
+
option :language_codes, type: :array,
|
|
21
|
+
desc: "Additional linked localization " \
|
|
22
|
+
"languages (the primary language is " \
|
|
23
|
+
"always included)"
|
|
16
24
|
|
|
17
25
|
def extract_terms(schema_manifest_file, output_path)
|
|
18
26
|
TermExtractor.new(
|
|
@@ -20,6 +28,9 @@ module Suma
|
|
|
20
28
|
output_path,
|
|
21
29
|
language_code: options[:language_code],
|
|
22
30
|
urn: options[:urn],
|
|
31
|
+
profile: options[:profile],
|
|
32
|
+
edition: options[:edition],
|
|
33
|
+
language_codes: options[:language_codes],
|
|
23
34
|
).call
|
|
24
35
|
end
|
|
25
36
|
end
|
|
@@ -17,6 +17,14 @@ module Suma
|
|
|
17
17
|
desc: "Human-readable reference label"
|
|
18
18
|
option :owner, type: :string, default: Suma::RegisterManifestGenerator::DEFAULT_OWNER,
|
|
19
19
|
desc: "Owner of the dataset (e.g. 'ISO/TC 184/SC 4')"
|
|
20
|
+
option :profile, type: :string, default: "iso10303",
|
|
21
|
+
desc: "Corpus profile (iso10303 or generic)"
|
|
22
|
+
option :year, type: :numeric,
|
|
23
|
+
desc: "Edition year for the register"
|
|
24
|
+
option :supersedes, type: :string,
|
|
25
|
+
desc: "URN/ref of the superseded edition"
|
|
26
|
+
option :edition, type: :numeric,
|
|
27
|
+
desc: "Edition number (tagged as edition:N)"
|
|
20
28
|
|
|
21
29
|
def generate_register(schema_manifest_file, output_path)
|
|
22
30
|
RegisterManifestGenerator.new(
|
|
@@ -27,6 +35,10 @@ module Suma
|
|
|
27
35
|
ref: options[:ref],
|
|
28
36
|
language_code: options[:language_code],
|
|
29
37
|
owner: options[:owner],
|
|
38
|
+
profile: options[:profile],
|
|
39
|
+
year: options[:year],
|
|
40
|
+
supersedes: options[:supersedes],
|
|
41
|
+
edition: options[:edition],
|
|
30
42
|
).generate
|
|
31
43
|
end
|
|
32
44
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module Cli
|
|
7
|
+
# Resolve command: emit canonical JSON + HTML artifacts for URNs.
|
|
8
|
+
class Resolve < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "resolve URN", "Resolve a URN to canonical JSON + HTML artifacts"
|
|
14
|
+
option :manifest, type: :string, required: true,
|
|
15
|
+
desc: "Schema manifest used for schema/entity URNs"
|
|
16
|
+
option :urn, type: :string,
|
|
17
|
+
desc: "Corpus URN base (defaults to the register URN)"
|
|
18
|
+
option :concepts, type: :string,
|
|
19
|
+
desc: "Directory of Glossarist concept YAML files"
|
|
20
|
+
option :register, type: :string,
|
|
21
|
+
desc: "Glossarist register.yaml (dataset URN)"
|
|
22
|
+
option :output, type: :string, aliases: "-o", required: true,
|
|
23
|
+
desc: "Artifact output directory"
|
|
24
|
+
option :all, type: :boolean, default: false,
|
|
25
|
+
desc: "Emit artifacts for every known URN"
|
|
26
|
+
def resolve(urn = nil)
|
|
27
|
+
resolver = UrnResolver.new(
|
|
28
|
+
manifest_file: options[:manifest],
|
|
29
|
+
urn: options[:urn],
|
|
30
|
+
concepts_dir: options[:concepts],
|
|
31
|
+
register_file: options[:register],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if options[:all]
|
|
35
|
+
resolver.write(options[:output])
|
|
36
|
+
elsif urn
|
|
37
|
+
resolver.write_urn(urn, options[:output])
|
|
38
|
+
Utils.log "Resolved #{urn}"
|
|
39
|
+
else
|
|
40
|
+
raise ArgumentError, "provide a URN or use --all"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/suma/cli/validate.rb
CHANGED
|
@@ -26,6 +26,22 @@ module Suma
|
|
|
26
26
|
puts LinkValidation.generate_summary(result)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
desc "closure SCHEMAS_MANIFEST [DOCUMENTS_PATH]",
|
|
30
|
+
"Validate referential closure: every EXPRESS-internal reference " \
|
|
31
|
+
"resolves; optionally merge documentation-layer link failures"
|
|
32
|
+
option :format, type: :string, default: "terminal",
|
|
33
|
+
desc: "Report format: terminal, json, yaml"
|
|
34
|
+
def closure(schemas_manifest_file, documents_path = nil)
|
|
35
|
+
graph = CorpusGraph.from_manifest(schemas_manifest_file)
|
|
36
|
+
violations = ClosureValidator.new(graph)
|
|
37
|
+
.validate_manifest(schemas_manifest_file)
|
|
38
|
+
if documents_path
|
|
39
|
+
violations += documentation_violations(schemas_manifest_file,
|
|
40
|
+
documents_path)
|
|
41
|
+
end
|
|
42
|
+
emit_closure_report(violations)
|
|
43
|
+
end
|
|
44
|
+
|
|
29
45
|
desc "mappings DOCUMENTS_PATH",
|
|
30
46
|
"Validate module mapping.yaml links and reference paths " \
|
|
31
47
|
"against the parsed module corpus; only NEW drift " \
|
|
@@ -48,6 +64,80 @@ module Suma
|
|
|
48
64
|
|
|
49
65
|
private
|
|
50
66
|
|
|
67
|
+
# Print the closure report and raise on any unresolved reference.
|
|
68
|
+
def emit_closure_report(violations)
|
|
69
|
+
if violations.empty?
|
|
70
|
+
puts closure_success_message
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts format_closure_report(violations)
|
|
75
|
+
raise Thor::Error,
|
|
76
|
+
"Referential closure failed: #{violations.size} unresolved " \
|
|
77
|
+
"reference(s)"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def closure_success_message
|
|
81
|
+
case options[:format]
|
|
82
|
+
when "json"
|
|
83
|
+
require "json"
|
|
84
|
+
{ "violations" => [] }.to_json
|
|
85
|
+
when "yaml"
|
|
86
|
+
require "yaml"
|
|
87
|
+
{ "violations" => [] }.to_yaml
|
|
88
|
+
else
|
|
89
|
+
"Referential closure OK: all references resolve."
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def format_closure_report(violations)
|
|
94
|
+
case options[:format]
|
|
95
|
+
when "json"
|
|
96
|
+
require "json"
|
|
97
|
+
{ "violations" => violations.map(&:to_h) }.to_json
|
|
98
|
+
when "yaml"
|
|
99
|
+
require "yaml"
|
|
100
|
+
{ "violations" => violations.map(&:to_h) }.to_yaml
|
|
101
|
+
else
|
|
102
|
+
violations.map { |v| format_closure_line(v) }
|
|
103
|
+
.sort
|
|
104
|
+
.join("\n")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_closure_line(violation)
|
|
109
|
+
location = [violation.file, violation.line].compact.join(":")
|
|
110
|
+
"[#{violation.kind}] #{violation.schema_id}/#{violation.owner_id} " \
|
|
111
|
+
"#{violation.reference} (#{location}) — #{violation.reason}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Merge documentation-layer link failures into the closure report:
|
|
115
|
+
# the same unresolved-reference predicate, applied by the doc-layer
|
|
116
|
+
# extractor.
|
|
117
|
+
def documentation_violations(schemas_file, documents_path)
|
|
118
|
+
result = LinkValidation.new(
|
|
119
|
+
schemas_file: schemas_file,
|
|
120
|
+
documents_path: documents_path,
|
|
121
|
+
output_file: nil,
|
|
122
|
+
).call
|
|
123
|
+
result.unresolved.map { |issue| documentation_violation(issue) }
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
Utils.log "documentation-layer validation skipped: #{e.message}"
|
|
126
|
+
[]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def documentation_violation(issue)
|
|
130
|
+
ClosureValidator::UnresolvedReference.new(
|
|
131
|
+
file: issue.file.to_s,
|
|
132
|
+
line: issue.line,
|
|
133
|
+
schema_id: issue.link.to_s.split(".").first,
|
|
134
|
+
owner_id: "(documentation)",
|
|
135
|
+
kind: "documentation",
|
|
136
|
+
reference: issue.link,
|
|
137
|
+
reason: issue.reason,
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
51
141
|
def default_baseline_path(documents_path)
|
|
52
142
|
File.join(documents_path, "mapping-drift-baseline.yml")
|
|
53
143
|
end
|
data/lib/suma/cli.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Suma
|
|
|
12
12
|
autoload :GenerateRegister, "suma/cli/generate_register"
|
|
13
13
|
autoload :GenerateSchemas, "suma/cli/generate_schemas"
|
|
14
14
|
autoload :Reformat, "suma/cli/reformat"
|
|
15
|
+
autoload :Resolve, "suma/cli/resolve"
|
|
15
16
|
autoload :Validate, "suma/cli/validate"
|
|
16
17
|
autoload :ValidateLinks, "suma/cli/validate_links"
|
|
17
18
|
end
|