Przejdź do treści

olski.rozstrzyganie

Warstwa za parserem: co rozstrzyga przyłączenie, którego gramatyka nie rozstrzyga.

Zalążek, a nie maszyna. Stoi tu po to, żeby kierunek dał się zmierzyć, a nie żeby werdykt zmienić: rozstrzygnij bierze gotowy wynik rozbioru i oddaje osobną odpowiedź obok niego, więc valid, ambiguous i rejected znaczą po jej dopisaniu dokładnie to, co znaczyły. Dlaczego akurat tak, wywodzi docs/disambiguation.md: ranking wstawiony w werdykt myliłby się co trzecie albo co czwarte zdanie w miejscu, w którym ten parser obiecuje prawdę o zdaniu.

Rozstrzygać jest przy tym co: nad Składnicą przyłączenie jest całą decyzją w większości zdań, które olski odrzuca za wieloznaczność (tamże).

Świadek jest jednostką tej warstwy. Każdy patrzy na jedno przyłączenie i albo wskazuje gospodarza wraz z powodem, albo milczy. Milczenie jest odpowiedzią pełnoprawną i jest odpowiedzią domyślną: świadek, który nie ma na czym stanąć, nie zgaduje. Powód wraca razem ze wskazaniem, żeby wskazanie dało się sprawdzić bez zaglądania do tabeli.

Świadkowie idą w kolejności, a kolejność jest kolejnością rodzaju dowodu. Pierwszy odpowiadający wygrywa, więc dowód o tym tekście bije dowód o korpusie wszędzie tam, gdzie oba mówią coś naraz. Pod kolejnością tą nie ma porównania dwóch trafności, tylko hipoteza: dobre ujednoznacznianie jest odczytaniem tego, co czytelnik ma przed sobą, a częstość nad cudzym korpusem odczytaniem nie jest, choćby trafiała częściej (docs/disambiguation.md). Dowód czyta Powtórzenie w akapicie, w którym zdanie stoi, Rama w leksykonie walencyjnym, a Skłonność w banku drzew, którego nikt z autorem tego tekstu nie uzgadniał. Rama stoi w środku, bo słownik mówi o polszczyźnie, a nie o tym tekście ani o cudzym korpusie: powtórzenie ją bije, a ona bije częstość.

Świadek ramowy odpowiada schematem, a nie konkurencją między czytaniami. Fraza, której rzeczownik żąda swoim schematem, przeczytana po stronie czasownika łamie ten schemat, a nad Składnicą pozycję taką ma kilkaset z kilku tysięcy wyrażeń spornych (docs/subset.md). Wskazuje sam rzeczownik i jest to połowa kryterium, o którą pytano: po stronie czasownika ta sama rama trafia tyle, ile rzut monetą, więc zostaje tam wetem, a nie wskazaniem (docs/rozstrzyganie.md).

Świadek kontekstowy odpowiada powtórzeniem, a nie znajomością rzeczy. Powtórzenie szuka w akapicie miejsca, w którym ta sama fraza stała już przy którymś z gospodarzy, i wtedy wskazuje tego gospodarza. Regułę szerszą — rzecz raz wprowadzona jest znana, więc fraza dochodzi do czasownika — odrzucono na kontrprzykładzie, który wraz z całym wywodem trzyma docs/rozstrzyganie.md.

Świadek statystyczny nazywa własną częstość pomyłek. Skłonność liczy, jak często ta para przyimka i gospodarza przyłączała się w banku drzew w tę stronę, i odpowiada dopiero powyżej progu wsparcia i progu przewagi. Sam przyimek progu nie przechodzi i to jest zamierzone: leksykon przyimków myli się nad tym korpusem co szóste wyrażenie, a nad najczęstszymi co trzecie (docs/subset.md), czyli mniej więcej tak jak reguła „zawsze do rzeczownika”, którą tamten dokument odrzucił jako konwencję.

Tabelę, którą ten świadek czyta, buduje z banku drzew harness/skłonności.py i tam też ocenia się ją na połowie, której budowa nie widziała.

SKŁONNOŚCI = Path(__file__).parent / 'skłonności.txt' module-attribute

WSPARCIE = 2 module-attribute

PRÓG = 0.85 module-attribute

Licznik = dict[tuple[str, str, str], tuple[int, int]] module-attribute

CZASOWNIKOWE = frozenset({'fin', 'praet', 'impt', 'bedzie', 'inf', 'ppas', 'pact', 'winien', 'imps', 'pred'}) module-attribute

IMIENNE = frozenset({'subst', 'ger', 'depr'}) module-attribute

IMIENNE_LUB_NIEZNANE = IMIENNE | {'ign'} module-attribute

Kandydat = tuple[str, str, Iterable[str]] module-attribute

SŁOWO = re.compile('[\\w-]+', re.UNICODE) module-attribute

ZASIĘG_FRAZY = 3 module-attribute

Kandydaci = Callable[[Sequence[str], int], Iterator[str]] module-attribute

PUSTE = Sąsiedztwo() module-attribute

Leksykon = Callable[[str, str], frozenset[str]] module-attribute

Rozstrzygnięcie dataclass

Wskazanie gospodarza wraz z tym, co je wydało.

Source code in olski/rozstrzyganie.py
119
120
121
122
123
124
125
126
127
128
129
130
131
@dataclass(frozen=True)
class Rozstrzygnięcie:
    """Wskazanie gospodarza wraz z tym, co je wydało."""

    #: Formy modyfikatora, czyli to samo, czym nazywa go werdykt.
    modyfikator: str
    #: Głowa gospodarza, wybrana spośród :attr:`Przyłączenie.gospodarze`.
    gospodarz: str
    #: Dowód, jednym zdaniem, do postawienia w wydruku obok wskazania.
    powód: str
    #: Nazwa świadka, który odpowiedział. Wypełnia ją :func:`rozstrzygnij` z
    #: :attr:`Świadek.nazwa`, więc świadek nie ma jak podpisać się cudzym imieniem.
    świadek: str = ""

