Skip to content

feat(extension): show each claim's reason and evidence on its step 5 row, and link where it is made (#172) - #179

Merged
lbildzinkas merged 2 commits into
masterfrom
fm/sl-claim-row-evidence
Oct 12, 2026
Merged

lbildzinkas merged 2 commits into
masterfrom
fm/sl-claim-row-evidence

Conversation

@lbildzinkas

@lbildzinkas lbildzinkas commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Intent

The ticket is #172 ("Show each claim's reason and evidence on its step 5 row, and link where it is made"), one ticket of the approved v1 order of lbildzinkas/second-look, a public MIT VS Code companion for human pull request review (default branch master). Read CONTEXT.md (glossary), docs/adr/ (decisions) and docs/ux/ (the chosen reviewing surface) before starting; the engine and the extension are TypeScript, the engine a separate local process. The ticket, quoted in full:

  • Milestone: v1.1 · size ~250 changed lines (excluding tests) · delivery: code, through no-mistakes

What to build

A claim's row in side bar step 5 (side-bar/page.ts) shows only its verdict, where the claim is made as plain text, and its quote. The result already holds the verdict's reason, its evidence source, its cited head-copy lines, its cited CI log lines and its recheck, but they show only in the overview's collapsed detail row or in a finding's thread. An unverifiable claim therefore gives the reviewer no reason and nothing to check it against.

  • Each step 5 claim row shows its reason, its evidence source, its cited head-copy lines as chips that open the head copy at the line (as the overview's claim evidence chips do), its cited CI log lines linking to the check run on GitHub (the check's url is already in the result), and its recheck when the engine dropped a citation.
  • Where a claim is made becomes a link, in step 5 and in the overview's claims table (claimWhere returns plain text today): a docstring or comment claim opens its line in the head copy; "description, line N" opens the overview with the description scrolled to that line and the line marked, since a pull request description has no line anchors on GitHub; a story claim opens the story.
  • Agent-derived text stays escaped as it is everywhere else. A link carries only a validated finding reference; the only outside link is a check run's GitHub page, opened through the extension and only for https://github.com/ addresses.

How to try it by hand: review lbildzinkas/interview-coach#98 and read, on each unverifiable claim in step 5, why it was not verified; press a description claim's place and land on its marked line in the overview.

Acceptance criteria

  • An unverifiable claim's step 5 row shows its reason without opening anything else.
  • Cited head-copy lines open the head copy at the line; cited CI log lines open the check run on GitHub.
  • "description, line N" opens the overview's description scrolled to that line, marked.
  • Agent-derived text in the new places is escaped (test).
  • Extension tests that need VS Code run in CI only.

Blocked by

  • None

Standing boundary for validating this ticket on this machine: the VS Code application is never launched, downloaded or installed here, and validation installs no tools; extension tests that need VS Code run only in CI, so live validation goes through the engine protocol, the unit tests and rendered output.

