Skip to content

feat: link library APIs to their documentation at the pinned version - #94

Merged
lbildzinkas merged 3 commits into
masterfrom
fm/second-look-doc-links
Oct 7, 2026
Merged

lbildzinkas merged 3 commits into
masterfrom
fm/second-look-doc-links

Conversation

@lbildzinkas

@lbildzinkas lbildzinkas commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Intent

Closes #49. Library APIs used in a change link to their documentation at the version the project pins. Links come first from published inventories, which the engine downloads: Sphinx inventories for Python libraries and the .NET API reference's cross-reference map. Links the agent suggests come after them and are labelled as suggestions. The overview panel and hovers on the head side of the diff show the links.

The acceptance criteria are tests for parsing both inventory formats and for version matching; agent-suggested links that are labelled and shown only after the inventory links; and a suggestion prompt that lands with its evaluation cases and score. Validation never launches, downloads or installs VS Code or any other tool: extension tests that need VS Code run only in CI, and live validation goes through the engine protocol, the unit tests and rendered output.

What Changed

  • Engine: after the review, a new doc-links stage links each library API a change's added lines use (Python via imports of lock-pinned libraries, C# via usings with the project's target framework and NuGet pins) to its documentation at the pinned version — parsed from Sphinx objects.inv inventories and the streamed .NET API reference .xrefmap.json, only when the inventory documents that exact version, downloaded by a hardened https fetcher (named public hosts only, no redirects, no credentials or ports, download/inflate caps); APIs no inventory links go to the agent, whose suggestions are checked but never fetched. The review result (schema v18) carries docLinks with every inventory link before every labelled suggestion.
  • Extension: hovering a library name on the head side of a part's diff shows the API's documentation links (inventory first, agent suggestions labelled as not checked), and the overview gains a Documentation section listing the links, the APIs left unlinked and per-library notes.
  • Evaluation: the doc-links prompt (version 1) is registered in prompts.json, run and scored over the canary cases (doc-links-on-site, doc-links-checked) with an excerpt of the .NET cross-reference map recorded for canary-csharp, and the baseline was rewritten; README and CONTEXT.md document the feature.

Risk Assessment

✅ Low: Both round-1 findings are fixed exactly as the recorded user decisions prescribed, the fix-round code adds no components beyond those prescriptions, the fixes are deterministic and covered by regression tests that fail without them, and the rest of the change re-traced clean against its acceptance criteria and security boundary.

Testing

Live-drove the product through the engine protocol as the intent's standing boundary prescribes: the real review pipeline ran to completion over JSON-RPC four times, and its docLinks output matched the ticket's behavior exactly — attrs.field linked from the 23.1.0 Sphinx inventory, a stable inventory documenting 26.1 refused with an explanatory note, .NET Stream/CopyTo linked at the net-8.0 moniker, the package type no inventory holds left unlinked even though a multi-target packages.lock.json and the project file pin it three times, the 'At most 60 library APIs are listed, so 2 with a link and 3 without one are left out' note, and the agent's suggestion checked, never fetched, labelled and placed after the inventory link. The panel's Documentation section and both hovers were rendered through the extension's own code from that engine answer. No screenshot was captured because no browser exists on PATH and validation installs no tools; the self-contained panel HTML in the evidence directory is the rendered artifact (the repo's own UX record validates this surface as a browser-opened HTML page). The two fixes from review round 1 were each verified live and by their requested tests. Two scenarios remain untested rather than passed: the in-editor hover (requires the VS Code application, which the standing boundary forbids launching, downloading or installing, so only the rendered hover Markdown and unit/integration tests could be produced) and the doc-links evaluation scoring (requires a live model credential, so only the scripted-agent evaluation harness tests could be run).

  • Live validation: ✅ go - 10 of 12 scenarios driven live against the product
Scenario Result Live Evidence
Hovering a library call in a Python change yields a link to that version's documentation, read from the library's published Sphinx inventory ✅ pass live run-a-python-inventory-and-agent.json and run-a-hovers.md (engine protocol drive A)
An inventory documenting another version is refused: the API stays unlinked and a note names the version the inventory documents (version-matching guard) ✅ pass live run-d-stable-inventory-refused.json (engine protocol drive D)
A C# change's .NET APIs under a using link through the cross-reference map at the project's target framework (net-8.0 moniker), including a member call ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json (engine protocol drive B): System.IO.Stream and System.IO.Stream.CopyTo at ?view=net-8.0
A pinned package's type no inventory holds is left unlinked and attributed to the one package a using names, even when a multi-target packages.lock.json and the project file pin it several times (revi… ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json and packages/engine/test/doc-links.test.ts 'takes the one package a using names however often a multi-target lock file and its project file pin it'
An API the map documents but not at the pinned version gets no link and a note says so, rather than a wrong-version link ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json note for Microsoft.Extensions.Logging.ILogger
Agent-suggested links are checked, never fetched, labelled as the agent's, and appear only after every deterministic inventory link ✅ pass live run-a-python-inventory-and-agent.json (ordering, labels, stage announcement, the suggested URL never requested) plus packages/extension/test/doc-hover.test.ts protocol-ordering rejections
Beyond the cap of 60 APIs, the result says in a plain note how many links and unlinked APIs were left out (review fix doc-links-silent-truncation) ✅ pass live run-c-cap-note.json and packages/engine/test/doc-links.test.ts 'names how many APIs are left out when a change uses more than the cap lists'
The overview panel's Documentation section shows inventory links first as buttons, suggestions flagged and labelled not checked, then unlinked APIs and the notes, with the prompt stamp ✅ pass live run-a-overview-panel.html, rendered by the extension's own overviewHtml from the engine's protocol answer
The engine's version-18 result with docLinks crosses the extension's protocol validator unchanged, and the validator refuses a suggestion before an inventory link ✅ pass live render-panel.mjs check (isReviewResult true on the JSON round trip of run-a-review-result.json) plus doc-hover.test.ts 'refuses a suggestion before an inventory's link...'
Hovering a library name in the editor shows the link for that exact line and name, head side only ⏸️ untested no The prior payload did not establish a live result: it recorded live=false because the in-editor hover surface requires the VS Code application, which the standing boundary forbids launching, downloadi…
Both published inventory formats parse and versions match (real recorded attrs objects.inv and gzipped .NET xrefmap), covered by parsing and matching tests ✅ pass live Engine drives A-D parsed the real recorded inventories through the full pipeline; npx vitest run packages/engine/test/doc-inventory.test.ts (33 tests) covers parsing and version matching
The doc-links suggestion prompt lands with its evaluation cases and score (prompt registry entry, canary case labels, score rows and baseline) ⏸️ untested no The prior payload did not establish a live result: it recorded live=false because a real evaluated model run needs a live agent credential, which cannot be installed or provisioned on this machine; th…
Evidence: Engine protocol drive A: Python change, pinned attrs 23.1.0 and httpx 0.27.2