modyfikator instance-attribute

gospodarz instance-attribute

powód instance-attribute

świadek = '' class-attribute instance-attribute

__init__(modyfikator, gospodarz, powód, świadek='')

Świadek

Bases: Protocol

Jedno źródło dowodu nad jednym przyłączeniem.

Sąsiedztwo dostaje każdy, także ten, który go nie czyta. Sygnatura jedna znaczy, że kolejność świadków jest listą, a nie dwiema listami wołanymi inaczej, i że świadek dopisany jutro nie rusza ani rozstrzygnij, ani miejsca, z którego warstwa jest wołana.

Source code in olski/rozstrzyganie.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class Świadek(Protocol):
    """Jedno źródło dowodu nad jednym przyłączeniem.

    Sąsiedztwo dostaje każdy, także ten, który go nie czyta. Sygnatura jedna
    znaczy, że kolejność świadków jest listą, a nie dwiema listami wołanymi
    inaczej, i że świadek dopisany jutro nie rusza ani :func:`rozstrzygnij`,
    ani miejsca, z którego warstwa jest wołana.
    """

    nazwa: str

    def __call__(
        self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo
    ) -> Rozstrzygnięcie | None:
        """Wskazanie albo milczenie; milczenie jest odpowiedzią, a nie brakiem."""

nazwa instance-attribute

__call__(przyłączenie, sąsiedztwo)

Wskazanie albo milczenie; milczenie jest odpowiedzią, a nie brakiem.

Source code in olski/rozstrzyganie.py
145
146
147
148
def __call__(
    self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo
) -> Rozstrzygnięcie | None:
    """Wskazanie albo milczenie; milczenie jest odpowiedzią, a nie brakiem."""

Pytany

Bases: Protocol

Świadek, którego dowodem jest przyimek i kandydaci na gospodarza.

Takich świadków jest dwóch — Rama i Skłonność — a różni ich dowód, nie droga do niego, więc drogą jest dla obu _wskaż, a wybierz jest tym, czym się różnią. Tym samym protokołem pyta ich oceń, bo wypadek banku drzew przyłączeniem nie jest i sygnatury Świadek nie ma czym wypełnić.

Source code in olski/rozstrzyganie.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class Pytany(Protocol):
    """Świadek, którego dowodem jest przyimek i kandydaci na gospodarza.

    Takich świadków jest dwóch — :class:`Rama` i :class:`Skłonność` —
    a różni ich dowód, nie droga do niego,
    więc drogą jest dla obu :func:`_wskaż`,
    a :meth:`wybierz` jest tym, czym się różnią.
    Tym samym protokołem pyta ich :func:`oceń`,
    bo wypadek banku drzew przyłączeniem nie jest
    i sygnatury :class:`Świadek` nie ma czym wypełnić.
    """

    def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
        """Etykieta wskazanego gospodarza wraz z powodem; ``None`` znaczy milczenie."""

wybierz(przyimek, kandydaci)

Etykieta wskazanego gospodarza wraz z powodem; None znaczy milczenie.

Source code in olski/rozstrzyganie.py
211
212
def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
    """Etykieta wskazanego gospodarza wraz z powodem; ``None`` znaczy milczenie."""

Dowód dataclass

Miejsce, w którym ta fraza stała już przy gospodarzu.

Lemat jest tym, którym gospodarz się dopasował, a zdanie tym, w którym fraza stała: powód wskazania cytuje oba, żeby dało się je sprawdzić bez wracania do tekstu.

Source code in olski/rozstrzyganie.py
325
326
327
328
329
330
331
332
333
334
335
@dataclass(frozen=True)
class Dowód:
    """Miejsce, w którym ta fraza stała już przy gospodarzu.

    Lemat jest tym, którym gospodarz się dopasował,
    a zdanie tym, w którym fraza stała:
    powód wskazania cytuje oba, żeby dało się je sprawdzić bez wracania do tekstu.
    """

    lemat: str
    zdanie: str

lemat instance-attribute

zdanie instance-attribute

__init__(lemat, zdanie)

Sąsiedztwo dataclass

Zdania, które w tym akapicie stoją przed zdaniem rozstrzyganym.

Akapit jest granicą, a nie okno o stałej długości, i granicę tę bierzemy stąd, skąd bierze ją druga strona: olski/skład/opowieść.py opuszcza podmiot tylko wtedy, gdy o rzeczy była mowa w zdaniu obok, a akapit jest tym, w czym „obok” się kończy. Nagłówek wypada z sąsiedztwa tą samą regułą, bo stoi we własnym akapicie.

Wstecz, a nie w obie strony, bo czytelnik idzie od początku do końca i zdania, którego jeszcze nie przeczytał, do rozstrzygnięcia nie ma.

