analyzthis_design 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/HOW-TO-USE.md +436 -0
  2. package/README.md +29 -13
  3. package/agents/cards/evolve-check.md +38 -0
  4. package/agents/manifests/evolve-check.json +16 -0
  5. package/dist/HOW-TO-USE.md +15 -3
  6. package/dist/README.md +29 -13
  7. package/dist/agents/cards/evolve-check.md +38 -0
  8. package/dist/agents/manifests/evolve-check.json +16 -0
  9. package/dist/bin/cli.js +1225 -1
  10. package/dist/lib/cache.js +111 -1
  11. package/dist/lib/chunk-executor.js +219 -1
  12. package/dist/lib/chunk-models.js +228 -1
  13. package/dist/lib/chunk-planner.js +328 -1
  14. package/dist/lib/chunk-router.js +66 -1
  15. package/dist/lib/chunk-run.js +199 -1
  16. package/dist/lib/chunk-synthesis.js +176 -1
  17. package/dist/lib/chunk-telemetry.js +88 -1
  18. package/dist/lib/collect.js +858 -1
  19. package/dist/lib/cost.js +119 -1
  20. package/dist/lib/dedup.js +167 -1
  21. package/dist/lib/deliberation.js +721 -1
  22. package/dist/lib/design-spec.js +236 -1
  23. package/dist/lib/evolution-metrics.js +197 -0
  24. package/dist/lib/evolve.js +361 -1
  25. package/dist/lib/export.js +77 -1
  26. package/dist/lib/feedback-submit.js +324 -1
  27. package/dist/lib/feedback.js +182 -1
  28. package/dist/lib/host-llm.js +251 -1
  29. package/dist/lib/install.js +301 -1
  30. package/dist/lib/knowledge.js +384 -1
  31. package/dist/lib/lessons.js +217 -1
  32. package/dist/lib/moodboard.js +563 -1
  33. package/dist/lib/orchestrator/run.js +935 -1
  34. package/dist/lib/outcome.js +193 -1
  35. package/dist/lib/platforms.js +166 -1
  36. package/dist/lib/provider.js +57 -1
  37. package/dist/lib/query-expander.js +83 -1
  38. package/dist/lib/ranker.js +105 -1
  39. package/dist/lib/reference-pack.js +221 -0
  40. package/dist/lib/research.js +143 -1
  41. package/dist/lib/retrieve.js +131 -1
  42. package/dist/lib/session.js +185 -1
  43. package/dist/lib/source-discovery.js +486 -1
  44. package/dist/lib/synthesis.js +155 -1
  45. package/dist/lib/token-gate.js +46 -1
  46. package/dist/skills/design-reference/google-fonts.csv +1924 -1924
  47. package/dist/skills/design-reference/products.csv +162 -162
  48. package/dist/skills/design-reference/schema.json +159 -0
  49. package/dist/skills/design-reference/stacks/angular.csv +1 -1
  50. package/dist/skills/design-reference/stacks/astro.csv +1 -1
  51. package/dist/skills/design-reference/stacks/laravel.csv +2 -2
  52. package/dist/skills/design-reference/stacks/threejs.csv +54 -54
  53. package/dist/skills/design-reference/styles.csv +85 -85
  54. package/dist/skills/design-reference/typography.csv +75 -74
  55. package/dist/skills/design-reference/ui-reasoning.csv +1 -1
  56. package/dist/skills/evolve-check/SKILL.md +106 -0
  57. package/package.json +8 -8
  58. package/scripts/validate-csvs.js +197 -0
  59. package/skills/design-reference/google-fonts.csv +1924 -1924
  60. package/skills/design-reference/products.csv +162 -162
  61. package/skills/design-reference/schema.json +159 -0
  62. package/skills/design-reference/stacks/angular.csv +1 -1
  63. package/skills/design-reference/stacks/astro.csv +1 -1
  64. package/skills/design-reference/stacks/laravel.csv +2 -2
  65. package/skills/design-reference/stacks/threejs.csv +54 -54
  66. package/skills/design-reference/styles.csv +85 -85
  67. package/skills/design-reference/typography.csv +75 -74
  68. package/skills/design-reference/ui-reasoning.csv +1 -1
  69. package/skills/evolve-check/SKILL.md +106 -0