docLinks.links = [attrs.field from inventory at https://www.attrs.org/en/23.1.0/api.html#attrs.field, httpx.Client from agent at https://www.python-httpx.org/api/]; notes name the inventory read and why httpx has none; stages include 'suggesting documentation links with fake'; the suggested URL is never fetched.

{
  "run": "A — Python change, pinned attrs 23.1.0 and httpx 0.27.2, an agent standing in for the reviewer’s installed one",
  "pullRequest": "101: Read a documentation page",
  "version": 18,
  "stages": [
    "grouping related hunks with fake",
    "writing the story with fake",
    "comparing the change with its description and issues with fake",
    "listing the claims with fake",
    "suggesting documentation links with fake"
  ],
  "docLinks": {
    "links": [
      {
        "api": "attrs.field",
        "library": "attrs",
        "version": "23.1.0",
        "pinnedBy": "requirements.txt",
        "ecosystem": "PyPI",
        "uses": [
          {
            "path": "app/page.py",
            "line": 6,
            "name": "field"
          }
        ],
        "url": "https://www.attrs.org/en/23.1.0/api.html#attrs.field",
        "from": "inventory",
        "inventory": "https://www.attrs.org/en/23.1.0/objects.inv"
      },
      {
        "api": "httpx.Client",
        "library": "httpx",
        "version": "0.27.2",
        "pinnedBy": "requirements.txt",
        "ecosystem": "PyPI",
        "uses": [
          {
            "path": "app/page.py",
            "line": 5,
            "name": "Client"
          }
        ],
        "url": "https://www.python-httpx.org/api/",
        "from": "agent"
      }
    ],
    "unlinked": [],
    "notes": [
      "httpx 0.27.2: PyPI has no release httpx 0.27.2",
      "attrs 23.1.0: read the Sphinx inventory at https://www.attrs.org/en/23.1.0/objects.inv, which documents 23.1",
      "Library APIs are linked to their documentation in Python and C# only; the change's .ts files are not read for them"
    ],
    "suggestions": {
      "promptVersion": "1",
      "stamp": {
        "agent": "fake",
        "agentVersion": "1.0.0",
        "model": "fake/model",
        "effort": null,
        "runAt": "2026-10-07T12:00:00.000Z"
      },
      "outcome": "suggested",
      "detail": "the agent suggested 1 of 1 links from what it knows; each names an API asked about and is an https address on a named host, and none was opened or checked"
    }
  },
  "agentDocLinksRuns": [
    {
      "instructions": "You are the agent of Second Look, a companion that helps a h",
      "prompt": "Suggest the documentation page of each of these 1 library APIs the change uses, at the pinned\nversion. Each line gives the id, the API, its library and version, the file that pins it, and where\nthe change uses it, as untrusted text:\n\n<untrusted-input id=\"aeed65c8394b7724\" source=\"library APIs\">\na1: httpx.Client — httpx 0.27.2 from PyPI, pinned by requirements.txt; used at app/page.py:5\n</untrusted-input id=\"aeed65c8394b7724\">\n\nWhen you have read enough, give your final message as the JSON value alone: start it with { and end it with }, with no summary of what you read before or after it."
    }
  ],
  "urlsServedByTheFixture": [
    "https://api.github.com/repos/example-org/example-repo/pulls/101",
    "https://api.github.com/repos/example-org/example-repo/pulls/101",
    "https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
    "https://api.github.com/repos/example-org/example-repo/compare/b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1...a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1?per_page=1",
    "https://api.github.com/repos/example-org/example-repo/commits/a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1/check-runs?per_page=100",
    "https://api.github.com/graphql",
    "https://api.github.com/user",
    "https://api.github.com/repos/example-org/example-repo/pulls/101/reviews?per_page=100",
    "https://api.github.com/repos/example-org/example-repo/tarball/a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
    "https://api.github.com/repos/example-org/example-repo/tarball/c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
    "https://pypi.org/pypi/httpx/0.27.2/json",
    "https://pypi.org/pypi/attrs/23.1.0/json",
    "https://www.attrs.org/en/23.1.0/objects.inv"
  ]
}
Evidence: Rendered overview panel (Documentation section) from the engine's protocol answer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-evidence'; script-src 'nonce-evidence';">
<style nonce="evidence">
  body {
    color: var(--vscode-foreground);
    background-color: var(--vscode-editor-background);
    font-family: var(--vscode-font-family);
    font-size: var(--vscode-font-size, 13px);
    margin: 0;
    padding: 0 26px;
  }
  main { max-width: 900px; padding: 18px 0 48px; }
  h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px; }
  h2 { font-size: 15px; font-weight: 600; margin: 16px 0 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .meta, .note { color: var(--vscode-descriptionForeground); font-size: 12px; }
  .note { margin: 0 0 6px; }
  .stages { display: flex; gap: 6px; flex-wrap: wrap; margin: 10px 0 4px; }
  .stg { font-size: 12px; border: 1px solid var(--vscode-panel-border); border-radius: 12px; padding: 1px 9px; }
  .stg.done::before { content: "✓ "; color: var(--vscode-testing-iconPassed, #89d185); }
  .stg.run { color: var(--vscode-descriptionForeground); }
  .stamp { font-size: 11px; font-weight: 400; color: var(--vscode-descriptionForeground); }
  .story {
    border: 1px solid var(--vscode-panel-border);
    border-left: 3px solid var(--vscode-textLink-foreground);
    border-radius: 3px;
    padding: 10px 12px;
    font-size: 13.5px;
    line-height: 1.55;
  }
  .sentence.focus, .answer.focus { background-color: var(--vscode-editor-findMatchHighlightBackground); }
  .pt {
    font: inherit;
    color: var(--vscode-textLink-foreground);
    background: none;
    border: none;
    border-bottom: 1px dotted var(--vscode-textLink-foreground);
    padding: 0;
    cursor: pointer;
  }
  .pt.focus { font-weight: 600; }
  .issue { font-style: italic; }
  code, .shown { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .description {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 10px 12px;
  }
  .alert {
    border-left: 3px solid var(--vscode-editorWarning-foreground);
    padding: 4px 10px;
    margin: 0 0 8px;
  }
  .hidden {
    border: 1px dashed var(--vscode-editorWarning-foreground);
    border-radius: 3px;
    padding: 0 4px;
  }
  .flag {
    color: var(--vscode-editorWarning-foreground);
    font-size: 11px;
    font-weight: 600;
    margin-right: 6px;
  }
  .claims { padding-left: 22px; margin: 0; }
  .claims li { margin-bottom: 8px; }
  .quote { overflow-wrap: anywhere; }
  .where { color: var(--vscode-descriptionForeground); font-size: 12px; margin-top: 2px; }
  .verdict { font-style: italic; }
  .verdict.finding { color: var(--vscode-editorWarning-foreground); font-weight: 600; }
  .why { color: var(--vscode-descriptionForeground); font-size: 12px; overflow-wrap: anywhere; }
  .evidence { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 2px 10px; font-size: 12px; margin-top: 2px; }
  .evidence .label, .cited { color: var(--vscode-descriptionForeground); }
  .evidence span { min-width: 0; overflow-wrap: anywhere; }
  .cite { font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
  .none { color: var(--vscode-editorWarning-foreground); }
  .findings, .checks { padding-left: 18px; margin: 0; }
  .findings li, .checks li { margin-bottom: 6px; overflow-wrap: anywhere; }
  .att, .sev, .check { font-size: 11px; font-weight: 600; border: 1px solid var(--vscode-panel-border); border-radius: 10px; padding: 0 7px; }
  .att.stale, .att.malformed, .sev.error, .sev.warning, .check.failed { color: var(--vscode-editorWarning-foreground); }
  .att.fresh, .check.passed { color: var(--vscode-testing-iconPassed, #89d185); }
  .log {
    font-family: var(--vscode-editor-font-family, monospace);
    font-size: 12px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px solid var(--vscode-panel-border);
    border-radius: 3px;
    padding: 6px 10px;
    margin: 4px 0;
    max-height: 320px;
    overflow-y: auto;
  }
  .stamps { padding-left: 18px; margin: 0; }
  .stamps li { margin-bottom: 4px; }
</style>
</head>
<body>
<main>
  <h1>Read a documentation page</h1>
  <div class="meta">example-org/example-repo #101 · author-login · feature → master · head a1a1a1a</div>
  
  <div class="stages"><span class="stg done">parts</span><span class="stg done">noise checks</span><span class="stg done">plain grouping kept</span><span class="stg done">plain ranking kept</span><span class="stg done">no story</span><span class="stg done">no comparison</span><span class="stg done">no claims</span><span class="stg done">documentation links</span></div>
  
  <section id="story"><h2>Story <span class="stamp">fake · fake/model · story prompt v1</span></h2><p class="note">No story: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;sentences&quot;; the answer has an unexpected &quot;not&quot;).</p></section>
  <section id="criteria"><h2>Acceptance criteria</h2><p class="note">this pull request links no issue.</p><p class="note">Each condition listed in the issues this pull request links, quoted from the checklist under &quot;Acceptance criteria&quot;. Issue text is untrusted: its hidden content is shown and flagged. None is checked yet.</p></section>
  <section id="unexplained"><h2>Unexplained changes <span class="stamp">fake · fake/model · unexplained prompt v1</span></h2><p class="note">No comparison: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;unexplained&quot;; the answer is missing &quot;described&quot;; the answer has an unexpected &quot;not&quot;).</p></section>
  <section id="claims"><h2>Claims <span class="stamp">fake · fake/model · claims prompt v1</span></h2><p class="note">No claims: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;claims&quot;; the answer has an unexpected &quot;not&quot;).</p></section>
  <section id="docs"><h2>Documentation <span class="stamp">fake · fake/model · doc-links prompt v1</span></h2><ol class="claims"><li><code>attrs.field</code> <button type="button" class="pt doc" data-doc="0">https://www.attrs.org/en/23.1.0/api.html#attrs.field</button><div class="where">From the published inventory of attrs 23.1.0, as requirements.txt pins it · used at app/page.py:6</div></li></ol><p class="note">Suggested by the agent for the APIs no inventory linked. Nothing checked these pages: open them knowing that.</p><ol class="claims"><li><span class="flag">suggested</span><code>httpx.Client</code> <button type="button" class="pt doc" data-doc="1">https://www.python-httpx.org/api/</button><div class="where">Suggested by the agent from what it knows, for httpx 0.27.2 as requirements.txt pins it; not checked · used at app/page.py:5</div></li></ol><p class="note">httpx 0.27.2: PyPI has no release httpx 0.27.2.</p><p class="note">attrs 23.1.0: read the Sphinx inventory at https://www.attrs.org/en/23.1.0/objects.inv, which documents 23.1.</p><p class="note">Library APIs are linked to their documentation in Python and C# only; the change&#39;s .ts files are not read for them.</p></section>
  <section id="pipeline"><h2>Pipeline and CI</h2><p><span class="att missing">no-mistakes report: none</span> <span class="note">the description carries no no-mistakes attestation.</span></p><p class="note">Checks listed at head a1a1a1a, ran on the merge commit: 0 check runs at the head commit, 0 failed; logs are read only for failed jobs.</p></section>
  <section id="description"><h2>Pull request description</h2><div class="description">Adds app/page.py that reads a documentation page and totals the cart.</div></section>
  <section id="stamps"><h2>How these results were made</h2><ul class="stamps"><li><b>Parts</b> plain grouping kept: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;parts&quot;; the answer has an unexpected &quot;not&quot;)</li><li><b>Ranking</b> plain ranking kept: the agent ranking is the default only where its evaluation matched or beat the plain ranking, and fake at its default effort has none</li><li><b>Story</b> none: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;sentences&quot;; the answer has an unexpected &quot;not&quot;)</li><li><b>Unexplained changes</b> none: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;unexplained&quot;; the answer is missing &quot;described&quot;; the answer has an unexpected &quot;not&quot;)</li><li><b>Claims</b> none: the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing &quot;claims&quot;; the answer has an unexpected &quot;not&quot;)</li><li><b>Documentation links</b> 1 read from published inventories at the pinned version; suggested by fake · fake/model · doc-links prompt v1: the agent suggested 1 of 1 links from what it knows; each names an API asked about and is an https address on a named host, and none was opened or checked</li></ul></section>