Source code in olski/rozstrzyganie.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
@dataclass(frozen=True)
class Sąsiedztwo:
    """Zdania, które w tym akapicie stoją przed zdaniem rozstrzyganym.

    Akapit jest granicą, a nie okno o stałej długości, i granicę tę bierzemy
    stąd, skąd bierze ją druga strona: ``olski/skład/opowieść.py`` opuszcza
    podmiot tylko wtedy, gdy o rzeczy była mowa w zdaniu obok, a akapit jest
    tym, w czym „obok” się kończy. Nagłówek wypada z sąsiedztwa tą samą regułą,
    bo stoi we własnym akapicie.

    Wstecz, a nie w obie strony, bo czytelnik idzie od początku do końca i
    zdania, którego jeszcze nie przeczytał, do rozstrzygnięcia nie ma.
    """

    #: Zdania w kolejności, w jakiej stoją w tekście.
    zdania: tuple[str, ...] = ()

    @cached_property
    def _słowa(self) -> tuple[tuple[str, ...], ...]:
        """Każde zdanie jako ciąg form, słowo po słowie.

        Lematów tu nie ma, bo pyta się o nie dwiema drogami —
        imienną dla rzeczownika frazy, pełną dla gospodarza —
        a pamiętają je :func:`_lematy` i :func:`_lematy_imienne`.
        Cięcie na słowa liczy się raz,
        bo zdanie sporne ma czasem kilka przyłączeń,
        a każde pyta o ten sam akapit.
        """
        return tuple(tuple(SŁOWO.findall(zdanie)) for zdanie in self.zdania)

    def przy_czym_stała(
        self,
        przyimek: str,
        rzeczownik: frozenset[str],
        gospodarze: Iterable[str],
        kandydaci: Kandydaci = _łańcuch,
        kopuly: frozenset[str] = KOPULA,
    ) -> dict[str, Dowód]:
        """Ci z gospodarzy, przy których ta fraza w tym sąsiedztwie już stała.

        Fraza jest tu przyimkiem i lematami swojego rzeczownika, a nie napisem,
        bo ``w systemie`` i ``w systemach`` są tą samą frazą o tej samej rzeczy.
        Przyimek też idzie przez lemat, bo ``z`` i ``ze`` są jednym słowem.
        Rzeczownik idzie przez lemat imienny po obu stronach dopasowania,
        bo lemat wszystkich czytań zlewa odsłownik z imiesłowem (:data:`IMIENNE`).

        Gospodarz idzie przez lematy wszystkich swoich czytań, bez zawężenia
        do imiennych, bo gospodarzem bywa czasownik:
        ``jest przetwarzany`` jest dowodem o gospodarzu ``przetwarzania``.
        Kandydatów na niego wyznacza :attr:`Powtórzenie.kandydaci`,
        domyślnie :func:`_łańcuch`, a nie jedna pozycja,
        i gospodarz, którego wśród kandydatów nie ma, dowodu stąd nie dostaje.
        Lematy, którymi dopasować się nie wolno, podaje :attr:`Powtórzenie.kopuly`,
        i podstawia się je z tego samego powodu co kandydatów: żeby sonda
        wypisała cenę tego warunku zamiast pozostawiać ją różnicy między commitami.
        """
        przyimki = _lematy(przyimek)
        lematy = {gospodarz: _lematy(gospodarz) for gospodarz in gospodarze}
        znalezione: dict[str, Dowód] = {}
        for zdanie, słowa in zip(self.zdania, self._słowa, strict=True):
            for i in _gdzie_stała(słowa, przyimki, rzeczownik):
                for gospodarz, lemat in _pasujący(kandydaci(słowa, i), lematy, kopuly):
                    znalezione.setdefault(gospodarz, Dowód(lemat, zdanie))
        return znalezione

zdania = () class-attribute instance-attribute

__init__(zdania=())

przy_czym_stała(przyimek, rzeczownik, gospodarze, kandydaci=_łańcuch, kopuly=KOPULA)

Ci z gospodarzy, przy których ta fraza w tym sąsiedztwie już stała.

Fraza jest tu przyimkiem i lematami swojego rzeczownika, a nie napisem, bo w systemie i w systemach są tą samą frazą o tej samej rzeczy. Przyimek też idzie przez lemat, bo z i ze są jednym słowem. Rzeczownik idzie przez lemat imienny po obu stronach dopasowania, bo lemat wszystkich czytań zlewa odsłownik z imiesłowem (IMIENNE).

Gospodarz idzie przez lematy wszystkich swoich czytań, bez zawężenia do imiennych, bo gospodarzem bywa czasownik: jest przetwarzany jest dowodem o gospodarzu przetwarzania. Kandydatów na niego wyznacza Powtórzenie.kandydaci, domyślnie _łańcuch, a nie jedna pozycja, i gospodarz, którego wśród kandydatów nie ma, dowodu stąd nie dostaje. Lematy, którymi dopasować się nie wolno, podaje Powtórzenie.kopuly, i podstawia się je z tego samego powodu co kandydatów: żeby sonda wypisała cenę tego warunku zamiast pozostawiać ją różnicy między commitami.

Source code in olski/rozstrzyganie.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def przy_czym_stała(
    self,
    przyimek: str,
    rzeczownik: frozenset[str],
    gospodarze: Iterable[str],
    kandydaci: Kandydaci = _łańcuch,
    kopuly: frozenset[str] = KOPULA,
) -> dict[str, Dowód]:
    """Ci z gospodarzy, przy których ta fraza w tym sąsiedztwie już stała.

    Fraza jest tu przyimkiem i lematami swojego rzeczownika, a nie napisem,
    bo ``w systemie`` i ``w systemach`` są tą samą frazą o tej samej rzeczy.
    Przyimek też idzie przez lemat, bo ``z`` i ``ze`` są jednym słowem.
    Rzeczownik idzie przez lemat imienny po obu stronach dopasowania,
    bo lemat wszystkich czytań zlewa odsłownik z imiesłowem (:data:`IMIENNE`).

    Gospodarz idzie przez lematy wszystkich swoich czytań, bez zawężenia
    do imiennych, bo gospodarzem bywa czasownik:
    ``jest przetwarzany`` jest dowodem o gospodarzu ``przetwarzania``.
    Kandydatów na niego wyznacza :attr:`Powtórzenie.kandydaci`,
    domyślnie :func:`_łańcuch`, a nie jedna pozycja,
    i gospodarz, którego wśród kandydatów nie ma, dowodu stąd nie dostaje.
    Lematy, którymi dopasować się nie wolno, podaje :attr:`Powtórzenie.kopuly`,
    i podstawia się je z tego samego powodu co kandydatów: żeby sonda
    wypisała cenę tego warunku zamiast pozostawiać ją różnicy między commitami.
    """
    przyimki = _lematy(przyimek)
    lematy = {gospodarz: _lematy(gospodarz) for gospodarz in gospodarze}
    znalezione: dict[str, Dowód] = {}
    for zdanie, słowa in zip(self.zdania, self._słowa, strict=True):
        for i in _gdzie_stała(słowa, przyimki, rzeczownik):
            for gospodarz, lemat in _pasujący(kandydaci(słowa, i), lematy, kopuly):
                znalezione.setdefault(gospodarz, Dowód(lemat, zdanie))
    return znalezione

