# Audit report

> 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.
>
> 1. 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?
> 2. 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?
> 3. 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?
>
> Report severity, reproduction and fix. Not in scope: 3D/rendering, media, Celestao, AIMarket.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `37e6afd001959f2b9dceb89597473a0495b48fcb` |
| Job | `351b0573-d704-49cb-8af4-76843a8afa3c` |
| Judged | 2026-10-27 06:16 UTC |
| Findings | 9 low · 4 info |

Four agents audited the code as it is at `37e6afd`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Low: "+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

`Submission15/source/src/world/pendingGoldCredit.ts:62`

```
 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));});
```

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.

**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.

### 2. Low: 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

`Submission15/source/src/world/economyWallet.ts:239`

```
  const done=same||own&&r.status==='0x0'||own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);
```

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.

**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.

### 3. Low: 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

`Submission15/source/src/world/economyWallet.ts:231`

```
  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');
```

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.

**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.

### 4. Low: 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

`Submission15/source/src/world/economyWallet.ts:183`

```
  this.set({phase:'checking',busy:true,step,error:null});
```

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.

**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.

### 5. Low: 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

`Submission15/source/src/world/WorldGoldPanel.tsx:111`

```
 <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)}/>
```

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.

**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.

### 6. Low: 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

`Submission15/source/src/world/WorldGoldPanel.tsx:81`

```
 useEffect(()=>{const o=payingOrder?data?.orders.find(o=>o.id===payingOrder):null;if(!data||!o||o.kind!=='topup'||!payingHash||o.status==='paid')return;
```

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.

**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.

### 7. Low: 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

`Submission15/source/scripts/deploy.mjs:172`

```
  const env=envFiles(ROOT),viteEnv=viteEnvNames(process.env),dependencies=dependencyMismatches(ROOT);
```

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).

**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.

### 8. Low: 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)

`Submission15/source/scripts/deploy.mjs:163`

```
  const r=spawnSync(process.execPath,args,{cwd:ROOT,encoding:'utf8',stdio:capture?['ignore','pipe','pipe']:'inherit',env:{...process.env,...env}});
```

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.

**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.

### 9. Low: 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

`Submission15/source/scripts/deploy.mjs:78`

```
  for(const k of Object.keys(hidden.packages))if(k.startsWith('node_modules/')&&!names.has(k))out.push('not in package-lock.json: '+k);
```

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.

**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.

### 10. Info: 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

`Submission15/source/src/world/economyWallet.ts:35`

```
const initial=():EconomyWalletState=>({phase:'idle',busy:false,plan:null,hash:null,step:null,error:null,legacyPending:false});
```

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.

**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.

### 11. Info: 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

`Submission15/source/scripts/deploy.mjs:55`

```
export const viteEnvNames=env=>Object.keys(env).filter(k=>/^VITE_/i.test(k)).sort();
```

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).

**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.

### 12. Info: 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

`Submission15/source/scripts/deploy.mjs:74`

```
    if(installed.version!==v.version)out.push(k+' '+installed.version+' installed, '+v.version+' locked');
```

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.

**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).

### 13. Info: 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

`Submission15/source/scripts/deploy.mjs:171`

```
  const commit=git('rev-parse','HEAD'),porcelain=git('status','--porcelain','--untracked-files=normal');
```

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.

**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.

---

Judge's submission `30c4eb3960afb8eb28e967d88ed6c22d45b3398c6fbbf223d597950b2931b31d`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