</main>
<script nonce="evidence">
(function () {
  'use strict';
  var vscode = acquireVsCodeApi();
  Array.prototype.forEach.call(document.querySelectorAll('button.pt[data-part]'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openPart', part: Number(button.getAttribute('data-part')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.issue'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openIssue', issue: Number(button.getAttribute('data-issue')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.doc'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openDoc', doc: Number(button.getAttribute('data-doc')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.cite'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({
        type: 'openEvidence',
        criterion: Number(button.getAttribute('data-criterion')),
        evidence: button.getAttribute('data-evidence'),
        index: Number(button.getAttribute('data-index'))
      });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.asked'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'openCited', answer: Number(button.getAttribute('data-answer')), index: Number(button.getAttribute('data-index')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.draft'), function (button) {
    button.addEventListener('click', function () {
      vscode.postMessage({ type: 'draft', finding: button.getAttribute('data-draft'), index: Number(button.getAttribute('data-index')) });
    });
  });
  Array.prototype.forEach.call(document.querySelectorAll('button.manual'), function (button) {
    button.addEventListener('click', function () {
      var description = document.getElementById('description');
      if (description !== null) {
        description.scrollIntoView({ block: 'start' });
      }
    });
  });
  var focused = document.querySelector('.answer.focus') || document.querySelector('.sentence.focus');
  if (focused !== null) {
    focused.scrollIntoView({ block: 'center' });
  }
}());
</script>
</body>
</html>
Evidence: Hover Markdown the editor shows on the two library names (inventory vs agent-suggested)