Powtórzenie dataclass

Gospodarz, przy którym ta sama fraza stała już w podanym sąsiedztwie.

Dowodem jest powtórzenie, a nie znajomość rzeczy. Wystąpiła awaria w systemie. mówi o Operator zgłosił awarię w systemie. to, czego nie mówi żaden słownik ani żadna tabela: że w systemie jest w tym tekście opisem awarii, a nie miejscem zgłoszenia, bo już raz nim było.

Jednostki sąsiedztwa świadek nie zna i nie nazywa jej w powodzie. Wyznacza ją Sąsiedztwo, dziś akapitem, a sonda mierząca cenę tej granicy podaje tu granicę inną, więc „w tym akapicie” byłoby tam nieprawdą.

Świadek milczy, kiedy fraza stała przy więcej niż jednym z gospodarzy, bo dowód wskazujący dwie strony naraz nie wskazuje żadnej. Dwaj gospodarze w jednym łańcuchu imiennym (_łańcuch) są tym samym wypadkiem: sąsiedztwo powtarza wtedy sporne przyłączenie, zamiast je rozstrzygać.

Milczy też tam, gdzie jedynym dowodem jest powtórzenie przy kopuli (kopuly), bo powtórzenie prawdziwe bywa puste. Kopula przy drugim dowodzie wskazania nie blokuje: dwóch gospodarzy liczy się po odsianiu par z takim lematem.

Source code in olski/rozstrzyganie.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
@dataclass(frozen=True)
class Powtórzenie:
    """Gospodarz, przy którym ta sama fraza stała już w podanym sąsiedztwie.

    Dowodem jest powtórzenie, a nie znajomość rzeczy. ``Wystąpiła awaria
    w systemie.`` mówi o ``Operator zgłosił awarię w systemie.`` to, czego nie
    mówi żaden słownik ani żadna tabela: że ``w systemie`` jest w tym tekście
    opisem awarii, a nie miejscem zgłoszenia, bo już raz nim było.

    Jednostki sąsiedztwa świadek nie zna i nie nazywa jej w powodzie. Wyznacza ją
    :class:`Sąsiedztwo`, dziś akapitem, a sonda mierząca cenę tej granicy podaje
    tu granicę inną, więc „w tym akapicie” byłoby tam nieprawdą.

    Świadek milczy, kiedy fraza stała przy więcej niż jednym z gospodarzy, bo
    dowód wskazujący dwie strony naraz nie wskazuje żadnej. Dwaj gospodarze
    w jednym łańcuchu imiennym (:func:`_łańcuch`) są tym samym wypadkiem:
    sąsiedztwo powtarza wtedy sporne przyłączenie, zamiast je rozstrzygać.

    Milczy też tam, gdzie jedynym dowodem jest powtórzenie przy kopuli
    (:attr:`kopuly`), bo powtórzenie prawdziwe bywa puste. Kopula przy drugim
    dowodzie wskazania nie blokuje: dwóch gospodarzy liczy się po odsianiu par
    z takim lematem.
    """

    nazwa: str = "powtórzenie"
    #: Reguła kandydata, czyli to, co w sąsiedztwie liczy się za miejsce
    #: „przy gospodarzu”. Podstawiana po to, żeby dała się wycenić wariantem,
    #: a nie po to, żeby ją zmieniać w werdykcie: cenę drukuje
    #: ``harness/powtórzenie.py``, a wywód nad nią trzyma ``docs/rozstrzyganie.md``.
    kandydaci: Kandydaci = _łańcuch
    #: Lematy, którymi gospodarz dowodu się nie dopasuje: kopula sama nic nie
    #: orzeka, więc powtórzenie przy niej mówi tylko tyle, że oba zdania mają to
    #: samo orzeczenie. Gospodarzem kopula zostaje, bo okolicznik zdania wisi na
    #: orzeczeniu; odpada dowód, a nie pozycja, i dlaczego tak, wywodzi
    #: ``docs/rozstrzyganie.md``. Lista jest pożyczona od gramatyki
    #: (``KOPULA`` w ``olski/walencja.py``), więc lemat dopisany tam przestaje być
    #: dowodem i tutaj.
    #: Podstawiane tą samą drogą i z tego samego powodu co :attr:`kandydaci`.
    kopuly: frozenset[str] = KOPULA

    def __call__(
        self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo
    ) -> Rozstrzygnięcie | None:
        formy = przyłączenie.modyfikator.split()
        if len(formy) < 2 or len(przyłączenie.gospodarze) < 2:
            return None
        rzeczownik = frozenset().union(*(_lematy_imienne(forma) for forma in formy[1:]))
        wskazani = sąsiedztwo.przy_czym_stała(
            formy[0].lower(),
            rzeczownik,
            przyłączenie.gospodarze,
            self.kandydaci,
            self.kopuly,
        )
        if len(wskazani) != 1:
            return None
        ((gospodarz, dowód),) = wskazani.items()
        return Rozstrzygnięcie(
            modyfikator=przyłączenie.modyfikator,
            gospodarz=gospodarz,
            powód=(
                f"„{przyłączenie.modyfikator}” stało już przy „{dowód.lemat}” "
                f"wyżej w tekście: „{dowód.zdanie}”"
            ),
        )

nazwa = 'powtórzenie' class-attribute instance-attribute

kandydaci = _łańcuch class-attribute instance-attribute

kopuly = KOPULA class-attribute instance-attribute

