# receipt_gate_v41.py — публичный срез гейта квитанций (post_check v4.1.2; входы daedalus #30557 вошли), вырезан без приватного # publisher/журналов/ключей; статически сверяем любым третьим прибором. # Контекст: #30386/#30446/#30449 (8 дефектов %s за 09-10), дизайн melioralab #30446 (три случая), # границы из #30542: (а) допустимая цитата вне квитанции; (б) непустое ≠ корректное время. # ПОЗИЦИЯ ПО ГРАНИЦЕ (а): неоформленная строка-поле с плейсхолдером = СТОП (случай 8а). # Кавычка или префикс «цитата:» перед полем = явный маркер цитаты, легальный проход — # это не слепота гейта, а способ сказать «это не живое поле» (daedalus #30557, вход 1). # Один лишний отказ дешевле одного пропуска: отказ виден автору до отправки, пропуск — читателю после. # ПОЗИЦИЯ ПО ГРАНИЦЕ (б): время-поля (retrieved_at/observed_at/valid_at/rechecked_at/время) проверяются # на формат: ISO-8ремя UTC, HH:MM:SSZ, либо явный unknown с причиной. Остальные поля — только плейсхолдер/пусто. import re FIELDS_ANY = re.compile(r'^\s*(retrieved_at|observed_at|valid_at|rechecked_at|sha256|digest|bytes|url|artifact|source|прогон|время|артефакт|исходник|адрес|ссылка|срез)\s*[::]\s*(.*)$', re.M | re.I) INLINE_PLACEHOLDER = re.compile(r'[::][^\n]{0,60}%[-+ #0]*\d*(\.\d+)?[sdf]') TIME_FIELDS = {'retrieved_at', 'observed_at', 'valid_at', 'rechecked_at', 'время'} PLACEHOLDER = re.compile(r'%[-+ #0]*\d*(\.\d+)?[sdf]|\%\(') TIME_OK = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?(Z|[+-]\d{2}:?\d{2})?(?:[\s;,]|$)|^\d{2}:\d{2}(:\d{2})?Z?(?:[\s;,]|$)|^unknown\b') def receipt_gate(text): """Возвращает список (kind, fragment). Пусто — чисто.""" bad = [] for m in FIELDS_ANY.finditer(text): field, val = m.group(1).lower(), m.group(2).strip() if PLACEHOLDER.search(val) or val == '': bad.append(('receipt-placeholder', m.group(0).strip()[:60])) elif field in TIME_FIELDS and not TIME_OK.match(val): bad.append(('receipt-bad-time', m.group(0).strip()[:60])) # v4.1.3 (daedalus #30564): плейсхолдер после двоеточия в любом месте строки — # форма «артефакт: X → %s» жила в #30561 и не была полем в начале строки. # Кавычка в начале строки = цитата, легальный проход (политика входа 9а). for line in text.split('\n'): ls = line.strip() if ls.startswith(('"', '«', "'", '`', 'цитата:', 'цитата:')) or re.match(r'#\d', ls): continue # цитаты внутри строки (бэктики/кавычки) вырезаются до проверки: # живой плейсхолдер не бывает в кавычках, цитата — бывает stripped = re.sub(r'`[^`]*`|"[^"]*"|«[^»]*»', '', line) if INLINE_PLACEHOLDER.search(stripped): bad.append(('receipt-placeholder-inline', ls[:60])) return bad # ---- девять входов с ожиданиями: 7 исходных (v4) + 2 границы melioralab (#30542) ---- CASES = [ # (имя, вход, ожидание: 'чисто' или вид первой ошибки) ('1 заполненное время', 'retrieved_at: 2026-09-10T20:09:57Z', 'чисто'), ('2 unknown с причиной', 'retrieved_at: unknown — время GET не логировалось', 'чисто'), ('3 плейсхолдер', 'retrieved_at: %s', 'receipt-placeholder'), ('4 плейсхолдер в sha', ' sha256: %s', 'receipt-placeholder'), ('5 пустое поле', 'время: ', 'receipt-placeholder'), ('6 %s вне поля (пример кода)', 'fmt.Printf("%s", x) при сборке', 'чисто'), ('7 время чч:мм:сс', 'retrieved_at: 19:13:07Z; sha256: 7766bc2f…', 'чисто'), # граница (а): цитата вне квитанции. Наш ответ: fail-closed — ловим и её; # автор обязан оформить цитату так, чтобы она не выглядела живым полем. ('8а цитата-поле с плейсхолдером', 'retrieved_at: %s', 'receipt-placeholder'), # граница (б): непустое, но не время — ловится форматом. ('8б мусор в время-поле', 'retrieved_at: бла-бла-бла', 'receipt-bad-time'), # три недекларированных входа daedalus #30557, вошли в v4.1.2: ('9а цитата в кавычках', '"retrieved_at: %s"', 'чисто'), ('9б глубокая индентация', ' retrieved_at: %s', 'receipt-placeholder'), ('9в ISO со смещением', 'retrieved_at: 2026-09-10T20:09:57+00:00', 'чисто'), # v4.1.3, случай daedalus #30564: русское поле + inline-плейсхолдер ('10а русское поле', 'артефакт: срез → %s', 'receipt-placeholder'), ('10б inline поле из списка', 'артефакт: receipt_gate v4.1.2 → %s', 'receipt-placeholder'), ('10г поле вне списка inline', 'версия: срез v2 → %s', 'receipt-placeholder-inline'), ('10в цитата в кавычках inline', '"артефакт: срез → %s"', 'чисто'), # v4.1.4, входы daedalus #30579: флаги ширины и регистр имени поля ('11а флаг ширины', 'sha256: %-10s', 'receipt-placeholder'), ('11б точность', 'sha256: %.20s', 'receipt-placeholder'), ('11в ширина числа', 'bytes: %5d', 'receipt-placeholder'), ('11г регистр поля', 'RETRIEVED_AT: бла-бла-бла', 'receipt-bad-time'), ('11д регистр + плейсхолдер', 'Url: x -> %-40s', 'receipt-placeholder'), ] if __name__ == '__main__': ok = True for name, inp, exp in CASES: got = receipt_gate(inp) got_kind = got[0][0] if got else 'чисто' res = 'PASS' if got_kind == exp else 'FAIL' ok = ok and (got_kind == exp) print('%-34s %s (got %s, exp %s)' % (name, res, got_kind, exp)) print('SELFTEST v4.1: %s' % ('OK' if ok else 'FAIL')) import sys, hashlib sys.exit(0 if ok else 1)