metanorma-mko 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.adoc +8 -0
- data/lib/metanorma/mko/language.rb +190 -0
- data/lib/metanorma/mko/schema/unit.rb +7 -0
- data/lib/metanorma/mko/version.rb +1 -1
- data/lib/metanorma/mko.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f884af7c676c4b65e7f157fc580605171ed9e35b857ea6ab6407cc158a6cfd5f
|
|
4
|
+
data.tar.gz: c01a9a9cf36a2ba2c933007605e550e3354193516798f213ebc584522ff9e4f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45c3f2a3c7226226a761c0684f8df548981c12ee2229f3f8ab8c37da42b1dc5c82613bda4c8ada865dceac397f550ba40d86fcc6330176c2f259bb1fd9ba1e81
|
|
7
|
+
data.tar.gz: 3b9e6249e7c78fd40010ea691ff92a152d1923eb07267f8346871cfd25fa808d29b87fdc8c9995552a86f7ed246018e5e6c0d30502204af783feb22c307072de
|
data/README.adoc
CHANGED
|
@@ -17,6 +17,14 @@ The FORMAT contract:
|
|
|
17
17
|
* the generated JSON Schemas — `Mko.json_schemas` (the schema classes
|
|
18
18
|
are the single source of truth; TS/Python consumers validate against
|
|
19
19
|
these, never hand-rolled copies);
|
|
20
|
+
* language resolution (`Mko::Language`) — W3C XML §2.12 nearest
|
|
21
|
+
ancestor-or-self `xml:lang`, then an honest stopword-ratio langid
|
|
22
|
+
(en/fr/es/de) over the element's own text, then the declared
|
|
23
|
+
document language, then an explicit `en` fallback. Every resolution
|
|
24
|
+
reports its provenance (`markup` / `heuristic` / `default` /
|
|
25
|
+
`fallback`); units carry it as `lang_source`. Consumer-side of
|
|
26
|
+
metanorma-standoc#1243: element-level `xml:lang` wins automatically
|
|
27
|
+
the day the converter emits it;
|
|
20
28
|
* the reference MCP server over any conforming bundle —
|
|
21
29
|
`Mko::Mcp::Server` (search_units / get_unit / walk_edges /
|
|
22
30
|
edition_diff; JSON-RPC 2.0 over stdio).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mko
|
|
5
|
+
# Language resolution for knowledge objects (#53 item 3),
|
|
6
|
+
# consumer-side of metanorma/metanorma-standoc#1243: standoc's
|
|
7
|
+
# converter does not emit element-level xml:lang and the models do
|
|
8
|
+
# not map it, so producer markup today cannot tell a translated
|
|
9
|
+
# annex from the body. Resolution follows W3C XML §2.12 — xml:lang
|
|
10
|
+
# is inheritable, an element's effective language is its nearest
|
|
11
|
+
# ancestor-or-self xml:lang — extended with an honest fallback
|
|
12
|
+
# chain for corpora whose markup carries no language at all:
|
|
13
|
+
#
|
|
14
|
+
# 1. "markup" — nearest ancestor-or-self xml:lang. Element-
|
|
15
|
+
# level attributes win automatically the day
|
|
16
|
+
# standoc#1243 lands; a root xml:lang covers
|
|
17
|
+
# every descendant until then.
|
|
18
|
+
# 2. "heuristic" — stopword-ratio langid over the element's own
|
|
19
|
+
# text (first SAMPLE_WORDS words; en/fr/es/de —
|
|
20
|
+
# the same heuristic the RAG consumer adopts).
|
|
21
|
+
# Fires only when NO xml:lang is in scope; an
|
|
22
|
+
# inconclusive sample scores nil, never an en
|
|
23
|
+
# bias. This is what tags a French terminology
|
|
24
|
+
# annex inside an EN edition as fr.
|
|
25
|
+
# 3. "default" — the document's declared language (the bibdata
|
|
26
|
+
# language, or the caller's document default).
|
|
27
|
+
# 4. "fallback" — DEFAULT_LANG with an explicit "fallback"
|
|
28
|
+
# source: nothing declared, nothing detected.
|
|
29
|
+
#
|
|
30
|
+
# Every Resolution carries its provenance in source — consumers
|
|
31
|
+
# can always tell markup-tagged language from a detected one.
|
|
32
|
+
module Language
|
|
33
|
+
# lang: a BCP 47 tag as declared ("en", "fr", ...); source:
|
|
34
|
+
# "markup" | "heuristic" | "default" | "fallback".
|
|
35
|
+
Resolution = Struct.new(:lang, :source, keyword_init: true)
|
|
36
|
+
|
|
37
|
+
# Assumed only when nothing is declared and detection fails —
|
|
38
|
+
# always reported with source "fallback", never silently.
|
|
39
|
+
DEFAULT_LANG = "en"
|
|
40
|
+
|
|
41
|
+
# The langid sample: the consumer contract is the first ~200
|
|
42
|
+
# words of the element's own text.
|
|
43
|
+
SAMPLE_WORDS = 200
|
|
44
|
+
|
|
45
|
+
# Honesty guards: below MIN_WORDS sampled words or MIN_HITS
|
|
46
|
+
# stopword hits there is no evidence — detect returns nil and
|
|
47
|
+
# the precedence chain falls through instead of guessing.
|
|
48
|
+
MIN_WORDS = 10
|
|
49
|
+
MIN_HITS = 3
|
|
50
|
+
|
|
51
|
+
# High-frequency function words per language (en/fr/es/de — the
|
|
52
|
+
# consumer's coverage need). Flat frequency lists, no weighting:
|
|
53
|
+
# every language scores over the same sample, so raw hit counts
|
|
54
|
+
# are the ratios. Collisions ("la" is fr+es) wash out over a
|
|
55
|
+
# real sample; the French single letters come from elision
|
|
56
|
+
# splits (l'annexe -> "l"), which EN/DE/ES prose never yields.
|
|
57
|
+
STOPWORDS = {
|
|
58
|
+
"en" => %w[
|
|
59
|
+
the of and to in is are that for with as this be on by at from
|
|
60
|
+
or an not it its shall should must may will can which
|
|
61
|
+
],
|
|
62
|
+
"fr" => %w[
|
|
63
|
+
le la les des de du un une et est sont dans pour avec sur au
|
|
64
|
+
aux par pas plus qui que ce cet cette ces être avoir selon
|
|
65
|
+
doit doivent entre comme tout toute tous l d qu
|
|
66
|
+
],
|
|
67
|
+
"es" => %w[
|
|
68
|
+
el la los las de del un una es son en para con por que no más
|
|
69
|
+
como se su sus al lo este esta estos estas y debe deben según
|
|
70
|
+
entre también sobre ser está están
|
|
71
|
+
],
|
|
72
|
+
"de" => %w[
|
|
73
|
+
der die das den dem des und ist in im zu mit für auf von ein
|
|
74
|
+
eine einer einem einen nicht sind oder bei nach über unter
|
|
75
|
+
soll sollen müssen kann können wird werden auch als durch
|
|
76
|
+
],
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
XML_NS = "http://www.w3.org/XML/1998/namespace"
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# The element-level machine. own: the element's own xml:lang
|
|
83
|
+
# (nil where models don't map it). inherited: the enclosing
|
|
84
|
+
# scope's Resolution (the nearest ancestor, already resolved).
|
|
85
|
+
# default: the document-level Resolution (or a bare tag).
|
|
86
|
+
def resolve(own: nil, text: nil, inherited: nil, default: nil)
|
|
87
|
+
own = normalize(own)
|
|
88
|
+
return Resolution.new(lang: own, source: "markup") if own
|
|
89
|
+
# xml:lang in scope: the nearest ancestor's markup stands and
|
|
90
|
+
# the heuristic stays off (W3C §2.12).
|
|
91
|
+
return inherited if inherited&.source == "markup"
|
|
92
|
+
|
|
93
|
+
detected = detect(text)
|
|
94
|
+
return Resolution.new(lang: detected, source: "heuristic") if detected
|
|
95
|
+
|
|
96
|
+
inherited || coerce(default) || fallback_resolution
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The document-level machine: the root xml:lang when carried,
|
|
100
|
+
# else the declared document language (bibdata), else
|
|
101
|
+
# detection over the document's own text, else the fallback.
|
|
102
|
+
def document(markup: nil, declared: nil, text: nil)
|
|
103
|
+
markup = normalize(markup)
|
|
104
|
+
return Resolution.new(lang: markup, source: "markup") if markup
|
|
105
|
+
|
|
106
|
+
declared = normalize(declared)
|
|
107
|
+
return Resolution.new(lang: declared, source: "default") if declared
|
|
108
|
+
|
|
109
|
+
detected = detect(text)
|
|
110
|
+
return Resolution.new(lang: detected, source: "heuristic") if detected
|
|
111
|
+
|
|
112
|
+
fallback_resolution
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# XML side (Nokogiri node): full §2.12 nearest
|
|
116
|
+
# ancestor-or-self over the live tree, then the element's own
|
|
117
|
+
# text, then the document default, then the fallback.
|
|
118
|
+
def for_node(node, default: nil)
|
|
119
|
+
markup = [node, *node.ancestors].filter_map do |n|
|
|
120
|
+
xml_lang_of(n)
|
|
121
|
+
end.first
|
|
122
|
+
return Resolution.new(lang: markup, source: "markup") if markup
|
|
123
|
+
|
|
124
|
+
detected = detect(node.text)
|
|
125
|
+
return Resolution.new(lang: detected, source: "heuristic") if detected
|
|
126
|
+
|
|
127
|
+
coerce(default) || fallback_resolution
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# XML side, document level: the root element's xml:lang, the
|
|
131
|
+
# declared language, the document's own text, the fallback.
|
|
132
|
+
def for_document(root, declared: nil)
|
|
133
|
+
document(markup: xml_lang_of(root), declared: declared,
|
|
134
|
+
text: root.text)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Stopword-ratio langid over the first SAMPLE_WORDS words of
|
|
138
|
+
# text. Honest scoring: nil when the sample is too small, has
|
|
139
|
+
# too few hits, or ties — never a default-to-en guess.
|
|
140
|
+
def detect(text)
|
|
141
|
+
words = text.to_s.downcase.gsub(/['’]/, " ")
|
|
142
|
+
.scan(/[[:alpha:]]+/).first(SAMPLE_WORDS)
|
|
143
|
+
return nil if words.size < MIN_WORDS
|
|
144
|
+
|
|
145
|
+
scores = STOPWORDS.transform_values do |list|
|
|
146
|
+
words.count { |word| list.include?(word) }
|
|
147
|
+
end
|
|
148
|
+
lang, hits = scores.max_by { |_, n| n }
|
|
149
|
+
return nil if hits < MIN_HITS
|
|
150
|
+
return nil if scores.any? { |l, n| l != lang && n == hits }
|
|
151
|
+
|
|
152
|
+
lang
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A tag worth believing: stripped, downcased (BCP 47 tags are
|
|
156
|
+
# case-insensitive); "" and "und" carry no information (XML
|
|
157
|
+
# §2.12), so inheritance keeps walking.
|
|
158
|
+
def normalize(tag)
|
|
159
|
+
tag = tag.to_s.strip.downcase
|
|
160
|
+
tag.empty? || tag == "und" ? nil : tag
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def xml_lang_of(node)
|
|
166
|
+
attr = node.attribute_with_ns("lang", XML_NS)
|
|
167
|
+
return normalize(attr.value) if attr
|
|
168
|
+
|
|
169
|
+
# namespace-stripped trees keep a literal "xml:lang" name
|
|
170
|
+
normalize(node["xml:lang"])
|
|
171
|
+
rescue NoMethodError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def coerce(value)
|
|
176
|
+
return value if value.is_a?(Resolution)
|
|
177
|
+
|
|
178
|
+
tag = normalize(value)
|
|
179
|
+
tag && Resolution.new(lang: tag, source: "default")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The explicit last resort: undeclared and undetectable.
|
|
183
|
+
# source "fallback" says so — never a silent en.
|
|
184
|
+
def fallback_resolution
|
|
185
|
+
Resolution.new(lang: DEFAULT_LANG, source: "fallback")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -190,6 +190,12 @@ module Metanorma
|
|
|
190
190
|
attribute :breadcrumb, :string, collection: true, default: -> { [] }
|
|
191
191
|
attribute :obligation, :string
|
|
192
192
|
attribute :lang, :string
|
|
193
|
+
# provenance of lang (#53 item 3, Mko::Language): "markup"
|
|
194
|
+
# (xml:lang in scope), "heuristic" (detected from the unit's
|
|
195
|
+
# own text), "default" (the declared document language),
|
|
196
|
+
# "fallback" (undeclared and undetectable). Optional so
|
|
197
|
+
# pre-resolution bundles stay schema-valid.
|
|
198
|
+
attribute :lang_source, :string
|
|
193
199
|
# authoring-time situating note (#53 item 7): AI-assisted,
|
|
194
200
|
# editor-approved, versioned with the document. Wire-ready —
|
|
195
201
|
# emitted the day the authoring system ships it.
|
|
@@ -210,6 +216,7 @@ module Metanorma
|
|
|
210
216
|
map "breadcrumb", to: :breadcrumb
|
|
211
217
|
map "obligation", to: :obligation
|
|
212
218
|
map "lang", to: :lang
|
|
219
|
+
map "lang_source", to: :lang_source, render_nil: false
|
|
213
220
|
map "ai_note", to: :ai_note, render_nil: false
|
|
214
221
|
map "text", to: :text
|
|
215
222
|
map "payload", to: :payload
|
data/lib/metanorma/mko.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metanorma-mko
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -111,6 +111,7 @@ files:
|
|
|
111
111
|
- lib/metanorma/mko/bundle.rb
|
|
112
112
|
- lib/metanorma/mko/diff.rb
|
|
113
113
|
- lib/metanorma/mko/export.rb
|
|
114
|
+
- lib/metanorma/mko/language.rb
|
|
114
115
|
- lib/metanorma/mko/mcp.rb
|
|
115
116
|
- lib/metanorma/mko/result.rb
|
|
116
117
|
- lib/metanorma/mko/schema.rb
|