{"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":"351b0573-d704-49cb-8af4-76843a8afa3c","kind":"audit","nodes":[{"acceptedSubmissionHash":"a8cbf0e06a48fa6ffc665019b4eb7b9a354f8f997915b69b857787201abf092e","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":"a0657d1cdb7baaa966f3d3331bfe571dbe8b9ef84ca42c0fe9b32b273ba0f16a","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":"30c4eb3960afb8eb28e967d88ed6c22d45b3398c6fbbf223d597950b2931b31d","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":"f17d522c7298990ac95af48033f39939680a208145734038c657ad8522fad603","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":"61072fc29005dd22885cd1afe2bd64bc44bd2987ca3c3f4798160bf818ca6890","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":"Review Submission15/ only (read Submission15/AUDIT_REQUEST.md first): World source fe8bc4b, live as Worker d5335f94. It answers audit 12e0525d on Submission14 (5 Low, 1 Info). No server code changed (the uploaded Worker module is byte-identical to Submission14's); no Solidity changed.\n\n1. src/world/economyWallet.ts finishReceipt: the wallet's own routed (EIP-7702 / MetaMask Delegation Manager) hash mined with status 0x0 is now judged TRANSACTION_REVERTED at once. Can a successful payment, approval, stake or withdrawal be shown as reverted, or a send intent be released for a transaction that is not this intent's own? Does the flow recover after a reverted approval or payment?\n2. src/world/pendingGoldCredit.ts, WorldGoldPanel.tsx, WalletBalanceHud.tsx: the display-only \"+N crediting\" note now ends on credit, revert, replacement (by the hash it was sent with), 30 minutes after quote expiry unless the wallet saw it mined, the 2 h cap, or a future date. Any case where it shows for a payment that cannot be credited, ends while a real payment is on its way, polls without bound, or is treated as credit?\n3. scripts/deploy.mjs: third read of HEAD, git status and .env* after wrangler uploads; refusal of VITE_* variables, NODE_ENV other than production, and a node_modules that differs from package-lock.json. Any false refusal on a clean deploy, or anything uploaded that the record does not name?\n\nReport severity, reproduction and fix. Not in scope: 3D/rendering, media, Celestao, AIMarket.","parentJobId":null,"planHash":"41b27175260ee9a06a083badef4a75452e88a33e3addf435da57189ef59ef989","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"351b0573-d704-49cb-8af4-76843a8afa3c","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":"52292","feedbackHash":"da040394d8640c9125699bd5c1fd7352a2d8eca1131fb824e64f1088ede330a1","nodeKey":"audit_economics","submissionHash":"a8cbf0e06a48fa6ffc665019b4eb7b9a354f8f997915b69b857787201abf092e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51128","feedbackHash":"7b2ff0169b5c710cbaeaadf59ea22c3545528d06b00f37288fff717cdcfa8b68","nodeKey":"audit_flow","submissionHash":"a0657d1cdb7baaa966f3d3331bfe571dbe8b9ef84ca42c0fe9b32b273ba0f16a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52264","feedbackHash":"8c1f87ac552aff8492a9df69a02a7af929f4f02298ff5e87577eac2c3e1a9e23","nodeKey":"audit_judge","submissionHash":"30c4eb3960afb8eb28e967d88ed6c22d45b3398c6fbbf223d597950b2931b31d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51250","feedbackHash":"8560bfd3052652bf0d247c9df3d8decf6937e4acb4d8e5658993c55d062f84da","nodeKey":"audit_math","submissionHash":"f17d522c7298990ac95af48033f39939680a208145734038c657ad8522fad603","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52114","feedbackHash":"fc51b23ab536e71ffb867a98995ceff147c75eb9087227915e33a9df93a2974d","nodeKey":"audit_permissions","submissionHash":"61072fc29005dd22885cd1afe2bd64bc44bd2987ca3c3f4798160bf818ca6890","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"3c45a17fecbd68bd4decf54cbab6d67bdb028b494ef8c7f213d1d51658745680","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c97fd6a73e3737dc","findings":[{"citation":"resolved","description":"settlePendingCredits drops a note whose order is still `pending` once `now > expires_ms + 30 min`, unless `p.mined` is set. `mined` is set only by WorldGoldPanel.tsx line 88-89 when this browser's controller itself reached phase 'confirmed' for the execute step. The controller polls the receipt for about 2 minutes (waitReceipt, 40 x 3 s) and then goes 'unknown'; nothing polls again unless the player presses 'Check original transaction' while the panel is open, and the controller is disposed when the panel closes. A payment sent near the end of the 15-minute quote with a low fee (or with the tab closed / page reloaded before the receipt) that mines later than that never gets `mined:true`. The server still books such a payment: economy-recovery.ts (Submission13/source/server/economy-recovery.ts:287) sets status='paid' for any pending EMBEO order with a matching payment event, with no expiry condition, and GoldPurchase.tsx:54 promises 'Any payment already sent will still be reconciled against the original order'. Timeline (expiry E): mined at E+25 min, final at about E+38 min, booked by the cron about E+39 min; the note is dropped at E+30 min + 1 ms by the HUD's 30 s poll or the panel's data refresh, so for ~9 minutes the top bar shows no '+N crediting' while a real payment is on its way, and when the order later turns paid settlePendingCredits returns false (no world:gold-changed refresh through the note). Display only; no credit is affected. This is the residual the audit request's question 2 asks about ('ends while a real payment is still on its way'); the requester chose the 30-minute margin deliberately for L-3, so it is recorded as a tradeoff with a concrete case, not a regression. Fix: keep the drop rule but let it ask more than the clock: (a) before dropping an unmined expired note, have WalletBalanceHud's check (or the panel) read eth_getTransactionReceipt for the note's hash once through the connected provider and set `mined` when a receipt with status 0x1 exists; or (b) widen PENDING_CREDIT_AFTER_EXPIRY_MS to cover a late inclusion plus finality (about 60 min), the 2 h cap still bounding everything; or (c) have the server expose 'payment seen, awaiting finality' on the order and keep the note while that is set. Merged from four specialists (math, flow, permissions, economics) reporting the same mechanism at this line.","line":62,"path":"Submission15/source/src/world/pendingGoldCredit.ts","reproduction":"Run against Submission15/source/src/world/pendingGoldCredit.ts (transpiled, localStorage shim): E=NOW+900000; trackPendingCredit(W,ID,TX,'1000',E-60000) with no markPendingCreditMined (controller gave up or panel closed); settlePendingCredits(W,[{id:ID,kind:'topup',gold:'1000',status:'pending',creditedGold:'0',expires_ms:E}],E+PENDING_CREDIT_AFTER_EXPIRY_MS+1) -> returns false, pendingGold(W,...) -> 0n, store empty. Then settlePendingCredits(W,[same order with status:'paid',creditedGold:'1000'],E+39min) -> false (no refresh signal). Expected: the note stays while a payment that the server will still book is mined/pending, as it does for a payment the wallet saw mined (markPendingCreditMined then settle at the same time -> 1000n kept). Contrast: an order the server already marked 'paid' but not yet booked is kept (status!=='pending'), confirming the drop depends only on the client clock and the local `mined` flag.","severity":"low","snippet":" const keep=list.filter(p=>{const o=order(p);return !(o&&(orderCredited(o)||!p.mined&&o.status==='pending'&&now>o.expires_ms+PENDING_CREDIT_AFTER_EXPIRY_MS));});","title":"\"+N crediting\" note is dropped 30 minutes after quote expiry while a late-mined payment the wallet never saw mined is still on its way to the ledger"},{"citation":"resolved","description":"For `own` (the hash the wallet returned for this intent) with r.status==='0x0', `done` is true and tx.hash===candidate, so the finality branch at line 243-248 (eth_getBlockByNumber('finalized') and the canonical-hash check) is skipped and the verdict is given from the latest block. The pending record is deleted, transaction=null, sendAttempted=false, phase 'error'/TRANSACTION_REVERTED; WorldGoldPanel's effect (line 87) drops the crediting note by order and by sentHash, and purchaseStage returns to 'review' so the player may prepare a second payment. A reverted receipt in a block that is not yet final can be reorged out; builders re-include the orphaned transaction, and a state-dependent revert (EMBEO balance or allowance that depended on another transaction's ordering in the dropped block, a paused payment contract) can succeed in the replacement block; the same holds for a same-nonce speed-up included in its place. The server then credits the order at finality while the page said 'reverted' and shows no '+N crediting', and if the player paid again both payments mine (different nonces; the inspect() paid() guard only stops the second send once the first is visible). This is the behaviour the direct-send (`same`) path has had since before Submission14 and is what L-1 asked for, and mainnet reorgs are usually 1-2 blocks, so this is a documented tradeoff rather than a regression; it is the one case where 'a successful payment can be shown as reverted' (question 1). Fix that keeps L-1's intent (never call the own reverted hash 'replaced'): keep the immediate 'reverted' message but keep the intent and the do-not-pay-again lock until the receipt's block is final, releasing it on the next check (e.g. for own && status 0x0 run the finality branch and map TRANSACTION_NOT_FINALIZED to a kept intent with the reverted message), or at least do not drop the crediting note for a reverted own hash until finality. Merged from three specialists (math, flow, economics) at this line.","line":239,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs, executed against the submitted economyWallet.ts: prepare(payPlan()); confirm(); finalized=false (eth_getBlockByNumber('finalized') answers 0x1f, receipt block 0x20); candidate(routed([],{status:'0x0'})); await checkReceipt(). Actual: {phase:'error',error:'TRANSACTION_REVERTED',hash:HASH,sentHash:HASH,store:0} and no eth_getBlockByNumber('finalized') call was made (the submission's own L-1 test asserts this outcome). A following prepare(payPlan()) succeeds (phase 'confirm'), so a second payment can be sent. Expected under 'a successful payment must not be shown as reverted': TRANSACTION_NOT_FINALIZED (intent kept) until finalized >= 0x20, as the pasted-hash path one line below does; chain state that realises the harm: block 0x20 orphaned, the same transaction (or its same-nonce speed-up) mines with status 0x1 in 0x21 and the ledger credits ORDER.","severity":"low","snippet":"  const done=same||own&&r.status==='0x0'||own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);","title":"finishReceipt judges the wallet's own routed hash with status 0x0 as TRANSACTION_REVERTED from a non-final block: a reorg that re-includes the transaction successfully leaves 'reverted' on screen, the"},{"citation":"resolved","description":"finishReceipt rejects the record whenever `record.nonce!==tx.nonce` (this line), where tx.nonce is the eth_getTransactionCount(address,'pending') value read before the prompt (line 192). The wallet is not bound to use the dapp-supplied nonce: MetaMask assigns the nonce from its own tracker at confirmation time unless the user enabled 'Customize transaction nonce', so if another transaction from the same account is confirmed between the count read and this prompt's confirmation (another dapp tab, a MetaMask-initiated send, a queued transaction confirmed first), the wallet returns a real hash for exactly this request, mined with nonce 0x9 while the intent holds 0x8. Every checkReceipt then throws TRANSACTION_MISMATCH -> fail() -> phase 'unknown' with the transaction kept; prepare() and payGold() refuse with PENDING_TRANSACTION (line 166, 279); clearUnknownAfterWalletCheck (line 267) returns at once because the version-2 intent has a hash; checkReceipt(H) with the same hash pasted gives the same result. The only exit is clearing localStorage by hand. The transaction itself may have succeeded (same calldata, status 0x1, matching Paid log), so a successful payment, approval, stake or withdrawal is left permanently 'uncertain' and the crediting note (tracked at phase 'pending') is neither marked mined nor dropped. This binding predates Submission15, but question 1 asks about wrong-nonce releases and stuck locks, and this is the stuck side of it. Fix: for the own hash (tx.hash===candidate) accept a record whose hash, from, chainId, block and exact calldata (or exact intended effect) match even when the nonce differs, treating the nonce as the wallet's renumbering (the nonce binding stays decisive for pasted hashes), or offer a finality-gated acknowledgement for a v2 intent whose own hash is mined with another nonce.","line":231,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs, executed against the submitted economyWallet.ts: prepare(payPlan()); confirm() (wallet returns HASH, intent nonce '0x8'); candidate({hash:HASH,to:PAY,input:<exact pay calldata sent>,nonce:'0x9',logs:[Paid(OID,A,token,treasury,1000)]}) with status 0x1 and a finalized block. Actual: checkReceipt() -> {phase:'unknown',error:'TRANSACTION_MISMATCH',hash:HASH,store:1}; a second checkReceipt() identical; clearUnknownAfterWalletCheck() leaves the state untouched (returns before action); prepare(payPlan()) -> {phase:'unknown',error:'PENDING_TRANSACTION',store:1}. Expected: the wallet's own hash for this request with this sender and exactly this calldata is judged (confirmed), or at least an acknowledgement path exists.","severity":"low","snippet":"  if(!record||record.hash?.toLowerCase()!==candidate.toLowerCase()||record.from?.toLowerCase()!==tx.from||record.chainId!=='0x1'||record.blockHash?.toLowerCase()!==r.blockHash.toLowerCase()||record.blockNumber!==r.blockNumber||(record.to?.toLowerCase()??null)!==(typeof r.to==='string'?r.to.toLowerCase():null)||typeof record.input!=='string'||!HEX.test(record.input)||!quantity(record.value)||(tx.version===2&&record.nonce!==tx.nonce))throw new EconomyClientError('TRANSACTION_MISMATCH');","title":"A wallet-returned own hash mined under a nonce other than the pre-read pending count is judged TRANSACTION_MISMATCH for ever and cannot be acknowledged: permanent PENDING_TRANSACTION lock for this add"},{"citation":"resolved","description":"send() persists a hashless version-2 payment intent before the wallet prompt and sets step 'execute' without clearing `hash` (this line and line 185 patch phase/step/error only; `hash` still holds the approval's hash set by finishReceipt at line 256). If eth_sendTransaction throws with a code other than 4001 (MetaMask -32002 'request already pending' when a second prompt is open, -32603 internal error, or a non-hash reply -> INVALID_TRANSACTION_HASH), fail() (line 163) sets phase 'unknown' and keeps the stale approval hash. GoldPurchase.tsx (Submission14/source, line 42) then shows 'Check original transaction' because wallet.hash is set, and hides the 'Wallet response is uncertain' alert (line 53, shown only when !wallet.hash). The button calls checkReceipt() with no argument, which returns immediately because transaction.hash is null (line 261): no RPC, no state change. The hashless intent blocks every new send for this address (PENDING_TRANSACTION), and the only exit, clearUnknownAfterWalletCheck, is offered by EconomyPane.tsx only; WorldGoldPanel.tsx never calls it. After a reload adoptPending restores hash null so the alert appears, but the GOLD panel still has no acknowledgement button. Fix: in send() clear the previous receipt's hash when the new intent is persisted (`this.set({phase:'sending',busy:true,step,hash:null,sentHash:null,error:null})` at line 185), so paymentHash/GoldPurchase see the hashless intent; and offer the hashless-intent acknowledgement (clearUnknownAfterWalletCheck) in the GOLD panel as EconomyPane does.","line":183,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs with allowance intercepted to 0, executed against the submitted economyWallet.ts: prepare(payPlan()) -> 'approve-required'; approve() (wallet returns HASH); candidate(routed([Approval(A,PAY,1000)])); allowance=1000; checkReceipt() -> {phase:'confirm',hash:HASH,step:'approve',store:0}. Then intercept eth_sendTransaction to throw {code:-32002}; confirm(). Actual: {phase:'unknown',error:'REQUEST_FAILED',step:'execute',hash:HASH (the approval), store:1} with the stored intent's hash null; checkReceipt() makes 0 RPC calls and changes nothing; prepare(payPlan()) -> PENDING_TRANSACTION. Expected: the stale hash is cleared so the uncertain-response alert and an acknowledgement are shown, or the check button checks something.","severity":"low","snippet":"  this.set({phase:'checking',busy:true,step,error:null});","title":"After a confirmed approval, a payment prompt that fails with any error other than the user's rejection leaves the approval hash on screen as 'the transaction': the GOLD panel's only button (checkRecei"},{"citation":"resolved","description":"MetaMask's Speed up / Cancel re-sends the same nonce under a new hash H2; the wallet-returned hash H1 then never gets a receipt, so waitReceipt ends in 'unknown' with error PENDING_TRANSACTION and the version-2 intent (with hash) stays in localStorage. economyWallet.ts resolves this only through a pasted hash: checkReceipt(hash) verifies nonce, sender, chain and finality for a replacement, and clearUnknownAfterWalletCheck refuses a v2 intent that has a hash (line 267). The GOLD panel passes `check={()=>void run(()=>controller!.checkReceipt())}` (this line) with no hash argument and renders no hash input; GoldPurchase shows only 'Check original transaction', which polls H1 again (40 x 3 s) and returns to 'unknown'. prepare() for any order then throws PENDING_TRANSACTION (economyWallet.ts:166). The staking pane (Submission13 EconomyPane.tsx:123) has the paste form and the same pending key, but nothing in the GOLD panel says so. The crediting note itself ends correctly (credited at finality if the speed-up paid, or 30 min after expiry if cancelled), so this is availability of further purchases and the recovery question 1 asks about ('does the flow recover'), not a credit error. Fix: accept a hash in the panel's check callback (check={hash=>void run(()=>controller!.checkReceipt(hash))}) and render the replacement-hash form EconomyPane renders when wallet.phase==='unknown' && wallet.hash, or link to it.","line":111,"path":"Submission15/source/src/world/WorldGoldPanel.tsx","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs, executed against the submitted economyWallet.ts: prepare(payPlan()); confirm() (hash H1=HASH, nonce 0x8); candidate({...routed([Paid(OID,1000)]),hash:OTHER}) so the receipt exists only under the speed-up hash. checkReceipt() (what the GOLD panel's button calls) -> {phase:'unknown',error:'PENDING_TRANSACTION',hash:HASH,store:1}; prepare(payPlan()) -> {phase:'unknown',error:'PENDING_TRANSACTION'}; clearUnknownAfterWalletCheck() -> no change; checkReceipt(OTHER) -> resolves ({phase:'replaced',store:0}). WorldGoldPanel.tsx line 111 passes check={()=>...checkReceipt()} and line 116's button also calls checkReceipt() without a hash; grep of WorldGoldPanel.tsx finds no hash input and no clearUnknownAfterWalletCheck. Expected: a way inside the GOLD panel to supply H2.","severity":"low","snippet":" <GoldPurchase data={data} selected={selected} wallet={wallet} config={configRef.current} busy={busy} zh={zh} pending={pending} quote={product=>void run(()=>quote(product))} prepare={()=>selected&&void run(()=>pay(selected))} approve={()=>void run(()=>controller!.approve())} confirm={()=>void run(()=>controller!.confirm())} check={()=>void run(()=>controller!.checkReceipt())} reconcile={()=>selected&&void run(()=>client.reconcile(selected.id))} clear={()=>setSelected(null)}/>","title":"GOLD panel offers no replacement-hash input: a payment cancelled or sped up in the wallet (same nonce, new hash) leaves phase 'unknown' with a button that only re-polls the dead hash, and every later "},{"citation":"resolved","description":"The tracking effect depends on [payingOrder,payingHash,data]. useGold replaces `data` every 12 s (setInterval) and on focus/visibility/gold-changed, so the effect re-runs for as long as the panel is open and the wallet stays in phase 'confirmed' with step 'execute' and a pay plan (nothing resets that state until the next prepare). trackPendingCredit (pendingGoldCredit.ts:34) skips only when readPendingCredits already holds the order, and readPendingCredits filters out entries older than PENDING_CREDIT_MAX_MS. So at atMs+2h the entry drops out of the read and the next refresh (at most 12 s later) writes a fresh entry with atMs=now; the mined effect (line 89) re-marks it mined in the same commit, so the after-expiry rule does not apply either, and settlePendingCredits (line 91) keeps it. The 'past 2 hours the entry is dropped' guarantee (pendingGoldCredit.ts:11) therefore holds only once the panel is closed or the controller disposed. While the panel is open, a payment the ledger never books (a reorged-out block judged before finality, see the finishReceipt finding, or a server-side reconcile error) keeps '+N crediting' in the top bar and on the purchase page indefinitely, and WalletBalanceHud's 30 s ledger poll restarts for the same time (usePendingGold re-arms because `gold` changes). Display only: nothing reads the note as credit (purchaseStage and GoldPurchase take 'credited' from orderCredited only). Fix: run the tracking once per (orderId, hash) (a ref of tracked pairs, or track in the code path that observes the phase change to 'pending'), or let trackPendingCredit refuse to re-add an orderId that the raw store (stored(), not the time-filtered read) already holds, and have settlePendingCredits prune expired raw entries.","line":81,"path":"Submission15/source/src/world/WorldGoldPanel.tsx","reproduction":"Run against Submission15/source/src/world/pendingGoldCredit.ts (transpiled, localStorage shim), replaying the panel's effect order (track, drop, mined, settle) on a data refresh: trackPendingCredit(W,ID,TX,'1000',NOW); markPendingCreditMined(W,TX,NOW); pendingGold(W,NOW+PENDING_CREDIT_MAX_MS) -> 0n (one raw entry remains in the store). Then, as the next refresh at NOW+2h+12s with the order still status 'pending': trackPendingCredit(W,ID,TX,'1000',NOW+2h+12s) saves a new entry (list.some(...) was false because the old one is filtered out); markPendingCreditMined marks it mined; settlePendingCredits(W,[order pending],NOW+2h+12s) keeps it; pendingGold(W,NOW+2h+12s) -> 1000n and pendingGold(W,NOW+4h-1ms) -> 1000n. Expected per pendingGoldCredit.ts:11 and AUDIT_REQUEST.md question 2: the note is gone for good at atMs+2h.","severity":"low","snippet":" useEffect(()=>{const o=payingOrder?data?.orders.find(o=>o.id===payingOrder):null;if(!data||!o||o.kind!=='topup'||!payingHash||o.status==='paid')return;","title":"The 2 h cap on the \"+N crediting\" note is defeated while the GOLD panel stays open: the tracking effect re-creates the note on every ledger refresh once the old entry has aged out"},{"citation":"resolved","description":"dependencyMismatches(ROOT) runs exactly once, at the start (this line), next to the first git status. The post-build re-check (lines 179-182) re-reads HEAD, git status and .env* but not node_modules, and the third read after wrangler (lines 196-197) re-reads the same three things again, not node_modules. The suite, tsc, vite build and wrangler's bundling take minutes (the real record shows 04:51:46 to 04:56:39), and node_modules is gitignored, so a package added, upgraded or linked meanwhile (an `npm install --no-save x`, `npm link`, `npm update` in another terminal on the same checkout, or a postinstall/prepare hook) is bundled by vite and wrangler, while the record states dependencies.mismatches:[] and dirty:false and the outcome exits 0. This is the same window L-4 closed for source files and .env files, applied to the dependency tree L-5 says is now refused; the existing check would detect the change, it is simply not re-run. (Distinct from the acknowledged residual 'a file edited inside an installed package at the locked version is not detected'.) Fix: recompute dependencyMismatches(ROOT) in the post-build refusal (append `??dependencyRefusal(dependencyMismatches(ROOT),dryRun)` at line 181) and include it in sourceUnchanged at line 197 (compare the JSON of the mismatch list, or hash node_modules/.package-lock.json and every locked package.json at start, after build and after upload, recording all three). Merged from two specialists (flow, economics).","line":172,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic fixture shaped like tests/deploy.test.mjs 'clean' with the submitted scripts/deploy.mjs copied in: package-lock.json and node_modules/.package-lock.json lock node_modules/a@1.0.0; the fake `vite build` additionally rewrites node_modules/a/package.json to version 9.9.9, creates node_modules/evil and lists it in node_modules/.package-lock.json (what a concurrent `npm install --no-save` does; package-lock.json untouched so git status stays clean); the fake wrangler bundles node_modules/a/package.json into worker/index.js. `node scripts/deploy.mjs` -> exit 0, outcome {deployment:'uploaded',verification:'passed',exitCode:0}, manifest.dirty=false, manifest.dependencies.mismatches=[], manifest.source.unchangedDuringUpload=true, and the recorded worker/index.js contains {\"version\":\"9.9.9\"}. Running dependencyMismatches(dir) right after the run returns ['node_modules/a 9.9.9 installed, 1.0.0 locked','not in package-lock.json: node_modules/evil']. Expected: refusal before upload (like changedRefusal) or at least dirty:true / unchangedDuringUpload:false / exit 1, as for an edited source file.","severity":"low","snippet":"  const env=envFiles(ROOT),viteEnv=viteEnvNames(process.env),dependencies=dependencyMismatches(ROOT);","title":"deploy.mjs checks node_modules against package-lock.json only once, before the multi-minute run: a package changed or added during tests/build/upload is bundled and uploaded while the record says mism"},{"citation":"resolved","description":"run() spawns wrangler with {...process.env,...env} (this line) and runStep spawns tsc/vite with a copy of process.env (line 98). L-5 refuses VITE_* names and a non-production NODE_ENV because 'nothing in git names them', but other variables of the same class pass through unexamined: CLOUDFLARE_ENV, documented by Cloudflare as 'the environment to use for Wrangler commands' without --env (verified 2026-10-27 on developers.cloudflare.com/workers/wrangler/system-environment-variables), makes `wrangler deploy` at line 191 target the env.<name> section of wrangler.jsonc or a Worker named imd-world-<name> and load .env.<name>; NODE_OPTIONS=--import/--require/--loader runs arbitrary code inside vite and inside wrangler's esbuild bundling (an APM agent preloaded from a shell profile is a common case); wrangler's own bundler inputs WRANGLER_BUILD_CONDITIONS / WRANGLER_BUILD_PLATFORM (read by wrangler's bundle step into the esbuild options; not verified against wrangler 4.143.0 here) change which package exports the Worker bundle resolves. The manifest records viteEnv, nodeEnv, config hashes and wrangler's exitCode/versionId only: no Worker name, environment, account or option list, so a bundle or target that differs from a clean build of the same commit is recorded as if the commit alone determined it; only the free-text wrangler.log would reveal it. Fix: add a name-only refusal (like viteEnvRefusal) for CLOUDFLARE_ENV / WRANGLER_ENV, WRANGLER_BUILD_*, and a NODE_OPTIONS containing --import, --require, --loader or --experimental-loader on a real deploy, list them on a dry run, and record wrangler's reported Worker name/environment (parse 'Uploaded <name>' from the log) in manifest.wrangler; stronger: spawn the children with an explicit environment from which these keys are removed, and record that. Merged from three specialists (flow, economics, permissions) reporting the same pass-through for different variables.","line":163,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic fixture shaped like tests/deploy.test.mjs 'clean' with the submitted scripts/deploy.mjs; the fake wrangler writes the variables it receives into worker/index.js (real wrangler reads CLOUDFLARE_ENV for the target environment and Node applies NODE_OPTIONS to the process). `WRANGLER_BUILD_CONDITIONS=node,import WRANGLER_BUILD_PLATFORM=node CLOUDFLARE_ENV=staging NODE_OPTIONS=--max-old-space-size=4096 node scripts/deploy.mjs` -> exit 0, no refusal, manifest.viteEnv=[], manifest.nodeEnv=null, manifest keys contain no field naming any of the four variables; the recorded worker/index.js reads 'conditions=node,import | platform=node | env=staging | node_options=--max-old-space-size=4096', i.e. the children received them. Expected: a real deploy refuses them by name (as VITE_X=1 is refused before `> npm test`) or the record names the environment/Worker actually uploaded to.","severity":"low","snippet":"  const r=spawnSync(process.execPath,args,{cwd:ROOT,encoding:'utf8',stdio:capture?['ignore','pipe','pipe']:'inherit',env:{...process.env,...env}});","title":"deploy.mjs passes every shell variable but VITE_* and NODE_ENV through to tsc, vite and wrangler: CLOUDFLARE_ENV selects another Worker environment for the upload and NODE_OPTIONS (--import/--require)"},{"citation":"resolved","description":"The final loop (this line) reports 'not in package-lock.json' only for keys of node_modules/.package-lock.json, and the earlier loop reads only the paths the lockfile names. Anything npm did not record is invisible: a directory copied into node_modules at top level, or nested under a locked package (node_modules/wrangler/node_modules/esbuild, node_modules/vite/node_modules/rolldown) where Node resolution finds it before the hoisted locked copy, is neither 'missing', 'another version', 'another source' nor 'not in package-lock.json'. AUDIT_REQUEST.md L-5 and the comment above the function say the refusal covers 'any installed package the lockfile does not name'; in fact only npm-recorded additions (`npm install --no-save`) are covered. Adjacent to the acknowledged residual (an edited file inside a locked package) but a distinct mechanism, and the record's claim (dependencies.mismatches: []) is stronger than the check. Needs a local actor with write access to the checkout, like the acknowledged residual. Fix: walk node_modules (every */package.json and @scope/*/package.json, recursing into nested node_modules) and refuse any package directory whose path is not a key of lock.packages; or refuse when `npm ls --all --json` reports extraneous/invalid entries.","line":78,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Fixture: package-lock.json and node_modules/.package-lock.json both name node_modules/wrangler@4.143.0 and node_modules/esbuild@0.25.0, both installed -> dependencyMismatches(dir) = []. Then create node_modules/wrangler/node_modules/esbuild/package.json ({\"version\":\"0.25.0\",\"name\":\"esbuild\"}) and node_modules/extra/package.json ({\"version\":\"9.9.9\"}) by hand. Actual (run against the submitted deploy.mjs): dependencyMismatches(dir) is still [], so dependencyRefusal returns null and a real deploy proceeds with dependencies.mismatches: [] in the record. Expected: two mismatches naming the paths the lockfile does not name.","severity":"low","snippet":"  for(const k of Object.keys(hidden.packages))if(k.startsWith('node_modules/')&&!names.has(k))out.push('not in package-lock.json: '+k);","title":"dependencyMismatches compares the lockfile only with npm's own install record, never with the node_modules tree: a package directory placed by hand (including one nested so that it shadows the locked "},{"citation":"resolved","description":"initial() (this line) has no sentHash key and set() merges patches, so after any judged receipt (line 256 sets sentHash) the value persists through prepare() ({...initial(),...} at line 168), payGold() (line 279) and every later phase of the next intent until that intent is judged. In WorldGoldPanel.tsx `sent=wallet.sentHash??wallet.hash` (line 86) therefore names the previous payment while a new one is prepared or pending (wallet.hash null or the new hash, sentHash still the old one). Today no consumer fires in those phases (the drop effect needs paymentFailed, the mined effect needs 'confirmed', both set together with a fresh sentHash at line 256), so there is no visible effect; it is a latent hazard for any future effect keyed on `sent` during 'pending'/'unknown' (e.g. a drop-on-unknown rule), which would hit the previous order's note. Fix: add sentHash:null to initial() and to the EMPTY constant in WorldGoldPanel.tsx.","line":35,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs, executed against the submitted economyWallet.ts: prepare(payPlan()); confirm(); candidate(routed([Paid(OID,1000)])); checkReceipt() -> {phase:'confirmed',hash:HASH,sentHash:HASH}. Then prepare({kind:'stake',shares:'1'}). Actual: {phase:'confirm',hash:null,step:null,sentHash:HASH}. Expected: sentHash null until this intent is judged.","severity":"info","snippet":"const initial=():EconomyWalletState=>({phase:'idle',busy:false,plan:null,hash:null,step:null,error:null,legacyPending:false});","title":"sentHash is not reset by initial(): a judged hash from a previous intent survives into the next prepare()/payGold() state and names the previous payment in the panel until the new intent is judged"},{"citation":"resolved","description":"Vite's loadEnv exposes a process.env key to import.meta.env only when `prefixes.some(prefix=>key.startsWith(prefix))` with the default prefix 'VITE_' (String.prototype.startsWith, case-sensitive), so `vite_debug=1` or `Vite_Mode=dev` in the deploy shell never reaches the client bundle on a case-sensitive OS. The refusal uses /^VITE_/i (this line) and refuses a real deploy whose `vite build` output would be identical to a clean one. The submission's own test asserts ['VITE_API','vite_debug'], so this is a deliberate over-approximation (on Windows, where variable names are case-insensitive, the broad match is right), but it is a false refusal of a clean deploy, which question 3 asks to flag. Fix: use /^VITE_/ except when process.platform==='win32', or keep the broad match but only list the lower-case names in the manifest instead of refusing. Merged from three specialists (math, flow, economics).","line":55,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"viteEnvNames({PATH:'x',vite_debug:'1',Vite_Mode:'dev'}) returns ['Vite_Mode','vite_debug'] and viteEnvRefusal(that,false) returns 'VITE_* environment variables would change the build but are not in git (Vite_Mode, vite_debug); unset them' (run against the submitted deploy.mjs). So `vite_debug=1 node scripts/deploy.mjs` on a clean tree exits 1 before `> npm test`. Expected on Linux: no refusal, because `vite build` ignores the variable and produces the same dist.","severity":"info","snippet":"export const viteEnvNames=env=>Object.keys(env).filter(k=>/^VITE_/i.test(k)).sort();","title":"viteEnvNames matches VITE_ case-insensitively while Vite's envPrefix match is case-sensitive: a lower-case vite_* shell variable that never reaches the bundle is a false refusal on a clean Linux/macOS"},{"citation":"resolved","description":"In a v2/v3 package-lock.json a `file:` or workspace dependency is recorded as \"node_modules/foo\": {\"resolved\":\"pkgs/foo\",\"link\":true} with the version on the target entry (\"pkgs/foo\": {\"version\":\"1.0.0\"}), and node_modules/foo is a symlink. The loop reads node_modules/foo/package.json through the symlink (version '1.0.0') and compares it with v.version (undefined) at this line, producing 'node_modules/foo 1.0.0 installed, undefined locked' and a refusal on a real deploy with a message that does not say why. The real repository has no such dependency today (the record shows mismatches: []), so this is latent. Fix: skip entries with link:true (or resolve them to lock.packages[v.resolved] and compare that version), and compare resolved/integrity only when the lock entry has them.","line":74,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Fixture (run against the submitted deploy.mjs): package-lock.json {lockfileVersion:3,packages:{'':{},'node_modules/foo':{resolved:'pkgs/foo',link:true},'pkgs/foo':{name:'foo',version:'1.0.0'}}}, node_modules/.package-lock.json listing node_modules/foo with link:true, pkgs/foo/package.json {version:'1.0.0'}, symlink node_modules/foo -> ../pkgs/foo. Actual: dependencyMismatches(dir) = ['node_modules/foo 1.0.0 installed, undefined locked'] and dependencyRefusal(...,false) refuses the deploy. Expected: [] (installed exactly as locked).","severity":"info","snippet":"    if(installed.version!==v.version)out.push(k+' '+installed.version+' installed, '+v.version+' locked');","title":"dependencyMismatches reports 'undefined locked' for any lockfile entry with link:true (a file:/workspace dependency), because such entries carry no version: a latent false refusal the first time one i"},{"citation":"resolved","description":"All three tree reads use `git status --porcelain --untracked-files=normal` (this line, 179, 196), which applies the committed .gitignore files and also the repository-local .git/info/exclude and core.excludesFile. Those two are outside every commit, so a new file listed there (a developer's local-only module imported by tracked code, or a file matched by a global pattern) is read by vite and bundled by wrangler while the record says dirty:false and source.unchangedDuringUpload:true. The same gap applies to paths marked skip-worktree/assume-unchanged. It needs the operator's own local git configuration, so this is an integrity note on what the record can claim rather than an attack. Fix: list untracked files with only the committed ignore rules (`git ls-files --others --exclude-per-directory=.gitignore`) plus `git ls-files -v | grep '^[Sh]'` for skip-worktree/assume-unchanged, refuse when either is non-empty on a real deploy, and record both in the manifest.","line":171,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic fixture shaped like tests/deploy.test.mjs 'clean' with the submitted scripts/deploy.mjs: commit everything; append 'src/extra.ts' to .git/info/exclude; write src/extra.ts ('NOT IN ANY COMMIT'); the fake wrangler bundles src/extra.ts into worker/index.js. `git status --porcelain --untracked-files=normal` prints nothing. `node scripts/deploy.mjs` -> exit 0, manifest.dirty=false, source.unchangedDuringUpload=true, and the recorded worker/index.js contains 'extra=NOT IN ANY COMMIT'. Expected: refusal ('working tree is not clean') or at least dirty:true.","severity":"info","snippet":"  const commit=git('rev-parse','HEAD'),porcelain=git('status','--porcelain','--untracked-files=normal');","title":"The clean-tree check honours .git/info/exclude and the user's global excludes file, which no commit names: an untracked source file hidden there is built and uploaded under dirty:false"}],"hash":"30c4eb3960afb8eb28e967d88ed6c22d45b3398c6fbbf223d597950b2931b31d","nodeId":"b27269d9-dc7b-441c-8f98-edf9ecb7051e","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 13 findings (9 Low, 4 Info), every one reproduced against the Submission15 code in a scratch harness outside the repository. No repository file was changed, and no Solidity is in scope, so no Foundry proofs apply.\n\n**What I did**\n\n- Read the audit request, the three changed modules, the tests, and the unchanged Submission13/14 files they depend on (GoldPurchase, WalletBalanceHud, EconomyPane, economy-recovery).\n- Transpiled the wallet and crediting-note modules with esbuild and replayed the submission's own test fixture against them. Ran the deploy tool end to end in synthetic git fixtures modelled on its test suite.\n- Merged 23 specialist findings into 13. Every specialist claim reproduced. The duplicates were the after-expiry drop (4 reports), the pre-finality reverted verdict (3), the VITE_ case rule (3), the one-time dependency check (2), and the shell-variable pass-through (3, merged into one root cause).\n\n**Findings by area**\n\n- **Question 1, wallet (economyWallet.ts).** Own routed hash with status 0x0 is judged reverted from a non-final block, so a reorg can show a successful payment as reverted and release the intent (Low, documented tradeoff). An own hash mined under a nonce other than the pre-read count is a permanent TRANSACTION_MISMATCH lock with no acknowledgement path (Low). A payment prompt failing with a non-4001 error keeps the approval hash on screen, and the GOLD panel's check button then does nothing (Low). The GOLD panel has no replacement-hash input, so a sped-up payment blocks all later purchases there (Low). `sentHash` is never reset by `initial()` (Info). A reverted approval or payment otherwise recovers cleanly.\n- **Question 2, crediting note.** The 30-minute after-expiry rule drops the note for a late-mined payment the wallet did not see mined, while the server still credits it (Low). The 2 h cap is defeated while the panel stays open, because the tracking effect re-creates the note after it ages out (Low). I confirmed nothing treats the note as credit.\n- **Question 3, deploy tool.** node_modules is checked once, not at the post-build or post-upload reads (Low). CLOUDFLARE_ENV and NODE_OPTIONS pass through to the children and are neither refused nor recorded (Low). Hand-placed package directories are invisible to the lockfile check (Low). False refusals: lower-case `vite_*` variables (Info) and `link:true` lockfile entries (Info). `.git/info/exclude` hides untracked files from the clean-tree check (Info).\n\n**Limits.** CLOUDFLARE_ENV semantics were verified against Cloudflare's documentation; the WRANGLER_BUILD_* variables are named in that finding as unverified against wrangler 4.143.0, which is not installed here. The wallet nonce finding relies on the wallet being free to renumber a dapp-supplied nonce, which the code itself cannot prevent.","treeHash":null,"usage":{"cachedInputTokens":2424449,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":45711,"runtime":"claude","turns":35,"wallClockMs":658893}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"918f8261a6fd589c","findings":[{"citation":"resolved","description":"settlePendingCredits drops a note whose order is still `pending` 30 minutes after the quote expired unless `p.mined` is set. `mined` is set only by WorldGoldPanel.tsx line 88-89 (`wallet.step==='execute'&&wallet.phase==='confirmed'`), i.e. only when this browser's controller judged the receipt. The controller polls the receipt for at most 40 x 3 s (economyWallet.ts waitReceipt) and then goes to phase `unknown`; nothing polls again unless the player presses \"Check original transaction\" while the panel is open. A payment sent shortly before the quote expired and mined late (low fee during congestion) is therefore never marked mined, and the note ends while the payment is mined and waiting for finality + the cron (about 15-16 minutes). The purchase page itself promises the opposite (\"Any payment already sent will still be reconciled against the original order\"). Display only: no credit is affected, but the top bar stops saying the GOLD is on its way, the balance refresh via the note is lost, and the player is told nothing. Fix: before applying the after-expiry drop, ask the server whether the payment has been seen (a `paymentSeenMs`/`awaitingFinality` field on the order from the latest-block indexer or the reconcile route), or have the HUD re-poll the note's hash through the provider once per check while the controller is idle; alternatively lengthen PENDING_CREDIT_AFTER_EXPIRY_MS so that it exceeds the longest plausible pending time plus finality (e.g. 60 minutes), which still keeps the 2 h cap.","line":62,"path":"Submission15/source/src/world/pendingGoldCredit.ts","reproduction":"State: quote expires at T0+1 min; the wallet returns the payment hash at T0 (note created with mined undefined); the receipt is still null when the 40 x 3 s poll ends -> controller phase `unknown`; the panel is closed (no re-check). The transaction mines at T0+21 min (20 min after expiry); it is final at about T0+36 min; the cron books it at about T0+37 min. Expected: the note shows \"+1,000 crediting\" until the ledger books it. Actual: at T0+31 min + 1 ms settlePendingCredits (HUD poll every 30 s) removes the note because `!p.mined && o.status==='pending' && now > expires_ms + 30 min`; pendingGold is 0 for the next ~6 minutes while the payment is on its way, and settlePendingCredits returns false when the order later turns paid (no balance refresh through the note). Test tests/repro-wallet.test.mjs \"F-B\" in the scratch workspace (merged Submission13/14/15 sources, Node 24) passes against this code:\n\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport {EconomyWalletController,ECONOMY_EFFECT_TOPICS} from '../src/world/economyWallet.ts';\nimport {ECONOMY_TOKEN,ECONOMY_TREASURY,SHARE_WEI} from '../src/world/economyClient.ts';\nimport {paymentHash,paymentFailed,trackPendingCredit,markPendingCreditMined,settlePendingCredits,pendingGold,PENDING_CREDIT_AFTER_EXPIRY_MS} from '../src/world/pendingGoldCredit.ts';\n\nconst INITIAL={account:null,session:null,phase:'idle',home:null,expired:false,ended:null,restored:false,sessionKnown:false,leaving:false,signing:null,checking:false,notice:null,waiting:false};\nconst A='0x'+'a'.repeat(40),VAULT='0x'+'1'.repeat(40),PAY='0x'+'2'.repeat(40),HASH='0x'+'3'.repeat(64),BLOCK='0x'+'5'.repeat(64),NOW=Date.UTC(2026,9,16);\nconst config={enabled:true,chainId:1,token:ECONOMY_TOKEN,tokenDecimals:18,stakingContract:VAULT,paymentContract:PAY,treasury:ECONOMY_TREASURY,shareTokens:'100000',shareWei:String(SHARE_WEI),rule:'utc-full-day-v1',goldTransferable:false,catalogue:[]};\nconst word=n=>'0x'+BigInt(n).toString(16).padStart(64,'0'),address=a=>'0x'+a.slice(2).padStart(64,'0');\nfunction mutex(){let tail=Promise.resolve();return {run(_key,fn){const run=tail.then(fn,fn);tail=run.catch(()=>{});return run;}};}\nfunction fixture({lock=mutex(),store=new Map()}={}){\n const auth={...INITIAL,account:A,session:{address:A,expiresAt:NOW+3600000},sessionKnown:true,restored:true},calls=[],sent=[];let candidate=null,finalized=true,intercept=null;\n const provider={on(){},removeListener(){},async request(args){\n  calls.push(args);if(intercept){const value=intercept(args);if(value!==undefined)return value;}\n  const {method,params}=args;\n  if(method==='eth_accounts')return [A];if(method==='eth_chainId')return '0x1';if(method==='eth_getTransactionCount')return '0x8';\n  if(method==='eth_call'){\n   const selector=params[0].data.slice(2,10);\n   if(selector==='fc0c546a')return address(ECONOMY_TOKEN);\n   if(selector==='10a223e7')return word(SHARE_WEI);\n   if(selector==='70a08231'||selector==='dd62ed3e')return word(10n*SHARE_WEI);\n   if(selector==='99fbab88')return address(A)+word(3n).slice(2);\n   if(selector==='61d027b3')return address(ECONOMY_TREASURY);\n   if(selector==='e9c1121e')return word(0n);\n   throw Error('unexpected selector '+selector);\n  }\n  if(method==='eth_sendTransaction'){sent.push(params[0]);return HASH;}\n  if(method==='eth_getTransactionReceipt')return candidate&&params[0]===candidate.hash?{transactionHash:candidate.hash,from:A,to:candidate.to,status:candidate.status??'0x1',blockHash:BLOCK,blockNumber:'0x20',logs:candidate.logs??[]}:null;\n  if(method==='eth_getTransactionByHash')return candidate&&params[0]===candidate.hash?{from:A,to:candidate.to,input:candidate.input,value:'0x0',chainId:'0x1',nonce:candidate.nonce,hash:candidate.hash,blockHash:BLOCK,blockNumber:'0x20',...candidate.patch}:null;\n  if(method==='eth_getBlockByNumber')return {number:params[0]==='finalized'&&!finalized?'0x1f':'0x20',hash:BLOCK};\n  throw Error('unexpected RPC '+method);\n }};\n const deps={config,auth:()=>auth,provider:()=","severity":"low","snippet":" const keep=list.filter(p=>{const o=order(p);return !(o&&(orderCredited(o)||!p.mined&&o.status==='pending'&&now>o.expires_ms+PENDING_CREDIT_AFTER_EXPIRY_MS));});","title":"\"+N crediting\" note is dropped by the after-expiry rule while a late-mined payment is still awaiting finality (the mined mark needs a manual re-check after the 2-minute poll)"},{"citation":"resolved","description":"After an approval is confirmed, finishReceipt sets `hash` to the approval hash and `inspect()` moves the phase to `confirm`. `confirm()` -> `send(..., 'execute')` persists a hashless payment intent before the wallet prompt and sets `step: 'execute'` without clearing `hash`. If `eth_sendTransaction` throws with any code other than 4001 (MetaMask -32002 \"already pending for origin\" when a second prompt is open, -32603 internal error, a wallet that answers with a non-hash value -> INVALID_TRANSACTION_HASH), `fail()` sets phase `unknown` and keeps the stale approval hash. GoldPurchase.tsx (Submission14/source, line 42) then shows \"Check original transaction\" because `wallet.hash` is set, and hides the \"Wallet response is uncertain\" alert (line 53, shown only when `!wallet.hash`). The button calls `checkReceipt()` with no argument, which returns immediately because `transaction.hash` is null (economyWallet.ts line 261), so nothing happens; the hashless v2 intent blocks every new send on this address (PENDING_TRANSACTION) and the only exit, `clearUnknownAfterWalletCheck`, is never called from WorldGoldPanel.tsx (only EconomyPane.tsx offers it). After a reload the display is correct (adoptPending restores hash null) but the GOLD panel still has no acknowledgement button. Fix: in `send()` clear `hash` (and `sentHash`) when the new intent is persisted (`this.set({phase:'sending',busy:true,step,hash:null,error:null})`), so paymentHash/GoldPurchase see the hashless intent; and offer the hashless-intent acknowledgement in the GOLD panel as EconomyPane does.","line":163,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Input: prepare(pay plan) with allowance 0 -> `approve-required`; approve() -> wallet returns HASH; routed receipt with exactly the Approval event -> phase `confirm`, hash HASH, step `approve`. Then confirm(): the provider throws {code:-32002} on eth_sendTransaction. Expected: either the stale hash is cleared so the \"wallet response uncertain\" alert and an acknowledgement are shown, or the check button checks something. Actual: state {phase:'unknown', step:'execute', error:'REQUEST_FAILED', hash: HASH (the approval)}, the stored intent has hash null, `checkReceipt()` makes no RPC call and changes nothing, and WorldGoldPanel.tsx never calls clearUnknownAfterWalletCheck. Test \"F-A\" in tests/repro-wallet.test.mjs (source above in the previous finding) passes against this code.","severity":"low","snippet":" private fail(error:unknown){const code=error instanceof EconomyClientError?error.code:(error as {code?:unknown})?.code===4001?'USER_REJECTED':'REQUEST_FAILED';if(code==='USER_REJECTED')this.sendAttempted=false;this.set({busy:false,phase:this.transaction||this.sendAttempted?'unknown':code==='USER_REJECTED'?'rejected':code==='CONTEXT_CHANGED'||code==='ACCOUNT_MISMATCH'?'context-changed':'error',error:code});}","title":"A payment prompt that fails with any error other than the user's rejection leaves the confirmed approval hash on screen as \"the transaction\": the check button is a no-op and the GOLD panel offers no e"},{"citation":"resolved","description":"dependencyMismatches compares package-lock.json only with npm's own record node_modules/.package-lock.json, never with the directory tree. Everything npm did not install is invisible: a directory copied into node_modules (top level, or nested under a locked package where Node resolution finds it before the hoisted locked copy, e.g. node_modules/wrangler/node_modules/esbuild or node_modules/vite/node_modules/rolldown) is neither \"missing\", \"another version\", \"another source\" nor \"not in package-lock.json\". AUDIT_REQUEST.md L-5 and the comment above the function claim the refusal covers \"any installed package the lockfile does not name\"; only `npm install --no-save` style additions (which npm records) are covered. This is adjacent to the acknowledged residual (an edited file inside a locked package) but is a distinct mechanism, and the claim in the record (`dependencies.mismatches: []`) is stronger than what is checked. Fix: walk node_modules (every `*/package.json` and `@scope/*/package.json`, recursively through nested node_modules) and refuse any package directory whose path is not a key of lock.packages; or, stronger, refuse when `npm ls --all --json` reports `extraneous`/`invalid`, which reads the tree.","line":78,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Fixture: package-lock.json and node_modules/.package-lock.json both name node_modules/wrangler@4.143.0 and node_modules/esbuild@0.25.0, both installed -> dependencyMismatches(dir) = []. Then create node_modules/wrangler/node_modules/esbuild/package.json ({\"version\":\"0.25.0\"}) and node_modules/extra/package.json by hand. Expected: a mismatch naming the two paths the lockfile does not name. Actual: dependencyMismatches(dir) is still [] and a real deploy proceeds with `dependencies.mismatches: []` in the record. Test \"F-C\" in tests/repro-deploy.test.mjs passes against this code:\n\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport {mkdtempSync,mkdirSync,writeFileSync,rmSync,copyFileSync,readdirSync,readFileSync,appendFileSync} from 'node:fs';\nimport {tmpdir} from 'node:os';\nimport {join,dirname} from 'node:path';\nimport {spawnSync} from 'node:child_process';\nimport {dependencyMismatches,RECORDS_DIR} from '../scripts/deploy.mjs';\nconst ROOT=new URL('..',import.meta.url).pathname;\nfunction tree(files){const dir=mkdtempSync(join(tmpdir(),'imd15-'));for(const [p,c] of Object.entries(files)){mkdirSync(dirname(join(dir,p)),{recursive:true});writeFileSync(join(dir,p),c);}return dir;}\nconst lock=packages=>JSON.stringify({lockfileVersion:3,packages:{'':{name:'x'},...packages}});\n\ntest('F-C: a package directory placed by hand (not by npm) passes dependencyMismatches, even where it shadows the locked copy',()=>{\n  const dir=tree({'package-lock.json':lock({'node_modules/wrangler':{version:'4.143.0'},'node_modules/esbuild':{version:'0.25.0'}}),\n    'node_modules/wrangler/package.json':'{\"version\":\"4.143.0\"}','node_modules/esbuild/package.json':'{\"version\":\"0.25.0\"}',\n    'node_modules/.package-lock.json':lock({'node_modules/wrangler':{version:'4.143.0'},'node_modules/esbuild':{version:'0.25.0'}})});\n  try{\n    assert.deepEqual(dependencyMismatches(dir),[]);\n    // Copied in by hand: Node resolution from node_modules/wrangler/** finds this one before node_modules/esbuild.\n    mkdirSync(join(dir,'node_modules/wrangler/node_modules/esbuild'),{recursive:true});\n    writeFileSync(join(dir,'node_modules/wrangler/node_modules/esbuild/package.json'),'{\"version\":\"0.25.0\",\"name\":\"esbuild\"}');\n    // And a top-level package the lockfile does not name, also copied in by hand.\n    mkdirSync(join(dir,'node_modules/extra'),{recursive:true});writeFileSync(join(dir,'node_modules/extra/package.json'),'{\"version\":\"9.9.9\",\"name\":\"extra\"}');\n    assert.deepEqual(dependencyMismatches(dir),[],'ACTUAL: nothing is reported; only npm\\'s own record (node_modules/.package-lock.json) is compared, never the tree');\n  }finally{rmSync(dir,{recursive:true,force:true});}\n});\n\ntest('F-D: an untracked source file hidden by .git/info/exclude is built and uploaded while the record says dirty:false',()=>{\n  const build=\"import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';mkdirSync('dist',{recursive:true});writeFileSync('dist/index.html',readFileSync('src/runtime.ts','utf8')+readFileSync('src/extra.ts','utf8'));\";\n  const upload=\"import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';import {join} from 'node:path';const out=process.argv[process.argv.indexOf('--outdir')+1];mkdirSync(out,{recursive:true});writeFileSync(join(out,'index.js'),readFileSync('src/extra.ts','utf8'));console.log('synthetic local upload, no network');\";\n  const dir=tree({'package.json':JSON.stringify({type:'module',scripts:{test:'node --test tests/*.test.mjs'}}),'tests/a.test.mjs':\"import test from 'node:test';test('ok',()=>{});\",\n    'src/runtime.ts':'committed','.gitignore':'deploy-records/\\nnode_modules/\\ndist/\\n.env*\\n','package-lock.json':'{\"lockfileVersion\":3,\"packages\":{\"\":{}}}','wrangler.jsonc':'{}','public/_headers':'/*\\n',\n    'node_modules/.package-lock.json':'{\"lockfileVersion\":3,\"packages\":{\"\":{}}}',\n    'node_modules/typescript/package.json':'{\"version\":\"1.0.0\"}','node_modules/typescript/bin/tsc':'',\n    'node_modules/vite/package.json':'{\"version\":\"1.0.0","severity":"low","snippet":"  for(const k of Object.keys(hidden.packages))if(k.startsWith('node_modules/')&&!names.has(k))out.push('not in package-lock.json: '+k);","title":"deploy.mjs: \"any installed package the lockfile does not name is refused\" is not what the check does; a package directory placed by hand (including one that shadows the locked copy) passes"},{"citation":"resolved","description":"All three tree reads use `git status --porcelain --untracked-files=normal`, which applies the committed .gitignore files and also the repository-local .git/info/exclude and the user's core.excludesFile. Those two are outside every commit, so a new file listed there (a developer's local-only module imported by tracked code, or a file matched by a global pattern) is read by vite and bundled by wrangler while the record says `dirty:false`, `source.unchangedDuringUpload:true`. The same gap applies to paths marked `skip-worktree`/`assume-unchanged`. It needs the operator's own local git configuration, so this is an integrity note rather than an attack. Fix: list untracked files with only the committed ignore rules, e.g. `git ls-files --others --exclude-per-directory=.gitignore` (and `git ls-files -v | grep '^[Sh]'` for skip-worktree/assume-unchanged), and refuse when either is non-empty on a real deploy; record both in the manifest.","line":171,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic deploy fixture (as in tests/deploy.test.mjs \"clean\"): commit everything; then `echo src/extra.ts >> .git/info/exclude`, write src/extra.ts (\"NOT IN ANY COMMIT\"); the fake vite reads it into dist and the fake wrangler writes it into the bundle. `git status --porcelain --untracked-files=normal` prints nothing. Expected: refusal (\"working tree is not clean\") or at least dirty:true. Actual: `node scripts/deploy.mjs` exits 0, manifest.dirty is false, source.unchangedDuringUpload is true, and deploy-records/<name>/worker/index.js contains \"NOT IN ANY COMMIT\". `git ls-files --others --exclude-per-directory=.gitignore` lists src/extra.ts. Test \"F-D\" in tests/repro-deploy.test.mjs (source in the previous finding) passes against this code.","severity":"info","snippet":"  const commit=git('rev-parse','HEAD'),porcelain=git('status','--porcelain','--untracked-files=normal');","title":"deploy.mjs: the clean-tree check honours .git/info/exclude and the global excludes file, which no commit names; an untracked source file hidden there is built and uploaded under dirty:false"},{"citation":"resolved","description":"The L-5 remediation refuses VITE_* variables and a non-production NODE_ENV because shell state outside git would change the build. NODE_OPTIONS is the same class and is passed through unchanged to every child (`env={...process.env}` in runStep, `{...process.env,...env}` in run): `--import`, `--require`, `--loader`/`--experimental-loader` run arbitrary code inside vite and inside wrangler's esbuild bundling (an APM agent preloaded from a shell profile is a common real-world example). The resulting dist/ and worker/index.js are hashed and uploaded as if they came from the commit; the manifest has no NODE_OPTIONS field. Fix: on a real deploy refuse a NODE_OPTIONS containing --import, --require, --loader or --experimental-loader (a dry run records it), and write `nodeOptions` (the value is not secret) into the manifest; spawning children with NODE_OPTIONS removed is a stronger alternative if no option is needed.","line":98,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Same synthetic fixture as the previous finding, clean tree, with NODE_OPTIONS=--import=<outside the repo>/hook.mjs where hook.mjs wraps fs.writeFileSync to replace \"abc\" with \"INJECTED\" (and calls syncBuiltinESMExports). Expected: refusal or a record naming the option. Actual: exit 0, manifest.dirty false, viteEnv [], nodeEnv null, no NODE_OPTIONS anywhere in the manifest, and deploy-records/<name>/worker/index.js (the bundle wrangler wrote and would upload) reads \"INJECTED\" instead of \"abc\". Test \"F-E\" in tests/repro-deploy.test.mjs (source two findings above) passes against this code.","severity":"info","snippet":"  const env={...process.env};delete env.NODE_TEST_CONTEXT;","title":"deploy.mjs: NODE_OPTIONS of the deploy shell is inherited by tsc, vite and wrangler; a --import/--require preload changes the uploaded bundle and the record does not name it"}],"hash":"61072fc29005dd22885cd1afe2bd64bc44bd2987ca3c3f4798160bf818ca6890","nodeId":"35206bc3-e365-4e8c-9fcd-b76d0583ef0f","outcome":"completed","summary":"The review is complete. Five findings are in `.imd-findings.json` at the repository root (3 Low, 2 Info), each with a reproduction that I ran against the Submission15 code in a scratch workspace outside the repository. No files in the tree were changed.\n\n**Answers to the three questions**\n\n1. **finishReceipt (L-1 fix).** No false \"reverted\" found. The own-hash status-0x0 shortcut sits behind the sender, chain, block and nonce checks on line 231, so a receipt with another nonce, sender or chain still ends in `TRANSACTION_MISMATCH` with the intent kept, and the wallet's own pending copy still polls. After a reverted routed approval or payment the intent is released and `prepare` works again (verified by a run). One adjacent stuck state is reported as Low: a payment prompt that fails with any error other than 4001 after a confirmed approval leaves the approval hash on screen, the \"Check original transaction\" button does nothing, and the GOLD panel never offers the hashless-intent acknowledgement that EconomyPane has.\n\n2. **Crediting note.** Nothing treats it as credit, and polling is bounded by the 2 h cap. One Low: the after-expiry rule relies on the `mined` mark, which is only set when this browser's controller judged the receipt. The receipt poll ends after about 2 minutes and nothing re-polls without a click, so a payment that mines late is never marked and its note is dropped at expiry + 30 min while it is still waiting for finality.\n\n3. **deploy.mjs.** No false refusal found on a clean deploy. Three provenance gaps: the \"package the lockfile does not name\" check reads npm's hidden lockfile rather than the tree, so a hand-placed nested package that shadows a locked one passes (Low); `git status` honours `.git/info/exclude` and the global excludes file, so a locally excluded source file ships under `dirty:false` (Info); `NODE_OPTIONS` preloads reach vite and wrangler and change the bundle with nothing in the record (Info).\n\n**Coverage and limits.** I covered the three named areas in depth with the Access Control, Asymmetry and Trust Gap passes, plus the general checklist. No Solidity changed, so no Foundry proof applies. The project's own suites could not be run as-is because the public tree lacks several modules they import, so the reproductions use a stand-in for one missing module and the same provider fixture as the project's recovery tests.","treeHash":null,"usage":{"cachedInputTokens":3192218,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":61697,"runtime":"claude","turns":44,"wallClockMs":821489}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"382ebcfad965e0cd","findings":[],"hash":"9950b3205482700072c6aa1afb670fc7f5a00543dfa12a0c1a18ddba56f820ca","nodeId":"b27269d9-dc7b-441c-8f98-edf9ecb7051e","outcome":"failed","summary":"Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":1166}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"48cf2e19b6c54386","findings":[{"citation":"resolved","description":"dependencyMismatches(ROOT) runs exactly once, at the start, next to the first git status. The post-build re-check (lines 179-182) re-reads HEAD, git status and .env* but not node_modules, and the third read after wrangler (lines 196-197) re-reads the same three things again, not node_modules. The suite, tsc, vite build and wrangler's bundling take minutes (the real record shows 04:51:46 to 04:56:39), and node_modules is gitignored, so a package added, upgraded or linked meanwhile (an `npm install --no-save x`, an `npm link`, an `npm update` in another terminal on the same checkout, or a postinstall/prepare hook) is bundled by vite and wrangler, while the record states dependencies.mismatches:[] and dirty:false and the outcome exits 0. This is the same class as 12e0525d L-4 (edit between the re-check and the bundling) applied to the dependency tree the submission says it now refuses; the start-time check gives a false 'matches the lockfile' statement for the tree that was actually built.","line":172,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic fixture (same shape as tests/deploy.test.mjs 'clean'): lockfile v3 with node_modules/a 1.0.0, node_modules/.package-lock.json listing only a, clean git tree. The fake vite build additionally creates node_modules/left-pad/package.json and rewrites node_modules/.package-lock.json to list left-pad (exactly what a concurrent `npm install --no-save left-pad` does; package-lock.json is untouched, so git status stays clean). Run `node scripts/deploy.mjs`. Actual: '> wrangler deploy' runs, outcome {deployment:'uploaded',verification:'passed',exitCode:0}, manifest.dependencies.mismatches is [] and manifest.dirty is false. Running dependencyMismatches(root) immediately after the run returns ['not in package-lock.json: node_modules/left-pad']. Expected: the post-build check refuses before upload (like changedRefusal) and the third read marks the record dirty / unchangedDuringUpload:false when the dependency tree differs from the start. Fix: recompute dependencyMismatches(ROOT) in the post-build refusal (`??dependencyRefusal(dependencyMismatches(ROOT),dryRun)` at line 181) and include it in sourceUnchanged at line 197 (compare the JSON of the mismatch list, or hash node_modules/.package-lock.json at start, after build and after upload and record all three).","severity":"low","snippet":"  const env=envFiles(ROOT),viteEnv=viteEnvNames(process.env),dependencies=dependencyMismatches(ROOT);","title":"deploy.mjs: node_modules is checked against package-lock.json only once, before the multi-minute run; a tree that changes during tests/build/upload ships with \"mismatches: []\" and exit 0"},{"citation":"resolved","description":"L-5 was closed for Vite's inputs (VITE_*, NODE_ENV, .env*) but wrangler bundles worker/index.ts with esbuild and takes its own inputs from the shell: WRANGLER_BUILD_CONDITIONS (comma list replacing the default ['workerd','worker','browser'] package.json export conditions) and WRANGLER_BUILD_PLATFORM ('browser'|'node'|'neutral'), both read in wrangler's deployment-bundle/bundle.ts via getBuildConditionsFromEnv/getBuildPlatformFromEnv and put straight into the esbuild options; and CLOUDFLARE_ENV, which selects the named environment exactly like --env (documented on Cloudflare's system environment variables page), changing the Worker name/bindings/routes the upload targets. run() spawns wrangler with {...process.env,...env}, so all of them reach it. None is refused on a real deploy, none is listed in the manifest (viteEnv only matches /^VITE_/), so a Worker bundle resolved with different export conditions (e.g. a dependency's 'node' entry instead of its 'workerd' entry) is uploaded under a record that says the commit alone determined it. The uploaded bytes are hashed, but nothing in the record explains why they differ from a clean build of the same commit.","line":191,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Same synthetic fixture as the previous finding, with a fake wrangler that writes the env it sees into worker/index.js (real wrangler feeds the same variables into esbuild). Run `WRANGLER_BUILD_CONDITIONS=node,import CLOUDFLARE_ENV=staging node scripts/deploy.mjs`. Actual: no refusal, exit 0, manifest.viteEnv is [], manifest.nodeEnv is null, nothing in the manifest mentions the two variables; worker/index.js contains 'conditions=node,import ... env=staging', i.e. a different bundle than the same commit builds in a clean shell. Expected: a real deploy refuses them (names only, like viteEnvRefusal) and a dry run lists them; alternatively pass wrangler an explicit environment with those keys removed (and record that). Fix: add `wranglerEnvNames=env=>Object.keys(env).filter(k=>/^(WRANGLER_BUILD_|CLOUDFLARE_ENV$|WRANGLER_ENV$)/.test(k))` to the refusal chain at line 173 and to the manifest.","severity":"low","snippet":"  const w=run(['node_modules/wrangler/bin/wrangler.js','deploy',...(dryRun?['--dry-run']:[]),'--outdir',join(tmp,'worker')],\n    {capture:true,env:{WRANGLER_LOG_PATH:join(tmp,'wrangler-logs'),WRANGLER_SEND_METRICS:'false'}});","title":"deploy.mjs: wrangler's own build-affecting shell variables (WRANGLER_BUILD_CONDITIONS, WRANGLER_BUILD_PLATFORM, CLOUDFLARE_ENV) are passed through to the upload and neither refused nor named in the re"},{"citation":"resolved","description":"The reverted case asked about in L-1 does recover (transaction and sendAttempted are cleared, purchaseStage returns 'review', prepare() runs inspect again and the allowance/paid checks pass). The replaced case does not recover here. MetaMask's Speed up / Cancel re-sends the same nonce under a new hash; the wallet-returned hash H1 then never gets a receipt, so waitReceipt ends in 'unknown' with error PENDING_TRANSACTION and the version-2 intent (with hash) stays in localStorage. checkReceipt(hash) in economyWallet.ts supports a pasted replacement hash (nonce, sender, chain and finality checked) and clearUnknownAfterWalletCheck refuses a v2 intent that has a hash, so the only resolution is a pasted hash. The GOLD panel calls checkReceipt() with no argument (this line) and has no hash input; GoldPurchase shows only 'Check original transaction', which polls H1 again (40 x 3 s) and returns to 'unknown'. Every later purchase from this browser for this address throws PENDING_TRANSACTION (prepare, line 166 of economyWallet.ts). The staking pane (EconomyPane.tsx) has the paste form, but nothing in the GOLD panel says so, and the shared pending key is the same for both panes. The crediting note itself ends correctly (credited at finality if the speed-up paid, or 30 min after expiry if cancelled), so this is availability of further purchases, not a display or credit error.","line":111,"path":"Submission15/source/src/world/WorldGoldPanel.tsx","reproduction":"State: wallet sent pay(orderId,amount) with nonce 0x8, hash H1 (phase 'pending'); the user clicks Speed up in MetaMask, which mines H2 (same nonce, same calldata, higher gas). eth_getTransactionReceipt(H1) is null for ever. Actual: waitReceipt → phase 'unknown', error PENDING_TRANSACTION, store still holds the intent; 'Check original transaction' → checkReceipt() → candidate=H1 → null again → 'unknown'; prepare() for any order → PENDING_TRANSACTION. Same in tests/economy-wallet-recovery.test.mjs's fixture: after confirm() with f.candidate({...routed([]),hash:OTHER}) (receipt only under OTHER), checkReceipt() never resolves the intent while checkReceipt(OTHER) does once finalized. Expected: the GOLD panel offers the replacement-hash input (or a link to the staking pane's) when wallet.phase==='unknown' && wallet.hash, so the user can resolve the intent with H2. Fix: pass a hash from an input into check (check={hash=>void run(()=>controller!.checkReceipt(hash))}) and render the same form EconomyPane.tsx renders.","severity":"low","snippet":" <GoldPurchase data={data} selected={selected} wallet={wallet} config={configRef.current} busy={busy} zh={zh} pending={pending} quote={product=>void run(()=>quote(product))} prepare={()=>selected&&void run(()=>pay(selected))} approve={()=>void run(()=>controller!.approve())} confirm={()=>void run(()=>controller!.confirm())} check={()=>void run(()=>controller!.checkReceipt())} reconcile={()=>selected&&void run(()=>client.reconcile(selected.id))} clear={()=>setSelected(null)}/>","title":"GOLD panel: a payment whose wallet hash never mines (cancelled or sped up in MetaMask, nonce consumed by another hash) has no way out inside the GOLD panel; its only button re-polls the dead hash"},{"citation":"resolved","description":"The server books a payment event against a pending order with no expiry condition (Submission13/source/server/economy-recovery.ts line 287: status='pending' AND EXISTS payment event; the purchase copy says 'Any payment already sent will still be reconciled'). `mined` is set only when this browser's controller judged the receipt (WorldGoldPanel markPendingCreditMined on step 'execute' && phase 'confirmed'). A payment sent with low gas, with the tab closed or the page reloaded before the receipt (after a reload the restored intent is 'unknown' until the user clicks Check), has mined:false. If it mines later than quote expiry + 30 min, settlePendingCredits drops the note while the payment is on its way; the GOLD then appears without a note at finality. Display only, conservative direction (a note disappears, none is invented), and the alternative accepted in L-3 (a cancelled payment's note living 2 h) is worse, so this is recorded as the residual the submission asked about, not a defect to fix at any cost.","line":62,"path":"Submission15/source/src/world/pendingGoldCredit.ts","reproduction":"NOW = order.created_ms; order.expires_ms = NOW+15 min; trackPendingCredit(W,ID,TX,'1000',NOW); no markPendingCreditMined (tab closed). Payment TX mines at NOW+50 min, finality at about NOW+63 min, cron books it after. settlePendingCredits(W,[{...order,status:'pending'}], NOW+45 min+1 ms) → the note is removed (pendingGold(W)=0n) at NOW+45 min; at NOW+63 min the order turns 'paid' and the balance rises with no 'crediting' shown in between. Expected under the 'mined' design: the note stays while the payment is still bookable. Possible mitigation: on the WalletBalanceHud's 30 s check, before dropping an unpaid expired note, let the panel's controller (or a one-off eth_getTransactionReceipt of p.hash through the already-connected provider) mark it mined; or drop at expiry+30 min only when the wallet's intent for that hash is no longer pending in localStorage.","severity":"info","snippet":" const keep=list.filter(p=>{const o=order(p);return !(o&&(orderCredited(o)||!p.mined&&o.status==='pending'&&now>o.expires_ms+PENDING_CREDIT_AFTER_EXPIRY_MS));});","title":"pendingGoldCredit.ts: the 30-minute after-expiry rule ends the note for a late-mined payment the wallet did not see mined, although the server still credits that payment"},{"citation":"resolved","description":"For `own && status 0x0` the branch at line 243 is skipped (tx.hash===candidate and done), so one unfinalized block with a reverted receipt ends the intent: transaction=null, sendAttempted=false, phase 'error'/TRANSACTION_REVERTED, and the panel drops the crediting note by order and by hash. The transaction bytes are unchanged by a reorg; if the block is replaced and the same transaction is re-included where its revert condition no longer holds (a revert that depended on another sender's state in the dropped block, e.g. an EMBEO transfer into this wallet, or a paused payment contract being unpaused), the payment succeeds on the canonical chain while the page said reverted and the user is invited to pay again. The second payment is stopped by inspect()'s paid(address,orderId,amount) check once the first is visible, and the server credits the order at finality regardless, so money is not lost; the harm is the wrong message, the dropped note and a possible duplicate prompt. This matches the pre-existing direct-send (`same`) path, which the submission deliberately mirrored, and mainnet one-block reorgs are rare, so it is recorded as a trust assumption of the design.","line":239,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs: f.finalized=false; f.candidate(routed([],{status:'0x0'})); await checkReceipt() → phase 'error', error TRANSACTION_REVERTED, store empty (the submission's own L-1 test). State on chain afterwards: block 0x20 is orphaned and the same transaction mines with status 0x1 in block 0x21. Actual: the page keeps 'reverted', the note is gone, the intent is released. Expected if finality were required for 0x0 as it is for 0x1-without-effect: TRANSACTION_NOT_FINALIZED until the finalized block passes 0x20, then the judgement is made on the canonical receipt. Minimal change if the owner wants it: keep the immediate judgement but re-run the canonical-block check (line 246-247) for 0x0 as well, or wait for finality only when the receipt's block is within the last few blocks.","severity":"info","snippet":"  const done=same||own&&r.status==='0x0'||own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);","title":"economyWallet.ts: the wallet's own routed hash with status 0x0 is judged reverted without the finality check, so a reorg that re-includes the same transaction successfully leaves the intent released a"},{"citation":"resolved","description":"Vite exposes to import.meta.env only the keys that start with one of envPrefix (default 'VITE_'), compared with String.prototype.startsWith, i.e. case-sensitively; `vite_debug=1` never reaches the bundle on a case-sensitive OS. The deploy tool refuses it anyway (the submission's own test asserts ['VITE_API','vite_debug']). Harmless on Windows (case-insensitive environment) and only a false refusal elsewhere; noted because the request asked for false refusals on a clean deploy.","line":55,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"On Linux: `vite_foo=1 npm run deploy` with a clean tree. Actual: 'VITE_* environment variables would change the build but are not in git (vite_foo); unset them', exit 1, no build. Expected: no refusal (the bundle is identical with or without vite_foo). Fix: drop the i flag on non-Windows, or keep it and say so in the message.","severity":"info","snippet":"export const viteEnvNames=env=>Object.keys(env).filter(k=>/^VITE_/i.test(k)).sort();","title":"deploy.mjs: viteEnvNames matches VITE_ case-insensitively while Vite's envPrefix match is case-sensitive, so a lower-case vite_* variable is a false refusal on Linux"},{"citation":"resolved","description":"initial() (line 35) has no sentHash key and set() merges patches, so after any judged receipt (line 256 sets sentHash) the value persists through prepare(), payGold() (line 279) and every later phase of the next intent until that intent is judged. In WorldGoldPanel `sent=wallet.sentHash??wallet.hash` therefore names the previous payment while a new one is being prepared or pending (wallet.hash is null or the new hash, sentHash still the old one). Today no consumer fires in those phases (the drop effect needs paymentFailed, the mined effect needs 'confirmed', both set together with a fresh sentHash at line 256), so there is no visible effect; it is a latent hazard for any future effect keyed on `sent` during 'pending'/'unknown' (for instance a drop-on-unknown rule), which would then hit the previous order's note.","line":168,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture of tests/economy-wallet-recovery.test.mjs: prepare(payPlan()); confirm(); candidate(routed([effect.paid(OID,1000n)])); checkReceipt() → state.sentHash===HASH. Then prepare({kind:'stake',shares:'1'}) (or a second pay plan). Actual: state.hash===null, state.phase==='confirm', state.sentHash===HASH (previous intent). Expected: sentHash null until this intent is judged. Fix: add sentHash:null to initial() (and to the EMPTY constant in WorldGoldPanel.tsx).","severity":"info","snippet":"  this.set({...initial(),phase:'checking',busy:true,plan:structuredClone(plan)});await this.inspect();","title":"economyWallet.ts: sentHash is not reset by initial(), so a judged hash from a previous intent survives into the next prepare()/payGold() state"}],"hash":"a0657d1cdb7baaa966f3d3331bfe571dbe8b9ef84ca42c0fe9b32b273ba0f16a","nodeId":"d0e913ef-c7fd-4d53-8d8d-8eef8ef25e0b","outcome":"completed","summary":"Review complete. The findings file holds 7 substantiated findings, every snippet checked against the tree, and nothing else in the repository was changed.\n\n**Headline:** no Medium or above. The three asked-about mechanisms hold on their central claims. A successful payment, approval, stake or withdrawal cannot be shown as reverted by the new rule, because `own` still requires the wallet's own hash with a matching sender and nonce before status 0x0 is judged. No path treats the \"+N crediting\" note as credit; only `orderCredited` from the ledger does. A reverted approval or payment recovers, since the intent is released and `prepare()` re-runs the allowance and paid checks. The deploy tool does not falsely refuse a clean deploy.\n\n**Low (3)**\n- **Deploy tool re-checks git and .env three times but node_modules once.** A package landing in node_modules during the multi-minute run (for example a concurrent `npm install --no-save`) is bundled, while the record says `mismatches: []`, `dirty: false`, exit 0. Reproduced in a synthetic fixture; `dependencyMismatches` run right after the deploy reports the extra package. Fix is to recompute it in the post-build refusal and the third read.\n- **Wrangler's own build inputs pass through unrefused and unrecorded.** `WRANGLER_BUILD_CONDITIONS` and `WRANGLER_BUILD_PLATFORM` go straight into wrangler's esbuild options, and `CLOUDFLARE_ENV` selects the deploy environment. The manifest names none of them. Reproduced: the bundle differs, the record is identical to a clean one.\n- **GOLD panel has no exit for a sped-up or cancelled payment.** The wallet's hash never mines, the controller's only resolution is a pasted replacement hash, and the GOLD panel calls `checkReceipt()` without one. Further purchases from that browser throw PENDING_TRANSACTION. The staking pane has the paste form, so this is availability, not a permanent lock. The reverted case asked about in L-1 does recover.\n\n**Info (4)**\n- The 30-minute after-expiry rule drops the note for a late-mined payment the wallet never saw mined, although the server still credits it. Display only, conservative direction, recorded as the residual the request asked for.\n- The own-hash 0x0 judgement skips finality, so a one-block reorg re-including the same transaction successfully leaves a wrong \"reverted\" message. Mirrors the pre-existing direct-send path. Double payment is blocked by the on-chain paid check.\n- `viteEnvNames` matches case-insensitively while Vite's prefix match is case-sensitive. A lower-case `vite_*` variable is a false refusal on Linux.\n- `sentHash` is not reset by `initial()`, so the previous intent's hash survives into the next `prepare()`. No consumer fires in those phases today, so it is latent only.\n\n**Coverage and limits.** All three areas were read in full with the Execution Trace, Periphery and First Principles passes, plus the server's reconciliation query to confirm late payments are credited. The wallet and note test suites could not be run here because the public copy has no node_modules, so the wallet-side reproductions are stated against the submission's own fixture rather than executed. The two deploy-tool reproductions were executed against the shipped script.\n\nSources: [Cloudflare system environment variables](https://developers.cloudflare.com/workers/wrangler/system-environment-variables/), [wrangler bundle.ts](https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/src/deployment-bundle/bundle.ts), [Vite env.ts](https://raw.githubusercontent.com/vitejs/vite/main/packages/vite/src/node/env.ts)","treeHash":null,"usage":{"cachedInputTokens":1751722,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":44298,"runtime":"claude","turns":33,"wallClockMs":655138}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddcecd7483f623f7","findings":[{"citation":"resolved","description":"L-5 made a real deploy refuse a node_modules that differs from package-lock.json, and L-4 made the tool re-read HEAD, `git status` and `.env*` after the build and again after wrangler. `dependencyMismatches(ROOT)` is evaluated only at line 172, before `npm test`, tsc and vite build (minutes), and is not part of the pre-upload re-check (line 181) nor of the third read (line 196: `commitAfter`, `porcelainAfter`, `envAfter` only). node_modules is gitignored, so `git status --porcelain` never shows it. A version swap, an extra package (`npm install --no-save x`, or an `npm install` triggered by another shell/editor) or a changed npm install record made after line 172 and before wrangler finishes is bundled into worker/index.js and the client dist, uploaded, and recorded as `dependencies.mismatches: []`, `dirty: false`, `source.unchangedDuringUpload: true`, exit code 0. The record therefore names a dependency state that is not the one that shipped. This is the same window the requester closed for source files and .env files; the dependency check was simply left out of the two later reads. The acknowledged residual ('a file edited inside an installed package at the locked version is not detected') is different: here the existing check would detect the change, it is just not re-run. Fix: compute `dependencyMismatches(ROOT)` again at the pre-upload re-check (refuse like `changedRefusal`) and at the third read (mark `dirty`, `source.unchangedDuringUpload:false`, fail the outcome), and write the post-upload result into the manifest; cheaper variant: hash `node_modules/.package-lock.json` and every locked `package.json` once at the start and compare the digest at both later reads.","line":172,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Synthetic fixture built like tests/deploy.test.mjs ('clean' variant) with package-lock.json locking node_modules/a@1.0.0 and node_modules/.package-lock.json agreeing, a fake vite build that (variant 'version-swapped') overwrites node_modules/a/package.json with version 9.9.9, or (variant 'extra-installed') adds node_modules/evil and lists it in node_modules/.package-lock.json, and a fake wrangler that bundles node_modules/a/package.json into worker/index.js. Expected: refusal (nothing uploaded) or at least dirty:true / exit 1, as for an edited source file. Actual, both variants (run 2026-10-27 with Submission15/source/scripts/deploy.mjs copied into the fixture): exit 0; outcome {deployment:'uploaded',verification:'passed',exitCode:0}; manifest.dirty=false, manifest.dependencies.mismatches=[], manifest.source.unchangedDuringUpload=true; the uploaded worker/index.js contains {\"version\":\"9.9.9\"}. Running `dependencyMismatches(dir)` on the fixture after the run returns ['node_modules/a 9.9.9 installed, 1.0.0 locked'] resp. ['not in package-lock.json: node_modules/evil'], so the existing check would have caught it. Script (node run.mjs <repo> version-swapped|extra-installed):\nimport {mkdirSync,writeFileSync,copyFileSync,readdirSync,readFileSync,mkdtempSync} from 'node:fs';\nimport {join,dirname} from 'node:path';\nimport {spawnSync} from 'node:child_process';\nconst REPO=process.argv[2],variant=process.argv[3];\nconst tree=files=>{const dir=mkdtempSync(join(process.env.TMPDIR,'deploy-'));for(const [p,c] of Object.entries(files)){mkdirSync(dirname(join(dir,p)),{recursive:true});writeFileSync(join(dir,p),c);}return dir;};\nconst lock=packages=>JSON.stringify({lockfileVersion:3,packages:{'':{name:'x'},...packages}});\n// The fake `vite build` is where the test suite, tsc and vite run: a sibling process (or the build itself) changes node_modules here.\nconst build=\"import {mkdirSync,writeFileSync} from 'node:fs';mkdirSync('dist',{recursive:true});writeFileSync('dist/index.html','abc');\"+\n (variant==='version-swapped'?\"writeFileSync('node_modules/a/package.json','{\\\"version\\\":\\\"9.9.9\\\"}');\":\n  variant==='extra-installed'?\"mkdirSync('node_modules/evil',{recursive:true});writeFileSync('node_modules/evil/package.json','{\\\"version\\\":\\\"1.0.0\\\"}');writeFileSync('node_modules/.package-lock.json',JSON.stringify({lockfileVersion:3,packages:{'':{},'node_modules/a':{version:'1.0.0'},'node_modules/evil':{version:'1.0.0'}}}));\":\n  variant==='dist-edited-after-build'?\"\":'');\nconst upload=\"import {mkdirSync,writeFileSync,readFileSync} from 'node:fs';import {join} from 'node:path';const out=process.argv[process.argv.indexOf('--outdir')+1];mkdirSync(out,{recursive:true});writeFileSync(join(out,'index.js'),readFileSync('node_modules/a/package.json','utf8'));console.log('synthetic local upload, no network');\";\nconst dir=tree({'package.json':JSON.stringify({type:'module',scripts:{test:'node --test tests/*.test.mjs'}}),'tests/a.test.mjs':\"import test from 'node:test';test('ok',()=>{});\",\n 'src/runtime.ts':'committed','.gitignore':'deploy-records/\\nnode_modules/\\ndist/\\n.env*\\n','package-lock.json':lock({'node_modules/a':{version:'1.0.0'}}),'wrangler.jsonc':'{}','public/_headers':'/*\\n',\n 'node_modules/.package-lock.json':lock({'node_modules/a':{version:'1.0.0'}}),'node_modules/a/package.json':'{\"version\":\"1.0.0\"}',\n 'node_modules/typescript/package.json':'{\"version\":\"1.0.0\"}','node_modules/typescript/bin/tsc':'',\n 'node_modules/vite/package.json':'{\"version\":\"1.0.0\"}','node_modules/vite/bin/vite.js':build,\n 'node_modules/wrangler/package.json':'{\"version\":\"1.0.0\"}','node_modules/wrangler/bin/wrangler.js':upload});\nmkdirSync(join(dir,'scripts'));copyFileSync(join(REPO,'Submission15/source/scripts/deploy.mjs'),join(dir,'scripts/deploy.mjs'));\nconst id=['-c','user.name=t','-c','user.email=t@example.invalid','-c','commit.gpgsign=false'];\nfor(const a of [['init','-q'],['add','-A'],['commit','-q','-m','fixture']]){const r=spawnSync('git',[...id,...a],{cwd:dir,encoding:'","severity":"low","snippet":"  const env=envFiles(ROOT),viteEnv=viteEnvNames(process.env),dependencies=dependencyMismatches(ROOT);","title":"deploy.mjs: node_modules is checked once before the tests; a package swapped or added during the run is bundled and uploaded, and the record says mismatches [] / dirty false / exit 0"},{"citation":"resolved","description":"Wrangler documents `CLOUDFLARE_ENV` as selecting the environment for commands run without `--env` (developers.cloudflare.com/workers/wrangler/system-environment-variables). With it set in the deploy shell, `wrangler deploy` at line 191 deploys the `env.<name>` section of wrangler.jsonc (different name, vars, routes, bindings) or, when wrangler.jsonc defines no environments, a Worker named `imd-world-<name>`; wrangler also then loads `.env.<name>` instead of `.env` into its own process (account id, API base URL). L-5 refused VITE_* and NODE_ENV because 'nothing in git names them'; the same holds for this variable: the commit, wrangler.jsonc hash and `manifest.wrangler` (exitCode, versionId only) do not say which environment or Worker received the upload. The manifest has no `name`, `environment` or `accountId` field; only the free-text wrangler.log would reveal it. `.env.<name>` is not matched by VITE_ENV_FILE either. Impact: a deploy run under a stray `CLOUDFLARE_ENV` publishes the bundle under another Worker/environment while the record reads like a production deploy; the production_state evidence (which compares the live Worker) is what would notice, not the record. Fix: refuse `CLOUDFLARE_ENV`/`WRANGLER_ENV`-style selectors (names only, like VITE_*) on a real deploy and list them on a dry run; add wrangler's reported Worker name and environment (or parse 'Uploaded <name>' from the log) and the account id to `manifest.wrangler`; extend the env-file regex to `.env.*` since wrangler, not only Vite, reads them.","line":191,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Input: shell with `CLOUDFLARE_ENV=staging` (or any name), clean tree, `npm run deploy`. Expected: refusal before tests, like `VITE_X=1` or `NODE_ENV=development`. Actual (by the code): `viteEnvNames` only matches /^VITE_/i, `nodeEnvRefusal` only NODE_ENV, so line 173 refuses nothing; line 191 spawns wrangler with the inherited environment; wrangler deploys to environment 'staging' (or Worker 'imd-world-staging'); the manifest written at lines 202-213 is identical to a production record except for the Version ID. Not run here (wrangler and credentials are not available on this machine); the behaviour of CLOUDFLARE_ENV is taken from Cloudflare's documentation fetched 2026-10-27.","severity":"low","snippet":"  const w=run(['node_modules/wrangler/bin/wrangler.js','deploy',...(dryRun?['--dry-run']:[]),'--outdir',join(tmp,'worker')],","title":"deploy.mjs: CLOUDFLARE_ENV (and the .env.<env> file wrangler loads with it) selects the Worker environment/name for the upload, is neither refused nor recorded, and the manifest names no Worker name, "},{"citation":"resolved","description":"Vite's loadEnv exposes a process.env key only when `prefixes.some(prefix=>key.startsWith(prefix))` with the default prefix 'VITE_' (case-sensitive, packages/vite/src/node/env.ts). The refusal uses /^VITE_/i, so a lowercase or mixed-case variable that cannot reach the bundle on Linux/macOS is refused as if it would. On Windows environment names are case-insensitive, which is presumably why the flag was chosen; there the stricter match is right. Fix: match /^VITE_/ exactly except when process.platform==='win32' (or keep /i but say in the message that the variable is refused by name only).","line":55,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"`node -e \"import('./Submission15/source/scripts/deploy.mjs').then(m=>{const n=m.viteEnvNames({PATH:'x',vite_debug:'1',Vite_Mode:'dev'});console.log(n,m.viteEnvRefusal(n,false))})\"` prints `[ 'Vite_Mode', 'vite_debug' ] VITE_* environment variables would change the build but are not in git (Vite_Mode, vite_debug); unset them`. Expected on Linux: no refusal, because `vite build` with these variables set produces the same bundle as without them.","severity":"info","snippet":"export const viteEnvNames=env=>Object.keys(env).filter(k=>/^VITE_/i.test(k)).sort();","title":"deploy.mjs: the VITE_* refusal is case-insensitive, so a shell variable Vite never loads (vite_debug, Vite_Mode) blocks a real deploy"},{"citation":"resolved","description":"`mined` is set only by WorldGoldPanel when the controller reaches phase 'confirmed' for the execute step, i.e. when finishReceipt saw the receipt. waitReceipt polls 40 x 3 s (about 2 minutes) and then leaves phase 'unknown' and stops; nothing polls afterwards unless the player presses 'Check original transaction', and the panel copy tells them they may go back to the World. A payment sent with a low fee that mines later than that never gets `mined:true`. Then the rule drops it 30 minutes after the quote expiry even though the payment is on chain or still pending and will be credited (the server matches late payments regardless of expiry, economy-recovery.ts 'late payments can match'). Timeline for the worst case (payment sent in the last seconds of the 15-minute quote at time T = expires): mined at T+m, final at about T+m+13, cron credit within a minute: credited at about T+m+14; dropped at T+30; so any payment that waits more than about 16 minutes in the mempool loses its note before the GOLD arrives, and the top bar shows no '+N crediting' while a real payment is on its way. The requester chose this tradeoff explicitly in the audit request; it is recorded here because the question asked whether such a case exists. Display only; no entitlement is affected. Fix options: have WalletBalanceHud's 30 s poll also ask the provider for the receipt of each note's hash while the note is unmined (one eth_getTransactionReceipt per poll, no signing) and set `mined` itself, or extend waitReceipt's attempts for the execute step.","line":62,"path":"Submission15/source/src/world/pendingGoldCredit.ts","reproduction":"State: wallet note {orderId:O, hash:H, atMs:T-5s, mined absent}; order O {status:'pending', expires_ms:T}; transaction H pending in the mempool until T+20min, then mined; controller gave up at T+2min (phase 'unknown'), panel closed. At T+30min+1ms `settlePendingCredits(W,[O],now)` returns false and removes the note (`pendingGold` 0). Server credits O at about T+34min. Expected: note until credit (as for a payment the wallet did see mined). Actual: 4 minutes with no note and the balance unchanged. Unit form: `trackPendingCredit(W,ID,TX,'1000',NOW); settlePendingCredits(W,[order({expires_ms:NOW+900000})],NOW+900001+PENDING_CREDIT_AFTER_EXPIRY_MS); pendingGold(W, ...) === 0n` (this is exactly the L-3 test; the difference is only that the transaction here is real and later credited).","severity":"info","snippet":" const keep=list.filter(p=>{const o=order(p);return !(o&&(orderCredited(o)||!p.mined&&o.status==='pending'&&now>o.expires_ms+PENDING_CREDIT_AFTER_EXPIRY_MS));});","title":"pendingGoldCredit.ts: the after-expiry rule ends the note while a slow payment is still in the mempool whenever the wallet's 2-minute receipt wait ended before it mined and the user did not press 'Che"},{"citation":"resolved","description":"With `own && r.status==='0x0'` the branch at line 243 (`eth_getBlockByNumber('finalized')` and canonical-hash check) is skipped, the pending record is deleted and the state becomes error/TRANSACTION_REVERTED; WorldGoldPanel then drops the crediting note and the purchase page returns to stage 'review', so the player can prepare and send a second payment for the same order at once. A receipt with status 0x0 in a block that is not yet final can be removed by a reorg; builders re-include the orphaned transactions, and a state-dependent revert (insufficient EMBEO balance or allowance at that moment, a Paid guard) can succeed in the new block. The player then sees 'reverted', pays again, and both payments mine (different nonces): the order is paid twice and the second EMBEO payment has no order. This is exactly the behaviour direct sends (`same`) have had since before Submission14 and what audit 12e0525d L-1 asked for, so it is reported as a trust/design note, not a regression: the shortcut trades the small reorg risk for an immediate verdict. Fix if wanted: for status 0x0 keep the immediate 'reverted' message but keep the intent (and the 'do not pay again' lock) until the block is final, releasing it automatically on the next check; or treat a reverted own hash like a reverted pasted hash (TRANSACTION_NOT_FINALIZED until final).","line":239,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Fixture as in tests/economy-wallet-recovery.test.mjs: `prepare(payPlan()); confirm(); finalized=false; candidate(routed([],{status:'0x0'})); checkReceipt()` gives phase 'error', error 'TRANSACTION_REVERTED' and store.size 0 although `eth_getBlockByNumber('finalized')` answers 0x1f, below the receipt's block 0x20 (the submission's own L-1 test asserts exactly this outcome). Expected under a strict reading of 'a successful payment must not be shown as reverted': hold the verdict (TRANSACTION_NOT_FINALIZED) until finalized >= blockNumber, as the pasted-hash path one line below does. Chain state that realises the harm: the player's EMBEO balance is short of the amount until an incoming transfer lands; in block N the transfer is ordered after the payment (nonce 8), so the payment reverts; the UI reads the receipt, says reverted, deletes the intent; block N is reorged out; in block N' the transfer precedes the payment and the payment succeeds. The player, told 'reverted', has meanwhile prepared and sent a second payment (nonce 9) for the same order.","severity":"info","snippet":"  const done=same||own&&r.status==='0x0'||own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);","title":"economyWallet.ts finishReceipt: the wallet's own reverted hash is judged final without the finality check, so a reverted receipt in a block that is later reorged can be shown as 'reverted' and the int"}],"hash":"a8cbf0e06a48fa6ffc665019b4eb7b9a354f8f997915b69b857787201abf092e","nodeId":"5cf4b4ad-5628-48ab-97dc-cc7c64a60b32","outcome":"completed","summary":"Review complete. The findings file `.imd-findings.json` holds 5 findings (2 Low, 3 Info), each with path, line and snippet verified against the tree. No Solidity changed, so no Foundry proofs were needed.\n\n**Question 1, finishReceipt (L-1).** The new `own && status 0x0` branch is sound: before it runs, the receipt and `eth_getTransactionByHash` record must agree on hash, sender, chain, block, and this intent's nonce, and a wallet-local pending copy still returns \"poll again\". A reverted own hash therefore consumed this intent's nonce and did nothing. Recovery works: the pending record is cleared, the purchase page returns to \"review\", and `prepare` is accepted again for both a reverted approval and a reverted payment. The one residual, filed as Info, is that the verdict is given before finality, so a reverted receipt in a block that is later reorged can be re-included and succeed while the player has already been told \"reverted\" and may pay again. Direct sends have always behaved this way and the previous audit asked for this, so it is a design note.\n\n**Question 2, crediting note.** Every ending the request lists is implemented and nothing reads the note as credit: the HUD and the purchase page only render it, and `purchaseStage` uses the server's book. Polling is bounded by the 2-hour cap. The server matches late payments regardless of expiry, so the 30-minute rule never hides a payment that cannot be credited. One Info: a payment that sits in the mempool for more than about 16 minutes after the wallet's 2-minute receipt wait ended, with the player not pressing \"Check\", loses its note before the GOLD arrives, because `mined` is set only from the panel's confirmed phase. The requester chose this tradeoff; I recorded the concrete timeline.\n\n**Question 3, deploy tool.** Two Low findings and one Info:\n- **node_modules is checked once**, at the start. A package swapped or added during tests, tsc, vite or wrangler is bundled and uploaded, and the record says `mismatches: []`, `dirty: false`, exit 0. Reproduced with a synthetic fixture modelled on the project's own deploy test; the existing check would have caught the change had it been re-run at the pre-upload and post-upload reads.\n- **CLOUDFLARE_ENV** selects wrangler's environment or Worker name, is neither refused nor recorded, and the manifest names no Worker name, environment or account. Documented wrangler behaviour; wrangler itself is not available here.\n- **Lowercase `vite_*` variables are refused** although Vite's prefix match is case-sensitive, a false refusal on Linux and macOS.\n\n**Out of scope but worth noting:** a `TRANSACTION_MISMATCH` on the wallet's own hash (a nonce race with another dapp) leaves a version-2 intent with a hash that neither `checkReceipt` nor `clearUnknownAfterWalletCheck` can release, a stuck lock that predates this submission.","treeHash":null,"usage":{"cachedInputTokens":2112442,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":49112,"runtime":"claude","turns":39,"wallClockMs":838057}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0cf632e317dfab7a","findings":[{"citation":"resolved","description":"The tracking effect depends on [payingOrder,payingHash,data]. `data` is replaced by useGold every 12 s (setInterval refresh) and on every focus/visibility/gold-changed event, so the effect re-runs for as long as the panel is open and the wallet stays in phase 'confirmed' with step 'execute' and a pay plan (nothing resets that state until the next prepare). trackPendingCredit only skips when readPendingCredits already holds the order, and readPendingCredits filters out entries older than PENDING_CREDIT_MAX_MS (2 h, pendingGoldCredit.ts:27). So at atMs+2h the entry drops out of the read, and the next refresh (at most 12 s later) writes a new entry with atMs=now and no `mined` flag (markPendingCreditMined then re-marks it on the same data change). The 'ends after 2 h' guarantee stated in pendingGoldCredit.ts:11 therefore holds only once the panel is closed or the controller disposed; while the panel is open, a payment the ledger never books (a wallet-confirmed own hash is judged at inclusion without finality, economyWallet.ts:243, so a reorged-out block; or a server-side reconcile error) keeps '+N crediting' in the top bar and the purchase page indefinitely, and WalletBalanceHud keeps its 30 s ledger poll (usePendingGold restarts because `gold` changes) for the same time. Display only: nothing reads the note as credit (purchaseStage and GoldPurchase take 'credited' from orderCredited only). Fix: make the tracking effect run once per (orderId, hash) (a ref of tracked pairs, or track in the same code path that observes the phase change to 'pending'), or let trackPendingCredit refuse to re-add an orderId that the raw store (stored(), not the time-filtered read) already holds, and have settlePendingCredits prune expired entries. Also consider holding `mined` across the re-add.","line":81,"path":"Submission15/source/src/world/WorldGoldPanel.tsx","reproduction":"State: GOLD panel open; wallet state {phase:'confirmed',step:'execute',plan:{kind:'pay',payment:{orderId:ID}}}, data.orders contains ID with status 'pending' (server never books it), note {orderId:ID,hash:TX,atMs:T0,mined:true}. Expected per pendingGoldCredit.ts:11 and the audit request: the note is gone at T0+2h. Actual: at T0+2h pendingGold()=0 for one render, then the next data refresh (<=12 s) calls trackPendingCredit(data.wallet,ID,TX,gold) which saves a fresh entry {atMs:T0+2h+<=12s}; pendingGold()=gold again until T0+4h, and so on while the panel stays open. Verified against the module itself with Node (localStorage shim): trackPendingCredit(W,ID,TX,'1000',NOW); markPendingCreditMined(W,TX,NOW); pendingGold(W,NOW+PENDING_CREDIT_MAX_MS) -> 0n; trackPendingCredit(W,ID,TX,'1000',NOW+PENDING_CREDIT_MAX_MS) -> saved (list.some(...) was false because the old entry is filtered out); pendingGold(W,NOW+2*PENDING_CREDIT_MAX_MS-1) -> 1000n.","severity":"low","snippet":" useEffect(()=>{const o=payingOrder?data?.orders.find(o=>o.id===payingOrder):null;if(!data||!o||o.kind!=='topup'||!payingHash||o.status==='paid')return;\n  trackPendingCredit(data.wallet,o.id,payingHash,o.gold);},[payingOrder,payingHash,data]);","title":"2 h cap on the \"+N crediting\" note is defeated: WorldGoldPanel re-creates the note on every ledger refresh while a confirmed payment stays on screen"},{"citation":"resolved","description":"The 30-minute margin after expires_ms is compared with client time only; the `mined` exemption is set solely by WorldGoldPanel when the controller itself reaches phase 'confirmed' (WorldGoldPanel.tsx:88-89). The controller polls a receipt for about 2 minutes (waitReceipt: 40 x 3 s) and then goes 'unknown'; it is disposed when the panel closes. A payment sent near the end of a 15-minute quote with a low fee can mine later than that (Ethereum inclusion under a low max fee commonly takes tens of minutes), so the sequence 'sent at T+14m, wallet poll ends 'unknown' at T+16m, user returns to the World, tx mines at T+40m' leaves the note without `mined`. The server still credits that payment: economy-recovery.ts sets economy_orders.status='paid' from the matching payment event (Submission13/source/server/economy-recovery.ts:287) with no expires_ms condition, and the purchase page itself promises 'Any payment already sent will still be reconciled against the original order' (GoldPurchase.tsx:54). Until the event is recorded at finality (~13 min after mining) and the cron books it, the order stays status 'pending', so at expires_ms+30 min (T+45m) settlePendingCredits (called every 30 s by WalletBalanceHud and on every panel data refresh) drops the note; the GOLD is credited at ~T+54m. The '+N crediting' indicator therefore ends while a real payment is on its way, exactly the case the request asks about, and nothing re-creates it (trackPendingCredit needs the plan, gone after reload or panel close). The reverse boundary also holds: a payment mined at T+30m finalizes at ~T+43m and is booked ~T+44m, one minute before the drop, so the margin is sized to finality from expiry, not to a late inclusion plus finality. Fix: have the drop rule ask the chain-independent ledger facts the panel already has rather than the clock alone: keep the note while the server reports the order as 'paid' (status 'paid' with creditedGold below gold is already excluded from the drop since status!=='pending'), and either widen the margin to cover a late inclusion plus finality (e.g. 60 min) or let the HUD poll the receipt of the stored hash once (eth_getTransactionReceipt via the provider) before dropping, setting `mined` when it exists. The 2 h cap still bounds everything.","line":62,"path":"Submission15/source/src/world/pendingGoldCredit.ts","reproduction":"Inputs: quote expires_ms=E; note {orderId:ID,hash:TX,atMs:E-60000} without `mined` (the wallet's receipt poll ended in 'unknown' and the panel was closed); the transaction TX is mined at E+25min and the server's order row still reads status:'pending' until finality at ~E+38min. Call settlePendingCredits(W,[order with status 'pending', expires_ms E], now=E+30min+1ms). Expected: the note stays because a payment is mined and will be credited. Actual: returns false and removes the note (verified with Node on the module: pendingGold -> 0n right after; the existing L-3 test asserts the same drop at NOW+900001+PENDING_CREDIT_AFTER_EXPIRY_MS). The GOLD then appears ~8 minutes later with no 'crediting' indication in between.","severity":"low","snippet":" const keep=list.filter(p=>{const o=order(p);return !(o&&(orderCredited(o)||!p.mined&&o.status==='pending'&&now>o.expires_ms+PENDING_CREDIT_AFTER_EXPIRY_MS));});","title":"After-expiry rule ends the note while a late-mined payment the wallet did not see mined is still on its way to the ledger"},{"citation":"resolved","description":"For `own` (the hash the wallet returned for this intent) with r.status==='0x0', `done` is true and tx.hash===candidate, so the finality branch at line 243 is skipped: the receipt is accepted from the latest block. The nonce check at line 231 binds the receipt to this intent's nonce, but it does not bind the final chain state: if the user sped the transaction up in the wallet (same nonce, same calldata, higher fee) and the reverted original was included in a block that is later reorged out, the speed-up can be included in its place and succeed (reverts caused by state that differs between the two forks, e.g. an allowance or balance set by another of the user's transactions in the dropped block, are the realistic case). By then the controller has already set phase 'error', error 'TRANSACTION_REVERTED', cleared the intent (transaction=null, sendAttempted=false, store entry removed) and WorldGoldPanel has dropped the crediting note by payingOrder and by sentHash (WorldGoldPanel.tsx:87). The server credits the successful speed-up at finality, so the user sees 'Transaction reverted' and no '+N crediting' for a payment that is in fact on its way, and is free to start a new payment for the same order (prepare()). The chain re-check in inspect() (paid(...) eth_call, line 148) prevents a second send only once the speed-up is mined. The direct-send path (`same`) has had the same pre-finality judgement for status 0x0 since before this submission; the L-1 change extends it to routed sends. The reorg window on mainnet is short (usually 1-2 blocks), hence Low. Fix (keeps L-1's intent of not calling it 'replaced'): for the own hash with status 0x0, keep `done` but still require the finality branch (`if(tx.hash!==candidate||!done||r.status==='0x0')`), and on TRANSACTION_NOT_FINALIZED keep polling instead of failing, so the reverted verdict is given only once the block is final; alternatively keep the immediate verdict but do not drop the crediting note for a reverted own hash until finality of that block.","line":239,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"State: intent {version:2,nonce:'0x8',hash:H} for pay(ORDER,amount); provider answers eth_getTransactionReceipt(H) with {status:'0x0',blockNumber:'0x20',blockHash:B}, eth_getTransactionByHash(H) with nonce '0x8' and blockHash B, and eth_getBlockByNumber('finalized') with number '0x1f' (block 0x20 not final). This is exactly the submission's own test 'reverted with finalized false' (tests/economy-wallet-recovery.test.mjs:91-96). Expected: no verdict until block 0x20 is final (or at least the note kept). Actual: state becomes {phase:'error',error:'TRANSACTION_REVERTED'}, the pending store is emptied, and the panel's effect runs dropPendingCredit(ORDER) and dropPendingCreditByHash(H). Then let block 0x20 be replaced by a block containing the same-nonce speed-up H2 with a Paid(ORDER,...) log: the ledger later credits ORDER while the page said reverted and shows no crediting note. Executed here against the submitted economyWallet.ts with the same fixture: own routed hash, receipt status '0x0', finalized block '0x1f' < receipt block '0x20' -> {phase:'error',error:'TRANSACTION_REVERTED',sentHash:H,store:0} without any eth_getBlockByNumber('finalized') gate.","severity":"low","snippet":"  const done=same||own&&r.status==='0x0'||own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);","title":"Own routed hash with status 0x0 is judged TRANSACTION_REVERTED before finality: a same-nonce speed-up mined after a reorg can make a successful payment read as reverted and drop its note"},{"citation":"resolved","description":"finishReceipt rejects the own hash with TRANSACTION_MISMATCH whenever record.nonce!==tx.nonce (line 231), where tx.nonce is the eth_getTransactionCount(address,'pending') value read before the prompt (line 192). MetaMask ignores a dapp-supplied `nonce` unless the user has enabled 'Customize transaction nonce'; it assigns the next nonce from its own tracker at confirmation time. If another transaction from the same account is confirmed between the count read and this prompt's confirmation (another dapp tab, a MetaMask-initiated send, a queued transaction confirmed first), the wallet returns a real hash H for this exact request mined with nonce 0x9 while the intent holds 0x8. Every checkReceipt then throws TRANSACTION_MISMATCH -> fail() -> phase 'unknown' with the transaction kept; prepare() and payGold() refuse with PENDING_TRANSACTION; and clearUnknownAfterWalletCheck returns at once because the v2 intent has a hash. The only exit is clearing localStorage by hand. The transaction itself may have succeeded (same calldata, status 0x1), so a successful payment, approval, stake or withdrawal is left permanently 'uncertain', and the crediting note (tracked at phase 'pending' with hash H) is neither marked mined nor dropped: it lives until the after-expiry rule or the 2 h cap. This code is unchanged by Submission15 (the binding exists since the nonce work), but the L-1 question asks about wrong-nonce releases and stuck locks, and this is the stuck side of that binding. Fix: for the own hash (tx.hash===candidate) accept a receipt whose from, chainId and calldata/intended effect match even when record.nonce differs, treating the nonce as the wallet's renumbering (the nonce binding stays decisive for pasted hashes), or provide a finality-gated acknowledgement path for a v2 intent whose own hash is mined with another nonce.","line":267,"path":"Submission15/source/src/world/economyWallet.ts","reproduction":"Inputs: intent {version:2,nonce:'0x8',from:A,to:PAY,data:pay(ORDER,1000),hash:H}; provider answers eth_getTransactionReceipt(H) {status:'0x1',from:A,blockNumber:'0x20',blockHash:B,logs:[Paid(ORDER,A,token,treasury,1000)]} and eth_getTransactionByHash(H) {hash:H,from:A,to:PAY,input:pay(ORDER,1000),value:'0x0',chainId:'0x1',nonce:'0x9',blockHash:B,blockNumber:'0x20'} (in the submission's fixture: f.candidate({hash:HASH,to:PAY,input:<pay calldata>,nonce:'0x9'})). Expected: the wallet's own hash for this request, with this sender and exactly this calldata, is judged (confirmed). Actual: checkReceipt() ends with error 'TRANSACTION_MISMATCH', phase 'unknown', store still holding the intent; a following prepare(payPlan()) ends with error 'PENDING_TRANSACTION'; clearUnknownAfterWalletCheck() returns undefined without changing state. Repeating checkReceipt() gives the same result every time. Executed here against the submitted economyWallet.ts (with Submission13's economyClient.ts, Node 24, the test suite's provider/lock/store fixture): after prepare(payPlan)+confirm(), with the provider answering the own hash mined at nonce '0x9' (status 0x1, exact pay calldata, matching Paid log), checkReceipt() -> {phase:'unknown',error:'TRANSACTION_MISMATCH',store:1}, a second checkReceipt() the same, clearUnknownAfterWalletCheck() left the state untouched, and prepare(payPlan) -> {phase:'unknown',error:'PENDING_TRANSACTION'}.","severity":"low","snippet":" async clearUnknownAfterWalletCheck(){if(!this.transaction||this.transaction.hash!==null&&this.transaction.version===2)return;return this.action(async()=>{","title":"A wallet-returned own hash whose mined nonce differs from the pre-read pending count is never judged and cannot be acknowledged: permanent PENDING_TRANSACTION lock for that address in this browser"},{"citation":"resolved","description":"Vite exposes to import.meta.env only the keys that start with the configured envPrefix using a case-sensitive startsWith ('VITE_' by default), so `vite_debug=1` or `vite_x` in the deploy shell never reaches the client bundle. viteEnvNames uses /^VITE_/i and viteEnvRefusal then refuses a real deploy ('VITE_* environment variables would change the build but are not in git (vite_debug)'). The submission's own test fixes this behaviour (tests/deploy.test.mjs asserts ['VITE_API','vite_debug']), so it is a deliberate over-approximation, but it is a refusal of a build that would be identical to a clean one, which question 3 asks to flag. On Windows, where variable names are case-insensitive, the broad match is harmless either way. Fix: use /^VITE_/ (case-sensitive) on POSIX, or keep the broad match but only list the lowercase names in the manifest instead of refusing.","line":55,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Run `vite_debug=1 node scripts/deploy.mjs` on a clean tree with no .env files and node_modules matching the lockfile. Expected: the deploy proceeds (vite build ignores vite_debug; `vite build` with and without it produces the same dist). Actual: exit 1 before `> npm test` with 'VITE_* environment variables would change the build but are not in git (vite_debug); unset them'. viteEnvNames({vite_debug:'1'}) returns ['vite_debug'].","severity":"info","snippet":"export const viteEnvNames=env=>Object.keys(env).filter(k=>/^VITE_/i.test(k)).sort();","title":"VITE_* refusal is case-insensitive while Vite's env prefix match is case-sensitive: a lowercase vite_* shell variable is a false refusal on a clean deploy"},{"citation":"resolved","description":"In a v2/v3 package-lock.json a `file:` or workspace dependency is recorded as `\"node_modules/foo\": {\"resolved\":\"pkgs/foo\",\"link\":true}` with the version on the target entry (`\"pkgs/foo\": {\"version\":\"1.0.0\"}`), and node_modules/foo is a symlink. The loop reads node_modules/foo/package.json through the symlink (version '1.0.0') and compares it with v.version (undefined), producing 'node_modules/foo 1.0.0 installed, undefined locked' and a refusal on a real deploy. The real repository has no such dependency today (the record shows mismatches:[]), so this is a latent false refusal that appears the first time one is added, with a message that does not say why. Fix: skip entries with `link:true` (or resolve them to lock.packages[v.resolved] and compare that version), and compare resolved/integrity only when the lock entry has them.","line":74,"path":"Submission15/source/scripts/deploy.mjs","reproduction":"Tree: package-lock.json {lockfileVersion:3,packages:{'':{},'node_modules/foo':{resolved:'pkgs/foo',link:true},'pkgs/foo':{name:'foo',version:'1.0.0'}}}, the same file as node_modules/.package-lock.json, pkgs/foo/package.json {version:'1.0.0'}, node_modules/foo -> ../pkgs/foo. Expected: dependencyMismatches(root) returns [] (installed exactly as locked). Actual (run here with Node 24 against the submitted scripts/deploy.mjs): ['node_modules/foo 1.0.0 installed, undefined locked'], so dependencyRefusal(...,false) refuses the deploy.","severity":"info","snippet":"    if(installed.version!==v.version)out.push(k+' '+installed.version+' installed, '+v.version+' locked');","title":"dependencyMismatches refuses any lockfile entry with link:true (file:/workspace dependency) because such entries carry no version"}],"hash":"f17d522c7298990ac95af48033f39939680a208145734038c657ad8522fad603","nodeId":"8fe94584-04d6-4c70-a063-a88f42882ab2","outcome":"completed","summary":"Review complete. The findings file holds six substantiated findings (4 Low, 2 Info), none critical or high, so no Foundry proof was required. No repository file was changed. All six snippets were verified line-for-line against the tree.\n\n**What I checked**\n\nI reviewed all four changed browser files, the deploy tool, the three test files, the cumulative diff, and the server-side crediting logic in Submission13 that the note lifecycle depends on. Four of the findings were executed against the submitted code with Node 24: the two wallet findings ran on the real controller with the suite's provider and store fixture, and the two note findings ran on the real pendingGoldCredit module with a localStorage shim.\n\n**Findings**\n\n- **Low, WorldGoldPanel.tsx:81.** The 2 h cap on \"+N crediting\" is not a cap while the panel is open. The tracking effect re-runs on every 12 s ledger refresh and re-creates the note with a fresh timestamp once the old entry ages out, so a payment the ledger never books stays \"crediting\" indefinitely and the HUD keeps its 30 s poll.\n- **Low, pendingGoldCredit.ts:62.** The after-expiry drop ends the note for a late-mined payment the wallet never saw mined. The wallet polls only about 2 minutes, the server credits late payments with no expiry condition, and a payment mined 25 minutes after expiry is dropped at expiry plus 30 while finality is still pending.\n- **Low, economyWallet.ts:239.** The own routed hash with status 0x0 is judged reverted with no finality gate. A same-nonce speed-up mined after a reorg can succeed while the page says reverted, the intent is released, and the note is dropped by sent hash. The direct path already behaved this way. The L-1 change extended it.\n- **Low, economyWallet.ts:267.** An own hash mined with a renumbered nonce is permanently TRANSACTION_MISMATCH, and the v2 intent cannot be acknowledged away. Executed: repeated checks, clear and a new prepare all fail, leaving a stuck PENDING_TRANSACTION lock. Pre-existing code, but it is the stuck side of the nonce binding the request asked about.\n- **Info, deploy.mjs:55.** The VITE_ filter is case-insensitive while Vite's prefix match is not, so a lowercase vite_ variable is a false refusal on a clean deploy.\n- **Info, deploy.mjs:74.** A lockfile entry with link:true carries no version, so a file or workspace dependency produces \"1.0.0 installed, undefined locked\" and a refusal. Executed on a synthetic tree. The real repository has none today.\n\n**Not found**\n\nNo path treats the note as credit. No case where a send intent is released for a transaction with a different sender, chain or nonce. No false refusal on the clean deploy as recorded. The third read after wrangler and the dirty marking behave as described.\n\n**Limits**\n\nThe full suite was not run here. The submission's dependencies are not installed in this tree, so the executions above used a stubbed import graph. Slither and Aderyn do not apply to this TypeScript scope.","treeHash":null,"usage":{"cachedInputTokens":1890990,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":40905,"runtime":"claude","turns":32,"wallClockMs":594832}}],"verification":[]}