What Changed

  • Step 5's claim rows now show the verdict's reason and evidence source, with each cited head-copy line as a chip that opens the line read-only, each cited CI log line as a chip that opens its check run on GitHub (only https://github.com/ addresses, opened through the extension), the recheck note when the engine dropped a citation, and plain chips for fetched-library citations.
  • Where a claim is made is now a link in both step 5 and the overview's claims table: a file claim opens its line in the head copy, a description claim opens the overview scrolled to its marked line (a pull request description has no line anchors on GitHub), and a story claim opens the story at its sentence; pipeline locations stay plain text.
  • The overview description renders one span per line with hidden content still flagged so a claimed line can be marked and scrolled to, new claimWhere/claimEvidence messages flow through the side-bar provider and extension handlers, and docs/reviewing.md plus unit tests (escaping of agent-derived text, link targets, and CI-only VS Code tests) cover the new surfaces.

Risk Assessment

✅ Low: A well-bounded UI wiring change that satisfies every stated acceptance criterion - reason and evidence on step 5 rows, head-copy and check-run chips, marked description line and story sentence, escaped agent text, and the single guarded outside link - with the only findings being two informational notes (markup duplication and name-based check-run matching).

Testing

Drove every acceptance criterion of the ticket against the real product code in this run: each unverifiable claim's step 5 row renders its reason and evidence source inline with cited head-copy lines as chips, CI log citations as chips whose only outside link is the check run's github.com page (an evil.example URL is refused), the dropped-citation recheck, and where-made as links in both step 5 and the overview claims table — a description claim reopens the overview with that description line marked and scrolled to, a story claim marks its sentence, a file claim opens the head copy line, a pipeline claim stays text, and fetched-library citations stay non-opening chips; hostile agent text in reason, recheck, paths and aria-labels stays escaped. Everything passed, verified through the ticket's standing boundary routes (real rendered output, real panels handling the real page scripts' messages, and the unit suites), with rendered-HTML artifacts as reviewer-visible evidence; no screenshot or GIF was possible because the boundary forbids launching VS Code (or any GUI) on this machine, and browser automation would require installing tools, which the boundary also forbids.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An unverifiable claim's step 5 row shows its reason and evidence source without opening anything else ✅ pass live Real sideBarBody/sideBarHtml executed in this run: rendered row contains '<div class="why">the model&#39;s memory: Nothing in the change shows it.</div>' inline (artifact side-bar-step5-claims.html);…
A cited head-copy line on a step 5 row is a chip that opens the head copy at that line ✅ pass live Live driver (real SideBarProvider): claimEvidence {claim:2,index:0} → deps.openLine('src/retry.py', 6); chip rendered with title 'Open the line in the head copy' in the artifact; side-bar-page.test.ts…
A cited CI log line opens its check run on GitHub, and only for https://github.com/ addresses ✅ pass live Live driver: claimEvidence {claim:0,index:0} → vscode.env.openExternal('https://github.com/example-org/example-repo/actions/runs/700/job/9001&#39;); after mutating the check url to https://evil.example/..…
A 'description, line N' where opens the overview's description scrolled to that line, marked ✅ pass live Live driver (real OverviewPanel): openClaimWhere claim 0 → reopened HTML has '<span class="dline focus" data-line="1">' and scrolls '.dline.focus' into view; side-bar route sends openOverviewAt({descr…
A story claim's where opens the story at its sentence, marked ✅ pass live Live driver: OverviewPanel openClaimWhere claim 3 → '<span class="sentence focus">Then read …' and SideBarProvider claimWhere claim 4 → openOverviewAt({storySentence:1})
A file claim's where opens its line in the head copy, and a pipeline claim's where stays plain text, in step 5 and the overview claims table ✅ pass live Live driver: claimWhere claim 2 → openLine('src/retry.py', 3); rendered output shows <button … data-claim-where> for file/description/story claims and <span class="where">pipeline report, Review step…
A fetched library's cited lines show as chips that open nothing ✅ pass live Live driver: fetchedResult claimEvidence {claim:2,index:0} → no openLine call; rendered output shows '<span class="chip">requests/models.py:1021</span>' with no data-claim-evidence button
The recheck shows on the row when the engine dropped a citation ✅ pass live Rendered output: '<div class="why">dropped to unverifiable: the model&#39;s memory never yields verified</div>' on the story claim's row (artifact side-bar-step5-claims.html)
Agent-derived text in the new places (reason, recheck, cited path, where path, aria-label) stays escaped ✅ pass live Live hostile render with <img>/<script> in reason, recheck, evidence path and where path: all appear as &lt;img src=y&gt;/&lt;script&gt; text, body matches no raw <img|<script; repo escaping tests in…
Malformed or out-of-range claim/evidence references open nothing and break nothing ✅ pass live Live driver: claimWhere claim 99, claimEvidence index 5, claimEvidence claim 99 → no opens, no errors; repo sideBarMessage tests reject non-integer claim/index ('1', '0')
Extension tests that need VS Code run in CI only ✅ pass live npx vitest list on the default config resolves no file under test/integration, test/real-host or test/package-smoke (only pure-helper unit tests at test/ top level); vitest.integration.config.ts resol…
Evidence: Step 5 claim rows as the side bar webview renders them (product output, verbatim)

