hummbl-bibliography 1.0.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 (95) hide show
  1. package/.cascade/rules/hummbl-base120.md +107 -0
  2. package/.github/CODEOWNERS +17 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +10 -0
  5. package/.github/ISSUE_TEMPLATE/new-entry.md +79 -0
  6. package/.github/ISSUE_TEMPLATE/quality-improvement.md +71 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  8. package/.github/dependabot.yml +17 -0
  9. package/.github/workflows/ci.yml +98 -0
  10. package/.github/workflows/doi-enrichment.yml +77 -0
  11. package/.github/workflows/security-audit.yml +92 -0
  12. package/.github/workflows/stats-report.yml +59 -0
  13. package/.github/workflows/validate-models.yml +194 -0
  14. package/.github/workflows/validate.yml +152 -0
  15. package/.husky/pre-commit +15 -0
  16. package/.husky/validation-rules.json +11 -0
  17. package/CHANGELOG.md +228 -0
  18. package/CONTRIBUTING.md +110 -0
  19. package/CONTRIBUTORS.md +257 -0
  20. package/DEVELOPMENT.md +110 -0
  21. package/Day_1_Audit_Worksheet.md +64 -0
  22. package/LICENSE +21 -0
  23. package/README.md +213 -0
  24. package/SECURITY.md +16 -0
  25. package/SITREP.md +141 -0
  26. package/bibliography/T10_collaboration.bib +281 -0
  27. package/bibliography/T11_security.bib +311 -0
  28. package/bibliography/T12_complexity.bib +272 -0
  29. package/bibliography/T13_reasoning.bib +231 -0
  30. package/bibliography/T1_canonical.bib +236 -0
  31. package/bibliography/T2_empirical.bib +258 -0
  32. package/bibliography/T3_applied.bib +219 -0
  33. package/bibliography/T4_agentic.bib +281 -0
  34. package/bibliography/T5_engineering.bib +243 -0
  35. package/bibliography/T6_governance.bib +277 -0
  36. package/bibliography/T7_emerging.bib +228 -0
  37. package/bibliography/T8_cognition.bib +260 -0
  38. package/bibliography/T9_economics.bib +275 -0
  39. package/bibliography/hummbl-transformations.json +84 -0
  40. package/dist/unified-bibliography.json +5699 -0
  41. package/docs/CONTRIBUTING.md +240 -0
  42. package/docs/GAP_ANALYSIS.md +142 -0
  43. package/docs/MULTI_AGENT_COORDINATION_PROTOCOL.md +700 -0
  44. package/docs/QUALITY_AUDIT_REPORT.md +576 -0
  45. package/docs/QUALITY_STANDARDS.md +350 -0
  46. package/docs/TRANSFORMATION_GUIDE.md +337 -0
  47. package/docs/metrics/model-accuracy.md +150 -0
  48. package/governance/CAES_CANONICAL.sha256 +1 -0
  49. package/governance/CAES_SPEC.md +107 -0
  50. package/governance/CAES_VERSION +1 -0
  51. package/governance/lexicon/ALLOWLIST_POLICY.md +63 -0
  52. package/governance/lexicon/CANONICALIZATION.md +63 -0
  53. package/governance/lexicon/acronym.schema.json +153 -0
  54. package/governance/lexicon/acronym_allowlist.txt +237 -0
  55. package/governance/lexicon/acronyms.v0.2.json +2555 -0
  56. package/llms.txt +1105 -0
  57. package/mappings/arcana_citations.json +219 -0
  58. package/mappings/bki_evidence.json +384 -0
  59. package/package.json +25 -0
  60. package/reports/.gitkeep +0 -0
  61. package/reports/citation_graph.json +119335 -0
  62. package/scripts/add_nist_tags.py +437 -0
  63. package/scripts/annotate_dois.py +204 -0
  64. package/scripts/check_palace_aliases.py +200 -0
  65. package/scripts/ingest_to_open_brain.py +307 -0
  66. package/scripts/monthly-review.sh +166 -0
  67. package/scripts/setup-hooks.sh +107 -0
  68. package/scripts/test_check_palace_aliases.py +194 -0
  69. package/sources/bki.bib +57 -0
  70. package/sources/theoretical-foundations.bib +589 -0
  71. package/toolkit/README.md +360 -0
  72. package/toolkit/docs/generated/quick-reference.md +179 -0
  73. package/toolkit/package-lock.json +1140 -0
  74. package/toolkit/package.json +66 -0
  75. package/toolkit/scripts/check-memory-palace-aliases.js +230 -0
  76. package/toolkit/scripts/check-memory-palace-aliases.test.js +297 -0
  77. package/toolkit/scripts/generate-docs.js +223 -0
  78. package/toolkit/src/check-duplicates.js +225 -0
  79. package/toolkit/src/check-required-fields.js +138 -0
  80. package/toolkit/src/citation-graph.js +425 -0
  81. package/toolkit/src/extensions/beyondBase120Audit.ts +250 -0
  82. package/toolkit/src/extensions/memoryPalace.ts +438 -0
  83. package/toolkit/src/extract-keywords.js +190 -0
  84. package/toolkit/src/find-missing-dois.js +178 -0
  85. package/toolkit/src/fix-duplicates.js +140 -0
  86. package/toolkit/src/merge-entries.js +29 -0
  87. package/toolkit/src/query.js +281 -0
  88. package/toolkit/src/stats.js +244 -0
  89. package/toolkit/src/test-validation.js +117 -0
  90. package/toolkit/src/utils/modelRegistry.ts +193 -0
  91. package/toolkit/src/utils/monitorModels.ts +150 -0
  92. package/toolkit/src/utils/validateModelCode.ts +196 -0
  93. package/toolkit/src/validate.js +251 -0
  94. package/toolkit/src/watch.js +100 -0
  95. package/toolkit/tsconfig.json +25 -0
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * citation-graph.js — Generate a D3.js force-directed citation relationship graph
5
+ * Output: reports/citation_graph.html (self-contained, no CDN dependencies)
6
+ *
7
+ * Edges are created from:
8
+ * 1. Explicit `crossref` fields pointing to another citation key
9
+ * 2. Shared authors across entries (co-citation signal)
10
+ * 3. Shared HUMMBL keyword tags (e.g. HUMMBL:BKI, HUMMBL:SY)
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ const args = process.argv.slice(2);
21
+ const bibDir = path.resolve(args[0] || '../bibliography');
22
+ const reportsDir = path.resolve(__dirname, '../../reports');
23
+ const outputPath = path.join(reportsDir, 'citation_graph.html');
24
+ const jsonOutputPath = path.join(reportsDir, 'citation_graph.json');
25
+
26
+ // Tier colors (matching bibliography tier structure)
27
+ const TIER_COLORS = {
28
+ T1: '#e63946', // canonical — red
29
+ T2: '#457b9d', // empirical — blue
30
+ T3: '#2a9d8f', // applied — teal
31
+ T4: '#e9c46a', // agentic — yellow
32
+ T5: '#f4a261', // engineering — orange
33
+ T6: '#264653', // governance — dark teal
34
+ T7: '#a8dadc', // emerging — light blue
35
+ T8: '#6d6875', // cognition — purple
36
+ T9: '#b5838d', // economics — rose
37
+ T10: '#e76f51', // collaboration — coral
38
+ T11: '#023e8a', // security — navy
39
+ T12: '#606c38', // complexity — olive
40
+ T13: '#9b2226', // reasoning — deep red
41
+ default: '#adb5bd',
42
+ };
43
+
44
+ /**
45
+ * Minimal BibTeX parser — extracts key, type, and field values.
46
+ * Handles multiline values and comment lines.
47
+ */
48
+ function parseBibFile(content) {
49
+ const entries = [];
50
+ // Match @type{key, ...} blocks
51
+ const entryRegex = /@(\w+)\s*\{\s*([^,\s]+)\s*,([^@]*)/g;
52
+ let match;
53
+ while ((match = entryRegex.exec(content)) !== null) {
54
+ const type = match[1].toLowerCase();
55
+ const key = match[2].trim();
56
+ const body = match[3];
57
+
58
+ if (type === 'comment' || type === 'string' || type === 'preamble') continue;
59
+
60
+ const fields = {};
61
+ // Match field = {value} or field = "value" or field = number
62
+ const fieldRegex = /(\w+)\s*=\s*(?:\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}|"([^"]*)"|(\d+))/g;
63
+ let fm;
64
+ while ((fm = fieldRegex.exec(body)) !== null) {
65
+ const name = fm[1].toLowerCase();
66
+ const value = (fm[2] ?? fm[3] ?? fm[4] ?? '').trim();
67
+ fields[name] = value;
68
+ }
69
+
70
+ entries.push({ key, type, fields });
71
+ }
72
+ return entries;
73
+ }
74
+
75
+ function extractAuthors(authorStr) {
76
+ if (!authorStr) return [];
77
+ return authorStr.split(' and ').map(a => a.trim().toLowerCase()).filter(Boolean);
78
+ }
79
+
80
+ function extractHummblTags(keywords) {
81
+ if (!keywords) return [];
82
+ return keywords.split(',').map(k => k.trim()).filter(k => k.startsWith('HUMMBL:'));
83
+ }
84
+
85
+ function tierFromFile(filename) {
86
+ const m = filename.match(/^(T\d+)_/);
87
+ return m ? m[1] : 'default';
88
+ }
89
+
90
+ // Load all .bib files
91
+ if (!fs.existsSync(bibDir)) {
92
+ console.error(`Bibliography directory not found: ${bibDir}`);
93
+ process.exit(1);
94
+ }
95
+
96
+ const bibFiles = fs.readdirSync(bibDir).filter(f => f.endsWith('.bib'));
97
+ const allEntries = [];
98
+
99
+ for (const file of bibFiles) {
100
+ const tier = tierFromFile(file);
101
+ const content = fs.readFileSync(path.join(bibDir, file), 'utf8');
102
+ const entries = parseBibFile(content);
103
+ entries.forEach(e => {
104
+ e.tier = tier;
105
+ e.file = file;
106
+ });
107
+ allEntries.push(...entries);
108
+ }
109
+
110
+ console.log(`Parsed ${allEntries.length} entries from ${bibFiles.length} files`);
111
+
112
+ // Build nodes
113
+ const keyIndex = new Map(allEntries.map(e => [e.key, e]));
114
+ const nodes = allEntries.map(e => ({
115
+ id: e.key,
116
+ tier: e.tier,
117
+ title: e.fields.title || e.key,
118
+ authors: extractAuthors(e.fields.author),
119
+ year: e.fields.year || '',
120
+ doi: e.fields.doi || '',
121
+ url: e.fields.url || '',
122
+ tags: extractHummblTags(e.fields.keywords || ''),
123
+ file: e.file,
124
+ }));
125
+
126
+ // Build edges
127
+ const edges = [];
128
+ const edgeSet = new Set();
129
+
130
+ function addEdge(source, target, type) {
131
+ const edgeKey = [source, target].sort().join('||');
132
+ if (!edgeSet.has(edgeKey) && source !== target) {
133
+ edgeSet.add(edgeKey);
134
+ edges.push({ source, target, type });
135
+ }
136
+ }
137
+
138
+ // 1. Crossref edges
139
+ for (const entry of allEntries) {
140
+ if (entry.fields.crossref && keyIndex.has(entry.fields.crossref)) {
141
+ addEdge(entry.key, entry.fields.crossref, 'crossref');
142
+ }
143
+ }
144
+
145
+ // 2. Shared HUMMBL tags (only specific tags, not generic ones)
146
+ const SHARED_TAG_MIN_SPECIFICITY = 2; // skip tags that appear on >50% of entries
147
+ const tagToEntries = new Map();
148
+ for (const node of nodes) {
149
+ for (const tag of node.tags) {
150
+ if (!tagToEntries.has(tag)) tagToEntries.set(tag, []);
151
+ tagToEntries.get(tag).push(node.id);
152
+ }
153
+ }
154
+ const maxTagEntries = Math.floor(allEntries.length * 0.5);
155
+ for (const [tag, keys] of tagToEntries) {
156
+ if (keys.length < SHARED_TAG_MIN_SPECIFICITY || keys.length > maxTagEntries) continue;
157
+ for (let i = 0; i < keys.length; i++) {
158
+ for (let j = i + 1; j < keys.length; j++) {
159
+ addEdge(keys[i], keys[j], 'shared-tag');
160
+ }
161
+ }
162
+ }
163
+
164
+ // 3. Shared authors (first author match only to reduce noise)
165
+ const firstAuthorToEntries = new Map();
166
+ for (const node of nodes) {
167
+ if (node.authors.length === 0) continue;
168
+ const first = node.authors[0];
169
+ if (!firstAuthorToEntries.has(first)) firstAuthorToEntries.set(first, []);
170
+ firstAuthorToEntries.get(first).push(node.id);
171
+ }
172
+ for (const [, keys] of firstAuthorToEntries) {
173
+ if (keys.length < 2 || keys.length > 10) continue; // skip prolific authors that would add too many edges
174
+ for (let i = 0; i < keys.length; i++) {
175
+ for (let j = i + 1; j < keys.length; j++) {
176
+ addEdge(keys[i], keys[j], 'shared-author');
177
+ }
178
+ }
179
+ }
180
+
181
+ console.log(`Graph: ${nodes.length} nodes, ${edges.length} edges`);
182
+
183
+ // Write JSON
184
+ const graphData = { nodes, edges, meta: { generated: new Date().toISOString(), entryCount: nodes.length, edgeCount: edges.length } };
185
+ fs.mkdirSync(reportsDir, { recursive: true });
186
+ fs.writeFileSync(jsonOutputPath, JSON.stringify(graphData, null, 2));
187
+ console.log(`JSON: ${jsonOutputPath}`);
188
+
189
+ // Build self-contained HTML with embedded D3 v7 (fetched at build time via inline)
190
+ // D3 is embedded as a data URI / inline script to satisfy "no external CDN" requirement.
191
+ // We use a minimal D3 bundle fetched from unpkg at GENERATION time and inlined.
192
+ // At runtime the HTML is fully self-contained.
193
+
194
+ let d3Source = '';
195
+ try {
196
+ // Try to use local node_modules d3 if available
197
+ const d3Path = path.resolve(__dirname, '../node_modules/d3/dist/d3.min.js');
198
+ if (fs.existsSync(d3Path)) {
199
+ d3Source = fs.readFileSync(d3Path, 'utf8');
200
+ console.log('Using local d3 from node_modules');
201
+ }
202
+ } catch {
203
+ // Will fall back to fetch below
204
+ }
205
+
206
+ const graphDataJson = JSON.stringify(graphData);
207
+ const tierColorsJson = JSON.stringify(TIER_COLORS);
208
+
209
+ const html = `<!DOCTYPE html>
210
+ <html lang="en">
211
+ <head>
212
+ <meta charset="UTF-8">
213
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
214
+ <title>HUMMBL Bibliography Citation Graph</title>
215
+ <style>
216
+ * { box-sizing: border-box; margin: 0; padding: 0; }
217
+ body { background: #0f1117; color: #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }
218
+ #header { position: fixed; top: 0; left: 0; right: 0; z-index: 10; background: rgba(15,17,23,0.92); border-bottom: 1px solid #2d3748; padding: 10px 16px; display: flex; align-items: center; gap: 16px; }
219
+ #header h1 { font-size: 14px; font-weight: 600; color: #a0aec0; }
220
+ #stats { font-size: 12px; color: #718096; }
221
+ #controls { display: flex; gap: 8px; margin-left: auto; }
222
+ button { background: #2d3748; border: 1px solid #4a5568; color: #e2e8f0; padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; }
223
+ button:hover { background: #4a5568; }
224
+ #legend { position: fixed; bottom: 16px; left: 16px; z-index: 10; background: rgba(15,17,23,0.92); border: 1px solid #2d3748; border-radius: 6px; padding: 10px; font-size: 11px; }
225
+ #legend h3 { font-size: 11px; color: #718096; margin-bottom: 6px; }
226
+ .legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
227
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
228
+ #tooltip { position: fixed; z-index: 20; background: #1a202c; border: 1px solid #4a5568; border-radius: 6px; padding: 10px 12px; font-size: 12px; max-width: 280px; pointer-events: none; display: none; }
229
+ #tooltip .t-title { font-weight: 600; color: #e2e8f0; margin-bottom: 4px; line-height: 1.3; }
230
+ #tooltip .t-meta { color: #718096; font-size: 11px; }
231
+ #tooltip .t-tags { margin-top: 4px; }
232
+ #tooltip .t-tag { display: inline-block; background: #2d3748; color: #a0aec0; border-radius: 3px; padding: 1px 5px; font-size: 10px; margin: 1px; }
233
+ svg { display: block; }
234
+ .node circle { cursor: pointer; stroke-width: 1.5; }
235
+ .node circle:hover { stroke-width: 3; }
236
+ .link { stroke-opacity: 0.3; }
237
+ .link.crossref { stroke: #e63946; stroke-opacity: 0.7; }
238
+ .link.shared-author { stroke: #457b9d; stroke-opacity: 0.25; }
239
+ .link.shared-tag { stroke: #2a9d8f; stroke-opacity: 0.2; }
240
+ #filter-panel { position: fixed; top: 48px; right: 16px; z-index: 10; background: rgba(15,17,23,0.92); border: 1px solid #2d3748; border-radius: 6px; padding: 10px; font-size: 12px; min-width: 160px; }
241
+ #filter-panel h3 { font-size: 11px; color: #718096; margin-bottom: 6px; }
242
+ .filter-item { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; cursor: pointer; }
243
+ .filter-item input { cursor: pointer; }
244
+ #search { background: #2d3748; border: 1px solid #4a5568; color: #e2e8f0; padding: 4px 8px; border-radius: 4px; font-size: 12px; width: 100%; margin-bottom: 8px; }
245
+ </style>
246
+ </head>
247
+ <body>
248
+ <div id="header">
249
+ <h1>HUMMBL Bibliography Citation Graph</h1>
250
+ <span id="stats"></span>
251
+ <div id="controls">
252
+ <button id="btn-reset">Reset View</button>
253
+ <button id="btn-toggle-labels">Toggle Labels</button>
254
+ </div>
255
+ </div>
256
+
257
+ <div id="filter-panel">
258
+ <input id="search" type="text" placeholder="Search entries..." />
259
+ <h3>Edge Types</h3>
260
+ <label class="filter-item"><input type="checkbox" data-edge="crossref" checked> Crossref</label>
261
+ <label class="filter-item"><input type="checkbox" data-edge="shared-author" checked> Shared Author</label>
262
+ <label class="filter-item"><input type="checkbox" data-edge="shared-tag" checked> Shared Tag</label>
263
+ </div>
264
+
265
+ <div id="legend">
266
+ <h3>Tiers</h3>
267
+ </div>
268
+
269
+ <div id="tooltip">
270
+ <div class="t-title"></div>
271
+ <div class="t-meta"></div>
272
+ <div class="t-tags"></div>
273
+ </div>
274
+
275
+ <svg id="graph"></svg>
276
+
277
+ ${d3Source ? `<script>${d3Source}</script>` : '<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>'}
278
+
279
+ <script>
280
+ const GRAPH = ${graphDataJson};
281
+ const TIER_COLORS = ${tierColorsJson};
282
+
283
+ const TIER_LABELS = {
284
+ T1:'T1 Canonical', T2:'T2 Empirical', T3:'T3 Applied', T4:'T4 Agentic',
285
+ T5:'T5 Engineering', T6:'T6 Governance', T7:'T7 Emerging', T8:'T8 Cognition',
286
+ T9:'T9 Economics', T10:'T10 Collaboration', T11:'T11 Security',
287
+ T12:'T12 Complexity', T13:'T13 Reasoning'
288
+ };
289
+
290
+ // Stats
291
+ document.getElementById('stats').textContent =
292
+ \`\${GRAPH.nodes.length} entries · \${GRAPH.edges.length} edges · generated \${GRAPH.meta.generated.slice(0,10)}\`;
293
+
294
+ // Legend
295
+ const legend = document.getElementById('legend');
296
+ const tiers = [...new Set(GRAPH.nodes.map(n => n.tier))].sort();
297
+ tiers.forEach(t => {
298
+ const div = document.createElement('div');
299
+ div.className = 'legend-item';
300
+ div.innerHTML = \`<div class="legend-dot" style="background:\${TIER_COLORS[t]||TIER_COLORS.default}"></div><span>\${TIER_LABELS[t]||t}</span>\`;
301
+ legend.appendChild(div);
302
+ });
303
+
304
+ const svg = d3.select('#graph');
305
+ const width = window.innerWidth;
306
+ const height = window.innerHeight;
307
+ svg.attr('width', width).attr('height', height);
308
+
309
+ const container = svg.append('g');
310
+
311
+ // Zoom
312
+ const zoom = d3.zoom().scaleExtent([0.1, 8]).on('zoom', e => container.attr('transform', e.transform));
313
+ svg.call(zoom);
314
+
315
+ // Data copies (d3 mutates)
316
+ const nodes = GRAPH.nodes.map(n => ({ ...n }));
317
+ const links = GRAPH.edges.map(e => ({ ...e }));
318
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
319
+
320
+ // Simulation
321
+ const simulation = d3.forceSimulation(nodes)
322
+ .force('link', d3.forceLink(links).id(d => d.id).distance(60).strength(0.3))
323
+ .force('charge', d3.forceManyBody().strength(-120))
324
+ .force('center', d3.forceCenter(width / 2, height / 2))
325
+ .force('collision', d3.forceCollide(10));
326
+
327
+ // Links
328
+ const linkGroup = container.append('g').attr('class', 'links');
329
+ let linkEls = linkGroup.selectAll('line')
330
+ .data(links)
331
+ .join('line')
332
+ .attr('class', d => \`link \${d.type}\`)
333
+ .attr('stroke', d => d.type === 'crossref' ? '#e63946' : d.type === 'shared-author' ? '#457b9d' : '#2a9d8f');
334
+
335
+ // Nodes
336
+ const nodeGroup = container.append('g').attr('class', 'nodes');
337
+ let nodeEls = nodeGroup.selectAll('g')
338
+ .data(nodes)
339
+ .join('g')
340
+ .attr('class', 'node')
341
+ .call(d3.drag()
342
+ .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
343
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
344
+ .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }));
345
+
346
+ nodeEls.append('circle')
347
+ .attr('r', 6)
348
+ .attr('fill', d => TIER_COLORS[d.tier] || TIER_COLORS.default)
349
+ .attr('stroke', '#0f1117');
350
+
351
+ // Labels (hidden by default for large graphs)
352
+ let labelsVisible = nodes.length <= 80;
353
+ const labelEls = nodeEls.append('text')
354
+ .attr('dx', 8).attr('dy', 4)
355
+ .style('font-size', '9px').style('fill', '#a0aec0')
356
+ .style('pointer-events', 'none')
357
+ .style('display', labelsVisible ? null : 'none')
358
+ .text(d => d.id.length > 25 ? d.id.slice(0, 25) + '…' : d.id);
359
+
360
+ // Tooltip
361
+ const tooltip = document.getElementById('tooltip');
362
+ nodeEls
363
+ .on('mouseover', (e, d) => {
364
+ const link = d.doi ? \`https://doi.org/\${d.doi}\` : d.url;
365
+ tooltip.querySelector('.t-title').textContent = d.title;
366
+ tooltip.querySelector('.t-meta').textContent = \`\${d.authors.slice(0,2).join(', ')} \${d.year} [\${d.tier}]\`;
367
+ tooltip.querySelector('.t-tags').innerHTML = d.tags.map(t => \`<span class="t-tag">\${t}</span>\`).join('');
368
+ tooltip.style.display = 'block';
369
+ })
370
+ .on('mousemove', e => {
371
+ tooltip.style.left = (e.clientX + 14) + 'px';
372
+ tooltip.style.top = (e.clientY - 10) + 'px';
373
+ })
374
+ .on('mouseout', () => { tooltip.style.display = 'none'; })
375
+ .on('click', (e, d) => {
376
+ const url = d.doi ? \`https://doi.org/\${d.doi}\` : d.url;
377
+ if (url) window.open(url, '_blank');
378
+ });
379
+
380
+ // Tick
381
+ simulation.on('tick', () => {
382
+ linkEls.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
383
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
384
+ nodeEls.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
385
+ });
386
+
387
+ // Controls
388
+ document.getElementById('btn-reset').addEventListener('click', () => {
389
+ svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1));
390
+ });
391
+
392
+ document.getElementById('btn-toggle-labels').addEventListener('click', () => {
393
+ labelsVisible = !labelsVisible;
394
+ labelEls.style('display', labelsVisible ? null : 'none');
395
+ });
396
+
397
+ // Edge type filters
398
+ document.querySelectorAll('[data-edge]').forEach(cb => {
399
+ cb.addEventListener('change', () => {
400
+ const edgeType = cb.dataset.edge;
401
+ linkEls.filter(d => d.type === edgeType).style('display', cb.checked ? null : 'none');
402
+ });
403
+ });
404
+
405
+ // Search
406
+ document.getElementById('search').addEventListener('input', e => {
407
+ const q = e.target.value.toLowerCase();
408
+ nodeEls.selectAll('circle')
409
+ .attr('stroke', d => (!q || d.id.toLowerCase().includes(q) || d.title.toLowerCase().includes(q)) ? '#0f1117' : '#555')
410
+ .attr('opacity', d => (!q || d.id.toLowerCase().includes(q) || d.title.toLowerCase().includes(q)) ? 1 : 0.15);
411
+ });
412
+
413
+ // Resize
414
+ window.addEventListener('resize', () => {
415
+ svg.attr('width', window.innerWidth).attr('height', window.innerHeight);
416
+ simulation.force('center', d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2));
417
+ simulation.alpha(0.1).restart();
418
+ });
419
+ </script>
420
+ </body>
421
+ </html>`;
422
+
423
+ fs.writeFileSync(outputPath, html);
424
+ console.log(`HTML: ${outputPath}`);
425
+ console.log(`\nDone. Open in browser:\n open ${outputPath}`);
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Beyond-Base120 Audit — Memory Palace drift, duplicate, and coverage detection.
3
+ *
4
+ * This is the second validation pass (after Base120 validation).
5
+ * It answers: "are the extended mental models in this content registered
6
+ * in the Memory Palace?"
7
+ *
8
+ * Responsibilities:
9
+ * - DRIFT: finds model names in content that look like extended models but
10
+ * aren't in the Memory Palace registry
11
+ * - DUPLICATES: detects registry entries that may overlap (reported by
12
+ * auditRegistry())
13
+ * - UNREGISTERED: flags terms that appear to be mental model references
14
+ * but are neither Base120 codes nor Memory Palace entries
15
+ * - COVERAGE: reports which rooms are represented and which are empty
16
+ *
17
+ * What this does NOT do:
18
+ * - Hard-block any specific term (no blocklist)
19
+ * - Flag Base120 codes (that's the Base120 validator's job)
20
+ * - Assume any term is automatically valid or invalid
21
+ */
22
+
23
+ import {
24
+ lookupMemoryPalace,
25
+ isMemoryPalaceModel,
26
+ auditRegistry,
27
+ getRoom,
28
+ getAllCanonicalNames,
29
+ MemoryPalaceRoom,
30
+ SourceType,
31
+ } from './memoryPalace.js';
32
+
33
+ import { validateModelCode } from '../utils/validateModelCode.js';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Types
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export type BeyondAuditSeverity = 'ERROR' | 'WARN' | 'INFO';
40
+
41
+ export interface BeyondAuditFinding {
42
+ term: string;
43
+ severity: BeyondAuditSeverity;
44
+ code: string; // Finding code e.g. "DRIFT001"
45
+ message: string;
46
+ source_type?: SourceType;
47
+ }
48
+
49
+ export interface BeyondAuditReport {
50
+ scanned_terms: string[];
51
+ findings: BeyondAuditFinding[];
52
+ registry_health: ReturnType<typeof auditRegistry>;
53
+ stats: {
54
+ total_terms_scanned: number;
55
+ registered: number;
56
+ unregistered: number;
57
+ base120_codes_skipped: number;
58
+ };
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Known extended-model patterns (heuristic scan, not a blocklist)
63
+ // These are patterns that suggest a term is a mental model reference.
64
+ // If matched but not in Memory Palace → DRIFT warning.
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Scan text for potential unregistered extended model references.
69
+ *
70
+ * Strategy: look for capitalised phrases (2-4 words) that appear in a
71
+ * "model reference" context and are NOT Base120 codes. Check each against
72
+ * the Memory Palace. Flag anything that looks like a model but isn't registered.
73
+ *
74
+ * Matches:
75
+ * - Standard Title Case: "Circle of Competence", "Map vs Territory"
76
+ * - ALL-CAPS acronym lead: "OODA Loop", "VUCA World"
77
+ * - Mixed with short connectors (in, of, the, vs, a, an, for, at):
78
+ * "Skin in the Game", "First Principles Thinking"
79
+ *
80
+ * This is intentionally conservative — false negatives are preferred over
81
+ * false positives. The goal is drift detection, not a censorship blocklist.
82
+ */
83
+ export function scanForExtendedModels(text: string): string[] {
84
+ // Match phrases that start with a capitalised or ALL-CAPS word, then
85
+ // allow 1-3 additional segments that are each either:
86
+ // (a) a capitalised/ALL-CAPS word, or
87
+ // (b) a short lowercase connector (≤5 chars: in, of, the, vs, a, an, for, at…)
88
+ // This covers "OODA Loop", "Skin in the Game", "Circle of Competence",
89
+ // "Map vs Territory", etc.
90
+ const titleCasePattern = /\b([A-Z][A-Za-z']*(?:[\s-](?:[A-Z][A-Za-z']*|[a-z]{1,5})){1,3})\b/g;
91
+ const candidates = new Set<string>();
92
+
93
+ let match: RegExpExecArray | null;
94
+ while ((match = titleCasePattern.exec(text)) !== null) {
95
+ const phrase = match[1];
96
+ // Skip if it's a Base120 code pattern
97
+ if (/^(P|IN|CO|DE|RE|SY)\d+$/.test(phrase)) continue;
98
+ // Skip very common non-model phrases
99
+ if (COMMON_NON_MODELS.has(phrase.toLowerCase())) continue;
100
+ candidates.add(phrase);
101
+ }
102
+
103
+ return Array.from(candidates);
104
+ }
105
+
106
+ // Common title-case phrases that are not mental models
107
+ const COMMON_NON_MODELS = new Set([
108
+ 'the following', 'for example', 'this section', 'see also',
109
+ 'note that', 'in addition', 'as follows', 'such as',
110
+ 'first principles', 'second order', 'third party',
111
+ 'united states', 'new york', 'san francisco',
112
+ ]);
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Main audit function
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Audit text for beyond-Base120 model compliance.
120
+ *
121
+ * @param text - Content to audit
122
+ * @param strict - In strict mode, unregistered extended model candidates
123
+ * are ERROR; in default mode, they are WARN
124
+ */
125
+ export function auditBeyondBase120(
126
+ text: string,
127
+ strict: boolean = false,
128
+ ): BeyondAuditReport {
129
+ const candidates = scanForExtendedModels(text);
130
+ const findings: BeyondAuditFinding[] = [];
131
+
132
+ let registered = 0;
133
+ let unregistered = 0;
134
+ let base120Skipped = 0;
135
+
136
+ for (const term of candidates) {
137
+ // Check if it's a Base120 code (skip — that's the other validator's job)
138
+ const base120Check = validateModelCode(term);
139
+ if (base120Check.isValid) {
140
+ base120Skipped++;
141
+ continue;
142
+ }
143
+
144
+ // Check Memory Palace
145
+ if (isMemoryPalaceModel(term)) {
146
+ registered++;
147
+ } else {
148
+ unregistered++;
149
+ findings.push({
150
+ term,
151
+ severity: strict ? 'ERROR' : 'WARN',
152
+ code: 'DRIFT001',
153
+ message: `"${term}" looks like a mental model reference but is not registered in the Memory Palace. ` +
154
+ `Add it to toolkit/src/extensions/memoryPalace.ts or confirm it is not a model reference.`,
155
+ });
156
+ }
157
+ }
158
+
159
+ // Registry health check
160
+ const registry_health = auditRegistry();
161
+
162
+ // Flag registry duplicates as errors
163
+ for (const dup of registry_health.duplicateSlugs) {
164
+ findings.push({
165
+ term: dup,
166
+ severity: 'ERROR',
167
+ code: 'DUP001',
168
+ message: `Duplicate slug in Memory Palace registry: "${dup}"`,
169
+ });
170
+ }
171
+
172
+ for (const dup of registry_health.duplicateNames) {
173
+ findings.push({
174
+ term: dup,
175
+ severity: 'ERROR',
176
+ code: 'DUP002',
177
+ message: `Duplicate name/alias in Memory Palace registry: "${dup}"`,
178
+ });
179
+ }
180
+
181
+ for (const { slug, fields } of registry_health.missingFields) {
182
+ findings.push({
183
+ term: slug,
184
+ severity: 'ERROR',
185
+ code: 'REG001',
186
+ message: `Memory Palace entry "${slug}" is missing required fields: ${fields.join(', ')}`,
187
+ });
188
+ }
189
+
190
+ for (const slug of registry_health.missingSourceTypes) {
191
+ findings.push({
192
+ term: slug,
193
+ severity: 'WARN',
194
+ code: 'REG002',
195
+ message: `Memory Palace entry "${slug}" is missing optional source_type. Add it to improve citation hygiene and staleness review.`,
196
+ });
197
+ }
198
+
199
+ return {
200
+ scanned_terms: candidates,
201
+ findings,
202
+ registry_health,
203
+ stats: {
204
+ total_terms_scanned: candidates.length,
205
+ registered,
206
+ unregistered,
207
+ base120_codes_skipped: base120Skipped,
208
+ },
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Quick check: does content pass beyond-base120 audit?
214
+ */
215
+ export function passesBeyondBase120(text: string, strict: boolean = false): boolean {
216
+ const report = auditBeyondBase120(text, strict);
217
+ return !report.findings.some(f => f.severity === 'ERROR');
218
+ }
219
+
220
+ /**
221
+ * Format a beyond-base120 audit report for console output.
222
+ */
223
+ export function formatBeyondReport(report: BeyondAuditReport): string {
224
+ const lines: string[] = ['BEYOND-BASE120 AUDIT REPORT', '='.repeat(40)];
225
+
226
+ lines.push(`Memory Palace: ${report.registry_health.totalEntries} entries`);
227
+ const roomSummary = Object.entries(report.registry_health.byRoom)
228
+ .map(([room, count]) => `${room}:${count}`)
229
+ .join(' | ');
230
+ lines.push(`Rooms: ${roomSummary}`);
231
+ lines.push('');
232
+ lines.push(`Scanned ${report.stats.total_terms_scanned} candidate terms`);
233
+ lines.push(` Registered: ${report.stats.registered}`);
234
+ lines.push(` Unregistered: ${report.stats.unregistered}`);
235
+ lines.push(` Base120 codes skipped: ${report.stats.base120_codes_skipped}`);
236
+ lines.push('');
237
+
238
+ if (report.findings.length === 0) {
239
+ lines.push('✅ No findings — all extended model references are registered');
240
+ } else {
241
+ lines.push(`Findings (${report.findings.length}):`);
242
+ for (const f of report.findings) {
243
+ const icon = f.severity === 'ERROR' ? '❌' : f.severity === 'WARN' ? '⚠️' : 'ℹ️';
244
+ const sourceType = f.source_type ? ` (source_type=${f.source_type})` : '';
245
+ lines.push(` ${icon} [${f.code}] ${f.message}${sourceType}`);
246
+ }
247
+ }
248
+
249
+ return lines.join('\n');
250
+ }