__init__(nazwa='powtórzenie', kandydaci=_łańcuch, kopuly=KOPULA)

__call__(przyłączenie, sąsiedztwo)

Source code in olski/rozstrzyganie.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
def __call__(
    self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo
) -> Rozstrzygnięcie | None:
    formy = przyłączenie.modyfikator.split()
    if len(formy) < 2 or len(przyłączenie.gospodarze) < 2:
        return None
    rzeczownik = frozenset().union(*(_lematy_imienne(forma) for forma in formy[1:]))
    wskazani = sąsiedztwo.przy_czym_stała(
        formy[0].lower(),
        rzeczownik,
        przyłączenie.gospodarze,
        self.kandydaci,
        self.kopuly,
    )
    if len(wskazani) != 1:
        return None
    ((gospodarz, dowód),) = wskazani.items()
    return Rozstrzygnięcie(
        modyfikator=przyłączenie.modyfikator,
        gospodarz=gospodarz,
        powód=(
            f"„{przyłączenie.modyfikator}” stało już przy „{dowód.lemat}” "
            f"wyżej w tekście: „{dowód.zdanie}”"
        ),
    )

Rama dataclass

Gospodarz imienny, którego rama żąda tego przyimka, gdy rama czasownika go nie żąda.

Dowodem jest słownik, a nie częstość, i tym ten świadek różni się od Skłonność: fraza, której schemat jednej ze stron żąda, przeczytana po drugiej stronie łamie ten schemat, więc wskazanie da się sprawdzić jednym wierszem olski/leksykon.txt, a nie wolno go przenieść na inną parę słów.

Wskazuje sam rzeczownik i jest to połowa kryterium, a nie jego całość. Obie strony wyceniono przed dopisaniem świadka (harness/rama.py) i wypadły inaczej: rama rzeczownika myli się rzadziej niż raz na dwadzieścia odpowiedzi, a rama czasownika tyle, ile rzut monetą nad wyborem dwóch stron. Powodem jest brak ramy, a nie rama: czasownik żąda przyimków tak licznie, że jego schemat pasuje do okolicznika, o którym nie mówi nic.

Rama czasownika zostaje przez to wetem. Żądanie obustronne kończy się milczeniem, bo schematu nie łamie wtedy żadne czytanie, i to samo weto odbiera wskazanie tam, gdzie żąda sam czasownik. Weto kosztuje zasięg — bez niego świadek odpowiada blisko dwa razy częściej — a kupuje powód pod wskazaniem: bez weta powód mówi o jednej stronie i milczy o drugiej. Obie ceny wypisuje --oceń, a wywód nad nimi trzyma docs/rozstrzyganie.md.

Milczy, gdy przyimka żąda rama więcej niż jednego gospodarza imiennego, bo dowód wskazujący dwie strony naraz nie wskazuje żadnej — tak samo jak Powtórzenie.

Source code in olski/rozstrzyganie.py
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
@dataclass(frozen=True)
class Rama:
    """Gospodarz imienny, którego rama żąda tego przyimka, gdy rama czasownika go nie żąda.

    Dowodem jest słownik, a nie częstość, i tym ten świadek różni się
    od :class:`Skłonność`:
    fraza, której schemat jednej ze stron żąda, przeczytana po drugiej stronie
    łamie ten schemat,
    więc wskazanie da się sprawdzić jednym wierszem ``olski/leksykon.txt``,
    a nie wolno go przenieść na inną parę słów.

    **Wskazuje sam rzeczownik i jest to połowa kryterium, a nie jego całość.**
    Obie strony wyceniono przed dopisaniem świadka (``harness/rama.py``)
    i wypadły inaczej:
    rama rzeczownika myli się rzadziej niż raz na dwadzieścia odpowiedzi,
    a rama czasownika tyle, ile rzut monetą nad wyborem dwóch stron.
    Powodem jest brak ramy, a nie rama:
    czasownik żąda przyimków tak licznie,
    że jego schemat pasuje do okolicznika, o którym nie mówi nic.

    **Rama czasownika zostaje przez to wetem.**
    Żądanie obustronne kończy się milczeniem,
    bo schematu nie łamie wtedy żadne czytanie,
    i to samo weto odbiera wskazanie tam, gdzie żąda sam czasownik.
    Weto kosztuje zasięg — bez niego świadek odpowiada blisko dwa razy częściej —
    a kupuje powód pod wskazaniem:
    bez weta powód mówi o jednej stronie i milczy o drugiej.
    Obie ceny wypisuje ``--oceń``,
    a wywód nad nimi trzyma ``docs/rozstrzyganie.md``.

    Milczy, gdy przyimka żąda rama więcej niż jednego gospodarza imiennego,
    bo dowód wskazujący dwie strony naraz nie wskazuje żadnej —
    tak samo jak :class:`Powtórzenie`.
    """

    nazwa: str = "rama"
    #: Czy żądanie po stronie czasownika odbiera wskazanie.
    #: Podstawiane po to, żeby cena weta wychodziła z wariantu,
    #: a nie z różnicy między commitami:
    #: wypisuje ją ``--oceń``, tak samo jak krzywą progów świadka statystycznego.
    weto: bool = True
    #: Rama słowa, domyślnie z ``olski/leksykon.txt`` (:data:`Leksykon`).
    leksykon: Leksykon = _leksykon

    def __call__(
        self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo = PUSTE
    ) -> Rozstrzygnięcie | None:
        return _wskaż(self, przyłączenie)

    def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
        """Gospodarz imienny, którego rama żąda tego przyimka, wraz z powodem.

        Jedno miejsce, w którym kryterium rozstrzyga, bo pytają o nie dwie strony,
        tak samo jak przy :meth:`Skłonność.wybierz`:
        werdykt, który ma formy gospodarzy,
        i ocena, która ma lematy z banku drzew.

        Przyimek idzie przez lemat, bo ``z`` i ``ze`` są jednym słowem,
        a Walenty wypisuje w schemacie lemat.
        Tą samą drogą idzie fraza u :meth:`Sąsiedztwo.przy_czym_stała`.

        Drugi człon powodu należy do weta, więc wariant bez weta go nie wypisuje:
        powód ma mówić, co świadek sprawdził, a nie co sprawdza zwykle.
        """
        przyimki = _lematy(przyimek)
        if self.weto and any(
            przyimki & self.leksykon(lemat, STRONA_CZASOWNIKOWA)
            for _etykieta, gdzie, lematy in kandydaci
            if gdzie == STRONA_CZASOWNIKOWA
            for lemat in lematy
        ):
            return None
        wskazani = []
        for etykieta, gdzie, lematy in kandydaci:
            if gdzie != STRONA_IMIENNA:
                continue
            if (żądanie := self._żądający(przyimki, lematy)) is not None:
                wskazani.append((etykieta, *żądanie))
        if len(wskazani) != 1:
            return None
        etykieta, lemat, przyimek_ramy = wskazani[0]
        powód = f"„{przyimek_ramy}” jest pozycją ramy „{lemat}”"
        if self.weto:
            powód += ", a rama czasownika tej pozycji nie ma"
        return etykieta, powód

    def _żądający(
        self, przyimki: frozenset[str], lematy: Iterable[str]
    ) -> tuple[str, str] | None:
        """Pierwszy lemat tej formy, którego rama żąda któregoś z tych przyimków.

        Wraca wraz z tym przyimkiem, bo powód go cytuje.
        Pierwszy, a nie każdy, bo liczą się gospodarze, a nie lematy:
        gospodarz o dwóch lematach żądających jest jednym wskazaniem, a nie dwoma.
        Alfabet rozstrzyga, gdy żąda kilka naraz — tak jak w :func:`_pasujący`,
        i z tego samego powodu: powód ma wyjść ten sam w każdym przebiegu.
        """
        for lemat in sorted(lematy):
            if pasujące := przyimki & self.leksykon(lemat, STRONA_IMIENNA):
                return lemat, sorted(pasujące)[0]
        return None