<li class="finding"><div class="head"><span class="pill tone-warn">? unverifiable</span><button type="button" class="where" id="step-5-where-claim-1" data-claim-where="1">pull request description, line 1</button></div><q class="quote">Retries failed sends.</q><div class="why">the model&#39;s memory: Nothing in the change shows it.</div></li> — plus the refuted rows' <div class="why">the change itself: The loop runs five times.</div> with chip buttons "src/retry.py:6" (title "Open the line in the head copy") and "CI log of check / test, line 2" (title "Open its check run on GitHub"), and the story claim's <div class="why">dropped to unverifiable: the model&#39;s memory never yields verified</div>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-N'; script-src 'nonce-N';">
<style nonce="N">
  body { color: var(--vscode-foreground); background-color: var(--vscode-sideBar-background); font-family: var(--vscode-font-family); font-size: var(--vscode-font-size, 13px); margin: 0; padding: 8px 10px; }
  .steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
  .step { border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 4px; }
  .step[aria-current] { border-color: var(--vscode-focusBorder); }
  .step.to-come { opacity: 0.75; }
  h2 { display: flex; align-items: center; margin: 0; font-size: inherit; }
  button { font: inherit; cursor: pointer; }
  button:focus-visible, input:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; }
  .fold { display: flex; align-items: center; gap: 6px; width: 100%; padding: 5px 8px; color: inherit; background: none; border: none; text-align: left; font-weight: 600; }
  .fold:hover { background-color: var(--vscode-list-hoverBackground); }
  .icon { width: 1em; text-align: center; flex: none; }
  .title { white-space: nowrap; flex: none; }
  .done .icon { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
  .current .icon, .running .icon { color: var(--vscode-progressBar-background, var(--vscode-focusBorder)); }
  .running .icon, .spin { display: inline-block; animation: spin 1.2s linear infinite; }
  .waiting .icon { color: var(--vscode-descriptionForeground); }
  @keyframes spin { to { transform: rotate(360deg); } }
  @media (prefers-reduced-motion: reduce) { .running .icon, .spin { animation: none; } }
  .progress { margin-bottom: 6px; }
  .tiles { list-style: none; margin: 0 0 5px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 5px; }
  .tile { display: flex; flex-direction: column; padding: 4px 6px; border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 4px; min-width: 0; }
  .tile .big { font-size: 16px; font-weight: 700; line-height: 1.2; }
  .tile small { font-size: 11px; font-weight: 400; color: var(--vscode-descriptionForeground); }
  .tile .lb { font-size: 11px; color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .strip { margin: 0; padding: 5px 8px; border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 4px; overflow-wrap: anywhere; }
  .strip.running .icon { color: var(--vscode-progressBar-background, var(--vscode-focusBorder)); }
  .strip.complete .icon { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
  .stage-notes { list-style: none; margin: 4px 0 0; padding: 0 8px; font-size: 12px; }
  .stage-notes li { margin: 2px 0; overflow-wrap: anywhere; }
  .stage-notes .fell-back .icon { color: var(--vscode-editorWarning-foreground, var(--vscode-foreground)); }
  .stage-notes .failed .icon { color: var(--vscode-errorForeground); }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .summary { margin-left: auto; font-weight: 400; color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .state { font-weight: 400; font-size: 11px; color: var(--vscode-descriptionForeground); white-space: nowrap; }
  .summary + .state::before { content: "· "; }
  .fold .title + .state { margin-left: auto; }
  .body { padding: 0 8px 8px; }
  .note { margin: 4px 0; color: var(--vscode-descriptionForeground); }
  .pr { margin: 4px 0; overflow-wrap: anywhere; }
  .group { margin: 8px 0 2px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--vscode-descriptionForeground); }
  .parts { list-style: none; margin: 0; padding: 0; }
  .part { display: flex; align-items: flex-start; gap: 6px; padding: 2px 0; }
  .row { display: flex; flex-direction: column; min-width: 0; }
  .menus { display: flex; margin-left: auto; flex: none; }
  .menus .link { padding: 0 4px; font-size: 12px; }
  .sent { margin: 4px 0; overflow-wrap: anywhere; }
  .open { padding: 0; color: var(--vscode-foreground); background: none; border: none; text-align: left; overflow-wrap: anywhere; }
  .open:hover { color: var(--vscode-textLink-activeForeground, var(--vscode-textLink-foreground)); text-decoration: underline; }
  .desc { font-size: 12px; color: var(--vscode-descriptionForeground); overflow-wrap: anywhere; }
  input[type="checkbox"] { margin: 2px 0 0; accent-color: var(--vscode-button-background); }
  .actions { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
  .primary, .secondary { border: 1px solid var(--vscode-button-border, transparent); border-radius: 2px; padding: 3px 10px; }
  .primary { color: var(--vscode-button-foreground); background-color: var(--vscode-button-background); }
  .primary:hover { background-color: var(--vscode-button-hoverBackground); }
  .secondary { color: var(--vscode-button-secondaryForeground); background-color: var(--vscode-button-secondaryBackground); }
  .secondary:hover { background-color: var(--vscode-button-secondaryHoverBackground); }
  button:disabled { opacity: 0.5; cursor: default; }
  .link { flex: none; padding: 0 8px; color: var(--vscode-textLink-foreground); background: none; border: none; }
  .link:hover { text-decoration: underline; }
  .setup .label { display: block; margin: 8px 0 3px; font-size: 11px; font-weight: 600; color: var(--vscode-descriptionForeground); }
  .choices { display: flex; flex-wrap: wrap; gap: 4px; }
  .choice { display: flex; flex-direction: column; align-items: flex-start; padding: 3px 8px; color: var(--vscode-foreground); background: none; border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 3px; text-align: left; }
  .choice[aria-checked="true"] { border-color: var(--vscode-focusBorder); background-color: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground, var(--vscode-foreground)); }
  .choice small { font-size: 11px; color: var(--vscode-descriptionForeground); }
  .setup-text { box-sizing: border-box; width: 100%; padding: 3px 6px; color: var(--vscode-input-foreground); background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: 2px; font: inherit; }
  .problem { color: var(--vscode-errorForeground); }
  .picker { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
  .pr-filter { box-sizing: border-box; width: 100%; padding: 3px 6px; color: var(--vscode-input-foreground); background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); border-radius: 2px; font: inherit; }
  .chip { padding: 0 8px; font-size: 11px; color: var(--vscode-descriptionForeground); background: none; border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 10px; line-height: 18px; }
  .chip[aria-pressed="true"] { color: var(--vscode-list-activeSelectionForeground, var(--vscode-foreground)); background-color: var(--vscode-list-activeSelectionBackground); border-color: var(--vscode-focusBorder); }
  .group .count { padding: 0 5px; border-radius: 8px; color: var(--vscode-badge-foreground); background-color: var(--vscode-badge-background); letter-spacing: 0; }
  .pr-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
  .pr-card, .pr-state { padding: 6px 8px; border: 1px solid var(--vscode-widget-border, var(--vscode-panel-border)); border-radius: 4px; }
  .pr-state h3 { margin: 0; font-size: inherit; }
  .pr-title { margin: 0; font-weight: 600; overflow-wrap: anywhere; }
  .pr-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; margin: 2px 0 0; font-size: 12px; color: var(--vscode-descriptionForeground); }
  .pr-summary { margin: 3px 0 0; font-size: 12px; overflow: hidden; display: -webkit-box; -webki

... [21799 bytes truncated] ...

d the criteria"><label for="step-6-tick">I&#39;ve checked the criteria</label></div></div></li><li class="step to-come" id="step-7"><h2><button type="button" class="fold" id="step-7-title" data-step="7" aria-expanded="false" aria-controls="step-7-body"><span class="icon" aria-hidden="true">○</span><span class="title">7 Comments</span><span class="state">to come</span></button></h2><div class="body" id="step-7-body" role="region" aria-labelledby="step-7-title" hidden><p class="note">No comment yet.</p><p class="note">Add more: the + beside any diff line, or “Comment on this part…” in a part’s ⋯ menu.</p><div class="actions"><button type="button" id="step-7-allParts" class="secondary" data-command="allParts">Open the threads in the diff</button></div></div></li><li class="step to-come" id="step-8"><h2><button type="button" class="fold" id="step-8-title" data-step="8" aria-expanded="false" aria-controls="step-8-body"><span class="icon" aria-hidden="true">○</span><span class="title">8 Send the review</span><span class="state">to come</span></button></h2><div class="body" id="step-8-body" role="region" aria-labelledby="step-8-title" hidden><p class="note">Nothing reaches GitHub until you press Submit on the page.</p><div class="actions"><button type="button" id="step-8-send" class="primary" data-command="send">Open the Send review page</button></div></div></li></ol></main>
<script nonce="N">
(function () {
  'use strict';
  var vscode = acquireVsCodeApi();
  var root = document.getElementById('root');
  var folds = {};
  var current = null;
  function anchorOf(element) {
    try { return JSON.parse(element.getAttribute('data-anchor')); } catch (error) { return undefined; }
  }
  function setOpen(fold, open) {
    fold.setAttribute('aria-expanded', String(open));
    var body = document.getElementById(fold.getAttribute('aria-controls'));
    if (body !== null) body.hidden = !open;
  }
  function keepFolds() {
    var steps = root.querySelector('.steps');
    var now = steps === null ? null : steps.getAttribute('data-current');
    if (now !== current) { folds = {}; current = now; }
    Array.prototype.forEach.call(root.querySelectorAll('button.fold'), function (fold) {
      var step = fold.getAttribute('data-step');
      if (Object.prototype.hasOwnProperty.call(folds, step)) setOpen(fold, folds[step]);
    });
  }
  root.addEventListener('click', function (event) {
    var pressed = event.target.closest('button');
    if (pressed === null || pressed.disabled) return;
    if (pressed.classList.contains('fold')) {
      var open = pressed.getAttribute('aria-expanded') !== 'true';
      folds[pressed.getAttribute('data-step')] = open;
      setOpen(pressed, open);
      // Step 2's list loads the first time its card shows.
      if (open && pressed.getAttribute('data-step') === '2') vscode.postMessage({ type: 'pullRequests', action: 'show', value: '' });
    } else if (pressed.hasAttribute('data-comment')) {
      vscode.postMessage({ type: 'comment', action: pressed.getAttribute('data-comment'), id: Number(pressed.getAttribute('data-id')) });
    } else if (pressed.hasAttribute('data-part-menu')) {
      vscode.postMessage({ type: 'partMenu', menu: pressed.getAttribute('data-part-menu'), anchor: anchorOf(pressed) });
    } else if (pressed.classList.contains('open')) {
      vscode.postMessage({ type: 'openPart', anchor: anchorOf(pressed) });
    } else if (pressed.hasAttribute('data-claim-where')) {
      vscode.postMessage({ type: 'claimWhere', claim: Number(pressed.getAttribute('data-claim-where')) });
    } else if (pressed.hasAttribute('data-claim-evidence')) {
      vscode.postMessage({ type: 'claimEvidence', claim: Number(pressed.getAttribute('data-claim-evidence')), index: Number(pressed.getAttribute('data-index')) });
    } else if (pressed.hasAttribute('data-finding')) {
      vscode.postMessage({ type: 'finding', action: pressed.getAttribute('data-finding'), kind: pressed.getAttribute('data-kind'), index: Number(pressed.getAttribute('data-index')) });
    } else if (pressed.hasAttribute('data-issue')) {
      vscode.postMessage({ type: 'openIssue', issue: Number(pressed.getAttribute('data-issue')) });
    } else if (pressed.hasAttribute('data-pr-action')) {
      vscode.postMessage({ type: 'pullRequests', action: pressed.getAttribute('data-pr-action'), value: pressed.getAttribute('data-value') });
    } else if (pressed.hasAttribute('data-command')) {
      vscode.postMessage({ type: 'command', command: pressed.getAttribute('data-command') });
    } else if (pressed.hasAttribute('data-field')) {
      vscode.postMessage({ type: 'setup', field: pressed.getAttribute('data-field'), value: pressed.getAttribute('data-value') });
    } else if (pressed.hasAttribute('data-action')) {
      var fields = {};
      Array.prototype.forEach.call(root.querySelectorAll('input.setup-text'), function (box) { fields[box.getAttribute('data-field')] = box.value; });
      vscode.postMessage({ type: 'setup', action: pressed.getAttribute('data-action'), value: pressed.getAttribute('data-value'), fields: fields });
    }
  });
  root.addEventListener('change', function (event) {
    var box = event.target;
    if (box.classList.contains('setup-text')) {
      vscode.postMessage({ type: 'setup', field: box.getAttribute('data-field'), value: box.value });
      return;
    }
    if (box.type !== 'checkbox') return;
    if (box.hasAttribute('data-tick')) {
      vscode.postMessage({ type: 'tick', step: box.getAttribute('data-tick'), checked: box.checked });
      return;
    }
    vscode.postMessage({ type: 'mark', anchor: anchorOf(box), reviewed: box.checked });
  });
  // The list's box filters as the reviewer types; a paste or Enter can start a review of the URL in it.
  root.addEventListener('input', function (event) {
    var box = event.target;
    if (box.id !== 'pr-filter') return;
    vscode.postMessage({ type: 'pullRequests', action: 'filter', value: box.value, pasted: event.inputType === 'insertFromPaste' });
  });
  root.addEventListener('keydown', function (event) {
    var box = event.target;
    if (box.id !== 'pr-filter' || event.key !== 'Enter') return;
    event.preventDefault();
    vscode.postMessage({ type: 'pullRequests', action: 'go', value: box.value });
  });
  window.addEventListener('message', function (event) {
    var message = event.data;
    if (message === null || typeof message !== 'object' || message.type !== 'render' || typeof message.body !== 'string') return;
    var active = document.activeElement !== null && root.contains(document.activeElement) ? document.activeElement : null;
    var focused = active === null ? '' : active.id;
    // A text box being typed in keeps its text: it reaches the extension only once it changes.
    var typed = active !== null && (active.classList.contains('setup-text') || active.id === 'pr-filter') ? active.value : null;
    var caret = typed === null ? null : [active.selectionStart, active.selectionEnd];
    root.innerHTML = message.body;
    keepFolds();
    var again = focused === '' ? null : document.getElementById(focused);
    if (again !== null && typed !== null) again.value = typed;
    if (again !== null) again.focus();
    if (again !== null && caret !== null && typeof again.setSelectionRange === 'function') again.setSelectionRange(caret[0], caret[1]);
  });
  // The running stage's time so far, as the extension wrote it, moves on each second.
  function elapsed(ms) {
    var seconds = Math.max(0, Math.floor(ms / 1000));
    var hours = Math.floor(seconds / 3600);
    var minutes = Math.floor((seconds % 3600) / 60);
    var rest = seconds % 60;
    var ss = (rest < 10 ? '0' : '') + rest;
    return hours > 0 ? hours + ':' + (minutes < 10 ? '0' : '') + minutes + ':' + ss : minutes + ':' + ss;
  }
  window.setInterval(function () {
    Array.prototype.forEach.call(root.querySelectorAll('.elapsed'), function (span) {
      var started = Number(span.getAttribute('data-started') || NaN);
      if (isFinite(started)) span.textContent = elapsed(Date.now() - started);
    });
  }, 1000);
  keepFolds();
  vscode.postMessage({ type: 'ready' });
}());
</script>
</body>
</html>
  • Evidence: Same page themed for browser viewing (identical body, theme variable values added) (local file: ~/.no-mistakes/evidence/01M4QS673C5RX3DAZWJ89P3B0Z/side-bar-step5-claims.preview.html)
Evidence: Overview opened at a description claim's line: line 2 marked and scrolled to (product output, verbatim)

Source: Overview opened at a description claim's line: line 2 marked and scrolled to (product output, verbatim) (local file: ~/.no-mistakes/evidence/01M4QS673C5RX3DAZWJ89P3B0Z/overview-opened-at-description-line-2.html)

<span class="dline focus" data-line="2">See ![chart](https://evil.example/chart.png).</span> with hidden content still flagged on line 1, and the page script scrolling document.querySelector('.dline.focus') into view; the claims table shows <button type="button" class="pt where" data-claim-where="1">pull request description, line 1</button> while the pipeline claim stays <span class="where">pipeline report, Review step · src/retry.py:6</span>
  • Evidence: Same overview themed for browser viewing (identical body, theme variable values added) (local file: ~/.no-mistakes/evidence/01M4QS673C5RX3DAZWJ89P3B0Z/overview-opened-at-description-line-2.preview.html)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ packages/extension/src/overview.ts:1290 - descriptionLines (overview.ts:1290-1304) re-implements the hidden-run markup that sanitiseUntrusted already builds (flag label plus shown span, overview.ts:346-360), and descriptionSection (overview.ts:1315) runs hiddenContent a second time through sanitiseUntrusted only to obtain the hidden counts for the summary line. One pass that returns both the per-line markup and the counts, or a shared span builder used by both, would keep the flagged rendering and the summary from drifting apart. Mechanical, non-functional simplification; the line splitting itself is correct (verified against multi-line and unclosed HTML comments: pushes happen exactly per newline, so data-line numbering matches the engine's 1-based flattenText lines).
  • ℹ️ packages/extension/src/side-bar/provider.ts:269 - A CI-log citation carries only the check run's name, and the handler resolves the URL by matching ci.checks on name (provider.ts:269, mirroring the engine's id-to-name mapping in verdicts.ts:421); if a pull request has two failed check runs with the same name, the chip opens the first one's run. Rare (GitHub matrix jobs usually carry distinct names), self-evident on landing, and a durable fix needs the engine's Citation to carry the check run reference rather than its name, which extends beyond this change - noting the limitation, not blocking on it.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 11 of 11 scenarios driven live against the product
Scenario Result Live Evidence
An unverifiable claim's step 5 row shows its reason and evidence source without opening anything else ✅ pass live Real sideBarBody/sideBarHtml executed in this run: rendered row contains '<div class="why">the model&#39;s memory: Nothing in the change shows it.</div>' inline (artifact side-bar-step5-claims.html);…
A cited head-copy line on a step 5 row is a chip that opens the head copy at that line ✅ pass live Live driver (real SideBarProvider): claimEvidence {claim:2,index:0} → deps.openLine('src/retry.py', 6); chip rendered with title 'Open the line in the head copy' in the artifact; side-bar-page.test.ts…
A cited CI log line opens its check run on GitHub, and only for https://github.com/ addresses ✅ pass live Live driver: claimEvidence {claim:0,index:0} → vscode.env.openExternal('https://github.com/example-org/example-repo/actions/runs/700/job/9001&#39;); after mutating the check url to https://evil.example/..…
A 'description, line N' where opens the overview's description scrolled to that line, marked ✅ pass live Live driver (real OverviewPanel): openClaimWhere claim 0 → reopened HTML has '<span class="dline focus" data-line="1">' and scrolls '.dline.focus' into view; side-bar route sends openOverviewAt({descr…
A story claim's where opens the story at its sentence, marked ✅ pass live Live driver: OverviewPanel openClaimWhere claim 3 → '<span class="sentence focus">Then read …' and SideBarProvider claimWhere claim 4 → openOverviewAt({storySentence:1})
A file claim's where opens its line in the head copy, and a pipeline claim's where stays plain text, in step 5 and the overview claims table ✅ pass live Live driver: claimWhere claim 2 → openLine('src/retry.py', 3); rendered output shows <button … data-claim-where> for file/description/story claims and <span class="where">pipeline report, Review step…
A fetched library's cited lines show as chips that open nothing ✅ pass live Live driver: fetchedResult claimEvidence {claim:2,index:0} → no openLine call; rendered output shows '<span class="chip">requests/models.py:1021</span>' with no data-claim-evidence button
The recheck shows on the row when the engine dropped a citation ✅ pass live Rendered output: '<div class="why">dropped to unverifiable: the model&#39;s memory never yields verified</div>' on the story claim's row (artifact side-bar-step5-claims.html)
Agent-derived text in the new places (reason, recheck, cited path, where path, aria-label) stays escaped ✅ pass live Live hostile render with <img>/<script> in reason, recheck, evidence path and where path: all appear as &lt;img src=y&gt;/&lt;script&gt; text, body matches no raw <img|<script; repo escaping tests in…
Malformed or out-of-range claim/evidence references open nothing and break nothing ✅ pass live Live driver: claimWhere claim 99, claimEvidence index 5, claimEvidence claim 99 → no opens, no errors; repo sideBarMessage tests reject non-integer claim/index ('1', '0')
Extension tests that need VS Code run in CI only ✅ pass live npx vitest list on the default config resolves no file under test/integration, test/real-host or test/package-smoke (only pure-helper unit tests at test/ top level); vitest.integration.config.ts resol…
  • npx vitest run packages/extension/test/overview.test.ts packages/extension/test/side-bar.test.ts packages/extension/test/side-bar-page.test.ts — 137 tests pass (drives the real page scripts and providers end to end)
  • Temporary live driver packages/extension/test/live-render-driver.test.ts (written, run, then removed): executed the real sideBarHtml/sideBarBody and overviewHtml renderers, the real OverviewPanel (openClaimWhere for file/description/story claims, marked dline/sentence, scroll selector) and the real SideBarProvider (claimWhere and claimEvidence messages: head-copy line opens, CI check run opens via vscode.env.openExternal, https://evil.example URL refused, out-of-range indices and fetched-library citations open nothing) — 11 checks pass
  • Rendered-output evidence: side-bar-step5-claims.html and overview-opened-at-description-line-2.html written verbatim from the product renderers, plus themed .preview.html copies for browser legibility
  • npx vitest list (default config) — confirms no integration/real-host/package-smoke VS Code suite runs locally; npx vitest list --config vitest.integration.config.ts resolves only packages/extension/test/integration/extension.test.ts; .github/workflows/ci.yml runs those suites under xvfb in CI only
  • npx tsc -b && npx tsc -p tsconfig.test.json — the change builds and all tests typecheck (build outputs cleaned up afterwards)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…row, and link where it is made (#172)

Each refuted or unverifiable claim's step 5 row now shows its evidence
source with its reason, each line it cites as a chip — a head-copy line
opening the line read-only as the overview's chips do, a CI log line
opening its check run on GitHub, the one outside link, taken only for
github.com addresses — and the recheck when the engine dropped a
citation, so an unverifiable claim says why it was not verified without
opening anything else. Where a claim is made becomes a link, on step 5
and in the overview's claims table: a docstring's or comment's line
opens in the head copy, a description claim's line opens the overview's
description scrolled to it and marked, since a pull request description
has no line anchors on GitHub, and a story claim's opens the story at
its sentence. Every piece of agent-derived text in the new places
reaches the page escaped.
…nsion/test/integration/extension.test.ts:2168 asserted the old step 5 claim-row markup where the claim's 'where' was plain text (<span class="where">). This PR (#172) changed claimFindingRow in packages/extension/src/side-bar/page.ts to render 'where' as a link (<button type="button" class="where" id="step-5-where-claim-1" data-claim-where="1">), and updated the unit tests (side-bar.test.ts, side-bar-page.test.ts) but missed the CI-only integration test, which npm test/npm run check never run locally. Invariant fixed: every assertion of the rendered step 5 claim-row markup must match the renderer; the single stale sibling assertion was updated to expect the button markup, identical to the already-updated side-bar.test.ts:302 assertion. Verified locally: npm run test:integration 79/79 pass (including the failing test), npm run check (build, tsc test config, lint, 1879 unit tests) all pass. No production code changed; smallest root-cause fix
@lbildzinkas
lbildzinkas merged commit 37f32d3 into master Oct 12, 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.

1 participant