feat: referentiel projets dynamique + validation nommage by design

Durcit la convention de nommage des projets (dérive constatée : 'Sliding
Automation', 'code_versioning'... au lieu des formes canoniques).

- trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label
  projet sur les notes de type=projet (source unique, pas de constante en dur).
  Note-projet CodeVersioning créée (manquait).
- mcp_server.py : _valider_projet() branché dans les 6 tools de création
  (add_decision/history/backlog, new_conversation, create_entite, add_skill).
  Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au
  processus de création de projet). Ne verrouille pas si référentiel illisible.
- lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace
  ET snake_case ; l'ancien 'contient un espace' ratait code_versioning).
- Données : 79 notes ré-étiquetées vers les 3 formes canoniques.

Quality by design : l'erreur de nommage devient impossible à l'écriture, le
Lint n'est plus que le filet de sécurité.
This commit is contained in:
2026-07-17 14:59:33 +02:00
parent d1e67431b6
commit 943acbc573
2425 changed files with 710525 additions and 6 deletions
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
"""
This module is intended to replicate some of the functionality
from the fastnumbers module in the event that module is not installed.
"""
import unicodedata
from typing import Callable, FrozenSet, Union
from natsort.unicode_numbers import decimal_chars
_NAN_INF = [
"INF",
"INf",
"Inf",
"inF",
"iNF",
"InF",
"inf",
"iNf",
"NAN",
"nan",
"NaN",
"nAn",
"naN",
"NAn",
"nAN",
"Nan",
]
_NAN_INF.extend(["+" + x[:2] for x in _NAN_INF] + ["-" + x[:2] for x in _NAN_INF])
NAN_INF = frozenset(_NAN_INF)
ASCII_NUMS = "0123456789+-"
POTENTIAL_FIRST_CHAR = frozenset(decimal_chars + list(ASCII_NUMS + "."))
StrOrFloat = Union[str, float]
StrOrInt = Union[str, int]
def fast_float(
x: str,
key: Callable[[str], str] = lambda x: x,
nan: float = float("inf"),
_uni: Callable[[str, StrOrFloat], StrOrFloat] = unicodedata.numeric,
_nan_inf: FrozenSet[str] = NAN_INF,
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
) -> StrOrFloat:
"""
Convert a string to a float quickly, return input as-is if not possible.
We don't need to accept all input that the real fast_int accepts because
natsort is controlling what is passed to this function.
Parameters
----------
x : str
String to attempt to convert to a float.
key : callable
Single-argument function to apply to *x* if conversion fails.
nan : float
Value to return instead of NaN if NaN would be returned.
Returns
-------
*str* or *float*
"""
if x[0] in _first_char or x.lstrip()[:3] in _nan_inf:
try:
ret = float(x)
return nan if ret != ret else ret
except ValueError:
try:
return _uni(x, key(x)) if len(x) == 1 else key(x)
except TypeError: # pragma: no cover
return key(x)
else:
try:
return _uni(x, key(x)) if len(x) == 1 else key(x)
except TypeError: # pragma: no cover
return key(x)
def fast_int(
x: str,
key: Callable[[str], str] = lambda x: x,
_uni: Callable[[str, StrOrInt], StrOrInt] = unicodedata.digit,
_first_char: FrozenSet[str] = POTENTIAL_FIRST_CHAR,
) -> StrOrInt:
"""
Convert a string to a int quickly, return input as-is if not possible.
We don't need to accept all input that the real fast_int accepts because
natsort is controlling what is passed to this function.
Parameters
----------
x : str
String to attempt to convert to an int.
key : callable
Single-argument function to apply to *x* if conversion fails.
Returns
-------
*str* or *int*
"""
if x[0] in _first_char:
try:
return int(x)
except ValueError:
try:
return _uni(x, key(x)) if len(x) == 1 else key(x)
except TypeError: # pragma: no cover
return key(x)
else:
try:
return _uni(x, key(x)) if len(x) == 1 else key(x)
except TypeError: # pragma: no cover
return key(x)
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
Interface for natsort to access fastnumbers functions without
having to worry if it is actually installed.
"""
import re
from typing import Callable, Iterable, Iterator, Tuple, Union
StrOrFloat = Union[str, float]
StrOrInt = Union[str, int]
__all__ = ["try_float", "try_int"]
def is_supported_fastnumbers(
fastnumbers_version: str, minimum: Tuple[int, int, int] = (2, 0, 0)
) -> bool:
match = re.match(
r"^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$",
fastnumbers_version,
flags=re.ASCII,
)
if not match:
raise ValueError(
"Invalid fastnumbers version number '{}'".format(fastnumbers_version)
)
(major, minor, patch) = match.group(1, 2, 4)
return (int(major), int(minor), int(patch)) >= minimum
# If the user has fastnumbers installed, they will get great speed
# benefits. If not, we use the simulated functions that come with natsort.
try:
# noinspection PyPackageRequirements
from fastnumbers import fast_float, fast_int, __version__ as fn_ver
# Require >= version 2.0.0.
if not is_supported_fastnumbers(fn_ver):
raise ImportError # pragma: no cover
# For versions of fastnumbers with mapping capability, use that
if is_supported_fastnumbers(fn_ver, (5, 0, 0)):
del fast_float, fast_int
from fastnumbers import try_float, try_int
except ImportError:
from natsort.compat.fake_fastnumbers import fast_float, fast_int # type: ignore
# Re-map the old-or-compatibility functions fast_float/fast_int to the
# newer API of try_float/try_int. If we already imported try_float/try_int
# then there is nothing to do.
if "try_float" not in globals():
def try_float( # type: ignore[no-redef] # noqa: F811
x: Iterable[str],
map: bool,
nan: float = float("inf"),
on_fail: Callable[[str], str] = lambda x: x,
) -> Iterator[StrOrFloat]:
assert map is True
return (fast_float(y, nan=nan, key=on_fail) for y in x)
if "try_int" not in globals():
def try_int( # type: ignore[no-redef] # noqa: F811
x: Iterable[str],
map: bool,
on_fail: Callable[[str], str] = lambda x: x,
) -> Iterator[StrOrInt]:
assert map is True
return (fast_int(y, key=on_fail) for y in x)
@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
Interface for natsort to access locale functionality without
having to worry about if it is using PyICU or the built-in locale.
"""
import sys
from typing import Callable, Union, cast
StrOrBytes = Union[str, bytes]
TrxfmFunc = Callable[[str], StrOrBytes]
# This string should be sorted after any other byte string because
# it contains the max unicode character repeated 20 times.
# You would need some odd data to come after that.
null_string = ""
null_string_max = chr(sys.maxunicode) * 20
# This variable could be str or bytes depending on the locale library
# being used, so give the type-checker this information.
null_string_locale: StrOrBytes
null_string_locale_max: StrOrBytes
# strxfrm can be buggy (especially on OSX and *possibly* some other
# BSD-based systems), so prefer icu if available.
try: # noqa: C901
import icu
from locale import getlocale
null_string_locale = b""
# This string should in theory be sorted after any other byte
# string because it contains the max byte char repeated many times.
# You would need some odd data to come after that.
null_string_locale_max = b"x7f" * 50
def dumb_sort() -> bool:
return False
# If using icu, get the locale from the current global locale,
def get_icu_locale() -> str:
language_code, encoding = getlocale()
if language_code is None or encoding is None: # pragma: no cover
return icu.Locale()
return icu.Locale(f"{language_code}.{encoding}")
def get_strxfrm() -> TrxfmFunc:
return icu.Collator.createInstance(get_icu_locale()).getSortKey
def get_thousands_sep() -> str:
sep = icu.DecimalFormatSymbols.kGroupingSeparatorSymbol
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
def get_decimal_point() -> str:
sep = icu.DecimalFormatSymbols.kDecimalSeparatorSymbol
return icu.DecimalFormatSymbols(get_icu_locale()).getSymbol(sep)
except ImportError:
import locale
from locale import strxfrm
null_string_locale = null_string
null_string_locale_max = null_string_max
# On some systems, locale is broken and does not sort in the expected
# order. We will try to detect this and compensate.
def dumb_sort() -> bool:
return strxfrm("A") < strxfrm("a")
def get_strxfrm() -> TrxfmFunc:
return strxfrm
def get_thousands_sep() -> str:
sep = cast(str, locale.localeconv()["thousands_sep"])
# If this locale library is broken, some of the thousands separator
# characters are incorrectly blank. Here is a lookup table of the
# corrections I am aware of.
if dumb_sort():
language_code, encoding = locale.getlocale()
if language_code is None or encoding is None:
# No locale loaded, default to ','
return ","
loc = f"{language_code}.{encoding}"
return {
"de_DE.ISO8859-15": ".",
"es_ES.ISO8859-1": ".",
"de_AT.ISO8859-1": ".",
"de_at": "\xa0",
"nl_NL.UTF-8": ".",
"es_es": ".",
"fr_CH.ISO8859-15": "\xa0",
"fr_CA.ISO8859-1": "\xa0",
"de_CH.ISO8859-1": ".",
"fr_FR.ISO8859-15": "\xa0",
"nl_NL.ISO8859-1": ".",
"ca_ES.UTF-8": ".",
"nl_NL.ISO8859-15": ".",
"de_ch": "'",
"ca_es": ".",
"de_AT.ISO8859-15": ".",
"ca_ES.ISO8859-1": ".",
"de_AT.UTF-8": ".",
"es_ES.UTF-8": ".",
"fr_fr": "\xa0",
"es_ES.ISO8859-15": ".",
"de_DE.ISO8859-1": ".",
"nl_nl": ".",
"fr_ch": "\xa0",
"fr_ca": "\xa0",
"de_DE.UTF-8": ".",
"ca_ES.ISO8859-15": ".",
"de_CH.ISO8859-15": ".",
"fr_FR.ISO8859-1": "\xa0",
"fr_CH.ISO8859-1": "\xa0",
"de_de": ".",
"fr_FR.UTF-8": "\xa0",
"fr_CA.ISO8859-15": "\xa0",
}.get(loc, sep)
else:
return sep
def get_decimal_point() -> str:
return cast(str, locale.localeconv()["decimal_point"])