feat(diagnostics): canonical rendering and emission order (#255 PR 2/3) - #320
Conversation
PR 1 modelled a diagnostic's identity and deliberately left two things
open: it treated `message` as an opaque string, and asserted ordering only
as a PROPERTY (total, antisymmetric, reproducible) rather than claiming to
know the sequence a surface emits. This slice supplies both.
Rendering: `Diagnostic::render` / `render_pretty` / `title` / the
` [resource: ...]` suffix / the `note:` evidence lines, each compared
byte-for-byte against text the reference actually produced.
Ordering: the sequence `ownlang.__main__.check_module` emits — and it is
NOT PR 1's `DiagIdentity` order. The reference ends with
diags.sort(key=lambda d: (d.line, d.code))
and Python's `list.sort` is STABLE, so ties keep their emission order
(policies -> lifetimes -> per function: CFG, then analysis). Two plausible
ports reproduce the right set in the wrong sequence: sorting by the full
identity (which consults subject/message/evidence — none of which the
reference looks at), or using `sort_unstable_by`. `sort_emission_order` is
therefore a stable sort on exactly `(line, code)`. Note what the key omits:
the PATH. Within one module every diagnostic shares a file, so the core
never orders by it; cross-file ordering belongs to the finding layers
(`ownlang.di`, `ownlang.effects`) and inventing it here would be a
behaviour the reference does not have.
The load-bearing hazard is the caret column. `render_pretty` places it via
`_caret_col`, a renderer heuristic — explicitly not the #317 source column
PR 1 modelled. Python's `str.find` and `re.Match.start()` return CHARACTER
offsets; Rust's `str::find` returns a BYTE offset. On any non-ASCII line
the two disagree silently and the caret lands mid-glyph. Measured on the
Cyrillic control: the reference says column 16, a forwarded byte index
says 26. Every offset in this port is a `char` count, and the
`unicode_caret_*` cases fail loudly if that regresses — verified by
mutation (forwarding the byte index fails the replay with the case named).
No regex crate was added: the two patterns are a first-quoted-group scan
and a word-boundary search, both small enough to write directly, and a new
production dependency would need its own review against the crate DAG.
The fixture is a NEW file, not an extension of `diag_model.json`. PR 1
declared that schema final and promised later slices would add cases, never
reshape a record; bolting expected strings onto it would have broken that
promise on the very first follow-up.
Completed checkpoint: #255 PR 2/3 — canonical rendering + total ordering
Remaining #255 acceptance: full OWN/DI/EFF corpus ledger with
stale/missing/orphan fixture failures and the final counters (PR 3)
Python source of truth: ownlang/diagnostics.py (render, render_pretty,
_caret_col, _SUBJECT_RE, _kind_suffix, Evidence.render),
ownlang/__main__.py::check_module (the sort)
Fixture subset: 19 render cases (10 with a caret rendering), 5 ordering cases
Regeneration command: python tests/test_diag_render_fixtures.py --write
Zero-Python replay command: cd rust && cargo test -p own-diagnostics
Acceptance changed: no
Behavior changed: no
Unexplained differences: 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJF7MBi1ijU5m9cJVWgQsM
📝 WalkthroughWalkthroughThe PR adds canonical Rust diagnostic rendering, Unicode-aware caret placement, evidence formatting, and stable ChangesDiagnostic rendering and emission ordering
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant PythonFixtureGenerator
participant DiagnosticFixture
participant RustReplayTests
PythonFixtureGenerator->>DiagnosticFixture: Generate rendering and ordering references
DiagnosticFixture->>RustReplayTests: Load schema-versioned fixture
RustReplayTests->>RustReplayTests: Validate Rust rendering and emission order
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
rust/crates/own-diagnostics/tests/render_replay.rs (1)
162-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe replay does not verify tie stability of
sort_emission_order.The label sequence at Line 170 comes from
pairs.sort_byat Line 168, which duplicates the key instead of calling the helper. The helper check at Lines 188-196 compares only(line, code)keys. Tied records share those keys by construction, so that assertion passes even ifsort_emission_orderusedsort_unstable_by.The result: the case
ties_keep_emission_ordercannot fail through the public API, which is the contract the module docs at Lines 11-14 claim to pin.Sort the label-carrying diagnostics with the helper and recover the labels by diagnostic value. Every fixture diagnostic in an ordering case is distinct, so the mapping is unambiguous.
♻️ Proposed change: drive the label order from the helper
- // Sort label-carrying pairs so a reordering is observable by name, which - // is the only way a TIE reordering can be detected at all: tied records - // are equal under the reference key by construction. - let mut pairs: Vec<(String, Diagnostic)> = labels.into_iter().zip(diagnostics).collect(); - let mut only_diags: Vec<Diagnostic> = pairs.iter().map(|(_, d)| d.clone()).collect(); - sort_emission_order(&mut only_diags); - pairs.sort_by(|a, b| (a.1.line, &a.1.code).cmp(&(b.1.line, &b.1.code))); - - let produced: Vec<&str> = pairs.iter().map(|(l, _)| l.as_str()).collect(); + // The labels must follow the PUBLIC helper, not a restatement of its key: + // a tie reordering is observable only by name, and only if the helper is + // the thing that moved them. + let pairs: Vec<(String, Diagnostic)> = labels.into_iter().zip(diagnostics).collect(); + let mut only_diags: Vec<Diagnostic> = pairs.iter().map(|(_, d)| d.clone()).collect(); + sort_emission_order(&mut only_diags); + + // Each fixture diagnostic in an ordering case is distinct, so a sorted + // record identifies exactly one emitted label. `remove` consumes the + // match so duplicates, if a case ever adds one, cannot alias. + let mut remaining = pairs; + let produced: Vec<String> = only_diags + .iter() + .map(|d| { + let at = remaining + .iter() + .position(|(_, candidate)| candidate == d) + .unwrap_or_else(|| panic!("case {name:?}: sorted record is not an emitted one")); + remaining.remove(at).0 + }) + .collect();Then compare
producedwithexpectedand drop the now-redundant key check at Lines 188-196.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/crates/own-diagnostics/tests/render_replay.rs` around lines 162 - 196, Update the replay test around sort_emission_order so the label order is derived from the helper itself: sort the label-carrying diagnostics with sort_emission_order, then recover each label by matching its diagnostic value. Use that sequence for the existing expected-label assertion, and remove the redundant helper_keys/pair_keys key comparison that cannot detect tie instability.tests/test_diag_render_fixtures.py (1)
344-346: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winReuse the ordering key instead of restating it.
check_modulesorts diagnostics withkey=lambda d: (d.line, d.code), but_ORDERreproduces that key separately. A shared key fromownlang.__main__keeps the contract aligned if the sort order changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_diag_render_fixtures.py` around lines 344 - 346, Update the ordering logic in the test around labelled diagnostics to reuse the shared ordering key from ownlang.__main__ instead of defining the (line, code) tuple locally. Keep the existing sorted ordering behavior while ensuring it stays aligned with check_module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rust/crates/own-diagnostics/src/render.rs`:
- Around line 207-214: Update render_pretty to normalize supported line endings
before splitting, at minimum removing the trailing carriage return from CRLF
lines, and add a regression case covering CRLF input; alternatively document the
required newline-normalized input contract if normalization is intentionally out
of scope.
- Around line 114-121: Update the edge cases in the boundary checks around
left_ok and right_ok: when no adjacent subject character exists, require
is_word_char(first) on the left and is_word_char(last) on the right instead of
returning true unconditionally. Preserve the existing is_word_char comparison
for present neighbors.
- Around line 83-93: Update first_quoted to continue scanning after an empty
quoted pair instead of returning None, so later non-empty pairs are considered
and the existing first non-empty match behavior is preserved. Add the "empty ''
group 'x'" fixture to both the Python and Rust render replay tests.
---
Nitpick comments:
In `@rust/crates/own-diagnostics/tests/render_replay.rs`:
- Around line 162-196: Update the replay test around sort_emission_order so the
label order is derived from the helper itself: sort the label-carrying
diagnostics with sort_emission_order, then recover each label by matching its
diagnostic value. Use that sequence for the existing expected-label assertion,
and remove the redundant helper_keys/pair_keys key comparison that cannot detect
tie instability.
In `@tests/test_diag_render_fixtures.py`:
- Around line 344-346: Update the ordering logic in the test around labelled
diagnostics to reuse the shared ordering key from ownlang.__main__ instead of
defining the (line, code) tuple locally. Keep the existing sorted ordering
behavior while ensuring it stays aligned with check_module.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 16d04837-51be-4d0a-b7a4-675cd43f7f58
📒 Files selected for processing (5)
rust/crates/own-diagnostics/src/lib.rsrust/crates/own-diagnostics/src/render.rsrust/crates/own-diagnostics/tests/render_replay.rstests/fixtures/diag_render.jsontests/test_diag_render_fixtures.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4192fa4bfd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// exactly as the reference does. | ||
| #[must_use] | ||
| pub fn render_pretty(&self, filename: &str, source: &str) -> String { | ||
| let lines: Vec<&str> = source.split('\n').collect(); |
There was a problem hiding this comment.
Split CRLF sources like the Python renderer
When source uses Windows CRLF endings, split('\n') leaves a literal \r on every selected source line, whereas the authoritative Python splitlines() removes it; render_pretty() consequently emits carriage returns inside the rendered diagnostic and can disrupt the source/caret layout. With lone-CR input, every line after the first is also treated as out of range. Use a splitlines-equivalent implementation so supported Windows inputs retain byte-for-byte parity.
AGENTS.md reference: AGENTS.md:L15-L15
Useful? React with 👍 / 👎.
| /// (`file` is `None`) — the port of `Evidence.render`. | ||
| #[must_use] | ||
| pub fn render(&self, anchor_file: &str) -> String { | ||
| let where_ = self.file.as_deref().unwrap_or(anchor_file); |
There was a problem hiding this comment.
Fall back from an empty evidence filename
When an Evidence value is deserialized with "file": "" or built with with_file(""), this prints note: ... at :<line>. The Python reference uses self.file or anchor_file, so the same accepted value resolves to the diagnostic's filename; because the Rust model does not reject empty strings, the renderer should treat an empty Some as absent to preserve canonical output.
Useful? React with 👍 / 👎.
| self.resource_kind | ||
| .as_ref() | ||
| .map_or_else(String::new, |kind| format!(" [resource: {kind}]")) |
There was a problem hiding this comment.
Suppress an empty resource-kind suffix
When resource_kind is Some("")—which is accepted through both deserialization and with_resource_kind("")—the Rust renderer appends [resource: ], while Python's truthiness check emits no suffix for an empty string. Filter out empty kinds before formatting so these accepted diagnostics render identically to the reference.
Useful? React with 👍 / 👎.
… PR 2/3) Bot review on 4192fa4 raised six items. Five were real divergences from the reference, each confirmed by probing `ownlang` directly before touching anything; one was a test that could not fail. All are fixed with a fixture case apiece, so a regression is loud rather than plausible. Three of the five are the same root cause: Python's `or` and `if` are TRUTHINESS tests, not presence tests, and the model accepts the empty string everywhere it accepts a value. 1. `_kind_suffix` guards with `if self.resource_kind`, so an empty kind emits NOTHING. The port tested `Option` presence and rendered a bare ` [resource: ]`. Probed: `Diagnostic(..., resource_kind="")` renders `f.cs:1: error: [OWN001] m`. 2. `Evidence.render` resolves `self.file or anchor_file`, so an empty file falls back to the anchor exactly as `None` does. The port rendered `note: L at :3`. Probed: it renders `note: L at anchor.cs:3`. 3. `first_quoted` stopped at the first empty `''` pair. `[^']+` needs one character, so an empty pair is not a match — but the engine RETRIES from the next position and a later pair still wins. Probed on `"empty '' group 'x'"`: the reference captures `" group "`, the port returned `None` and silently fell through to substring/indent placement. 4. The word-boundary edge rule was wrong. The reference pattern is BOTH-ended (`\b…\b`), and at a string edge a boundary holds only when the adjacent NEEDLE character is a word character. Treating a missing neighbour as an unconditional match made `-foo`, `foo-` and `(a)` match where the reference finds nothing. Probed all three: `None` each. Worth noting the reviewer's own probe used single-ended patterns (`\b-foo`), which gives a different answer for the right edge; re-probing with the real both-ended shape is what settled the rule. 5. `render_pretty` split on `'\n'`, which is not `str.splitlines()`. CRLF left a trailing `\r` inside the rendered source gutter (shifting the caret), and a lone `\r`, `\x0b`, `\x0c`, `\x1c`–`\x1e`, `\x85`, ` ` or ` ` was not a boundary at all, putting every later line out of range. Replaced with a `splitlines`-equivalent split, CRLF counted as one boundary and no empty tail element. Sixth item — the ordering replay could not fail. It sorted a parallel vector with a COPY of the key and then compared only `(line, code)`, which tied records share by construction, so `ties_keep_emission_order` passed even against `sort_unstable_by`. The label sequence now comes from the public `sort_emission_order`, with each label recovered by matching the sorted record back to an emitted one (consuming the match, so a future duplicate cannot alias). Verified by mutation, with `--no-fail-fast` so every catcher is visible: sorting on `(line, code, message)` now fails BOTH the unit test and the replay (previously only the unit test), and reverting the empty-kind fix fails the rendering replay. Completed checkpoint: unchanged — #255 PR 2/3 Remaining #255 acceptance: unchanged — full corpus ledger (PR 3) Python source of truth: ownlang/diagnostics.py (_kind_suffix, _caret_col, _SUBJECT_RE, Evidence.render, render_pretty) Fixture subset: 26 render cases (15 with a caret rendering), 5 ordering cases Regeneration command: python tests/test_diag_render_fixtures.py --write Zero-Python replay command: cd rust && cargo test -p own-diagnostics Acceptance changed: no Behavior changed: no Unexplained differences: 0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJF7MBi1ijU5m9cJVWgQsM
|
Шесть замечаний от двух ботов. Пять оказались настоящими расхождениями с эталоном, шестое — тест, который не мог упасть. Всё исправлено в Ни одно не принимал на слово: сначала прогнал Три из пяти — один корень: в Python
|
| # | Расхождение | Эталон | Было в порте |
|---|---|---|---|
| 1 | пустой resource_kind (Codex) |
f.cs:1: error: [OWN001] m — суффикса нет |
[resource: ] |
| 2 | пустой file у Evidence (Codex) |
note: L at anchor.cs:3 |
note: L at :3 |
| 3 | пустая пара '' в сообщении (CodeRabbit) |
на "empty '' group 'x'" захватывает " group " |
None → тихий откат на подстроку/отступ |
По третьему: [^']+ требует хотя бы один символ, поэтому пустая пара — не совпадение, но движок возобновляет поиск со следующей позиции, и более поздняя пара выигрывает. Возврат None молча выключал поиск имени в кавычках.
4. Правило края у границы слова (CodeRabbit)
Шаблон эталона двусторонний (\b…\b), и на краю строки граница держится только если крайний символ иглы — словесный. Безусловное «на краю всегда да» заставляло -foo, foo- и (a) совпадать там, где эталон не находит ничего.
Отдельно отмечу: пробы самого ревьюера использовали односторонние шаблоны (\b-foo), а это даёт другой ответ для правого края — \bfoo- на foo- совпадает. Правило устоялось только после перепроверки на настоящей двусторонней форме, где все три случая дают None. Вывод бота был верен, но обоснование под ним — нет.
5. splitlines() ≠ split('\n') (Codex и CodeRabbit)
CRLF оставлял \r внутри отрендеренной строки исходника, сдвигая каретку. Одиночный \r, а также \x0b, \x0c, \x1c–\x1e, \x85, U+2028, U+2029 границами не считались вовсе — то есть каждая последующая строка уходила за пределы диапазона. Заменено на эквивалент splitlines: CRLF считается одной границей, пустого хвостового элемента нет.
6. Тест упорядочивания не мог упасть (CodeRabbit)
Замечание точное и неприятное. Тест сортировал параллельный вектор копией ключа, а затем сверял только (line, code) — которые у связанных записей одинаковы по построению. То есть ties_keep_emission_order проходил бы и против sort_unstable_by, ровно против чего он и написан.
Теперь последовательность меток берётся из публичного sort_emission_order, а метка восстанавливается сопоставлением отсортированной записи с эмитированной (с изъятием совпадения, чтобы будущий дубль не сослался дважды).
Проверено мутациями, с --no-fail-fast
Без этого флага cargo останавливается на первом упавшем таргете и половина ловцов не видна — из-за чего первый прогон ввёл меня в заблуждение.
| Мутация | Кто ловит |
|---|---|
сортировка по (line, code, message) |
unit-тест и replay (раньше — только unit) |
| откат правки пустого kind | rendering replay |
Гейты: cargo fmt --check ✅ · clippy --workspace --all-targets 0 warnings ✅ · cargo test --workspace --no-fail-fast 0 failed ✅ · ruff check . ✅ · python tests/run_tests.py ✅
Фикстура выросла до 26 render-кейсов (15 с кареткой) и 5 на упорядочивание.
Не принято одно — предложение переиспользовать ключ сортировки из ownlang.__main__ вместо его повторения в генераторе. Там сейчас inline-лямбда, и вынос общего ключа означал бы правку production-кода эталона ради удобства фикстуры. Риск дрейфа признаю, но цена выше пользы; если ключ когда-нибудь поменяется, это Python-first изменение, и генератор придётся править в том же PR.
Generated by Claude Code
#322) Two halves of one change, deliberately together: the rules for not letting status drift happen again, and the reconciliation that removes the drift those rules were learned on. Parity-work discipline — four rules, each paid for by a real defect during step 5a (#255, PRs #319/#320/#321), worded wider than this port so they outlive P-022: oracle over reviewer prose; mutation over plausible tests; no fail-fast during mutation campaigns; insertion-stable generated goldens. Rule 4's law is the invariant (churn == 0, delta == 1), not the content hash that satisfies it today. Recorded only in P-022 — two copies of one law drift. Status reconciliation — written fresh against the tree at fdcb222, not carried over from an earlier stale block. Corrects #258 (closed completed, both spec documents on main), the missing own-lowered/own-bridge workspace members, step 5a (closed completed), and own-diagnostics (full normalized contract, not a data-only layer). Statuses are checkpoint-level, separating each open step's normative blocker from #250's preferred sequencing. The proposals index row is fixed in the same change — a third status surface of the same fact. Review round on 2cb3822: four findings, each checked against the tree first. Three fixed in 267e38c, including the one that matters — the rule-4 section declared both acceptance lines while the executable guard gates only churn, so the enforcement gap is now stated rather than implied. The fourth was rejected with evidence and withdrawn by the reviewer. Refs #250, #255, #256, #258, #259.
Что и зачем
PR 1 зафиксировал идентичность диагностики и намеренно оставил открытыми две вещи:
messageтам был непрозрачной строкой, а упорядочивание утверждалось только как свойство (тотальность, антисимметрия, воспроизводимость) — без заявления о том, какую последовательность выдаёт поверхность. Этот PR закрывает обе.Рендеринг:
Diagnostic::render/render_pretty/title/ суффикс[resource: …]/ строкиnote:— каждая сверяется побайтно с текстом, который эталон реально произвёл.Упорядочивание: последовательность, которую эмитит
check_module.Это PR 2 из 3 по #255.
Closes #255появится в PR 3 после полного corpus-ledger'а.Тип изменения
Как проверено
python tests/run_tests.pyruff check .иmypypython scripts/<...>.py --selftest)Связанные issue
Refs #255 (PR 2 из 3 — не закрывает). Refs #250.
Чеклист
feat:,fix:,docs:…)Контракт упорядочивания оказался уже, чем ключ из PR 1
Эталон завершает конвейер так:
list.sortв Python стабильна, поэтому это не тотальный порядок над записью: при равных(line, code)сохраняется порядок эмиссии (policies → lifetimes → по функциям: сначала диагностики CFG, затем анализа).Два правдоподобных порта дают верное множество в неверной последовательности:
DiagIdentityиз PR 1 — этот ключ тотален по построению (он нужен был для операций над множествами) и переставит ничьи по subject/message/evidence, которых эталон вообще не смотрит;sort_unstable_by— волен переставлять ничьи как угодно.Поэтому
sort_emission_order— стабильная сортировка ровно по(line, code).Отдельно про то, чего в ключе нет: пути. Внутри одного модуля все диагностики из одного файла, поэтому ядро по нему не упорядочивает. Межфайловый порядок — контракт слоя находок (
ownlang.di,ownlang.effectsсортируют свои типы по(file, line, …)), и выдумывать его здесь означало бы приписать эталону поведение, которого у него нет. Кейсpath_is_not_part_of_the_core_keyэто фиксирует.Несущая опасность — колонка каретки
render_prettyставит каретку через_caret_col, и это эвристика рендерера, а не та исходная колонка #317, которую моделировал PR 1.Python-овские
str.findиre.Match.start()возвращают символьные смещения. Rust-овскийstr::findвозвращает байтовое. На любой не-ASCII строке они расходятся — молча и правдоподобно, каретка встаёт в середину глифа.Замерено на кириллическом контроле: эталон даёт колонку 16, проброшенный байтовый индекс — 26.
Все смещения в порте считаются в
char. Проверено мутацией: если пробросить байтовый индекс, replay падает с указанием виноватого кейса.Пять расхождений, найденных ревью (исправлены в
2e497cb)Каждое проверено прогоном
ownlangнапрямую, а не принято на слово. Три из пяти — один корень: в Pythonorиifпроверяют истинность, а не наличие, тогда как модель принимает пустую строку всюду, где принимает значение.resource_kind[resource: ]fileу Evidencenote: … at :4''в сообщении"empty '' group 'x'"захватывает" group "None→ тихий откат на подстроку/отступ\b…\b); на краю строки граница держится только если крайний символ иглы словесный —-foo,foo-,(a)не совпадаютsplitlines()vssplit('\n')\rне остаётся;\r,\x0b,\x0c,\x1c–\x1e,\x85,U+2028,U+2029тоже границы\rпопадал внутрь строки исходника и сдвигал каретку; прочие границы не видны, все последующие строки вне диапазонаШестое замечание — тест упорядочивания, который не мог упасть: он сортировал параллельный вектор копией ключа и сверял только
(line, code), одинаковые у связанных записей по построению. Теперь последовательность меток берётся из публичногоsort_emission_order.Контроли
Рендеринг: базовая строка · суффикс
[resource: …]· неизвестный kind (passthrough, не справочник) · пустой kind (суффикса нет) · warning-тир · evidence сfile=None, с явным другим файлом и с пустым файлом (откат на якорь) · порядок evidence в выводе · якоря DI004 и DI005.Каретка: имя в кавычках · предпочтение границы слова (
'a'попадает в аргументHash(a), а не вaвнутриHash) · откат на подстроку · откат на отступ · пустая строка (колонки нет вовсе) · строка за пределами исходника · два имени в кавычках (первое выигрывает) · непарная кавычка · пустая пара''перед настоящей · имя с несловесными краями ('(a)') · два Unicode-кейса с разной шириной символов · CRLF · одиночный\r· U+2028.Упорядочивание:
(line, code)в правильном приоритете · стабильность ничьих · безразличие к severity и evidence · отсутствие пути в ключе · лексикографическое сравнение кодов (OWN009<OWN010, а не числовой разбор хвоста).Решения по реализации
Regex-крейт не добавлял. Нужны всего два шаблона — поиск первой группы в одинарных кавычках и поиск по границе слова. Оба пишутся напрямую, а новая production-зависимость потребовала бы отдельного ревью против crate DAG.
Фикстура — новый файл, а не расширение
diag_model.json. PR 1 объявил ту схему финальной и пообещал, что последующие срезы добавляют кейсы, а не переделывают запись. Пришить ожидаемые строки к ней означало бы нарушить обещание на первом же продолжении.Не принято: переиспользовать ключ сортировки из
ownlang.__main__вместо повторения в генераторе. Там inline-лямбда, и вынос общего ключа означал бы правку production-кода эталона ради удобства фикстуры. Риск дрейфа признан: если ключ поменяется, это Python-first изменение и генератор правится в том же PR.