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,385 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';const fs=require('fs'),path=require('path'),os=require('os'),CONFIG_DIR=path['join'](os['homedir'](),'.analyzthis_design'),CONFIG_FILE=path['join'](CONFIG_DIR,'config.json'),KNOWLEDGE_SKILL=path['join'](__dirname,'..','..','skills','knowledge-bank','SKILL.md'),KNOWLEDGE_SLICE_ROOT=path['join'](CONFIG_DIR,'kb-slices'),{resolvePackageRoot}=require('./platforms'),PACKAGE_ROOT=resolvePackageRoot(__dirname),AGENTS_DIR=path['join'](PACKAGE_ROOT,'agents'),CATEGORIES={'prd':['prd','requirements','user\x20story','user\x20stories','acceptance\x20criteria','as\x20a\x20user','as\x20a\x20','given\x20when','given/when','epic','jtbd','job\x20to\x20be\x20done','job-to-be-done','use\x20case','done\x20when','fails\x20when','success\x20criteria','definition\x20of\x20done'],'brand':['brand','color','typography','logo','visual','style','tone','voice','identity'],'product':['product','feature','roadmap','vision','goal','north-star','metric','kpi','okr'],'design':['design','ux','ui','wireframe','component','pattern','system','figma','layout'],'research':['research','user','interview','survey','insight','pain','feedback','analytics','data'],'tech':['tech','stack','api','backend','frontend','infrastructure','constraint','architecture'],'web':['web-context','fetched:','search\x20stub','source:','http://','https://']};function loadConfig(){if(!fs['existsSync'](CONFIG_FILE))return{'sources':[]};try{return JSON['parse'](fs['readFileSync'](CONFIG_FILE,'utf8'));}catch{return{'sources':[]};}}function saveConfig(a){fs['mkdirSync'](CONFIG_DIR,{'recursive':!![]}),fs['writeFileSync'](CONFIG_FILE,JSON['stringify'](a,null,0x2));}function readMarkdownFiles(a){const b=[];if(!fs['existsSync'](a))return b;const c=d=>{for(const e of fs['readdirSync'](d,{'withFileTypes':!![]})){const f=path['join'](d,e['name']);if(e['isDirectory']()&&!e['name']['startsWith']('.'))c(f);else{if(e['isFile']()&&e['name']['endsWith']('.md'))b['push'](f);}}};return c(a),b;}function parseFrontmatter(a){const b=a['match'](/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);if(!b)return{'meta':{},'body':a};const c={};for(const d of b[0x1]['split']('\x0a')){const [e,...f]=d['split'](':');if(e&&f['length'])c[e['trim']()]=f['join'](':')['trim']();}return{'meta':c,'body':b[0x2]};}function extractTags(a,b){const c=[];if(a['tags'])c['push'](...a['tags']['replace'](/[\[\]]/g,'')['split'](',')['map'](e=>e['trim']()['toLowerCase']()));const d=b['match'](/#(\w+)/g)||[];return c['push'](...d['map'](e=>e['slice'](0x1)['toLowerCase']())),[...new Set(c)];}function cleanObsidian(a){return a['replace'](/\[\[([^\]|]+)\|([^\]]+)\]\]/g,'$2')['replace'](/\[\[([^\]]+)\]\]/g,'$1')['replace'](/!\[\[([^\]]+)\]\]/g,'')['replace'](/%%[\s\S]*?%%/g,'')['trim']();}function categorize(a,b,c){const d=(a+'\x20'+b['join']('\x20')+'\x20'+c['substring'](0x0,0x1f4))['toLowerCase']();for(const [e,f]of Object['entries'](CATEGORIES)){if(f['some'](g=>d['includes'](g)))return e;}return'other';}function connect({vaultPath:a,include:include=[],tags:tags=[]}){const b=path['resolve'](a);if(!fs['existsSync'](b))throw new Error('Path\x20does\x20not\x20exist:\x20'+b);const c=loadConfig();return c['sources']=c['sources']['filter'](d=>d['path']!==b),c['sources']['push']({'path':b,'include':include,'tags':tags,'addedAt':new Date()['toISOString']()}),saveConfig(c),b;}function disconnect(a){const b=path['resolve'](a),c=loadConfig();c['sources']=c['sources']['filter'](d=>d['path']!==b),saveConfig(c);}function sync({targets:targets=['cursor']}={}){const a=loadConfig();if(!a['sources']||a['sources']['length']===0x0)return{'synced':0x0,'message':'No\x20sources\x20connected.\x20Run:\x20npx\x20analyzthis_design\x20connect\x20--vault\x20/path/to/vault'};const b={'prd':[],'brand':[],'product':[],'design':[],'research':[],'tech':[],'web':[],'other':[]};let c=0x0;for(const m of a['sources']){const n=readMarkdownFiles(m['path']);for(const o of n){const p=fs['readFileSync'](o,'utf8'),{meta:q,body:r}=parseFrontmatter(p),s=extractTags(q,r);if(m['tags']['length']>0x0&&!m['tags']['some'](x=>s['includes'](x['toLowerCase']())))continue;if(m['include']['length']>0x0){const x=path['relative'](m['path'],o),y=m['include']['some'](z=>x['startsWith'](z['replace']('/**','')['replace']('/*','')));if(!y)continue;}const u=q['title']||path['basename'](o,'.md'),v=cleanObsidian(r);if(!v['trim']())continue;const w=categorize(u,s,v);b[w]['push']({'title':u,'content':v}),c++;}}try{const z=require('./research'),A=z['readWebContext']();A&&A['content']&&A['content']['trim']()&&(b['web']['push']({'title':'Session\x20web\x20research','content':A['content']}),c++);}catch{}const d=a['sources']['map'](B=>B['path'])['join'](',\x20'),e=new Date()['toISOString']()['split']('T')[0x0],f=[{'key':'prd','heading':'##\x20PRDs,\x20User\x20Stories\x20&\x20Acceptance\x20Criteria'},{'key':'brand','heading':'##\x20Brand\x20&\x20Design\x20Guidelines'},{'key':'product','heading':'##\x20Product\x20Context'},{'key':'design','heading':'##\x20Design\x20Decisions\x20&\x20Patterns'},{'key':'research','heading':'##\x20Research\x20&\x20User\x20Insights'},{'key':'web','heading':'##\x20Web\x20Research\x20Context'},{'key':'tech','heading':'##\x20Technical\x20Context'},{'key':'other','heading':'##\x20Additional\x20Context'}];let g='---\x0aname:\x20knowledge-bank\x0adescription:\x20Personal\x20knowledge\x20bank\x20—\x20takes\x20precedence\x20over\x20all\x20built-in\x20persona\x20defaults.\x0adisable-model-invocation:\x20true\x0a---\x0a\x0a#\x20Knowledge\x20Bank\x0a\x0a>\x20Last\x20synced:\x20'+e+'\x0a>\x20Sources:\x20'+d+'\x0a>\x20Files\x20loaded:\x20'+c+'\x0a\x0a**INSTRUCTION\x20TO\x20ALL\x20PERSONAS:**\x20This\x20knowledge\x20bank\x20contains\x20project-specific\x20context\x20that\x20overrides\x20your\x20built-in\x20defaults.\x20Read\x20every\x20section\x20below\x20before\x20forming\x20any\x20opinion.\x20When\x20this\x20knowledge\x20bank\x20conflicts\x20with\x20your\x20built-in\x20knowledge,\x20this\x20knowledge\x20bank\x20wins.\x0a\x0a---\x0a\x0a',h=![];for(const {key:B,heading:C}of f){if(b[B]['length']===0x0)continue;h=!![],g+=C+'\x0a\x0a';for(const D of b[B]){g+='###\x20'+D['title']+'\x0a\x0a'+D['content']+'\x0a\x0a';}g+='---\x0a\x0a';}!h&&(g+='_No\x20matching\x20files\x20found.\x20Check\x20your\x20--tags\x20or\x20--include\x20filters,\x20or\x20remove\x20filters\x20to\x20include\x20all\x20notes._\x0a');fs['mkdirSync'](path['dirname'](KNOWLEDGE_SKILL),{'recursive':!![]}),fs['writeFileSync'](KNOWLEDGE_SKILL,g);try{const E=require('./session'),F=E['getProjectId']();writePersonaSlices(b,f,F);}catch{}const {TARGETS:i,resolveTargets:j}=require('./platforms'),k=[],l=targets['includes']('all')?j('all'):targets['filter'](G=>i[G]);for(const G of l){const H=i[G],I=[{'root':H['root'],'layout':H['layout']}];if(H['also'])I['push']({'root':H['also']['root'],'layout':H['also']['layout']});for(const J of I){fs['mkdirSync'](J['root'],{'recursive':!![]});if(J['layout']==='dir'){const K=path['join'](J['root'],'knowledge-bank');fs['mkdirSync'](K,{'recursive':!![]}),fs['copyFileSync'](KNOWLEDGE_SKILL,path['join'](K,'SKILL.md'));}else fs['copyFileSync'](KNOWLEDGE_SKILL,path['join'](J['root'],'knowledge-bank.md'));k['push'](G+'\x20→\x20'+J['root']);}}a['lastSync']=new Date()['toISOString'](),saveConfig(a);try{require('./cache')['invalidatePrefix']('kb:');}catch{}return{'synced':c,'copiedTo':k};}function loadManifest(a){const b=path['join'](AGENTS_DIR,'manifests',a+'.json');if(!fs['existsSync'](b))return null;return JSON['parse'](fs['readFileSync'](b,'utf8'));}function personaRelevance(a,b){if(!b||!b['length'])return!![];return b['indexOf'](a)!==-0x1;}function buildPersonaSlice(a,b,c,d){const e=loadManifest(a),f=e&&e['knowledge_categories']?e['knowledge_categories']:[];let g='---\x0aname:\x20knowledge-bank-'+a+'\x0adescription:\x20Project\x20context\x20filtered\x20for\x20the\x20'+a+'\x20persona.\x0adisable-model-invocation:\x20true\x0a---\x0a\x0a#\x20'+a['toUpperCase']()+'\x20Knowledge\x20Slice\x0a\x0a**INSTRUCTION:**\x20This\x20slice\x20contains\x20the\x20notes\x20most\x20relevant\x20to\x20your\x20lens.\x20Read\x20it\x20first,\x20then\x20scan\x20the\x20Additional\x20Context\x20section\x20briefly.\x0a\x0a---\x0a\x0a',h=![];for(const {key:j,heading:k}of c){if(!personaRelevance(j,f))continue;if(b[j]['length']===0x0)continue;h=!![],g+=k+'\x0a\x0a';for(const l of b[j]){g+='###\x20'+l['title']+'\x0a\x0a'+l['content']+'\x0a\x0a';}g+='---\x0a\x0a';}!h&&(g+='_No\x20primary\x20notes\x20for\x20your\x20lens.\x20Reading\x20general\x20context\x20below._\x0a\x0a');g+='##\x20Additional\x20Context\x20(secondary)\x0a\x0a';let i=0x0;for(const {key:m,heading:n}of c){if(personaRelevance(m,f))continue;if(b[m]['length']===0x0)continue;g+=n+'\x0a\x0a';for(const o of b[m]['slice'](0x0,0x2)){g+='###\x20'+o['title']+'\x0a\x0a'+o['content']['slice'](0x0,0x258)+(o['content']['length']>0x258?'…':'')+'\x0a\x0a',i++;if(i>=0x2)break;}g+='---\x0a\x0a';if(i>=0x2)break;}return g;}function writePersonaSlices(a,b,c){const d=['arjun','meera','priya','zara','noor','anuj','raj'],e=path['join'](KNOWLEDGE_SLICE_ROOT,c||'default');fs['mkdirSync'](e,{'recursive':!![]});const f=[];for(const g of d){const h=buildPersonaSlice(g,a,b,c),i=path['join'](e,g+'.md');fs['writeFileSync'](i,h),f['push'](i);}return f;}function readPersonaSlice(a,b){const c=path['join'](KNOWLEDGE_SLICE_ROOT,a||'default',b+'.md');if(!fs['existsSync'](c))return[];const d=fs['readFileSync'](c,'utf8'),e=[],f=d['match'](/### (.*?)\n\n([\s\S]*?)(?=\n\n---|\n### |$)/g);if(f)for(const g of f){const h=g['match'](/### (.*?)\n\n/);if(!h)continue;e['push']({'title':h[0x1]['trim'](),'content':g['replace'](/### .*?\n\n/,'')['trim']()});}return e;}function getPersonaSliceForPrompt(a,b){const c=a&&a['project_id']?a['project_id']:'default',d='kb:'+c+':'+b+':slice',e=require('./cache')['get'](d);if(e)return e;const f=readPersonaSlice(c,b);return require('./cache')['set'](d,f),f;}function status(){return loadConfig();}module['exports']={'connect':connect,'disconnect':disconnect,'sync':sync,'status':status,'buildPersonaSlice':buildPersonaSlice,'writePersonaSlices':writePersonaSlices,'readPersonaSlice':readPersonaSlice,'getPersonaSliceForPrompt':getPersonaSliceForPrompt,'KNOWLEDGE_SLICE_ROOT':KNOWLEDGE_SLICE_ROOT};
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const CONFIG_DIR = path.join(os.homedir(), '.analyzthis_design');
9
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
10
+ // When published, this file lives at dist/lib/ — go up two levels to reach package root
11
+ const KNOWLEDGE_SKILL = path.join(__dirname, '..', '..', 'skills', 'knowledge-bank', 'SKILL.md');
12
+ const KNOWLEDGE_SLICE_ROOT = path.join(CONFIG_DIR, 'kb-slices');
13
+
14
+ const { resolvePackageRoot } = require('./platforms');
15
+ const PACKAGE_ROOT = resolvePackageRoot(__dirname);
16
+ const AGENTS_DIR = path.join(PACKAGE_ROOT, 'agents');
17
+
18
+ // Keywords used to auto-group vault notes into categories
19
+ const CATEGORIES = {
20
+ // PRD/stories must be checked first — highest priority for ux-story-gate
21
+ prd: ['prd', 'requirements', 'user story', 'user stories', 'acceptance criteria', 'as a user',
22
+ 'as a ', 'given when', 'given/when', 'epic', 'jtbd', 'job to be done', 'job-to-be-done',
23
+ 'use case', 'done when', 'fails when', 'success criteria', 'definition of done'],
24
+ brand: ['brand', 'color', 'typography', 'logo', 'visual', 'style', 'tone', 'voice', 'identity'],
25
+ product: ['product', 'feature', 'roadmap', 'vision', 'goal', 'north-star', 'metric', 'kpi', 'okr'],
26
+ design: ['design', 'ux', 'ui', 'wireframe', 'component', 'pattern', 'system', 'figma', 'layout'],
27
+ research: ['research', 'user', 'interview', 'survey', 'insight', 'pain', 'feedback', 'analytics', 'data'],
28
+ tech: ['tech', 'stack', 'api', 'backend', 'frontend', 'infrastructure', 'constraint', 'architecture'],
29
+ web: ['web-context', 'fetched:', 'search stub', 'source:', 'http://', 'https://'],
30
+ };
31
+
32
+ // ─── Config helpers ──────────────────────────────────────────────────────────
33
+
34
+ function loadConfig() {
35
+ if (!fs.existsSync(CONFIG_FILE)) return { sources: [] };
36
+ try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
37
+ catch { return { sources: [] }; }
38
+ }
39
+
40
+ function saveConfig(config) {
41
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
42
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
43
+ }
44
+
45
+ // ─── Vault reading ───────────────────────────────────────────────────────────
46
+
47
+ // Recursively collect all .md files under a directory
48
+ function readMarkdownFiles(dir) {
49
+ const files = [];
50
+ if (!fs.existsSync(dir)) return files;
51
+ const walk = (cur) => {
52
+ for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
53
+ const full = path.join(cur, entry.name);
54
+ if (entry.isDirectory() && !entry.name.startsWith('.')) walk(full);
55
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(full);
56
+ }
57
+ };
58
+ walk(dir);
59
+ return files;
60
+ }
61
+
62
+ // Parse YAML-ish frontmatter from a markdown file
63
+ function parseFrontmatter(raw) {
64
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
65
+ if (!m) return { meta: {}, body: raw };
66
+ const meta = {};
67
+ for (const line of m[1].split('\n')) {
68
+ const [k, ...v] = line.split(':');
69
+ if (k && v.length) meta[k.trim()] = v.join(':').trim();
70
+ }
71
+ return { meta, body: m[2] };
72
+ }
73
+
74
+ // Collect tags from frontmatter and inline #hashtags
75
+ function extractTags(meta, body) {
76
+ const tags = [];
77
+ if (meta.tags) tags.push(...meta.tags.replace(/[\[\]]/g, '').split(',').map(t => t.trim().toLowerCase()));
78
+ const inline = body.match(/#(\w+)/g) || [];
79
+ tags.push(...inline.map(t => t.slice(1).toLowerCase()));
80
+ return [...new Set(tags)];
81
+ }
82
+
83
+ // Strip Obsidian-specific syntax so the AI reads clean markdown
84
+ function cleanObsidian(text) {
85
+ return text
86
+ .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2') // [[note|alias]] → alias
87
+ .replace(/\[\[([^\]]+)\]\]/g, '$1') // [[note]] → note text
88
+ .replace(/!\[\[([^\]]+)\]\]/g, '') // ![[embed]] → remove
89
+ .replace(/%%[\s\S]*?%%/g, '') // %% comments %% → remove
90
+ .trim();
91
+ }
92
+
93
+ // Map a note to a section category
94
+ function categorize(title, tags, body) {
95
+ const text = `${title} ${tags.join(' ')} ${body.substring(0, 500)}`.toLowerCase();
96
+ for (const [cat, keywords] of Object.entries(CATEGORIES)) {
97
+ if (keywords.some(k => text.includes(k))) return cat;
98
+ }
99
+ return 'other';
100
+ }
101
+
102
+ // ─── Public API ──────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Register a vault or folder as a knowledge source.
106
+ * Options:
107
+ * include — array of sub-folder prefixes to include, e.g. ['Design', 'Brand']
108
+ * tags — array of tags to filter by, e.g. ['ux', 'design', 'brand']
109
+ */
110
+ function connect({ vaultPath, include = [], tags = [] }) {
111
+ const abs = path.resolve(vaultPath);
112
+ if (!fs.existsSync(abs)) throw new Error(`Path does not exist: ${abs}`);
113
+
114
+ const config = loadConfig();
115
+ // Replace any existing entry with the same path
116
+ config.sources = config.sources.filter(s => s.path !== abs);
117
+ config.sources.push({ path: abs, include, tags, addedAt: new Date().toISOString() });
118
+ saveConfig(config);
119
+ return abs;
120
+ }
121
+
122
+ /**
123
+ * Remove a vault/folder from the knowledge sources list.
124
+ */
125
+ function disconnect(vaultPath) {
126
+ const abs = path.resolve(vaultPath);
127
+ const config = loadConfig();
128
+ config.sources = config.sources.filter(s => s.path !== abs);
129
+ saveConfig(config);
130
+ }
131
+
132
+ /**
133
+ * Read all connected sources, filter notes, build knowledge-bank.md,
134
+ * and copy it to all requested target AI tool directories.
135
+ *
136
+ * targets — array of tool names: 'cursor', 'claude', 'codex'
137
+ */
138
+ function sync({ targets = ['cursor'] } = {}) {
139
+ const config = loadConfig();
140
+
141
+ if (!config.sources || config.sources.length === 0) {
142
+ return { synced: 0, message: 'No sources connected. Run: npx analyzthis_design connect --vault /path/to/vault' };
143
+ }
144
+
145
+ const sections = { prd: [], brand: [], product: [], design: [], research: [], tech: [], web: [], other: [] };
146
+ let totalFiles = 0;
147
+
148
+ for (const source of config.sources) {
149
+ const files = readMarkdownFiles(source.path);
150
+
151
+ for (const filePath of files) {
152
+ const raw = fs.readFileSync(filePath, 'utf8');
153
+ const { meta, body } = parseFrontmatter(raw);
154
+ const tags = extractTags(meta, body);
155
+
156
+ // Tag filter: skip file if it doesn't carry one of the required tags
157
+ if (source.tags.length > 0 && !source.tags.some(t => tags.includes(t.toLowerCase()))) continue;
158
+
159
+ // Include-folder filter: skip file if it's not inside one of the allowed sub-paths
160
+ if (source.include.length > 0) {
161
+ const rel = path.relative(source.path, filePath);
162
+ const ok = source.include.some(p => rel.startsWith(p.replace('/**', '').replace('/*', '')));
163
+ if (!ok) continue;
164
+ }
165
+
166
+ const title = meta.title || path.basename(filePath, '.md');
167
+ const cleaned = cleanObsidian(body);
168
+ if (!cleaned.trim()) continue;
169
+
170
+ const cat = categorize(title, tags, cleaned);
171
+ sections[cat].push({ title, content: cleaned });
172
+ totalFiles++;
173
+ }
174
+ }
175
+
176
+ // Merge session web-context.md research artifacts (if any) into the web section
177
+ try {
178
+ const research = require('./research');
179
+ const web = research.readWebContext();
180
+ if (web && web.content && web.content.trim()) {
181
+ sections.web.push({ title: 'Session web research', content: web.content });
182
+ totalFiles++;
183
+ }
184
+ } catch { /* no session / research available — fine */ }
185
+
186
+ // Build the knowledge-bank markdown
187
+ const sourceList = config.sources.map(s => s.path).join(', ');
188
+ const date = new Date().toISOString().split('T')[0];
189
+
190
+ // PRD/stories listed first — ux-story-gate reads this section in Phase 0
191
+ const sectionDefs = [
192
+ { key: 'prd', heading: '## PRDs, User Stories & Acceptance Criteria' },
193
+ { key: 'brand', heading: '## Brand & Design Guidelines' },
194
+ { key: 'product', heading: '## Product Context' },
195
+ { key: 'design', heading: '## Design Decisions & Patterns' },
196
+ { key: 'research', heading: '## Research & User Insights' },
197
+ { key: 'web', heading: '## Web Research Context' },
198
+ { key: 'tech', heading: '## Technical Context' },
199
+ { key: 'other', heading: '## Additional Context' },
200
+ ];
201
+
202
+ let md = `---
203
+ name: knowledge-bank
204
+ description: Personal knowledge bank — takes precedence over all built-in persona defaults.
205
+ disable-model-invocation: true
206
+ ---
207
+
208
+ # Knowledge Bank
209
+
210
+ > Last synced: ${date}
211
+ > Sources: ${sourceList}
212
+ > Files loaded: ${totalFiles}
213
+
214
+ **INSTRUCTION TO ALL PERSONAS:** This knowledge bank contains project-specific context that overrides your built-in defaults. Read every section below before forming any opinion. When this knowledge bank conflicts with your built-in knowledge, this knowledge bank wins.
215
+
216
+ ---
217
+
218
+ `;
219
+
220
+ let hasContent = false;
221
+ for (const { key, heading } of sectionDefs) {
222
+ if (sections[key].length === 0) continue;
223
+ hasContent = true;
224
+ md += `${heading}\n\n`;
225
+ for (const note of sections[key]) {
226
+ md += `### ${note.title}\n\n${note.content}\n\n`;
227
+ }
228
+ md += '---\n\n';
229
+ }
230
+
231
+ if (!hasContent) {
232
+ md += `_No matching files found. Check your --tags or --include filters, or remove filters to include all notes._\n`;
233
+ }
234
+
235
+ // Write to the package's own skills/knowledge-bank/SKILL.md
236
+ fs.mkdirSync(path.dirname(KNOWLEDGE_SKILL), { recursive: true });
237
+ fs.writeFileSync(KNOWLEDGE_SKILL, md);
238
+
239
+ // Build per-persona knowledge slices (priority + fallback)
240
+ try {
241
+ const session = require('./session');
242
+ const projectId = session.getProjectId();
243
+ writePersonaSlices(sections, sectionDefs, projectId);
244
+ } catch {
245
+ // sessions unavailable — skip slices
246
+ }
247
+
248
+ // Copy knowledge-bank into every requested platform (dir and/or flat layout)
249
+ const { TARGETS, resolveTargets } = require('./platforms');
250
+ const copiedTo = [];
251
+ const resolved = targets.includes('all')
252
+ ? resolveTargets('all')
253
+ : targets.filter((t) => TARGETS[t]);
254
+
255
+ for (const targetId of resolved) {
256
+ const t = TARGETS[targetId];
257
+ const destinations = [{ root: t.root, layout: t.layout }];
258
+ if (t.also) destinations.push({ root: t.also.root, layout: t.also.layout });
259
+
260
+ for (const dest of destinations) {
261
+ fs.mkdirSync(dest.root, { recursive: true });
262
+ if (dest.layout === 'dir') {
263
+ const skillDir = path.join(dest.root, 'knowledge-bank');
264
+ fs.mkdirSync(skillDir, { recursive: true });
265
+ fs.copyFileSync(KNOWLEDGE_SKILL, path.join(skillDir, 'SKILL.md'));
266
+ } else {
267
+ fs.copyFileSync(KNOWLEDGE_SKILL, path.join(dest.root, 'knowledge-bank.md'));
268
+ }
269
+ copiedTo.push(`${targetId} → ${dest.root}`);
270
+ }
271
+ }
272
+
273
+ // Persist lastSync timestamp
274
+ config.lastSync = new Date().toISOString();
275
+ saveConfig(config);
276
+
277
+ // Knowledge bank content just changed for every project — drop any cached
278
+ // knowledge-bank slices so the next run re-reads the fresh sync.
279
+ try { require('./cache').invalidatePrefix('kb:'); } catch { /* cache module unavailable — fine */ }
280
+
281
+ return { synced: totalFiles, copiedTo };
282
+ }
283
+
284
+ function loadManifest(id) {
285
+ const p = path.join(AGENTS_DIR, 'manifests', `${id}.json`);
286
+ if (!fs.existsSync(p)) return null;
287
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
288
+ }
289
+
290
+ function personaRelevance(category, personaCategories) {
291
+ if (!personaCategories || !personaCategories.length) return true;
292
+ return personaCategories.indexOf(category) !== -1;
293
+ }
294
+
295
+ function buildPersonaSlice(personaId, sections, sectionDefs, projectId) {
296
+ const manifest = loadManifest(personaId);
297
+ const cats = manifest && manifest.knowledge_categories ? manifest.knowledge_categories : [];
298
+ let md = `---\nname: knowledge-bank-${personaId}\ndescription: Project context filtered for the ${personaId} persona.\ndisable-model-invocation: true\n---\n\n# ${personaId.toUpperCase()} Knowledge Slice\n\n**INSTRUCTION:** This slice contains the notes most relevant to your lens. Read it first, then scan the Additional Context section briefly.\n\n---\n\n`;
299
+
300
+ let hasPrimary = false;
301
+ for (const { key, heading } of sectionDefs) {
302
+ if (!personaRelevance(key, cats)) continue;
303
+ if (sections[key].length === 0) continue;
304
+ hasPrimary = true;
305
+ md += `${heading}\n\n`;
306
+ for (const note of sections[key]) {
307
+ md += `### ${note.title}\n\n${note.content}\n\n`;
308
+ }
309
+ md += '---\n\n';
310
+ }
311
+
312
+ if (!hasPrimary) {
313
+ md += '_No primary notes for your lens. Reading general context below._\n\n';
314
+ }
315
+
316
+ md += '## Additional Context (secondary)\n\n';
317
+ let secondaryCount = 0;
318
+ for (const { key, heading } of sectionDefs) {
319
+ if (personaRelevance(key, cats)) continue;
320
+ if (sections[key].length === 0) continue;
321
+ md += `${heading}\n\n`;
322
+ for (const note of sections[key].slice(0, 2)) {
323
+ md += `### ${note.title}\n\n${note.content.slice(0, 600)}${note.content.length > 600 ? '…' : ''}\n\n`;
324
+ secondaryCount++;
325
+ if (secondaryCount >= 2) break;
326
+ }
327
+ md += '---\n\n';
328
+ if (secondaryCount >= 2) break;
329
+ }
330
+
331
+ return md;
332
+ }
333
+
334
+ function writePersonaSlices(sections, sectionDefs, projectId) {
335
+ const personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
336
+ const dir = path.join(KNOWLEDGE_SLICE_ROOT, projectId || 'default');
337
+ fs.mkdirSync(dir, { recursive: true });
338
+ const written = [];
339
+ for (const personaId of personas) {
340
+ const md = buildPersonaSlice(personaId, sections, sectionDefs, projectId);
341
+ const file = path.join(dir, `${personaId}.md`);
342
+ fs.writeFileSync(file, md);
343
+ written.push(file);
344
+ }
345
+ return written;
346
+ }
347
+
348
+ function readPersonaSlice(projectId, personaId) {
349
+ const file = path.join(KNOWLEDGE_SLICE_ROOT, projectId || 'default', `${personaId}.md`);
350
+ if (!fs.existsSync(file)) return [];
351
+ const text = fs.readFileSync(file, 'utf8');
352
+ const notes = [];
353
+ const matches = text.match(/### (.*?)\n\n([\s\S]*?)(?=\n\n---|\n### |$)/g);
354
+ if (matches) {
355
+ for (const m of matches) {
356
+ const titleMatch = m.match(/### (.*?)\n\n/);
357
+ if (!titleMatch) continue;
358
+ notes.push({
359
+ title: titleMatch[1].trim(),
360
+ content: m.replace(/### .*?\n\n/, '').trim(),
361
+ });
362
+ }
363
+ }
364
+ return notes;
365
+ }
366
+
367
+ function getPersonaSliceForPrompt(state, personaId) {
368
+ const projectId = state && state.project_id ? state.project_id : 'default';
369
+ const cacheKey = `kb:${projectId}:${personaId}:slice`;
370
+ const cached = require('./cache').get(cacheKey);
371
+ if (cached) return cached;
372
+
373
+ const notes = readPersonaSlice(projectId, personaId);
374
+ require('./cache').set(cacheKey, notes);
375
+ return notes;
376
+ }
377
+
378
+ /**
379
+ * Return current config (sources list, lastSync).
380
+ */
381
+ function status() {
382
+ return loadConfig();
383
+ }
384
+
385
+ module.exports = { connect, disconnect, sync, status, buildPersonaSlice, writePersonaSlices, readPersonaSlice, getPersonaSliceForPrompt, KNOWLEDGE_SLICE_ROOT };
@@ -1 +1,217 @@
1
- 'use strict';var fs=require('fs'),path=require('path'),os=require('os'),session=require('./session'),cache=require('./cache'),LESSONS_ROOT=path['join'](os['homedir'](),'.analyzthis_design','lessons');function ensureLessonsDir(){fs['mkdirSync'](LESSONS_ROOT,{'recursive':!![]});}function personaLessonFile(a){return path['join'](LESSONS_ROOT,a+'.jsonl');}function loadLessons(a){var b=personaLessonFile(a);if(!fs['existsSync'](b))return[];var c=fs['readFileSync'](b,'utf8'),d=c['trim']()['split']('\x0a')['filter'](function(h){return h['trim']();}),f=[];for(var g=0x0;g<d['length'];g++){try{f['push'](JSON['parse'](d[g]));}catch(h){}}return f;}function appendLesson(a,b){ensureLessonsDir();var c=personaLessonFile(a);fs['appendFileSync'](c,JSON['stringify'](b)+'\x0a');}function keywordOverlap(c,d){var e=(c||'')['toLowerCase']()['split'](/\W+/)['filter'](function(m){return m['length']>0x2;}),f=(d||'')['toLowerCase']()['split'](/\W+/)['filter'](function(m){return m['length']>0x2;}),g={};for(var h=0x0;h<e['length'];h++)g[e[h]]=!![];var k=0x0;for(var l=0x0;l<f['length'];l++){if(g[f[l]])k++;}return k;}function extractLessonsFromSession(a,b){var c=a['persona_outputs']&&a['persona_outputs'][b];if(!c||c['accepted']!==!![])return[];var d='unknown';if(a['outcome']&&a['outcome']['confirmed']&&a['outcome']['confirmed'][b])d=a['outcome']['confirmed'][b]['value'];else a['outcome']&&a['outcome']['inferred']&&a['outcome']['inferred'][b]&&(d=a['outcome']['inferred'][b]['value']);var e=[],f=c['text']||'',g=f['match'](/Top\s*2\s*fixes:[\s\S]*?(?:\n\n|```|$)/i);if(g){var h=g[0x0]['split']('\x0a');for(var k=0x0;k<h['length'];k++){var l=h[k]['trim'](),n=l['match'](/^\d+\.\s*(.+)$/);if(n){var o=n[0x1]['trim'](),p=o;e['push']({'id':require('crypto')['randomBytes'](0x6)['toString']('hex'),'persona':b,'task_type':a['task_type']||'','pattern':p,'fix':o,'outcome':d,'session_id':a['project_id'],'extracted_at':new Date()['toISOString'](),'citation':c['citations']?c['citations'][0x0]:''});}}}var q=f['match'](/Hierarchy\[([A-F])\]/gi);if(q)for(var r=0x0;r<q['length'];r++){var s='Visual\x20hierarchy\x20issue:\x20'+q[r];e['push']({'id':require('crypto')['randomBytes'](0x6)['toString']('hex'),'persona':b,'task_type':a['task_type']||'','pattern':s,'fix':'Review\x20visual\x20hierarchy\x20per\x20DS\x20tokens','outcome':d,'session_id':a['project_id'],'extracted_at':new Date()['toISOString'](),'citation':''});}return e;}function extractLessons(a){var b=a['project'],c=a['persona'],d=b?[b]:session['listProjects'](),e=0x0;for(var f=0x0;f<d['length'];f++){var g=d[f],h=session['show']({'project':g});if(!h)continue;var k=extractLessonsFromSession(h,c);for(var l=0x0;l<k['length'];l++){appendLesson(c,k[l]),e++;}}return{'extracted':e,'persona':c};}function extractAllLessons(a){var b=a['windowDays']||0x7,c=Date['now']()-b*0x18*0x3c*0x3c*0x3e8,d=session['listProjects'](),e=0x0,f=['arjun','meera','priya','zara','noor','anuj','raj'];for(var g=0x0;g<d['length'];g++){var h=d[g],k=session['show']({'project':h});if(!k)continue;if(k['updated_at']&&new Date(k['updated_at'])['getTime']()<c)continue;for(var l=0x0;l<f['length'];l++){var m=f[l],n=extractLessonsFromSession(k,m);for(var o=0x0;o<n['length'];o++){appendLesson(m,n[o]),e++;}}}return{'extracted':e,'windowDays':b};}function retrieveLessons(a){var b=a['task']||'',c=a['personaId'],d=a['limit']||0x3;if(!c)return{'lessons':[],'cacheHit':![]};var e='lessons:'+c+':'+require('crypto')['createHash']('sha1')['update'](b)['digest']('hex')['slice'](0x0,0xc),f=cache['get'](e);if(f)return{'lessons':f,'cacheHit':!![]};var g=loadLessons(c);if(!g['length'])return{'lessons':[],'cacheHit':![]};var h=g['map'](function(j){var k=keywordOverlap(b,j['pattern']+'\x20'+j['fix']+'\x20'+j['task_type']);return{'lesson':j,'score':k};});h['sort'](function(j,k){return k['score']-j['score'];});var i=h['slice'](0x0,d)['map'](function(j){return j['lesson'];});return cache['set'](e,i),{'lessons':i,'cacheHit':![]};}function buildLessonsInjection(a){if(!a||!a['length'])return'';var b=['Past\x20lessons\x20for\x20this\x20persona\x20on\x20similar\x20tasks:'];for(var c=0x0;c<a['length'];c++){var d=a[c];b['push'](c+0x1+'.\x20['+d['pattern']+']\x20\u2192\x20['+d['fix']+']\x20(outcome:\x20'+d['outcome']+')');}return b['join']('\x0a');}function stats(){var a=['arjun','meera','priya','zara','noor','anuj','raj'],b={},c=0x0;for(var d=0x0;d<a['length'];d++){var e=a[d],f=loadLessons(e)['length'];b[e]=f,c+=f;}return{'totalLessons':c,'perPersona':b};}module['exports']={'extractLessons':extractLessons,'extractAllLessons':extractAllLessons,'retrieveLessons':retrieveLessons,'buildLessonsInjection':buildLessonsInjection,'stats':stats,'loadLessons':loadLessons,'LESSONS_ROOT':LESSONS_ROOT};
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var os = require('os');
6
+ var session = require('./session');
7
+ var cache = require('./cache');
8
+
9
+ var LESSONS_ROOT = path.join(os.homedir(), '.analyzthis_design', 'lessons');
10
+
11
+ function ensureLessonsDir() {
12
+ fs.mkdirSync(LESSONS_ROOT, { recursive: true });
13
+ }
14
+
15
+ function personaLessonFile(personaId) {
16
+ return path.join(LESSONS_ROOT, personaId + '.jsonl');
17
+ }
18
+
19
+ function loadLessons(personaId) {
20
+ var file = personaLessonFile(personaId);
21
+ if (!fs.existsSync(file)) return [];
22
+ var text = fs.readFileSync(file, 'utf8');
23
+ var lines = text.trim().split('\n').filter(function(l) { return l.trim(); });
24
+ var lessons = [];
25
+ for (var i = 0; i < lines.length; i++) {
26
+ try {
27
+ lessons.push(JSON.parse(lines[i]));
28
+ } catch (e) {
29
+ // skip corrupt lines
30
+ }
31
+ }
32
+ return lessons;
33
+ }
34
+
35
+ function appendLesson(personaId, lesson) {
36
+ ensureLessonsDir();
37
+ var file = personaLessonFile(personaId);
38
+ fs.appendFileSync(file, JSON.stringify(lesson) + '\n');
39
+ }
40
+
41
+ function keywordOverlap(a, b) {
42
+ var wordsA = (a || '').toLowerCase().split(/\W+/).filter(function(w) { return w.length > 2; });
43
+ var wordsB = (b || '').toLowerCase().split(/\W+/).filter(function(w) { return w.length > 2; });
44
+ var setA = {};
45
+ for (var i = 0; i < wordsA.length; i++) setA[wordsA[i]] = true;
46
+ var overlap = 0;
47
+ for (var j = 0; j < wordsB.length; j++) {
48
+ if (setA[wordsB[j]]) overlap++;
49
+ }
50
+ return overlap;
51
+ }
52
+
53
+ function extractLessonsFromSession(state, personaId) {
54
+ var entry = state.persona_outputs && state.persona_outputs[personaId];
55
+ if (!entry || entry.accepted !== true) return [];
56
+
57
+ var outcomeVal = 'unknown';
58
+ if (state.outcome && state.outcome.confirmed && state.outcome.confirmed[personaId]) {
59
+ outcomeVal = state.outcome.confirmed[personaId].value;
60
+ } else if (state.outcome && state.outcome.inferred && state.outcome.inferred[personaId]) {
61
+ outcomeVal = state.outcome.inferred[personaId].value;
62
+ }
63
+
64
+ var lessons = [];
65
+ var text = entry.text || '';
66
+
67
+ var fixesMatch = text.match(/Top\s*2\s*fixes:[\s\S]*?(?:\n\n|```|$)/i);
68
+ if (fixesMatch) {
69
+ var lines = fixesMatch[0].split('\n');
70
+ for (var i = 0; i < lines.length; i++) {
71
+ var line = lines[i].trim();
72
+ var m = line.match(/^\d+\.\s*(.+)$/);
73
+ if (m) {
74
+ var fix = m[1].trim();
75
+ var pattern = fix;
76
+ lessons.push({
77
+ id: require('crypto').randomBytes(6).toString('hex'),
78
+ persona: personaId,
79
+ task_type: state.task_type || '',
80
+ pattern: pattern,
81
+ fix: fix,
82
+ outcome: outcomeVal,
83
+ session_id: state.project_id,
84
+ extracted_at: new Date().toISOString(),
85
+ citation: entry.citations ? entry.citations[0] : ''
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ var hierarchyMatch = text.match(/Hierarchy\[([A-F])\]/gi);
92
+ if (hierarchyMatch) {
93
+ for (var j = 0; j < hierarchyMatch.length; j++) {
94
+ var pattern2 = 'Visual hierarchy issue: ' + hierarchyMatch[j];
95
+ lessons.push({
96
+ id: require('crypto').randomBytes(6).toString('hex'),
97
+ persona: personaId,
98
+ task_type: state.task_type || '',
99
+ pattern: pattern2,
100
+ fix: 'Review visual hierarchy per DS tokens',
101
+ outcome: outcomeVal,
102
+ session_id: state.project_id,
103
+ extracted_at: new Date().toISOString(),
104
+ citation: ''
105
+ });
106
+ }
107
+ }
108
+
109
+ return lessons;
110
+ }
111
+
112
+ function extractLessons(opts) {
113
+ var project = opts.project;
114
+ var persona = opts.persona;
115
+
116
+ var projectIds = project ? [project] : session.listProjects();
117
+ var total = 0;
118
+
119
+ for (var i = 0; i < projectIds.length; i++) {
120
+ var pid = projectIds[i];
121
+ var state = session.show({ project: pid });
122
+ if (!state) continue;
123
+
124
+ var lessons = extractLessonsFromSession(state, persona);
125
+ for (var j = 0; j < lessons.length; j++) {
126
+ appendLesson(persona, lessons[j]);
127
+ total++;
128
+ }
129
+ }
130
+
131
+ return { extracted: total, persona: persona };
132
+ }
133
+
134
+ function extractAllLessons(opts) {
135
+ var windowDays = opts.windowDays || 7;
136
+ var cutoff = Date.now() - (windowDays * 24 * 60 * 60 * 1000);
137
+ var projectIds = session.listProjects();
138
+ var total = 0;
139
+ var personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
140
+
141
+ for (var i = 0; i < projectIds.length; i++) {
142
+ var pid = projectIds[i];
143
+ var state = session.show({ project: pid });
144
+ if (!state) continue;
145
+ if (state.updated_at && new Date(state.updated_at).getTime() < cutoff) continue;
146
+
147
+ for (var p = 0; p < personas.length; p++) {
148
+ var pers = personas[p];
149
+ var lessons = extractLessonsFromSession(state, pers);
150
+ for (var j = 0; j < lessons.length; j++) {
151
+ appendLesson(pers, lessons[j]);
152
+ total++;
153
+ }
154
+ }
155
+ }
156
+
157
+ return { extracted: total, windowDays: windowDays };
158
+ }
159
+
160
+ function retrieveLessons(opts) {
161
+ var task = opts.task || '';
162
+ var personaId = opts.personaId;
163
+ var limit = opts.limit || 3;
164
+
165
+ if (!personaId) return { lessons: [], cacheHit: false };
166
+
167
+ var cacheKey = 'lessons:' + personaId + ':' + require('crypto').createHash('sha1').update(task).digest('hex').slice(0, 12);
168
+ var cached = cache.get(cacheKey);
169
+ if (cached) return { lessons: cached, cacheHit: true };
170
+
171
+ var allLessons = loadLessons(personaId);
172
+ if (!allLessons.length) return { lessons: [], cacheHit: false };
173
+
174
+ var scored = allLessons.map(function(lesson) {
175
+ var overlap = keywordOverlap(task, lesson.pattern + ' ' + lesson.fix + ' ' + lesson.task_type);
176
+ return { lesson: lesson, score: overlap };
177
+ });
178
+
179
+ scored.sort(function(a, b) { return b.score - a.score; });
180
+
181
+ var top = scored.slice(0, limit).map(function(s) { return s.lesson; });
182
+ cache.set(cacheKey, top);
183
+ return { lessons: top, cacheHit: false };
184
+ }
185
+
186
+ function buildLessonsInjection(lessons) {
187
+ if (!lessons || !lessons.length) return '';
188
+ var lines = ['Past lessons for this persona on similar tasks:'];
189
+ for (var i = 0; i < lessons.length; i++) {
190
+ var l = lessons[i];
191
+ lines.push((i + 1) + '. [' + l.pattern + '] \u2192 [' + l.fix + '] (outcome: ' + l.outcome + ')');
192
+ }
193
+ return lines.join('\n');
194
+ }
195
+
196
+ function stats() {
197
+ var personas = ['arjun', 'meera', 'priya', 'zara', 'noor', 'anuj', 'raj'];
198
+ var perPersona = {};
199
+ var total = 0;
200
+ for (var i = 0; i < personas.length; i++) {
201
+ var p = personas[i];
202
+ var count = loadLessons(p).length;
203
+ perPersona[p] = count;
204
+ total += count;
205
+ }
206
+ return { totalLessons: total, perPersona: perPersona };
207
+ }
208
+
209
+ module.exports = {
210
+ extractLessons: extractLessons,
211
+ extractAllLessons: extractAllLessons,
212
+ retrieveLessons: retrieveLessons,
213
+ buildLessonsInjection: buildLessonsInjection,
214
+ stats: stats,
215
+ loadLessons: loadLessons,
216
+ LESSONS_ROOT: LESSONS_ROOT
217
+ };