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 includes line, 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
acquire facts; - 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-ir round-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.
Child of #266. Producer-enablement for the physical anchor OwnAudit's
finding-occurrence/v1uses.Что делаем
Сегодня Roslyn-путь сворачивает позицию до одной строки:
Program.cs:543—LineOf(node) => node.GetLocation().GetLineSpan().StartLinePosition.Line + 1. Символ.Characterотбрасывается прямо там.Нужен путь:
Координата принадлежит фактам, а не рендереру: 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):Почему 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) фиксирует это прямо:А
sub["line"]не приходит из .own-парсера:_lower_flowкладётn["line"]флоу-опа вacq_line[name](ownir.py:2234) и дальше в flow-local handle.check_factsстроит core Module непосредственно из фактов — без генерации .own-текста и повторного разбора.Реальная цепочка этих записей:
Никаких изменений
Diagnostic,_caret_colили .own parser spans для них не требуется.Разбивка корпуса
sub["line"](subscription fact)sub["line"](field fact)sub["line"](flow-local acquire fact)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()).Гейт падает, если
Обязательная передача column
Как минимум через:
acquirefacts;Finding.column;region.startColumn.Dedupe и сортировка
columnобязан войти в dedupe key и в детерминированный sort key. Комментарий на 2911 прямо говорит, что ключ включаетlineради различения leak-сайтов; безcolumnдве находки на одной строке, различающиеся только колонкой, снова схлопнутся — и тест «несколько findings на одной строке» станет декоративным.Обязательные тесты
startColumnFinding.columnсохранён, SARIF содержитstartColumncolumn= 0 / отрицательное / bool / string ->OwnIRErrorstartColumnOWNIR_VERSIONостаётся0line+column; core diagnostic может иметь другойd.line; итоговыеFinding.line/columnобязаны совпасть именно с acquire-фактом; SARIF содержит ту же пару; ни один parser/Diagnosticфайл не изменён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 .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.