blun-king-cli 9.1.564 → 9.1.566

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 (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. 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())