Skip to content

OwnIR: carry real Roslyn source columns into own-check SARIF #317

Description

@PhysShell

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.

Activity

  1. PhysShell commented on Jul 30, 2026

    @PhysShell
    OwnerAuthor

    Final acceptance replay

    Исторический acceptance-ledger в этом issue описывал более ранний зафиксированный STS-артефакт:

    • 613 SARIF results
    • 380 scored findings
    • 233 OWN050 advisories

    Тот артефакт не нёс достаточного source/producer provenance для парного replay, поэтому не был переиспользован как baseline.

    Acceptance выполнен как свежий парный replay на одном и том же зафиксированном STS source snapshot:

    • STS commit: 93d78305e61cd3e37975941a4105874b645ae153
    • STS tree: 07be0854c3cca705b2ce2e10f5e50352f12b3cd9
    • baseline producer: 0ded8352d9fc15c1654570af995712b718baa6f1
    • candidate producer: bdb33070c6dac70cdeddbe081c63973177978dab
    • OwnAudit harness: b164e386346c5e15b0a28cd544a653ee9d25e830

    Оба producer worktree и STS tree были чистыми (git status --porcelain пуст) до, между и после обоих прогонов. В обоих прогонах использовались одни и те же baseline/candidate extractor-бинарники — SHA-256 бинарников зафиксирован до и после запуска и совпал, то есть это доказанный факт, а не заявление аргумента командной строки.

    Свежий corpus ledger

    rule resource scored candidate with startColumn
    OWN001 subscription token 402 402
    OWN001 disposable 24 24
    OWN001 disposable field 23 23
    OWN001 timer 4 4
    OWN014 subscription token 9 9
    Total 462 462

    Результаты

    • baseline start-column coverage: 0/462
    • candidate start-column coverage: 462/462
    • occurrence coverage: 462/462
    • ambiguous physical anchors: 0
    • identity limitations: 0
    • allowed transformation startColumn: null -> positive integer: 462
    • added findings: 0
    • removed findings: 0
    • path/rule/message/line/severity/suppression changes: 0
    • invalid, removed, changed, or still-missing columns: 0
    • unexpected duplicate collapses: 0

    Normalized 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) заархивирован:

    • SHA-256: b25420e2b0ecfc9fe7103a6801207ea62b41b6582bcab345817950b5b9ef1c68
    • Хранится приватно (содержит пути и сообщения находок реального STS-дерева, не для публичного репозитория); доступен по запросу.

    Decision: ACCEPTED / CLOSED.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions