Initial commit: Converge portfolio rationalization app

Next.js app for mapping a customer's competitive tool landscape onto our
portfolio and building the consolidation business case: assessment
workbench, customer report and Word export, product catalog, competitive
knowledge base, dossier import, shareable landscape questionnaires with a
review inbox, tool-footprint and business-unit value analysis, and an
implementation timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 11:48:42 -04:00
commit d5c77543e8
64 changed files with 30861 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
const B='http://127.0.0.1:3500';
const get=(u)=>fetch(B+u).then(r=>r.json());
const put=(u,b)=>fetch(B+u,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)}).then(r=>r.json());
const del=(u)=>fetch(B+u,{method:'DELETE'}).then(r=>r.json());
const post=(u,b)=>fetch(B+u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)}).then(r=>r.json());
let pass=0,fail=0; const t=(n,ok)=>{ok?(pass++,console.log(' PASS',n)):(fail++,console.log(' FAIL',n))};
const entry=(kb,l)=>kb.entries.find(e=>e.label===l);
const cand=(o)=>({id:'c-x',incumbentVendor:'',incumbentProduct:'',domain:'',productIds:[],annualSpend:0,units:0,unitLabel:'',renewalDate:'',capability:4,switchEase:3,timeToRoiMonths:12,savingsPct:50,migrationCost:0,strategicValue:3,risks:'',notes:'',included:true,...o});
// Self-contained: create an assessment, learn from it, then take things away.
const a = await post('/api/assessments', { customerName: 'Prune Test Co' });
a.candidates = [
cand({ id:'c-1', incumbentVendor:'CyberArk', incumbentProduct:'PAM', domain:'iam', productIds:['esg-pam'], annualSpend:400000, savingsPct:55 }),
cand({ id:'c-2', incumbentVendor:'Okta', incumbentProduct:'Workforce Identity', domain:'iam', productIds:['esg-siteminder'], annualSpend:300000, savingsPct:40 }),
];
await put('/api/assessments/'+a.id, a);
const learned = await get('/api/knowledge-base');
t('both candidates learned', entry(learned,'CyberArk PAM').observations.some(o=>o.assessmentId===a.id)
&& entry(learned,'Okta Workforce Identity').observations.some(o=>o.assessmentId===a.id));
t('learned the real savings figure', entry(learned,'CyberArk PAM').savingsPct === 55);
// Remove one candidate -> only its observation is pruned.
a.candidates = a.candidates.filter(c => c.id !== 'c-1');
await put('/api/assessments/'+a.id, a);
const pruned = await get('/api/knowledge-base');
t('removed candidate is forgotten', !entry(pruned,'CyberArk PAM').observations.some(o=>o.assessmentId===a.id));
t('defaults revert to seeded values', entry(pruned,'CyberArk PAM').savingsPct === 50);
t('the entry survives for reuse', entry(pruned,'CyberArk PAM').productIds.includes('esg-pam'));
t('the other candidate is untouched', entry(pruned,'Okta Workforce Identity').observations.some(o=>o.assessmentId===a.id));
// Delete the assessment -> everything it taught is forgotten.
await del('/api/assessments/'+a.id);
const gone = await get('/api/knowledge-base');
t('deleting the assessment forgets the rest', !entry(gone,'Okta Workforce Identity').observations.some(o=>o.assessmentId===a.id));
t('unrelated learning is preserved', entry(gone,'Splunk Enterprise').observations.length === 1);
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail?1:0);