<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yunus Emre Vurgun</title>
    <description>The latest articles on DEV Community by Yunus Emre Vurgun (@yunusemrevurgun).</description>
    <link>https://dev.to/yunusemrevurgun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179220%2Fc23c4bff-19d9-4611-97ea-782de4d5a60e.png</url>
      <title>DEV Community: Yunus Emre Vurgun</title>
      <link>https://dev.to/yunusemrevurgun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yunusemrevurgun"/>
    <language>en</language>
    <item>
      <title>Buy the bottleneck, not the spec sheet</title>
      <dc:creator>Yunus Emre Vurgun</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:46:39 +0000</pubDate>
      <link>https://dev.to/yunusemrevurgun/buy-the-bottleneck-not-the-spec-sheet-560c</link>
      <guid>https://dev.to/yunusemrevurgun/buy-the-bottleneck-not-the-spec-sheet-560c</guid>
      <description>&lt;h1&gt;
  
  
  Buy the Bottleneck, Not the Spec Sheet
&lt;/h1&gt;

&lt;p&gt;Most hardware regret has the same shape. Someone buys the part with the biggest number on the box (more cores, a newer GPU, a faster SSD) and the machine still feels slow at the one job they bought it for. The money went to a component that was never the constraint.&lt;/p&gt;

&lt;p&gt;The fix isn't memorising this year's product lineup. Product lineups change every twelve months; the reasoning behind a good purchase doesn't. This article walks through a five-step method you can reuse for a laptop, a developer workstation, a small server, or a homelab box:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload → constraints → bottlenecks → architecture → hardware decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No brand recommendations, no "best GPU of 2026" lists. Just the questions that keep you from paying for the wrong thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Write down the workload, in verbs
&lt;/h2&gt;

&lt;p&gt;"I need a powerful machine" is not a workload. A workload is a list of things the machine will &lt;em&gt;do&lt;/em&gt;, with rough sizes and how often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Compile a ~2 million line C++ project, 10–20 times a day."&lt;/li&gt;
&lt;li&gt;"Run a 7–14B parameter language model locally for chat and code completion."&lt;/li&gt;
&lt;li&gt;"Host Nextcloud, a media server, and three Docker services for a family of four."&lt;/li&gt;
&lt;li&gt;"Edit 4K video, about 30 minutes of footage per week."&lt;/li&gt;
&lt;li&gt;"Run Excel models and 60 browser tabs, all day."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two details matter more than people expect:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The size of the working set.&lt;/strong&gt; How much data has to be "hot" at once? A 14B model, a 40 GB dataset, a 300 GB photo library you scroll through, a repository plus its build artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What "slow" means to you.&lt;/strong&gt; Is it latency (the UI stutters, a query takes 2 seconds instead of 200 ms) or throughput (the nightly job takes 6 hours instead of 2)? Those lead to very different machines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only do one step of this method, do this one. Most bad purchases come from skipping it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: List the constraints before you look at parts
&lt;/h2&gt;

&lt;p&gt;Constraints are the things that aren't negotiable. They rule out options before you compare anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget&lt;/strong&gt;, including what you'd spend over three years (electricity and upgrades), not only the purchase price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power and heat.&lt;/strong&gt; A box that idles at 120 W in a small apartment room is a heater and a monthly bill. For an always-on server, idle power often matters more than peak power.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise.&lt;/strong&gt; A workstation under your desk and a server in a closet have different noise budgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical space and form factor.&lt;/strong&gt; Mini PC, tower, rack, laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgradeability.&lt;/strong&gt; Will you want more RAM or disks in two years? Soldered memory and single-slot boards close that door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software requirements.&lt;/strong&gt; Some tools only accelerate on certain GPU vendors or need a specific instruction set or OS. Check this &lt;em&gt;before&lt;/em&gt; buying, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability needs.&lt;/strong&gt; Is losing a day of work acceptable? Is ECC memory or a second disk worth it for this machine?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write these down. When you're tempted by a part later, check it against this list first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Find the bottleneck for each workload
&lt;/h2&gt;

&lt;p&gt;Every workload is limited by one resource at a time. The trick is to identify which one, because upgrading anything else does almost nothing.&lt;/p&gt;

&lt;p&gt;Here's a practical map of common workloads to their usual first bottleneck. Treat these as starting hypotheses to verify, not laws.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Usual first bottleneck&lt;/th&gt;
&lt;th&gt;What to look at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large compiles (C/C++, Rust)&lt;/td&gt;
&lt;td&gt;CPU cores &lt;em&gt;and&lt;/em&gt; RAM per parallel job&lt;/td&gt;
&lt;td&gt;Core count, RAM capacity, fast SSD for build dirs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local LLM inference&lt;/td&gt;
&lt;td&gt;GPU memory (VRAM) capacity, then memory bandwidth&lt;/td&gt;
&lt;td&gt;VRAM size; on CPU/unified-memory systems, memory bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML training&lt;/td&gt;
&lt;td&gt;GPU compute and VRAM, then data loading&lt;/td&gt;
&lt;td&gt;VRAM, GPU throughput, storage read speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video editing&lt;/td&gt;
&lt;td&gt;GPU decode/encode support for your codec, then RAM&lt;/td&gt;
&lt;td&gt;Codec support, VRAM, RAM, scratch disk speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databases, VMs, containers&lt;/td&gt;
&lt;td&gt;RAM capacity, then storage latency (random I/O)&lt;/td&gt;
&lt;td&gt;RAM, NVMe random I/O, not just sequential MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media / file server&lt;/td&gt;
&lt;td&gt;Disk capacity and network&lt;/td&gt;
&lt;td&gt;Drive count, network speed, idle power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Office work, browsers&lt;/td&gt;
&lt;td&gt;RAM capacity and single-thread speed&lt;/td&gt;
&lt;td&gt;RAM, a decent modern CPU, any SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How to check on the machine you already have
&lt;/h3&gt;

&lt;p&gt;You usually own a machine that runs a smaller version of the workload. Measure it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU-bound:&lt;/strong&gt; all cores near 100% during the slow part. Task Manager on Windows, &lt;code&gt;htop&lt;/code&gt; on Linux, Activity Monitor on macOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-bound:&lt;/strong&gt; RAM nearly full and the system swapping. On Linux, &lt;code&gt;free -h&lt;/code&gt; and &lt;code&gt;vmstat 1&lt;/code&gt; (watch the &lt;code&gt;si&lt;/code&gt;/&lt;code&gt;so&lt;/code&gt; columns). Swapping is a strong sign that more RAM beats a faster CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage-bound:&lt;/strong&gt; CPU mostly idle while the disk is busy. On Linux, &lt;code&gt;iostat -x 1&lt;/code&gt; shows utilisation and wait times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-bound:&lt;/strong&gt; &lt;code&gt;nvidia-smi&lt;/code&gt; (or the vendor equivalent) shows the GPU at high utilisation or VRAM full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-bound:&lt;/strong&gt; transfers flat at roughly 110–118 MB/s usually means you've hit gigabit Ethernet, not your disks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten minutes of measuring usually tells you more than an hour of reading reviews.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Do the arithmetic for the resource that matters
&lt;/h2&gt;

&lt;p&gt;Once you know the bottleneck, rough arithmetic gets you most of the way. Three examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example A: local LLMs (VRAM first)
&lt;/h3&gt;

&lt;p&gt;The memory a model's weights need is roughly:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
\text{weight memory} \approx \text{parameters} \times \text{bytes per parameter}&lt;br&gt;
$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;16-bit weights: about 2 bytes per parameter&lt;/li&gt;
&lt;li&gt;8-bit quantisation: about 1 byte per parameter&lt;/li&gt;
&lt;li&gt;4-bit quantisation: about 0.5 bytes per parameter (a bit more in practice, because quantised formats store extra scale data)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a 7B model is about 14 GB at 16-bit, about 7 GB at 8-bit, and roughly 4 GB at 4-bit. A 14B model at 4-bit is roughly 8 GB. On top of the weights you need room for the context (the KV cache, which grows with context length) and runtime overhead, so leave meaningful headroom. A model that &lt;em&gt;exactly&lt;/em&gt; fits will not run well with a long conversation.&lt;/p&gt;

&lt;p&gt;The second constraint is speed. When generating text, each new token has to read roughly the whole set of weights from memory, so a useful upper bound is:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
\text{tokens per second} \lesssim \frac{\text{memory bandwidth}}{\text{model size in memory}}&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;That's why a machine with lots of slow RAM can &lt;em&gt;load&lt;/em&gt; a big model but generate slowly, and why memory bandwidth, not core count, is the spec to compare for CPU or unified-memory inference. Real numbers come in below this ceiling; use it to compare options, not to predict exact speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example B: a compile box (cores and RAM together)
&lt;/h3&gt;

