{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"114b644c-bd95-47b4-8b3d-d5f60e75b5a9","kind":"audit","nodes":[{"acceptedSubmissionHash":"f72a059cc303c1df707bbc96b7a6eb93e03eccd3441e9a8b5bb05051176d3633","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"361672398dd8635e15b90136fb54953b6b538c792937aa800d0e3ba9993786bb","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"dfe45254dbf8bf5ba3a99b575647151241bce274a34866384cad2cef325170df","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","subagents":4,"tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"5b9b9db79edd81da92e81e139a50acb8ff011689bd6ec9930df2dd1c6e783206","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0808fb23d652a91663b64add9c982a99fd328dd9f10628d5498a4c62a8edd2c0","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit the Python and HTML code under viewer at commit 288a03eda34957e23fd6ab19c2581ce984ba2a1d. Focus on build-lane.py, settlement-viewer-2026-11-09.html and test_lane_disagree.py: whether a row can ever show green unless the receipt's job, seat, DID and request_id exactly match the ledger row, and whether the identity-mismatch or disagree labels can be hidden or reordered. Require a file/line, impact and runnable reproduction for every finding. Offline tooling, read-only review. The imd-flop and spend-guard folders are outside this audit.","parentJobId":null,"planHash":"04503140a8b1f4b225c798d698e81e9a33232b989ddfdb915231195de3abf61d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"114b644c-bd95-47b4-8b3d-d5f60e75b5a9","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"54235","feedbackHash":"84bfd6e097025f2d2a859e20722911f7666d07a35a42873dd6186066ca6584d3","nodeKey":"audit_economics","submissionHash":"f72a059cc303c1df707bbc96b7a6eb93e03eccd3441e9a8b5bb05051176d3633","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"d000dd44226a1ee965a614e207f2c2b142346a55e2a32cd2379646013fbf4d79","nodeKey":"audit_flow","submissionHash":"361672398dd8635e15b90136fb54953b6b538c792937aa800d0e3ba9993786bb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52599","feedbackHash":"06eaa1cf22510dd014ebc0f51ba3345cc89dbc95365eee59f7d884515bc22c8b","nodeKey":"audit_judge","submissionHash":"dfe45254dbf8bf5ba3a99b575647151241bce274a34866384cad2cef325170df","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51486","feedbackHash":"29f7f57f4ed19cfa3309dc7c7ff6ddf3b96bb37e850fee2f8cc62839b685b9c7","nodeKey":"audit_math","submissionHash":"5b9b9db79edd81da92e81e139a50acb8ff011689bd6ec9930df2dd1c6e783206","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51084","feedbackHash":"e5227bfccfac53174a478b4ddb9570149c37c27e6e02a69882e7fc0df7fab6b8","nodeKey":"audit_permissions","submissionHash":"0808fb23d652a91663b64add9c982a99fd328dd9f10628d5498a4c62a8edd2c0","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"76f6d24b5a89faba4e2e23f283c3dc646ceea5beeb6abdfc899abad0f4b867ba","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7c0191a32541eb74","findings":[{"citation":"resolved","description":"Every row built from the ledger starts with link=[\"green\",\"linked\",None] (build-lane.py:45) and the only thing that changes it is e.get(\"link_bypassed\") on line 46. No ledger line ever carries that key: imd-flop's LedgerEntry (imd-flop/src/ledger.ts:14-40) has no link field, and run.ts only returns link_bypassed in the CLI result (run.ts:298), never in the ledger. So the lane renders a green 'linked' chip from the absence of data, not from a link verification result, and it cannot be switched off. Two consequences. (1) A row whose receipt fails identity_ok still shows a green chip in the Link check column (committed HTML would show it exactly like lines 95/102/109), while the legend on settlement-viewer-2026-11-09.html:86 promises that such a row 'can never go green'. (2) A mock run made with --no-link, which the CLI itself prints as 'LINK BYPASSED' and 'not a valid FLOP route', produces a ledger line byte-for-byte shaped like the committed mock-receipts/ledger.jsonl lines and is shown as green 'linked'. The reference row 'Example no route' shows the column is meant to carry a real verify result (red 'no route', 'link verify failed'). test_lane_disagree.py:75-76 asserts exactly one green chip on a mismatch row and that it is 'linked', so the suite locks the defect in. Fix: default link to grey 'not checked' and set green only from a positive field written by the producer (or from link.json for the seat), and make the legend's 'never green' claim cover the whole row.","line":45,"path":"viewer/build-lane.py","reproduction":"Input: any ledger line with the shape of viewer/mock-receipts/ledger.jsonl (none has a link_bypassed key), e.g. job 'j', request_id 'rid-1', seat '1234', did 'did:key:zA', state 'sent'; receipt rid-1.json with request_id 'rid-1' but job 'someone-elses-job'. Run: python3 viewer/build-lane.py <that dir> (or call from_files()/render()). Actual: the row has class 'row bad', red 'mismatch' chip, and still contains '<span class=\"chip c-green\">linked</span>'. Also: run 'imd-flop run --provider mock --no-link ...' and point build-lane.py at its receipts dir: the row renders green 'linked'. Expected: no green chip on an identity-mismatch row and no 'linked' chip without a recorded link result.","severity":"high","snippet":"                   outcome=[\"grey\", \"not read\", \"mock job, no imd lookup\"], link=[\"green\", \"linked\", None], bad=False)","title":"Link check column is hard-coded green 'linked' for every receipt row; identity-mismatch rows keep a green chip and --no-link runs look linked"},{"citation":"resolved","description":"The disagree gate on line 71 only fires when the receipt state is 'settled' or 'released'. Any other receipt state ('reserved', 'estimated', null, or an arbitrary string) skips it, and line 85 then appends the green 'chain final' chip whenever r.get(\"chain_final\") is truthy. The check does not require the receipt to be settled, does not require a known settled amount, does not look at the ledger state (which may still be 'sent' or 'timeout_unknown'), and does not require a boolean: the string \"false\" is truthy in Python. The result is a green chain-final chip beside a blank observed spend and a ledger row still at 'sent', with no DISAGREE flag, which is exactly the state the DISAGREE text ('reservation held, chain-final green blocked') says must not go green. imd-flop's current writer always sets chain_final to false (receipts.ts:64), so today this is reachable only through the viewer's own input contract (any receipts directory passed as argv[1]); it becomes live the moment chain verification is wired in or a receipt is produced by another tool. Fix: require chain_final is True and receipt state in ('settled','released') and a numeric settled amount equal to the ledger spend and ledger state not in ('sent','timeout_unknown','error'); otherwise show amber DISAGREE.","line":85,"path":"viewer/build-lane.py","reproduction":"Input: ledger line job 'j', request_id 'rid-1', seat '1234', did 'did:key:zA', state 'sent', spend null, cap_charge 0.198; receipt rid-1.json {request_id:'rid-1', job:'j', seat:'1234', did:'did:key:zA', state:'reserved', settled:null, stage:'gateway observed', chain_final:true}. Run build-lane.py on that dir. Actual: chips 'sent' + green 'chain final', observed spend blank, row not marked bad, no 'receipt and ledger disagree' flag. Variant: same receipt with state 'estimated' and chain_final:\"false\" (string) also renders green 'chain final'. Expected: amber DISAGREE chip and flag, no green.","severity":"medium","snippet":"            if ok and r.get(\"chain_final\") and not row[\"bad\"]: chips.append([\"green\", \"chain final\"])","title":"Green 'chain final' chip is driven only by the receipt's chain_final truthiness, not by receipt state, ledger state or a known settled amount"},{"citation":"resolved","description":"The timeout flag ('timeout: spend unknown, not retried'), the red 'unknown' chip and bad=True are produced only in the 'elif r is None' branch (lines 53-56). If a receipt file named after the request_id exists, the ledger's own state and reason are never consulted again: the row goes down the receipt branch, gets grey 'sent' plus amber 'gateway observed', 'match matches', bad=False, and the ledger's 'spend unknown' reason disappears from the page. The HTML Notes promise 'A timeout or 404 shows as unknown and gets flagged.' The asymmetry is between the no-receipt branch (ledger state drives the row) and the receipt branch (ledger state is dropped entirely except for st=='sent' inside the disagree check). With imd-flop's current writer a receipt for a timed-out attempt is only written by 'receipt --refresh', which also rewrites the ledger state, so this is reachable through the viewer's input contract rather than the current CLI flow. Fix: when st in ('timeout_unknown','error') keep the red 'unknown' chip, the flag and bad=True regardless of the receipt, and treat a non-settled receipt beside such a row as DISAGREE.","line":53,"path":"viewer/build-lane.py","reproduction":"Input: ledger line job 'j', request_id 'rid-1', seat '1234', did 'did:key:zA', state 'timeout_unknown', reason 'TimeoutError: no answer; spend unknown, not retried', spend null; receipt rid-1.json {request_id:'rid-1', job:'j', seat:'1234', did:'did:key:zA', state:'estimated', settled:null, stage:'gateway observed', chain_final:false}. Run build-lane.py. Actual: chips [['grey','sent'],['amber','gateway observed']], no red chip, no 'timeout' text, bad=False. Same with state 'error', reason 'HTTP 500': the reason is not rendered. Expected: red 'unknown' chip, the timeout/error reason as a flag, bad=True.","severity":"medium","snippet":"        elif r is None:","title":"Ledger states timeout_unknown and error are ignored once any receipt file exists for the request_id"},{"citation":"resolved","description":"from_files() collapses the ledger to one entry per job (last[e[\"job\"]] = e). imd-flop keys attempts by request_id (ledger.ts:49-53, 'The latest line for a request_id wins') and has no guard against running the same job name twice; each run mints a fresh request_id (run.ts:196). When a job's first attempt times out (state timeout_unknown, cap_charge held, spend null) and the holder runs the job again, the viewer shows only the second attempt: the first request_id, its red 'unknown' chip and its 'spend unknown, not retried' flag vanish from the lane even though the ledger still charges its reservation (the second line's spent_before includes it). The same collapse hides an earlier DISAGREE or identity-mismatch row for that job. This is reachable through normal use of the CLI. Fix: key rows by (job, request_id) or by request_id and list every attempt of a job, or at least append a visible flag when a job has more than one attempt and any earlier one is not settled.","line":33,"path":"viewer/build-lane.py","reproduction":"Input ledger.jsonl (two lines, same job): {job:'j', request_id:'rid-1', seat:'1234', did:'did:key:zA', state:'timeout_unknown', reason:'TimeoutError: no answer; spend unknown, not retried', spend:null, cap_charge:0.198, spent_before:0, ...} then {job:'j', request_id:'rid-2', state:'estimated', spent_before:0.198, cost_reported:0.198, http:200, ...}; receipt rid-2.json matching rid-2 (state 'estimated', chain_final false). Run build-lane.py. Actual: 1 row, text 'rid-1' absent, no 'timeout' text, bad=[False]. Expected: the rid-1 attempt rendered as red 'unknown' with its timeout flag, or the single row flagged that an earlier attempt of this job has unknown spend.","severity":"medium","snippet":"            e = json.loads(line); last[e[\"job\"]] = e","title":"Rows are keyed by job name, so a re-run of the same job hides the earlier attempt, including a timed-out one whose spend is unknown and whose reservation is still charged"},{"citation":"resolved","description":"html.escape is applied to job (line 43) and reason (line 52) but not to request_id, seat, provider, network (lines 41-42) or payer (line 43). num() on line 99 passes any non-numeric, non-None value through raw, so a string in quote_padded, per_call_cap, cap_charge or the receipt's settled field is inserted as markup (receipt settled reaches line 89 only after identity_ok and the disagree check pass). short() keeps 14 characters, which is enough for '<style>'. Because the generated lane never emits '</style>', an opening '<style>' in one row's request_id turns the remainder of the document into CSS text in any browser: every following row, every red 'mismatch' or 'receipt and ledger disagree' flag, and the Notes section become invisible. This is the hiding path for the labels the task asks about. The ledger is written locally by imd-flop (request ids are generated in run.ts:196), so with the current producer this needs a hand-edited or foreign ledger/receipt directory; it is reported as low for that reason, but the viewer explicitly accepts any directory as argv[1]. Fix: html.escape every value taken from the ledger or a receipt (request_id, seat, provider, network, payer, and the num() fallback), and reject non-numeric values in numeric slots by rendering BLANK.","line":41,"path":"viewer/build-lane.py","reproduction":"Input ledger.jsonl: line 1 job 'j', request_id '<style>' (plus a receipt named '<style>.json' or none); line 2 job 'k', request_id 'rid-2'; receipt rid-2.json with job 'other' (identity mismatch). Run build-lane.py. Actual: output contains '<small>req <style>…</small>' with no closing tag; tokenising the output with html.parser (feed+close) shows the visible text ends at 'jreq ' and the string 'receipt identity mismatch' and the 'mismatch' chip for job k are inside the <style> element's data, not visible text. Also: a ledger line with seat '<b onmouseover=alert(1)>x</b>', provider '<i>p</i>', payer '<u>h</u>', quote_padded '<s>0.1</s>' renders each verbatim; a receipt with settled '<style>' (state 'estimated') renders 'observed spend</span> <style>'. Expected: all such values escaped, every later row and flag still visible.","severity":"low","snippet":"        sub = [f\"req {short(e['request_id'] or 'none', 14)}\", f\"seat #{e['seat']} · node {BLANK_NODE}\",","title":"Ledger request_id, seat, provider, network, payer and any string in a numeric slot are rendered unescaped; a '<style>' request_id makes the browser swallow every later row and flag"},{"citation":"resolved","description":"The request_id comparison on line 23 insists on a non-empty string, but the did and job comparisons on line 24 are plain == on r.get(...) against e[...]. imd-flop's LedgerEntry types did as 'string | null' (ledger.ts:19), and a receipt that omits the key yields None from r.get. None == None is True, so a ledger row recorded without a DID and a receipt that carries no DID at all pass the identity check, show 'match matches', and can receive the green 'chain final' chip. That is not the 'exactly match' rule the legend states (request_id, job, seat and DID). seat is protected by the str() wrapper only accidentally ('None' == 'None' would also pass). Fix: require each of job, seat and did to be a non-empty string on both sides before comparing, mirroring the request_id check.","line":24,"path":"viewer/build-lane.py","reproduction":"Unit level: identity_ok({'job':'j','seat':'1','request_id':'rid-1'}, {'job':'j','seat':'1','did':None,'request_id':'rid-1'}) returns True. Row level: ledger line job 'j', request_id 'rid-1', seat '1234', did null, state 'settled', spend 0.1, cap_charge 0.1; receipt rid-1.json {request_id:'rid-1', job:'j', seat:'1234', state:'settled', settled:0.1, stage:'gateway observed', chain_final:true} with no did key. Run build-lane.py. Actual: '<span class=\"k\">match</span> matches', green 'chain final' chip, bad=False. Expected: red 'mismatch' with the identity flag, because the receipt's DID is not present to match.","severity":"low","snippet":"            and r.get(\"job\") == e[\"job\"] and str(r.get(\"seat\")) == str(e[\"seat\"]) and r.get(\"did\") == e[\"did\"])","title":"identity_ok treats a DID (or job) that is absent on both sides as an exact match, so a receipt with no DID can turn a did-less ledger row green"},{"citation":"resolved","description":"The test for a foreign receipt asserts that the rendered mismatch row contains exactly one green chip and that it is 'linked' (lines 75-76 and 111-112), so the suite requires the behaviour reported in the first finding rather than guarding against it, and it will fail when that finding is fixed. The suite also has no test in which identity matches, the receipt is settled, chain_final is true and the ledger agrees (the only path that should ever yield green 'chain final'), so a regression that makes that chip appear too early (second finding) or never is not caught; no test puts a receipt beside a timeout_unknown or error ledger line (third finding); no test re-runs a job (fourth finding); test_identity_check_compares_request_id covers request_id None on both sides but not did or job None on both sides (sixth finding). load_lane() strips a '\\nmain()\\n' that build-lane.py does not contain (it uses an __name__ guard), which is harmless but shows the loader was written for a different file. Fix: replace the count==1/'linked' assertions with 'no c-green chip at all on a mismatch row', and add the four missing cases as tests that fail on the current code.","line":75,"path":"viewer/test_lane_disagree.py","reproduction":"Run: python3 -I viewer/test_lane_disagree.py. Actual: 7 tests pass, including the assertion that a mismatch row contains 'class=\"chip c-green\">linked'. Apply the fix for the first finding (link defaults to grey 'not checked'): test_foreign_settled_receipt_beside_sent_row_shows_identity_mismatch_first and test_foreign_request_id_settled_receipt_beside_sent_row_is_red_identity_only then fail at lines 75-76 / 111-112. Expected: a suite that forbids any green chip on a mismatch row and covers the green chain-final, timeout-with-receipt, re-run and did-null cases.","severity":"low","snippet":"            self.assertEqual(html.count('class=\"chip c-green\"'), 1)","title":"test_lane_disagree.py locks in the green 'linked' chip on a mismatch row and never exercises the green chain-final path, the timeout-with-receipt case, or a non-settled receipt with chain_final true"}],"hash":"0808fb23d652a91663b64add9c982a99fd328dd9f10628d5498a4c62a8edd2c0","nodeId":"6347f3fd-f595-431d-9fd1-798ccd7760ca","outcome":"completed","summary":"The review is complete. Seven substantiated findings are in `.imd-findings.json` at the repository root, each with file and line, a copied snippet, impact, and a reproduction that I ran against build-lane.py at commit 288a03e. No repository files were changed (the untracked entries in git status are the sandbox's own dotfiles, not mine). The committed HTML is byte-identical to a fresh rebuild from mock-receipts, and the existing test suite passes as shipped.\n\n**Answer to the two questions asked**\n\n- Can a row show green without an exact identity match? Yes. The green \"chain final\" chip is correctly gated on identity_ok, but the Link check column is hard-coded green \"linked\" for every row. The ledger has no link field at all, so even a `--no-link` run renders green, and an identity-mismatch row still carries a green chip while the legend says it \"can never go green\". The test suite asserts this green chip exists. Also, a DID that is null in the ledger and absent in the receipt passes identity_ok, so a did-less receipt can reach green \"chain final\".\n- Can the mismatch or disagree labels be hidden or reordered? Hidden, yes, by three routes. Re-running a job collapses it to one row and drops the earlier attempt (including a timed-out one with unknown spend). A receipt file beside a timeout_unknown or error ledger line suppresses the red chip and reason entirely. Unescaped ledger fields let a `<style>` request id swallow every later row and flag in a browser. I found no way to reorder the labels within a row; they are constants appended in fixed order.\n\n**Findings by severity**\n\n| Severity | Location | Issue |\n|---|---|---|\n| high | build-lane.py:45 | Link column always green \"linked\", never switched off, shown on mismatch rows |\n| medium | build-lane.py:85 | Green \"chain final\" from receipt truthiness only, ignores state, spend, ledger, and accepts the string \"false\" |\n| medium | build-lane.py:53 | timeout_unknown and error states dropped once a receipt file exists |\n| medium | build-lane.py:33 | Rows keyed by job, so a re-run hides the earlier timed-out attempt |\n| low | build-lane.py:41 | request_id, seat, provider, network, payer and string values in numeric slots rendered unescaped |\n| low | build-lane.py:24 | identity_ok treats DID or job absent on both sides as a match |\n| low | test_lane_disagree.py:75 | Suite locks in the green \"linked\" chip and lacks the green-path, timeout-with-receipt, re-run and did-null cases |\n\n**Reachability notes**\n\nThe job re-run collapse and the unconditional green link chip are reachable through normal use of the imd-flop CLI. The chain-final, timeout-with-receipt, injection and DID-null cases need a receipt or ledger the current imd-flop writer does not produce, so they are exposed through the viewer's own input contract (any directory passed as the first argument) and will become live as soon as chain verification or another producer is wired in. Severities reflect that.\n\n**Coverage**\n\nAll three focus files were read in full, plus lane-examples.json, the mock receipts, and the imd-flop ledger, run, receipt and CLI sources as reachability context only. The Access Control, Asymmetry and Trust Gap guides were applied as review passes. No subagents ran. No browser was available, so the style-injection hiding was demonstrated with Python's HTML tokenizer rather than a rendered page.","treeHash":null,"usage":{"cachedInputTokens":869495,"cost":{"source":"runtime","usdMicros":4039759},"inputTokens":322,"model":"claude-fable-5-1","outputTokens":34411,"runtime":"claude","turns":21,"wallClockMs":480083}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"render() only wraps a flop value when it is None/int/float; any other type (a string) is concatenated into the page verbatim (line 99-100). Line 89 feeds the receipt's `settled` field straight into that path, so a receipt whose `settled` is a string is written unescaped. The same happens to ledger fields at line 41 (`request_id` via short(), `seat`), line 42 (`provider`, `network`) and line 95 (`payer`), and to string-typed `quote_padded`/`per_call_cap`/`cap_charge` through num(). Only `job` and `reason` are html.escape()d. Because a <style> element applies to the whole document, one row's value can hide the `.flag` divs and red/amber chips of every other row, including the IDENTITY and DISAGREE labels the legend promises are always visible, or (an unclosed `<style>` in request_id, 7 chars, passes short(…,14) untruncated) swallow the rest of the document. The viewer's own comment at line 37 says receipt contents are untrusted and are only checked by identity_ok, yet it renders them. Precondition: a receipt file with a non-numeric `settled` (imd-flop's current writer filters settled to a finite number, but any older/foreign writer or hand-placed receipt in the receipts dir is enough) or a ledger `provider`/`payer`/`seat`/`request_id` carrying markup. Fix: html.escape() every value that reaches the page (sub lines, payer, flop values, est/per_call/daily_left) and render non-numeric `settled` as the unknown blank, never raw.","line":99,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\n\nledger.jsonl (two lines):\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\n{\"job\":\"k\",\"request_id\":\"req-2\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\n\nreq-1.json (identity matches row j, state not settled so no disagree, settled is a string):\n{\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"state\":\"estimated\",\"settled\":\"<style>.flag,.c-red,.c-amber,.row.bad{display:none}</style>\",\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\n\nreq-2.json (foreign receipt for row k):\n{\"request_id\":\"req-2\",\"job\":\"SOMEONE-ELSE\",\"seat\":\"1234\",\"did\":\"did:key:zOTHER\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\n\nExpected: the string is escaped or shown as the unknown blank; row k shows the red `mismatch` chip and the flag 'receipt identity mismatch; chain-final green blocked' visibly.\nActual: the output contains `<span class=\"k\">observed spend</span> <style>.flag,.c-red,.c-amber,.row.bad{display:none}</style>` verbatim (checked: `inj in html` is True). The style rule applies document-wide, so row k's `<div class=\"flag\">receipt identity mismatch…</div>` and its red chip are display:none in any browser. Variant 2: ledger line with \"provider\":\"<style>.flag{display:none}</style>\" -> the markup appears verbatim in the <small> line. Variant 3: ledger \"request_id\":\"<style>\" with receipt file named `<style>.json` -> output contains `<small>req <style></small>`, an unclosed style element that hides every later row.","severity":"high","snippet":"        v = num(v) if (v is None or isinstance(v, (int, float))) else v","title":"Receipt and ledger fields are interpolated into the page as raw HTML; one crafted value hides every identity-mismatch and disagree label"},{"citation":"resolved","description":"Every row starts with link=[green, linked]. The sole downgrade is `if e.get(\"link_bypassed\")` at line 46, but LedgerEntry (imd-flop/src/ledger.ts:12-39) has no link_bypassed field; it exists only on the in-memory run result (run.ts:298), so the guard is dead and the chip is unconditionally green. Nothing in the ledger records that the seat's link was verified, so the viewer asserts a verification it never saw. The identity-mismatch path (lines 60-69) leaves link untouched, so a mismatch row carries a green chip while the legend on the same page (settlement-viewer-2026-11-09.html:86 and the committed Example mismatch row at line 155) says 'that row can never go green'. The CLI's matchReceipt treats link_missing/link_invalid/seat(link)/did(link) as mismatch; the viewer's 'match: matches' and green 'linked' ignore the link entirely. test_lane_disagree.py:75-76 asserts exactly this green chip on a mismatch row, so the test suite enshrines it. Fix: default link to ['grey','not checked',None]; show green only from a ledger/receipt field that records a verified link; force link to grey/red on identity-mismatch rows.","line":45,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\n\nledger.jsonl:\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\n\nreq-1.json (foreign job and DID):\n{\"request_id\":\"req-1\",\"job\":\"SOMEONE-ELSE\",\"seat\":\"1234\",\"did\":\"did:key:zOTHER\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\n\nExpected: no green chip anywhere in the row (legend: 'that row can never go green'); the link column should be grey/unknown since the ledger carries no link verification result.\nActual: rows[0]['link'] == ['green','linked',None]; the rendered row contains `<span class=\"chip c-green\">linked</span>`; re.findall(r'c-green\">([^<]+)', html) == ['linked']. The same is true for every row of the committed mock ledger, none of whose lines has any link_* key.","severity":"medium","snippet":"                   outcome=[\"grey\", \"not read\", \"mock job, no imd lookup\"], link=[\"green\", \"linked\", None], bad=False)","title":"Link check column is green 'linked' by default with no ledger evidence, including on identity-mismatch rows; the only guard reads a field the ledger never contains"},{"citation":"resolved","description":"The receipt-vs-ledger comparison is gated on the receipt's state. If the receipt reports reserved/estimated/null/any other state, no comparison is made even when the ledger row says state=settled with a known spend, and when the receipt's settled amount is null (unknown) it compares equal to a ledger spend of null. Control then reaches line 85 and, with chain_final truthy, appends the green 'chain final' chip and prints 'match matches' (line 88) with a blank observed spend. So a receipt that contradicts the ledger (ledger: settled 0.198; receipt: reserved, nothing settled) is shown as fully agreeing and chain-final, and the DISAGREE label is never emitted. imd-flop currently always writes chain_final=false, so the path is latent today but is exactly the path that matters once chain verification lands. Fix: compare whenever either side has a terminal state or a known spend, e.g. disagree = (receipt_state in settled/released or st in settled/released or e.get('spend') is not None or r.get('settled') is not None) and (st=='sent' or receipt_state not in settled/released or e.get('spend') != r.get('settled')); and never show 'chain final' while observed spend is unknown.","line":71,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\n\nCase A ledger.jsonl:\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"settled\",\"spend\":0.198}\n\nreq-1.json:\n{\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"state\":\"reserved\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}\n\nExpected: amber chip and flag 'receipt and ledger disagree; reservation held, chain-final green blocked' (ledger says settled 0.198, receipt says nothing settled); row.bad True; no green.\nActual: chips end with ['green','chain final'], the row text says `<span class=\"k\">match</span> matches`, DISAGREE is absent, bad is False, observed spend is blank.\n\nCase B ledger.jsonl: {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\nreq-1.json: {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"state\":\"settled\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}\nExpected: disagree (receipt settled, ledger never settled, amount unknown). Actual: `None != None` is False, no DISAGREE, green 'chain final' chip rendered.","severity":"medium","snippet":"            disagree = receipt_state in (\"settled\", \"released\") and (st == \"sent\" or e.get(\"spend\") != r.get(\"settled\"))","title":"Disagree check runs only when the receipt says settled/released; a non-settled receipt with chain_final set turns a settled ledger row green with 'matches' and blank spend"},{"citation":"resolved","description":"from_files() keeps one ledger line per job (the last one written). imd-flop's ledger is keyed by request_id ('request_id wins', ledger.ts:3 and :51) and a job may be run again under the same name (demo.sh reuses demo-job-1..3; a holder re-running an over-cap job gets a refused_local line with request_id null). When that happens the earlier request's row, including a red mismatch or amber disagree it was showing and the reservation it still holds, disappears from the lane, and the new line's state (e.g. 'empty / refused before the call') is all that remains. The receipt for the earlier request_id is still in the directory but is never looked up. Fix: key rows by (job, request_id), or by request_id with job as a label, so every request that reached 'sent' keeps its own row; at minimum render every line whose request_id is non-null and unresolved.","line":33,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\n\nStep 1 ledger.jsonl:\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\nreq-1.json (foreign identity):\n{\"request_id\":\"req-1\",\"job\":\"SOMEONE-ELSE\",\"seat\":\"1234\",\"did\":\"did:key:zOTHER\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\n-> lane shows 1 row with red `mismatch` and the IDENTITY flag (correct).\n\nStep 2: append one more line to ledger.jsonl for the same job (a later refused attempt):\n{\"job\":\"j\",\"request_id\":null,\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:01:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"refused_local\",\"reason\":\"padded quote 0.204 FLOP is over the remaining daily cap 0.152\",\"spend\":null}\n\nExpected: the req-1 mismatch row is still shown (its reservation is still held and its receipt still fails identity), plus the refused attempt.\nActual: still 1 row; rows[0]['chips'] is None ('empty' chip), IDENTITY flag absent, the string 'req-1' appears nowhere in the lane. Appending instead a second 'sent' line with request_id req-9 gives chips [['grey','sent'],['red','unknown']] and again no IDENTITY flag.","severity":"medium","snippet":"            e = json.loads(line); last[e[\"job\"]] = e","title":"Lane rows are keyed by job name only; a later ledger line for the same job replaces the earlier request's row and hides its identity-mismatch or disagree state"},{"citation":"resolved","description":"request_id is required to be a non-empty string (line 23) but job, seat and DID are compared with plain ==/str(): None == None passes, a receipt without a `did` key passes against a ledger row whose did is null (LedgerEntry.did is `string | null`, ledger.ts:18, so this is a real ledger state), and str() makes a missing receipt seat ('None') equal a ledger seat 'None' and an integer 1234 equal '1234'. The CLI's matchReceipt (imd-flop/src/match.ts:30-31) uses strict !== on seat and rejects a falsy DID outright, so the viewer and the CLI disagree on what 'matches' means and the viewer is the looser one. With a null-DID ledger row and a receipt lacking did, the row renders 'match matches' and, with chain_final, the green 'chain final' chip, i.e. green without the DID part of the identity ever being present. Fix: require job, seat and did to be non-empty strings on both sides and compare them strictly, mirroring match.ts.","line":24,"path":"viewer/build-lane.py","reproduction":"In Python after loading build-lane.py as test_lane_disagree.load_lane() does:\n  ok = ns['identity_ok']\n  ok({'request_id':'req-1','job':'j','seat':'1','did':None}, {'request_id':'req-1','job':'j','seat':'1','did':None})  -> True (expected False)\n  ok({'request_id':'req-1','job':'j','seat':'1'}, {'request_id':'req-1','job':'j','seat':'1','did':None})  -> True (expected False)\n  ok({'request_id':'req-1','job':None,'seat':'1','did':'d'}, {'request_id':'req-1','job':None,'seat':'1','did':'d'})  -> True (expected False)\n  ok({'request_id':'req-1','job':'j','seat':1234,'did':'d'}, {'request_id':'req-1','job':'j','seat':'1234','did':'d'})  -> True (CLI: mismatch 'seat')\n  ok({'request_id':'req-1','job':'j','did':'d'}, {'request_id':'req-1','job':'j','seat':'None','did':'d'})  -> True\n\nFull-row case. Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\nledger.jsonl:\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":null,\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"settled\",\"spend\":0.198}\nreq-1.json (no did key at all):\n{\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"state\":\"settled\",\"settled\":0.198,\"stage\":\"gateway observed\",\"chain_final\":true}\nExpected: red mismatch / IDENTITY flag (no DID on the receipt). Actual: `match matches` and `<span class=\"chip c-green\">chain final</span>` rendered.","severity":"medium","snippet":"            and r.get(\"job\") == e[\"job\"] and str(r.get(\"seat\")) == str(e[\"seat\"]) and r.get(\"did\") == e[\"did\"])","title":"identity_ok accepts a receipt with no DID (or no job) when the ledger row lacks it too, and coerces seat types; a row can go green with no DID binding"},{"citation":"resolved","description":"The green decision is `r.get(\"chain_final\")` truthy rather than `is True`. A receipt whose chain_final is the string \"false\", \"pending\", \"0\" or a non-empty object is treated as finalized. imd-flop writes a boolean today, so this needs a receipt from another writer or an edited file, but the viewer's own contract (line 37) is that receipt contents are untrusted. Line 87 has the mirror problem (`not r.get(\"chain_final\")`). Fix: `r.get(\"chain_final\") is True`.","line":85,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\nledger.jsonl:\n{\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"settled\",\"spend\":0.198}\nreq-1.json:\n{\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"state\":\"settled\",\"settled\":0.198,\"stage\":\"gateway observed\",\"chain_final\":\"false\",\"flag\":null}\nExpected: no green chip; 'chain final: not checked yet'. Actual: rows[0]['chips'] == [['grey','sent'],['amber','gateway observed'],['green','chain final']] and the row contains `<span class=\"chip c-green\">chain final</span>`.","severity":"low","snippet":"            if ok and r.get(\"chain_final\") and not row[\"bad\"]: chips.append([\"green\", \"chain final\"])","title":"chain_final is tested for truthiness, so the string \"false\" (or any non-empty string) renders the green 'chain final' chip"},{"citation":"resolved","description":"Every *.json in the receipts dir is loaded and later used as a dict (`r.get`, line 40/59), every ledger line is parsed with json.loads and indexed with e[\"job\"], e['seat'], e['provider'], e['ts'], and daily_left is computed with arithmetic on e[\"daily_cap\"] (line 48). A receipt that is a JSON list, a ledger line missing a key, a non-numeric daily_cap or an unparseable ts raises an uncaught exception before VIEWER is rewritten (main() only writes at line 127). The script exits with a traceback and the viewer keeps whatever lane was last committed, which may no longer reflect the ledger (a row that has since gone to mismatch keeps showing its old state). Fix: validate per file/line and render a red 'unreadable' row for the bad item instead of aborting.","line":36,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\nCase A: ledger.jsonl = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null} ; req-1.json = [1, 2]\n-> AttributeError: 'list' object has no attribute 'get' (expected: a flagged row, lane still rebuilt).\nCase B: ledger.jsonl = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":\"0.5\",\"spent_before\":0,\"state\":\"estimated\",\"spend\":null} ; req-1.json = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\n-> TypeError: unsupported operand type(s) for -: 'str' and 'float' at line 48; settlement-viewer-2026-11-09.html is left unchanged.","severity":"low","snippet":"        r = json.load(open(f))","title":"One malformed receipt or ledger value aborts the whole rebuild, so the previously committed lane (and any stale green) stays in the HTML"},{"citation":"resolved","description":"On identity mismatch the row shows only the ledger side (req/seat from `e`) plus a generic 'request_id, job, seat or DID' flag. The receipt's job/seat/did/request_id are never printed, so a reader cannot tell a foreign receipt from a typo, whereas the reference row in lane-examples.json shows 'receipt job ex-other… (row says ex-mismatch…)' and imd-flop's matchReceipt names the fields. Not a correctness defect in the green/red decision, but it weakens the viewer as an audit surface. Fix: list the differing fields and the receipt's values (escaped).","line":63,"path":"viewer/build-lane.py","reproduction":"Run: write the files below into an empty directory D, then `python3 viewer/build-lane.py D` (or exec from_files()/render() as test_lane_disagree.py does) and inspect the lane HTML between <!-- lane:start --> and <!-- lane:end -->.\nledger.jsonl: {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:zA\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"estimated\",\"spend\":null}\nreq-1.json: {\"request_id\":\"req-1\",\"job\":\"SOMEONE-ELSE\",\"seat\":\"1234\",\"did\":\"did:key:zOTHER\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}\nExpected: the row names job and did as the mismatching fields and shows SOMEONE-ELSE / did:key:zOTHER. Actual: neither string appears in the rendered row; only the generic flag text.","severity":"info","snippet":"                row[\"flags\"].append(\"receipt's request_id, job, seat or DID doesn't match this row\")","title":"Mismatch rows do not say which identity field disagreed or show the receipt's own identity, unlike the CLI and the hand-written example row"},{"citation":"resolved","description":"test_foreign_settled_receipt_beside_sent_row_shows_identity_mismatch_first (lines 75-76) and test_foreign_request_id_settled_receipt_beside_sent_row_is_red_identity_only (111-112) require exactly one green chip, 'linked', on a mismatch row, locking in the behaviour reported in the link-column finding and contradicting the legend text the suite also checks (line 136). Untested edges: a receipt differing in only did, only seat or only job (the identity test at 82-88 varies request_id only); receipt state reserved/estimated/null with chain_final true beside a settled ledger row; a string chain_final; a string `settled`; markup in any field; two ledger lines for one job. All of these currently pass through to a wrong or hidden label (see the other findings) and the suite is green.","line":75,"path":"viewer/test_lane_disagree.py","reproduction":"Run `python3 viewer/test_lane_disagree.py`: 7 tests pass. Then apply any of the inputs from the other findings (e.g. receipt settled=\"<style>.flag{display:none}</style>\", or receipt state reserved + chain_final true beside ledger state settled spend 0.198): still 7 passing tests, no assertion covers them. Expected: at least one failing test for each of those inputs.","severity":"info","snippet":"            self.assertEqual(html.count('class=\"chip c-green\"'), 1)","title":"Test suite asserts the green 'linked' chip on an identity-mismatch row and never exercises single-field mismatches, non-settled receipts with chain_final, or HTML escaping"}],"hash":"361672398dd8635e15b90136fb54953b6b538c792937aa800d0e3ba9993786bb","nodeId":"bb644f63-77d3-489d-ad91-d903156b5270","outcome":"completed","summary":"The review is complete. The findings file at `.imd-findings.json` holds nine substantiated findings, each with file and line, a quoted snippet verified against the tree, impact, and a runnable reproduction. No repository files were changed. No Foundry proofs apply, since the audited code is Python and HTML, not Solidity.\n\n**Answer to the two questions asked**\n\n- **Can a row show green without an exact identity match?** Yes, in three ways. The link-check column is green on every row, including identity-mismatch rows, because its only guard reads a field the ledger never contains. The identity check passes when both sides lack a DID or job, and coerces seat types, where the CLI's matcher rejects both. And the disagree check is skipped whenever the receipt's state is not settled or released, so a reserved receipt with chain_final set beside a settled ledger row renders green with \"matches\".\n- **Can the mismatch or disagree labels be hidden?** Yes. Receipt and ledger string values are written into the page unescaped. A `<style>` block in one receipt's `settled` field, or in a ledger `provider`, `payer` or `request_id`, hides every flag and red chip document-wide. Separately, rows are keyed by job name, so a later ledger line for the same job replaces the earlier request's row and its mismatch label disappears.\n\n**Findings by severity**\n\n| Severity | Location | Issue |\n|---|---|---|\n| high | build-lane.py:99 | Raw HTML interpolation of receipt and ledger fields; one value hides all labels |\n| medium | build-lane.py:45 | Link column green by default and on mismatch rows; guard is dead code |\n| medium | build-lane.py:71 | Disagree check gated on receipt state; contradictory receipt goes green |\n| medium | build-lane.py:33 | Per-job keying lets a later ledger line hide an earlier request's mismatch |\n| medium | build-lane.py:24 | identity_ok accepts null DID or job on both sides, coerces seat |\n| low | build-lane.py:85 | chain_final truthiness: the string \"false\" renders green |\n| low | build-lane.py:36 | One malformed file aborts the rebuild, leaving stale HTML in place |\n| info | build-lane.py:63 | Mismatch rows never name the differing field or the receipt's identity |\n| info | test_lane_disagree.py:75 | Suite asserts the green linked chip on a mismatch row; key edges untested |\n\n**Coverage and limits**\n\n- All three named files were read in full, plus lane-examples.json, the mock ledger and receipt, and the imd-flop ledger, receipt and matcher sources to establish which inputs are externally controlled. The imd-flop and spend-guard folders were used only as trust-boundary context and carry no findings.\n- The existing seven tests pass, and rebuilding the lane from the committed mock receipts reproduces the committed HTML byte for byte.\n- The injection finding is proven from the raw output HTML. I could not render it in a browser because the cached Chromium binaries fail to load a missing GTK accessibility library on this machine, and installing system libraries is outside what I can do here.\n- The high finding's precondition is stated plainly in the file: imd-flop's current writer filters `settled` to a number, so the receipt-side vector needs an older or foreign writer or a hand-placed file, while the ledger-side vectors need a crafted provider, payer or request_id.","treeHash":null,"usage":{"cachedInputTokens":868156,"cost":{"source":"runtime","usdMicros":3519033},"inputTokens":386,"model":"claude-fable-5-1","outputTokens":32962,"runtime":"claude","turns":21,"wallClockMs":516724}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0e78515c4d95885a","findings":[{"citation":"resolved","description":"from_files() collapses the ledger into one entry per job name (last line wins). The CLI keys attempts by request_id (imd-flop/src/ledger.ts latestAttempts) and explicitly sums several attempts per job (jobSpent), so a job can legitimately have a second request_id. When it does, the earlier attempt's row disappears from the lane, including a red identity-mismatch row, a red timeout 'spend unknown' row, or a reservation-held disagree row. The lane therefore can hide exactly the labels the legend promises are always visible, with no trace that a prior attempt existed. A later refused_local line (request_id null) for the same job also replaces a timeout_unknown attempt with an 'empty / refused before the call' row. Fix: key rows by request_id (with refused_local lines kept as their own rows), as the ledger module does, or render every attempt of a job.","line":33,"path":"viewer/build-lane.py","reproduction":"Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl, two lines: line 1 = {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-1\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0,\"state\":\"timeout_unknown\",\"reason\":\"TimeoutError: no answer; spend unknown, not retried\",\"spend\":null,\"cost_reported\":null,\"cap_charge\":0.198,\"http\":null} ; line 2 = {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-2\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0.198,\"state\":\"estimated\",\"reason\":null,\"spend\":null,\"cost_reported\":null,\"cap_charge\":0.198,\"http\":null} ; receipt req-2.json = {\"request_id\":\"req-2\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"state\":\"estimated\",\"reserved\":0.198,\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Expected: two rows for job j, one red 'unknown' row flagged 'timeout: spend unknown, not retried' for req-1 and one for req-2. Actual: one row (req-2, sent + gateway observed, not bad); the string 'req-1' and the timeout flag do not appear in the output. Same with line 1 state 'estimated' and a foreign receipt req-1.json (request_id 'req-OTHER'): the red mismatch row vanishes. Same with line 2 = a refused_local line for job j (request_id null, cap_charge 0): the timeout attempt is replaced by an 'empty' chip row.","severity":"medium","snippet":"            e = json.loads(line); last[e[\"job\"]] = e","title":"Rows are keyed by job, so a later attempt of the same job silently removes an earlier mismatch or timeout-unknown row"},{"citation":"resolved","description":"`or 0` turns a null spent_before (and a null cap_charge) into 0 before computing daily cap left. The CLI really writes spent_before: null on two refusal paths (imd-flop/src/run.ts refuse(..., null) when the estimate fails and when spentToday throws UnusableLedgerCharge, i.e. when a prior charge is unusable and today's spend is unknown). For those rows the lane prints 'daily cap left 0.5' (the whole cap) while the module docstring and legend promise that unknown spend is a blank slot, never 0. A reader sees a full cap remaining precisely when the CLI refused because spend could not be established. Fix: if spent_before or cap_charge is None, set daily_left to None (blank).","line":47,"path":"viewer/build-lane.py","reproduction":"Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl, one line: {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":null,\"quote_raw\":null,\"quote_padded\":null,\"spent_before\":null,\"state\":\"refused_local\",\"reason\":\"ledger charge unusable; today's spend unknown\",\"spend\":null,\"cost_reported\":null,\"cap_charge\":0,\"http\":null}. Expected: daily cap left rendered as the blank unknown slot. Actual: rows[0]['daily_left'] == 0.5 and the HTML contains '<span class=\"k\">daily cap left</span> <span class=\"mono\">0.5</span>'.","severity":"medium","snippet":"        spent = (e.get(\"spent_before\") or 0) + (e.get(\"cap_charge\") or 0)\n        row[\"daily_left\"] = r6(e[\"daily_cap\"] - spent) if e.get(\"daily_cap\") is not None else None","title":"Unknown spent_before is coerced to 0, so 'daily cap left' shows the full daily cap on a refusal whose spend is unknown"},{"citation":"resolved","description":"Every row is created with link=['green','linked'] and the only other branch reads e.get('link_bypassed'). LedgerEntry (imd-flop/src/ledger.ts) has no such field and run.ts never writes it to the ledger (link_bypassed exists only on the in-memory RunResult), so that branch is dead and the chip is green for every row: identity-mismatch rows, timeout rows, refused rows, and mock runs made with --no-link where no link was verified at all. The ledger also records none of the CLI matcher's link outcomes (link_missing, link_invalid, seat(link), did(link), did(revoked)), so a green 'linked' chip can appear for a seat whose link is revoked. This contradicts the legend's claim that a mismatch row 'can never go green'; test_lane_disagree.py even asserts the green chip is present on the mismatch row. Fix: render the link chip grey 'not checked' unless the ledger row carries an explicit link verification result, and have the CLI write that result (or link_bypassed) into the ledger line.","line":45,"path":"viewer/build-lane.py","reproduction":"Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl, one line: {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-1\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0,\"state\":\"sent\",\"reason\":null,\"spend\":null,\"cost_reported\":null,\"cap_charge\":0.198,\"http\":null} ; receipt req-1.json = {\"request_id\":\"req-OTHER\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"state\":\"settled\",\"reserved\":0.198,\"settled\":0.05,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}. Expected: a red mismatch row with no green chip, or a link chip that reflects a recorded link check. Actual: rows[0]['link'] == ['green','linked',None] and the row HTML contains '<span class=\"chip c-green\">linked</span>'. The same green chip renders for a refused_local row and for any ledger produced by `imd-flop run --provider mock --no-link`, whose ledger line has no link_bypassed key (grep -rn link_bypassed imd-flop/src: only run.ts RunResult and cli.ts output, never appendLedger).","severity":"medium","snippet":"                   outcome=[\"grey\", \"not read\", \"mock job, no imd lookup\"], link=[\"green\", \"linked\", None], bad=False)\n        if e.get(\"link_bypassed\"): row[\"link\"] = [\"grey\", \"bypassed (mock)\", None]","title":"Link check column is hardcoded green 'linked' from no data; the ledger never carries link_bypassed, so --no-link and mismatch rows still show a green chip"},{"citation":"resolved","description":"Only job and reason are passed through html.escape. request_id (shortened), seat, provider, network, ts, payer, and on line 99 any non-numeric receipt 'settled' value are interpolated raw. A seat value of '<!--' opens an HTML comment in the label cell that is never closed before the lane:end marker, so the red mismatch chip, the identity flag text and every following row are commented out; a receipt whose settled field is a string containing a <style> rule hides .flag and .c-red for the whole page. The CLI currently writes these fields from local identity/config and normalises settled to a number, so triggering this needs a ledger or receipt file whose contents differ from what the CLI writes (the viewer accepts any directory as argv[1]). The viewer's guarantee that mismatch labels are 'visible row text' therefore rests on file contents it does not validate. Fix: html.escape every interpolated value and render non-numeric settled as the blank unknown slot.","line":41,"path":"viewer/build-lane.py","reproduction":"Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl, one line: {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"<!--\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-1\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0,\"state\":\"sent\",\"reason\":null,\"spend\":null,\"cost_reported\":null,\"cap_charge\":0.198,\"http\":null} ; receipt req-1.json = {\"request_id\":\"req-OTHER\",\"job\":\"j\",\"seat\":\"<!--\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"state\":\"settled\",\"reserved\":0.198,\"settled\":0.05,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}. Expected: the label shows the seat text escaped and the red mismatch chip plus the flag 'receipt identity mismatch; chain-final green blocked' are visible. Actual: the rendered row contains '<small>seat #<!-- · node' with no '-->' anywhere after it, so a browser hides the mismatch chip, both flag lines and every later row. Second input: ledger state 'estimated', receipt req-1.json with state 'estimated' and \"settled\":\"<style>.flag,.c-red{display:none}</style>\": the style tag is emitted verbatim in the observed-spend line.","severity":"low","snippet":"        sub = [f\"req {short(e['request_id'] or 'none', 14)}\", f\"seat #{e['seat']} · node {BLANK_NODE}\",\n               f\"{e['provider']} · {e['network']} · {when(e['ts'])}\"]","title":"Unescaped ledger and receipt fields are written raw into the HTML, so an injected seat, request_id, payer or settled value can comment out or hide the mismatch and disagree labels"},{"citation":"resolved","description":"The CLI matcher (imd-flop/src/match.ts) requires strict equality on seat and treats a missing receipt DID as a mismatch ('did' is pushed when !receipt.did). The viewer accepts seat 1234 (number) against '1234', seat true against 'True', and did null against did null (the ledger type allows did: string | null). Line 85 then grants the green 'chain final' chip on any truthy chain_final, including the string 'false'. So a receipt with no DID at all, or with a different JSON type for seat, renders 'match matches' and can go green, while the CLI would report 'receipt identity mismatch; chain-final green blocked' for the same files. Fix: compare seat with == on the raw values (no str()), require r.get('did') to be a non-empty string equal to e['did'], and test r.get('chain_final') is True.","line":24,"path":"viewer/build-lane.py","reproduction":"python3 -c snippet: exec viewer/build-lane.py as test_lane_disagree.load_lane does, then: e={'job':'j','seat':'1234','did':'did:key:z','request_id':'req-1'}; identity_ok(dict(e,seat=1234),e) -> True (expected False); identity_ok(dict(e,seat=True),dict(e,seat='True')) -> True; identity_ok(dict(e,did=None),dict(e,did=None)) -> True (match.ts: mismatch). Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl = {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":null,\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-1\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0,\"state\":\"settled\",\"reason\":null,\"spend\":0.05,\"cost_reported\":null,\"cap_charge\":0.198,\"http\":null} ; receipt req-1.json = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":1234,\"did\":null,\"provider\":\"mock\",\"state\":\"settled\",\"reserved\":0.198,\"settled\":0.05,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}. Expected: red mismatch row. Actual: chips [['grey','sent'],['amber','gateway observed'],['green','chain final']], bad False, 'match matches'. With the original ledger (state settled, spend 0.05) and receipt \"chain_final\":\"false\" the same green 'chain final' chip appears.","severity":"low","snippet":"            and r.get(\"job\") == e[\"job\"] and str(r.get(\"seat\")) == str(e[\"seat\"]) and r.get(\"did\") == e[\"did\"])","title":"identity_ok is not an exact match: seat is compared after str() coercion and a null DID on both sides passes, unlike the CLI matcher; chain_final is a truthiness test"},{"citation":"resolved","description":"disagree only fires when the receipt is settled/released. If the ledger's latest line says settled with a known spend but the receipt file on disk is older (state estimated, settled null) and carries chain_final true, the row shows sent + gateway observed + green chain final, observed spend blank, and no 'receipt and ledger disagree' label even though the two sources disagree on both state and amount. The CLI writes the receipt before the ledger line, so this needs a receipt file that was replaced or edited after the run; the viewer has no check for it. Fix: also flag disagree when the ledger state is settled/released and the receipt state or settled amount differs.","line":71,"path":"viewer/build-lane.py","reproduction":"Run from the repository root: mkdir -p $TMPDIR/r && write the ledger.jsonl and receipt file(s) below into it, then `python3 viewer/build-lane.py $TMPDIR/r` and read the block between <!-- lane:start --> and <!-- lane:end --> in viewer/settlement-viewer-2026-11-09.html (restore it afterwards with git checkout). ledger.jsonl = {\"ts\":\"2026-11-10T00:00:00.000Z\",\"day\":\"2026-11-10\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"job\":\"j\",\"provider\":\"mock\",\"network\":\"devnet\",\"payer\":\"holder\",\"model\":\"m\",\"max_tokens\":8,\"prompt_sha256\":\"x\",\"per_call_cap\":0.25,\"daily_cap\":0.5,\"job_budget\":null,\"mock\":true,\"request_id\":\"req-1\",\"quote_raw\":0.078,\"quote_padded\":0.198,\"spent_before\":0,\"state\":\"settled\",\"reason\":null,\"spend\":0.05,\"cost_reported\":null,\"cap_charge\":0.05,\"http\":null} ; receipt req-1.json = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"state\":\"estimated\",\"reserved\":0.198,\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":true,\"flag\":null}. Expected: amber 'receipt and ledger disagree; reservation held, chain-final green blocked', no green chip. Actual: chips [['grey','sent'],['amber','gateway observed'],['green','chain final']] and flags [].","severity":"low","snippet":"            disagree = receipt_state in (\"settled\", \"released\") and (st == \"sent\" or e.get(\"spend\") != r.get(\"settled\"))","title":"Disagree check is one-directional: a ledger row already settled beside a receipt still 'estimated' with no settled amount renders green chain final and no disagree label"},{"citation":"resolved","description":"Ledger request ids are '<mock|imdflop>-<seat>-<16 hex>' (imd-flop/src/run.ts). short(id, 14) keeps the first 14 characters: for 'mock-1234-' that is 4 hex characters of the random part; for 'imdflop-1234567-...' it is zero, so every request of that seat renders as 'imdflop-123456…'. Combined with the job-keyed rows above, a reader cannot tell which attempt a row describes. Fix: show the tail of the id (or the full id in a title attribute).","line":20,"path":"viewer/build-lane.py","reproduction":"python3 -c \"s=lambda x,n=10: x if len(x)<=n+2 else x[:n]+'…'; print(s('mock-1234-5614aaaaaaaaaa',14), s('mock-1234-5614bbbbbbbbbb',14), s('imdflop-1234567-0123456789abcdef',14))\" -> 'mock-1234-5614… mock-1234-5614… imdflop-123456…'. Expected: distinguishable labels for distinct request ids.","severity":"info","snippet":"def short(s, n=10): return s if len(s) <= n + 2 else s[:n] + \"…\"","title":"Request id is truncated to 14 characters, which leaves 4 random hex characters for mock ids and none for a 7-digit seat, so distinct attempts look identical"},{"citation":"resolved","description":"test_identity_check_compares_request_id varies only request_id; the foreign-receipt tests change job, seat and DID together or request_id alone. No test checks a receipt that differs only in seat, only in DID, or only in job; none feeds seat 1234 as a number or did null; none asserts the positive path (matching settled receipt with chain_final true gets exactly one green 'chain final' chip and key_expired blocks it); none appends two attempts for one job; none passes spent_before null; none checks HTML escaping. Each of the findings above would have been caught by one of those cases.","line":85,"path":"viewer/test_lane_disagree.py","reproduction":"Add to test_identity_check_compares_request_id: self.assertFalse(ns['identity_ok']({'job':'j','seat':1,'did':'d','request_id':'req-1'}, e)) -> fails on the current code (identity_ok returns True because str(1) == '1'). Add a ledger with two lines for job 'j' (request ids req-1 timeout_unknown, req-2 estimated) and assert len(rows) == 2 -> fails (1 row).","severity":"info","snippet":"        self.assertTrue(ns[\"identity_ok\"]({\"job\": \"j\", \"seat\": \"1\", \"did\": \"d\", \"request_id\": \"req-1\"}, e))","title":"Tests never exercise a single-field identity mismatch, a seat/DID type difference, a matching receipt turning green, or the same job run twice"},{"citation":"resolved","description":"test_legend_uses_shared_string_and_names_request_id forbids the old 'job, seat or DID' wording in the legend and requires the shared IDENTITY string, but the reference row still renders the flag 'receipt doesn't match this row, never green' and a 'receipt job' line, so the example teaches a different label than real rows show. Fix: use the IDENTITY string and the same flop lines as from_files().","line":19,"path":"viewer/lane-examples.json","reproduction":"python3 viewer/build-lane.py (restore the HTML afterwards) and search the Reference rows block: the Example mismatch row contains 'receipt doesn't match this row, never green' and not 'receipt identity mismatch; chain-final green blocked'. Expected: the shared string.","severity":"info","snippet":"   \"chips\":[[\"grey\",\"sent\"],[\"red\",\"mismatch\"]],\"flop\":[[\"receipt job\",\"ex-other… (row says ex-mismatch…)\"],[\"observed spend\",null]],\"flags\":[\"receipt doesn't match this row, never green\"],","title":"Example mismatch row uses the retired flag wording and does not name request_id, unlike the legend the tests enforce"}],"hash":"5b9b9db79edd81da92e81e139a50acb8ff011689bd6ec9930df2dd1c6e783206","nodeId":"9f8714f8-1ebe-4258-a7fe-9084fb7280c2","outcome":"completed","summary":"The findings file is written and every quoted snippet was verified against the tree. The tracked files are untouched (the only untracked entries are the sandbox dotfiles). The existing viewer test suite passes on this commit, so none of these cases are covered by it.\n\n**Outcome:** 9 findings in `.imd-findings.json`, three medium, three low, three info. No critical or high, and no Solidity in scope, so no Foundry proofs were written.\n\n**Can a row go green without an exact identity match?** Yes, in three ways:\n\n- **Every row carries a green \"linked\" chip from no data.** The chip is hardcoded and the only alternate branch reads a `link_bypassed` key the CLI never writes to the ledger. Mismatch rows, refused rows and mock runs made with `--no-link` all show it. The project's own test asserts this green chip on the mismatch row.\n- **The identity check is not exact.** Seat is compared after string coercion and a null DID on both sides passes, while the CLI matcher treats both as mismatches. A receipt with no DID and a numeric seat renders \"matches\" and gets the green \"chain final\" chip. The green chip also fires on any truthy `chain_final`, including the string \"false\".\n- **The disagree check is one-directional.** A ledger line already settled beside a stale receipt still at \"estimated\" renders green chain final with no disagree label. This one needs a replaced receipt file, so it is rated low.\n\n**Can the mismatch or disagree labels be hidden?** Yes, in two ways:\n\n- **Rows are keyed by job name, last line wins.** The CLI explicitly supports several attempts per job and keys by request id. Running a job a second time removes the earlier row entirely, including a red mismatch row, a timeout \"spend unknown\" row, or a reservation-held row. A later cap refusal for the same job replaces a timeout row with an \"empty\" chip. This is the most consequential finding.\n- **Unescaped fields.** Seat, request id, payer, provider, network and any non-numeric receipt `settled` value are interpolated raw. A seat of `<!--` comments out the mismatch chip, both flag lines and every following row. Rated low because the CLI currently writes benign values for those fields, so it requires a ledger or receipt the CLI did not produce.\n\n**Math finding:** a null `spent_before` is coerced to 0, so \"daily cap left\" shows the full daily cap. The CLI genuinely writes null there on the estimate-failure and unusable-ledger-charge refusals, which contradicts the stated \"unknown is blank, never 0\" rule.\n\n**Info items:** request ids are truncated so distinct attempts look identical (zero random characters visible for a seven-digit seat), the tests never exercise single-field mismatches, type differences, the positive green path or a repeated job, and the example mismatch row still uses the retired flag wording.\n\nEach finding includes the exact ledger line and receipt JSON that triggers it, plus the expected versus actual rendering, so the author can replay them with `python3 viewer/build-lane.py <dir>` on a scratch directory.","treeHash":null,"usage":{"cachedInputTokens":527137,"cost":{"source":"runtime","usdMicros":3618174},"inputTokens":258,"model":"claude-fable-5-1","outputTokens":31456,"runtime":"claude","turns":15,"wallClockMs":418469}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a315ee4cb81c640c","findings":[{"citation":"resolved","description":"from_files() collapses ledger.jsonl with last[e['job']] = e. imd-flop keys attempts by request_id (imd-flop/src/ledger.ts:2-3 'The latest line for a request_id wins', latestAttempts at :49) and each run mints a fresh request_id, so running a job name twice, or a later cap refusal for the same job (refused_local, request_id null, run.ts:183), is normal CLI use. The earlier attempt then disappears from the lane completely: its red 'mismatch' chip and IDENTITY flag, its red 'unknown' chip and 'timeout: spend unknown, not retried' flag, or its amber DISAGREE label, plus the reservation it still holds. This is a way to hide exactly the labels the legend says are always shown. The receipt for the earlier request_id is still in the directory but nothing looks it up. Reported by all four specialists (math, flow, permissions, economics); merged here. Impact: a holder sees one clean or 'empty' row for a job whose earlier attempt has a foreign receipt or unknown spend that is still charged against the daily cap. Fix: key rows by request_id (keep each refused_local line, which has no request_id, as its own row) and render every attempt, or at least carry bad/flags forward from superseded unresolved attempts.","line":33,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Case 1: ledger [LED], receipt req-1.json = REC with job 'OTHER', did 'did:key:other', state 'settled', settled 0.05, chain_final true -> 1 row, IDENTITY flag present (correct). Append a second ledger line dict(LED, request_id=null, state='refused_local', cap_charge=0, quote_padded=0.9, reason='over cap', ts='2026-11-10T00:01:00.000Z'). Expected: the req-1 mismatch row is still shown. Actual (checked): rows=1, chips None (the 'empty' chip), 'receipt identity mismatch' absent from the HTML, 'req-1' appears nowhere. Case 2: ledger [dict(LED,state='timeout_unknown',reason='TimeoutError'), dict(LED,request_id='req-2',state='estimated',spent_before=0.198)], receipt req-2.json = REC with request_id 'req-2'. Expected: a red 'unknown' row with the timeout flag for req-1. Actual: rows=1, 'timeout' not in the HTML, bad False.","severity":"medium","snippet":"            e = json.loads(line); last[e[\"job\"]] = e","title":"Lane rows are keyed by job name, so a later ledger line for the same job removes an earlier attempt's identity-mismatch, timeout or disagree row"},{"citation":"resolved","description":"Every row starts with link=['green','linked',None]; the only downgrade, line 46, reads e.get('link_bypassed'), a key LedgerEntry (imd-flop/src/ledger.ts) does not have. link_bypassed exists only on the in-memory RunResult (run.ts:51, :298) and is never written by appendLedger, and none of the committed mock ledger lines has a link key. So the grey branch never runs, and the chip claims a link verification the viewer never saw. The mismatch branch (lines 60-69) leaves link alone, so a row with a red 'mismatch' chip also shows a green chip, which contradicts the legend generated at line 117 ('that row can never go green'). A mock run made with --no-link, which the CLI itself prints as LINK BYPASSED, also shows as green 'linked'. The CLI matcher treats link_missing, link_invalid and seat/did(link) as mismatches (match.ts:38-), but the viewer ignores link state entirely. test_lane_disagree.py:75-76 and :111-112 assert this green chip on a mismatch row, so the tests lock the behaviour in. Merged from math, flow, permissions and economics. Fix: default the chip to grey 'not checked'; show green only from a recorded link-verification field (the CLI would need to write one to the ledger), and never green on a bad row.","line":45,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Ledger [LED]; receipt req-1.json = REC with job 'OTHER', did 'did:key:other', state 'settled', settled 0.05, chain_final true. Expected: no green chip on this mismatch row. Actual (checked): rows[0]['bad'] is True, rows[0]['link'] == ['green','linked',None], and re.findall(r'c-green\">([^<]+)', html) == ['linked']. Also: every line of viewer/mock-receipts/ledger.jsonl has no key containing 'link', so the line-46 branch can never trigger from a CLI-written ledger.","severity":"medium","snippet":"                   outcome=[\"grey\", \"not read\", \"mock job, no imd lookup\"], link=[\"green\", \"linked\", None], bad=False)","title":"Link check column is hard-coded green 'linked' with no ledger evidence, so identity-mismatch rows and --no-link runs show a green chip"},{"citation":"resolved","description":"Only job (line 43) and reason (lines 52/56) pass through html.escape. request_id (via short(), which keeps 14 characters, enough for '<style>' or '<!--x'), seat, provider, network (lines 41-42), payer (line 95) and any non-numeric flop value (line 99, which includes the receipt's 'settled' from line 89) are inserted raw. Line 37 says receipt contents are untrusted. A request_id or seat of '<!--' opens a comment that closes only at '<!-- lane:end -->', so every later row and its red 'mismatch' chip and IDENTITY flag drop out of the rendered page. A request_id of '<style>' does the same: the lane never emits a closing '</style>'. A receipt whose settled field is '<style>.flag,.c-red{display:none}</style>' hides every flag and red chip on the page. The CLI writes request_id itself and normalises settled to number|null (receipts.ts:61), so triggering this needs a hand-edited, older or foreign ledger or receipt. The viewer accepts any directory as argv[1], and validating those files is the job it claims to do. Merged from math (low), flow (high), permissions (low) and economics (high + medium). Rated medium because it needs files the CLI would not write. Fix: html.escape every file-derived string in from_files() and render(), and render any non-numeric value in a numeric slot as BLANK.","line":41,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Case 1: ledger [dict(LED, request_id='<!--x'), dict(LED, job='k', request_id='req-2')], receipts '<!--x.json' = REC, 'req-2.json' = REC with request_id 'req-2' and job 'OTHER'. Expected: row k shows 'receipt identity mismatch; chain-final green blocked' as visible text. Actual (checked): the HTML contains 'req <!--x' raw. The IDENTITY string is present in the markup but missing from the visible text when the output goes through html.parser (it sits inside the comment). Same result with seat '<!--' on both ledger and receipt, and with request_id '<style>' (no receipt): the IDENTITY flag and the 'mismatch' chip are not visible text. Case 2: ledger [LED, dict(LED, job='k', request_id='req-2')], req-1.json = REC with settled '<style>.flag,.c-red{display:none}</style>', req-2.json foreign as above. Actual: the style element is emitted verbatim inside the 'observed spend' line and hides row k's flag and red chip page-wide.","severity":"medium","snippet":"        sub = [f\"req {short(e['request_id'] or 'none', 14)}\", f\"seat #{e['seat']} · node {BLANK_NODE}\",","title":"Ledger and receipt fields go into the page unescaped; a crafted request_id, seat or receipt 'settled' value hides every later identity-mismatch and disagree label"},{"citation":"resolved","description":"disagree is computed only when receipt_state is 'settled' or 'released'. For any other receipt state, the ledger state and spend are never compared. Line 85 then adds the green 'chain final' chip on identity_ok plus a truthy chain_final, and line 88 prints 'match matches'. Observed spend stays blank. Also, when both sides' amounts are null, e.get('spend') != r.get('settled') is None != None, which is False, so a 'settled' receipt with no amount beside an 'estimated' ledger row is not flagged either. The result is a green row although the ledger says the call never completed (state 'sent') or recorded a spend the receipt does not confirm. These are the states DISAGREE ('reservation held, chain-final green blocked') exists for. The CLI sets chain_final only when ctx.chainVerified === true (receipts.ts:64), and nothing verifies it yet, so this path is latent today. It goes live as soon as chain verification is wired in, and for any receipt from another writer. Merged from math, flow, permissions and economics. Fix: allow green only when the receipt state is settled/released, the settled amount is numeric and equal to the ledger spend, and the ledger state is settled/released. Otherwise mark the row DISAGREE.","line":71,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Case 1: ledger [dict(LED, state='settled', spend=0.198)], receipt REC with state 'reserved', chain_final true. Expected: amber DISAGREE, no green. Actual (checked): chips [['grey','sent'],['amber','gateway observed'],['green','chain final']], 'disagree' not in the HTML, 'matches' rendered. Case 2: ledger [LED] (state 'sent'), receipt REC with chain_final true: the same green chips, no disagree. Case 3: ledger [dict(LED, state='estimated')], receipt REC with state 'settled', settled null, chain_final true: the same green chips, no disagree.","severity":"medium","snippet":"            disagree = receipt_state in (\"settled\", \"released\") and (st == \"sent\" or e.get(\"spend\") != r.get(\"settled\"))","title":"Disagree check only runs for settled/released receipts; a reserved or estimated receipt with chain_final set turns a settled or still-'sent' ledger row green with 'matches'"},{"citation":"resolved","description":"request_id must be a non-empty string (line 23), but job and DID are compared with plain ==, and seat with str(r.get('seat')) == str(e['seat']). So the receipt seat 1234 (a number) matches the ledger seat '1234'. A receipt with no 'did' key, or did null, matches a ledger row whose did is null (LedgerEntry.did is 'string | null', ledger.ts:19). job None matches job None. The CLI's reference predicate (imd-flop/src/match.ts:29-31) compares seat with !== and rejects a falsy receipt DID outright, so the viewer is looser than the CLI it claims to mirror, and test_lane_disagree.py syncs only the IDENTITY string with match.ts, not the predicate. Impact: a row can show 'match matches' and the green 'chain final' chip without job, seat, DID and request_id being present and exactly equal, which is the core property under audit. It needs a receipt whose types differ from what the CLI writes, or a null-DID ledger row. Merged from math (low), flow (medium), permissions (low) and economics (medium). Fix: require job, seat and did to be non-empty strings on both sides and compare them without coercion.","line":24,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Unit (checked): identity_ok(dict(e, seat=1234), e) -> True, where e = {'job':'j','seat':'1234','did':'did:key:z','request_id':'req-1'}. identity_ok(dict(e, did=None), dict(e, did=None)) -> True. identity_ok(e without the 'did' key, dict(e, did=None)) -> True. identity_ok(dict(e, job=None), dict(e, job=None)) -> True. Expected False for each, per match.ts. Row: ledger [dict(LED, did=null, state='settled', spend=0.198)], receipt REC without the 'did' key, state 'settled', settled 0.198, chain_final true. Actual: chips end with ['green','chain final'], bad False. Same green result with ledger seat '1234' and receipt seat 1234 (a number).","severity":"medium","snippet":"            and r.get(\"job\") == e[\"job\"] and str(r.get(\"seat\")) == str(e[\"seat\"]) and r.get(\"did\") == e[\"did\"])","title":"identity_ok is not an exact match: seat is compared after str() coercion, and a missing or null DID or job on both sides passes, unlike the CLI's matchReceipt"},{"citation":"resolved","description":"Line 85 grants green on r.get('chain_final') truthy rather than 'is True', and line 87 uses the same truthiness to suppress the 'not checked yet' slot. The CLI writes a boolean, but a receipt from another writer or an edited file with chain_final 'false', 'pending' or 1 is shown as chain-final. Separate from the disagree finding: this one is about type validation, and the fix is different. Merged from math, flow and economics. Fix: use r.get('chain_final') is True on both lines.","line":85,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Ledger [dict(LED, state='settled', spend=0.198)], receipt REC with state 'settled', settled 0.198, chain_final \"false\" (a string). Expected: no green chip, and 'chain final: not checked yet' shown. Actual (checked): chips [['grey','sent'],['amber','gateway observed'],['green','chain final']], and 'not checked yet' does not appear.","severity":"low","snippet":"            if ok and r.get(\"chain_final\") and not row[\"bad\"]: chips.append([\"green\", \"chain final\"])","title":"Green 'chain final' tests chain_final for truthiness, so the string \"false\" (or any non-empty value) renders green"},{"citation":"resolved","description":"(e.get('spent_before') or 0) turns a null prior spend into 0. The CLI writes spent_before null on real refusal paths: refuse(..., null) when the estimate fails (run.ts:192, :194) and when spentToday throws UnusableLedgerCharge (run.ts:202), i.e. exactly when today's spend cannot be established. The lane then prints 'daily cap left 0.5', the full cap, while the module docstring (line 6) and the legend promise that unknown spend is a blank slot, never 0. Merged from math (medium) and economics (low). Fix: set daily_left to None when spent_before or cap_charge is None.","line":47,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Ledger [dict(LED, request_id=null, state='refused_local', spent_before=null, cap_charge=0, reason='ledger charge unusable')], no receipts. Expected: daily cap left rendered as BLANK. Actual (checked): rows[0]['daily_left'] == 0.5, and the HTML contains 'daily cap left</span> <span class=\"mono\">0.5'.","severity":"low","snippet":"        spent = (e.get(\"spent_before\") or 0) + (e.get(\"cap_charge\") or 0)","title":"Unknown spent_before is coerced to 0, so 'daily cap left' shows the whole daily cap on refusals made because today's spend is unknown"},{"citation":"resolved","description":"The red 'unknown' chip, the timeout flag and bad=True are set only in the 'elif r is None' branch. If a receipt named after the request_id exists, the ledger state is not consulted again except for st == 'sent' inside the disagree test. A timeout_unknown or error row beside a non-settled receipt renders grey 'sent' plus amber 'gateway observed', bad False, and the ledger's reason never appears. The page Notes ('A timeout or 404 shows as unknown and gets flagged') do not hold. With the current CLI, 'receipt --refresh' also rewrites the ledger state, so this needs an inconsistent receipts directory. Reported by permissions. Fix: when st is timeout_unknown or error, keep the red chip, the flag and bad=True regardless of the receipt, and treat a non-settled receipt beside it as DISAGREE.","line":53,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Ledger [dict(LED, state='timeout_unknown', reason='TimeoutError: no answer; spend unknown, not retried')], receipt REC. Expected: red 'unknown' chip, timeout flag, bad True. Actual (checked): chips [['grey','sent'],['amber','gateway observed']], bad False, no 'timeout' text. With state 'error' and reason 'HTTP 500': the same chips, bad False, 'HTTP 500' absent.","severity":"low","snippet":"        elif r is None:","title":"Ledger states timeout_unknown and error are dropped once any receipt file exists for the request_id"},{"citation":"resolved","description":"Every *.json in the receipts dir is loaded and used as a dict, and ledger values are used without validation (e['daily_cap'] arithmetic at line 48, e['ts'] parsing, e['seat'] indexing). Any receipt that is not an object, or a non-numeric daily_cap, raises before main() writes VIEWER (line 127). The script exits with a traceback and the viewer keeps its old lane, which may show a state, such as no mismatch, that the current files no longer support. Reported by flow. Fix: validate per file and per line, and render a red 'unreadable' row for the bad item instead of aborting.","line":36,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Run main() with ns['VIEWER'] pointed at a temp copy of settlement-viewer-2026-11-09.html. Case A: ledger [LED], req-1.json = [1, 2] -> AttributeError: 'list' object has no attribute 'get', copy unchanged (checked). Case B: ledger [dict(LED, daily_cap='0.5')], req-1.json = REC -> TypeError: unsupported operand type(s) for -: 'str' and 'float', copy unchanged (checked). Expected: the lane is rebuilt with the bad item flagged.","severity":"low","snippet":"        r = json.load(open(f))","title":"One malformed receipt or ledger value aborts the whole rebuild and leaves the previously committed lane in the HTML"},{"citation":"resolved","description":"short(id, 14) keeps 'mock-1234-' plus 4 hex characters for mock ids and no random characters at all for a 7-digit seat ('imdflop-1234567-...'). Together with the job-keyed collapse, a reader cannot tell which attempt a row describes. Fix: show the tail of the id, or the full id in a title attribute (escaped).","line":20,"path":"viewer/build-lane.py","reproduction":"Checked: short('mock-1234-5614aaaaaaaaaa',14), short('mock-1234-5614bbbbbbbbbb',14) and short('imdflop-1234567-0123456789abcdef',14) give 'mock-1234-5614…', 'mock-1234-5614…' and 'imdflop-123456…'. Expected: distinct labels for distinct ids.","severity":"info","snippet":"def short(s, n=10): return s if len(s) <= n + 2 else s[:n] + \"…\"","title":"Request id label truncated to 14 characters makes distinct attempts indistinguishable"},{"citation":"resolved","description":"On mismatch the row prints only the ledger side and a generic 'request_id, job, seat or DID' line. The receipt's job, seat and DID are never shown, whereas match.ts reports the failing fields and the hand-written example row shows 'receipt job ... (row says ...)'. This does not change the red/green decision, but it weakens the row as audit evidence. Reported by flow. Fix: list the differing field names and the receipt values (escaped).","line":63,"path":"viewer/build-lane.py","reproduction":"Reproduce with python3 -I: exec viewer/build-lane.py into a namespace with sys.argv=['b', D] (as test_lane_disagree.load_lane does), where D is a fresh temp dir holding ledger.jsonl and <request_id>.json receipts, then call from_files() and render() on each row. Base ledger line LED = {\"job\":\"j\",\"request_id\":\"req-1\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"provider\":\"mock\",\"network\":\"devnet\",\"ts\":\"2026-11-10T00:00:00.000Z\",\"payer\":\"holder\",\"quote_padded\":0.198,\"cap_charge\":0.198,\"per_call_cap\":0.25,\"daily_cap\":0.5,\"spent_before\":0,\"state\":\"sent\",\"spend\":null,\"reason\":null}; base receipt REC = {\"request_id\":\"req-1\",\"job\":\"j\",\"seat\":\"1234\",\"did\":\"did:key:z\",\"state\":\"estimated\",\"settled\":null,\"stage\":\"gateway observed\",\"chain_final\":false,\"flag\":null}. Ledger [LED], receipt REC with job 'SOMEONE-ELSE' and did 'did:key:zOTHER'. Expected: the row names job and did as the mismatching fields. Actual: neither 'SOMEONE-ELSE' nor 'did:key:zOTHER' appears in the rendered row, only the generic flag lines.","severity":"info","snippet":"                row[\"flags\"].append(\"receipt's request_id, job, seat or DID doesn't match this row\")","title":"Identity-mismatch rows do not say which field differed or show the receipt's values"},{"citation":"resolved","description":"Lines 75-76 and 111-112 require exactly one green chip ('linked') on an identity-mismatch row, which locks in the link-column defect, and these tests will fail once it is fixed. test_identity_check_compares_request_id varies only request_id. No test covers: a matching settled receipt with chain_final true (the only legitimate green path), a non-settled receipt with chain_final true, a non-boolean chain_final, seat type differences or null DID/job, two ledger lines for one job, markup in any field, a timeout row with a receipt, or a null spent_before. The suite passes (7 tests, checked) while every reproduction above shows wrong output. Merged from all four specialists. Fix: assert no c-green chip on any bad row, and add one failing test per finding above.","line":75,"path":"viewer/test_lane_disagree.py","reproduction":"python3 -I viewer/test_lane_disagree.py -> OK (7 tests). Adding self.assertFalse(ns['identity_ok']({'job':'j','seat':1,'did':'d','request_id':'req-1'}, e)) to test_identity_check_compares_request_id fails on this code (identity_ok returns True). A two-line ledger for job 'j' (req-1 timeout_unknown, req-2 estimated) with assertEqual(len(rows), 2) fails (1 row).","severity":"info","snippet":"            self.assertEqual(html.count('class=\"chip c-green\"'), 1)","title":"Test suite asserts a green 'linked' chip on mismatch rows and never covers the green path, single-field mismatches, re-runs, escaping or null spend"},{"citation":"resolved","description":"The example row's flag reads 'receipt doesn't match this row, never green', not IDENTITY ('receipt identity mismatch; chain-final green blocked'), which the legend and real rows use and test_legend_uses_shared_string_and_names_request_id enforces for the legend only. The reference row teaches a label that real rows never show. Reported by math. Fix: use the IDENTITY string in the example row.","line":19,"path":"viewer/lane-examples.json","reproduction":"Run build-lane.py on a temp copy of the viewer, or read the committed settlement-viewer-2026-11-09.html line 152: the Example mismatch row contains 'receipt doesn't match this row, never green' and not 'receipt identity mismatch; chain-final green blocked'.","severity":"info","snippet":"   \"chips\":[[\"grey\",\"sent\"],[\"red\",\"mismatch\"]],\"flop\":[[\"receipt job\",\"ex-other… (row says ex-mismatch…)\"],[\"observed spend\",null]],\"flags\":[\"receipt doesn't match this row, never green\"],","title":"Reference 'Example mismatch' row still uses the retired flag wording instead of the shared IDENTITY string"}],"hash":"dfe45254dbf8bf5ba3a99b575647151241bce274a34866384cad2cef325170df","nodeId":"04fc5b88-548e-42e1-aa3f-4490f7aafc32","outcome":"completed","summary":"I've written 13 findings to `.imd-findings.json`, after reproducing every one against commit 288a03e. The answer to the audit's question is no: a row can show green without the receipt's job, seat, DID and request_id exactly matching the ledger row, and the identity-mismatch and disagree labels can be hidden. The specialists' 34 findings merged into these 13, and none of them failed to reproduce. The repository is unchanged. The existing test suite still passes (7 tests), even though every reproduction below produces wrong output.\n\n**Medium**\n1. **Rows are keyed by job name** (`viewer/build-lane.py:33`). Running a job again, or a later cap refusal for the same job, wipes out the earlier attempt's row. That row's red mismatch, red timeout or amber disagree label disappears, even though the attempt is still charged against the daily cap. This happens in normal use of the command-line tool. All four specialists reported it.\n2. **The Link check column is always green \"linked\"** (`build-lane.py:45`). The only check that could turn it grey reads a `link_bypassed` field that the ledger never contains. So every identity-mismatch row still shows a green chip, which contradicts the page's own \"that row can never go green\".\n3. **Unescaped fields** (`build-lane.py:41`, also lines 95 and 99). A `request_id` or seat of `<!--` or `<style>`, or a receipt `settled` value carrying a `<style>` rule, hides the mismatch label and red chip on every later row. An HTML parse confirmed the label text is no longer visible. It needs a hand-edited or foreign file; the CLI wouldn't write these values.\n4. **The disagree check only runs for settled or released receipts** (`build-lane.py:71`). A reserved or estimated receipt with `chain_final` true turns a settled or still-sent ledger row green, showing \"matches\" and no disagree label. This is latent today, because the CLI always writes `chain_final` false.\n5. **The identity check is looser than an exact match** (`build-lane.py:24`). Seat `1234` (a number) matches `\"1234\"`, and a missing DID matches a null DID. Either way the row can go green. The CLI's own matcher (`match.ts:29-31`) rejects both cases.\n\n**Low**\n- `chain_final` is tested for truthiness, so the string `\"false\"` renders green (line 85).\n- When today's prior spend is unknown, \"daily cap left\" shows the full cap instead of a blank (line 47). The CLI does write this case on real refusal paths.\n- A timeout or error ledger state is ignored once any receipt file exists for that request (line 53).\n- One malformed receipt or ledger value stops the rebuild, leaving the old lane in the page (line 36).\n\n**Info**\n- Request ids are cut to 14 characters, so different attempts look the same (line 20).\n- Mismatch rows don't say which field differed (line 63).\n- `test_lane_disagree.py:75-76` and `:111-112` require the green \"linked\" chip on mismatch rows. The suite also never tests the green path, single-field mismatches, re-runs or escaping.\n- The example mismatch row in `lane-examples.json:19` still uses the old flag wording.\n\nNo finding has a Foundry proof attached: the audited code is Python and HTML, and none of the specialists supplied a proof to run. Each finding's reproduction gives the exact ledger and receipt inputs, with expected and actual output.","treeHash":null,"usage":{"cachedInputTokens":420625,"cost":{"source":"runtime","usdMicros":1028079},"inputTokens":12,"model":"claude-opus-5-5","outputTokens":15624,"runtime":"claude","turns":6,"wallClockMs":140205}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bc39660800b5f882","findings":[{"citation":"resolved","description":"from_files() collapses ledger.jsonl with last[e[\"job\"]] = e, but imd-flop's ledger is keyed per request_id (ledger.ts: 'The latest line for a request_id wins', latestAttempts()). A job name is reused whenever the same imd job is attempted again: a cap refusal (request_id null, state refused_local) or a successful second call (new request_id) appends a new line for the same job and the viewer drops the earlier attempt entirely. If that earlier attempt's receipt had a foreign request_id/job/seat/DID, the red 'mismatch' chip, the IDENTITY row note and the held reservation (0.186) disappear from the page; the lane shows only the refused or clean later attempt. The legend's promise that a mismatching row 'can never go green' is defeated by simply making the row vanish. Expected: one row per attempt (per request_id, as the ledger defines it), or at least a persistent mismatch marker on the job. Actual: the mismatch row is replaced. Fix: key `last` by request_id (fall back to job only for refused_local lines that have none) and render every attempt, or carry forward bad=True/flags from superseded attempts of the same job.","line":33,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\nmismatch_receipt = dict(REC, job=\"OTHER\", seat=\"9\", did=\"did:key:other\", state=\"settled\", settled=0.05, chain_final=True)\n# 1) alone, the mismatch is shown\nrows, html = build([LED], {\"req-1\": mismatch_receipt})\nassert \"receipt identity mismatch; chain-final green blocked\" in html and rows[0][\"bad\"]\n# 2) the same job is attempted again and refused locally (request_id null) -> mismatch row gone\nrefused = dict(LED, request_id=None, state=\"refused_local\", cap_charge=0, spent_before=0.186, quote_padded=0.9,\n               reason=\"padded quote 0.9 FLOP is over the remaining daily cap 0.814\", ts=\"2026-11-10T00:01:00.000Z\")\nrows, html = build([LED, refused], {\"req-1\": mismatch_receipt})\nprint(\"rows:\", len(rows), \"| mismatch label present:\", \"receipt identity mismatch\" in html, \"| req-1 shown:\", \"req-1\" in html)\n# actual: rows: 1 | mismatch label present: False | req-1 shown: False   (expected: True / True)\n# 3) a successful re-run with a fresh request_id also hides it\nrerun = dict(LED, request_id=\"req-2\", state=\"estimated\", spent_before=0.186, ts=\"2026-11-10T00:01:00.000Z\")\nrows, html = build([LED, rerun], {\"req-1\": mismatch_receipt, \"req-2\": dict(REC, request_id=\"req-2\")})\nprint(\"rows:\", len(rows), \"| mismatch label present:\", \"receipt identity mismatch\" in html, \"| bad:\", rows[0][\"bad\"])\n# actual: rows: 1 | mismatch label present: False | bad: False","severity":"high","snippet":"            e = json.loads(line); last[e[\"job\"]] = e","title":"Lane keys rows by job name, so any later ledger line for the same job hides an earlier attempt's identity-mismatch row and its reservation"},{"citation":"resolved","description":"render() only wraps v in num() when it is None/int/float; any other type (a string) from the receipt's 'settled' field (build-lane.py:89, row['flop'].append(['observed spend', r.get('settled')])) is concatenated into the HTML verbatim. The viewer's docstring and legend say file contents are checked and that mismatch rows 'can never go green', yet a receipt whose identity matches its own ledger row and whose settled field is the string '<style>.flag,.c-red,.c-amber{display:none}</style><span class=\"chip c-green\">chain final</span>' (a) injects a global stylesheet that hides the identity-mismatch and disagree labels of every other row on the page, and (b) renders a green 'chain final' chip for itself although chain_final is false. imd-flop currently writes settled as number|null, so the precondition is a corrupted, hand-edited or third-party-written receipt in the receipts directory, which is exactly the trust boundary this viewer claims to check. Same root cause for other receipt-derived strings reaching the page (e.g. r['flag'] is compared, not rendered, but r['settled'] is). Fix: html.escape() every file-derived value in render()/from_files(), and only accept int/float for observed spend (anything else renders BLANK).","line":99,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\ninj = '<style>.flag,.c-red,.c-amber{display:none}</style><span class=\"chip c-green\">chain final</span>'\nrows, html = build([LED, dict(LED, job=\"k\", request_id=\"req-2\")],\n                   {\"req-1\": dict(REC, settled=inj),  # identity matches row j\n                    \"req-2\": dict(REC, request_id=\"req-2\", job=\"OTHER\", state=\"settled\", settled=0.05, chain_final=True)})  # mismatch row k\nprint(\"raw <style> in page:\", \"<style>\" in html, \"| fake green chain-final chip:\", 'c-green\">chain final' in html)\n# actual: True | True.  Expected: False | False (observed spend should be a blank slot; strings escaped)\n# Loading the page: row k's red 'mismatch' chip and 'receipt identity mismatch; chain-final green blocked' note are display:none.\n# Full page: copy settlement-viewer-2026-11-09.html to a temp file, set ns[\"VIEWER\"] to it, call ns[\"main\"]() with the same dir: the file then contains two <style> tags.","severity":"high","snippet":"        v = num(v) if (v is None or isinstance(v, (int, float))) else v","title":"Receipt 'settled' value is inserted into the page unescaped, so one receipt file can hide every red/amber label on the page and fake a green chip"},{"citation":"resolved","description":"The disagree check is one-directional: it only runs when the receipt state is 'settled' or 'released'. A receipt in state 'estimated' or 'reserved' with chain_final true, beside a ledger line whose state is still 'sent' (no completion recorded) or 'settled' with a known spend, is not flagged. Control falls through to line 85 where the only requirements for the green chip are identity_ok, a truthy chain_final and no key_expired flag, so the row shows grey sent, amber gateway observed and green chain final with 'observed spend' blank and no DISAGREE label, while the ledger says the call never completed (or recorded a spend the receipt does not confirm). test_lane_disagree.py's own premise ('A settled receipt beside a ledger row still at sent must keep the reservation label') is enforced only for the settled/released case. Expected: amber DISAGREE chip, reservation held, no green. Actual: green chain final. Fix: treat chain_final true with receipt state not in (settled, released), or with ledger state 'sent'/'timeout_unknown'/'error', or with ledger spend known but receipt settled unknown, as a disagreement.","line":71,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\n# A: ledger still 'sent', receipt estimated but chain_final true\nrows, html = build([LED], {\"req-1\": dict(REC, chain_final=True)})\nprint(rows[0][\"chips\"], \"| disagree label:\", \"disagree\" in html)\n# actual: [['grey','sent'],['amber','gateway observed'],['green','chain final']] | disagree label: False\n# A2: ledger says settled 0.05, receipt says only 'reserved' yet chain_final true\nrows, html = build([dict(LED, state=\"settled\", spend=0.05)], {\"req-1\": dict(REC, state=\"reserved\", chain_final=True)})\nprint(rows[0][\"chips\"], \"| disagree label:\", \"disagree\" in html)\n# actual: green 'chain final' again, no disagree. Expected both: amber DISAGREE, no green.","severity":"medium","snippet":"            disagree = receipt_state in (\"settled\", \"released\") and (st == \"sent\" or e.get(\"spend\") != r.get(\"settled\"))","title":"Green 'chain final' is granted on a receipt that is not settled/released beside a ledger row still at 'sent'; the disagree gate only fires for settled/released receipts"},{"citation":"resolved","description":"Every row starts with link=['green','linked',None] and is only changed if e.get('link_bypassed') is truthy. imd-flop's LedgerEntry (imd-flop/src/ledger.ts:14-39) has no link_bypassed or link-result field; link_bypassed exists only on the in-memory RunResult (run.ts:51). The mock ledger lines in viewer/mock-receipts/ledger.jsonl confirm no such key. So a run made with --no-link, or whose link verification failed/was never performed, is shown as green 'linked' with no evidence; the grey 'bypassed (mock)' branch is dead code against the real ledger. The same default also puts a green chip on identity-mismatch rows, contradicting the legend's 'that row can never go green', and test_lane_disagree.py:75-76 enshrines exactly one green chip ('linked') on a mismatch row. Expected: grey 'not checked' unless the ledger (or a receipt field) records a verified link. Actual: green. Fix: default to grey 'not checked' and render green only from a recorded link verification result; if link status must be shown, have imd-flop record it in the ledger (outside this audit's write scope) and read that field.","line":45,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\n# the committed mock ledger has no link field at all\nfor line in (ROOT / \"mock-receipts\" / \"ledger.jsonl\").read_text().splitlines():\n    assert \"link\" not in json.loads(line), line\n# an identity-mismatch row (and any --no-link row, which is indistinguishable in the ledger) still shows a green chip\nrows, html = build([LED], {\"req-1\": dict(REC, job=\"OTHER\", state=\"settled\", settled=0.05, chain_final=True)})\nprint(\"bad:\", rows[0][\"bad\"], \"| link:\", rows[0][\"link\"], \"| green chips:\", html.count('class=\"chip c-green\"'), \"| text:\", 'c-green\">linked' in html)\n# actual: bad: True | link: ['green', 'linked', None] | green chips: 1 | text: True.  Expected: no green chip on this row.\n# grep -c link_bypassed ../imd-flop/src/ledger.ts  -> 0 ; the grey 'bypassed (mock)' branch (line 46) can never trigger from a real ledger.","severity":"medium","snippet":"                   outcome=[\"grey\", \"not read\", \"mock job, no imd lookup\"], link=[\"green\", \"linked\", None], bad=False)","title":"Link check column is hard-coded green 'linked' for every receipt row; the ledger has no link field, so bypassed (--no-link) or failed link checks render green, including on identity-mismatch rows"},{"citation":"resolved","description":"Only the job label (line 43) and reason (line 52/56) pass through html.escape(). request_id (via short(), 14 chars kept), seat, provider, network, ts-derived text and payer (line 95) go into the markup raw. A ledger request_id of '<!--x' opens an HTML comment that the page only closes at '<!-- lane:end -->' (settlement-viewer-2026-11-09.html:158), so every subsequent receipt row and the whole 'Reference rows' section are swallowed, including any row carrying the red 'mismatch' chip and IDENTITY note. Reachability: imd-flop generates request_id locally (run.ts:196) and payer comes from its config file, so this needs a tampered ledger/config or a different ledger writer; it is the same missing-escape root cause as the receipt 'settled' finding but on the ledger side and with a different effect (whole rows hidden rather than restyled). Expected: a literal 'req <!--x…' label. Actual: all following rows hidden. Fix: html.escape() every ledger-derived string in from_files()/render().","line":41,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\nimport shutil\ntmp = tempfile.mkdtemp()\nPath(tmp, \"ledger.jsonl\").write_text(json.dumps(dict(LED, request_id=\"<!--x\")) + \"\\n\" + json.dumps(dict(LED, job=\"k\", request_id=\"req-2\")) + \"\\n\")\nPath(tmp, \"<!--x.json\").write_text(json.dumps(dict(REC, request_id=\"<!--x\")))\nPath(tmp, \"req-2.json\").write_text(json.dumps(dict(REC, request_id=\"req-2\", job=\"OTHER\", state=\"settled\", settled=0.05, chain_final=True)))\nv = Path(tmp, \"viewer.html\"); shutil.copy(ROOT / \"settlement-viewer-2026-11-09.html\", v)\nns = lane(tmp); ns[\"VIEWER\"] = str(v); ns[\"main\"]()\nout = v.read_text(); i = out.index(\"req <!--x\"); j = out.index(\"-->\", i)\nprint(\"comment closes at:\", repr(out[j-16:j+3]), \"| row k's mismatch note is inside the comment:\", i < out.index(\"receipt identity mismatch; chain-final green blocked</div>\") < j)\n# actual: comment closes at: '>\\n<!-- lane:end -->' | True.  Expected: request_id rendered as text, row k visible.","severity":"medium","snippet":"        sub = [f\"req {short(e['request_id'] or 'none', 14)}\", f\"seat #{e['seat']} · node {BLANK_NODE}\",","title":"Ledger-derived request_id, seat, provider, network and payer are rendered unescaped; a request_id beginning with '<!--' comments out every following lane row, including mismatch rows"},{"citation":"resolved","description":"The ledger schema allows did: string | null (imd-flop/src/ledger.ts:18). identity_ok compares r.get('did') == e['did'], so a ledger row with did null and a receipt with no 'did' key (or did null) passes the DID leg of the identity check and the row can proceed to amber/green. The CLI's reference check (imd-flop/src/match.ts:31) is `if (!receipt.did || receipt.did !== row.did) fields.push(\"did\")`, i.e. a missing DID is an identity failure; it also compares seat with !== (match.ts:30) whereas the viewer accepts 1234 (number) against '1234'. test_lane_disagree.py syncs only the IDENTITY string with match.ts, not the predicate, so the two 'identity-first' checks disagree on what an identity match is. Expected (per the brief and match.ts): a row is not identity-ok unless job, seat, DID and request_id are present and exactly equal. Actual: null DID == null DID is accepted. Fix: require isinstance(did, str) and did != '' on both sides (same for job/seat), and compare seat without str() casting or normalise on the writer side only.","line":24,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\nns = lane(str(ROOT / \"mock-receipts\"))\ne = {\"request_id\": \"r\", \"job\": \"j\", \"seat\": \"1\", \"did\": None}\nprint(\"null DID both sides:\", ns[\"identity_ok\"]({\"request_id\": \"r\", \"job\": \"j\", \"seat\": \"1\"}, e))          # actual True, expected False\nprint(\"int seat vs str seat:\", ns[\"identity_ok\"]({\"request_id\": \"r\", \"job\": \"j\", \"seat\": 1, \"did\": \"d\"}, dict(e, did=\"d\")))  # actual True; match.ts: mismatch\n# end-to-end: ledger did null + receipt without did reaches the green path\nrows, html = build([dict(LED, did=None)], {\"req-1\": {k: v for k, v in dict(REC, chain_final=True).items() if k != \"did\"}})\nprint(rows[0][\"chips\"], \"| bad:\", rows[0][\"bad\"])   # actual: [..., ['green', 'chain final']] | bad: False","severity":"medium","snippet":"            and r.get(\"job\") == e[\"job\"] and str(r.get(\"seat\")) == str(e[\"seat\"]) and r.get(\"did\") == e[\"did\"])","title":"identity_ok treats a null DID on both sides as a match and str()-casts seat, diverging from imd-flop's match.ts which rejects a missing DID and compares seat strictly"},{"citation":"resolved","description":"imd-flop declares chain_final: boolean, but the viewer accepts any truthy JSON value. A receipt with \"chain_final\": \"false\" or \"chain_final\": \"pending\" (e.g. a future writer or a hand-edited file) renders the green 'chain final' chip and suppresses the 'not checked yet' slot (line 87 also uses truthiness). Expected: green only on JSON true. Actual: green on any truthy value. Fix: `r.get(\"chain_final\") is True`.","line":85,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\nrows, html = build([LED], {\"req-1\": dict(REC, chain_final=\"false\")})\nprint(rows[0][\"chips\"], \"| 'not checked yet' slot shown:\", \"not checked yet\" in html)\n# actual: [['grey','sent'],['amber','gateway observed'],['green','chain final']] | False.  Expected: no green, slot shown.","severity":"low","snippet":"            if ok and r.get(\"chain_final\") and not row[\"bad\"]: chips.append([\"green\", \"chain final\"])","title":"Green 'chain final' gate tests chain_final for truthiness, so any non-boolean non-empty value (the string \"false\", 1, \"pending\") turns the row green"},{"citation":"resolved","description":"LedgerEntry.spent_before is number | null (imd-flop/src/ledger.ts:32). `(e.get(\"spent_before\") or 0)` turns an unknown prior spend into 0, so the Spend column shows a definite 'daily cap left' figure derived from unknown data (daily_cap 1, cap_charge 0.186, spent_before null -> 0.814). The module docstring (line 6) and the page legend promise a blank slot for unknown spend. Expected: blank slot when spent_before is null. Actual: 0.814. Fix: set daily_left to None when spent_before or cap_charge is None.","line":47,"path":"viewer/build-lane.py","reproduction":"# run from any empty dir: python3 -I repro.py /path/to/repo\nimport json, sys, tempfile\nfrom pathlib import Path\nROOT = Path(sys.argv[1]) / \"viewer\"\ndef lane(rdir):\n    src = (ROOT / \"build-lane.py\").read_text()\n    ns = {\"__name__\": \"lane_under_test\", \"__file__\": str(ROOT / \"build-lane.py\")}\n    old = sys.argv; sys.argv = [\"build-lane.py\", rdir]\n    try: exec(compile(src, str(ROOT / \"build-lane.py\"), \"exec\"), ns)\n    finally: sys.argv = old\n    return ns\nLED = {\"job\": \"j\", \"request_id\": \"req-1\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"provider\": \"mock\", \"network\": \"devnet\",\n       \"ts\": \"2026-11-10T00:00:00.000Z\", \"payer\": \"holder\", \"quote_padded\": 0.186, \"cap_charge\": 0.186, \"per_call_cap\": 0.25,\n       \"daily_cap\": 1, \"spent_before\": 0, \"state\": \"sent\", \"spend\": None, \"reason\": None}\nREC = {\"request_id\": \"req-1\", \"job\": \"j\", \"seat\": \"1234\", \"did\": \"did:key:z\", \"state\": \"estimated\", \"settled\": None,\n       \"stage\": \"gateway observed\", \"chain_final\": False, \"flag\": None}\ndef build(ledgers, receipts):\n    tmp = tempfile.mkdtemp()\n    Path(tmp, \"ledger.jsonl\").write_text(\"\".join(json.dumps(l) + \"\\n\" for l in ledgers))\n    for fn, r in receipts.items(): Path(tmp, fn + \".json\").write_text(json.dumps(r))\n    ns = lane(tmp); rows = ns[\"from_files\"](); return rows, \"\".join(ns[\"render\"](r) for r in rows)\n\nrows, html = build([dict(LED, spent_before=None)], {\"req-1\": REC})\nprint(\"daily_left:\", rows[0][\"daily_left\"], \"| rendered:\", 'daily cap left</span> <span class=\"mono\">0.814' in html)\n# actual: daily_left: 0.814 | rendered: True.  Expected: None and a blank slot.","severity":"low","snippet":"        spent = (e.get(\"spent_before\") or 0) + (e.get(\"cap_charge\") or 0)","title":"Unknown spent_before (null) is coerced to 0 when computing 'daily cap left', contradicting 'unknown spend renders as an empty slot, never 0'"},{"citation":"resolved","description":"The suite locks in the hard-coded green 'linked' chip on a mismatch row (lines 75-76 and 111-112), so fixing the link-column default will require changing these assertions. No test covers: a matching receipt with chain_final true (the only path that can produce the green 'chain final' chip), a non-settled receipt with chain_final true beside a 'sent' ledger row, non-boolean chain_final, string/HTML content in receipt or ledger fields, two ledger lines for the same job, null DID/seat on both sides, or null spent_before. The CLI-parity test (line 116) compares only the IDENTITY string, not the identity predicate. These are the edges where the findings above live.","line":75,"path":"viewer/test_lane_disagree.py","reproduction":"python3 -I viewer/test_lane_disagree.py passes (7 tests) on this commit while each reproduction in the other findings prints the defective output; grep -n 'chain_final\": True\\|chain_final=True' viewer/test_lane_disagree.py shows chain_final true only on receipts that fail identity, never on a matching receipt.","severity":"info","snippet":"            self.assertEqual(html.count('class=\"chip c-green\"'), 1)","title":"test_lane_disagree.py asserts a green chip on an identity-mismatch row and never exercises the green chain-final path, escaping, or re-run superseding"}],"hash":"f72a059cc303c1df707bbc96b7a6eb93e03eccd3441e9a8b5bb05051176d3633","nodeId":"567f9cb1-9aa5-42d2-a009-bd2f6a3de91a","outcome":"completed","summary":"The review is complete. All nine findings are written to `.imd-findings.json` in the repository root, every snippet was verified against the tree, and every stored reproduction script was executed as-is and printed the defective output it describes. No repository files were changed.\n\n**Answer to the brief's two questions**\n\n- **Can a row show green without an exact identity match?** The green \"chain final\" chip itself is gated behind `identity_ok`, but that gate is weaker than the CLI's: a null DID on both sides passes and seat is compared after string casting, where imd-flop's `match.ts` rejects a missing DID and compares seat strictly. Separately, every receipt row carries a hard-coded green \"linked\" chip regardless of identity, because the ledger has no link field at all, so the grey \"bypassed\" branch is dead code and a mismatch row still shows green.\n- **Can the mismatch or disagree labels be hidden or reordered?** Yes, by three mechanisms. The lane keys rows by job name while the ledger is keyed by request_id, so re-running the same job name (even a local cap refusal) silently replaces and removes an earlier attempt's mismatch row and its held reservation. A receipt whose `settled` field is a string is pasted into the page raw, so one file can inject a stylesheet that hides every red and amber label on the page and fake a green chip. A ledger request_id starting with `<!--` comments out every following row up to the lane end marker.\n\n**Findings by severity** (all in `viewer/build-lane.py` unless noted)\n\n| Severity | Line | Issue |\n|---|---|---|\n| high | 33 | Per-job collapsing hides an earlier attempt's identity-mismatch row on re-run |\n| high | 99 | Receipt `settled` string rendered unescaped; page-wide label hiding, fake green chip |\n| medium | 71 | Disagree gate only fires for settled/released receipts; unsettled receipt with chain_final true beside a \"sent\" ledger row goes green |\n| medium | 45 | Link column defaults to green with no ledger evidence; bypassed or failed link checks render green |\n| medium | 41 | Ledger request_id/seat/provider/network/payer unescaped; `<!--` swallows subsequent rows |\n| medium | 24 | `identity_ok` accepts null DID on both sides and str-casts seat, diverging from `match.ts` |\n| low | 85 | `chain_final` tested for truthiness; the string \"false\" turns the row green |\n| low | 47 | Null `spent_before` coerced to 0 in \"daily cap left\", against the stated never-0 rule |\n| info | test file:75 | Suite enshrines a green chip on a mismatch row and never exercises the green path, escaping, or re-run superseding |\n\n**Scope and limits.** I covered build-lane.py, the committed viewer HTML, test_lane_disagree.py, lane-examples.json and the mock receipts in depth, and read imd-flop's ledger, receipt, run and match sources only to establish field provenance and the CLI's identity predicate. The existing 7-test suite passes on this commit. The reproductions are Python scripts embedded in each finding; no Solidity is in scope so no Foundry proofs were written. Two findings (unescaped receipt and ledger strings) depend on a tampered or third-party-written file in the receipts directory, which I state explicitly in each; imd-flop's own writer currently emits numeric `settled` and locally generated request_ids. The hidden-row-on-rerun finding needs only ordinary imd-flop usage.","treeHash":null,"usage":{"cachedInputTokens":436625,"cost":{"source":"runtime","usdMicros":3003039},"inputTokens":226,"model":"claude-fable-5-1","outputTokens":28559,"runtime":"claude","turns":15,"wallClockMs":450446}}],"verification":[]}