Hugdrigo commited on
Commit
46f9812
·
verified ·
1 Parent(s): d3e5c2a

Upload 3 files

Browse files
Files changed (3) hide show
  1. LICENSE +21 -0
  2. README.md +103 -6
  3. index.html +92 -18
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BudgetForge Project Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,109 @@
1
  ---
2
- title: Budgetforge Allocation Lab
3
- emoji: 📊
4
- colorFrom: indigo
5
- colorTo: gray
6
  sdk: static
 
7
  pinned: false
8
  license: mit
9
- short_description: Offline public budget allocation and equity lab
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "BudgetForge | Strategic Budget Intelligence"
3
+ emoji: "🏛️"
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
  license: mit
10
+ short_description: "Offline public budget allocation and equity lab"
11
+ tags:
12
+ - operations-research
13
+ - decision-support
14
+ - optimization
15
+ - public-policy
16
+ - budgeting
17
+ - data-visualization
18
+ - javascript
19
  ---
20
 
21
+ # BudgetForge | Strategic Budget Intelligence
22
+
23
+ **Project #72 · Portfolio optimization · Transparent policy trade-offs · Local-first**
24
+
25
+ BudgetForge is a standalone browser application for exploring how a fixed, illustrative budget can be allocated among candidate public, academic, and community projects. It combines an **exact binary project-portfolio optimizer**, a hard minimum priority-area spending requirement, adjustable decision weights and reproducible preference sensitivity analysis.
26
+
27
+ > **Educational prototype:** Every example, amount, impact score, cost and beneficiary count shipped with the app is **synthetic**. This is not a fiscal forecast, procurement authorization, audit certification, causal evaluation, or substitute for community consultation.
28
+
29
+ ## Live application
30
+
31
+ The `index.html` file contains all CSS, JavaScript, data examples and charts. It runs entirely in your browser as a **Hugging Face Static Space**. No Python server, external libraries, backend, model inference API, registration or paid hardware is required.
32
+
33
+ ## Features
34
+
35
+ | Area | What it does |
36
+ |---|---|
37
+ | Command Center | Budget use, priority-area spending, selected programs and beneficiaries, plus an exact-versus-greedy comparison |
38
+ | Program Portfolio | Search and edit program costs, impact and readiness scores |
39
+ | Allocation Studio | Four adjustable criteria; re-optimize the portfolio immediately |
40
+ | Equity & Coverage | Compare priority-area shares, regional coverage and the unconstrained benchmark |
41
+ | Sensitivity Lab | Re-optimize 120 times under seeded perturbations of the decision weights |
42
+ | Data & Audit | Validated CSV import, seven downloadable outputs and methodology disclosure |
43
+
44
+ ## Included synthetic examples
45
+
46
+ 1. **Municipal Services:** ten community and infrastructure programs, $145k illustrative budget, minimum 25% priority spending.
47
+ 2. **University Research:** ten academic investments, $120k illustrative budget, minimum 30% priority spending.
48
+ 3. **Climate Resilience:** ten adaptation investments, $185k illustrative budget, minimum 30% priority spending.
49
+
50
+ All currency figures are in **thousands of illustrative US dollars** and do not reflect any jurisdiction's current costs.
51
+
52
+ ## Mathematical approach
53
+
54
+ For each project `i`, `x_i ∈ {0,1}` means either fully selected or not selected.
55
+
56
+ **Maximize** `Σ score_i · x_i` subject to:
57
+
58
+ - **Budget:** `Σ cost_i · x_i ≤ B`
59
+ - **Priority share:** `Σ_priority cost_i · x_i ≥ q · Σ_all cost_i · x_i`
60
+ - At least one selected project, and a maximum of fourteen imported candidates.
61
+
62
+ Each `score_i` combines impact (`impact / 100`), impact-per-dollar efficiency (normalized to the maximum impact/cost ratio in the present portfolio), readiness (`readiness / 100`) and low-risk preference (`(100-risk)/100`). Four user-controlled nonnegative weights are normalized to sum to one. The optimizer enumerates every subset of at most 14 candidates; therefore the reported objective is globally optimal **only within this explicitly defined model**. Ties are resolved by larger reported beneficiary reach and then lower cost.
63
+
64
+ **Not in the objective:** Urgency is shown as metadata; estimated beneficiaries are used only to break an exact-score tie. It is misleading to read this objective as social welfare, net present value or verified public benefit.
65
+
66
+ The reference greedy method sorts projects by `score/cost`. It may be infeasible under the priority-share rule and is not a certified optimizer.
67
+
68
+ ### Sensitivity protocol
69
+
70
+ The application generates 120 deterministic preference perturbations using a seeded pseudo-random number generator. The four weights are independently multiplied by factors between 0.58 and 1.42 and then normalized. The budget and area target remain fixed. Selection frequency indicates **robustness to modeled preference changes**, not empirical confidence or an effect probability.
71
+
72
+ ## Import CSV
73
+
74
+ Choose **Data & Audit → CSV template**, fill in these exact fields, and import the file:
75
+
76
+ ```csv
77
+ id,name,area,cost,impact,urgency,risk,readiness,people
78
+ P01,Community learning centre,Priority,24,88,82,25,90,1300
79
+ P02,Urban information portal,General,15,70,62,20,95,1800
80
+ ```
81
+
82
+ - **2–14 rows**, unique IDs, program names up to 120 characters.
83
+ - `area` must be **Priority** or **General** (exact spelling).
84
+ - `cost` is strictly positive, in illustrative thousands of USD.
85
+ - `impact`, `urgency`, `risk`, `readiness` are numerical scores in `[0,100]`.
86
+ - `people` is nonnegative, illustrative reach and **not deduplicated**.
87
+ - Invalid imports are rejected without replacing the current portfolio.
88
+ - Imported datasets and edits live in the current browser session. Download a snapshot before closing or refreshing.
89
+
90
+ ## Exported files
91
+
92
+ The **Data & Audit** section provides a Markdown research report, a full JSON snapshot, selected-project CSV, full-portfolio CSV, decision audit CSV, deterministic sensitivity CSV and import template CSV. CSV downloads escape spreadsheet formula prefixes in text fields.
93
+
94
+ ## Limitations and ethical use
95
+
96
+ - No partial funding, multi-year disbursement, project dependency, uncertainty about impact or mutually exclusive program groupings.
97
+ - Budget equity is based on **spending share**, not the distribution of actual outcomes, access, legal rights or fairness.
98
+ - Beneficiary counts may overlap and are not a measured or audited impact.
99
+ - Input scores are subjective and must be validated with stakeholder evidence, public consultation, feasibility checks and independent review.
100
+ - No procurement, fiscal or regulatory compliance is checked.
101
+ - As this is a purely static app, data is not transmitted to a backend. Do not publish sensitive raw data in the public Space repository.
102
+
103
+ ## Verification
104
+
105
+ The provided technical archive contains `tests/test_engine.js` and `tests/test_browser.py`. The engine suite checks random small cases against an independent recursive exhaustive search. Chromium functional checks cover all six sections, both screen sizes, editing, data validation, exports, and reset flows. These are **local checks**, not confirmation of deployment to a user's Hugging Face account.
106
+
107
+ ## License
108
+
109
+ MIT, see [LICENSE](LICENSE).
index.html CHANGED
@@ -1,19 +1,93 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0b1528">
5
+ <meta name="description" content="BudgetForge: local-first, exact budget portfolio optimization and equity planning with reproducible sensitivity analysis.">
6
+ <title>BudgetForge | Strategic Budget Intelligence Studio</title>
7
+ <style>
8
+ :root{--bg:#08111f;--sidebar:#0d182a;--panel:#101d31;--panel2:#14233a;--border:#26364e;--text:#e8eef9;--muted:#9cacc2;--accent:#59ddb6;--accent2:#64a5f5;--warning:#ffc37e;--danger:#fb8999;--shadow:0 12px 40px rgba(2,7,20,.24)}
9
+ *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.5}a{color:var(--accent2)}.app{display:grid;grid-template-columns:246px minmax(0,1fr);min-height:100vh}.side{background:var(--sidebar);border-right:1px solid var(--border);padding:26px 15px;position:sticky;top:0;height:100vh;display:flex;flex-direction:column}.brand{display:flex;gap:11px;align-items:center;padding:3px 8px 25px}.logo{width:39px;height:39px;border-radius:12px;display:grid;place-items:center;font-size:21px;color:#041525;background:linear-gradient(130deg,#5ee5b2,#84b9ff)}.brand strong{font-size:18px;letter-spacing:-.6px}.brand small,.subdued{color:var(--muted);font-size:11px;display:block}.section-label{font-size:10px;color:#7c90ab;letter-spacing:1.4px;text-transform:uppercase;padding:8px 12px;margin:9px 0 6px;font-weight:800}.nav{display:grid;gap:5px}.nav button{background:none;border:1px solid transparent;color:#afbed2;text-align:left;padding:12px 13px;border-radius:10px;display:flex;align-items:center;gap:10px;font-weight:600}.nav button.active{background:#163347;border-color:#295b62;color:#77ebc6}.nav button:hover{background:#172840;color:#fff}.nav .nicon{width:22px;font-size:16px;text-align:center}.side-bottom{margin-top:auto;padding:15px 10px;color:var(--muted);font-size:11px;border-top:1px solid var(--border)}.side-bottom b{color:#e3f1fa}.main{padding:28px clamp(17px,3.3vw,53px) 48px;min-width:0;max-width:1700px;width:100%;margin:0 auto}.topbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:27px}.crumb{color:#a8bbd4;font-size:12px}.topright{display:flex;align-items:center;gap:9px}.chip{border:1px solid #2c725f;background:#12392f;padding:6px 11px;border-radius:99px;color:#8aedd0;font-size:11px;font-weight:750;white-space:nowrap}.chip.secondary{border-color:#344967;background:#13243c;color:#b8cef3}.btn{background:var(--accent);border:1px solid #7ee5c9;color:#05231f;border-radius:9px;padding:10px 14px;font-weight:800;display:inline-flex;align-items:center;justify-content:center;gap:7px}.btn:hover{filter:brightness(1.08)}.btn.alt{background:#172841;border-color:#395171;color:#cbdcf3}.btn.small{padding:7px 10px;font-size:12px}.btn.danger{background:#3c2030;color:#ffe1e5;border-color:#794053}h1{font-size:clamp(26px,2.7vw,39px);letter-spacing:-1.35px;line-height:1.1;margin:0 0 11px;font-weight:830}h2{font-size:17px;letter-spacing:-.4px;margin:0 0 5px}h3{font-size:13px;margin:0 0 6px}.muted{color:var(--muted)}.lead{max-width:750px;color:var(--muted);margin:0 0 24px}.headline{display:flex;justify-content:space-between;align-items:start;gap:20px}.title-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.small-note{font-size:11px;color:var(--muted)}.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.formfield{display:flex;flex-direction:column;gap:5px;min-width:120px}.formfield label,.range-label{font-size:11px;color:#b5c5da;font-weight:700}select,input[type=number],input[type=text],input[type=search]{background:#0b182a;color:var(--text);border:1px solid #364a65;border-radius:8px;padding:9px 11px;min-width:0;width:100%;outline:none}input:focus,select:focus{border-color:#62d9b8;box-shadow:0 0 0 2px #4acfa62a}input[type=range]{width:100%;accent-color:var(--accent)}.grid{display:grid;gap:15px}.kpis{grid-template-columns:repeat(4,minmax(0,1fr));margin:22px 0}.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;box-shadow:var(--shadow);min-width:0}.cardpad{padding:20px}.kpi{padding:19px 20px;position:relative;overflow:hidden}.kpi:before{position:absolute;content:"";height:3px;background:linear-gradient(90deg,#51ddb3,transparent);left:0;right:0;top:0}.kpi .label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:800}.kpi .num{font-size:29px;font-weight:820;letter-spacing:-1.3px;margin:8px 0 2px;white-space:nowrap}.kpi .detail{color:#a7bbcd;font-size:11px}.split{grid-template-columns:minmax(0,1.48fr) minmax(310px,1fr)}.cols3{grid-template-columns:repeat(3,minmax(0,1fr))}.cardheader{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:16px}.eyebrow{font-size:10px;letter-spacing:1.5px;font-weight:800;color:#79dbba;text-transform:uppercase;margin-bottom:8px}.pill{border-radius:99px;border:1px solid #345774;background:#152b40;padding:4px 9px;color:#c7d9ec;font-size:10px;white-space:nowrap}.pill.good{color:#73ebbe;border-color:#326b61;background:#173b35}.pill.bad{color:#ffb2b2;border-color:#74505d;background:#402432}.progress{height:9px;background:#21344c;border-radius:20px;overflow:hidden}.progress>div{height:100%;background:linear-gradient(90deg,#65d9ad,#5ca6ed);border-radius:20px}.stat-row{display:flex;justify-content:space-between;align-items:center;gap:13px;margin:11px 0 5px}.stat-row b{font-size:12px}.stat-row span{font-size:12px;color:var(--muted)}.legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:12px;color:var(--muted);font-size:11px}.dot{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:6px;background:#65ddb7}.dot.blue{background:#77aafa}.dot.orange{background:#ffc17f}.notice{background:#14293a;border:1px solid #30546a;padding:12px 14px;border-radius:10px;font-size:12px;color:#c1d5e8;margin:12px 0}.notice.warn{background:#392a20;border-color:#795f45;color:#ffe0b4}.empty{padding:25px;text-align:center;color:var(--muted)}.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:10px}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap;font-size:12px}th{background:#16243b;color:#a8bed7;letter-spacing:.45px;font-size:10px;text-transform:uppercase}th,td{padding:11px 12px;border-bottom:1px solid #26364b}tbody tr:last-child td{border-bottom:0}tbody tr:hover{background:#1a2d44}td b{font-size:12px}.status{padding:4px 8px;border-radius:8px;background:#244937;color:#aaf6d4;font-size:10px;font-weight:800}.status.out{background:#253149;color:#a2b3ce}.two{display:grid;grid-template-columns:1fr 1fr;gap:15px}.slider-row{border-bottom:1px solid #26364b;padding:12px 0}.slider-row:last-child{border:0}.slider-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px}.slider-top b{font-size:12px}.slider-top span{font-size:12px;color:var(--accent);font-weight:800}.mini-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.metric-mini{padding:14px;border-radius:10px;background:#162840;border:1px solid #2a405a}.metric-mini strong{display:block;font-size:19px}.metric-mini span{font-size:10px;color:#a6bbd1}.select-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}.scenario-btn{border-radius:10px;background:#102139;border:1px solid #34506a;color:#b6cee7;padding:13px;text-align:left}.scenario-btn strong{display:block;font-size:12px;color:#f3f8fc}.scenario-btn small{display:block;color:#8babc9;margin-top:4px}.scenario-btn.selected{border-color:#62ddb9;background:#1a3e3e}.bar-chart{display:grid;gap:12px}.barline{display:grid;grid-template-columns:minmax(120px,1.3fr) minmax(120px,2fr) 64px;gap:11px;align-items:center;font-size:11px}.bar-track{height:12px;border-radius:20px;background:#20334b;overflow:hidden}.bar-track div{height:100%;border-radius:20px;background:linear-gradient(90deg,#3c987f,#79eac8)}.bar-track .dim{background:#567194}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.hide{display:none!important}.page{display:none}.page.active{display:block}.divider{height:1px;background:var(--border);margin:17px 0}.download-actions{display:flex;gap:9px;flex-wrap:wrap}.summary{display:flex;gap:16px;align-items:center;flex-wrap:wrap}.summary strong{font-size:23px}.info-tile{border:1px solid #2e425e;border-radius:10px;padding:14px;background:#14253a}.flow-note{margin-top:12px;font-size:12px;color:#8eaac0}.switch{display:flex;align-items:center;gap:8px;font-size:11px}.switch input{accent-color:var(--accent)}.toast{position:fixed;right:22px;bottom:20px;background:#193a34;border:1px solid #57b99d;padding:12px 17px;border-radius:11px;color:#e7fff7;box-shadow:0 10px 30px #0009;z-index:100;font-size:12px;max-width:min(400px,90vw)}.footer{margin:30px 0 0;text-align:center;color:#71869f;font-size:11px}.scatter{width:100%;height:auto;max-height:340px;display:block}.scroll-tabs{display:none}button:focus-visible{outline:2px solid #86f7d9;outline-offset:3px}.help{color:#8db0cd;font-size:11px;line-height:1.6}.number-edit{max-width:78px;padding:6px!important}#importFile{max-width:100%;font-size:12px;color:#d4e4f4}#importFile::file-selector-button{padding:7px;border-radius:8px;margin-right:12px;border:1px solid #54728c;background:#16364b;color:#b7dff1;cursor:pointer}
10
+ @media(max-width:1150px){.app{grid-template-columns:205px minmax(0,1fr)}.side{padding:20px 10px}.split{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,1fr)}}
11
+ @media(max-width:760px){.app{display:block}.side{position:static;height:auto;padding:13px 15px;border-right:0;border-bottom:1px solid var(--border)}.brand{padding:0}.section-label,.side-bottom,.side .nav{display:none}.main{padding:18px 15px 34px}.topbar{margin-bottom:18px}.topright .chip.secondary{display:none}.headline{display:block}.title-actions{margin-top:15px;justify-content:start}.kpis{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;margin:17px 0}.kpi{padding:14px}.kpi .num{font-size:23px}.grid{gap:11px}.cardpad{padding:16px}.split,.two,.cols3,.select-grid{grid-template-columns:1fr}.mini-metrics{grid-template-columns:repeat(2,1fr)}.scroll-tabs{display:flex;overflow:auto;gap:7px;padding:10px 0 3px;scrollbar-width:thin}.scroll-tabs button{flex:0 0 auto;background:#142840;border:1px solid #33516d;border-radius:9px;padding:9px 13px;color:#c7d7e7;font-size:11px}.scroll-tabs button.active{border-color:#62ddb9;color:#77f4ce;background:#1a3b3b}.barline{grid-template-columns:minmax(90px,1fr) minmax(100px,1fr) 50px;gap:6px}.lead{margin-bottom:16px}.headline h1{font-size:29px}}
12
+ </style>
13
+ </head><body>
14
+ <div class="app">
15
+ <aside class="side"><div class="brand"><div class="logo">◈</div><div><strong>BudgetForge</strong><small>Decision Intelligence Studio</small></div></div><div class="section-label">Workspace</div><nav class="nav" id="nav"></nav><div class="side-bottom"><b>LOCAL-FIRST ANALYSIS</b><br>All calculations run in your browser. No server or account required.<br><br>Version 1.0 · Synthetic demonstrations</div></aside>
16
+ <main class="main"><div class="topbar"><div class="crumb">Research toolkit <span style="color:#66859e">/</span> Project #72 <span style="color:#66859e">/</span> BudgetForge</div><div class="topright"><span class="chip secondary">● Reproducible models</span><span class="chip">● Offline ready</span></div></div>
17
+ <div class="scroll-tabs" id="mobileNav" aria-label="Modules"></div>
18
+ <header class="headline"><div><div class="eyebrow">PUBLIC FINANCE · RESOURCE ALLOCATION · DECISION SCIENCE</div><h1 id="pageTitle">Budget Intelligence</h1><p class="lead" id="pageDesc">Optimize a portfolio of projects under a fixed budget, equity commitments and transparent scoring rules.</p></div><div class="title-actions"><button class="btn alt small" id="resetBtn">↺ Reset scenario</button><button class="btn small" id="downloadReport">↓ Download report</button></div></header>
19
+ <div class="controls" style="margin:0 0 12px"><div class="formfield" style="min-width:210px"><label for="scenarioSelect">DEMONSTRATION DATASET</label><select id="scenarioSelect"></select></div><div class="formfield" style="min-width:126px"><label for="budgetInput">BUDGET (THOUSANDS USD)</label><input type="number" id="budgetInput" min="0" max="10000" step="1"></div><div class="formfield" style="min-width:140px"><label for="quotaInput">PRIORITY AREA MIN. (%)</label><input type="number" id="quotaInput" min="0" max="100" step="5"></div></div>
20
+ <div class="page active" id="view-overview"></div><div class="page" id="view-portfolio"></div><div class="page" id="view-allocation"></div><div class="page" id="view-equity"></div><div class="page" id="view-sensitivity"></div><div class="page" id="view-data"></div>
21
+ <div class="footer">BUDGETFORGE v1.0 · PROJECT #72 · MIT · EDUCATIONAL, SYNTHETIC DATA ONLY · NOT FISCAL OR LEGAL ADVICE</div></main></div>
22
+ <div role="status" aria-live="polite" id="toast" class="toast hide"></div>
23
+ <script id="engine">
24
+ /* Public mathematical core: no external dependencies. */
25
+ const Engine=(()=>{
26
+ const clamp=(x,lo,hi)=>Math.max(lo,Math.min(hi,x));
27
+ const round=(v,n=2)=>Math.round((v+Number.EPSILON)*10**n)/10**n;
28
+ const allowed=['Priority','General'];
29
+ const raw=[
30
+ {key:'municipal',name:'Municipal Services',subtitle:'Community development & infrastructure',budget:145,quota:25,programs:[
31
+ ['M01','Primary care outreach','Priority',35,94,85,24,91,3100],['M02','Public transport links','General',40,88,73,35,87,4500],['M03','Rural water repairs','Priority',28,95,94,27,79,2600],['M04','Neighborhood digital hubs','Priority',25,82,72,20,83,2100],['M05','City data portal','General',20,76,68,18,96,900],['M06','School facility upgrades','General',34,91,82,25,86,3900],['M07','Accessible bus stops','Priority',22,83,80,18,92,1700],['M08','Park maintenance','General',19,65,55,19,92,1500],['M09','Emergency response kits','Priority',30,87,95,22,83,3000],['M10','Street lighting retrofit','General',26,79,77,14,94,2600]]},
32
+ {key:'university',name:'University Research',subtitle:'Academic programs & capacity',budget:120,quota:30,programs:[
33
+ ['U01','Student writing lab','Priority',25,91,87,19,91,1800],['U02','Open research data','General',20,83,70,22,88,1250],['U03','Rural student grants','Priority',31,97,95,15,89,1650],['U04','AI teaching training','General',24,81,73,32,91,2150],['U05','Digital library access','General',34,90,82,17,94,2850],['U06','Academic integrity centre','General',23,83,86,21,85,1700],['U07','Community field methods','Priority',21,89,90,31,81,950],['U08','Research equipment','General',30,86,74,42,71,1400],['U09','Inclusive tutoring','Priority',19,88,91,16,94,1600],['U10','International seminars','General',18,67,43,29,79,620]]},
34
+ {key:'climate',name:'Climate Resilience',subtitle:'Illustrative local adaptation initiatives',budget:185,quota:30,programs:[
35
+ ['C01','Flood early warnings','Priority',45,94,94,33,83,4200],['C02','Urban tree corridors','General',26,80,61,20,91,2100],['C03','Watershed restoration','Priority',39,93,91,29,78,3600],['C04','Cool roofing pilot','General',29,79,77,18,92,2400],['C05','Drought assistance','Priority',40,96,94,35,79,3200],['C06','Resilient community centres','Priority',37,84,82,27,85,2600],['C07','Climate data dashboard','General',22,75,68,17,94,1200],['C08','Storm drain upgrades','General',49,92,87,33,82,4400],['C09','Heat awareness','Priority',18,78,74,15,98,1500],['C10','Community solar backup','General',35,84,82,28,84,2800]]}
36
+ ];
37
+ const demos=raw.map(s=>({...s,programs:s.programs.map(a=>({id:a[0],name:a[1],area:a[2],cost:a[3],impact:a[4],urgency:a[5],risk:a[6],readiness:a[7],people:a[8]}))}));
38
+ const defaultWeights={impact:40,efficiency:25,readiness:20,risk:15};
39
+ function validate(ps){if(!Array.isArray(ps)||ps.length<2||ps.length>14)throw Error('Provide between 2 and 14 projects');const ids=new Set();for(let i=0;i<ps.length;i++){const p=ps[i];if(!p.id||String(p.id).length>48||ids.has(p.id))throw Error('Project IDs must be unique and at most 48 characters');ids.add(p.id);if(!p.name||String(p.name).length>120)throw Error('Project names must have 1 to 120 characters');if(!allowed.includes(p.area))throw Error('Area must be Priority or General');for(const k of ['cost','impact','urgency','risk','readiness','people']){if(!Number.isFinite(p[k]))throw Error('Invalid number for '+k+' on row '+(i+2));}if(p.cost<=0||p.cost>10000||p.people<0||p.people>10000000||['impact','urgency','risk','readiness'].some(k=>p[k]<0||p[k]>100))throw Error('Out-of-range values on row '+(i+2));}return true;}
40
+ function normalized(w){const keys=['impact','efficiency','readiness','risk'];const vals=keys.map(k=>Number.isFinite(w[k])?Math.max(0,w[k]):0);let den=vals.reduce((a,b)=>a+b,0);if(!den)den=1;return Object.fromEntries(keys.map((k,i)=>[k,vals[i]/den]));}
41
+ function scored(ps,w){const n=normalized(w),eff=ps.map(p=>p.impact/p.cost),max=Math.max(...eff);return ps.map((p,i)=>({...p,score:round(100*(n.impact*p.impact/100+n.efficiency*(max?eff[i]/max:0)+n.readiness*p.readiness/100+n.risk*(100-p.risk)/100),6)}));}
42
+ function optimize(ps,budget,quota,w){validate(ps);if(!Number.isFinite(budget)||budget<0||budget>10000||!Number.isFinite(quota)||quota<0||quota>100)return {feasible:false,reason:'Invalid budget or equity target'};
43
+ const arr=scored(ps,w), N=arr.length, n=1<<N;let best=null;for(let mask=1;mask<n;mask++){let cost=0,priority=0,value=0,people=0,count=0;for(let i=0;i<N;i++)if(mask&(1<<i)){const p=arr[i];cost+=p.cost;if(cost>budget+1e-8)break;if(p.area==='Priority')priority+=p.cost;value+=p.score;people+=p.people;count++;}if(cost>budget+1e-8||!count||priority+1e-9<quota/100*cost)continue;const sol={feasible:true,mask,cost:round(cost,6),priorityCost:round(priority,6),value:round(value,6),people,selected:count,equityShare:round(100*priority/cost,4)};if(!best||value>best.value+1e-6||(Math.abs(value-best.value)<=1e-6&&(people>best.people||(people===best.people&&cost<best.cost))))best=sol;}
44
+ if(!best)return {feasible:false,reason:'No project portfolio meets the budget and priority-area share. Adjust the budget or rules.'};return {...best,ids:arr.filter((p,i)=>best.mask&(1<<i)).map(p=>p.id),items:arr.filter((p,i)=>best.mask&(1<<i)),scores:arr};}
45
+ function greedy(ps,budget,quota,w){const arr=scored(ps,w).sort((a,b)=>b.score/b.cost-a.score/a.cost||a.id.localeCompare(b.id));let cost=0,priority=0,value=0,people=0,ids=[];for(let p of arr){if(cost+p.cost<=budget+1e-8){cost+=p.cost;if(p.area==='Priority')priority+=p.cost;value+=p.score;people+=p.people;ids.push(p.id)}}return {cost,priorityCost:priority, value:round(value,6),people,ids,feasible:cost>0&&priority+1e-9>=quota/100*cost, equityShare:cost?round(100*priority/cost,4):0};}
46
+ function seeded(seed){let s=seed>>>0;return()=>{s=(Math.imul(1664525,s)+1013904223)>>>0;return s/4294967296;};}
47
+ function sensitivity(ps,budget,quota,w,trials=120,seed=72026){validate(ps);const rand=seeded(seed),base=normalized(w);const tally=Object.fromEntries(ps.map(p=>[p.id,0]));let valid=0,totalValue=0;for(let t=0;t<trials;t++){const rw={};for(const key of Object.keys(base))rw[key]=Math.max(.01,base[key]*(.58+.84*rand()));let sol=optimize(ps,budget,quota,rw);if(sol.feasible){valid++;totalValue+=sol.value;for(const id of sol.ids)tally[id]++;}}
48
+ return {trials,valid,avgValue:valid?round(totalValue/valid,2):null,rows:ps.map(p=>({...p,count:tally[p.id],pct:round(100*tally[p.id]/trials,1)})).sort((a,b)=>b.pct-a.pct||a.id.localeCompare(b.id))};}
49
+ function tokenizeCsv(t){t=String(t).replace(/^\ufeff/,'');if(t.length>2000000)throw Error('CSV must be smaller than 2 MB');let rows=[],cell='',row=[],q=false,atEnd=false;for(let i=0;i<t.length;i++){const c=t[i];if(q){if(c==='"'&&t[i+1]==='"'){cell+='"';i++;}else if(c==='"'){q=false;atEnd=true;}else cell+=c;}else if(atEnd){if(c===','){row.push(cell);cell='';atEnd=false;}else if(c==='\n'||c==='\r'){row.push(cell);rows.push(row);row=[];cell='';atEnd=false;if(c==='\r'&&t[i+1]==='\n')i++;}else throw Error('Unexpected characters after a closing quote');}else if(c==='"'){if(cell!=='')throw Error('Unexpected quote in CSV');q=true;}else if(c===','){row.push(cell);cell='';}else if(c==='\n'||c==='\r'){row.push(cell);rows.push(row);row=[];cell='';if(c==='\r'&&t[i+1]==='\n')i++;}else cell+=c;}if(q)throw Error('Unclosed quote in CSV');if(cell!==''||row.length||atEnd){row.push(cell);rows.push(row);}return rows.filter(r=>r.some(x=>x.trim()!==''));}
50
+ const required=['id','name','area','cost','impact','urgency','risk','readiness','people'];
51
+ function importProjects(text){const rows=tokenizeCsv(text);if(rows.length<3)throw Error('CSV requires a header and at least two projects');const hdr=rows[0].map(x=>x.trim().toLowerCase());if(hdr.length!==required.length||required.some((k,i)=>hdr[i]!==k))throw Error('CSV headers must be exactly: '+required.join(','));const ps=rows.slice(1).map((r,i)=>{if(r.length!==required.length)throw Error('Wrong column count at row '+(i+2));const ob={};required.forEach((k,j)=>{if(['id','name','area'].includes(k))ob[k]=r[j].trim();else {if(!r[j].trim()||!/^(?:\d+\.?\d*|\.\d+)$/.test(r[j].trim()))throw Error('Invalid nonnegative number at row '+(i+2));ob[k]=Number(r[j]);}});return ob;});validate(ps);return ps;}
52
+ const safe=(v)=>{const s=String(v??'');return /^[=+\-@\t\r]/.test(s)?"'"+s:s};
53
+ const csv=(rows)=>rows.map(row=>row.map(v=>{const s=safe(v);return '"'+s.replace(/"/g,'""')+'"';}).join(',')).join('\r\n')+'\r\n';
54
+ function exportProjects(ps){return csv([required,...ps.map(p=>required.map(k=>p[k]))]);}
55
+ return {demos,defaultWeights,clamp,round,validate,normalized,scored,optimize,greedy,sensitivity,seeded,tokenizeCsv,importProjects,exportProjects,csv,required};
56
+ })();
57
+ </script>
58
+ <script id="ui">
59
+ (()=>{
60
+ const pages=[['overview','◫','Command Center','Budget Intelligence','Optimize a portfolio of projects under a fixed budget, equity commitments and transparent scoring rules.'],['portfolio','▦','Program Portfolio','Portfolio Explorer','Inspect and edit program-level costs, impact scores and project readiness.'],['allocation','◇','Allocation Studio','Allocation Studio','Tune your priorities, compare the exact optimum against a simple greedy baseline and explain every selection.'],['equity','◉','Equity & Coverage','Equity & Coverage','Explore geographic allocation, priority-area targets and project-level reach.'],['sensitivity','〽','Sensitivity Lab','Decision Robustness','Recompute optimized portfolios under 120 reproducible perturbations of the decision weights.'],['data','▤','Data & Audit','Data & Audit','Import validated project data and export a complete, reproducible decision audit.']];
61
+ const $=id=>document.getElementById(id);
62
+ const esc=(v)=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
63
+ const fmt=(n,d=0)=>Number(n).toLocaleString('en-US',{maximumFractionDigits:d,minimumFractionDigits:d});
64
+ const usd=n=>'$'+fmt(n,0)+'k';
65
+ const initial=i=>{let d=Engine.demos[i];return {key:d.key,title:d.name,programs:d.programs.map(p=>({...p})),budget:d.budget,quota:d.quota,weights:{...Engine.defaultWeights},source:'Synthetic demonstration',edited:false};};
66
+ let state=initial(0),active='overview',cached=null,review='';
67
+ const score=()=>{cached=Engine.optimize(state.programs,state.budget,state.quota,state.weights);return cached};
68
+ const alerts=s=>{const notes=[];if(!s.feasible)notes.push(s.reason);else if(s.budget-s.cost>40)notes.push('Unallocated funds remain because the model selects only whole projects.');return notes;};
69
+ function notify(text,error=false){const el=$('toast');el.textContent=text;el.style.background=error?'#422636':'#193a34';el.classList.remove('hide');clearTimeout(notify.timer);notify.timer=setTimeout(()=>el.classList.add('hide'),3300)}
70
+ function renderNav(){for(let target of ['nav','mobileNav'])$(target).innerHTML=pages.map(([key,icon,title])=>`<button type="button" data-page="${key}" class="${active===key?'active':''}" aria-current="${active===key?'page':'false'}"><span class="nicon">${icon}</span>${title}</button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>goto(b.dataset.page));}
71
+ function goto(key){active=key;document.querySelectorAll('.page').forEach(p=>p.classList.toggle('active',p.id==='view-'+key));const d=pages.find(p=>p[0]===key);$('pageTitle').textContent=d[3];$('pageDesc').textContent=d[4];renderNav();render();}
72
+ function resetConfirm(){if(state.edited&&!confirm('Reset this dataset and discard unsaved changes? Download a snapshot first if needed.'))return;const i=Math.max(0,Engine.demos.findIndex(s=>s.key===state.key));state=initial(i);render();notify('Scenario restored to original synthetic data.');}
73
+ function chartBar(label,value,max,selected=true){return `<div class="barline"><span title="${esc(label)}">${esc(label)}</span><div class="bar-track"><div class="${selected?'':'dim'}" style="width:${Math.max(0,Math.min(100,value/max*100))}%"></div></div><b style="text-align:right">${fmt(value,1)}</b></div>`}
74
+ function kpis(s){return `<section class="grid kpis"><div class="card kpi"><div class="label">Budget committed</div><div class="num">${s.feasible?usd(s.cost):'N/A'}</div><div class="detail">of ${usd(state.budget)} authorized</div></div><div class="card kpi"><div class="label">Projects selected</div><div class="num">${s.feasible?s.selected:'0'} <span style="font-size:16px;color:#8da2bd">/ ${state.programs.length}</span></div><div class="detail">Binary project approvals</div></div><div class="card kpi"><div class="label">Priority-area allocation</div><div class="num" style="color:${s.feasible&&s.equityShare>=state.quota?'#75edc4':'#ffc47e'}">${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</div><div class="detail">minimum required ${fmt(state.quota)}%</div></div><div class="card kpi"><div class="label">People reached (planned)</div><div class="num">${s.feasible?fmt(s.people):'N/A'}</div><div class="detail">Sum of synthetic beneficiaries</div></div></section>`;}
75
+ function overview(s){let all=Engine.scored(state.programs,state.weights),shown=s.feasible?s.items:[],top=shown.slice().sort((a,b)=>b.score-a.score),greedy=Engine.greedy(state.programs,state.budget,state.quota,state.weights);$('view-overview').innerHTML=kpis(s)+`<div class="grid split"><section class="card cardpad"><div class="cardheader"><div><div class="eyebrow">ALLOCATION PORTFOLIO</div><h2>Selected programs</h2><div class="small-note">Exact search over all feasible binary portfolios</div></div><span class="pill ${s.feasible?'good':'bad'}">${s.feasible?'✓ Feasible':'No feasible selection'}</span></div>${s.feasible?`<div class="bar-chart">${top.map(p=>chartBar(p.name,p.score,100,true)).join('')}</div>`:`<div class="empty">${esc(s.reason)}</div>`}<div class="legend"><span><span class="dot"></span>Decision-weighted program value</span></div></section><section class="card cardpad"><div class="eyebrow">BUDGET STRUCTURE</div><h2>Allocation breakdown</h2><div class="small-note">All amounts in thousands of illustrative USD</div><div class="stat-row"><b>Selected programs</b><span>${s.feasible?usd(s.cost):'N/A'}</span></div><div class="progress"><div style="width:${s.feasible&&state.budget?Math.min(100,s.cost/state.budget*100):0}%"></div></div><div class="stat-row"><b>Priority-area projects</b><span>${s.feasible?usd(s.priorityCost):'N/A'}</span></div><div class="progress"><div style="width:${s.feasible?Math.min(100,s.equityShare):0}%"></div></div><div class="stat-row"><b>Uncommitted budget</b><span>${s.feasible?usd(Math.max(0,state.budget-s.cost)):usd(state.budget)}</span></div><div class="divider"></div><div class="mini-metrics"><div class="metric-mini"><strong>${s.feasible?fmt(s.value,1):'N/A'}</strong><span>Decision score (sum)</span></div><div class="metric-mini"><strong>${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</strong><span>Priority-area share</span></div><div class="metric-mini"><strong>${fmt(state.programs.length)}</strong><span>Candidate projects</span></div></div></section></div><div class="grid split" style="margin-top:16px"><section class="card cardpad"><div class="cardheader"><div><div class="eyebrow">METHOD COMPARISON</div><h2>Exact allocation vs. greedy baseline</h2></div><span class="pill">Deterministic</span></div><div class="two"><div class="info-tile"><span class="muted small-note">EXACT SOLUTION</span><h2 style="font-size:26px">${s.feasible?fmt(s.value,1):'N/A'}</h2><div class="small-note">${s.feasible?s.selected+' projects, '+usd(s.cost):'No solution'}</div></div><div class="info-tile"><span class="muted small-note">GREEDY REFERENCE</span><h2 style="font-size:26px">${fmt(greedy.value,1)}</h2><div class="small-note">${greedy.ids.length} projects · ${greedy.feasible?'Equity feasible':'Equity rule not met'}</div></div></div><div class="flow-note">The greedy baseline sorts projects by value per cost. It is not guaranteed to satisfy the equity rule or maximize value.</div></section><section class="card cardpad"><div class="eyebrow">INTERPRETATION</div><h2>What the optimizer actually does</h2><p class="muted" style="font-size:12px">It evaluates every possible subset of ${state.programs.length} projects, rejects portfolios over budget or below the priority-area spending target, and maximizes the sum of weighted project scores.</p><div class="notice">All example budgets, scores, benefits and reach estimates are synthetic. This is a transparent demonstration, not an official fiscal assessment.</div>${alerts(s).map(x=>`<div class="notice warn">${esc(x)}</div>`).join('')}</section></div>`;}
76
+ function portfolio(s){const all=Engine.scored(state.programs,state.weights),sel=new Set(s.ids||[]);$('view-portfolio').innerHTML=`<div class="grid split"><div class="card cardpad"><div class="cardheader"><div><div class="eyebrow">INPUT DATA</div><h2>Project register</h2><div class="small-note">Edit costs, impact or readiness. Recalculate instantly.</div></div><div style="min-width:190px"><input type="search" id="programSearch" placeholder="Find a program..." aria-label="Search programs"></div></div><div class="table-wrap"><table><thead><tr><th>Project</th><th>Area</th><th>Cost $k</th><th>Impact</th><th>Readiness</th><th>Score</th><th>Chosen</th></tr></thead><tbody id="portfolioRows"></tbody></table></div><p class="flow-note">Editable values affect the algorithm immediately. Export your work before reloading the page.</p></div><div class="card cardpad"><div class="eyebrow">MODEL INPUTS</div><h2>How projects are rated</h2><p class="muted">Scores are normalized across the current portfolio and use four dimensions: estimated impact, impact-to-cost efficiency, readiness and inverse risk.</p><div class="divider"></div>${all.sort((a,b)=>b.score-a.score).slice(0,6).map(p=>chartBar(p.name,p.score,100,sel.has(p.id))).join('')}<div class="notice">Urgency and beneficiaries are shown for context. Urgency does not enter the objective; beneficiary count is used only as a tie-breaker.</div></div></div>`;
77
+ const rows=()=>{const key=($('programSearch')?.value||'').toLowerCase(),sel=new Set(score().ids||[]);$('portfolioRows').innerHTML=Engine.scored(state.programs,state.weights).filter(p=>(p.name+' '+p.id+' '+p.area).toLowerCase().includes(key)).map(p=>`<tr><td><b>${esc(p.name)}</b><br><span class="small-note mono">${esc(p.id)}</span></td><td>${esc(p.area)}</td><td><input aria-label="Cost for ${esc(p.name)}" data-edit="cost" data-id="${esc(p.id)}" type="number" class="number-edit" min=".1" max="10000" step="1" value="${p.cost}"></td><td><input aria-label="Impact for ${esc(p.name)}" data-edit="impact" data-id="${esc(p.id)}" type="number" class="number-edit" min="0" max="100" value="${p.impact}"></td><td><input aria-label="Readiness for ${esc(p.name)}" data-edit="readiness" data-id="${esc(p.id)}" type="number" class="number-edit" min="0" max="100" value="${p.readiness}"></td><td>${fmt(p.score,1)}</td><td><span class="status ${sel.has(p.id)?'':'out'}">${sel.has(p.id)?'SELECTED':'NOT SELECTED'}</span></td></tr>`).join('');document.querySelectorAll('[data-edit]').forEach(el=>el.onchange=()=>{const p=state.programs.find(p=>p.id===el.dataset.id);const v=Number(el.value),k=el.dataset.edit;if(!el.value.trim()||!Number.isFinite(v)||(k==='cost'&&(v<=0||v>10000))||(k!=='cost'&&(v<0||v>100))){notify('Invalid value. Cost must be positive; scores 0–100.',true);el.value=p[k];return;}p[k]=v;state.edited=true;render();notify('Project edited; allocation recalculated.')})};rows();$('programSearch').oninput=rows;}
78
+ function allocation(s){const n=Engine.normalized(state.weights),g=Engine.greedy(state.programs,state.budget,state.quota,state.weights),sorted=Engine.scored(state.programs,state.weights).sort((a,b)=>b.score-a.score);$('view-allocation').innerHTML=kpis(s)+`<div class="grid split"><section class="card cardpad"><div class="eyebrow">DECISION PRIORITIES</div><h2>Weighting studio</h2><p class="small-note">Change any weight. Values are normalized to 100% automatically.</p>${[['impact','Impact quality'],['efficiency','Impact-per-dollar efficiency'],['readiness','Implementation readiness'],['risk','Low-risk preference']].map(([k,t])=>`<div class="slider-row"><div class="slider-top"><b>${t}</b><span id="${k}WeightText">${fmt(n[k]*100,0)}% normalized</span></div><input type="range" aria-label="${t}" min="0" max="100" step="1" value="${state.weights[k]}" data-weight="${k}"></div>`).join('')}<div class="notice">Weights affect project scores, not eligibility. The equity target is a separate hard spending constraint.</div></section><section class="card cardpad"><div class="eyebrow">OPTIMAL PORTFOLIO</div><h2>Recommended commitments</h2><div class="table-wrap"><table><thead><tr><th>Program</th><th>Amount</th><th>Score</th></tr></thead><tbody>${s.feasible?s.items.sort((a,b)=>b.score-a.score).map(p=>`<tr><td>${esc(p.name)}</td><td>${usd(p.cost)}</td><td>${fmt(p.score,1)}</td></tr>`).join(''):`<tr><td colspan="3">${esc(s.reason)}</td></tr>`}</tbody></table></div><div class="divider"></div><div class="two"><div class="metric-mini"><span>Exact objective</span><strong>${s.feasible?fmt(s.value,1):'N/A'}</strong></div><div class="metric-mini"><span>Greedy objective</span><strong>${fmt(g.value,1)}</strong></div></div><p class="small-note" style="margin-top:12px">${g.feasible?'Both approaches meet the constraints.':'Greedy is not equity-feasible, so its score is not a directly comparable feasible solution.'}</p></section></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">EXPLAINABLE RANKING</div><h2>All candidate scores</h2><div class="bar-chart">${sorted.map(p=>chartBar(p.name,p.score,100,(s.ids||[]).includes(p.id))).join('')}</div></div>`;document.querySelectorAll('[data-weight]').forEach(el=>el.oninput=()=>{state.weights[el.dataset.weight]=Number(el.value);state.edited=true;render();});}
79
+ function equity(s){const sel=s.feasible?s.items:[],pri=sel.filter(p=>p.area==='Priority'),gen=sel.filter(p=>p.area==='General'),pall=state.programs.filter(p=>p.area==='Priority'),gall=state.programs.filter(p=>p.area==='General');$('view-equity').innerHTML=kpis(s)+`<div class="grid split"><div class="card cardpad"><div class="eyebrow">EQUITY RULE</div><h2>Priority-area investment</h2><p class="muted">A minimum of ${fmt(state.quota)}% of committed funds must flow to programs tagged as a priority-area investment.</p><div class="stat-row"><b>Current priority-area share</b><span>${s.feasible?fmt(s.equityShare,1)+'%':'N/A'}</span></div><div class="progress"><div style="width:${s.feasible?s.equityShare:0}%"></div></div><div class="stat-row"><b>Required threshold</b><span>${fmt(state.quota)}%</span></div><div class="progress"><div style="width:${state.quota}%;background:#ffa95c"></div></div><div class="divider"></div><div class="two"><div class="metric-mini"><strong>${usd(pri.reduce((a,p)=>a+p.cost,0))}</strong><span>Priority spending</span></div><div class="metric-mini"><strong>${usd(gen.reduce((a,p)=>a+p.cost,0))}</strong><span>General spending</span></div></div><div class="notice">Equity here measures spending share, not actual fairness, geographic effectiveness or distribution of outcomes. These require external evidence.</div></div><div class="card cardpad"><div class="eyebrow">COVERAGE PROFILE</div><h2>Selected reach & projects</h2>${[['Priority area',pri,pall],['General area',gen,gall]].map(([label,items,all])=>`<div class="stat-row"><b>${label}</b><span>${items.length}/${all.length} projects</span></div><div class="progress"><div style="width:${all.length?items.length/all.length*100:0}%"></div></div><div class="stat-row"><b>Indicative people reached</b><span>${fmt(items.reduce((a,p)=>a+p.people,0))}</span></div>`).join('<div class="divider"></div>')}</div></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">EQUITY TRADE-OFFS</div><h2>What changes without the spending requirement?</h2>${(()=>{let u=Engine.optimize(state.programs,state.budget,0,state.weights);return `<div class="two"><div class="info-tile"><span class="small-note">WITH THE EQUITY RULE</span><h2>${s.feasible?fmt(s.value,1):'N/A'}</h2><div class="small-note">${s.feasible?fmt(s.equityShare,1)+'% priority share':'No feasible set'}</div></div><div class="info-tile"><span class="small-note">WITHOUT THE RULE</span><h2>${u.feasible?fmt(u.value,1):'N/A'}</h2><div class="small-note">${u.feasible?fmt(u.equityShare,1)+'% priority share':'No feasible set'}</div></div></div>`})()}<p class="help">This comparison shows the modeled cost of a hard equity constraint in decision-score units. Scores are not monetary benefits.</p></div>`;}
80
+ function sensitivityView(s){let t=Engine.sensitivity(state.programs,state.budget,state.quota,state.weights,120,72026);$('view-sensitivity').innerHTML=`<div class="grid kpis"><div class="card kpi"><div class="label">Weight perturbations</div><div class="num">120</div><div class="detail">Fixed seed 72026</div></div><div class="card kpi"><div class="label">Feasible runs</div><div class="num">${t.valid}</div><div class="detail">Same budget and quota</div></div><div class="card kpi"><div class="label">Mean score</div><div class="num">${t.avgValue===null?'N/A':fmt(t.avgValue,1)}</div><div class="detail">Scores across reweighted models</div></div><div class="card kpi"><div class="label">Programs tested</div><div class="num">${state.programs.length}</div><div class="detail">Exact subset search per run</div></div></div><div class="grid split"><section class="card cardpad"><div class="eyebrow">SELECTION FREQUENCY</div><h2>Which programs survive preference changes?</h2><div class="bar-chart">${t.rows.map(p=>chartBar(p.name,p.pct,100,true)).join('')}</div></section><section class="card cardpad"><div class="eyebrow">UNCERTAINTY MODEL</div><h2>Reproducible preference stress test</h2><p class="muted">Each run multiplies the four decision weights by independently generated factors between 0.58 and 1.42. Weights are normalized and the exact allocation is solved again.</p><div class="notice">This is a sensitivity test to assumptions about preferences, not a confidence interval or forecast of real-world impact.</div><h3>Interpretation</h3><p class="help">A high selection frequency indicates stability under the specified perturbation distribution, not proof of value, fairness or policy effectiveness.</p><button class="btn alt small" id="exportSensitivity">↓ Download sensitivity CSV</button></section></div>`;$('exportSensitivity').onclick=()=>download('sensitivity');}
81
+ function data(s){$('view-data').innerHTML=`<div class="grid split"><div class="card cardpad"><div class="eyebrow">IMPORT DATA</div><h2>Upload your project portfolio</h2><p class="muted">Use the downloadable CSV template and keep the exact column names. Files are processed only in the browser.</p><div class="notice">Allowed: 2–14 projects; IDs must be unique; area must be <b>Priority</b> or <b>General</b>. Costs and people must be nonnegative. Scores must be between 0 and 100.</div><input id="importFile" type="file" accept=".csv,text/csv" aria-label="Import CSV file"><div class="download-actions" style="margin-top:16px"><button class="btn small" id="doImport">↑ Import CSV</button><button class="btn alt small" id="downloadTemplate">↓ CSV template</button></div><div class="divider"></div><span class="small-note">Current dataset: <b>${esc(state.title)}</b> · ${state.programs.length} projects · ${esc(state.source)}</span><p class="help">Unsaved changes are lost on reload. Export a snapshot to preserve your work.</p></div><div class="card cardpad"><div class="eyebrow">EXPORT AUDIT</div><h2>Download reproducible outputs</h2><div class="download-actions">${[['report','Research report (Markdown)'],['snapshot','Full project snapshot (JSON)'],['selected','Selected projects (CSV)'],['portfolio','Full portfolio (CSV)'],['audit','Decision audit (CSV)'],['sensitivity','Sensitivity results (CSV)'],['template','CSV import template']].map(([k,v])=>`<button class="btn alt small" data-export="${k}">↓ ${v}</button>`).join('')}</div><div class="notice">CSV text beginning with spreadsheet formula characters is escaped to reduce accidental formula execution.</div></div></div><div class="card cardpad" style="margin-top:15px"><div class="eyebrow">TRANSPARENT MODEL SPECIFICATION</div><h2>Methodology & limitations</h2><div class="two"><div><h3>Exact binary portfolio optimization</h3><p class="muted">Enumerates up to 2¹⁴ subsets. Each program is either fully funded or not funded. A portfolio is feasible when its total cost is at most the budget and its priority-area expenditure is at least the required share of committed funds.</p><h3>Objective</h3><p class="muted">Maximizes the sum of normalized program scores. Criteria include impact, impact-per-dollar efficiency (relative to this dataset), readiness and low-risk preference. Estimated beneficiaries break ties.</p></div><div><h3>Scope limits</h3><p class="muted">The examples are synthetic. The model does not account for partial funding, program dependencies, legal procurement constraints, displacement, double-counting of beneficiaries or distributional outcomes.</p><h3>Decision protocol</h3><p class="muted">For any real allocation, validate cost estimates, eligibility, equity definitions, conflicts of interest, public consultation and implementation feasibility before using the result.</p></div></div></div>`;document.querySelectorAll('[data-export]').forEach(b=>b.onclick=()=>download(b.dataset.export));$('downloadTemplate').onclick=()=>download('template');$('doImport').onclick=async()=>{const f=$('importFile').files[0];if(!f){notify('Choose a CSV file before importing.',true);return;}if(f.size>2000000){notify('CSV file exceeds 2 MB.',true);return;}try{const contents=await f.text(),ps=Engine.importProjects(contents);const st={...state,key:'custom',programs:ps,title:'Imported portfolio',source:'User CSV',edited:true};Engine.optimize(st.programs,st.budget,st.quota,st.weights);if(state.edited&&!confirm('Replace the current portfolio with this valid import?'))return;state=st;render();notify(ps.length+' projects imported successfully.');}catch(e){notify('Import rejected: '+e.message,true);}};}
82
+ function makeDownload(name,content,type){const blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);}
83
+ function report(s){return `# BudgetForge Decision Audit\n\nGenerated from local browser state. All supplied demonstrations are synthetic.\n\nDataset: ${state.title}\nSource: ${state.source}\nBudget (thousands USD): ${state.budget}\nPriority spending share required: ${state.quota}%\nWeights: ${JSON.stringify(Engine.normalized(state.weights))}\nFeasible: ${Boolean(s.feasible)}\n${s.feasible?`Committed: ${s.cost}\nPriority spending: ${s.priorityCost}\nActual share: ${s.equityShare}%\nWeighted decision objective (dimensionless): ${s.value}\nBeneficiaries (synthetic; not deduplicated): ${s.people}\nSelected: ${s.ids.join(', ')}`:'Reason: '+s.reason}\n\n## Selected programs\n\n${s.feasible?s.items.map(p=>'- '+p.id+' | '+p.name+' | cost $'+p.cost+'k | score '+Engine.round(p.score,2)).join('\n'):'None'}\n\n## Method\n\nExact exhaustive search over binary program subsets subject to a fixed budget and minimum priority-area expenditure share. Scores are based on normalized criteria: impact, impact/cost efficiency, readiness and inverse risk. The weights are user-configurable. People are used only as a tie-breaker.\n\n## Limitations\n\nNo evidence of causal effects, actual beneficiary counts, distributive justice, true market prices or statutory compliance is provided. Real-world decisions require expert review.\n`;}
84
+ function download(kind){let s=score(),date='BudgetForge_72_'+kind,body='',type='text/plain;charset=utf-8',ext='txt';const header=['id','name','area','cost_kusd','impact','urgency','risk','readiness','people','score','selected'];switch(kind){case'report':body=report(s);ext='md';type='text/markdown;charset=utf-8';break;case'snapshot':body=JSON.stringify({project:'BudgetForge',version:'1.0',metadata:{title:state.title,source:state.source,synthetic:state.source!=='User CSV'},budget:state.budget,quota:state.quota,weights:state.weights,normalizedWeights:Engine.normalized(state.weights),programs:state.programs,solution:{...s,items:undefined,scores:undefined}},null,2);ext='json';type='application/json;charset=utf-8';break;case'portfolio':body=Engine.exportProjects(state.programs);ext='csv';type='text/csv;charset=utf-8';break;case'template':body=Engine.exportProjects(Engine.demos[0].programs.slice(0,3));ext='csv';type='text/csv;charset=utf-8';break;case'selected':case'audit':{let arr=Engine.scored(state.programs,state.weights),sel=new Set(s.ids||[]);if(kind==='selected')arr=arr.filter(p=>sel.has(p.id));body=Engine.csv([header,...arr.map(p=>[p.id,p.name,p.area,p.cost,p.impact,p.urgency,p.risk,p.readiness,p.people,Engine.round(p.score,3),sel.has(p.id)?'yes':'no'])]);ext='csv';type='text/csv;charset=utf-8';break;}case'sensitivity':{const t=Engine.sensitivity(state.programs,state.budget,state.quota,state.weights,120,72026);body=Engine.csv([['id','name','selected_runs','total_runs','selection_percent'],...t.rows.map(p=>[p.id,p.name,p.count,t.trials,p.pct])]);ext='csv';type='text/csv;charset=utf-8';break;}default:return;}makeDownload(date+'.'+ext,body,type);notify(kind+' exported.');}
85
+ function render(){const s=score();$('scenarioSelect').innerHTML=Engine.demos.map(p=>`<option value="${p.key}">${esc(p.name)} · synthetic</option>`).join('')+(Engine.demos.some(x=>x.key===state.key)?'':`<option value="custom">Imported portfolio</option>`);$('scenarioSelect').value=state.key;$('budgetInput').value=state.budget;$('quotaInput').value=state.quota;
86
+ if(active==='overview')overview(s);if(active==='portfolio')portfolio(s);if(active==='allocation')allocation(s);if(active==='equity')equity(s);if(active==='sensitivity')sensitivityView(s);if(active==='data')data(s);
87
+ }
88
+ $('scenarioSelect').onchange=e=>{if(state.edited&&!confirm('Switching datasets discards unsaved changes. Continue?')){render();return;}const i=Engine.demos.findIndex(x=>x.key===e.target.value);if(i<0){render();return;}state=initial(i);render();notify('Loaded '+state.title+'.');};
89
+ const numberChange=(id,k,lo,hi)=>$(id).onchange=e=>{const raw=e.target.value,v=Number(raw);if(!raw.trim()||!Number.isFinite(v)||v<lo||v>hi){notify('Enter a number from '+lo+' to '+hi+'.',true);render();return;}state[k]=v;state.edited=true;render();};numberChange('budgetInput','budget',0,10000);numberChange('quotaInput','quota',0,100);
90
+ $('resetBtn').onclick=resetConfirm;$('downloadReport').onclick=()=>download('report');renderNav();render();
91
+ })();
92
+ </script>
93
+ </body></html>