@@ -1,2 +1,487 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';const fs=require('fs'),path=require('path'),os=require('os'),knowledge=require('./knowledge'),HOME=os['homedir'](),VAULT_NAME_HINTS=/\b(vault|obsidian|wiki|knowledge|docs|notes|brain|second.?brain)\b/i,REFERENCE_FILES=['README.md','AGENTS.md','CLAUDE.md','CONTRIBUTING.md','package.json','.cursorrules','cursor.md'],MIN_MD_FOR_VAULT=0x5,PATH_REF_PATTERNS=[/(?:connect\s+--vault\s+)([^\s`"']+)/gi,/(?:vault|obsidian|knowledge[-_ ]?bank|knowledge[-_ ]?graph|wiki)[:\s]+[`"']?([~/][^\s`"'\],)]+)/gi,/(?:path|folder|directory)[:\s]+[`"']?([~/][^\s`"'\],)]*(?:vault|obsidian|wiki|knowledge)[^\s`"'\],)]*)/gi];function loadConfig(){const a=path['join'](HOME,'.analyzthis_design','config.json');if(!fs['existsSync'](a))return{'sources':[]};try{return JSON['parse'](fs['readFileSync'](a,'utf8'));}catch{return{'sources':[]};}}function expandPath(a,b){if(!a||typeof a!=='string')return null;let c=a['trim']()['replace'](/^[`"']|[`"']$/g,'');if(c['startsWith']('~'))c=path['join'](HOME,c['slice'](0x1)['replace'](/^\//,''));else{if(!path['isAbsolute'](c))c=path['resolve'](b,c);}try{return c=fs['realpathSync'](c),c;}catch{return fs['existsSync'](c)?path['resolve'](c):null;}}function countMarkdown(a,b=0x1f4){let c=0x0;const d=(f,g)=>{if(g>0x6||c>=b)return;let h;try{h=fs['readdirSync'](f,{'withFileTypes':!![]});}catch{return;}for(const i of h){if(i['name']['startsWith']('.')&&i['name']!=='.obsidian')continue;const j=path['join'](f,i['name']);if(i['isDirectory']())d(j,g+0x1);else{if(i['name']['endsWith']('.md'))c+=0x1;}}};return d(a,0x0),c;}function listTopFolders(a,b=0x8){try{return fs['readdirSync'](a,{'withFileTypes':!![]})['filter'](c=>c['isDirectory']()&&!c['name']['startsWith']('.'))['map'](c=>c['name'])['slice'](0x0,b);}catch{return[];}}function isObsidianVault(a){return fs['existsSync'](path['join'](a,'.obsidian'));}function hasKnowledgeGraph(a){const b=path['join'](a,'.obsidian','graph.json');if(fs['existsSync'](b))return!![];const c=['graph.json','knowledge-graph.json','kg.json'];try{return fs['readdirSync'](a)['some'](d=>c['includes'](d['toLowerCase']()));}catch{return![];}}function statsForPath(a){const b=countMarkdown(a),c=listTopFolders(a);return{'markdown_files':b,'top_folders':c,'has_obsidian':isObsidianVault(a),'has_graph':hasKnowledgeGraph(a)};}function makeDiscovery({path:a,type:b,discoveredFrom:c,confidence:d,cwd:e}){if(!a||!fs['existsSync'](a))return null;try{const g=fs['statSync'](a);if(!g['isDirectory']())return null;}catch{return null;}const f=statsForPath(a);if(f['markdown_files']===0x0&&!f['has_obsidian'])return null;return{'path':a,'type':b,'discovered_from':c,'confidence':d,'stats':f,'slug':path['basename'](a)['toLowerCase']()['replace'](/[^a-z0-9]+/g,'-')['slice'](0x0,0x28)||'source'};}function addDiscovery(a,b){if(!b)return;const c=b['path'],d=a['get'](c);(!d||rankConfidence(b['confidence'])>rankConfidence(d['confidence']))&&a['set'](c,b);}function rankConfidence(a){return a==='high'?0x3:a==='medium'?0x2:0x1;}function findObsidianVaultsInTree(a,b=0x5,c=new Set()){const d=[],e=(f,g)=>{if(g>b)return;let h;try{h=fs['readdirSync'](f,{'withFileTypes':!![]});}catch{return;}if(h['some'](i=>i['name']==='.obsidian'&&i['isDirectory']())){d['push'](f);return;}for(const i of h){if(!i['isDirectory']())continue;if(i['name']['startsWith']('.')||c['has'](i['name']))continue;if(['node_modules','dist','build','.git','vendor']['includes'](i['name']))continue;e(path['join'](f,i['name']),g+0x1);}};return e(a,0x0),d;}function findMarkdownRichFolders(a,b=0x3){const c=[],d=(f,g)=>{if(g>b)return;let h;try{h=fs['readdirSync'](f,{'withFileTypes':!![]});}catch{return;}const i=path['basename'](f),j=h['filter'](k=>k['isFile']()&&k['name']['endsWith']('.md'))['length'];j>=MIN_MD_FOR_VAULT&&(VAULT_NAME_HINTS['test'](i)||VAULT_NAME_HINTS['test'](f))&&c['push'](f);for(const k of h){if(!k['isDirectory']()||k['name']['startsWith']('.'))continue;if(['node_modules','dist','build','.git']['includes'](k['name']))continue;d(path['join'](f,k['name']),g+0x1);}};return d(a,0x0),c;}function extractPathRefs(a,b){const c=new Set();for(const e of PATH_REF_PATTERNS){e['lastIndex']=0x0;let f;while((f=e['exec'](a))!==null){const g=expandPath(f[0x1],b);if(g)c['add'](g);}}const d=a['match'](/obsidian:\/\/[^\s"']+/gi)||[];for(const h of d)c['add'](h);return[...c];}function collectReferenceTexts(a){const b=[];for(const d of REFERENCE_FILES){const e=path['join'](a,d);if(fs['existsSync'](e))try{b['push']({'rel':d,'content':fs['readFileSync'](e,'utf8')});}catch{}}const c=['docs','.cursor','.github'];for(const f of c){const g=path['join'](a,f);if(!fs['existsSync'](g))continue;const h=(i,j)=>{if(j>0x4)return;let k;try{k=fs['readdirSync'](i,{'withFileTypes':!![]});}catch{return;}for(const l of k){const m=path['join'](i,l['name']);if(l['isDirectory']()&&!l['name']['startsWith']('.'))h(m,j+0x1);else{if(l['isFile']()&&/\.(md|json|txt)$/i['test'](l['name']))try{b['push']({'rel':path['relative'](a,m),'content':fs['readFileSync'](m,'utf8')['slice'](0x0,0xc350)});}catch{}}}};h(g,0x0);}return b;}function scanHomeVaultHints(a){const b=[];if(a['collect']?.['scan_home_vaults']!==!![])return b;const c=a['collect']?.['home_vault_paths']||[path['join'](HOME,'Documents'),path['join'](HOME,'Obsidian'),path['join'](HOME,'Vaults')];for(const d of c){if(!fs['existsSync'](d))continue;let f;try{f=fs['readdirSync'](d,{'withFileTypes':!![]});}catch{continue;}for(const g of f){if(!g['isDirectory']())continue;const h=path['join'](d,g['name']);if(isObsidianVault(h))b['push'](h);}}return b;}function discoverKnowledgeSources({cwd:a,config:b}){const c=b||loadConfig(),d=new Map();for(const e of c['sources']||[]){addDiscovery(d,makeDiscovery({'path':expandPath(e['path'],a),'type':'registered_source','discoveredFrom':'~/.analyzthis_design/config.json','confidence':'high','cwd':a}));}for(const f of c['collect']?.['source_paths']||[]){addDiscovery(d,makeDiscovery({'path':expandPath(f,a),'type':'config_path','discoveredFrom':'collect.source_paths','confidence':'high','cwd':a}));}for(const g of findObsidianVaultsInTree(a)){addDiscovery(d,makeDiscovery({'path':g,'type':'obsidian_vault','discoveredFrom':'.obsidian/\x20marker\x20in\x20repo','confidence':'high','cwd':a}));}for(const h of findMarkdownRichFolders(a)){addDiscovery(d,makeDiscovery({'path':h,'type':'markdown_vault','discoveredFrom':'markdown-rich\x20folder\x20in\x20repo','confidence':'medium','cwd':a}));}for(const {rel:i,content:j}of collectReferenceTexts(a)){for(const k of extractPathRefs(j,a)){if(k['startsWith']('obsidian://'))continue;addDiscovery(d,makeDiscovery({'path':k,'type':'referenced_path','discoveredFrom':i,'confidence':'medium','cwd':a}));}if(/\b\.obsidian\/graph\b/i['test'](j)){const l=path['dirname'](path['join'](a,i));(isObsidianVault(l)||hasKnowledgeGraph(l))&&addDiscovery(d,makeDiscovery({'path':l,'type':'knowledge_graph_ref','discoveredFrom':i+'\x20(.obsidian/graph\x20reference)','confidence':'high','cwd':a}));}}for(const m of scanHomeVaultHints(c)){addDiscovery(d,makeDiscovery({'path':m,'type':'obsidian_vault','discoveredFrom':'home\x20vault\x20scan\x20(collect.scan_home_vaults)','confidence':'medium','cwd':a}));}return[...d['values']()]['sort']((n,o)=>rankConfidence(o['confidence'])-rankConfidence(n['confidence']));}function buildSourceNote(a,b){const {path:c,type:d,discovered_from:e,confidence:f,stats:g}=a,h=d['replace'](/_/g,'\x20')+'\x20—\x20'+path['basename'](c),i=['---','title:\x20'+JSON['stringify'](h),'tags:\x20[source,\x20discovered,\x20kavi]','source_type:\x20\x22'+d+'\x22','source_path:\x20'+JSON['stringify'](c),'discovered_from:\x20'+JSON['stringify'](e),'confidence:\x20\x22'+f+'\x22','markdown_files:\x20'+g['markdown_files'],'has_obsidian:\x20'+g['has_obsidian'],'has_knowledge_graph:\x20'+g['has_graph'],'collected_at:\x20\x22'+new Date()['toISOString']()['split']('T')[0x0]+'\x22','---','']['join']('\x0a'),j=['#\x20'+h,'','##\x20Summary','Potential\x20knowledge-bank\x20source\x20discovered\x20by\x20Kavi\x20during\x20collect.','','-\x20**Absolute\x20path:**\x20`'+c+'`','-\x20**Type:**\x20'+d,'-\x20**Confidence:**\x20'+f,'-\x20**Discovered\x20from:**\x20'+e,'','##\x20Contents\x20snapshot','-\x20Markdown\x20files\x20(approx):\x20**'+g['markdown_files']+'**','-\x20Top\x20folders:\x20'+(g['top_folders']['length']?g['top_folders']['map'](k=>'`'+k+'`')['join'](',\x20'):'_(none)_'),'-\x20Obsidian\x20vault:\x20'+(g['has_obsidian']?'yes':'no'),'-\x20Knowledge\x20graph\x20data:\x20'+(g['has_graph']?'yes':'no'),'','##\x20How\x20personas\x20use\x20this','When\x20auto-connected,\x20notes\x20from\x20this\x20path\x20are\x20merged\x20into\x20the\x20**knowledge\x20bank**\x20on\x20sync.','Personas\x20read\x20PRDs,\x20brand,\x20research,\x20and\x20design\x20context\x20from\x20connected\x20sources\x20before\x20critiquing.','','##\x20Manual\x20connect','```bash','npx\x20analyzthis_design\x20connect\x20--vault\x20\x22'+c+'\x22','npx\x20analyzthis_design\x20sync\x20--target\x20all','```','','##\x20Related','-\x20[[_meta/knowledge-sources|All\x20discovered\x20sources]]','-\x20[[_meta/index|Vault\x20index]]','']['join']('\x0a');return{'vaultRel':'Sources/'+String(b)['padStart'](0x2,'0')+'-'+a['slug']+'.md','content':i+j};}function writeSourceManifestNotes({discoveries:a,vaultPath:b,dryRun:c}){if(!a['length'])return{'written':[],'indexPath':null};const e=a['map']((h,j)=>buildSourceNote(h,j+0x1)),f=['---','title:\x20\x22Discovered\x20knowledge\x20sources\x22','tags:\x20[meta,\x20sources,\x20moc]','updated_at:\x20\x22'+new Date()['toISOString']()+'\x22','---','','#\x20Discovered\x20knowledge\x20sources','','Kavi\x20found\x20these\x20potential\x20knowledge-bank\x20areas\x20during\x20collect.\x20Manifest\x20notes\x20live\x20in\x20`Sources/`.','','|\x20Source\x20|\x20Type\x20|\x20Confidence\x20|\x20Markdown\x20files\x20|','|\x20---\x20|\x20---\x20|\x20---\x20|\x20---\x20|'];for(const h of a){f['push']('|\x20`'+h['path']+'`\x20|\x20'+h['type']+'\x20|\x20'+h['confidence']+'\x20|\x20'+h['stats']['markdown_files']+'\x20|');}f['push']('','##\x20Source\x20notes','');for(const i of e){const j=i['vaultRel']['replace'](/\.md$/,'')['replace'](/\\/g,'/');f['push']('-\x20[['+j+']]');}f['push']('');if(c)return{'written':e['map'](k=>k['vaultRel']),'indexPath':'_meta/knowledge-sources.md','discoveries':a};fs['mkdirSync'](path['join'](b,'Sources'),{'recursive':!![]}),fs['mkdirSync'](path['join'](b,'_meta'),{'recursive':!![]});for(const k of e){fs['writeFileSync'](path['join'](b,k['vaultRel']),k['content']);}const g=path['join'](b,'_meta','knowledge-sources.md');return fs['writeFileSync'](g,f['join']('\x0a')),{'written':e['map'](l=>l['vaultRel']),'indexPath':'_meta/knowledge-sources.md'};}function connectDiscoveredSources({discoveries:a,kaviVaultPath:b,config:c,autoConnect:autoConnect=!![]}){const e=[],f=[];if(!autoConnect)return{'connected':e,'skipped':f};const g=c['collect']?.['auto_connect_min_confidence']==='low'?0x1:c['collect']?.['auto_connect_min_confidence']==='high'?0x3:0x2;for(const h of a){if(rankConfidence(h['confidence'])<g){f['push']({'path':h['path'],'reason':'low\x20confidence'});continue;}if(h['path']===b){f['push']({'path':h['path'],'reason':'kavi\x20output\x20vault'});continue;}try{knowledge['connect']({'vaultPath':h['path'],'tags':[],'include':[]}),e['push'](h['path']);}catch(i){f['push']({'path':h['path'],'reason':i['message']});}}return{'connected':e,'skipped':f};}module['exports']={'discoverKnowledgeSources':discoverKnowledgeSources,'writeSourceManifestNotes':writeSourceManifestNotes,'connectDiscoveredSources':connectDiscoveredSources,'expandPath':expandPath,'isObsidianVault':isObsidianVault};
2
+ 'use strict';
3
+
4
+ /**
5
+ * Discover potential knowledge-bank sources: Obsidian vaults, markdown wikis,
6
+ * knowledge-graph references, and paths cited in repo docs — then write manifest
7
+ * notes for Kavi and optionally auto-connect them.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const knowledge = require('./knowledge');
14
+
15
+ const HOME = os.homedir();
16
+ const VAULT_NAME_HINTS = /\b(vault|obsidian|wiki|knowledge|docs|notes|brain|second.?brain)\b/i;
17
+ const REFERENCE_FILES = [
18
+ 'README.md', 'AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md',
19
+ 'package.json', '.cursorrules', 'cursor.md',
20
+ ];
21
+ const MIN_MD_FOR_VAULT = 5;
22
+
23
+ const PATH_REF_PATTERNS = [
24
+ /(?:connect\s+--vault\s+)([^\s`"']+)/gi,
25
+ /(?:vault|obsidian|knowledge[-_ ]?bank|knowledge[-_ ]?graph|wiki)[:\s]+[`"']?([~/][^\s`"'\],)]+)/gi,
26
+ /(?:path|folder|directory)[:\s]+[`"']?([~/][^\s`"'\],)]*(?:vault|obsidian|wiki|knowledge)[^\s`"'\],)]*)/gi,
27
+ ];
28
+
29
+ function loadConfig() {
30
+ const configPath = path.join(HOME, '.analyzthis_design', 'config.json');
31
+ if (!fs.existsSync(configPath)) return { sources: [] };
32
+ try { return JSON.parse(fs.readFileSync(configPath, 'utf8')); }
33
+ catch { return { sources: [] }; }
34
+ }
35
+
36
+ function expandPath(raw, cwd) {
37
+ if (!raw || typeof raw !== 'string') return null;
38
+ let p = raw.trim().replace(/^[`"']|[`"']$/g, '');
39
+ if (p.startsWith('~')) p = path.join(HOME, p.slice(1).replace(/^\//, ''));
40
+ else if (!path.isAbsolute(p)) p = path.resolve(cwd, p);
41
+ try {
42
+ p = fs.realpathSync(p);
43
+ return p;
44
+ } catch {
45
+ return fs.existsSync(p) ? path.resolve(p) : null;
46
+ }
47
+ }
48
+
49
+ function countMarkdown(dir, max = 500) {
50
+ let n = 0;
51
+ const walk = (cur, depth) => {
52
+ if (depth > 6 || n >= max) return;
53
+ let entries;
54
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
55
+ catch { return; }
56
+ for (const e of entries) {
57
+ if (e.name.startsWith('.') && e.name !== '.obsidian') continue;
58
+ const full = path.join(cur, e.name);
59
+ if (e.isDirectory()) walk(full, depth + 1);
60
+ else if (e.name.endsWith('.md')) n += 1;
61
+ }
62
+ };
63
+ walk(dir, 0);
64
+ return n;
65
+ }
66
+
67
+ function listTopFolders(dir, limit = 8) {
68
+ try {
69
+ return fs.readdirSync(dir, { withFileTypes: true })
70
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
71
+ .map((e) => e.name)
72
+ .slice(0, limit);
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ function isObsidianVault(dir) {
79
+ return fs.existsSync(path.join(dir, '.obsidian'));
80
+ }
81
+
82
+ function hasKnowledgeGraph(dir) {
83
+ const obsGraph = path.join(dir, '.obsidian', 'graph.json');
84
+ if (fs.existsSync(obsGraph)) return true;
85
+ const names = ['graph.json', 'knowledge-graph.json', 'kg.json'];
86
+ try {
87
+ return fs.readdirSync(dir).some((f) => names.includes(f.toLowerCase()));
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function statsForPath(absPath) {
94
+ const mdCount = countMarkdown(absPath);
95
+ const folders = listTopFolders(absPath);
96
+ return {
97
+ markdown_files: mdCount,
98
+ top_folders: folders,
99
+ has_obsidian: isObsidianVault(absPath),
100
+ has_graph: hasKnowledgeGraph(absPath),
101
+ };
102
+ }
103
+
104
+ function makeDiscovery({ path: absPath, type, discoveredFrom, confidence, cwd }) {
105
+ if (!absPath || !fs.existsSync(absPath)) return null;
106
+ try {
107
+ const stat = fs.statSync(absPath);
108
+ if (!stat.isDirectory()) return null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ const stats = statsForPath(absPath);
113
+ if (stats.markdown_files === 0 && !stats.has_obsidian) return null;
114
+
115
+ return {
116
+ path: absPath,
117
+ type,
118
+ discovered_from: discoveredFrom,
119
+ confidence,
120
+ stats,
121
+ slug: path.basename(absPath).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40) || 'source',
122
+ };
123
+ }
124
+
125
+ function addDiscovery(map, item) {
126
+ if (!item) return;
127
+ const key = item.path;
128
+ const existing = map.get(key);
129
+ if (!existing || rankConfidence(item.confidence) > rankConfidence(existing.confidence)) {
130
+ map.set(key, item);
131
+ }
132
+ }
133
+
134
+ function rankConfidence(c) {
135
+ return c === 'high' ? 3 : c === 'medium' ? 2 : 1;
136
+ }
137
+
138
+ function findObsidianVaultsInTree(root, maxDepth = 5, exclude = new Set()) {
139
+ const found = [];
140
+ const walk = (dir, depth) => {
141
+ if (depth > maxDepth) return;
142
+ let entries;
143
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
144
+ catch { return; }
145
+ if (entries.some((e) => e.name === '.obsidian' && e.isDirectory())) {
146
+ found.push(dir);
147
+ return;
148
+ }
149
+ for (const e of entries) {
150
+ if (!e.isDirectory()) continue;
151
+ if (e.name.startsWith('.') || exclude.has(e.name)) continue;
152
+ if (['node_modules', 'dist', 'build', '.git', 'vendor'].includes(e.name)) continue;
153
+ walk(path.join(dir, e.name), depth + 1);
154
+ }
155
+ };
156
+ walk(root, 0);
157
+ return found;
158
+ }
159
+
160
+ function findMarkdownRichFolders(root, maxDepth = 3) {
161
+ const found = [];
162
+ const walk = (dir, depth) => {
163
+ if (depth > maxDepth) return;
164
+ let entries;
165
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
166
+ catch { return; }
167
+ const base = path.basename(dir);
168
+ const mdCount = entries.filter((e) => e.isFile() && e.name.endsWith('.md')).length;
169
+ if (mdCount >= MIN_MD_FOR_VAULT && (VAULT_NAME_HINTS.test(base) || VAULT_NAME_HINTS.test(dir))) {
170
+ found.push(dir);
171
+ }
172
+ for (const e of entries) {
173
+ if (!e.isDirectory() || e.name.startsWith('.')) continue;
174
+ if (['node_modules', 'dist', 'build', '.git'].includes(e.name)) continue;
175
+ walk(path.join(dir, e.name), depth + 1);
176
+ }
177
+ };
178
+ walk(root, 0);
179
+ return found;
180
+ }
181
+
182
+ function extractPathRefs(text, cwd) {
183
+ const paths = new Set();
184
+ for (const re of PATH_REF_PATTERNS) {
185
+ re.lastIndex = 0;
186
+ let m;
187
+ while ((m = re.exec(text)) !== null) {
188
+ const expanded = expandPath(m[1], cwd);
189
+ if (expanded) paths.add(expanded);
190
+ }
191
+ }
192
+ // Obsidian URIs: obsidian://open?vault=Name — record name only (path unknown)
193
+ const vaultNames = text.match(/obsidian:\/\/[^\s"']+/gi) || [];
194
+ for (const uri of vaultNames) paths.add(uri);
195
+ return [...paths];
196
+ }
197
+
198
+ function collectReferenceTexts(cwd) {
199
+ const texts = [];
200
+ for (const rel of REFERENCE_FILES) {
201
+ const full = path.join(cwd, rel);
202
+ if (fs.existsSync(full)) {
203
+ try { texts.push({ rel, content: fs.readFileSync(full, 'utf8') }); } catch { /* ignore */ }
204
+ }
205
+ }
206
+ const extraDirs = ['docs', '.cursor', '.github'];
207
+ for (const dir of extraDirs) {
208
+ const base = path.join(cwd, dir);
209
+ if (!fs.existsSync(base)) continue;
210
+ const walk = (cur, depth) => {
211
+ if (depth > 4) return;
212
+ let entries;
213
+ try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
214
+ catch { return; }
215
+ for (const e of entries) {
216
+ const full = path.join(cur, e.name);
217
+ if (e.isDirectory() && !e.name.startsWith('.')) walk(full, depth + 1);
218
+ else if (e.isFile() && /\.(md|json|txt)$/i.test(e.name)) {
219
+ try {
220
+ texts.push({ rel: path.relative(cwd, full), content: fs.readFileSync(full, 'utf8').slice(0, 50000) });
221
+ } catch { /* ignore */ }
222
+ }
223
+ }
224
+ };
225
+ walk(base, 0);
226
+ }
227
+ return texts;
228
+ }
229
+
230
+ function scanHomeVaultHints(config) {
231
+ const results = [];
232
+ if (config.collect?.scan_home_vaults !== true) return results;
233
+ const searchRoots = config.collect?.home_vault_paths || [
234
+ path.join(HOME, 'Documents'),
235
+ path.join(HOME, 'Obsidian'),
236
+ path.join(HOME, 'Vaults'),
237
+ ];
238
+ for (const root of searchRoots) {
239
+ if (!fs.existsSync(root)) continue;
240
+ let entries;
241
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); }
242
+ catch { continue; }
243
+ for (const e of entries) {
244
+ if (!e.isDirectory()) continue;
245
+ const full = path.join(root, e.name);
246
+ if (isObsidianVault(full)) results.push(full);
247
+ }
248
+ }
249
+ return results;
250
+ }
251
+
252
+ /**
253
+ * Discover knowledge sources around the project and in config.
254
+ */
255
+ function discoverKnowledgeSources({ cwd, config: cfgIn }) {
256
+ const config = cfgIn || loadConfig();
257
+ const map = new Map();
258
+
259
+ // Already registered in config
260
+ for (const src of config.sources || []) {
261
+ addDiscovery(map, makeDiscovery({
262
+ path: expandPath(src.path, cwd),
263
+ type: 'registered_source',
264
+ discoveredFrom: '~/.analyzthis_design/config.json',
265
+ confidence: 'high',
266
+ cwd,
267
+ }));
268
+ }
269
+
270
+ // Explicit collect paths
271
+ for (const p of config.collect?.source_paths || []) {
272
+ addDiscovery(map, makeDiscovery({
273
+ path: expandPath(p, cwd),
274
+ type: 'config_path',
275
+ discoveredFrom: 'collect.source_paths',
276
+ confidence: 'high',
277
+ cwd,
278
+ }));
279
+ }
280
+
281
+ // Obsidian vaults in repo
282
+ for (const vaultRoot of findObsidianVaultsInTree(cwd)) {
283
+ addDiscovery(map, makeDiscovery({
284
+ path: vaultRoot,
285
+ type: 'obsidian_vault',
286
+ discoveredFrom: '.obsidian/ marker in repo',
287
+ confidence: 'high',
288
+ cwd,
289
+ }));
290
+ }
291
+
292
+ // Markdown-rich folders (wiki / docs vaults)
293
+ for (const folder of findMarkdownRichFolders(cwd)) {
294
+ addDiscovery(map, makeDiscovery({
295
+ path: folder,
296
+ type: 'markdown_vault',
297
+ discoveredFrom: 'markdown-rich folder in repo',
298
+ confidence: 'medium',
299
+ cwd,
300
+ }));
301
+ }
302
+
303
+ // Paths referenced in README, AGENTS.md, docs, etc.
304
+ for (const { rel, content } of collectReferenceTexts(cwd)) {
305
+ for (const ref of extractPathRefs(content, cwd)) {
306
+ if (ref.startsWith('obsidian://')) {
307
+ // URI-only — documented in manifest, not connectable
308
+ continue;
309
+ }
310
+ addDiscovery(map, makeDiscovery({
311
+ path: ref,
312
+ type: 'referenced_path',
313
+ discoveredFrom: rel,
314
+ confidence: 'medium',
315
+ cwd,
316
+ }));
317
+ }
318
+ if (/\b\.obsidian\/graph\b/i.test(content)) {
319
+ const candidate = path.dirname(path.join(cwd, rel));
320
+ if (isObsidianVault(candidate) || hasKnowledgeGraph(candidate)) {
321
+ addDiscovery(map, makeDiscovery({
322
+ path: candidate,
323
+ type: 'knowledge_graph_ref',
324
+ discoveredFrom: `${rel} (.obsidian/graph reference)`,
325
+ confidence: 'high',
326
+ cwd,
327
+ }));
328
+ }
329
+ }
330
+ }
331
+
332
+ // Optional home scan
333
+ for (const homeVault of scanHomeVaultHints(config)) {
334
+ addDiscovery(map, makeDiscovery({
335
+ path: homeVault,
336
+ type: 'obsidian_vault',
337
+ discoveredFrom: 'home vault scan (collect.scan_home_vaults)',
338
+ confidence: 'medium',
339
+ cwd,
340
+ }));
341
+ }
342
+
343
+ return [...map.values()].sort((a, b) => rankConfidence(b.confidence) - rankConfidence(a.confidence));
344
+ }
345
+
346
+ function buildSourceNote(discovery, index) {
347
+ const { path: absPath, type, discovered_from, confidence, stats } = discovery;
348
+ const title = `${type.replace(/_/g, ' ')} — ${path.basename(absPath)}`;
349
+ const fm = [
350
+ '---',
351
+ `title: ${JSON.stringify(title)}`,
352
+ 'tags: [source, discovered, kavi]',
353
+ `source_type: "${type}"`,
354
+ `source_path: ${JSON.stringify(absPath)}`,
355
+ `discovered_from: ${JSON.stringify(discovered_from)}`,
356
+ `confidence: "${confidence}"`,
357
+ `markdown_files: ${stats.markdown_files}`,
358
+ `has_obsidian: ${stats.has_obsidian}`,
359
+ `has_knowledge_graph: ${stats.has_graph}`,
360
+ `collected_at: "${new Date().toISOString().split('T')[0]}"`,
361
+ '---',
362
+ '',
363
+ ].join('\n');
364
+
365
+ const body = [
366
+ `# ${title}`,
367
+ '',
368
+ '## Summary',
369
+ `Potential knowledge-bank source discovered by Kavi during collect.`,
370
+ '',
371
+ `- **Absolute path:** \`${absPath}\``,
372
+ `- **Type:** ${type}`,
373
+ `- **Confidence:** ${confidence}`,
374
+ `- **Discovered from:** ${discovered_from}`,
375
+ '',
376
+ '## Contents snapshot',
377
+ `- Markdown files (approx): **${stats.markdown_files}**`,
378
+ `- Top folders: ${stats.top_folders.length ? stats.top_folders.map((f) => `\`${f}\``).join(', ') : '_(none)_'}`,
379
+ `- Obsidian vault: ${stats.has_obsidian ? 'yes' : 'no'}`,
380
+ `- Knowledge graph data: ${stats.has_graph ? 'yes' : 'no'}`,
381
+ '',
382
+ '## How personas use this',
383
+ 'When auto-connected, notes from this path are merged into the **knowledge bank** on sync.',
384
+ 'Personas read PRDs, brand, research, and design context from connected sources before critiquing.',
385
+ '',
386
+ '## Manual connect',
387
+ '```bash',
388
+ `npx analyzthis_design connect --vault "${absPath}"`,
389
+ 'npx analyzthis_design sync --target all',
390
+ '```',
391
+ '',
392
+ `## Related`,
393
+ `- [[_meta/knowledge-sources|All discovered sources]]`,
394
+ `- [[_meta/index|Vault index]]`,
395
+ '',
396
+ ].join('\n');
397
+
398
+ return { vaultRel: `Sources/${String(index).padStart(2, '0')}-${discovery.slug}.md`, content: fm + body };
399
+ }
400
+
401
+ /**
402
+ * Write manifest notes under vault/Sources/ and _meta/knowledge-sources.md
403
+ */
404
+ function writeSourceManifestNotes({ discoveries, vaultPath, dryRun }) {
405
+ if (!discoveries.length) return { written: [], indexPath: null };
406
+
407
+ const notes = discoveries.map((d, i) => buildSourceNote(d, i + 1));
408
+ const indexLines = [
409
+ '---',
410
+ 'title: "Discovered knowledge sources"',
411
+ 'tags: [meta, sources, moc]',
412
+ `updated_at: "${new Date().toISOString()}"`,
413
+ '---',
414
+ '',
415
+ '# Discovered knowledge sources',
416
+ '',
417
+ 'Kavi found these potential knowledge-bank areas during collect. Manifest notes live in `Sources/`.',
418
+ '',
419
+ '| Source | Type | Confidence | Markdown files |',
420
+ '| --- | --- | --- | --- |',
421
+ ];
422
+
423
+ for (const d of discoveries) {
424
+ indexLines.push(`| \`${d.path}\` | ${d.type} | ${d.confidence} | ${d.stats.markdown_files} |`);
425
+ }
426
+ indexLines.push('', '## Source notes', '');
427
+ for (const n of notes) {
428
+ const link = n.vaultRel.replace(/\.md$/, '').replace(/\\/g, '/');
429
+ indexLines.push(`- [[${link}]]`);
430
+ }
431
+ indexLines.push('');
432
+
433
+ if (dryRun) {
434
+ return {
435
+ written: notes.map((n) => n.vaultRel),
436
+ indexPath: '_meta/knowledge-sources.md',
437
+ discoveries,
438
+ };
439
+ }
440
+
441
+ fs.mkdirSync(path.join(vaultPath, 'Sources'), { recursive: true });
442
+ fs.mkdirSync(path.join(vaultPath, '_meta'), { recursive: true });
443
+ for (const n of notes) {
444
+ fs.writeFileSync(path.join(vaultPath, n.vaultRel), n.content);
445
+ }
446
+ const indexPath = path.join(vaultPath, '_meta', 'knowledge-sources.md');
447
+ fs.writeFileSync(indexPath, indexLines.join('\n'));
448
+ return { written: notes.map((n) => n.vaultRel), indexPath: '_meta/knowledge-sources.md' };
449
+ }
450
+
451
+ /**
452
+ * Auto-connect discovered sources (high/medium confidence) into config.sources.
453
+ */
454
+ function connectDiscoveredSources({ discoveries, kaviVaultPath, config, autoConnect = true }) {
455
+ const connected = [];
456
+ const skipped = [];
457
+ if (!autoConnect) return { connected, skipped };
458
+
459
+ const minRank = config.collect?.auto_connect_min_confidence === 'low' ? 1
460
+ : config.collect?.auto_connect_min_confidence === 'high' ? 3 : 2;
461
+
462
+ for (const d of discoveries) {
463
+ if (rankConfidence(d.confidence) < minRank) {
464
+ skipped.push({ path: d.path, reason: 'low confidence' });
465
+ continue;
466
+ }
467
+ if (d.path === kaviVaultPath) {
468
+ skipped.push({ path: d.path, reason: 'kavi output vault' });
469
+ continue;
470
+ }
471
+ try {
472
+ knowledge.connect({ vaultPath: d.path, tags: [], include: [] });
473
+ connected.push(d.path);
474
+ } catch (err) {
475
+ skipped.push({ path: d.path, reason: err.message });
476
+ }
477
+ }
478
+ return { connected, skipped };
479
+ }
480
+
481
+ module.exports = {
482
+ discoverKnowledgeSources,
483
+ writeSourceManifestNotes,
484
+ connectDiscoveredSources,
485
+ expandPath,
486
+ isObsidianVault,
487
+ };
@@ -1 +1,155 @@
1
- 'use strict';const deliberation=require('./deliberation'),CRITIQUE_PERSONAS=['arjun','meera','priya','zara'];function extractScore(a){if(!a)return null;const b=a['match'](/(\d+(?:\.\d+)?)\s*\/\s*5/)||a['match'](/Combined score:\s*(\d+(?:\.\d+)?)/i)||a['match'](/Score:\s*\[?(\d+(?:\.\d+)?)/i);return b?Number(b[0x1]):null;}function parseHierarchyCheck(a){if(!a)return'unknown';if(/does not match/i['test'](a))return'does\x20not\x20match';if(/hierarchy check:\s*matches/i['test'](a))return'matches';return'unknown';}function parseVisualHierarchyGrade(a){if(!a)return null;const b=a['match'](/Hierarchy\[([A-F])\]/i);return b?b[0x1]['toUpperCase']():null;}function gradeBelowC(a){if(!a)return![];return['D','E','F']['includes'](a['toUpperCase']());}function verdictFromTotal(a,b){const c=b?deliberation['parseDeliberationOutput'](b):null;if(c?.['verdict']&&['SHIP','REVISE','BLOCK']['includes'](c['verdict']))return c['verdict'];if(a>=0x10)return'SHIP';if(a>=0xa)return'REVISE';return'BLOCK';}function collectRevisions(a){const b=[];for(const [d,e]of Object['entries'](a||{})){const f=e['deliberation']?.['revisions']||[];for(const h of f){b['push']({'change':h,'assigned':d['charAt'](0x0)['toUpperCase']()+d['slice'](0x1)});}const g=(e['text']||'')['match'](/Top 2 fixes:\s*1\.\s*([^\n]+)/i);if(g)b['push']({'change':g[0x1]['trim'](),'assigned':d['charAt'](0x0)['toUpperCase']()+d['slice'](0x1)});}const c=new Set();return b['filter'](j=>{const l=j['change']['slice'](0x0,0x3c);if(c['has'](l))return![];return c['add'](l),!![];});}function buildSynthesis(a,b={}){const c={};let d=0x0,e=0x0;for(const n of CRITIQUE_PERSONAS){const o=a[n];if(!o)continue;if(n==='zara'&&/no delight needed/i['test'](o['text']||'')){c['zara']='N/A';continue;}const p=extractScore(o['text']);p!=null&&(c[n]=p,d+=p,e+=0x1);}const f=e*0x5,g=verdictFromTotal(d,a['raj']?.['text']),h=parseVisualHierarchyGrade(a['arjun']?.['text']),i=parseHierarchyCheck(a['meera']?.['text']),j=gradeBelowC(h)||i==='does\x20not\x20match',k=collectRevisions(a),l=k['slice'](0x0,0x3);if(j&&l['length']){const q='Fix\x20information\x20hierarchy\x20—\x20primary\x20task\x20action\x20must\x20be\x20rank\x20#1\x20on\x20screen';if(!l['some'](r=>/hierarchy/i['test'](r['change']))){l['unshift']({'change':q,'assigned':'Arjun','gate':!![]});if(l['length']>0x3)l['pop']();}}const m=['##\x20Composite\x20Score','Arjun\x20(UX\x20+\x20Visual):\x20'+(c['arjun']??'—'),'Meera\x20(Business):\x20\x20\x20\x20'+(c['meera']??'—'),'Priya\x20(Feasibility):\x20'+(c['priya']??'—'),'Zara\x20(Delight):\x20\x20\x20\x20\x20\x20'+(c['zara']??'—'),'Total:\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'+d+'/'+(f||0x14),'','Verdict:\x20'+g,'','Top\x203\x20actionable\x20changes\x20(ranked\x20by\x20impact):'];l['forEach']((r,t)=>{const u=r['gate']?'\x20—\x20*Inserted\x20by\x20Information\x20Hierarchy\x20Gate*':'';m['push'](t+0x1+'.\x20'+r['change']+'\x20—\x20assigned\x20to\x20'+r['assigned']+u);}),m['push']('','##\x20Information\x20Hierarchy\x20Gate','Arjun\x27s\x20Visual\x20Hierarchy\x20grade:\x20'+(h||'unknown'),'Meera\x27s\x20Hierarchy\x20check:\x20'+i,'Gate\x20status:\x20'+(j?'FAIL':'PASS'));if(a['raj']?.['text']){const r=a['raj']['text']['split']('\x0a')['filter'](t=>!/^## Raj/i['test'](t['trim']()))['slice'](0x0,0x6);m['push']('','##\x20Raj\x20—\x20Stalemate\x20Resolution',...r);}return{'markdown':m['join']('\x0a'),'composite':{'scores':c,'total':d,'max_total':f||0x14,'verdict':g,'top3':l,'hierarchy_gate':{'arjun_visual_hierarchy':h,'meera_hierarchy_check':i,'status':j?'FAIL':'PASS'}}};}module['exports']={'buildSynthesis':buildSynthesis,'extractScore':extractScore,'CRITIQUE_PERSONAS':CRITIQUE_PERSONAS};
1
+ 'use strict';
2
+
3
+ /**
4
+ * Phase 5 composite synthesis — design-critic output block from persona_outputs.
5
+ */
6
+
7
+ const deliberation = require('./deliberation');
8
+
9
+ const CRITIQUE_PERSONAS = ['arjun', 'meera', 'priya', 'zara'];
10
+
11
+ function extractScore(text) {
12
+ if (!text) return null;
13
+ const m = text.match(/(\d+(?:\.\d+)?)\s*\/\s*5/)
14
+ || text.match(/Combined score:\s*(\d+(?:\.\d+)?)/i)
15
+ || text.match(/Score:\s*\[?(\d+(?:\.\d+)?)/i);
16
+ return m ? Number(m[1]) : null;
17
+ }
18
+
19
+ function parseHierarchyCheck(meeraText) {
20
+ if (!meeraText) return 'unknown';
21
+ if (/does not match/i.test(meeraText)) return 'does not match';
22
+ if (/hierarchy check:\s*matches/i.test(meeraText)) return 'matches';
23
+ return 'unknown';
24
+ }
25
+
26
+ function parseVisualHierarchyGrade(arjunText) {
27
+ if (!arjunText) return null;
28
+ const m = arjunText.match(/Hierarchy\[([A-F])\]/i);
29
+ return m ? m[1].toUpperCase() : null;
30
+ }
31
+
32
+ function gradeBelowC(grade) {
33
+ if (!grade) return false;
34
+ return ['D', 'E', 'F'].includes(grade.toUpperCase());
35
+ }
36
+
37
+ function verdictFromTotal(total, rajText) {
38
+ const rajParsed = rajText ? deliberation.parseDeliberationOutput(rajText) : null;
39
+ if (rajParsed?.verdict && ['SHIP', 'REVISE', 'BLOCK'].includes(rajParsed.verdict)) {
40
+ return rajParsed.verdict;
41
+ }
42
+ if (total >= 16) return 'SHIP';
43
+ if (total >= 10) return 'REVISE';
44
+ return 'BLOCK';
45
+ }
46
+
47
+ function collectRevisions(personaOutputs) {
48
+ const items = [];
49
+ for (const [id, out] of Object.entries(personaOutputs || {})) {
50
+ const revs = out.deliberation?.revisions || [];
51
+ for (const r of revs) {
52
+ items.push({ change: r, assigned: id.charAt(0).toUpperCase() + id.slice(1) });
53
+ }
54
+ const fixes = (out.text || '').match(/Top 2 fixes:\s*1\.\s*([^\n]+)/i);
55
+ if (fixes) items.push({ change: fixes[1].trim(), assigned: id.charAt(0).toUpperCase() + id.slice(1) });
56
+ }
57
+ const seen = new Set();
58
+ return items.filter((i) => {
59
+ const k = i.change.slice(0, 60);
60
+ if (seen.has(k)) return false;
61
+ seen.add(k);
62
+ return true;
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Build composite synthesis markdown + structured object.
68
+ */
69
+ function buildSynthesis(personaOutputs, opts = {}) {
70
+ const scores = {};
71
+ let total = 0;
72
+ let counted = 0;
73
+
74
+ for (const id of CRITIQUE_PERSONAS) {
75
+ const out = personaOutputs[id];
76
+ if (!out) continue;
77
+ if (id === 'zara' && /no delight needed/i.test(out.text || '')) {
78
+ scores.zara = 'N/A';
79
+ continue;
80
+ }
81
+ const s = extractScore(out.text);
82
+ if (s != null) {
83
+ scores[id] = s;
84
+ total += s;
85
+ counted += 1;
86
+ }
87
+ }
88
+
89
+ const maxTotal = counted * 5;
90
+ const verdict = verdictFromTotal(total, personaOutputs.raj?.text);
91
+
92
+ const arjunGrade = parseVisualHierarchyGrade(personaOutputs.arjun?.text);
93
+ const hierarchyCheck = parseHierarchyCheck(personaOutputs.meera?.text);
94
+ const hierarchyFail = gradeBelowC(arjunGrade) || hierarchyCheck === 'does not match';
95
+
96
+ const revisions = collectRevisions(personaOutputs);
97
+ const top3 = revisions.slice(0, 3);
98
+
99
+ if (hierarchyFail && top3.length) {
100
+ const gateFix = 'Fix information hierarchy — primary task action must be rank #1 on screen';
101
+ if (!top3.some((t) => /hierarchy/i.test(t.change))) {
102
+ top3.unshift({ change: gateFix, assigned: 'Arjun', gate: true });
103
+ if (top3.length > 3) top3.pop();
104
+ }
105
+ }
106
+
107
+ const lines = [
108
+ '## Composite Score',
109
+ `Arjun (UX + Visual): ${scores.arjun ?? '—'}`,
110
+ `Meera (Business): ${scores.meera ?? '—'}`,
111
+ `Priya (Feasibility): ${scores.priya ?? '—'}`,
112
+ `Zara (Delight): ${scores.zara ?? '—'}`,
113
+ `Total: ${total}/${maxTotal || 20}`,
114
+ '',
115
+ `Verdict: ${verdict}`,
116
+ '',
117
+ 'Top 3 actionable changes (ranked by impact):',
118
+ ];
119
+
120
+ top3.forEach((item, i) => {
121
+ const gateNote = item.gate ? ' — *Inserted by Information Hierarchy Gate*' : '';
122
+ lines.push(`${i + 1}. ${item.change} — assigned to ${item.assigned}${gateNote}`);
123
+ });
124
+
125
+ lines.push(
126
+ '',
127
+ '## Information Hierarchy Gate',
128
+ `Arjun's Visual Hierarchy grade: ${arjunGrade || 'unknown'}`,
129
+ `Meera's Hierarchy check: ${hierarchyCheck}`,
130
+ `Gate status: ${hierarchyFail ? 'FAIL' : 'PASS'}`,
131
+ );
132
+
133
+ if (personaOutputs.raj?.text) {
134
+ const rajLines = personaOutputs.raj.text.split('\n').filter((l) => !/^## Raj/i.test(l.trim())).slice(0, 6);
135
+ lines.push('', '## Raj — Stalemate Resolution', ...rajLines);
136
+ }
137
+
138
+ return {
139
+ markdown: lines.join('\n'),
140
+ composite: {
141
+ scores,
142
+ total,
143
+ max_total: maxTotal || 20,
144
+ verdict,
145
+ top3,
146
+ hierarchy_gate: {
147
+ arjun_visual_hierarchy: arjunGrade,
148
+ meera_hierarchy_check: hierarchyCheck,
149
+ status: hierarchyFail ? 'FAIL' : 'PASS',
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ module.exports = { buildSynthesis, extractScore, CRITIQUE_PERSONAS };