blun-king-cli 9.1.563 → 9.1.565

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.
Files changed (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,353 +1,353 @@
1
- #!/usr/bin/env python3
2
- """Flag common ASCII substitutions and non-NFC text in prose files."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import re
8
- import sys
9
- import unicodedata
10
- from pathlib import Path
11
-
12
-
13
- RULES: dict[str, tuple[tuple[str, str], ...]] = {
14
- "de": (
15
- (r"\bfuer\b", "für"),
16
- (r"\bueber", "über…"),
17
- (r"\bzurueck", "zurück…"),
18
- (r"\bmuess", "müss…"),
19
- (r"\bkoenn", "könn…"),
20
- (r"\bmoech", "möch…"),
21
- (r"\bschoen", "schön…"),
22
- (r"\bgrues", "grüß…"),
23
- (r"\bkoeln\b", "Köln"),
24
- (r"\bgroess", "größ…"),
25
- (r"\baender", "änder…"),
26
- (r"\bpruef", "prüf…"),
27
- (r"\bgepruef", "geprüf…"),
28
- (r"\buebersetz", "übersetz…"),
29
- (r"\boeffn", "öffn…"),
30
- (r"\bloesch", "lösch…"),
31
- (r"\bwaehr", "währ…"),
32
- (r"\bwaer(?:e|en|st|t)\b", "wär…"),
33
- (r"\bhaett", "hätt…"),
34
- (r"\bwuerd", "würd…"),
35
- (r"\bduerf", "dürf…"),
36
- (r"\berklaer", "erklär…"),
37
- (r"\bgeklaer", "geklär…"),
38
- (r"\bnaechst", "nächst…"),
39
- (r"\bspaet", "spät…"),
40
- (r"\bfrueh", "früh…"),
41
- (r"\bwuensch", "wünsch…"),
42
- (r"\bgueltig", "gültig…"),
43
- (r"\bzuverlaess", "zuverläss…"),
44
- (r"\bhaeufig", "häufig…"),
45
- (r"\bzusaetz", "zusätz…"),
46
- (r"\bvollstaendig", "vollständig…"),
47
- (r"\bpersoenlich", "persönlich…"),
48
- (r"\bnatuerlich", "natürlich…"),
49
- (r"\bmoeglich", "möglich…"),
50
- (r"\bnoetig", "nötig…"),
51
- (r"\baehnlich", "ähnlich…"),
52
- (r"\bgehoer", "gehör…"),
53
- (r"\benthaelt", "enthält"),
54
- (r"\bwaehl", "wähl…"),
55
- (r"\bveroeffent", "veröffent…"),
56
- (r"\bunterstuetz", "unterstütz…"),
57
- (r"\bbestaet", "bestät…"),
58
- (r"\bergaenz", "ergänz…"),
59
- ),
60
- "sv": (
61
- (r"\bGoteborg\b", "Göteborg"),
62
- (r"\bMalmo\b", "Malmö"),
63
- (r"\bOresund\b", "Öresund"),
64
- (r"\bsmorgasbord\b", "smörgåsbord"),
65
- (r"\bAngstrom\b", "Ångström"),
66
- (r"\bforstar\b", "förstår"),
67
- (r"\bbehover\b", "behöver"),
68
- (r"\bmojlig", "möjlig…"),
69
- (r"\bborja", "börja…"),
70
- (r"\bfraga", "fråga…"),
71
- (r"\bsjalv", "själv…"),
72
- (r"\bhar ar\b", "här är"),
73
- (r"\bdet ar\b", "det är"),
74
- (r"\bjag ar\b", "jag är"),
75
- (r"\bvi ar\b", "vi är"),
76
- (r"\bdu ar\b", "du är"),
77
- (r"\bar det\b", "är det"),
78
- (r"\bfor att\b", "för att"),
79
- (r"\bfor dig\b", "för dig"),
80
- (r"\bfor er\b", "för er"),
81
- (r"\bpa en\b", "på en"),
82
- (r"\bpa ett\b", "på ett"),
83
- (r"\bpa den\b", "på den"),
84
- (r"\bpa det\b", "på det"),
85
- (r"\bsprak", "språk…"),
86
- (r"\boppn", "öppn…"),
87
- ),
88
- "es": (
89
- (r"\bespanol\b", "español"),
90
- (r"\bsenor", "señor…"),
91
- (r"\binformacion\b", "información"),
92
- (r"\btambien\b", "también"),
93
- (r"\bpagina\b", "página"),
94
- (r"\bnumero\b", "número"),
95
- (r"\badios\b", "adiós"),
96
- (r"\bQue\?", "¿Qué?"),
97
- ),
98
- "fr": (
99
- (r"\bfrancais\b", "français"),
100
- (r"\becole\b", "école"),
101
- (r"\bcreme brulee\b", "crème brûlée"),
102
- (r"\btres\b", "très"),
103
- (r"\bdeja\b", "déjà"),
104
- (r"\betre\b", "être"),
105
- ),
106
- "pt": (
107
- (r"\bSao Paulo\b", "São Paulo"),
108
- (r"\bcoracao\b", "coração"),
109
- (r"\bportugues\b", "português"),
110
- (r"\bvoce\b", "você"),
111
- (r"\bnao\b", "não"),
112
- ),
113
- "it": (
114
- (r"\bperche\b", "perché"),
115
- (r"\bcitta\b", "città"),
116
- (r"\bpiu\b", "più"),
117
- ),
118
- "da": (
119
- (r"\bKobenhavn\b", "København"),
120
- (r"\bvaer", "vær…"),
121
- (r"\bsprogforstaelse\b", "sprogforståelse"),
122
- ),
123
- "no": (
124
- (r"\bvaer", "vær…"),
125
- (r"\bforsta", "forstå…"),
126
- (r"\bsporsmal\b", "spørsmål"),
127
- ),
128
- "is": (
129
- (r"\bReykjavik\b", "Reykjavík"),
130
- (r"\bThingvellir\b", "Þingvellir"),
131
- (r"\bislenska\b", "íslenska"),
132
- ),
133
- "cs": (
134
- (r"\bcestina\b", "čeština"),
135
- (r"\bcesk", "česk…"),
136
- (r"\bprilis\b", "příliš"),
137
- (r"\bDvorak\b", "Dvořák"),
138
- (r"\bdekuji\b", "děkuji"),
139
- (r"\bmesto\b", "město"),
140
- ),
141
- "sk": (
142
- (r"\bslovencina\b", "slovenčina"),
143
- (r"\bdakujem\b", "ďakujem"),
144
- ),
145
- "pl": (
146
- (r"\bLodz\b", "Łódź"),
147
- (r"\bjezyk\b", "język"),
148
- (r"\bdziekuje\b", "dziękuję"),
149
- (r"\bzolty\b", "żółty"),
150
- ),
151
- "hu": (
152
- (r"\bMagyarorszag\b", "Magyarország"),
153
- (r"\bkoszonom\b", "köszönöm"),
154
- (r"\borom\b", "öröm"),
155
- ),
156
- "ro": (
157
- (r"\bromana\b", "română"),
158
- (r"\bBucuresti\b", "București"),
159
- (r"\bmultumesc\b", "mulțumesc"),
160
- (r"\btara\b", "țară"),
161
- ),
162
- "tr": (
163
- (r"\bIstanbul\b", "İstanbul"),
164
- (r"\bTurkce\b", "Türkçe"),
165
- (r"\btesekkur", "teşekkür…"),
166
- (r"\bgorusuruz\b", "görüşürüz"),
167
- ),
168
- "hr": (
169
- (r"\bDorde\b", "Đorđe"),
170
- (r"\bvec\b", "već"),
171
- (r"\bcovjek\b", "čovjek"),
172
- ),
173
- "bs": (
174
- (r"\bDorde\b", "Đorđe"),
175
- (r"\bvec\b", "već"),
176
- (r"\bcovjek\b", "čovjek"),
177
- ),
178
- "sr": (
179
- (r"\bDorde\b", "Đorđe"),
180
- (r"\bvec\b", "već"),
181
- (r"\bcovek\b", "čovek"),
182
- ),
183
- "sl": (
184
- (r"\bslovenscina\b", "slovenščina"),
185
- (r"\bzivjo\b", "živjo"),
186
- ),
187
- "nl": (
188
- (r"\bgeinteresseerd\b", "geïnteresseerd"),
189
- ),
190
- "fi": (
191
- (r"\bhyvaa\b", "hyvää"),
192
- (r"\bpaiva\b", "päivä"),
193
- ),
194
- "et": (
195
- (r"\bTonu\b", "Tõnu"),
196
- (r"\bvoimalik\b", "võimalik"),
197
- ),
198
- "lv": (
199
- (r"\bRiga\b", "Rīga"),
200
- (r"\blatviesu\b", "latviešu"),
201
- ),
202
- "lt": (
203
- (r"\blietuviu\b", "lietuvių"),
204
- (r"\baciu\b", "ačiū"),
205
- ),
206
- "vi": (
207
- (r"\bTieng Viet\b", "Tiếng Việt"),
208
- (r"\bcam on\b", "cảm ơn"),
209
- (r"\bViet Nam\b", "Việt Nam"),
210
- ),
211
- "ca": (
212
- (r"\bcatala\b", "català"),
213
- (r"\bcollegi\b", "col·legi"),
214
- ),
215
- "ga": (
216
- (r"\bSlainte\b", "Sláinte"),
217
- (r"\bfailte\b", "fáilte"),
218
- ),
219
- "mt": (
220
- (r"\bghaliex\b", "għaliex"),
221
- ),
222
- "sq": (
223
- (r"\bShqiperi\b", "Shqipëri"),
224
- ),
225
- }
226
-
227
- LANGUAGE_MARKERS: dict[str, tuple[str, ...]] = {
228
- "de": (" der ", " die ", " das ", " und ", " ist ", " nicht ", " ich ", " wir ", " mit "),
229
- "sv": (" den ", " det ", " och ", " att ", " är ", " inte ", " jag ", " vi ", " med "),
230
- "es": (" el ", " la ", " los ", " las ", " que ", " una ", " con ", " para "),
231
- "fr": (" le ", " la ", " les ", " des ", " est ", " une ", " avec ", " pour "),
232
- "pt": (" o ", " os ", " uma ", " que ", " com ", " para ", " não "),
233
- "it": (" il ", " lo ", " gli ", " che ", " una ", " con ", " per "),
234
- "da": (" den ", " det ", " og ", " ikke ", " jeg ", " med ", " til "),
235
- "no": (" den ", " det ", " og ", " ikke ", " jeg ", " med ", " til "),
236
- "is": (" og ", " ekki ", " ég ", " með ", " til ", " sem "),
237
- "cs": (" ten ", " tato ", " a ", " že ", " není ", " jsem ", " pro "),
238
- "sk": (" ten ", " táto ", " a ", " že ", " nie ", " som ", " pre "),
239
- "pl": (" ten ", " ta ", " i ", " że ", " nie ", " jest ", " dla "),
240
- "hu": (" az ", " egy ", " és ", " nem ", " van ", " hogy "),
241
- "ro": (" un ", " o ", " și ", " nu ", " este ", " pentru "),
242
- "tr": (" bu ", " bir ", " ve ", " değil ", " için ", " ile "),
243
- "hr": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
244
- "bs": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
245
- "sr": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
246
- "sl": (" ta ", " ena ", " in ", " ni ", " za ", " je "),
247
- "nl": (" de ", " het ", " een ", " en ", " niet ", " voor "),
248
- "fi": (" tämä ", " yksi ", " ja ", " ei ", " on ", " varten "),
249
- "et": (" see ", " üks ", " ja ", " ei ", " on ", " jaoks "),
250
- "lv": (" tas ", " viena ", " un ", " nav ", " ir ", " par "),
251
- "lt": (" tai ", " viena ", " ir ", " nėra ", " yra ", " už "),
252
- "vi": (" một ", " và ", " không ", " là ", " cho ", " của "),
253
- "ca": (" el ", " la ", " els ", " una ", " amb ", " per "),
254
- "ga": (" an ", " na ", " agus ", " ní ", " tá ", " le "),
255
- "mt": (" il ", " u ", " mhux ", " huwa ", " għal "),
256
- "sq": (" ky ", " një ", " dhe ", " nuk ", " është ", " për "),
257
- }
258
-
259
-
260
- def mask_technical_text(text: str) -> str:
261
- """Mask common technical spans while retaining line positions."""
262
- patterns = (
263
- r"```.*?```",
264
- r"`[^`\n]+`",
265
- r"https?://\S+",
266
- r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b",
267
- )
268
- masked = text
269
- for pattern in patterns:
270
- masked = re.sub(
271
- pattern,
272
- lambda match: "".join("\n" if char == "\n" else " " for char in match.group(0)),
273
- masked,
274
- flags=re.DOTALL,
275
- )
276
- return masked
277
-
278
-
279
- def detect_languages(text: str) -> tuple[str, ...]:
280
- padded = f" {text.casefold()} "
281
- scores = {
282
- language: sum(padded.count(marker) for marker in markers)
283
- for language, markers in LANGUAGE_MARKERS.items()
284
- }
285
- best = max(scores.values(), default=0)
286
- if best == 0:
287
- return ()
288
- return tuple(language for language, score in scores.items() if score == best)
289
-
290
-
291
- def iter_findings(text: str, languages: tuple[str, ...]):
292
- for language in languages:
293
- for pattern, suggestion in RULES[language]:
294
- for match in re.finditer(pattern, text, flags=re.IGNORECASE):
295
- line = text.count("\n", 0, match.start()) + 1
296
- yield line, language, match.group(0), suggestion
297
-
298
-
299
- def check_file(path: Path, language: str) -> int:
300
- try:
301
- text = path.read_text(encoding="utf-8")
302
- except UnicodeDecodeError:
303
- print(f"{path}: not valid UTF-8", file=sys.stderr)
304
- return 1
305
- except OSError as error:
306
- print(f"{path}: {error}", file=sys.stderr)
307
- return 1
308
-
309
- status = 0
310
- if text != unicodedata.normalize("NFC", text):
311
- print(f"{path}: Unicode text is not NFC-normalized")
312
- status = 1
313
-
314
- prose = mask_technical_text(text)
315
- normalized_language = language.casefold().split("-", 1)[0].split("_", 1)[0]
316
- if language == "auto":
317
- languages = detect_languages(prose)
318
- elif language == "all":
319
- languages = tuple(RULES)
320
- elif normalized_language not in RULES:
321
- languages = ()
322
- print(
323
- f"{path}: no deterministic diacritics rules for {language!r}; "
324
- "Unicode NFC passed, but the native-orthography review is still required."
325
- )
326
- else:
327
- languages = (normalized_language,)
328
- for line, code, found, suggestion in iter_findings(prose, languages):
329
- print(f"{path}:{line}: [{code}] {found!r} -> check {suggestion!r}")
330
- status = 1
331
- return status
332
-
333
-
334
- def main() -> int:
335
- parser = argparse.ArgumentParser(
336
- description="Flag common missing diacritics in UTF-8 prose files."
337
- )
338
- parser.add_argument("files", nargs="+", type=Path)
339
- parser.add_argument(
340
- "--language",
341
- default="auto",
342
- help=(
343
- "target language or BCP 47 tag; unsupported languages still receive "
344
- "Unicode checks and exit successfully when clean; default: auto"
345
- ),
346
- )
347
- args = parser.parse_args()
348
-
349
- return max(check_file(path, args.language) for path in args.files)
350
-
351
-
352
- if __name__ == "__main__":
353
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ """Flag common ASCII substitutions and non-NFC text in prose files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import re
8
+ import sys
9
+ import unicodedata
10
+ from pathlib import Path
11
+
12
+
13
+ RULES: dict[str, tuple[tuple[str, str], ...]] = {
14
+ "de": (
15
+ (r"\bfuer\b", "für"),
16
+ (r"\bueber", "über…"),
17
+ (r"\bzurueck", "zurück…"),
18
+ (r"\bmuess", "müss…"),
19
+ (r"\bkoenn", "könn…"),
20
+ (r"\bmoech", "möch…"),
21
+ (r"\bschoen", "schön…"),
22
+ (r"\bgrues", "grüß…"),
23
+ (r"\bkoeln\b", "Köln"),
24
+ (r"\bgroess", "größ…"),
25
+ (r"\baender", "änder…"),
26
+ (r"\bpruef", "prüf…"),
27
+ (r"\bgepruef", "geprüf…"),
28
+ (r"\buebersetz", "übersetz…"),
29
+ (r"\boeffn", "öffn…"),
30
+ (r"\bloesch", "lösch…"),
31
+ (r"\bwaehr", "währ…"),
32
+ (r"\bwaer(?:e|en|st|t)\b", "wär…"),
33
+ (r"\bhaett", "hätt…"),
34
+ (r"\bwuerd", "würd…"),
35
+ (r"\bduerf", "dürf…"),
36
+ (r"\berklaer", "erklär…"),
37
+ (r"\bgeklaer", "geklär…"),
38
+ (r"\bnaechst", "nächst…"),
39
+ (r"\bspaet", "spät…"),
40
+ (r"\bfrueh", "früh…"),
41
+ (r"\bwuensch", "wünsch…"),
42
+ (r"\bgueltig", "gültig…"),
43
+ (r"\bzuverlaess", "zuverläss…"),
44
+ (r"\bhaeufig", "häufig…"),
45
+ (r"\bzusaetz", "zusätz…"),
46
+ (r"\bvollstaendig", "vollständig…"),
47
+ (r"\bpersoenlich", "persönlich…"),
48
+ (r"\bnatuerlich", "natürlich…"),
49
+ (r"\bmoeglich", "möglich…"),
50
+ (r"\bnoetig", "nötig…"),
51
+ (r"\baehnlich", "ähnlich…"),
52
+ (r"\bgehoer", "gehör…"),
53
+ (r"\benthaelt", "enthält"),
54
+ (r"\bwaehl", "wähl…"),
55
+ (r"\bveroeffent", "veröffent…"),
56
+ (r"\bunterstuetz", "unterstütz…"),
57
+ (r"\bbestaet", "bestät…"),
58
+ (r"\bergaenz", "ergänz…"),
59
+ ),
60
+ "sv": (
61
+ (r"\bGoteborg\b", "Göteborg"),
62
+ (r"\bMalmo\b", "Malmö"),
63
+ (r"\bOresund\b", "Öresund"),
64
+ (r"\bsmorgasbord\b", "smörgåsbord"),
65
+ (r"\bAngstrom\b", "Ångström"),
66
+ (r"\bforstar\b", "förstår"),
67
+ (r"\bbehover\b", "behöver"),
68
+ (r"\bmojlig", "möjlig…"),
69
+ (r"\bborja", "börja…"),
70
+ (r"\bfraga", "fråga…"),
71
+ (r"\bsjalv", "själv…"),
72
+ (r"\bhar ar\b", "här är"),
73
+ (r"\bdet ar\b", "det är"),
74
+ (r"\bjag ar\b", "jag är"),
75
+ (r"\bvi ar\b", "vi är"),
76
+ (r"\bdu ar\b", "du är"),
77
+ (r"\bar det\b", "är det"),
78
+ (r"\bfor att\b", "för att"),
79
+ (r"\bfor dig\b", "för dig"),
80
+ (r"\bfor er\b", "för er"),
81
+ (r"\bpa en\b", "på en"),
82
+ (r"\bpa ett\b", "på ett"),
83
+ (r"\bpa den\b", "på den"),
84
+ (r"\bpa det\b", "på det"),
85
+ (r"\bsprak", "språk…"),
86
+ (r"\boppn", "öppn…"),
87
+ ),
88
+ "es": (
89
+ (r"\bespanol\b", "español"),
90
+ (r"\bsenor", "señor…"),
91
+ (r"\binformacion\b", "información"),
92
+ (r"\btambien\b", "también"),
93
+ (r"\bpagina\b", "página"),
94
+ (r"\bnumero\b", "número"),
95
+ (r"\badios\b", "adiós"),
96
+ (r"\bQue\?", "¿Qué?"),
97
+ ),
98
+ "fr": (
99
+ (r"\bfrancais\b", "français"),
100
+ (r"\becole\b", "école"),
101
+ (r"\bcreme brulee\b", "crème brûlée"),
102
+ (r"\btres\b", "très"),
103
+ (r"\bdeja\b", "déjà"),
104
+ (r"\betre\b", "être"),
105
+ ),
106
+ "pt": (
107
+ (r"\bSao Paulo\b", "São Paulo"),
108
+ (r"\bcoracao\b", "coração"),
109
+ (r"\bportugues\b", "português"),
110
+ (r"\bvoce\b", "você"),
111
+ (r"\bnao\b", "não"),
112
+ ),
113
+ "it": (
114
+ (r"\bperche\b", "perché"),
115
+ (r"\bcitta\b", "città"),
116
+ (r"\bpiu\b", "più"),
117
+ ),
118
+ "da": (
119
+ (r"\bKobenhavn\b", "København"),
120
+ (r"\bvaer", "vær…"),
121
+ (r"\bsprogforstaelse\b", "sprogforståelse"),
122
+ ),
123
+ "no": (
124
+ (r"\bvaer", "vær…"),
125
+ (r"\bforsta", "forstå…"),
126
+ (r"\bsporsmal\b", "spørsmål"),
127
+ ),
128
+ "is": (
129
+ (r"\bReykjavik\b", "Reykjavík"),
130
+ (r"\bThingvellir\b", "Þingvellir"),
131
+ (r"\bislenska\b", "íslenska"),
132
+ ),
133
+ "cs": (
134
+ (r"\bcestina\b", "čeština"),
135
+ (r"\bcesk", "česk…"),
136
+ (r"\bprilis\b", "příliš"),
137
+ (r"\bDvorak\b", "Dvořák"),
138
+ (r"\bdekuji\b", "děkuji"),
139
+ (r"\bmesto\b", "město"),
140
+ ),
141
+ "sk": (
142
+ (r"\bslovencina\b", "slovenčina"),
143
+ (r"\bdakujem\b", "ďakujem"),
144
+ ),
145
+ "pl": (
146
+ (r"\bLodz\b", "Łódź"),
147
+ (r"\bjezyk\b", "język"),
148
+ (r"\bdziekuje\b", "dziękuję"),
149
+ (r"\bzolty\b", "żółty"),
150
+ ),
151
+ "hu": (
152
+ (r"\bMagyarorszag\b", "Magyarország"),
153
+ (r"\bkoszonom\b", "köszönöm"),
154
+ (r"\borom\b", "öröm"),
155
+ ),
156
+ "ro": (
157
+ (r"\bromana\b", "română"),
158
+ (r"\bBucuresti\b", "București"),
159
+ (r"\bmultumesc\b", "mulțumesc"),
160
+ (r"\btara\b", "țară"),
161
+ ),
162
+ "tr": (
163
+ (r"\bIstanbul\b", "İstanbul"),
164
+ (r"\bTurkce\b", "Türkçe"),
165
+ (r"\btesekkur", "teşekkür…"),
166
+ (r"\bgorusuruz\b", "görüşürüz"),
167
+ ),
168
+ "hr": (
169
+ (r"\bDorde\b", "Đorđe"),
170
+ (r"\bvec\b", "već"),
171
+ (r"\bcovjek\b", "čovjek"),
172
+ ),
173
+ "bs": (
174
+ (r"\bDorde\b", "Đorđe"),
175
+ (r"\bvec\b", "već"),
176
+ (r"\bcovjek\b", "čovjek"),
177
+ ),
178
+ "sr": (
179
+ (r"\bDorde\b", "Đorđe"),
180
+ (r"\bvec\b", "već"),
181
+ (r"\bcovek\b", "čovek"),
182
+ ),
183
+ "sl": (
184
+ (r"\bslovenscina\b", "slovenščina"),
185
+ (r"\bzivjo\b", "živjo"),
186
+ ),
187
+ "nl": (
188
+ (r"\bgeinteresseerd\b", "geïnteresseerd"),
189
+ ),
190
+ "fi": (
191
+ (r"\bhyvaa\b", "hyvää"),
192
+ (r"\bpaiva\b", "päivä"),
193
+ ),
194
+ "et": (
195
+ (r"\bTonu\b", "Tõnu"),
196
+ (r"\bvoimalik\b", "võimalik"),
197
+ ),
198
+ "lv": (
199
+ (r"\bRiga\b", "Rīga"),
200
+ (r"\blatviesu\b", "latviešu"),
201
+ ),
202
+ "lt": (
203
+ (r"\blietuviu\b", "lietuvių"),
204
+ (r"\baciu\b", "ačiū"),
205
+ ),
206
+ "vi": (
207
+ (r"\bTieng Viet\b", "Tiếng Việt"),
208
+ (r"\bcam on\b", "cảm ơn"),
209
+ (r"\bViet Nam\b", "Việt Nam"),
210
+ ),
211
+ "ca": (
212
+ (r"\bcatala\b", "català"),
213
+ (r"\bcollegi\b", "col·legi"),
214
+ ),
215
+ "ga": (
216
+ (r"\bSlainte\b", "Sláinte"),
217
+ (r"\bfailte\b", "fáilte"),
218
+ ),
219
+ "mt": (
220
+ (r"\bghaliex\b", "għaliex"),
221
+ ),
222
+ "sq": (
223
+ (r"\bShqiperi\b", "Shqipëri"),
224
+ ),
225
+ }
226
+
227
+ LANGUAGE_MARKERS: dict[str, tuple[str, ...]] = {
228
+ "de": (" der ", " die ", " das ", " und ", " ist ", " nicht ", " ich ", " wir ", " mit "),
229
+ "sv": (" den ", " det ", " och ", " att ", " är ", " inte ", " jag ", " vi ", " med "),
230
+ "es": (" el ", " la ", " los ", " las ", " que ", " una ", " con ", " para "),
231
+ "fr": (" le ", " la ", " les ", " des ", " est ", " une ", " avec ", " pour "),
232
+ "pt": (" o ", " os ", " uma ", " que ", " com ", " para ", " não "),
233
+ "it": (" il ", " lo ", " gli ", " che ", " una ", " con ", " per "),
234
+ "da": (" den ", " det ", " og ", " ikke ", " jeg ", " med ", " til "),
235
+ "no": (" den ", " det ", " og ", " ikke ", " jeg ", " med ", " til "),
236
+ "is": (" og ", " ekki ", " ég ", " með ", " til ", " sem "),
237
+ "cs": (" ten ", " tato ", " a ", " že ", " není ", " jsem ", " pro "),
238
+ "sk": (" ten ", " táto ", " a ", " že ", " nie ", " som ", " pre "),
239
+ "pl": (" ten ", " ta ", " i ", " że ", " nie ", " jest ", " dla "),
240
+ "hu": (" az ", " egy ", " és ", " nem ", " van ", " hogy "),
241
+ "ro": (" un ", " o ", " și ", " nu ", " este ", " pentru "),
242
+ "tr": (" bu ", " bir ", " ve ", " değil ", " için ", " ile "),
243
+ "hr": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
244
+ "bs": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
245
+ "sr": (" ovaj ", " jedna ", " i ", " nije ", " za ", " sa "),
246
+ "sl": (" ta ", " ena ", " in ", " ni ", " za ", " je "),
247
+ "nl": (" de ", " het ", " een ", " en ", " niet ", " voor "),
248
+ "fi": (" tämä ", " yksi ", " ja ", " ei ", " on ", " varten "),
249
+ "et": (" see ", " üks ", " ja ", " ei ", " on ", " jaoks "),
250
+ "lv": (" tas ", " viena ", " un ", " nav ", " ir ", " par "),
251
+ "lt": (" tai ", " viena ", " ir ", " nėra ", " yra ", " už "),
252
+ "vi": (" một ", " và ", " không ", " là ", " cho ", " của "),
253
+ "ca": (" el ", " la ", " els ", " una ", " amb ", " per "),
254
+ "ga": (" an ", " na ", " agus ", " ní ", " tá ", " le "),
255
+ "mt": (" il ", " u ", " mhux ", " huwa ", " għal "),
256
+ "sq": (" ky ", " një ", " dhe ", " nuk ", " është ", " për "),
257
+ }
258
+
259
+
260
+ def mask_technical_text(text: str) -> str:
261
+ """Mask common technical spans while retaining line positions."""
262
+ patterns = (
263
+ r"```.*?```",
264
+ r"`[^`\n]+`",
265
+ r"https?://\S+",
266
+ r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b",
267
+ )
268
+ masked = text
269
+ for pattern in patterns:
270
+ masked = re.sub(
271
+ pattern,
272
+ lambda match: "".join("\n" if char == "\n" else " " for char in match.group(0)),
273
+ masked,
274
+ flags=re.DOTALL,
275
+ )
276
+ return masked
277
+
278
+
279
+ def detect_languages(text: str) -> tuple[str, ...]:
280
+ padded = f" {text.casefold()} "
281
+ scores = {
282
+ language: sum(padded.count(marker) for marker in markers)
283
+ for language, markers in LANGUAGE_MARKERS.items()
284
+ }
285
+ best = max(scores.values(), default=0)
286
+ if best == 0:
287
+ return ()
288
+ return tuple(language for language, score in scores.items() if score == best)
289
+
290
+
291
+ def iter_findings(text: str, languages: tuple[str, ...]):
292
+ for language in languages:
293
+ for pattern, suggestion in RULES[language]:
294
+ for match in re.finditer(pattern, text, flags=re.IGNORECASE):
295
+ line = text.count("\n", 0, match.start()) + 1
296
+ yield line, language, match.group(0), suggestion
297
+
298
+
299
+ def check_file(path: Path, language: str) -> int:
300
+ try:
301
+ text = path.read_text(encoding="utf-8")
302
+ except UnicodeDecodeError:
303
+ print(f"{path}: not valid UTF-8", file=sys.stderr)
304
+ return 1
305
+ except OSError as error:
306
+ print(f"{path}: {error}", file=sys.stderr)
307
+ return 1
308
+
309
+ status = 0
310
+ if text != unicodedata.normalize("NFC", text):
311
+ print(f"{path}: Unicode text is not NFC-normalized")
312
+ status = 1
313
+
314
+ prose = mask_technical_text(text)
315
+ normalized_language = language.casefold().split("-", 1)[0].split("_", 1)[0]
316
+ if language == "auto":
317
+ languages = detect_languages(prose)
318
+ elif language == "all":
319
+ languages = tuple(RULES)
320
+ elif normalized_language not in RULES:
321
+ languages = ()
322
+ print(
323
+ f"{path}: no deterministic diacritics rules for {language!r}; "
324
+ "Unicode NFC passed, but the native-orthography review is still required."
325
+ )
326
+ else:
327
+ languages = (normalized_language,)
328
+ for line, code, found, suggestion in iter_findings(prose, languages):
329
+ print(f"{path}:{line}: [{code}] {found!r} -> check {suggestion!r}")
330
+ status = 1
331
+ return status
332
+
333
+
334
+ def main() -> int:
335
+ parser = argparse.ArgumentParser(
336
+ description="Flag common missing diacritics in UTF-8 prose files."
337
+ )
338
+ parser.add_argument("files", nargs="+", type=Path)
339
+ parser.add_argument(
340
+ "--language",
341
+ default="auto",
342
+ help=(
343
+ "target language or BCP 47 tag; unsupported languages still receive "
344
+ "Unicode checks and exit successfully when clean; default: auto"
345
+ ),
346
+ )
347
+ args = parser.parse_args()
348
+
349
+ return max(check_file(path, args.language) for path in args.files)
350
+
351
+
352
+ if __name__ == "__main__":
353
+ raise SystemExit(main())