Repository navigation
OwnIR: carry real Roslyn source columns into own-check SARIF #317
Description
Child of #266. Producer-enablement for the physical anchor OwnAudit's finding-occurrence/v1 uses.
Контракт исправлен. Первая редакция этого issue объявляла 23 записи корпуса «parser-span exclusions» и фиксировала ledger
357/380. Это было построено на неверном чтенииcheck_facts: под-веткаOWN025была принята за всю flow-local ветку. Разбор ниже — по фактическому кодуmain@0ded835.
Что делаем
Сегодня Roslyn-путь сворачивает позицию до одной строки:
Roslyn syntax location -> OwnIR fact: line only -> Finding: line only -> SARIF: startLine only
Program.cs:543 — LineOf(node) => node.GetLocation().GetLineSpan().StartLinePosition.Line + 1. Символ .Character отбрасывается прямо там.
Нужен путь:
node.GetLocation() -> OwnIR optional column -> Finding.column: int | None -> SARIF region.startColumn
Координата принадлежит фактам, а не рендереру: OwnIR — шов между frontend и единственным checker'ом. Producer лишь сохраняет наблюдаемую координату; provenance-логика остаётся в OwnAudit.
Хелпер уже есть — FixSpanOf (Program.cs:570) считает start_line/start_column из того же GetLineSpan().StartLinePosition, но применяется только в лейне --fix-candidates (строки 785, 806). Форму переиспользуем, второй параллельный хелпер не заводим.
Контракт якоря
Строка не меняется. Колонка приходит из того же SyntaxNode, который сегодня передаётся в LineOf(node) — не выбираем «более красивый» токен заново. Если у категории нет реального Roslyn-узла, колонка отсутствует. Не подставлять 1.
Приёмка
По записанному STS-корпусу (ownsharp.sarif, 613 результатов; 380 scored после вычета 233 advisory OWN050):
corpus total: 380
producer in-scope: 380
producer passed: 380 / 380
parser-span exclusions in corpus: 0
unexpected failures: 0
Почему in-scope именно 380, включая flow-local OWN001
В check_facts только специальная ветка OWN025 (POOL005, полнодлинный view пулового буфера) создаёт Finding(line=d.line) — она сознательно репортит на view-site, а не на acquire. Обычная flow-local ветка, включая OWN001, создаёт Finding(line=_as_int(sub.get("line", 0))).
Комментарий дедупликации (ownlang/ownir.py:2911) фиксирует это прямо:
For a flow-local every such diagnostic remaps to the same acquire line (
sub["line"]) above, collapsing to byte-identical findings — keep one. The key includesline, so genuinely distinct leak sites stay distinct.
А sub["line"] не приходит из .own-парсера: _lower_flow кладёт n["line"] флоу-опа в acq_line[name] (ownir.py:2234) и дальше в flow-local handle. check_facts строит core Module непосредственно из фактов — без генерации .own-текста и повторного разбора.
Реальная цепочка этих записей:
Roslyn acquire SyntaxNode -> flow op { line, column } -> flow-local handle { line, column }
-> Finding.column -> SARIF region.startColumn
Никаких изменений Diagnostic, _caret_col или .own parser spans для них не требуется.
Разбивка корпуса
| rule | resource | n | якорь |
|---|---|---|---|
| OWN001 | subscription token | 326 | sub["line"] (subscription fact) |
| OWN001 | disposable field | 24 | sub["line"] (field fact) |
| OWN001 | disposable | 23 | sub["line"] (flow-local acquire fact) |
| OWN014 | subscription token | 7 | sub["line"] (subscription fact) |
Отдельная acceptance-группа — flow-local OWN001 acquire anchors (23 записи) — обязана получить настоящие Roslyn-колонки, а не быть исключённой. Это самая содержательная группа среза: именно на ней проверяется, что колонка дошла через flow-op и handle, а не только через прямые facts. В неё входит Core/Mail.cs:32 — известный подтверждённый true positive (SmtpClient с закомментированным //client.Dispose()).
Гейт падает, если
- любое из 380 in-scope ожиданий не совпало;
- acceptance-группа flow-local acquire anchors не получила колонки;
- появилась новая категория расхождения;
- срез тронул parser/span-код.
Обязательная передача column
Как минимум через:
- component resource records;
- flow
acquirefacts; - flow-local handles;
Finding.column;- SARIF
region.startColumn.
Dedupe и сортировка
column обязан войти в dedupe key и в детерминированный sort key. Комментарий на 2911 прямо говорит, что ключ включает line ради различения leak-сайтов; без column две находки на одной строке, различающиеся только колонкой, снова схлопнутся — и тест «несколько findings на одной строке» станет декоративным.
Обязательные тесты
- old fact without column -> принимается, старый SARIF остаётся без
startColumn - fact with column ->
Finding.columnсохранён, SARIF содержитstartColumn column= 0 / отрицательное / bool / string ->OwnIRError- misleading diagnostic message -> не влияет на emitted
startColumn - один и тот же исходник с разным отступом -> колонка следует Roslyn location
OWNIR_VERSIONостаётся0- extractor fixture с несколькими findings на одной строке — колонка различает позиции
- flow-local acquire anchor: acquire-факт с
line+column; core diagnostic может иметь другойd.line; итоговыеFinding.line/columnобязаны совпасть именно с acquire-фактом; SARIF содержит ту же пару; ни один parser/Diagnosticфайл не изменён - Rust
own-irround-trip suite проходит: additive-поля сохраняются во flattened extra по его контракту, менять крейт не обязательно — но «необязательно менять» и «можно не проверять» это разные вещи
Версия и порядок полей
OWNIR_VERSION не повышаем: новое необязательное поле с безопасным default None версию не двигает (spec/OwnIR.md §2). В Finding поле объявляется последним — после ignore_reason — column: int | None = None, чтобы позиционные конструкторы не получили ещё один шанс молча перепутать аргументы.
Затрагиваются: frontend/roslyn/OwnSharp.Extractor/Program.cs, spec/ownir.schema.json, spec/OwnIR.md, ownlang/ownir.py, tests/test_ownir.py, extractor fixtures/tests.
Non-goals
No occurrence/provenance implementation
No OwnAudit changes
No Diagnostic/_caret_col changes
No .own parser-span work
No relatedLocation columns
No human/MSBuild/GitHub rendering change
No locationless recovery
No lineage
No .own parser-span work остаётся, но означает узкое: он ограничивает diagnostic-site колонки — для находок, чей первичный якорь действительно равен сайту диагностики, прежде всего специальной ветки OWN025. Он не исключает acquire-anchored flow-local OWN001, координата которых приходит из Roslyn-факта. В текущих 380 scored rows таких исключений нет.
Diagnostic._caret_col() не трогаем и не переиспользуем: он достаёт subject из текста сообщения, ищет его в строке и откатывается к первому непробельному символу. Это эвристика рендерера. Настоящая колонка для .own-поверхности должна прийти отдельным срезом из parser/token span.
Human, GitHub-annotation и MSBuild output остаются byte-for-byte прежними. Цель конкретна: SARIF producer evidence для occurrence anchor в OwnAudit.
Refs #266, PhysShell/OwnAudit#58.
Activity
feat(ownir): carry an optional source column from fact to SARIF (brid…
docs(ownir): specify the optional column; prove it survives the Rust …
test(ownir): observe the alias handle directly, not through the finding
feat(extractor): emit a real Roslyn source column on every anchor record
style(ownir): fix a continuation indent left by the column plumbing
chore(lint): make the #317 test files ruff-clean
fix(ownir): widen the hoist map's return type; format the Rust round-…
test(goldens): carry the new additive column into the pre-S0 parity g…
docs(goldens): the derived columns are confirmed, not pending
fix(ownir): canonical zero-arg sig, shared column schema, temp-file c…
feat(aggregate): add the producer A/B harness for a physical-anchor c…
fix(aggregate): close three fail-open holes in the A/B verdict
fix(aggregate): bind the normalized payload to the candidate SARIF
PhysShell commentedon Jul 30, 2026
Final acceptance replay
Исторический acceptance-ledger в этом issue описывал более ранний зафиксированный STS-артефакт:
Тот артефакт не нёс достаточного source/producer provenance для парного replay, поэтому не был переиспользован как baseline.
Acceptance выполнен как свежий парный replay на одном и том же зафиксированном STS source snapshot:
93d78305e61cd3e37975941a4105874b645ae15307be0854c3cca705b2ce2e10f5e50352f12b3cd90ded8352d9fc15c1654570af995712b718baa6f1bdb33070c6dac70cdeddbe081c63973177978dabb164e386346c5e15b0a28cd544a653ee9d25e830Оба producer worktree и STS tree были чистыми (
git status --porcelainпуст) до, между и после обоих прогонов. В обоих прогонах использовались одни и те же baseline/candidate extractor-бинарники — SHA-256 бинарников зафиксирован до и после запуска и совпал, то есть это доказанный факт, а не заявление аргумента командной строки.Свежий corpus ledger
Результаты
0/462462/462462/46200startColumn: null -> positive integer:46200000Normalized payload (
normalized-findings/v2) привязан к точным байтам candidate SARIF черезproducer-provenance/v1(input_digest= SHA-256 candidate SARIF, проверен при чтении); все 462 pattern ID и occurrence ID пересчитаны независимо и успешно.Почему исторические 380 не блокер
Старый ledger (380) относился к другому, недоказуемому по provenance снимку STS. Рост корпуса до 462 одинаков на baseline и candidate стороне одного и того же свежего снимка — то есть это свойство снимка, а не producer drift, и differential по всем категориям чист независимо от абсолютного числа. Историческая цифра 380 сохраняется здесь только как документация исходного recorded artifact; итоговой acceptance authority является воспроизводимый pinned STS snapshot и парный replay на 462 записях, описанный выше.
Evidence archive
Полный evidence bundle (raw SARIF, OwnIR facts, producer-provenance/v1 manifest, normalized-findings/v2, diagnostic report, pre/post identity capture, exit codes) заархивирован:
b25420e2b0ecfc9fe7103a6801207ea62b41b6582bcab345817950b5b9ef1c68Decision: ACCEPTED / CLOSED.
fix(diagnostics): pin the fixture's read shape; reject column 0 (#255 …
feat(ownir): make the strict door a section-local validator, not serde
feat(ownir): make the strict door accept the same language as the ref…
feat(ownir): make the strict door a section-local validator, not serde