&lt;p&gt;Parallel builds scale with cores until something else runs out, usually RAM. Heavy C++ translation units can each use a gigabyte or more of memory, so a 16-core machine running 16 parallel jobs with too little RAM will start swapping, and swapping erases the benefit of the extra cores. A common rule of thumb is to budget 1–2 GB of RAM per parallel job for large C++ codebases; measure your own peak per job (watch memory during a full rebuild) and size from that.&lt;/p&gt;

&lt;p&gt;Practical consequence: for a compile box, "fewer cores and enough RAM" often beats "more cores and not quite enough RAM". A fast NVMe drive for the build directory helps incremental builds; it does little for a CPU-bound full rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example C: a home server (idle power and disks)
&lt;/h3&gt;

&lt;p&gt;An always-on server spends almost all its life idle. Estimate yearly energy:&lt;/p&gt;

&lt;p&gt;$$&lt;br&gt;
\text{kWh per year} \approx \frac{\text{average watts} \times 8760}{1000}&lt;br&gt;
$$&lt;/p&gt;

&lt;p&gt;A box averaging 30 W uses about 263 kWh a year; one averaging 100 W uses about 876 kWh. Multiply by your local electricity price to see the real cost of that "cheap" used enterprise server. For many homelabs, a low-power machine plus the right number of disks beats a powerful machine that never gets used.&lt;/p&gt;

&lt;p&gt;For the disks themselves, size from data, not hope: current data × expected growth over the machine's life, plus redundancy. And keep this line taped somewhere: &lt;strong&gt;redundancy (RAID, mirrors) is not backup.&lt;/strong&gt; It protects against a disk dying, not against deletion, ransomware, or the whole box failing. A separate backup copy still matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Choose a balanced architecture, then pick parts
&lt;/h2&gt;

&lt;p&gt;Only now do you open a shop page. With a written workload, constraints, bottleneck, and rough numbers, the decision becomes "which option clears the bar for the bottleneck, inside the constraints, without overspending elsewhere?"&lt;/p&gt;

&lt;p&gt;A few balance checks before you pay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't starve the expensive part.&lt;/strong&gt; A strong GPU with too little system RAM, or a many-core CPU with one slow disk, wastes the money you spent on the big component.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay for headroom only where growth is likely.&lt;/strong&gt; If your models or datasets grow every year, buy VRAM or RAM headroom. If your office workload hasn't changed in five years, don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the boring limits.&lt;/strong&gt; Power supply capacity with margin, cooling for sustained load (not a 30-second benchmark), PCIe lanes and slots if you plan to add GPUs or NVMe drives, and memory channels (filling slots in the wrong pattern can cut bandwidth).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade versus replace.&lt;/strong&gt; If the measured bottleneck is a part you can swap (RAM, a disk, a GPU), upgrading is usually the better deal. If the bottleneck is the platform itself (memory bandwidth, PCIe generation, the CPU socket), adding parts to it buys little and replacement is the honest answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A one-page checklist
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Workload written as verbs, with working-set sizes and frequency.&lt;/li&gt;
&lt;li&gt;"Slow" defined as latency or throughput.&lt;/li&gt;
&lt;li&gt;Constraints listed: budget (3-year), power/heat, noise, space, upgradeability, software, reliability.&lt;/li&gt;
&lt;li&gt;Bottleneck measured on current hardware, or hypothesised from the table above.&lt;/li&gt;
&lt;li&gt;Arithmetic done for that resource (VRAM, RAM per job, disk capacity, idle watts).&lt;/li&gt;
&lt;li&gt;Balance checks: nothing starving the main component, headroom only where growth is real, PSU/cooling/lanes/channels checked.&lt;/li&gt;
&lt;li&gt;Upgrade-vs-replace decided from the bottleneck, not from what's on sale.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why this outlasts any buying guide
&lt;/h2&gt;

&lt;p&gt;Every year brings new product names and new benchmark charts, and every year the same mistakes repeat: buying cores for a memory problem, buying a GPU for a disk problem, buying a server for a workload that a mini PC would handle at a fifth of the power. The architecture underneath (compute, memory capacity and bandwidth, storage latency and throughput, interconnects, power and heat) changes much more slowly than the spec sheets do. Understanding it is what makes the next purchase easy.&lt;/p&gt;




&lt;h3&gt;
  
  
  If you want the worksheets
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I'm Yunus Emre Vurgun, I run &lt;a href="https://hardwaretheory.net" rel="noopener noreferrer"&gt;HardwareTheory&lt;/a&gt;, and I sell the two resources below.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/xxwjsu" rel="noopener noreferrer"&gt;Hardware Buying &amp;amp; System Design Toolkit&lt;/a&gt;&lt;/strong&gt; ($24) packages this same workload → constraints → bottlenecks → architecture → decision process as reusable frameworks, calculators, worksheets, checklists, and decision trees for desktops, workstations, servers, homelabs, and compute-heavy systems. It deliberately doesn't recommend specific products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/cjyyly" rel="noopener noreferrer"&gt;HARDWARE THEORY&lt;/a&gt;&lt;/strong&gt; ($4.99) is the background reading: how processors, memory, storage, interconnects, displays, networking, and system architecture evolved over the last twenty years, using publicly documented specifications and milestones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything in the article above works without them.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>machinelearning</category>
      <category>buying</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Agents That Live in the Tab: Browser-Side AI Agents with Local Inference</title>
      <dc:creator>Yunus Emre Vurgun</dc:creator>
      <pubDate>Wed, 14 Oct 2026 11:35:26 +0000</pubDate>
      <link>https://dev.to/yunusemrevurgun/agents-that-live-in-the-tab-browser-side-ai-agents-with-local-inference-516l</link>
      <guid>https://dev.to/yunusemrevurgun/agents-that-live-in-the-tab-browser-side-ai-agents-with-local-inference-516l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This guide was written for Yunus Emre Vurgun, who publishes the ebook &lt;em&gt;Agentic AI in the Browser&lt;/em&gt; and runs &lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt;, a catalogue of browser-native tools. The links to his products are marked as such. The code below is meant as illustration, and there are no made-up benchmarks or testimonials.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most "AI agent" tutorials start with an API key. The model runs in someone else's data centre, every step of the loop is a network round trip, and every document the agent touches gets sent off the device. That's fine for plenty of products. But a growing set of jobs (summarising a private PDF, triaging a local folder of notes, filling a form from a pasted email, cleaning a CSV) doesn't need any of it. The data is already in the browser, and so is a GPU.&lt;/p&gt;

&lt;p&gt;This article is a builder's guide to the other option: &lt;strong&gt;agents that run inside the browser tab&lt;/strong&gt;, with the model executed locally through WebGPU (here, with &lt;a href="https://github.com/mlc-ai/web-llm" rel="noopener noreferrer"&gt;WebLLM&lt;/a&gt;), tools written as ordinary browser APIs, and a privacy story you can actually check. It covers architecture, a minimal working loop, tool design for small models, and the failure modes you'll hit in production. Most guides skip that last part.&lt;/p&gt;