nazwa = 'rama' class-attribute instance-attribute

weto = True class-attribute instance-attribute

leksykon = _leksykon class-attribute instance-attribute

__init__(nazwa='rama', weto=True, leksykon=_leksykon)

__call__(przyłączenie, sąsiedztwo=PUSTE)

Source code in olski/rozstrzyganie.py
563
564
565
566
def __call__(
    self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo = PUSTE
) -> Rozstrzygnięcie | None:
    return _wskaż(self, przyłączenie)

wybierz(przyimek, kandydaci)

Gospodarz imienny, którego rama żąda tego przyimka, wraz z powodem.

Jedno miejsce, w którym kryterium rozstrzyga, bo pytają o nie dwie strony, tak samo jak przy Skłonność.wybierz: werdykt, który ma formy gospodarzy, i ocena, która ma lematy z banku drzew.

Przyimek idzie przez lemat, bo z i ze są jednym słowem, a Walenty wypisuje w schemacie lemat. Tą samą drogą idzie fraza u Sąsiedztwo.przy_czym_stała.

Drugi człon powodu należy do weta, więc wariant bez weta go nie wypisuje: powód ma mówić, co świadek sprawdził, a nie co sprawdza zwykle.

Source code in olski/rozstrzyganie.py
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
    """Gospodarz imienny, którego rama żąda tego przyimka, wraz z powodem.

    Jedno miejsce, w którym kryterium rozstrzyga, bo pytają o nie dwie strony,
    tak samo jak przy :meth:`Skłonność.wybierz`:
    werdykt, który ma formy gospodarzy,
    i ocena, która ma lematy z banku drzew.

    Przyimek idzie przez lemat, bo ``z`` i ``ze`` są jednym słowem,
    a Walenty wypisuje w schemacie lemat.
    Tą samą drogą idzie fraza u :meth:`Sąsiedztwo.przy_czym_stała`.

    Drugi człon powodu należy do weta, więc wariant bez weta go nie wypisuje:
    powód ma mówić, co świadek sprawdził, a nie co sprawdza zwykle.
    """
    przyimki = _lematy(przyimek)
    if self.weto and any(
        przyimki & self.leksykon(lemat, STRONA_CZASOWNIKOWA)
        for _etykieta, gdzie, lematy in kandydaci
        if gdzie == STRONA_CZASOWNIKOWA
        for lemat in lematy
    ):
        return None
    wskazani = []
    for etykieta, gdzie, lematy in kandydaci:
        if gdzie != STRONA_IMIENNA:
            continue
        if (żądanie := self._żądający(przyimki, lematy)) is not None:
            wskazani.append((etykieta, *żądanie))
    if len(wskazani) != 1:
        return None
    etykieta, lemat, przyimek_ramy = wskazani[0]
    powód = f"„{przyimek_ramy}” jest pozycją ramy „{lemat}”"
    if self.weto:
        powód += ", a rama czasownika tej pozycji nie ma"
    return etykieta, powód

Skłonność dataclass

Jak często para przyimka i gospodarza przyłączała się w banku drzew w tę stronę.

Kluczem jest lemat, a nie forma, bo o gwieździe i o gwiazdach mówią o tej samej parze. Forma gospodarza ma jednak lematów kilka, a warstwa ta stoi za parserem, który lematu nie wybrał (signature w olski/parse/czytanie.py), więc pytanie idzie o każdy lemat formy naraz i liczniki się sumują. Podnosi to wsparcie i rozmywa skłonność, a rozmycie kończy się milczeniem, czyli stroną bezpieczną.