[attrs\.field](https://www.attrs.org/en/23.1.0/api.html#attrs.field): documentation of attrs 23\.1\.0, from its published inventory — **Suggested by the agent, not checked:** [httpx\.Client](https://www.python-httpx.org/api/), for httpx 0\.27\.2

# Hovers the reviewer sees on the head side of the diff

Hovering `field` on app/page.py:6 (linked from attrs’ published inventory for the pinned 23.1.0):

`` `markdown
[attrs\.field](https://www.attrs.org/en/23.1.0/api.html#attrs.field): documentation of attrs 23\.1\.0, from its published inventory
`` `

Hovering `Client` on app/page.py:5 (suggested by the agent, labelled and not checked):

`` `markdown
**Suggested by the agent, not checked:** [httpx\.Client](https://www.python-httpx.org/api/), for httpx 0\.27\.2
`` `
Evidence: Engine protocol drive B: C# change under net8.0/net9.0 with the package pinned by a multi-target lock file and the project file

links: System.IO.Stream and System.IO.Stream.CopyTo at ?view=net-8.0 from the cross-reference map; unlinked: Microsoft.IO.RecyclableMemoryStreamManager pinned by src/packages.lock.json despite the three duplicate pins; note: the map documents Microsoft.Extensions.Logging.ILogger, but not at the version pinned.

{
  "run": "B — C# change under net8.0/net9.0, the package pinned by a multi-target lock file and the project file",
  "docLinks": {
    "links": [
      {
        "api": "System.IO.Stream",
        "library": ".NET",
        "version": "net8.0",
        "pinnedBy": "src/BlobTool.csproj",
        "ecosystem": ".NET",
        "uses": [
          {
            "path": "src/BlobReader.cs",
            "line": 9,
            "name": "Stream"
          }
        ],
        "url": "https://learn.microsoft.com/dotnet/api/system.io.stream?view=net-8.0",
        "from": "inventory",
        "inventory": "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json"
      },
      {
        "api": "System.IO.Stream.CopyTo",
        "library": ".NET",
        "version": "net8.0",
        "pinnedBy": "src/BlobTool.csproj",
        "ecosystem": ".NET",
        "uses": [
          {
            "path": "src/BlobReader.cs",
            "line": 12,
            "name": "CopyTo"
          }
        ],
        "url": "https://learn.microsoft.com/dotnet/api/system.io.stream.copyto?view=net-8.0",
        "from": "inventory",
        "inventory": "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json"
      }
    ],
    "unlinked": [
      {
        "api": "Microsoft.IO.RecyclableMemoryStreamManager",
        "library": "Microsoft.IO.RecyclableMemoryStream",
        "version": "3.0.1",
        "pinnedBy": "src/packages.lock.json",
        "ecosystem": "NuGet",
        "uses": [
          {
            "path": "src/BlobReader.cs",
            "line": 11,
            "name": "RecyclableMemoryStreamManager"
          }
        ]
      }
    ],
    "notes": [
      ".NET: read the API reference's cross-reference map, https://learn.microsoft.com/en-us/dotnet/.xrefmap.json",
      ".NET: the API reference documents Microsoft.Extensions.Logging.ILogger, but not at the version pinned, so no link is given"
    ]
  },
  "urlsServedByTheFixture": [
    "https://api.github.com/repos/example-org/example-repo/pulls/102",
    "https://api.github.com/repos/example-org/example-repo/pulls/102",
    "https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2",
    "https://api.github.com/repos/example-org/example-repo/compare/b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2...a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2?per_page=1",
    "https://api.github.com/repos/example-org/example-repo/commits/a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2/check-runs?per_page=100",
    "https://api.github.com/graphql",
    "https://api.github.com/user",
    "https://api.github.com/repos/example-org/example-repo/pulls/102/reviews?per_page=100",
    "https://api.github.com/repos/example-org/example-repo/tarball/a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2",
    "https://api.github.com/repos/example-org/example-repo/tarball/c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2",
    "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json"
  ]
}
Evidence: Engine protocol drive C: 65 library APIs used against the 60-API cap

Source: Engine protocol drive C: 65 library APIs used against the 60-API cap (local file: ~/.no-mistakes/evidence/01M4BESXE1D0DCDHJQXKTT6P8E/run-c-cap-note.json)

linksShown: 60; note: 'At most 60 library APIs are listed, so 2 with a link and 3 without one are left out'
Evidence: Engine protocol drive D (adversarial): pinned 22.2.0, only the stable inventory (26.1) exists

links: 0; unlinked: attrs.field; note: 'attrs 22.2.0: no Sphinx inventory under https://www.attrs.org/ documents 22.2.0; https://www.attrs.org/en/stable/objects.inv documents 26.1'

{
  "run": "D — attrs pinned at 22.2.0, only the stable inventory (26.1) exists",
  "docLinks": {
    "links": [],
    "unlinked": [
      {
        "api": "attrs.field",
        "library": "attrs",
        "version": "22.2.0",
        "pinnedBy": "requirements.txt",
        "ecosystem": "PyPI",
        "uses": [
          {
            "path": "app/page.py",
            "line": 6,
            "name": "field"
          }
        ]
      }
    ],
    "notes": [
      "attrs 22.2.0: no Sphinx inventory under https://www.attrs.org/ documents 22.2.0; https://www.attrs.org/en/stable/objects.inv documents 26.1"
    ]
  },
  "urlsServedByTheFixture": [
    "https://api.github.com/repos/example-org/example-repo/pulls/104",
    "https://api.github.com/repos/example-org/example-repo/pulls/104",
    "https://api.github.com/repos/example-org/example-repo/contents/.gitattributes?ref=a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4",
    "https://api.github.com/repos/example-org/example-repo/compare/b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4...a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4?per_page=1",
    "https://api.github.com/repos/example-org/example-repo/commits/a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4/check-runs?per_page=100",
    "https://api.github.com/graphql",
    "https://api.github.com/user",
    "https://api.github.com/repos/example-org/example-repo/pulls/104/reviews?per_page=100",
    "https://api.github.com/repos/example-org/example-repo/tarball/a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4",
    "https://api.github.com/repos/example-org/example-repo/tarball/c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4",
    "https://pypi.org/pypi/attrs/22.2.0/json",
    "https://www.attrs.org/en/22.2.0/objects.inv",
    "https://www.attrs.org/en/v22.2.0/objects.inv",
    "https://www.attrs.org/en/22.2.x/objects.inv",
    "https://www.attrs.org/en/stable/objects.inv",
    "https://www.attrs.org/en/latest/objects.inv",
    "https://www.attrs.org/objects.inv"
  ]
}
Evidence: Full review result as the engine answered it over the protocol (version 18, docLinks present)
{
  "version": 18,
  "pullRequest": {
    "url": "https://github.com/example-org/example-repo/pull/101",
    "number": 101,
    "title": "Read a documentation page",
    "author": "author-login",
    "description": "Adds app/page.py that reads a documentation page and totals the cart.",
    "base": "master",
    "head": "feature",
    "baseCommit": "b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1",
    "headSha": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
  },
  "copies": {
    "base": {
      "commit": "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
      "path": "/var/folders/ng/f_l3tkbs2vd2dsh3lfd97mhh0000gn/T/second-look-drive-a-N7toH7/github.com/example-org/example-repo/pull-101/c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1",
      "reused": false
    },
    "head": {
      "commit": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
      "path": "/var/folders/ng/f_l3tkbs2vd2dsh3lfd97mhh0000gn/T/second-look-drive-a-N7toH7/github.com/example-org/example-repo/pull-101/a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
      "reused": false
    }
  },
  "parseTimeMs": 2.888,
  "parts": [
    {
      "path": "app/page.py",
      "changeKind": "addition",
      "isBinary": false,
      "oldMissingFinalNewline": false,
      "newMissingFinalNewline": false,
      "hunks": [
        {
          "oldStart": 0,
          "oldLines": 0,
          "newStart": 1,
          "newLines": 8,
          "lines": [
            {
              "kind": "addition",
              "newLineNumber": 1,
              "text": "import attrs"
            },
            {
              "kind": "addition",
              "newLineNumber": 2,
              "text": "import httpx"
            },
            {
              "kind": "addition",
              "newLineNumber": 3,
              "text": ""
            },
            {
              "kind": "addition",
              "newLineNumber": 4,
              "text": ""
            },
            {
              "kind": "addition",
              "newLineNumber": 5,
              "text": "def page(client: httpx.Client) -> str:"
            },
            {
              "kind": "addition",
              "newLineNumber": 6,
              "text": "    probe = attrs.field(default=None)"
            },
            {
              "kind": "addition",
              "newLineNumber": 7,
              "text": "    return client.get(\"/\").text"
            },
            {
              "kind": "addition",
              "newLineNumber": 8,
              "text": ""
            }
          ],
          "entities": [
            {
              "kind": "function",
              "name": "page",
              "public": true,
              "change": "added"
            }
          ]
        }
      ],
      "additions": 8,
      "deletions": 0,
      "syntax": {
        "language": "python",
        "formattingOnly": {
          "status": "structure-changed",
          "reason": "the file is new"
        },
        "checksNotRun": []
      },
      "newMode": "100644",
      "noise": {
        "label": "none",
        "note": "no rule applied"
      },
      "name": "page in app/page.py",
      "origin": "plain",
      "signals": {
        "novelty": "new",
        "role": "code",
        "changedLines": 8,
        "publicSurface": [
          "page"
        ],
        "references": {
          "basis": "name-based",
          "names": [
            "page"
          ],
          "files": 0
        }
      },
      "rank": {
        "importance": "must review",
        "reason": "changes the public surface: page; code; new code; 8 changed lines",
        "signals": [
          "changes the public surface: page",
          "code",
          "new code",
          "8 changed lines"
        ]
      }
    },
    {
      "path": "web/cart.ts",
      "changeKind": "addition",
      "isBinary": false,
      "oldMissingFinalNewline": false,
      "newMissingFinalNewline": false,
      "hunks": [
        {
          "oldStart": 0,
          "oldLines": 0,
          "newStart": 1,
          "newLines": 1,
          "lines": [
            {
              "kind": "addition",
              "newLineNumber": 1,
              "text": "export const total = 1;"
            }
          ],
          "entities": []
        }
      ],
      "additions": 1,
      "deletions": 0,
      "syntax": {
        "language": "typescript",
        "formattingOnly": {
          "status": "structure-changed",
          "reason": "the file is new"
        },
        "checksNotRun": []
      },
      "newMode": "100644",
      "noise": {
        "label": "none",
        "note": "no rule applied"
      },
      "name": "top-level code in web/cart.ts",
      "origin": "plain",
      "signals": {
        "novelty": "new",
        "role": "code",
        "changedLines": 1,
        "publicSurface": [],
        "references": {
          "basis": "name-based",
          "names": [],
          "files": 0
        }
      },
      "rank": {
        "importance": "worth reviewing",
        "reason": "code; new code; 1 changed line",
        "signals": [
          "code",
          "new code",
          "1 changed line"
        ]
      }
    }
  ],
  "grouping": {
    "by": "plain",
    "agent": {
      "promptVersion": "2",
      "stamp": {
        "agent": "fake",
        "agentVersion": "1.0.0",
        "model": "fake/model",
        "effort": null,
        "runAt": "2026-10-07T12:00:00.000Z"
      },
      "outcome": "fell back",
      "detail": "the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing \"parts\"; the answer has an unexpected \"not\")",
      "leftOut": 0
    }
  },
  "ranking": {
    "by": "plain",
    "agent": {
      "promptVersion": "1",
      "outcome": "not tested",
      "detail": "the agent ranking is the default only where its evaluation matched or beat the plain ranking, and fake at its default effort has none"
    }
  },
  "criteria": {
    "outcome": "read",
    "detail": "this pull request links no issue",
    "heading": "Acceptance criteria",
    "issues": [],
    "criteria": []
  },
  "pipeline": {
    "attestation": "missing",
    "detail": "the description carries no no-mistakes attestation",
    "steps": [],
    "findings": []
  },
  "ci": {
    "headSha": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
    "outcome": "read",
    "detail": "0 check runs at the head commit, 0 failed; logs are read only for failed jobs",
    "checks": []
  },
  "story": {
    "promptVersion": "1",
    "stamp": {
      "agent": "fake",
      "agentVersion": "1.0.0",
      "model": "fake/model",
      "effort": null,
      "runAt": "2026-10-07T12:00:00.000Z"
    },
    "outcome": "fell back",
    "detail": "the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing \"sentences\"; the answer has an unexpected \"not\")",
    "sentences": []
  },
  "unexplained": {
    "promptVersion": "1",
    "parts": [],
    "described": [],
    "outcome": "fell back",
    "detail": "the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing \"unexplained\"; the answer is missing \"described\"; the answer has an unexpected \"not\")",
    "stamp": {
      "agent": "fake",
      "agentVersion": "1.0.0",
      "model": "fake/model",
      "effort": null,
      "runAt": "2026-10-07T12:00:00.000Z"
    }
  },
  "claims": {
    "promptVersion": "1",
    "stamp": {
      "agent": "fake",
      "agentVersion": "1.0.0",
      "model": "fake/model",
      "effort": null,
      "runAt": "2026-10-07T12:00:00.000Z"
    },
    "outcome": "fell back",
    "detail": "the agent gave no usable answer (invalid-answer: the answer was invalid twice: the answer is missing \"claims\"; the answer has an unexpected \"not\")",
    "claims": []
  },
  "docLinks": {
    "links": [
      {
        "api": "attrs.field",
        "library": "attrs",
        "version": "23.1.0",
        "pinnedBy": "requirements.txt",
        "ecosystem": "PyPI",
        "uses": [
          {
            "path": "app/page.py",
            "line": 6,
            "name": "field"
          }
        ],
        "url": "https://www.attrs.org/en/23.1.0/api.html#attrs.field",
        "from": "inventory",
        "inventory": "https://www.attrs.org/en/23.1.0/objects.inv"
      },
      {
        "api": "httpx.Client",
        "library": "httpx",
        "version": "0.27.2",
        "pinnedBy": "requirements.txt",
        "ecosystem": "PyPI",
        "uses": [
          {
            "path": "app/page.py",
            "line": 5,
            "name": "Client"
          }
        ],
        "url": "https://www.python-httpx.org/api/",
        "from": "agent"
      }
    ],
    "unlinked": [],
    "notes": [
      "httpx 0.27.2: PyPI has no release httpx 0.27.2",
      "attrs 23.1.0: read the Sphinx inventory at https://www.attrs.org/en/23.1.0/objects.inv, which documents 23.1",
      "Library APIs are linked to their documentation in Python and C# only; the change's .ts files are not read for them"
    ],
    "suggestions": {
      "promptVersion": "1",
      "stamp": {
        "agent": "fake",
        "agentVersion": "1.0.0",
        "model": "fake/model",
        "effort": null,
        "runAt": "2026-10-07T12:00:00.000Z"
      },
      "outcome": "suggested",
      "detail": "the agent suggested 1 of 1 links from what it knows; each names an API asked about and is an https address on a named host, and none was opened or checked"
    }
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ packages/engine/src/doc-links.ts:348 - The fallback that attributes an unmapped C# type to 'the one pinned package a using names' requires exactly one (using, pin) holder, but nugetPins returns the same package more than once in common setups: lockPins (packages/engine/src/nuget-fetch.ts:62-77) emits one pin per target framework in a multi-target packages.lock.json, and a lock file plus the project file that generated it (standard NuGet lock-file mode) each add a pin for the same package at the same version. Concrete sequence: head copy has packages.lock.json listing Microsoft.IO.RecyclableMemoryStream 3.0.1 and src/BlobTool.csproj with &lt;PackageReference Include=&#34;Microsoft.IO.RecyclableMemoryStream&#34; Version=&#34;3.0.1&#34; /&gt;; an added line under using Microsoft.IO; writes new RecyclableMemoryStreamManager(); the type is not in the .NET cross-reference map, so the holders branch runs, pins.filter matches both pins for the same using, holders.length === 2, and the type is skipped — no link, no unlinked entry, no note, and the agent is never asked, so the API vanishes from the Documentation section and the hover even though the canary case (project-only pin) shows it works there. Remedy: dedupe the matched holders by package id + version before the uniqueness check (keep skipping when two genuinely different packages or versions match); the sibling consumer of the same pins, pins.find inside link() at packages/engine/src/doc-links.ts:322, already tolerates duplicates by taking the first, so only this site needs the fix.
  • ℹ️ packages/engine/src/doc-links.ts:414 - Beyond MAX_APIS (60), inventory links and unlinked APIs are silently dropped (links.slice(0, MAX_APIS), unlinked.slice(0, ...) at lines 414-415) with no note, while the per-library notes still say the Sphinx inventory was read; a reviewer hovering a dropped API on the head side gets nothing and no explanation. The cap itself is a reasonable bound; this only records the tradeoff that truncation is invisible in the result.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 10 of 12 scenarios driven live against the product
Scenario Result Live Evidence
Hovering a library call in a Python change yields a link to that version's documentation, read from the library's published Sphinx inventory ✅ pass live run-a-python-inventory-and-agent.json and run-a-hovers.md (engine protocol drive A)
An inventory documenting another version is refused: the API stays unlinked and a note names the version the inventory documents (version-matching guard) ✅ pass live run-d-stable-inventory-refused.json (engine protocol drive D)
A C# change's .NET APIs under a using link through the cross-reference map at the project's target framework (net-8.0 moniker), including a member call ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json (engine protocol drive B): System.IO.Stream and System.IO.Stream.CopyTo at ?view=net-8.0
A pinned package's type no inventory holds is left unlinked and attributed to the one package a using names, even when a multi-target packages.lock.json and the project file pin it several times (revi… ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json and packages/engine/test/doc-links.test.ts 'takes the one package a using names however often a multi-target lock file and its project file pin it'
An API the map documents but not at the pinned version gets no link and a note says so, rather than a wrong-version link ✅ pass live run-b-csharp-xrefmap-and-deduped-pins.json note for Microsoft.Extensions.Logging.ILogger
Agent-suggested links are checked, never fetched, labelled as the agent's, and appear only after every deterministic inventory link ✅ pass live run-a-python-inventory-and-agent.json (ordering, labels, stage announcement, the suggested URL never requested) plus packages/extension/test/doc-hover.test.ts protocol-ordering rejections
Beyond the cap of 60 APIs, the result says in a plain note how many links and unlinked APIs were left out (review fix doc-links-silent-truncation) ✅ pass live run-c-cap-note.json and packages/engine/test/doc-links.test.ts 'names how many APIs are left out when a change uses more than the cap lists'
The overview panel's Documentation section shows inventory links first as buttons, suggestions flagged and labelled not checked, then unlinked APIs and the notes, with the prompt stamp ✅ pass live run-a-overview-panel.html, rendered by the extension's own overviewHtml from the engine's protocol answer
The engine's version-18 result with docLinks crosses the extension's protocol validator unchanged, and the validator refuses a suggestion before an inventory link ✅ pass live render-panel.mjs check (isReviewResult true on the JSON round trip of run-a-review-result.json) plus doc-hover.test.ts 'refuses a suggestion before an inventory's link...'
Hovering a library name in the editor shows the link for that exact line and name, head side only ⏸️ untested no The prior payload did not establish a live result: it recorded live=false because the in-editor hover surface requires the VS Code application, which the standing boundary forbids launching, downloadi…
Both published inventory formats parse and versions match (real recorded attrs objects.inv and gzipped .NET xrefmap), covered by parsing and matching tests ✅ pass live Engine drives A-D parsed the real recorded inventories through the full pipeline; npx vitest run packages/engine/test/doc-inventory.test.ts (33 tests) covers parsing and version matching
The doc-links suggestion prompt lands with its evaluation cases and score (prompt registry entry, canary case labels, score rows and baseline) ⏸️ untested no The prior payload did not establish a live result: it recorded live=false because a real evaluated model run needs a live agent credential, which cannot be installed or provisioned on this machine; th…
  • npx vitest run packages/engine/test/doc-inventory.test.ts packages/engine/test/doc-fetch.test.ts packages/engine/test/doc-links.test.ts — 88 tests: Sphinx objects.inv and .NET xrefmap parsing, version matching, fetch guards, findDocLinks for both languages, the multi-target lock-plus-project pin-holder test and the MAX_APIS left-out note test
  • npx vitest run packages/engine/test/review.test.ts packages/engine/test/server.test.ts packages/engine/test/cli.test.ts — 74 tests: the doc-links stage through reviewPullRequest and the RPC server, protocol version 18
  • npx vitest run packages/extension/test/doc-hover.test.ts packages/evaluation/test/run.test.ts packages/evaluation/test/score.test.ts — 83 tests: hover links and labels, panel section, protocol acceptance and ordering rejection, and the doc-links evaluation wiring with its cases and scores
  • npm run build && node tmp-validation/drive-rpc.mjs — four live JSON-RPC drives of the built engine's runRpcServer (the serve command's loop) against disposable fixture pull requests with recorded inventories served through the engine's own fetch seam and a scripted agent at its adapter seam: run A Python inventory+agent, run B C# xrefmap+deduped pins, run C cap note, run D wrong-version inventory refused
  • node --import ./tmp-validation/register.mjs tmp-validation/render-panel.mjs — the extension's isReviewResult accepted the engine's protocol answer (JSON round trip), overviewHtml rendered the Documentation section from it, doc-hover rendered both hovers
  • grep for doc-links-on-site/doc-links-checked in packages/evaluation/baseline.json — the prompt's score rows and prompt version 1 are recorded in the baseline
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

After the review, the library APIs the change's added lines use link to
their documentation at the version the project pins. A Python name
counts when the file imports it from a library a lock file pins, the
module named like the library; a C# name when it sits under one of the
file's usings, .NET's own APIs at the target framework the nearest
project file names, and a package's at the version a lock or project
file pins. Other languages get a note saying their files are not read
for links.

The links come first from published inventories, which the engine
downloads: the Sphinx inventory under the documentation site PyPI names
for the pinned release, tried in Read the Docs' folder for that version
first and read only when the version its header names documents the
pinned one, and the .NET API reference's cross-reference map, scanned as
it streams in, each entry linked with the view of the pinned version only
when the map lists it there. Inventories are untrusted data, parsed and
never run: every download is https on a named host with no credentials
or port, connects only to public addresses, follows no redirect, and is
capped downloaded and inflated. A library whose inventory cannot be read
is a note, never a failure.

Then the agent suggests links for the APIs no inventory linked, from what
it knows; the engine checks each names an API it asked about, once, at an
https address on a named public host, and never fetches it. The result,
now version 18, carries docLinks: every inventory link, then every
suggestion labelled as the agent's, the APIs left without a link and a
note per library. The overview lists them in that order, suggestions
flagged as not checked, and a hover on the name on the head side of the
diff shows the same.

The doc-links prompt lands with its cases and score: one labelled API
each on canary-python and canary-csharp, scored by doc-links-on-site and
doc-links-checked. The baseline records Pi 0.86.1 with
zai-coding-cn/glm-5.3: both scores 1. The plain rows are rewritten from a
full model-free run; every other agent row stays.

Closes #49
@lbildzinkas
lbildzinkas merged commit 5d47bdd into master Oct 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Version-matched documentation links

1 participant