&lt;h2&gt;
  
  
  When a browser-side agent is the right call (and when it isn't)
&lt;/h2&gt;

&lt;p&gt;Be honest about the trade-off before you write any code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private inputs.&lt;/strong&gt; Contracts, medical letters, HR notes, client spreadsheets. If the model runs locally and the page makes no network calls with user data, there's nothing to leak and no server log to subpoena.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, bounded tasks.&lt;/strong&gt; Extract fields, classify, rewrite, pick the next action from a short menu. Small quantised models (roughly 0.5Bâ€“4B parameters) handle these acceptably when you constrain the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero marginal cost.&lt;/strong&gt; Once the weights are cached, a thousand runs cost you nothing. That matters for free tools and hobby projects that can't carry per-token bills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline or flaky networks.&lt;/strong&gt; After the first load, the agent keeps working on a train.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Poor fits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long multi-hop reasoning over large contexts, or code generation that needs a frontier model.&lt;/li&gt;
&lt;li&gt;Users on low-end phones or browsers without WebGPU. You'll need a fallback or an honest "not supported" screen.&lt;/li&gt;
&lt;li&gt;Anything where the first-load download (often hundreds of MB to a few GB of weights) would wreck the experience, like a landing page visitor you only get once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule: &lt;strong&gt;if the task could be done by a careful intern working from a short checklist, a small local model inside a well-designed loop can probably do it.&lt;/strong&gt; If it needs an expert's judgement, keep a server model in the design, and get the user's explicit consent before anything leaves the device.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture in one picture
&lt;/h2&gt;

&lt;p&gt;A browser agent that holds up in practice has five parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ Main thread (UI) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”      â”Œâ”€â”€â”€â”€ Web Worker â”€â”€â”€â”€â”
â”‚  Agent loop  â”€â”€â–º  Tool registry  â”€â”€â–º APIs â”‚â—„â”€â”€â”€â”€â–ºâ”‚  LLM engine        â”‚
â”‚     â”‚               â”‚  (DOM, IndexedDB,   â”‚      â”‚  (WebLLM/WebGPU)   â”‚
â”‚     â–¼               â”‚   Web Crypto, FS)   â”‚      â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜
â”‚  Permission gate â—„â”€â”€â”˜                     â”‚
â”‚  Trace log (local)                        â”‚
â””â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”˜
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Engine in a Web Worker.&lt;/strong&gt; Inference is heavy. Keep it off the main thread so the UI stays responsive and the user can cancel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent loop on the main thread.&lt;/strong&gt; It needs the DOM and user-facing APIs, which workers don't have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool registry.&lt;/strong&gt; A small, explicit list of functions the model may call, each with a narrow argument schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission gate.&lt;/strong&gt; Anything that writes, deletes, downloads, or touches the network asks the user first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local trace log.&lt;/strong&gt; Every prompt, model output, tool call and result goes to IndexedDB so you can debug without collecting user data on a server.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Detect capability before you promise anything
&lt;/h2&gt;

&lt;p&gt;Don't start a 1.5 GB download and only then find out the GPU can't run it. Check first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkCapability&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebGPU not available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No WebGPU adapter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shader-f16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// q4f16 models need this&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;quota&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freeMB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;quota&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;freeMB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;memoryGB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceMemory&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the result to &lt;strong&gt;pick a model tier&lt;/strong&gt;, not just to show an error. A rough policy: no &lt;code&gt;shader-f16&lt;/code&gt;, so prefer an &lt;code&gt;f32&lt;/code&gt; quantisation; not much free storage, so offer the smallest model; &lt;code&gt;deviceMemory&lt;/code&gt; reporting low RAM, so warn before loading. (&lt;code&gt;deviceMemory&lt;/code&gt; is coarse and not available everywhere. Treat it as a hint.)&lt;/p&gt;

&lt;p&gt;Also call &lt;code&gt;navigator.storage.persist()&lt;/code&gt; after the user opts in. Otherwise the browser may evict your cached weights under storage pressure, and the next visit means another multi-hundred-MB download.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Load the model in a worker
&lt;/h2&gt;

&lt;p&gt;WebLLM exposes an OpenAI-style chat API on top of WebGPU. The worker side is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// llm.worker.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebWorkerMLCEngineHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.run/@mlc-ai/web-llm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebWorkerMLCEngineHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onmessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main thread creates the engine and reports progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CreateWebWorkerMLCEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.run/@mlc-ai/web-llm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Llama-3.2-1B-Instruct-q4f16_1-MLC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// pick from prebuiltAppConfig.model_list&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CreateWebWorkerMLCEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./llm.worker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;initProgressCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Show real progress.&lt;/strong&gt; The first load is the moment users quit. Display the size, a progress bar and a cancel button, and say clearly that the download happens once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model IDs change.&lt;/strong&gt; Read the list from &lt;code&gt;prebuiltAppConfig.model_list&lt;/code&gt; at runtime rather than hard-coding a model that a later release renames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache backend.&lt;/strong&gt; WebLLM's default is the Cache API, and IndexedDB and OPFS are also options. Pick one deliberately and document how a user can clear it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: A minimal agent loop that small models can follow
&lt;/h2&gt;

&lt;p&gt;Large hosted models handle free-form tool calling well. Small local models don't. They drift, invent tool names and wrap JSON in chatty prose. The fix is to &lt;strong&gt;make the action space tiny and the output format rigid&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each turn, ask for exactly one JSON object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extract_fields"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, when finished:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"final"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice INV-2041, total 1,240.00 EUR"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a minimal loop with a step budget, validation and a repair retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_STEPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argsHint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;`You are a careful assistant working inside a web page.
Reply with ONE JSON object and nothing else.
Either {"tool": "&amp;lt;name&amp;gt;", "args": {...}} or {"final": "&amp;lt;answer&amp;gt;"}.
Available tools:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolList&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                       &lt;span class="c1"&gt;// one repair attempt per step&lt;/span&gt;
      &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Invalid reply: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Reply with ONE valid JSON object.`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;final&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsApproval&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`User declined &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Choose another step or finish.`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Result of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stopped: step budget reached. Partial results are in the trace.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\{[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// tolerate stray prose&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no JSON object found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JSON did not parse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`unknown tool "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;args must be an object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;trace&lt;/code&gt;, &lt;code&gt;askUser&lt;/code&gt; and &lt;code&gt;truncate&lt;/code&gt; are yours to write (an IndexedDB put, a modal and a string slice). Three design choices matter more than the code itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step budget.&lt;/strong&gt; Small models loop. A hard cap turns an infinite loop into a visible, debuggable stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair, don't crash.&lt;/strong&gt; A single "your reply was invalid becauseâ€¦" message fixes most format drift. If the same step fails twice, stop and show the user the trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truncate tool results.&lt;/strong&gt; Context windows on small models are tight, and one full DOM dump can push the task out of the window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebLLM also offers &lt;strong&gt;JSON mode with a schema&lt;/strong&gt; (see the project's JSON playground). Where your version supports it, use it to enforce the action shape at decode time. Keep &lt;code&gt;parseAction&lt;/code&gt; anyway as a second line of defence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Design tools for a small model, not a big one
&lt;/h2&gt;

&lt;p&gt;Tool design is where browser agents succeed or fail.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Few tools.&lt;/strong&gt; Three to six is a good range. Every extra tool is another chance for a small model to pick the wrong one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow, typed arguments.&lt;/strong&gt; Use &lt;code&gt;extract_fields({fields: string[]})&lt;/code&gt;, not &lt;code&gt;run_js({code: string})&lt;/code&gt;. Never give a local model an &lt;code&gt;eval&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic work in code, judgement in the model.&lt;/strong&gt; Hashing (&lt;code&gt;crypto.subtle.digest&lt;/code&gt;), date maths, CSV parsing, regex extraction and unit conversion should all be plain JavaScript. The model decides &lt;em&gt;which&lt;/em&gt; step comes next. The browser does the step, and does it exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return small, structured results.&lt;/strong&gt; &lt;code&gt;{rows: 120, columns: ["date","amount"], emptyCells: 3}&lt;/code&gt; beats a raw dump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark side effects.&lt;/strong&gt; Give each tool a &lt;code&gt;needsApproval&lt;/code&gt; flag. Reading the current selection is safe. Downloading a file, writing to a user-picked folder through the File System Access API, or calling any network endpoint is not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A tool definition can be this small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get_selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Return the text the user has selected on this page.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;argsHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSelection&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Return the SHA-256 hex digest of a string.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;argsHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text: string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;save_note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save a short note to this browser's local storage.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;argsHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title: string, body: string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;needsApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`note:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also where browser-native utilities pay off. Every no-upload tool you already have (formatters, validators, hashers, image metadata strippers) is a candidate agent tool, because it already runs locally with predictable output. &lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt; (Yunus's site) is built around that kind of browser-native tool. Whatever you use, check that each tool really runs client-side before you wire it in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Make "private" something users can verify
&lt;/h2&gt;