Source code in olski/rozstrzyganie.py
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
@dataclass(frozen=True)
class Skłonność:
    """Jak często para przyimka i gospodarza przyłączała się w banku drzew w tę stronę.

    Kluczem jest lemat, a nie forma, bo ``o gwieździe`` i ``o gwiazdach`` mówią
    o tej samej parze. Forma gospodarza ma jednak lematów kilka, a warstwa ta
    stoi za parserem, który lematu nie wybrał (``signature`` w ``olski/parse/czytanie.py``),
    więc pytanie idzie o każdy lemat formy naraz i liczniki się sumują. Podnosi
    to wsparcie i rozmywa skłonność, a rozmycie kończy się milczeniem, czyli
    stroną bezpieczną.
    """

    #: ``(przyimek, strona, lemat)`` → ``(przyłączeń w tę stronę, wszystkich)``.
    licznik: Licznik
    nazwa: str = "skłonność"
    wsparcie: int = WSPARCIE
    próg: float = PRÓG

    @classmethod
    def z_pliku(cls, path: Path = SKŁONNOŚCI, **kwargs) -> Skłonność:
        return cls(licznik=czytaj(path), **kwargs)

    def __call__(
        self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo = PUSTE
    ) -> Rozstrzygnięcie | None:
        return _wskaż(self, przyłączenie)

    def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
        """Kandydat o najwyższej skłonności wraz z powodem, albo ``None`` poniżej progów.

        Jedno miejsce, w którym progi rozstrzygają, bo pytają o to dwie strony:
        werdykt, który ma formy gospodarzy, i ocena, która ma lematy z banku drzew.
        Druga kopia tej reguły kazałaby ocenie mierzyć innego świadka niż ten,
        którego wypuszcza ``olski-check``.

        Liczniki sumują się po lematach formy (:data:`Kandydat`), co podnosi
        wsparcie i rozmywa skłonność, a rozmycie kończy się milczeniem.
        """
        najlepszy = None
        for etykieta, strona, lematy in kandydaci:
            trafień, wszystkich = self._para(przyimek, strona, lematy)
            if wszystkich < self.wsparcie or trafień / wszystkich < self.próg:
                continue
            udział = trafień / wszystkich
            if najlepszy is None or udział > najlepszy[0]:
                najlepszy = (udział, etykieta, trafień, wszystkich)
        if najlepszy is None:
            return None
        _udział, etykieta, trafień, wszystkich = najlepszy
        return etykieta, (
            f"„{przyimek}” przy „{etykieta}” doszło tam w {trafień} z {wszystkich} "
            f"wypadków banku drzew, {trafień / wszystkich:.0%}"
        )

    def _para(self, przyimek: str, strona: str, lematy: Iterable[str]) -> tuple[int, int]:
        """Liczniki pary, zsumowane po podanych lematach."""
        trafień = wszystkich = 0
        for lemat in lematy:
            para = self.licznik.get((przyimek, strona, lemat))
            if para:
                trafień += para[0]
                wszystkich += para[1]
        return trafień, wszystkich

licznik instance-attribute

nazwa = 'skłonność' class-attribute instance-attribute

wsparcie = WSPARCIE class-attribute instance-attribute

próg = PRÓG class-attribute instance-attribute

__init__(licznik, nazwa='skłonność', wsparcie=WSPARCIE, próg=PRÓG)

z_pliku(path=SKŁONNOŚCI, **kwargs) classmethod

Source code in olski/rozstrzyganie.py
645
646
647
@classmethod
def z_pliku(cls, path: Path = SKŁONNOŚCI, **kwargs) -> Skłonność:
    return cls(licznik=czytaj(path), **kwargs)

__call__(przyłączenie, sąsiedztwo=PUSTE)

Source code in olski/rozstrzyganie.py
649
650
651
652
def __call__(
    self, przyłączenie: Przyłączenie, sąsiedztwo: Sąsiedztwo = PUSTE
) -> Rozstrzygnięcie | None:
    return _wskaż(self, przyłączenie)

wybierz(przyimek, kandydaci)

Kandydat o najwyższej skłonności wraz z powodem, albo None poniżej progów.

Jedno miejsce, w którym progi rozstrzygają, bo pytają o to dwie strony: werdykt, który ma formy gospodarzy, i ocena, która ma lematy z banku drzew. Druga kopia tej reguły kazałaby ocenie mierzyć innego świadka niż ten, którego wypuszcza olski-check.

Liczniki sumują się po lematach formy (Kandydat), co podnosi wsparcie i rozmywa skłonność, a rozmycie kończy się milczeniem.

Source code in olski/rozstrzyganie.py
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
def wybierz(self, przyimek: str, kandydaci: Sequence[Kandydat]) -> tuple[str, str] | None:
    """Kandydat o najwyższej skłonności wraz z powodem, albo ``None`` poniżej progów.

    Jedno miejsce, w którym progi rozstrzygają, bo pytają o to dwie strony:
    werdykt, który ma formy gospodarzy, i ocena, która ma lematy z banku drzew.
    Druga kopia tej reguły kazałaby ocenie mierzyć innego świadka niż ten,
    którego wypuszcza ``olski-check``.

    Liczniki sumują się po lematach formy (:data:`Kandydat`), co podnosi
    wsparcie i rozmywa skłonność, a rozmycie kończy się milczeniem.
    """
    najlepszy = None
    for etykieta, strona, lematy in kandydaci:
        trafień, wszystkich = self._para(przyimek, strona, lematy)
        if wszystkich < self.wsparcie or trafień / wszystkich < self.próg:
            continue
        udział = trafień / wszystkich
        if najlepszy is None or udział > najlepszy[0]:
            najlepszy = (udział, etykieta, trafień, wszystkich)
    if najlepszy is None:
        return None
    _udział, etykieta, trafień, wszystkich = najlepszy
    return etykieta, (
        f"„{przyimek}” przy „{etykieta}” doszło tam w {trafień} z {wszystkich} "
        f"wypadków banku drzew, {trafień / wszystkich:.0%}"
    )

