diff --git a/ventures/up-to-date/README.md b/ventures/up-to-date/README.md
new file mode 100644
index 0000000..360c1b2
--- /dev/null
+++ b/ventures/up-to-date/README.md
@@ -0,0 +1,7 @@
+# UpToDate — issue #85
+
+Monitor-first taste test inspired by Chai. Buildless HTML/JavaScript; no dependencies or secrets. Serve `site/` over HTTP and paste a public GitHub repository, or press Try Express. The page scans the default branch at an immutable commit, inventories up to 24 files, checks up to 12 unique dependencies against npm and OSV, and links repository releases. Download JSON evidence to retain a scan.
+
+Manual refresh or timed refresh while the page remains open. No autonomous edits, unattended monitoring, private repository support, authentication or alerts. Public unlisted URL: https://notplanned.teahub.xyz/v/up-to-date/. GitHub/API limits, CORS or network failures are explicit unknowns. Docker/Actions and unsupported lock formats are inventory-only; exact npm package-lock / Python requirement pins have OSV coverage. No advisory matches is not proof of safety. npm metadata dates are release times; deprecation notices are observed at scan time. Nested duplicate dependency versions remain in inventory, but only one preferred exact version per package is signal-checked. File and dependency caps mean results are partial, not exhaustive.
+
+Canonical source: matcha/notplanned ventures/up-to-date/. Deploy using existing venture-day static publication infrastructure; do not replace shared services. Signup uses the existing NotPlanned endpoint. Free monitor; hosted/team offerings are ideas only.
diff --git a/ventures/up-to-date/site/app.js b/ventures/up-to-date/site/app.js
new file mode 100644
index 0000000..610e11d
--- /dev/null
+++ b/ventures/up-to-date/site/app.js
@@ -0,0 +1,33 @@
+'use strict';
+const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+const link=(url,label)=>`${esc(label)}`;
+let report=null, previous=null, timer=null;
+async function json(url,options={}){const response=await fetch(url,{...options,signal:AbortSignal.timeout(15000)});if(!response.ok)throw new Error(`HTTP ${response.status}${response.status===403||response.status===429?' (API limit or denied access)':''}`);return response.json();}
+function parseRepo(value){let s=value.trim().replace(/\/$/,'');if(s.startsWith('https://github.com/'))s=s.slice(19);if(!/^[\w.-]+\/[\w.-]+$/.test(s))throw new Error('Enter owner/repository or an https://github.com/owner/repository URL. Public GitHub only in v0.');return s.replace(/\.git$/,'');}
+function parseFile(path,text,url,items,gaps){const add=(name,version,ecosystem,exact=false,coverage='Declared; installed version unknown')=>items.push({name,version,ecosystem,exact,path,url,coverage});
+try{
+if(path.endsWith('package.json')){const p=JSON.parse(text);for(const kind of ['dependencies','devDependencies','peerDependencies','optionalDependencies'])for(const [n,v]of Object.entries(p[kind]||{}))add(n,v,'npm',false,kind);for(const[n,v]of Object.entries(p.engines||{}))add(n,v,'runtime');}
+else if(path.endsWith('package-lock.json')){const p=JSON.parse(text);if(p.packages){for(const[n,v]of Object.entries(p.packages)){if(n.startsWith('node_modules/')&&!n.slice(13).includes('/node_modules/')&&v.version)add(n.slice(13),v.version,'npm',true,'Resolved lockfile version; runtime not verified');}}else for(const[n,v]of Object.entries(p.dependencies||{}))if(v.version)add(n,v.version,'npm',true,'Resolved lockfile version');}
+else if(/requirements[^/]*\.txt$/.test(path)){for(const line of text.split('\n')){const m=line.match(/^([\w.-]+)(?:\[[^\]]+\])?\s*(==|>=|~=|<=|>|<)?\s*([^;#\s]*)/);if(m)add(m[1],m[3]||'unversioned','PyPI',m[2]==='==',m[2]==='=='?'Exact manifest pin':'Range / installed version unknown');}}
+else if(/Dockerfile[^/]*$/.test(path)){for(const m of text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/gmi))add(m[1].split(':')[0],m[1],'container');}
+else if(path.includes('.github/workflows/')){for(const m of text.matchAll(/\buses:\s*['"]?([^\s'"#]+)/g))add(m[1].split('@')[0],m[1].split('@')[1]||'unversioned','action');}
+else {add(path.split('/').pop(),'Detected','stack file');gaps.push(`${path}: detected but full parser unavailable (including yarn/pnpm/Python project lock formats).`);}
+}catch(e){gaps.push(`${path}: parse failed: ${e.message}`);}}
+function signal(title,detail,url,date,severity='info'){report.signals.push({title,detail,url,date,severity});}
+async function scan(){if($('run').disabled)return;let repo;try{repo=parseRepo($('repo').value)}catch(e){$('status').textContent=e.message;return;}$('run').disabled=true;$('demo').disabled=true;$('export').disabled=true;$('results').hidden=true;
+report={repo,checkedAt:new Date().toISOString(),items:[],signals:[],gaps:[],files:[]};const gaps=report.gaps;try{
+$('status').textContent='Reading repository metadata…';const api=`https://api.github.com/repos/${repo}`;const meta=await json(api);const branch=meta.default_branch;const commit=await json(`${api}/commits/${encodeURIComponent(branch)}`);report.commit=commit.sha;report.branch=branch;
+const tree=await json(`${api}/git/trees/${commit.sha}?recursive=1`);if(tree.truncated)gaps.push('GitHub tree is truncated; inventory may omit files.');const paths=tree.tree.filter(f=>f.type==='blob'&&/(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|requirements[^/]*\.txt|pyproject\.toml|poetry\.lock|uv\.lock|go\.mod|Cargo\.toml|Dockerfile[^/]*|\.nvmrc|\.node-version)$|(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(f.path));if(paths.length>24)gaps.push(`${paths.length} relevant files found; first 24 scanned.`);
+for(const f of paths.slice(0,24)){$('status').textContent=`Inspecting ${f.path}…`;const url=`https://github.com/${repo}/blob/${commit.sha}/${f.path}`;try{if(f.size>400000){gaps.push(`${f.path}: exceeds 400 KB limit.`);continue;}const blob=await json(`${api}/git/blobs/${f.sha}`);const bytes=Uint8Array.from(atob(blob.content.replace(/\s/g,'')),c=>c.charCodeAt(0));const text=new TextDecoder().decode(bytes);report.files.push({path:f.path,url});parseFile(f.path,text,url,report.items,gaps);}catch(e){gaps.push(`${f.path}: ${e.message}`);}}
+if(!report.items.length)gaps.push('No supported stack declarations found. This is not evidence of a dependency-free repository.');
+const unique=new Map();for(const i of report.items){const key=i.ecosystem+':'+i.name;const old=unique.get(key);if(!old||i.exact&&!old.exact)unique.set(key,i);}const dependencies=[...unique.values()].filter(i=>['npm','PyPI'].includes(i.ecosystem));if(dependencies.length>12)gaps.push(`${dependencies.length} dependencies inventoried; release/advisory checks limited to first 12 unique dependencies.`);
+for(const i of dependencies.slice(0,12)){$('status').textContent=`Checking upstream signals for ${i.name}…`;if(i.ecosystem==='npm')try{const p=await json(`https://registry.npmjs.org/${encodeURIComponent(i.name)}`);const latest=p['dist-tags']?.latest, current=i.exact?p.versions?.[i.version]:null;signal(`${i.name} · latest ${latest||'unknown'}`,`Observed ${i.version}. ${i.exact&&latest===i.version?'Matches npm latest.': 'Review compatibility and release notes; a differing version/range is not proof an upgrade is required.'}`,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}`,p.time?.[latest]||report.checkedAt);if(current?.deprecated)signal(`${i.name} · deprecated version`,current.deprecated,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}/v/${encodeURIComponent(i.version)}`,report.checkedAt,'alert');else if(!i.exact)gaps.push(`${i.name}: installed version and version-specific deprecation unknown (range).`);}catch(e){gaps.push(`${i.name}: npm release/deprecation check unavailable: ${e.message}`);}
+if(i.exact&&/^\d+\.\d+/.test(i.version))try{const result=await json('https://api.osv.dev/v1/query',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({package:{name:i.name,ecosystem:i.ecosystem},version:i.version})});const active=(result.vulns||[]).filter(v=>!v.withdrawn);if(!active.length)signal(`${i.name} ${i.version} · no OSV matches`,`OSV returned no published matches for this exact version at scan time. This is not a security certification.`, 'https://osv.dev/',report.checkedAt);for(const v of active)signal(`${i.name} ${i.version} · ${v.id}`,`${v.summary||'Published vulnerability'} — lockfile/pin matches; runtime reachability and exploitability require review.`,`https://osv.dev/vulnerability/${encodeURIComponent(v.id)}`,v.modified||v.published||report.checkedAt,'alert');}catch(e){gaps.push(`${i.name}: OSV check unavailable: ${e.message}`);}else gaps.push(`${i.name}: vulnerability status unknown; no supported exact version.`);}
+try{const releases=await json(`${api}/releases?per_page=3`);for(const r of releases)signal(`Repository release · ${r.tag_name}`,`${r.name||r.tag_name}${r.prerelease?' (prerelease)':''}. Review release notes for changes affecting this stack.`,r.html_url,r.published_at);if(!releases.length)gaps.push('Repository has no GitHub releases; tags/changelog not evaluated.');}catch(e){gaps.push(`Repository releases unavailable: ${e.message}`);}
+gaps.push('Docker and Action refs: inventory only; release, security and deprecation coverage unknown. Private repos, GitLab, runtime discovery and unattended alerts are unsupported.');
+const signature=JSON.stringify(report.items.map(i=>[i.name,i.version,i.path]));report.comparison=previous?(signature===previous?'No inventory changes since previous scan in this tab.':'Inventory changed since previous scan in this tab; review evidence.'):'First scan in this tab; no previous baseline.';previous=signature;
+$('status').textContent=`Scan complete · ${report.checkedAt} · ${repo}`;
+}catch(e){gaps.push(`Scan incomplete: ${e.message}. Check repository visibility, connectivity and GitHub's unauthenticated rate limit (usually 60 requests/hour/IP). Retry later; no clean bill of health is implied.`);$('status').textContent='Scan incomplete — see unknowns below.';}
+render();$('run').disabled=false;$('demo').disabled=false;$('export').disabled=false;}
+function render(){$('results').hidden=false;$('summary').innerHTML=`
Live API evidence ${report.items.length} observations · ${report.files.length} files · ${esc(report.checkedAt)}
${esc(report.comparison||'Incomplete scan; no baseline saved.')} ${report.commit?link(`https://github.com/${report.repo}/tree/${report.commit}`,`Source ${report.commit.slice(0,8)}`):''}
';
diff --git a/ventures/up-to-date/site/index.html b/ventures/up-to-date/site/index.html
new file mode 100644
index 0000000..f4b0b7a
--- /dev/null
+++ b/ventures/up-to-date/site/index.html
@@ -0,0 +1,8 @@
+UpToDate · Stack monitor
+NotPlanned / UpToDate01 — monitor first
Your stack moves. Keep an eye on it.
Inspect a public GitHub repository. See what it runs, what changed upstream, and which exact versions have published advisories. Read the evidence before deciding what to change.
+
+
Live, unauthenticated APIs. No tokens, repository writes, or background service. This unlisted link is publicly accessible, not access-controlled.
Ready. The Express example is a real repository, not sample data.
Stack inventory
Component
Observed version
Evidence
Coverage
Signals & impact
Unknowns & scan boundaries
+How this taste test works
We read up to 24 matching files from the default branch, including nested manifests and CI files; check up to 12 dependencies; and keep source links and timestamps. Exact npm lockfile / Python pins can be checked against OSV. Ranges are declarations, not proof of installed versions. Docker tags and Action refs are inventoried; their vulnerability coverage is unknown. Refresh compares the inventory with your previous scan in this tab. Close the tab and monitoring stops.
+Follow the experiment
Free: this browser monitor. Hosted alerts and team monitoring are early-access ideas, with no checkout or paid service yet.
Email is stored privately by NotPlanned for experiment updates.