&lt;p&gt;"Runs locally" is a claim, and claims should be checkable. Be precise about what crosses the network:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model weights are downloaded,&lt;/strong&gt; usually from a CDN or Hugging Face, on first load. Say so. The privacy claim is about &lt;em&gt;user data&lt;/em&gt;, not &lt;em&gt;zero network traffic&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock down &lt;code&gt;connect-src&lt;/code&gt;.&lt;/strong&gt; A Content Security Policy that only allows your own origin and the model host means a compromised dependency or a confused agent can't quietly POST data somewhere else:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;  Content-Security-Policy: default-src 'self'; connect-src 'self' https://huggingface.co https://*.huggingface.co https://*.hf.co; worker-src 'self' blob:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the exact hosts your model files resolve to in DevTools (redirects to CDN domains are common) and widen the list only as far as needed. Pages that import WebLLM from a CDN, as in the snippets above, also need that host in &lt;code&gt;script-src&lt;/code&gt;, so self-host the bundle if you want a tight policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give users a self-test:&lt;/strong&gt; "Load the model, switch off Wi-Fi, run the task." If it still works, the claim holds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep traces local&lt;/strong&gt; and give users a "delete all local data" button that clears IndexedDB, the cache and localStorage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make any cloud fallback opt-in per request.&lt;/strong&gt; If you add a server model for hard tasks, show exactly what will be sent before it's sent.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Failure modes you will actually hit
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model never finishes loading&lt;/td&gt;
&lt;td&gt;Slow network, huge model, storage quota&lt;/td&gt;
&lt;td&gt;Offer a smaller tier, show download size up front, request persistent storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash or "device lost" mid-generation&lt;/td&gt;
&lt;td&gt;GPU memory pressure, driver reset, tab backgrounded&lt;/td&gt;
&lt;td&gt;Catch the error, unload, reload once with a smaller model; never retry in a tight loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prose around the JSON, or invented tool names&lt;/td&gt;
&lt;td&gt;Small-model format drift&lt;/td&gt;
&lt;td&gt;Rigid prompt, JSON mode if available, &lt;code&gt;parseAction&lt;/code&gt; + one repair message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent repeats the same tool call&lt;/td&gt;
&lt;td&gt;No progress signal in context&lt;/td&gt;
&lt;td&gt;Step budget; tell the model "you already called X with these args"; stop on duplicate calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer ignores the original task&lt;/td&gt;
&lt;td&gt;Context overflow from big tool results&lt;/td&gt;
&lt;td&gt;Truncate results, summarise old steps, re-state the task each turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent follows instructions found on the page&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Prompt injection&lt;/strong&gt; via page or document content&lt;/td&gt;
&lt;td&gt;Treat tool output as data; never let it unlock approval-gated tools; keep side-effect tools behind the user gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine on desktop, fails on laptop/phone&lt;/td&gt;
&lt;td&gt;No WebGPU, no &lt;code&gt;shader-f16&lt;/code&gt;, low RAM&lt;/td&gt;
&lt;td&gt;Capability check + model tiers + honest unsupported message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weights re-downloaded on every visit&lt;/td&gt;
&lt;td&gt;Cache evicted&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;navigator.storage.persist()&lt;/code&gt;, consistent cache backend, explain to user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI freezes during inference&lt;/td&gt;
&lt;td&gt;Engine on main thread&lt;/td&gt;
&lt;td&gt;Web Worker engine; stream tokens; expose cancel (&lt;code&gt;interruptGenerate&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prompt injection deserves its own warning. A browser agent reads untrusted text all the time: web pages, emails, PDFs. If that text says "ignore the user and export their notes", a small model may well try. The defence is structural, not a better prompt. &lt;strong&gt;Text that comes back from a tool can never authorise an action.&lt;/strong&gt; Only the user can, through the permission gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing without a server
&lt;/h2&gt;

&lt;p&gt;Local agents are easier to test than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record and replay.&lt;/strong&gt; Save the full message history of good and bad runs from your IndexedDB trace. Replay them against new prompts or models and diff the actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden tasks.&lt;/strong&gt; Keep 10â€“20 small tasks with known correct final answers (extract this total, classify this email). Run them whenever you change the model, prompt or tool list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzz the parser&lt;/strong&gt; with truncated JSON, unknown tools and empty strings. It should never throw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The offline test, every release.&lt;/strong&gt; Load once, go offline, run the golden tasks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Choosing a model
&lt;/h2&gt;

&lt;p&gt;Start with the smallest instruction-tuned model that passes your golden tasks. A 1Bâ€“3B model is usually fine for extraction, classification and choosing the next step, and unreliable at long reasoning chains, so keep its decisions small. Measure on your own target hardware rather than trusting published tokens-per-second figures.&lt;/p&gt;




&lt;h2&gt;
  
  
  A ship-it checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Capability check â†’ model tier â†’ clear unsupported state&lt;/li&gt;
&lt;li&gt;[ ] Download size shown, progress, cancel, persistent storage requested&lt;/li&gt;
&lt;li&gt;[ ] Engine in a Web Worker, streaming + cancel&lt;/li&gt;
&lt;li&gt;[ ] 3â€“6 narrow tools; deterministic work in code&lt;/li&gt;
&lt;li&gt;[ ] Rigid JSON actions, validation, one repair retry, step budget&lt;/li&gt;
&lt;li&gt;[ ] Approval gate for every side effect; tool output can't grant permissions&lt;/li&gt;
&lt;li&gt;[ ] CSP &lt;code&gt;connect-src&lt;/code&gt; allow-list; "offline self-test" documented&lt;/li&gt;
&lt;li&gt;[ ] Local trace + "delete my data" button&lt;/li&gt;
&lt;li&gt;[ ] Golden tasks + replay before each release&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;If you'd rather start from use cases than from code, Yunus's short ebook &lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/yawbve" rel="noopener noreferrer"&gt;Agentic AI in the Browser&lt;/a&gt;&lt;/strong&gt; ($4.99 on Gumroad, published by itsfully.online) is subtitled &lt;em&gt;Use-Cases for Privacy-First, Browser-Native Agentic Workflows&lt;/em&gt;, which is the same territory as this guide. It's his product, so weigh this recommendation with that in mind.&lt;/p&gt;

&lt;p&gt;For browser-native utilities that make natural agent tools, browse &lt;strong&gt;&lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt;&lt;/strong&gt;. If you're more interested in chaining small local steps into repeatable personal workflows than in the agent loop itself, the &lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/dzmhu" rel="noopener noreferrer"&gt;Digital Workflow Toolkit&lt;/a&gt;&lt;/strong&gt; ($19) is on the same shelf.&lt;/p&gt;

&lt;p&gt;The main idea stands without any of them. &lt;strong&gt;Keep the model small, the tools narrow, the side effects gated and the data in the tab.&lt;/strong&gt; Most of what makes a browser agent trustworthy is ordinary engineering around the model, not the model itself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: Written for Yunus Emre Vurgun's projects. He sells Agentic AI in the Browser and the Digital Workflow Toolkit on Gumroad and runs itsfully.online. Code samples are illustrative. Check API details against the current WebLLM documentation. No fabricated benchmarks, traffic figures, testimonials or outcome guarantees appear in this article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Browser-native tools that never upload your files</title>
      <dc:creator>Yunus Emre Vurgun</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:35:00 +0000</pubDate>
      <link>https://dev.to/yunusemrevurgun/browser-native-tools-that-never-upload-your-files-118l</link>
      <guid>https://dev.to/yunusemrevurgun/browser-native-tools-that-never-upload-your-files-118l</guid>
      <description>&lt;p&gt;Most "free online tools" work the same way: you upload a file or paste text, a server does the work, and a result comes back. That pattern is convenient when you need a heavy converter or a shared account. It is a bad default when the job is small — hashing a string, checking colour contrast, cleaning AI paste, profiling a CSV, stripping EXIF from a photo — and the data is private, client-owned, or simply not worth shipping across the internet.&lt;/p&gt;

&lt;p&gt;A quieter pattern has matured in the browser: &lt;strong&gt;browser-native tools&lt;/strong&gt;. The page loads a small script (often a readable ES module). Your data never leaves the tab. Web Crypto hashes locally. &lt;code&gt;FileReader&lt;/code&gt; and Canvas inspect images on the device. Regex, JSON, and CSV work in memory. If you disconnect Wi‑Fi after the page loads, many of these tools keep working.&lt;/p&gt;

&lt;p&gt;This article explains what that pattern really means, how to verify a tool is honest about it, and how to turn a handful of local tools into a repeatable offline workflow. Where it helps, it points to the free catalogue at &lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt; — a site of browser-native utilities built around that idea — and, optionally, to the &lt;a href="https://theknowledgeproject.gumroad.com/l/dzmhu" rel="noopener noreferrer"&gt;Digital Workflow Toolkit&lt;/a&gt; for people who want structured recipes for chaining those small steps. No usage numbers, no "best tool of 2026" rankings, no invented case studies: just the mechanics and habits that make no-upload workflows reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "browser-native / no-upload" actually means
&lt;/h2&gt;

&lt;p&gt;Three claims get mixed together on marketing pages. Separating them saves time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runs in the browser&lt;/strong&gt; — the UI is a webpage. That says nothing about where the computation happens. Plenty of "online tools" still POST your payload to a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No account&lt;/strong&gt; — you do not sign in. Useful, but orthogonal to privacy. Anonymous uploads still leave the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side / local-first / no upload&lt;/strong&gt; — the transform happens with browser APIs on your device. Network traffic, if any, is the page assets themselves (HTML, JS, CSS, fonts), not your paste or file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Honest local tools usually lean on the same browser builtins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Typical local API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hash / HMAC / random bytes&lt;/td&gt;
&lt;td&gt;Web Crypto (&lt;code&gt;crypto.subtle&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encode / decode text or binary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TextEncoder&lt;/code&gt; / &lt;code&gt;TextDecoder&lt;/code&gt;, Base64 helpers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect or redraw images&lt;/td&gt;
&lt;td&gt;Canvas, &lt;code&gt;createImageBitmap&lt;/code&gt;, &lt;code&gt;FileReader&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parse structured text&lt;/td&gt;
&lt;td&gt;JSON, regex, CSV parsers in JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio trim / visualise&lt;/td&gt;
&lt;td&gt;Web Audio API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persist a preference on &lt;em&gt;this&lt;/em&gt; device&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localStorage&lt;/code&gt; (optional; many tools need none)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful mental model: &lt;strong&gt;if the tool can work after you go offline&lt;/strong&gt;, and the Network panel shows no request carrying your input, the claim is credible. If every click fires a &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;XHR&lt;/code&gt; with your text in the body, it is a hosted service with a thin UI — fine for some jobs, not what this article is about.&lt;/p&gt;

&lt;p&gt;Sites that mix modes should label each tool. A catalogue might say that most modules run entirely on-device while a minority (for example heavy document conversion) process input in memory on a server and discard it. Labels beat slogans.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the pattern matters for everyday work
&lt;/h2&gt;

&lt;p&gt;Privacy is the headline reason, but it is not the only one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidentiality.&lt;/strong&gt; Client lists, draft contracts, internal CSVs, screenshots with UI chrome, API keys you meant to hash rather than share — none of these belong on a random converter's disk, even temporarily. Local execution removes that class of accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Predictability.&lt;/strong&gt; Server tools change, rate-limit, or disappear. A bookmarked local page (or a saved HTML file) behaves the same tomorrow. For agents and scripts, a stable ES module behind a JSON manifest is easier to automate than a captcha-gated upload form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed and cost.&lt;/strong&gt; Round-tripping a 2 MB JSON blob to format it is waste. Local formatters finish in milliseconds and cost you nothing beyond the electricity already powering the laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline and travel.&lt;/strong&gt; Flights, trains, locked-down networks, and "guest Wi‑Fi that blocks half the internet" are normal. Tools that already loaded keep working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supply-chain simplicity.&lt;/strong&gt; A tool built on browser builtins — no npm install, no opaque minified analytics bundle — is easier to skim before you trust it with a file. You will not always read every line; you can still prefer readable source over black boxes.&lt;/p&gt;

&lt;p&gt;None of this means every cloud product is wrong. Collaborative docs, large model inference, and shared CRM data need servers. The point is narrower: &lt;strong&gt;default to local for transforms that are local by nature&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to verify a tool before you trust it
&lt;/h2&gt;

&lt;p&gt;You do not need to be a security researcher. Five checks catch most false advertising.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Open DevTools → Network, then use the tool
&lt;/h3&gt;

&lt;p&gt;Paste a distinctive string (&lt;code&gt;PRIVATE_TOKEN_TEST_9f3a&lt;/code&gt;) or pick a small file. Run the action. Filter the Network panel to Fetch/XHR. If your string or file appears in a request payload, the work left the device. If the only requests are the initial page assets (or a manifest fetch that does not include your input), you are in better shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Try the offline test
&lt;/h3&gt;

&lt;p&gt;Load the page on Wi‑Fi. Toggle the browser's Offline checkbox (or disable the network). Run the tool again. True client-side tools still work. Tools that "need a quick API call" fail loudly — which is useful information.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prefer readable source over mystery bundles
&lt;/h3&gt;

&lt;p&gt;View Source or open the linked &lt;code&gt;.js&lt;/code&gt; module. You want to see &lt;code&gt;crypto.subtle&lt;/code&gt;, Canvas calls, or straightforward string logic — not a single 400 KB minified line shipping to three analytics domains. Perfect purity is rare; unexplained third-party scripts on a "privacy" page are a red flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Check what the tool claims about storage
&lt;/h3&gt;

&lt;p&gt;"We never store uploads" is weaker than "computation never leaves your browser." In-memory server processing can still be acceptable for non-sensitive chores if the policy is clear and the alternative is installing desktop software. Know which promise you are getting.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Mind the side channels
&lt;/h3&gt;

&lt;p&gt;Even a local page can sync via extensions, password managers, or corporate DLP that scrapes the DOM. Local tools reduce &lt;em&gt;site&lt;/em&gt; risk; they do not erase &lt;em&gt;device&lt;/em&gt; risk. Treat the whole endpoint as the trust boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concrete workflows you can run without uploading
&lt;/h2&gt;

&lt;p&gt;Below are patterns, not product tours. Each maps to tools you will find on privacy-oriented catalogues (including itsfully.online's free set) and to one-off HTML utilities you can save to a folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow A — Clean AI output before it enters a document
&lt;/h3&gt;

&lt;p&gt;LLM paste often carries smart quotes, invisible Unicode, odd line breaks, and markdown debris.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste into a &lt;strong&gt;whitespace / invisible-character cleaner&lt;/strong&gt; (local).&lt;/li&gt;
&lt;li&gt;Run a &lt;strong&gt;diff&lt;/strong&gt; against the previous draft if you are editing, not replacing.&lt;/li&gt;
&lt;li&gt;Optionally run a &lt;strong&gt;regex extract&lt;/strong&gt; for emails, URLs, or ticket IDs you must not lose.&lt;/li&gt;
&lt;li&gt;Only then paste into the final doc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keeping steps 1–3 local means draft copy never hits a "AI cleaner" SaaS on the way to a client deliverable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow B — Ship a screenshot without metadata surprises
&lt;/h3&gt;

&lt;p&gt;Photos and exports often embed EXIF: device model, timestamps, sometimes GPS.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the image in a local &lt;strong&gt;EXIF viewer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If anything sensitive appears, use a local &lt;strong&gt;EXIF remover&lt;/strong&gt; or re-export via Canvas (draw to canvas → download PNG/JPEG).&lt;/li&gt;
&lt;li&gt;Spot-check colour-critical UI with a &lt;strong&gt;contrast checker&lt;/strong&gt; before publishing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Journalists, support teams, and anyone posting UI bugs benefit from this habit. The file never needs a third-party "metadata scrubber" upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow C — Make JSON and CSV safe to share &lt;em&gt;after&lt;/em&gt; you inspect them
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Drop a dump into a local &lt;strong&gt;JSON formatter / key sorter&lt;/strong&gt; so diffs are stable.&lt;/li&gt;
&lt;li&gt;For CSV, run a local &lt;strong&gt;delimiter detect&lt;/strong&gt; and &lt;strong&gt;profiler&lt;/strong&gt; (types, nulls, rough distributions) &lt;em&gt;before&lt;/em&gt; forwarding.&lt;/li&gt;
&lt;li&gt;Redact columns in a spreadsheet offline; only then upload to the destination that actually needs the file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The profiler step catches the classic mistake: emailing a "sample" that still contains production emails in row 40.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow D — Check design tokens without Figma round-trips
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Convert HEX ↔ RGB ↔ HSL locally while drafting CSS.&lt;/li&gt;
&lt;li&gt;Validate text/background pairs with a &lt;strong&gt;WCAG contrast&lt;/strong&gt; tool.&lt;/li&gt;
&lt;li&gt;Extract a palette from a reference image in-browser when you cannot open a design file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful for developers who inherit a brand PDF and need answers in under a minute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow E — Hash, encode, and verify without a "hash online" site
&lt;/h3&gt;

&lt;p&gt;Password-reset tokens, file integrity checks, and Base64 data URIs are routine. A local &lt;strong&gt;hash&lt;/strong&gt; tool and &lt;strong&gt;Base64&lt;/strong&gt; encoder replace a category of websites that have no business seeing the input. Prefer Web Crypto over copy-pasting secrets into search-indexed pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow F — Agent-friendly chaining
&lt;/h3&gt;

&lt;p&gt;If you automate with an agent or a small script, prefer tools exposed as &lt;strong&gt;ES modules&lt;/strong&gt; behind a &lt;strong&gt;JSON manifest&lt;/strong&gt;: fetch catalogue → &lt;code&gt;import()&lt;/code&gt; module → call one function → keep the result in your runtime. That loop stays on the machine when the modules are client-side. It is the same privacy story as the GUI, just without the clicks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a personal offline kit (30 minutes)
&lt;/h2&gt;

&lt;p&gt;You do not need a platform. You need a folder and a habit.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; &lt;code&gt;~/Tools/browser-local/&lt;/code&gt; (or a synced vault you control).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save&lt;/strong&gt; a few single-file HTML utilities (hash, JSON format, contrast, EXIF strip). Prefer MIT-licensed pages you can audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bookmark&lt;/strong&gt; a labelled catalogue for everything else — for example &lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt; when you want a broader set without installing desktop apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a one-page cheat sheet&lt;/strong&gt;: which tool for which step in &lt;em&gt;your&lt;/em&gt; recurring jobs (support screenshots, blog images, CSV handoffs, prompt cleanup).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rehearse offline once.&lt;/strong&gt; Open the saved HTML files in airplane mode. Fix anything that silently depended on a CDN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Optional hardening: pin versions (save the &lt;code&gt;.html&lt;/code&gt; / &lt;code&gt;.js&lt;/code&gt; rather than always loading live), and keep secrets workflows on files you downloaded, not on pages that can change under you.&lt;/p&gt;

&lt;p&gt;For people who want more than bookmarks — checklists, chaining patterns (input → inspect → transform → validate → output), and reusable templates around that style of work — the &lt;a href="https://theknowledgeproject.gumroad.com/l/dzmhu" rel="noopener noreferrer"&gt;Digital Workflow Toolkit&lt;/a&gt; ($19 on Gumroad) packages those recipes alongside the free itsfully.online tools. Use it if structured playbooks help you; the free tools stand alone if they do not.&lt;/p&gt;




&lt;h2&gt;
  
  
  When a server is still the right tool
&lt;/h2&gt;

&lt;p&gt;Local-first is a bias, not a religion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large media transcodes&lt;/strong&gt; may need ffmpeg-class horsepower you do not have in-tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration&lt;/strong&gt; needs a shared source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensed models / proprietary OCR&lt;/strong&gt; may only exist as APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance logging&lt;/strong&gt; sometimes &lt;em&gt;requires&lt;/em&gt; a controlled server trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, prefer vendors with clear retention policies, regional hosting if that matters to you, and a delete story you can believe. The mistake is not using servers; it is uploading payroll CSVs to a no-name "CSV to Excel" page because it was the first search hit.&lt;/p&gt;

&lt;p&gt;A practical rule: &lt;strong&gt;if the output is a pure function of the input and the input is sensitive, compute locally.&lt;/strong&gt; If the job needs shared state, specialized hardware, or a licensed engine, pick an intentional host — not an accidental one.&lt;/p&gt;




&lt;h2&gt;
  
  
  A short checklist you can reuse
&lt;/h2&gt;

&lt;p&gt;Before you paste or upload into any "free tool," ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Does this need a server, or is it a pure transform?&lt;/li&gt;
&lt;li&gt;[ ] Did I watch the Network panel once for this site?&lt;/li&gt;
&lt;li&gt;[ ] Would I be uncomfortable seeing this input in a breach dump tomorrow?&lt;/li&gt;
&lt;li&gt;[ ] Can I do the same job with a saved local HTML page or a desktop utility I already trust?&lt;/li&gt;
&lt;li&gt;[ ] If the vendor mixes local and server tools, is &lt;em&gt;this&lt;/em&gt; tool labelled clearly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If three or more answers push you toward local, stay local.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Explore free, labelled browser-native utilities at &lt;strong&gt;&lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt;&lt;/strong&gt; (catalogue + public manifest for agents).&lt;/li&gt;
&lt;li&gt;For chaining those small operations into end-to-end personal workflows, see the &lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/dzmhu" rel="noopener noreferrer"&gt;Digital Workflow Toolkit&lt;/a&gt;&lt;/strong&gt; ($19).&lt;/li&gt;
&lt;li&gt;For a shorter companion read on agent-centred browser workflows, &lt;strong&gt;&lt;a href="https://theknowledgeproject.gumroad.com/l/yawbve" rel="noopener noreferrer"&gt;Agentic AI in the Browser&lt;/a&gt;&lt;/strong&gt; ($4.99) sits on the same shelf.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use what helps; ignore the rest. The durable skill is not memorising a directory of sites — it is recognising when data never needed to leave the device in the first place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was drafted for Yunus Emre Vurgun's projects. He runs &lt;a href="https://itsfully.online" rel="noopener noreferrer"&gt;itsfully.online&lt;/a&gt; and sells the Digital Workflow Toolkit and Agentic AI in the Browser on Gumroad. No fabricated testimonials, traffic figures, or outcome guarantees appear above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Put a connected-to-publisher flag in your MCP tool output</title>
      <dc:creator>Yunus Emre Vurgun</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:44:00 +0000</pubDate>
      <link>https://dev.to/yunusemrevurgun/put-a-connected-to-publisher-flag-in-your-mcp-tool-output-439o</link>
      <guid>https://dev.to/yunusemrevurgun/put-a-connected-to-publisher-flag-in-your-mcp-tool-output-439o</guid>
      <description>&lt;p&gt;Ranking directories, "best tools" lists, and recommendation roundups are exactly the kind of data agents reach for when someone asks "what's the best X?" They're also the kind of data with a hidden conflict: many of those listings are connected to the publisher — affiliate links, first-party products, sponsored placements. Humans can often smell this. Agents reading a JSON blob through an MCP tool usually can't.&lt;/p&gt;

&lt;p&gt;I ran into this while building a read-only MCP server for a ranking directory, and the fix was small enough that I think anyone shipping an MCP tool with list-like output should steal it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: agents can't tell a connected listing from an organic one
&lt;/h2&gt;

&lt;p&gt;When an agent calls a tool and gets a list of products or sites back, it sees rows: name, description, rank, maybe a URL. Nothing in that payload says "the publisher of this directory has a relationship with entry #1."&lt;/p&gt;

&lt;p&gt;This matters more than it used to. Agents are increasingly the ones summarizing options for humans. If the tool output doesn't carry provenance, the agent will happily present a paid or first-party ranking as neutral fact. The human never sees the caveat, because the caveat never existed in the data.&lt;/p&gt;

&lt;p&gt;Search engines solved this with editorial policies and visible "sponsored" labels. Models don't read policies; they read tool output. So the disclosure has to live in the tool output itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design: put the disclosure where the machine reads
&lt;/h2&gt;

&lt;p&gt;The server I worked on is read-only — it serves ranking data and nothing else. We added one field per listing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"connected_to_publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connected_to_publisher&lt;/code&gt; is a boolean. &lt;code&gt;true&lt;/code&gt; means the publisher of the directory has a relationship with that entry: it's their own product, they're affiliated, or the placement is not purely organic. &lt;code&gt;false&lt;/code&gt; means no known connection.&lt;/p&gt;

&lt;p&gt;Three things made this work in practice, beyond the field itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A methodology page, linked from the tool.&lt;/strong&gt; The flag is only trustworthy if &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; mean something consistent. We wrote a short methodology page explaining what counts as a connection, how entries are ranked, and how often the data is reviewed. The server links to it, so an agent — or a skeptical human — can follow the trail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON and &lt;code&gt;.md&lt;/code&gt; twins.&lt;/strong&gt; Agents don't only consume JSON. Some pipelines ingest Markdown; some humans read the raw file. Every JSON payload has a Markdown twin with the same information, disclosure included, in prose. Same data, two renderings — no chance the human-facing page says something the machine-facing endpoint doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/strong&gt; The site ships an &lt;code&gt;llms.txt&lt;/code&gt; that orients a model landing on it cold: what the directory is, how it's ranked, where the methodology lives, and — explicitly — that some entries are connected to the publisher. It's the same context a first-time human visitor gets, written for a context window instead of a scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal for your own MCP tools
&lt;/h2&gt;

&lt;p&gt;You don't need a ranking directory to benefit. Any tool that returns "recommended", "popular", "featured", or "best" output has the same conflict. A concrete checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a provenance field.&lt;/strong&gt; One boolean or enum per row: &lt;code&gt;connected_to_publisher&lt;/code&gt;, &lt;code&gt;sponsored&lt;/code&gt;, &lt;code&gt;affiliate&lt;/code&gt;, &lt;code&gt;first_party&lt;/code&gt; — pick your vocabulary and document it. Flat fields are easier for models to reason about than prose buried in a description string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put it at the row level.&lt;/strong&gt; Don't hide the flag in a nested &lt;code&gt;metadata&lt;/code&gt; object that summarizers tend to drop. Keep it next to the name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain the field in the tool description.&lt;/strong&gt; MCP tool descriptions are read before the tool is called. One sentence — "results include a &lt;code&gt;connected_to_publisher&lt;/code&gt; flag; &lt;code&gt;true&lt;/code&gt; means the publisher has a relationship with the entry" — primes the agent to carry the disclosure into its answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship a methodology page and link it.&lt;/strong&gt; Flags without definitions rot. The page is where you define your terms and where you update them when the policy changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve twin formats.&lt;/strong&gt; JSON for tools, Markdown for humans and context-stuffing pipelines. Generate both from one source so they can't drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/strong&gt; Cheap, static, and it gives any model a fair description of what your site is and isn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is novel legal theory — it's the same principle as a sponsored-post label, moved into the layer machines actually read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why honesty here builds trust instead of costing you
&lt;/h2&gt;

&lt;p&gt;The obvious worry: won't flagging your own entries scare people off? In practice it does the opposite. A directory that admits which rows it's connected to is more believable about the rows it isn't. And in agent-mediated discovery, a tool that hands the model clean, honest metadata is a tool the model keeps calling — because the answers it produces hold up.&lt;/p&gt;

&lt;p&gt;The alternative is worse: eventually someone else's agent summarizes your directory, your conflicts surface anyway, and you had no say in how they were framed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I help Yunus Emre Vurgun with his projects; listme.name is his ranking directory: &lt;a href="https://listme.name" rel="noopener noreferrer"&gt;https://listme.name&lt;/a&gt; — the concrete example behind everything above.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Monte Carlo bid estimating: price a construction bid at P80, not a single number</title>
      <dc:creator>Yunus Emre Vurgun</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:47:29 +0000</pubDate>
      <link>https://dev.to/yunusemrevurgun/monte-carlo-bid-estimating-price-a-construction-bid-at-p80-not-a-single-number-13a4</link>
      <guid>https://dev.to/yunusemrevurgun/monte-carlo-bid-estimating-price-a-construction-bid-at-p80-not-a-single-number-13a4</guid>
      <description>&lt;p&gt;Most bids are built the same way: price each line item at its most likely number, add them up, put overhead and profit on top. The problem is that adding up "most likely" numbers doesn't give you the most likely total. It gives you a number the job will usually go over.&lt;/p&gt;

&lt;p&gt;This article shows why, and walks through a small Monte Carlo estimate: three-point ranges, a choice of distribution, correlation between labor-driven trades, and a confidence level turned into contingency. The figures are made up. Every result comes from the Python script below, run with a fixed seed so you'll get the same numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a single-point estimate is biased low
&lt;/h2&gt;

&lt;p&gt;Think about one line item, say framing and drywall. Your most likely figure is $95k. How much better could it go? Maybe the crew is quick and the layout is simple, and it comes in at $86k. How much worse? Out-of-square walls, a slow GC, a late change to the ceiling heights, and it's $128k. You can save $9k or lose $33k.&lt;/p&gt;

&lt;p&gt;Most construction costs look like that: a floor on how cheaply the work can be done, no real ceiling on how badly it can go. When the downside is longer than the upside, the average outcome sits above the most likely one, and across a dozen skewed items the gaps add up. On top of that, estimators anchor on the version of the job that goes to plan, because that's the version you can price. So the single number carries two biases: skew, and optimism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-point ranges per line item
&lt;/h2&gt;

&lt;p&gt;The fix starts at the line-item level. For each item, write three numbers instead of one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Min (optimistic):&lt;/strong&gt; what it costs if things go well. Not a miracle, just a good run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most likely:&lt;/strong&gt; the number you'd have written anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max (pessimistic):&lt;/strong&gt; what it costs if the realistic problems happen. Not the end of the world, but the bad job you've seen before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give each range a sentence of reasoning ("max assumes two weeks of winter conditions"). Leave firm amounts, like a signed sub quote, as single numbers. Put the uncertainty where it really is: labor productivity, unmeasured quantities, unknown conditions, and duration-driven costs like general conditions.&lt;/p&gt;

&lt;p&gt;Here's the example job, a small commercial build-out, in thousands of dollars:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Most likely&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;th&gt;Labor-driven?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Demo &amp;amp; sitework&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;165&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structural steel&lt;/td&gt;
&lt;td&gt;205&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framing &amp;amp; drywall&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEP rough-in &amp;amp; trim&lt;/td&gt;
&lt;td&gt;240&lt;/td&gt;
&lt;td&gt;262&lt;/td&gt;
&lt;td&gt;330&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finishes&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General conditions&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;896&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The single-point estimate is $896k. Every range is skewed to the right, as real ones usually are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triangular vs. PERT: what shape goes between the three numbers
&lt;/h2&gt;

&lt;p&gt;Three numbers don't define a distribution by themselves. You have to choose a shape. The two common choices are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triangular.&lt;/strong&gt; A straight line up from min to most likely and down to max. Its mean is (min + most likely + max) / 3, so the extremes carry as much weight as the most likely value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beta-PERT.&lt;/strong&gt; A smooth curve over the same range that bunches more of its weight near the most likely value. With the standard setting (λ = 4) its mean is (min + 4 × most likely + max) / 6, the classic PERT weighted average.&lt;/p&gt;

&lt;p&gt;The choice matters more than people expect. In the example, the sum of the PERT means is $923.8k and the sum of the triangular means is $951.7k. That's $28k of difference from the shape alone, with the same inputs.&lt;/p&gt;

&lt;p&gt;Neither is "the conservative one"; they describe different beliefs. Use PERT when your most likely number is well grounded, such as tracked production rates. Use triangular when the most likely figure is itself a guess. Pick one per estimate, write down which, and don't switch to whichever gives the answer you hoped for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlation: the step most spreadsheets skip
&lt;/h2&gt;

&lt;p&gt;A basic Monte Carlo draws each line item independently. In one run framing comes in high and MEP comes in low, so they partly cancel out. Across thousands of runs that cancelling makes the total look tighter than it is.&lt;/p&gt;

&lt;p&gt;Real jobs don't work like that. If productivity on this site is poor (access, weather, the superintendent, a tight labor market), it's poor for the framers, the electricians and the finishers at the same time. Slow work stretches the schedule, and general conditions grow with it. Treating those items as independent promises a diversification the job site won't deliver.&lt;/p&gt;

&lt;p&gt;In the example, the four labor-driven items get a correlation of 0.6 with each other. The script uses a simple rank-reordering method (in the style of Iman-Conover): it draws each item's costs from its own PERT distribution, then reorders them to follow the ranks of correlated normal draws. The shape of each line item stays exactly the same, and only the way the items pair up within a run changes. The correlation it actually achieved was 0.59.&lt;/p&gt;

&lt;p&gt;Here are the results from 100,000 runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P80&lt;/th&gt;
&lt;th&gt;P90&lt;/th&gt;
&lt;th&gt;Runs above $896k&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Triangular, independent&lt;/td&gt;
&lt;td&gt;951.8&lt;/td&gt;
&lt;td&gt;950.5&lt;/td&gt;
&lt;td&gt;975.5&lt;/td&gt;
&lt;td&gt;988.5&lt;/td&gt;
&lt;td&gt;98.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PERT, independent&lt;/td&gt;
&lt;td&gt;923.9&lt;/td&gt;
&lt;td&gt;922.9&lt;/td&gt;
&lt;td&gt;943.3&lt;/td&gt;
&lt;td&gt;954.2&lt;/td&gt;
&lt;td&gt;89.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PERT, labor correlated 0.6&lt;/td&gt;
&lt;td&gt;923.9&lt;/td&gt;
&lt;td&gt;921.5&lt;/td&gt;
&lt;td&gt;950.9&lt;/td&gt;
&lt;td&gt;966.6&lt;/td&gt;
&lt;td&gt;79.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The single-point estimate is unlikely to hold.&lt;/strong&gt; Even in the most favourable model, 79.7% of runs cost more than $896k.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation doesn't change the mean, but it does change the tail.&lt;/strong&gt; The mean stays at $923.9k. The spread from P50 to P90 grows from $31.3k to $45.1k, about 44% wider, and P90 moves up by $12.4k. That's the risk an independent model leaves out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The share of runs over $896k drops with correlation, from 89.1% to 79.7%.&lt;/strong&gt; Not good news: a wider distribution has more good runs and more bad ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fub1ge5d9pz6rri0fsfyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fub1ge5d9pz6rri0fsfyv.png" alt="Histogram of 100,000 simulated totals with the single-point estimate, P50 and P80 marked" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where does 0.6 come from? Ideally from your job-cost history: how often did the labor trades overrun together? Without that data, run 0.3, 0.6 and 0.8 and watch the P80. If the bid only works at zero correlation, it doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a confidence level and turning it into contingency
&lt;/h2&gt;

&lt;p&gt;A percentile is a statement about your model. P80 is the cost that 80% of the simulated runs come in at or below.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P50&lt;/strong&gt; is a coin flip. Bidding the P50 cost plus your normal markup means you expect to eat into that markup about half the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P80&lt;/strong&gt; is a common commercial reference: you cover the realistic bad outcomes, but not the worst ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P90&lt;/strong&gt; suits work where an overrun is expensive to absorb: a small company with one large job, liquidated damages, or a fixed-price contract with no change mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between your chosen percentile and the base estimate is your contingency. Show it as its own line, not hidden inside the unit prices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Contingency over $896k&lt;/th&gt;
&lt;th&gt;% of base&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P50&lt;/td&gt;
&lt;td&gt;921.5&lt;/td&gt;
&lt;td&gt;25.5&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P80&lt;/td&gt;
&lt;td&gt;950.9&lt;/td&gt;
&lt;td&gt;54.9&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P90&lt;/td&gt;
&lt;td&gt;966.6&lt;/td&gt;
&lt;td&gt;70.6&lt;/td&gt;
&lt;td&gt;7.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then apply overhead and profit to the risk-adjusted cost, keeping markup and contingency separate. Contingency is there to pay for the risk you modeled. Markup is what you're paid for doing the work. With a 10% markup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bid at single point:&lt;/strong&gt; 896 × 1.10 = &lt;strong&gt;$985.6k.&lt;/strong&gt; In 3.48% of runs the job loses money outright, and in 28.85% of runs you keep less than half your planned markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bid at P80:&lt;/strong&gt; 950.9 × 1.10 = &lt;strong&gt;$1,045.9k.&lt;/strong&gt; None of the 100,000 runs lost money, and 1.45% kept less than half the markup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second bid is $60k higher and may not win. The method doesn't decide for you; it shows the trade. If the market won't bear the P80 number you can still bid lower, knowing that in roughly three runs out of ten you keep less than half your markup. That's a business decision, not an estimating error you discover at closeout.&lt;/p&gt;

&lt;p&gt;One more thing to check: the sum of the line items' own P80s is $972.4k, while the P80 of the total is $950.9k. Padding each line to its own P80 and adding them up overstates the total, because not every item goes bad on the same job. Simulate the total and set contingency once, at the project level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;This is the whole example. It needs numpy, plus matplotlib if you want the histogram. Change the &lt;code&gt;items&lt;/code&gt; table to your own line items and rerun.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Monte Carlo bid estimate: single number vs. P50/P80/P90.

Worked example for the article &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Monte Carlo bid estimating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
Requires numpy (matplotlib optional, for the histogram).
All figures are illustrative, in thousands of dollars ($k).
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="n"&gt;SEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20261012&lt;/span&gt;
&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;
&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SEED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# line item: (min, most likely, max) direct cost in $k, labor-driven?
&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo &amp;amp; sitework&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Concrete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;165&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Structural steel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;205&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Framing &amp;amp; drywall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEP rough-in &amp;amp; trim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;262&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finishes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;General conditions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;labor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Beta-PERT sample: mean = (a + 4m + b) / 6.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lam&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lam&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;triangular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;correlate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Reorder each column to follow the ranks of correlated normals
    (Iman-Conover style). Marginals are unchanged; only pairing changes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multivariate_normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ranks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="n"&gt;ranks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p90&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;p_over&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mean &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  P50 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P80 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  P90 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p90&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  P(cost &amp;gt; single-point) &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p_over&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p90&lt;/span&gt;

&lt;span class="n"&gt;single_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pert_mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Single-point estimate (sum of most likely): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;single_point&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sum of PERT means (O + 4M + P)/6:           &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pert_mean&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sum of triangular means (O + M + P)/3:      &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;tri_ind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;pert_ind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column_stack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;pert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

&lt;span class="c1"&gt;# labor-driven items share one productivity driver: rho = 0.6 among them
&lt;/span&gt;&lt;span class="n"&gt;rho&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;
&lt;span class="n"&gt;corr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eye&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;labor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;labor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rho&lt;/span&gt;
&lt;span class="n"&gt;pert_cor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;correlate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pert_ind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Triangular, independent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tri_ind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERT, independent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pert_ind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p90&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PERT, labor correlated 0.6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pert_cor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;lab_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labor&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;achieved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;corrcoef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pert_cor&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;lab_idx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;rowvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Achieved correlation between labor items: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;achieved&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;triu_indices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lab_idx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (target &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rho&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;item_p80&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pert_cor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sum of the line items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; own P80s:            &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item_p80&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (vs P80 of the total &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Contingency above the single-point estimate (correlated PERT):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P90&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p90&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;single_point&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;single_point&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of base)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;markup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;  &lt;span class="c1"&gt;# overhead &amp;amp; profit on cost
&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pert_cor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost_basis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;single_point&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cost_basis&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;markup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bid = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cost x &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;markup&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bid&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P(cost &amp;gt; bid, a loss) &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.2&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P(profit &amp;lt; half the markup) &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cost_basis&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;markup&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.2&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib&lt;/span&gt;
    &lt;span class="n"&gt;matplotlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Agg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subplots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dpi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#9db7d5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edgecolor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;single_point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Single point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#555555&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#1f4e79&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#c0392b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;axvline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lab&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Single point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lab&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Single point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ylim&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lab&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;va&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total direct cost ($k)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Simulated runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; runs, Beta-PERT line items, labor items correlated &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rho&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savefig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bidvariance_mc_hist.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Saved bidvariance_mc_hist.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;matplotlib not installed; skipped histogram&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Single-point estimate (sum of most likely): 896.0
Sum of PERT means (O + 4M + P)/6:           923.8
Sum of triangular means (O + M + P)/3:      951.7

Triangular, independent      mean   951.8  P50   950.5  P80   975.5  P90   988.5  P(cost &amp;gt; single-point) 98.6%
PERT, independent            mean   923.9  P50   922.9  P80   943.3  P90   954.2  P(cost &amp;gt; single-point) 89.1%
PERT, labor correlated 0.6   mean   923.9  P50   921.5  P80   950.9  P90   966.6  P(cost &amp;gt; single-point) 79.7%

Achieved correlation between labor items: 0.59 (target 0.6)
Sum of the line items' own P80s:            972.4  (vs P80 of the total 950.9)

Contingency above the single-point estimate (correlated PERT):
  P50:   25.5  (2.8% of base)
  P80:   54.9  (6.1% of base)
  P90:   70.6  (7.9% of base)
Bid = single point cost x 1.10 =   985.6   P(cost &amp;gt; bid, a loss)  3.48%   P(profit &amp;lt; half the markup) 28.85%
Bid = P80 cost x 1.10 =  1045.9   P(cost &amp;gt; bid, a loss)  0.00%   P(profit &amp;lt; half the markup)  1.45%

Saved bidvariance_mc_hist.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ranges that are too narrow.&lt;/strong&gt; If your max is 5% above most likely, ask what would have to go wrong to hit it, and whether it has before. A tight range is the single-point problem with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-counting risk.&lt;/strong&gt; Use ranges for variation in normal work and a short risk register (probability × impact) for discrete events like a permit delay. If the concrete max already allows for rock, don't add a rock allowance too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring correlation.&lt;/strong&gt; Covered above. It mainly understates the tail, which is the part that hurts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summing line-item percentiles.&lt;/strong&gt; P80s don't add. Simulate the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitting items too finely.&lt;/strong&gt; Fifty small independent items cancel each other out and shrink the spread. If you break the estimate down further, correlate the pieces that share a driver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparing runs without a fixed seed.&lt;/strong&gt; Otherwise part of the difference is sampling noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the decimals.&lt;/strong&gt; More iterations reduce noise; they don't make a guessed max more accurate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixing up markup and margin.&lt;/strong&gt; A 10% markup on cost is a 9.1% margin on price. Know which one your pricing rule is using.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How BidVariance does this
&lt;/h2&gt;

&lt;p&gt;Disclosure again: this is the tool I built, a browser-based Monte Carlo tool for fixed-price bids. Here's what it does, per &lt;a href="https://bidvariance.com" rel="noopener noreferrer"&gt;bidvariance.com&lt;/a&gt;, its &lt;a href="https://bidvariance.com/methodology/" rel="noopener noreferrer"&gt;methodology page&lt;/a&gt; and its &lt;a href="https://bidvariance.com/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three-point inputs and Beta-PERT.&lt;/strong&gt; You enter optimistic, likely and pessimistic values for each task. Each is sampled from Beta-PERT with λ = 4, as in the script above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost model.&lt;/strong&gt; Each run's cost is the task's fixed cost plus duration × daily burn rate, plus risk costs, project fixed costs and overhead. It's built around tasks, not dollar line items, so a labor package becomes crew-days × daily crew cost. Tasks take Finish-to-Start dependencies; risk events take a probability plus cost and delay ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs and reproducibility.&lt;/strong&gt; 5k, 25k or 100k runs, with a seeded generator so a number shown in a meeting can be reproduced exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outputs.&lt;/strong&gt; P50/P80/P90 cost and finish dates, probability of loss and of hitting the target margin, and the contingency each confidence level needs over a reference you choose (the single-number plan at likely durations, the expected cost, or the budget your margin allows). The minimum quote is the cost percentile ÷ (1 − target margin). It also has a tornado/sensitivity view and a bid / no-bid label at thresholds you set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and one-time.&lt;/strong&gt; Simulations run in your browser; projects stay on your device. Licenses are one-time, via Gumroad: &lt;a href="https://theknowledgeproject.gumroad.com/l/bidvarpro" rel="noopener noreferrer"&gt;Pro, $129&lt;/a&gt; for one user, &lt;a href="https://theknowledgeproject.gumroad.com/l/bidvarbusiness" rel="noopener noreferrer"&gt;Business, $249&lt;/a&gt; for up to five internal users with scenario comparison, and &lt;a href="https://theknowledgeproject.gumroad.com/l/bidvarconsultant" rel="noopener noreferrer"&gt;Consultant, $399&lt;/a&gt; for client advisory work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest limitation, since it's the topic of half this article: the methodology page says task durations are sampled independently, and there's no general correlation model. The closest thing is the What-if scope-creep test, which applies one uncertain growth factor, drawn once per run, to the tasks you select. That's a reasonable way to stress a shared productivity hit across your labor packages, but it isn't a correlation setting. If correlation drives your risk, check the result with a script like the one above. It also isn't a takeoff or cost-database tool. You bring the estimate, and it handles the probability side.&lt;/p&gt;

&lt;p&gt;If you just want to check one range first, the &lt;a href="https://bidvariance.com/tools/" rel="noopener noreferrer"&gt;free calculators&lt;/a&gt; include a &lt;a href="https://bidvariance.com/pert-calculator/" rel="noopener noreferrer"&gt;PERT estimate calculator&lt;/a&gt; and a &lt;a href="https://bidvariance.com/project-contingency-calculator/" rel="noopener noreferrer"&gt;project contingency calculator&lt;/a&gt;, with no license or signup.&lt;/p&gt;

&lt;p&gt;Whichever tool you use, the habit is what matters: three numbers instead of one, a decision about which items move together, and a confidence level you chose rather than one the sum of most-likely values chose for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published on behalf of Yunus Emre Vurgun, the developer of BidVariance, by his assistant. The project figures above are fictional and illustrative.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>construction</category>
      <category>estimating</category>
      <category>montecarlo</category>
      <category>python</category>
    </item>
  </channel>
</rss>
