metanorma-mko 1.0.0

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.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ class Ids < Lutaml::Model::Serializable
7
+ attribute :canonical, :string
8
+ attribute :short, :string
9
+ # parsed identity (issue #50 follow-up 1): consumers filter and
10
+ # group on series/number/part — they must never decompose the
11
+ # canonical string themselves (a silent parse failure on the
12
+ # consumer side hid a whole corpus lane from retrieval)
13
+ attribute :series, :string
14
+ attribute :number, :string
15
+ attribute :part, :string
16
+ attribute :docid, :string, collection: true
17
+ attribute :urn, :string, collection: true
18
+
19
+ json do
20
+ map "canonical", to: :canonical
21
+ map "short", to: :short
22
+ map "series", to: :series
23
+ map "number", to: :number
24
+ map "part", to: :part
25
+ map "docid", to: :docid
26
+ map "urn", to: :urn
27
+ end
28
+ end
29
+
30
+ class TitleEntry < Lutaml::Model::Serializable
31
+ attribute :lang, :string
32
+ attribute :text, :string
33
+
34
+ json do
35
+ map "lang", to: :lang
36
+ map "text", to: :text
37
+ end
38
+ end
39
+
40
+ class DateEntry < Lutaml::Model::Serializable
41
+ attribute :type, :string
42
+ attribute :on, :string
43
+
44
+ json do
45
+ map "type", to: :type
46
+ map "on", to: :on
47
+ end
48
+ end
49
+
50
+ class StatusInfo < Lutaml::Model::Serializable
51
+ attribute :stage, :string
52
+ attribute :substage, :string
53
+ attribute :abbreviation, :string
54
+
55
+ json do
56
+ map "stage", to: :stage
57
+ map "substage", to: :substage
58
+ map "abbreviation", to: :abbreviation
59
+ end
60
+ end
61
+
62
+ class RelationEntry < Lutaml::Model::Serializable
63
+ attribute :type, :string
64
+ attribute :to, :string
65
+
66
+ json do
67
+ map "type", to: :type
68
+ map "to", to: :to
69
+ end
70
+ end
71
+
72
+ class StructureNode < Lutaml::Model::Serializable
73
+ attribute :id, :string
74
+ attribute :number, :string
75
+ attribute :title, :string
76
+ attribute :children, StructureNode, collection: true,
77
+ default: -> { [] }
78
+
79
+ json do
80
+ map "id", to: :id
81
+ map "number", to: :number
82
+ map "title", to: :title
83
+ map "children", to: :children
84
+ end
85
+ end
86
+
87
+ # document.json — identity, status, and the numbered structure tree.
88
+ class Document < Lutaml::Model::Serializable
89
+ attribute :ids, Ids
90
+ attribute :flavor, :string
91
+ attribute :doctype, :string
92
+ attribute :titles, TitleEntry, collection: true
93
+ attribute :edition, :string
94
+ attribute :languages, :string, collection: true
95
+ attribute :status, StatusInfo
96
+ # derived from succession edges (Relaton relations verbatim) —
97
+ # authoritative over the hand-entered status field when present
98
+ attribute :derived_status, :string
99
+ attribute :dates, DateEntry, collection: true
100
+ attribute :relations, RelationEntry, collection: true
101
+ attribute :structure, StructureNode, collection: true
102
+
103
+ json do
104
+ map "ids", to: :ids
105
+ map "flavor", to: :flavor
106
+ map "doctype", to: :doctype
107
+ map "titles", to: :titles
108
+ map "edition", to: :edition
109
+ map "languages", to: :languages
110
+ map "status", to: :status
111
+ map "derived_status", to: :derived_status, render_nil: false
112
+ map "dates", to: :dates
113
+ map "relations", to: :relations
114
+ map "structure", to: :structure
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ # One graph edge between knowledge objects (or an external key).
7
+ # Kinds: part_of, cites, defines, tested_by, class_of, amends,
8
+ # variant_of, supersedes.
9
+ class Edge < Lutaml::Model::Serializable
10
+ attribute :from, :string
11
+ attribute :to, :string
12
+ attribute :kind, :string
13
+
14
+ json do
15
+ map "from", to: :from
16
+ map "to", to: :to
17
+ map "kind", to: :kind
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ # identifiers.json — the document's identifiers; `parsed` carries
7
+ # the pubid monogem's native JSON when a flavor covers it.
8
+ class IdentifierInfo < Lutaml::Model::Serializable
9
+ attribute :original, :string
10
+ attribute :type, :string
11
+ attribute :primary, :boolean
12
+ attribute :parsed, :hash
13
+
14
+ json do
15
+ map "original", to: :original
16
+ map "type", to: :type
17
+ map "primary", to: :primary
18
+ map "parsed", to: :parsed
19
+ end
20
+ end
21
+
22
+ class Identifiers < Lutaml::Model::Serializable
23
+ attribute :identifiers, IdentifierInfo, collection: true
24
+
25
+ json do
26
+ map "identifiers", to: :identifiers
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ # JSON Schema (draft 2020-12) generation for the MKO wire
7
+ # contract. The lutaml schema classes are the single source of
8
+ # truth: types come from the attribute declarations, wire names
9
+ # from the classes' json mappings (e.g. :klass renders as
10
+ # "class"). Consumers validate against these schemas — never
11
+ # against hand-written copies that drift.
12
+ module JsonSchema
13
+ PRIMITIVES = {
14
+ "Lutaml::Model::Type::String" => { "type" => "string" },
15
+ "Lutaml::Model::Type::Integer" => { "type" => "integer" },
16
+ "Lutaml::Model::Type::Boolean" => { "type" => "boolean" },
17
+ "Lutaml::Model::Type::Hash" => { "type" => "object" },
18
+ }.freeze
19
+
20
+ class << self
21
+ def schema_for(klass, seen = {})
22
+ return { "$ref" => seen[klass] } if seen.key?(klass)
23
+
24
+ seen[klass] = "#"
25
+ properties = klass.attributes.each_with_object({}) do |(name, attr), h|
26
+ h[wire_name(klass, name)] = property_for(attr, seen)
27
+ end
28
+ {
29
+ "$schema" => "https://json-schema.org/draft/2020-12/schema",
30
+ "title" => klass.name.to_s.split("::").last,
31
+ "type" => "object",
32
+ "properties" => properties,
33
+ }
34
+ end
35
+
36
+ # The published contract: one schema per wire component and
37
+ # per typed payload, plus the consumer excerpt.
38
+ def all
39
+ Unit # trigger payload-class autoloads
40
+ schemas = {
41
+ "unit" => schema_for(Unit),
42
+ "edge" => schema_for(Edge),
43
+ "manifest" => schema_for(Manifest),
44
+ "document" => schema_for(Document),
45
+ "identifiers" => schema_for(Identifiers),
46
+ "collection" => schema_for(Schema::Collection),
47
+ "payload-table" => schema_for(TablePayload),
48
+ "payload-formula" => schema_for(FormulaPayload),
49
+ "payload-figure" => schema_for(FigurePayload),
50
+ "payload-term" => schema_for(TermPayload),
51
+ "payload-requirement" => schema_for(RequirementPayload),
52
+ "payload-reference" => schema_for(ReferencePayload),
53
+ "payload-clause" => schema_for(SectionPayload),
54
+ "payload-annex" => schema_for(SectionPayload),
55
+ }
56
+ schemas["excerpt"] = excerpt_schema(schemas)
57
+ schemas
58
+ end
59
+
60
+ # Consumer clause: text with [[u:…]] references plus resolved
61
+ # producer payload blocks (MN 116 §consumer-refs).
62
+ def excerpt_schema(payload_schemas)
63
+ block = {
64
+ "type" => "object",
65
+ "properties" => {
66
+ "unit_id" => { "type" => "string", "pattern" => "\\Au:" },
67
+ "type" => { "type" => "string" },
68
+ "docidentifier" => { "type" => "string" },
69
+ "edition" => { "type" => "string" },
70
+ "payload" => {
71
+ "oneOf" => payload_schemas
72
+ .select { |name, _| name.start_with?("payload-") }
73
+ .map { |_, s| s }.uniq,
74
+ },
75
+ },
76
+ "required" => %w[unit_id type payload],
77
+ }
78
+ {
79
+ "$schema" => "https://json-schema.org/draft/2020-12/schema",
80
+ "title" => "Excerpt",
81
+ "type" => "object",
82
+ "properties" => {
83
+ "text" => { "type" => "string" },
84
+ "blocks" => { "type" => "array", "items" => block },
85
+ },
86
+ "required" => %w[text blocks],
87
+ }
88
+ end
89
+
90
+ private
91
+
92
+ def property_for(attr, seen)
93
+ type = primitive(attr) || schema_for(attr.type, seen)
94
+ attr.collection? ? { "type" => "array", "items" => type } : type
95
+ end
96
+
97
+ def primitive(attr)
98
+ PRIMITIVES[attr.type.name.to_s]
99
+ end
100
+
101
+ # The json mappings are the wire contract; the attribute name
102
+ # is only the Ruby-side handle.
103
+ def wire_name(klass, attr_name)
104
+ wire_names(klass)[attr_name] || attr_name.to_s
105
+ end
106
+
107
+ def wire_names(klass)
108
+ @wire_names ||= {}
109
+ @wire_names[klass] ||= begin
110
+ mapping = klass.mappings_for(:json)
111
+ mapping.mappings_hash.values.each_with_object({}) do |rule, h|
112
+ h[rule.to] = rule.name.to_s
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ class ManifestComponent < Lutaml::Model::Serializable
7
+ attribute :name, :string
8
+ attribute :file, :string
9
+ attribute :media_type, :string
10
+ attribute :count, :integer
11
+ attribute :hash, :string
12
+
13
+ json do
14
+ map "name", to: :name
15
+ map "file", to: :file
16
+ map "media_type", to: :media_type
17
+ map "count", to: :count
18
+ map "hash", to: :hash
19
+ end
20
+ end
21
+
22
+ class Generated < Lutaml::Model::Serializable
23
+ attribute :tool, :string
24
+ attribute :schema_version, :string
25
+ attribute :flavor, :string
26
+ attribute :timestamp, :string
27
+
28
+ json do
29
+ map "tool", to: :tool
30
+ map "schema_version", to: :schema_version
31
+ map "flavor", to: :flavor
32
+ map "timestamp", to: :timestamp
33
+ end
34
+ end
35
+
36
+ class Manifest < Lutaml::Model::Serializable
37
+ attribute :schema, :string
38
+ attribute :schema_version, :string
39
+ attribute :generated, Generated
40
+ attribute :source_file, :string
41
+ attribute :source_hash, :string
42
+ attribute :components, ManifestComponent, collection: true
43
+ attribute :license, :string
44
+ attribute :copyright, :string
45
+
46
+ json do
47
+ map "schema", to: :schema
48
+ map "schema_version", to: :schema_version
49
+ map "generated", to: :generated
50
+ map "source", to: :source_file
51
+ map "source-hash", to: :source_hash
52
+ map "components", to: :components
53
+ map "license", to: :license
54
+ map "copyright", to: :copyright
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ module Schema
6
+ class TableColumn < Lutaml::Model::Serializable
7
+ attribute :label, :string
8
+ # typed column: unit references the units register (#55 GAP-2)
9
+ attribute :type, :string
10
+ attribute :unit, :string
11
+ attribute :units, :string
12
+
13
+ json do
14
+ map "label", to: :label
15
+ map "type", to: :type, render_nil: false
16
+ map "unit", to: :unit, render_nil: false
17
+ map "units", to: :units, render_nil: false
18
+ end
19
+ end
20
+
21
+ class TablePayload < Lutaml::Model::Serializable
22
+ attribute :caption, :string
23
+ attribute :columns, TableColumn, collection: true
24
+ attribute :rows, :string, collection: true, default: -> { [] }
25
+ # The block's native JSON object — the Mirror node tree
26
+ # (lossless; the serialization the JS renderer consumes).
27
+ # Provenance/fidelity, not an embedding form: the retrieval
28
+ # contract is the typed text fields.
29
+ attribute :mirror, :hash
30
+
31
+ json do
32
+ map "caption", to: :caption
33
+ map "columns", to: :columns
34
+ map "rows", to: :rows
35
+ map "mirror", to: :mirror
36
+ end
37
+
38
+ def embed_text
39
+ cols = columns.map { |c| c.label + (c.unit ? " [#{c.unit}]" : "") }
40
+ .join(", ")
41
+ "Table: #{caption}; columns: #{cols}; " +
42
+ rows.map { |r| "row: #{r}" }.join(" | ")
43
+ end
44
+ end
45
+
46
+ class FormulaPayload < Lutaml::Model::Serializable
47
+ attribute :asciimath, :string
48
+ attribute :mathml, :string
49
+ # units register references (units.jsonl entry ids)
50
+ attribute :units, :string, collection: true, default: -> { [] }
51
+ # reserved: evaluation forms (typed params + expression in a
52
+ # declared language); absent unless authored (#55 GAP-3)
53
+ attribute :semantics, :hash
54
+ attribute :description, :string
55
+
56
+ # Derived encodings — never canonical; each names its converter
57
+ # (#55: re-encoding at the producer is a provenance break)
58
+ class DerivedForm < Lutaml::Model::Serializable
59
+ attribute :form, :string
60
+ attribute :converter, :string
61
+
62
+ json do
63
+ map "form", to: :form
64
+ map "converter", to: :converter
65
+ end
66
+ end
67
+ attribute :latex, DerivedForm
68
+ attribute :omml, DerivedForm
69
+
70
+ json do
71
+ map "asciimath", to: :asciimath
72
+ map "mathml", to: :mathml
73
+ map "units", to: :units, render_empty: false
74
+ map "semantics", to: :semantics, render_nil: false
75
+ map "latex", to: :latex, render_nil: false
76
+ map "omml", to: :omml, render_nil: false
77
+ map "description", to: :description
78
+ end
79
+ end
80
+
81
+ class FigurePayload < Lutaml::Model::Serializable
82
+ attribute :alt, :string
83
+ attribute :uri, :string
84
+ # hash-addressed bundle asset ("assets/<sha256>"), when bytes
85
+ # were available at export (data URIs or assets_from)
86
+ attribute :asset, :string
87
+ attribute :caption, :string
88
+ # Native JSON object: the Mirror node tree (see TablePayload).
89
+ attribute :mirror, :hash
90
+
91
+ json do
92
+ map "alt", to: :alt
93
+ map "asset", to: :asset
94
+ map "uri", to: :uri
95
+ map "caption", to: :caption
96
+ map "mirror", to: :mirror
97
+ end
98
+ end
99
+
100
+ class TermSourceEntry < Lutaml::Model::Serializable
101
+ attribute :citeas, :string
102
+
103
+ json do
104
+ map "citeas", to: :citeas
105
+ end
106
+ end
107
+
108
+ class TermPayload < Lutaml::Model::Serializable
109
+ attribute :concept, :string
110
+ attribute :designations, :string, collection: true, default: -> { [] }
111
+ attribute :admitted, :string, collection: true, default: -> { [] }
112
+ attribute :deprecated, :string, collection: true, default: -> { [] }
113
+ attribute :definition, :string
114
+ attribute :sources, TermSourceEntry, collection: true, default: -> { [] }
115
+
116
+ json do
117
+ map "concept", to: :concept
118
+ map "designations", to: :designations
119
+ map "admitted", to: :admitted, render_empty: false
120
+ map "deprecated", to: :deprecated, render_empty: false
121
+ map "definition", to: :definition
122
+ map "sources", to: :sources
123
+ end
124
+ end
125
+
126
+ class RequirementPayload < Lutaml::Model::Serializable
127
+ attribute :identifier, :string
128
+ attribute :klass, :string
129
+ attribute :obligation, :string
130
+ attribute :subject, :string
131
+ attribute :statement, :string
132
+ attribute :inherits, :string, collection: true, default: -> { [] }
133
+
134
+ json do
135
+ map "identifier", to: :identifier
136
+ map "class", to: :klass
137
+ map "obligation", to: :obligation
138
+ map "subject", to: :subject
139
+ map "statement", to: :statement
140
+ map "inherits", to: :inherits
141
+ end
142
+ end
143
+
144
+ # Coverage of a section unit (#56): the deterministic summary and
145
+ # the ordered membership. Children are unit ids in document order;
146
+ # the same relation the part_of edges carry bottom-up, given here
147
+ # top-down so tree navigation never re-derives structure from
148
+ # anchor strings.
149
+ class SectionPayload < Lutaml::Model::Serializable
150
+ # deterministic composition (title + covered sub-clauses) —
151
+ # never model-inferred; consumers may override with their own
152
+ # LLM summaries, but the default requires none
153
+ attribute :summary, :string
154
+ attribute :children, :string, collection: true, default: -> { [] }
155
+
156
+ json do
157
+ map "summary", to: :summary
158
+ map "children", to: :children
159
+ end
160
+ end
161
+
162
+ class ReferencePayload < Lutaml::Model::Serializable
163
+ attribute :key, :string
164
+ attribute :cited, :string
165
+
166
+ json do
167
+ map "key", to: :key
168
+ map "cited", to: :cited
169
+ end
170
+ end
171
+
172
+ # One knowledge object. payload is one of the typed payloads above,
173
+ # chosen by type. text is the human-readable display text.
174
+ class Unit < Lutaml::Model::Serializable
175
+ attribute :id, :string
176
+ attribute :type, :string
177
+ attribute :anchor, :string
178
+ attribute :number, :string
179
+ # document order as an integer (#56): reading order is a sort,
180
+ # never a parse of dotted anchor strings. Position, not content —
181
+ # excluded from the content hash by contract.
182
+ attribute :ordinal, :integer
183
+ # the anchor a READER cites (issue #50 follow-up 2): the parent
184
+ # clause number for embedded objects (tables/formulas/figures),
185
+ # the unit's own number for top-level sections. Consumers cite
186
+ # identically without walking the parent chain.
187
+ attribute :cite_as, :string
188
+ attribute :title, :string
189
+ attribute :parent, :string
190
+ attribute :breadcrumb, :string, collection: true, default: -> { [] }
191
+ attribute :obligation, :string
192
+ attribute :lang, :string
193
+ # authoring-time situating note (#53 item 7): AI-assisted,
194
+ # editor-approved, versioned with the document. Wire-ready —
195
+ # emitted the day the authoring system ships it.
196
+ attribute :ai_note, :string
197
+ attribute :text, :string
198
+ attribute :payload, :hash
199
+ attribute :hash, :string
200
+
201
+ json do
202
+ map "id", to: :id
203
+ map "type", to: :type
204
+ map "anchor", to: :anchor
205
+ map "number", to: :number
206
+ map "ordinal", to: :ordinal, render_nil: false
207
+ map "cite_as", to: :cite_as
208
+ map "title", to: :title
209
+ map "parent", to: :parent
210
+ map "breadcrumb", to: :breadcrumb
211
+ map "obligation", to: :obligation
212
+ map "lang", to: :lang
213
+ map "ai_note", to: :ai_note, render_nil: false
214
+ map "text", to: :text
215
+ map "payload", to: :payload
216
+ map "hash", to: :hash
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metanorma
4
+ module Mko
5
+ # The MKO wire schema. Every class is a lutaml-model Serializable with
6
+ # JSON mappings — one schema, framework-generated serializations only.
7
+ module Schema
8
+ autoload :Manifest, "metanorma/mko/schema/manifest"
9
+ autoload :Document, "metanorma/mko/schema/document"
10
+ autoload :StructureNode, "metanorma/mko/schema/document"
11
+ autoload :Unit, "metanorma/mko/schema/unit"
12
+ autoload :Edge, "metanorma/mko/schema/edge"
13
+ autoload :Identifiers, "metanorma/mko/schema/identifiers"
14
+ autoload :JsonSchema, "metanorma/mko/schema/json_schema"
15
+ autoload :Collection, "metanorma/mko/schema/collection"
16
+ autoload :CollectionMember, "metanorma/mko/schema/collection"
17
+ autoload :IdentifierInfo, "metanorma/mko/schema/identifiers"
18
+ end
19
+ end
20
+ end