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.
- package/.cascade/rules/hummbl-base120.md +107 -0
- package/.github/CODEOWNERS +17 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +10 -0
- package/.github/ISSUE_TEMPLATE/new-entry.md +79 -0
- package/.github/ISSUE_TEMPLATE/quality-improvement.md +71 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- package/.github/dependabot.yml +17 -0
- package/.github/workflows/ci.yml +98 -0
- package/.github/workflows/doi-enrichment.yml +77 -0
- package/.github/workflows/security-audit.yml +92 -0
- package/.github/workflows/stats-report.yml +59 -0
- package/.github/workflows/validate-models.yml +194 -0
- package/.github/workflows/validate.yml +152 -0
- package/.husky/pre-commit +15 -0
- package/.husky/validation-rules.json +11 -0
- package/CHANGELOG.md +228 -0
- package/CONTRIBUTING.md +110 -0
- package/CONTRIBUTORS.md +257 -0
- package/DEVELOPMENT.md +110 -0
- package/Day_1_Audit_Worksheet.md +64 -0
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/SECURITY.md +16 -0
- package/SITREP.md +141 -0
- package/bibliography/T10_collaboration.bib +281 -0
- package/bibliography/T11_security.bib +311 -0
- package/bibliography/T12_complexity.bib +272 -0
- package/bibliography/T13_reasoning.bib +231 -0
- package/bibliography/T1_canonical.bib +236 -0
- package/bibliography/T2_empirical.bib +258 -0
- package/bibliography/T3_applied.bib +219 -0
- package/bibliography/T4_agentic.bib +281 -0
- package/bibliography/T5_engineering.bib +243 -0
- package/bibliography/T6_governance.bib +277 -0
- package/bibliography/T7_emerging.bib +228 -0
- package/bibliography/T8_cognition.bib +260 -0
- package/bibliography/T9_economics.bib +275 -0
- package/bibliography/hummbl-transformations.json +84 -0
- package/dist/unified-bibliography.json +5699 -0
- package/docs/CONTRIBUTING.md +240 -0
- package/docs/GAP_ANALYSIS.md +142 -0
- package/docs/MULTI_AGENT_COORDINATION_PROTOCOL.md +700 -0
- package/docs/QUALITY_AUDIT_REPORT.md +576 -0
- package/docs/QUALITY_STANDARDS.md +350 -0
- package/docs/TRANSFORMATION_GUIDE.md +337 -0
- package/docs/metrics/model-accuracy.md +150 -0
- package/governance/CAES_CANONICAL.sha256 +1 -0
- package/governance/CAES_SPEC.md +107 -0
- package/governance/CAES_VERSION +1 -0
- package/governance/lexicon/ALLOWLIST_POLICY.md +63 -0
- package/governance/lexicon/CANONICALIZATION.md +63 -0
- package/governance/lexicon/acronym.schema.json +153 -0
- package/governance/lexicon/acronym_allowlist.txt +237 -0
- package/governance/lexicon/acronyms.v0.2.json +2555 -0
- package/llms.txt +1105 -0
- package/mappings/arcana_citations.json +219 -0
- package/mappings/bki_evidence.json +384 -0
- package/package.json +25 -0
- package/reports/.gitkeep +0 -0
- package/reports/citation_graph.json +119335 -0
- package/scripts/add_nist_tags.py +437 -0
- package/scripts/annotate_dois.py +204 -0
- package/scripts/check_palace_aliases.py +200 -0
- package/scripts/ingest_to_open_brain.py +307 -0
- package/scripts/monthly-review.sh +166 -0
- package/scripts/setup-hooks.sh +107 -0
- package/scripts/test_check_palace_aliases.py +194 -0
- package/sources/bki.bib +57 -0
- package/sources/theoretical-foundations.bib +589 -0
- package/toolkit/README.md +360 -0
- package/toolkit/docs/generated/quick-reference.md +179 -0
- package/toolkit/package-lock.json +1140 -0
- package/toolkit/package.json +66 -0
- package/toolkit/scripts/check-memory-palace-aliases.js +230 -0
- package/toolkit/scripts/check-memory-palace-aliases.test.js +297 -0
- package/toolkit/scripts/generate-docs.js +223 -0
- package/toolkit/src/check-duplicates.js +225 -0
- package/toolkit/src/check-required-fields.js +138 -0
- package/toolkit/src/citation-graph.js +425 -0
- package/toolkit/src/extensions/beyondBase120Audit.ts +250 -0
- package/toolkit/src/extensions/memoryPalace.ts +438 -0
- package/toolkit/src/extract-keywords.js +190 -0
- package/toolkit/src/find-missing-dois.js +178 -0
- package/toolkit/src/fix-duplicates.js +140 -0
- package/toolkit/src/merge-entries.js +29 -0
- package/toolkit/src/query.js +281 -0
- package/toolkit/src/stats.js +244 -0
- package/toolkit/src/test-validation.js +117 -0
- package/toolkit/src/utils/modelRegistry.ts +193 -0
- package/toolkit/src/utils/monitorModels.ts +150 -0
- package/toolkit/src/utils/validateModelCode.ts +196 -0
- package/toolkit/src/validate.js +251 -0
- package/toolkit/src/watch.js +100 -0
- 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
|
+
}
|