rozstrzygnij(przyłączenia, świadkowie=None, sąsiedztwo=None)

Po jednej odpowiedzi na przyłączenie, w kolejności, w jakiej je podano.

Przyłączenie, o którym nie wypowiedział się nikt, wraca takie, jakie weszło, a nie znika: warstwa ma powiedzieć, czego nie rozstrzygnęła, tak samo jak to, co rozstrzygnęła.

Sąsiedztwem domyślnym jest puste, czyli zdanie postawione samo. Odpowiada to olski-check -c z jednym zdaniem i jest stroną bezpieczną: świadek kontekstowy milczy wtedy zamiast czytać kontekst, którego nie dostał.

Source code in olski/rozstrzyganie.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def rozstrzygnij(
    przyłączenia: Iterable[Przyłączenie],
    świadkowie: Sequence[Świadek] | None = None,
    sąsiedztwo: Sąsiedztwo | None = None,
) -> list[Rozstrzygnięcie | Przyłączenie]:
    """Po jednej odpowiedzi na przyłączenie, w kolejności, w jakiej je podano.

    Przyłączenie, o którym nie wypowiedział się nikt, wraca takie, jakie weszło,
    a nie znika: warstwa ma powiedzieć, czego nie rozstrzygnęła, tak samo jak to,
    co rozstrzygnęła.

    Sąsiedztwem domyślnym jest puste, czyli zdanie postawione samo. Odpowiada to
    ``olski-check -c`` z jednym zdaniem i jest stroną bezpieczną: świadek
    kontekstowy milczy wtedy zamiast czytać kontekst, którego nie dostał.
    """
    if świadkowie is None:
        świadkowie = domyślni()
    if sąsiedztwo is None:
        sąsiedztwo = PUSTE
    odpowiedzi: list[Rozstrzygnięcie | Przyłączenie] = []
    for przyłączenie in przyłączenia:
        odpowiedzi.append(_pierwszy(przyłączenie, świadkowie, sąsiedztwo) or przyłączenie)
    return odpowiedzi

domyślni()

Świadkowie w kolejności rodzaju dowodu, od tekstu autora do cudzego korpusu.

Source code in olski/rozstrzyganie.py
186
187
188
def domyślni() -> list[Świadek]:
    """Świadkowie w kolejności rodzaju dowodu, od tekstu autora do cudzego korpusu."""
    return [Powtórzenie(), Rama(), Skłonność.z_pliku()]

sąsiedztwa(text)

Po jednym sąsiedztwie na zdanie tekstu, w kolejności zdań.

Dokument buduje się tu drugi raz, obok tego, który zdania oddał gramatyce, i nie jest to rozjazd: podział jest własnością tekstu, a nie miejsca, które o niego pyta (Document w olski/document.py).

Source code in olski/rozstrzyganie.py
408
409
410
411
412
413
414
415
416
417
418
419
420
def sąsiedztwa(text: str) -> list[Sąsiedztwo]:
    """Po jednym sąsiedztwie na zdanie tekstu, w kolejności zdań.

    Dokument buduje się tu drugi raz, obok tego, który zdania oddał gramatyce, i
    nie jest to rozjazd: podział jest własnością tekstu, a nie miejsca, które o
    niego pyta (``Document`` w ``olski/document.py``).
    """
    document = Document(text)
    zdania = [document.slice(span) for span in document.sentences]
    return [
        Sąsiedztwo(tuple(zdania[numer] for numer in wcześniejsze))
        for wcześniejsze in document.wcześniejsze
    ]

strona(forma)

Po której stronie wyboru stoi gospodarz o tej formie.

Jedno miejsce, bo pytają o to dwie strony: świadek statystyczny, który pod tą nazwą bierze liczniki pary, i pomiar, który jego wskazanie zestawia z cudzym drzewem (harness/wskazania.py). Druga kopia tej reguły kazałaby pomiarowi mierzyć innego świadka niż ten, którego wypuszcza olski-check.

Source code in olski/rozstrzyganie.py
759
760
761
762
763
764
765
766
767
768
769
770
def strona(forma: str) -> str:
    """Po której stronie wyboru stoi gospodarz o tej formie.

    Jedno miejsce, bo pytają o to dwie strony:
    świadek statystyczny, który pod tą nazwą bierze liczniki pary,
    i pomiar, który jego wskazanie zestawia z cudzym drzewem
    (``harness/wskazania.py``).
    Druga kopia tej reguły kazałaby pomiarowi mierzyć innego świadka niż ten,
    którego wypuszcza ``olski-check``.
    """
    czasownikowa = any(reading.tag.pos in CZASOWNIKOWE for reading in _czytania(forma))
    return STRONA_CZASOWNIKOWA if czasownikowa else STRONA_IMIENNA

czytaj(path=SKŁONNOŚCI)

Tabela z pliku; brak pliku znaczy świadka, który milczy zawsze.

Wypisuje ten plik harness/skłonności.py, czyli druga strona granicy pakietu: czyta go każdy, kto sprawdza własny tekst, a buduje ten, kto olskiego zmienia.

Source code in olski/rozstrzyganie.py
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def czytaj(path: Path = SKŁONNOŚCI) -> Licznik:
    """Tabela z pliku; brak pliku znaczy świadka, który milczy zawsze.

    Wypisuje ten plik ``harness/skłonności.py``, czyli druga strona granicy
    pakietu: czyta go każdy, kto sprawdza własny tekst, a buduje ten, kto
    olskiego zmienia.
    """
    if not path.exists():
        return {}
    licznik = {}
    for wiersz in path.read_text(encoding="utf-8").splitlines():
        if not wiersz or wiersz.startswith("#"):
            continue
        przyimek, strona, lemat, trafień, ile = wiersz.split("\t")
        licznik[(przyimek, strona, lemat)] = (int(trafień), int(ile))
    return licznik