@kernel.chat/kbot 3.43.0 → 3.45.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/dist/agent-teams.d.ts +1 -1
- package/dist/agent-teams.d.ts.map +1 -1
- package/dist/agent-teams.js +56 -2
- package/dist/agent-teams.js.map +1 -1
- package/dist/agents/specialists.d.ts.map +1 -1
- package/dist/agents/specialists.js +109 -0
- package/dist/agents/specialists.js.map +1 -1
- package/dist/cli.js +5 -5
- package/dist/episodic-memory.d.ts.map +1 -1
- package/dist/episodic-memory.js +8 -0
- package/dist/episodic-memory.js.map +1 -1
- package/dist/learned-router.d.ts.map +1 -1
- package/dist/learned-router.js +48 -0
- package/dist/learned-router.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lab-health.d.ts +2 -0
- package/dist/tools/lab-health.d.ts.map +1 -0
- package/dist/tools/lab-health.js +2054 -0
- package/dist/tools/lab-health.js.map +1 -0
- package/dist/tools/lab-humanities.d.ts +2 -0
- package/dist/tools/lab-humanities.d.ts.map +1 -0
- package/dist/tools/lab-humanities.js +1993 -0
- package/dist/tools/lab-humanities.js.map +1 -0
- package/dist/tools/lab-neuro.d.ts +2 -0
- package/dist/tools/lab-neuro.d.ts.map +1 -0
- package/dist/tools/lab-neuro.js +2699 -0
- package/dist/tools/lab-neuro.js.map +1 -0
- package/dist/tools/lab-social.d.ts +2 -0
- package/dist/tools/lab-social.d.ts.map +1 -0
- package/dist/tools/lab-social.js +2557 -0
- package/dist/tools/lab-social.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
// kbot Lab Humanities Tools — Linguistics, Philosophy, History, Digital Humanities
|
|
2
|
+
// Computational text analysis, formal logic, argument mapping, ethical frameworks,
|
|
3
|
+
// historical timelines, language typology, IPA phonetics, stylometry,
|
|
4
|
+
// philosophical encyclopedia, and archival search.
|
|
5
|
+
import { registerTool } from './index.js';
|
|
6
|
+
// ─── Text Helpers ───────────────────────────────────────────────────────────
|
|
7
|
+
/** Tokenize text into lowercase words, stripping punctuation */
|
|
8
|
+
function tokenize(text) {
|
|
9
|
+
return text
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^\p{L}\p{N}\s'-]/gu, ' ')
|
|
12
|
+
.split(/\s+/)
|
|
13
|
+
.filter(w => w.length > 0);
|
|
14
|
+
}
|
|
15
|
+
/** Split text into sentences (rough heuristic) */
|
|
16
|
+
function sentenceSplit(text) {
|
|
17
|
+
return text
|
|
18
|
+
.split(/(?<=[.!?])\s+/)
|
|
19
|
+
.map(s => s.trim())
|
|
20
|
+
.filter(s => s.length > 0);
|
|
21
|
+
}
|
|
22
|
+
// ─── Corpus Analyze ─────────────────────────────────────────────────────────
|
|
23
|
+
function wordFrequency(tokens, top) {
|
|
24
|
+
const freq = new Map();
|
|
25
|
+
for (const t of tokens)
|
|
26
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
27
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
28
|
+
return new Map(sorted.slice(0, top));
|
|
29
|
+
}
|
|
30
|
+
function computeNgrams(tokens, n, top) {
|
|
31
|
+
const freq = new Map();
|
|
32
|
+
for (let i = 0; i <= tokens.length - n; i++) {
|
|
33
|
+
const gram = tokens.slice(i, i + n).join(' ');
|
|
34
|
+
freq.set(gram, (freq.get(gram) || 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
37
|
+
return new Map(sorted.slice(0, top));
|
|
38
|
+
}
|
|
39
|
+
function hapaxLegomena(tokens) {
|
|
40
|
+
const freq = new Map();
|
|
41
|
+
for (const t of tokens)
|
|
42
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
43
|
+
return [...freq.entries()].filter(([, c]) => c === 1).map(([w]) => w);
|
|
44
|
+
}
|
|
45
|
+
function typeTokenRatio(tokens) {
|
|
46
|
+
const types = new Set(tokens).size;
|
|
47
|
+
return { types, tokens: tokens.length, ttr: tokens.length > 0 ? types / tokens.length : 0 };
|
|
48
|
+
}
|
|
49
|
+
function vocabularyGrowthCurve(tokens) {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
const curve = [];
|
|
52
|
+
const step = Math.max(1, Math.floor(tokens.length / 50));
|
|
53
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
54
|
+
seen.add(tokens[i]);
|
|
55
|
+
if (i % step === 0 || i === tokens.length - 1) {
|
|
56
|
+
curve.push([i + 1, seen.size]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Heaps' law fit: V = K * N^beta via log-log linear regression
|
|
60
|
+
const logN = [];
|
|
61
|
+
const logV = [];
|
|
62
|
+
for (const [n, v] of curve) {
|
|
63
|
+
if (n > 0 && v > 0) {
|
|
64
|
+
logN.push(Math.log(n));
|
|
65
|
+
logV.push(Math.log(v));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let beta = 0.5, K = 1;
|
|
69
|
+
if (logN.length >= 2) {
|
|
70
|
+
const meanX = logN.reduce((a, b) => a + b, 0) / logN.length;
|
|
71
|
+
const meanY = logV.reduce((a, b) => a + b, 0) / logV.length;
|
|
72
|
+
let num = 0, den = 0;
|
|
73
|
+
for (let i = 0; i < logN.length; i++) {
|
|
74
|
+
num += (logN[i] - meanX) * (logV[i] - meanY);
|
|
75
|
+
den += (logN[i] - meanX) ** 2;
|
|
76
|
+
}
|
|
77
|
+
beta = den !== 0 ? num / den : 0.5;
|
|
78
|
+
K = Math.exp(meanY - beta * meanX);
|
|
79
|
+
}
|
|
80
|
+
return { curve, heapsK: K, heapsBeta: beta };
|
|
81
|
+
}
|
|
82
|
+
function concordance(tokens, keyword, windowSize = 5) {
|
|
83
|
+
const kw = keyword.toLowerCase();
|
|
84
|
+
const results = [];
|
|
85
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
86
|
+
if (tokens[i] === kw) {
|
|
87
|
+
const left = tokens.slice(Math.max(0, i - windowSize), i).join(' ');
|
|
88
|
+
const right = tokens.slice(i + 1, i + 1 + windowSize).join(' ');
|
|
89
|
+
results.push(`${left.padStart(40)} **${tokens[i]}** ${right}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
function lexLogic(expr) {
|
|
95
|
+
const tokens = [];
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (i < expr.length) {
|
|
98
|
+
const ch = expr[i];
|
|
99
|
+
if (/\s/.test(ch)) {
|
|
100
|
+
i++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '(') {
|
|
104
|
+
tokens.push({ type: 'LPAREN' });
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === ')') {
|
|
109
|
+
tokens.push({ type: 'RPAREN' });
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === '¬' || ch === '!' || ch === '~') {
|
|
114
|
+
tokens.push({ type: 'NOT' });
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (ch === '∧' || ch === '&') {
|
|
119
|
+
tokens.push({ type: 'AND' });
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '∨' || ch === '|') {
|
|
124
|
+
tokens.push({ type: 'OR' });
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '→' || (ch === '-' && expr[i + 1] === '>')) {
|
|
129
|
+
tokens.push({ type: 'IMPLIES' });
|
|
130
|
+
i += ch === '→' ? 1 : 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === '↔' || (ch === '<' && expr[i + 1] === '-' && expr[i + 2] === '>')) {
|
|
134
|
+
tokens.push({ type: 'IFF' });
|
|
135
|
+
i += ch === '↔' ? 1 : 3;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Multi-char keywords
|
|
139
|
+
const rest = expr.slice(i).toUpperCase();
|
|
140
|
+
if (rest.startsWith('AND') && (i + 3 >= expr.length || !/[A-Z]/i.test(expr[i + 3]))) {
|
|
141
|
+
tokens.push({ type: 'AND' });
|
|
142
|
+
i += 3;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (rest.startsWith('OR') && (i + 2 >= expr.length || !/[A-Z]/i.test(expr[i + 2]))) {
|
|
146
|
+
tokens.push({ type: 'OR' });
|
|
147
|
+
i += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (rest.startsWith('NOT') && (i + 3 >= expr.length || !/[A-Z]/i.test(expr[i + 3]))) {
|
|
151
|
+
tokens.push({ type: 'NOT' });
|
|
152
|
+
i += 3;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (rest.startsWith('IMPLIES') && (i + 7 >= expr.length || !/[A-Z]/i.test(expr[i + 7]))) {
|
|
156
|
+
tokens.push({ type: 'IMPLIES' });
|
|
157
|
+
i += 7;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (rest.startsWith('IFF') && (i + 3 >= expr.length || !/[A-Z]/i.test(expr[i + 3]))) {
|
|
161
|
+
tokens.push({ type: 'IFF' });
|
|
162
|
+
i += 3;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
// Variable (single uppercase letter)
|
|
166
|
+
if (/[A-Z]/i.test(ch)) {
|
|
167
|
+
let name = '';
|
|
168
|
+
while (i < expr.length && /[A-Za-z0-9_]/.test(expr[i])) {
|
|
169
|
+
name += expr[i];
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
tokens.push({ type: 'VAR', name: name.toUpperCase() });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
i++; // skip unknown
|
|
176
|
+
}
|
|
177
|
+
return tokens;
|
|
178
|
+
}
|
|
179
|
+
// Recursive descent parser — precedence (low to high): IFF, IMPLIES, OR, AND, NOT
|
|
180
|
+
class LogicParser {
|
|
181
|
+
tokens;
|
|
182
|
+
pos = 0;
|
|
183
|
+
constructor(tokens) {
|
|
184
|
+
this.tokens = tokens;
|
|
185
|
+
}
|
|
186
|
+
parse() {
|
|
187
|
+
const node = this.parseIff();
|
|
188
|
+
return node;
|
|
189
|
+
}
|
|
190
|
+
peek() { return this.tokens[this.pos]; }
|
|
191
|
+
advance() { return this.tokens[this.pos++]; }
|
|
192
|
+
parseIff() {
|
|
193
|
+
let left = this.parseImplies();
|
|
194
|
+
while (this.peek()?.type === 'IFF') {
|
|
195
|
+
this.advance();
|
|
196
|
+
const right = this.parseImplies();
|
|
197
|
+
left = { kind: 'iff', left, right };
|
|
198
|
+
}
|
|
199
|
+
return left;
|
|
200
|
+
}
|
|
201
|
+
parseImplies() {
|
|
202
|
+
let left = this.parseOr();
|
|
203
|
+
while (this.peek()?.type === 'IMPLIES') {
|
|
204
|
+
this.advance();
|
|
205
|
+
// Right-associative
|
|
206
|
+
const right = this.parseImplies();
|
|
207
|
+
left = { kind: 'implies', left, right };
|
|
208
|
+
}
|
|
209
|
+
return left;
|
|
210
|
+
}
|
|
211
|
+
parseOr() {
|
|
212
|
+
let left = this.parseAnd();
|
|
213
|
+
while (this.peek()?.type === 'OR') {
|
|
214
|
+
this.advance();
|
|
215
|
+
left = { kind: 'or', left, right: this.parseAnd() };
|
|
216
|
+
}
|
|
217
|
+
return left;
|
|
218
|
+
}
|
|
219
|
+
parseAnd() {
|
|
220
|
+
let left = this.parseNot();
|
|
221
|
+
while (this.peek()?.type === 'AND') {
|
|
222
|
+
this.advance();
|
|
223
|
+
left = { kind: 'and', left, right: this.parseNot() };
|
|
224
|
+
}
|
|
225
|
+
return left;
|
|
226
|
+
}
|
|
227
|
+
parseNot() {
|
|
228
|
+
if (this.peek()?.type === 'NOT') {
|
|
229
|
+
this.advance();
|
|
230
|
+
return { kind: 'not', operand: this.parseNot() };
|
|
231
|
+
}
|
|
232
|
+
return this.parsePrimary();
|
|
233
|
+
}
|
|
234
|
+
parsePrimary() {
|
|
235
|
+
const tok = this.peek();
|
|
236
|
+
if (tok?.type === 'LPAREN') {
|
|
237
|
+
this.advance();
|
|
238
|
+
const node = this.parseIff();
|
|
239
|
+
if (this.peek()?.type === 'RPAREN')
|
|
240
|
+
this.advance();
|
|
241
|
+
return node;
|
|
242
|
+
}
|
|
243
|
+
if (tok?.type === 'VAR') {
|
|
244
|
+
this.advance();
|
|
245
|
+
return { kind: 'var', name: tok.name };
|
|
246
|
+
}
|
|
247
|
+
// Fallback: treat as variable 'X'
|
|
248
|
+
this.advance();
|
|
249
|
+
return { kind: 'var', name: 'X' };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function extractVars(node) {
|
|
253
|
+
const vars = new Set();
|
|
254
|
+
const walk = (n) => {
|
|
255
|
+
if (n.kind === 'var')
|
|
256
|
+
vars.add(n.name);
|
|
257
|
+
else if (n.kind === 'not')
|
|
258
|
+
walk(n.operand);
|
|
259
|
+
else if ('left' in n && 'right' in n) {
|
|
260
|
+
walk(n.left);
|
|
261
|
+
walk(n.right);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
walk(node);
|
|
265
|
+
return vars;
|
|
266
|
+
}
|
|
267
|
+
function evalLogic(node, env) {
|
|
268
|
+
switch (node.kind) {
|
|
269
|
+
case 'var': return env.get(node.name) ?? false;
|
|
270
|
+
case 'not': return !evalLogic(node.operand, env);
|
|
271
|
+
case 'and': return evalLogic(node.left, env) && evalLogic(node.right, env);
|
|
272
|
+
case 'or': return evalLogic(node.left, env) || evalLogic(node.right, env);
|
|
273
|
+
case 'implies': return !evalLogic(node.left, env) || evalLogic(node.right, env);
|
|
274
|
+
case 'iff': return evalLogic(node.left, env) === evalLogic(node.right, env);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function formatLogicNode(node) {
|
|
278
|
+
switch (node.kind) {
|
|
279
|
+
case 'var': return node.name;
|
|
280
|
+
case 'not': {
|
|
281
|
+
const inner = formatLogicNode(node.operand);
|
|
282
|
+
return node.operand.kind === 'var' ? `¬${inner}` : `¬(${inner})`;
|
|
283
|
+
}
|
|
284
|
+
case 'and': return `(${formatLogicNode(node.left)} ∧ ${formatLogicNode(node.right)})`;
|
|
285
|
+
case 'or': return `(${formatLogicNode(node.left)} ∨ ${formatLogicNode(node.right)})`;
|
|
286
|
+
case 'implies': return `(${formatLogicNode(node.left)} → ${formatLogicNode(node.right)})`;
|
|
287
|
+
case 'iff': return `(${formatLogicNode(node.left)} ↔ ${formatLogicNode(node.right)})`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function buildTruthTable(node) {
|
|
291
|
+
const vars = [...extractVars(node)].sort();
|
|
292
|
+
if (vars.length > 6)
|
|
293
|
+
throw new Error('Too many variables (max 6)');
|
|
294
|
+
const rows = [];
|
|
295
|
+
const n = 1 << vars.length;
|
|
296
|
+
for (let i = 0; i < n; i++) {
|
|
297
|
+
const env = new Map();
|
|
298
|
+
const assignment = [];
|
|
299
|
+
for (let j = 0; j < vars.length; j++) {
|
|
300
|
+
const val = Boolean((i >> (vars.length - 1 - j)) & 1);
|
|
301
|
+
env.set(vars[j], val);
|
|
302
|
+
assignment.push(val);
|
|
303
|
+
}
|
|
304
|
+
rows.push({ assignment, result: evalLogic(node, env) });
|
|
305
|
+
}
|
|
306
|
+
return { vars, rows };
|
|
307
|
+
}
|
|
308
|
+
function checkInference(expression) {
|
|
309
|
+
// Known inference rules
|
|
310
|
+
const rules = [
|
|
311
|
+
{
|
|
312
|
+
name: 'Modus Ponens',
|
|
313
|
+
pattern: /\(?\s*(\w+)\s*(?:→|->)\s*(\w+)\s*\)?\s*(?:∧|&|,)\s*\1/i,
|
|
314
|
+
explain: (m) => `From ${m[1]} → ${m[2]} and ${m[1]}, we conclude ${m[2]}.`,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'Modus Tollens',
|
|
318
|
+
pattern: /\(?\s*(\w+)\s*(?:→|->)\s*(\w+)\s*\)?\s*(?:∧|&|,)\s*(?:¬|!|~)\s*\2/i,
|
|
319
|
+
explain: (m) => `From ${m[1]} → ${m[2]} and ¬${m[2]}, we conclude ¬${m[1]}.`,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'Disjunctive Syllogism',
|
|
323
|
+
pattern: /\(?\s*(\w+)\s*(?:∨|\|)\s*(\w+)\s*\)?\s*(?:∧|&|,)\s*(?:¬|!|~)\s*\1/i,
|
|
324
|
+
explain: (m) => `From ${m[1]} ∨ ${m[2]} and ¬${m[1]}, we conclude ${m[2]}.`,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'Hypothetical Syllogism',
|
|
328
|
+
pattern: /\(?\s*(\w+)\s*(?:→|->)\s*(\w+)\s*\)?\s*(?:∧|&|,)\s*\(?\s*\2\s*(?:→|->)\s*(\w+)\s*\)?/i,
|
|
329
|
+
explain: (m) => `From ${m[1]} → ${m[2]} and ${m[2]} → ${m[3]}, we conclude ${m[1]} → ${m[3]}.`,
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
const parts = ['## Inference Rule Analysis\n'];
|
|
333
|
+
let found = false;
|
|
334
|
+
for (const rule of rules) {
|
|
335
|
+
const m = expression.match(rule.pattern);
|
|
336
|
+
if (m) {
|
|
337
|
+
parts.push(`**${rule.name}** detected`);
|
|
338
|
+
parts.push(rule.explain(m));
|
|
339
|
+
found = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!found) {
|
|
343
|
+
parts.push('No standard inference rule pattern detected in this expression.');
|
|
344
|
+
parts.push('Supported rules: Modus Ponens, Modus Tollens, Disjunctive Syllogism, Hypothetical Syllogism.');
|
|
345
|
+
}
|
|
346
|
+
return parts.join('\n');
|
|
347
|
+
}
|
|
348
|
+
const FALLACY_TAXONOMY = [
|
|
349
|
+
{ name: 'Ad Hominem', category: 'Relevance', description: 'Attacking the person rather than the argument.',
|
|
350
|
+
patterns: [/\b(stupid|idiot|fool|moron|incompetent|ignorant|biased)\b/i, /\b(you|they|he|she)\b.*\b(wrong because|can't be trusted|would say that)\b/i, /\battack(s|ing)?\s+(the\s+)?(person|character|motives?)\b/i] },
|
|
351
|
+
{ name: 'Straw Man', category: 'Relevance', description: 'Misrepresenting someone\'s argument to make it easier to attack.',
|
|
352
|
+
patterns: [/\b(so (you're|you are) saying|what (you|they) really mean)\b/i, /\b(obviously|clearly) (thinks?|believes?|wants?)\b/i] },
|
|
353
|
+
{ name: 'False Dilemma', category: 'Presumption', description: 'Presenting only two options when more exist.',
|
|
354
|
+
patterns: [/\beither\s+.+\s+or\b/i, /\b(only (two|2)|no (other|third))\s*(option|choice|alternative)/i, /\byou('re|\s+are)\s+(either|with\s+us\s+or)/i] },
|
|
355
|
+
{ name: 'Slippery Slope', category: 'Presumption', description: 'Claiming one event will inevitably lead to extreme consequences without justification.',
|
|
356
|
+
patterns: [/\b(will (inevitably|eventually|necessarily)|next thing you know|before you know it|where will it end|lead(s)? to)\b/i, /\bif we (allow|let|permit).*then.*then\b/i] },
|
|
357
|
+
{ name: 'Appeal to Authority', category: 'Relevance', description: 'Using an authority figure\'s opinion as evidence, especially outside their expertise.',
|
|
358
|
+
patterns: [/\b(expert(s)?|scientist(s)?|professor|doctor|study|research)\s+(says?|shows?|proves?|confirms?)\b/i, /\baccording to\b/i, /\b(famous|renowned|well-known)\s+(person|figure|expert)\b/i] },
|
|
359
|
+
{ name: 'Circular Reasoning', category: 'Presumption', description: 'Using the conclusion as a premise (begging the question).',
|
|
360
|
+
patterns: [/\bbecause\s+.{5,40}\s+because\b/i, /\btrue because.{5,50}true\b/i, /\b(obviously|clearly|evidently)\s+true\b/i] },
|
|
361
|
+
{ name: 'Appeal to Emotion', category: 'Relevance', description: 'Manipulating emotions rather than using logic.',
|
|
362
|
+
patterns: [/\b(think of the children|innocent|suffering|victims?|heartless|cruel|monstrous)\b/i, /\b(imagine|picture|visualize)\s+(if|how|what)\b/i] },
|
|
363
|
+
{ name: 'Hasty Generalization', category: 'Weak Induction', description: 'Drawing a broad conclusion from insufficient evidence.',
|
|
364
|
+
patterns: [/\b(all|every|always|never|none)\b.*\b(because|since)\s+(one|a few|i|my|this one)\b/i, /\b(i know someone who|my friend|this one time)\b/i] },
|
|
365
|
+
{ name: 'Red Herring', category: 'Relevance', description: 'Introducing an irrelevant topic to divert attention.',
|
|
366
|
+
patterns: [/\bbut what about\b/i, /\b(the real (issue|problem|question)|more importantly|let's (talk|focus) (about|on) something)\b/i] },
|
|
367
|
+
{ name: 'Tu Quoque', category: 'Relevance', description: 'Deflecting criticism by pointing out the critic\'s hypocrisy.',
|
|
368
|
+
patterns: [/\b(you (also|too|yourself)|look who's talking|pot calling|hypocrit(e|ical))\b/i, /\bbut you\b/i] },
|
|
369
|
+
{ name: 'Appeal to Nature', category: 'Presumption', description: 'Arguing that what is natural is good or correct.',
|
|
370
|
+
patterns: [/\b(natural|unnatural|nature intended|against nature|way nature|god intended)\b/i] },
|
|
371
|
+
{ name: 'Bandwagon', category: 'Relevance', description: 'Arguing something is true because many people believe it.',
|
|
372
|
+
patterns: [/\b(everyone|everybody|millions|most people)\s+(knows?|believes?|thinks?|agrees?)\b/i, /\b(popular|mainstream|widely accepted)\b/i] },
|
|
373
|
+
{ name: 'False Cause', category: 'Weak Induction', description: 'Assuming correlation implies causation.',
|
|
374
|
+
patterns: [/\b(caused by|because of|leads? to|results? in)\b/i, /\b(after|since|ever since).*\b(therefore|so|thus|hence)\b/i] },
|
|
375
|
+
{ name: 'Equivocation', category: 'Ambiguity', description: 'Using a word with different meanings in the same argument.',
|
|
376
|
+
patterns: [/\b(in (one|another) sense|depends on what you mean|technically)\b/i] },
|
|
377
|
+
{ name: 'Appeal to Ignorance', category: 'Presumption', description: 'Claiming something is true because it hasn\'t been proven false (or vice versa).',
|
|
378
|
+
patterns: [/\b(no (evidence|proof)|hasn't been (proven|shown|disproven)|can't (prove|disprove))\b/i, /\b(absence of evidence|prove me wrong|nobody has shown)\b/i] },
|
|
379
|
+
{ name: 'Composition/Division', category: 'Ambiguity', description: 'Assuming what is true of parts is true of the whole, or vice versa.',
|
|
380
|
+
patterns: [/\b(each|every)\s+.+\s+(so|therefore|thus)\s+(the (whole|group|team|organization))\b/i] },
|
|
381
|
+
{ name: 'No True Scotsman', category: 'Presumption', description: 'Redefining criteria to exclude counterexamples.',
|
|
382
|
+
patterns: [/\b(no (true|real|genuine)|a real .+ would(n't| not))\b/i] },
|
|
383
|
+
{ name: 'Loaded Question', category: 'Presumption', description: 'Asking a question that contains an unjustified assumption.',
|
|
384
|
+
patterns: [/\b(have you stopped|when did you start|why do you always|do you still)\b/i] },
|
|
385
|
+
{ name: 'Genetic Fallacy', category: 'Relevance', description: 'Judging an argument based on its origin rather than its merit.',
|
|
386
|
+
patterns: [/\b(of course .+ would say|coming from|consider the source|that's (just|only) because)\b/i] },
|
|
387
|
+
{ name: 'Middle Ground', category: 'Presumption', description: 'Assuming the truth is always between two extremes.',
|
|
388
|
+
patterns: [/\b(compromise|middle ground|truth is (somewhere )?in (the )?between|both sides have a point)\b/i] },
|
|
389
|
+
];
|
|
390
|
+
function analyzeArgumentStructure(text) {
|
|
391
|
+
const sentences = sentenceSplit(text);
|
|
392
|
+
const conclusionIndicators = /\b(therefore|thus|hence|so|consequently|it follows|in conclusion|we can conclude|this means|this shows|this proves)\b/i;
|
|
393
|
+
const premiseIndicators = /\b(because|since|given that|as|for|the reason is|due to|assuming|if|whereas)\b/i;
|
|
394
|
+
const assumptionIndicators = /\b(obviously|clearly|everyone knows|it is evident|of course|naturally|surely|undoubtedly|it goes without saying)\b/i;
|
|
395
|
+
const premises = [];
|
|
396
|
+
const conclusions = [];
|
|
397
|
+
const assumptions = [];
|
|
398
|
+
const other = [];
|
|
399
|
+
for (const s of sentences) {
|
|
400
|
+
if (conclusionIndicators.test(s))
|
|
401
|
+
conclusions.push(s);
|
|
402
|
+
else if (premiseIndicators.test(s))
|
|
403
|
+
premises.push(s);
|
|
404
|
+
else if (assumptionIndicators.test(s))
|
|
405
|
+
assumptions.push(s);
|
|
406
|
+
else
|
|
407
|
+
other.push(s);
|
|
408
|
+
}
|
|
409
|
+
// If no conclusion found, treat the last sentence as likely conclusion
|
|
410
|
+
if (conclusions.length === 0 && sentences.length > 0) {
|
|
411
|
+
const last = other.pop() || premises.pop() || sentences[sentences.length - 1];
|
|
412
|
+
conclusions.push(`[Implied] ${last}`);
|
|
413
|
+
}
|
|
414
|
+
// If no premises found, treat remaining as premises
|
|
415
|
+
if (premises.length === 0 && other.length > 0) {
|
|
416
|
+
premises.push(...other.splice(0));
|
|
417
|
+
}
|
|
418
|
+
const parts = ['## Argument Structure\n'];
|
|
419
|
+
if (premises.length > 0) {
|
|
420
|
+
parts.push('### Premises');
|
|
421
|
+
premises.forEach((p, i) => parts.push(`${i + 1}. ${p}`));
|
|
422
|
+
parts.push('');
|
|
423
|
+
}
|
|
424
|
+
if (assumptions.length > 0) {
|
|
425
|
+
parts.push('### Implicit Assumptions');
|
|
426
|
+
assumptions.forEach((a, i) => parts.push(`${i + 1}. ${a}`));
|
|
427
|
+
parts.push('');
|
|
428
|
+
}
|
|
429
|
+
if (conclusions.length > 0) {
|
|
430
|
+
parts.push('### Conclusion(s)');
|
|
431
|
+
conclusions.forEach((c, i) => parts.push(`${i + 1}. ${c}`));
|
|
432
|
+
parts.push('');
|
|
433
|
+
}
|
|
434
|
+
if (other.length > 0) {
|
|
435
|
+
parts.push('### Supporting/Context Statements');
|
|
436
|
+
other.forEach((o, i) => parts.push(`${i + 1}. ${o}`));
|
|
437
|
+
parts.push('');
|
|
438
|
+
}
|
|
439
|
+
// Determine form
|
|
440
|
+
parts.push('### Logical Form');
|
|
441
|
+
if (premises.length > 0 && conclusions.length > 0) {
|
|
442
|
+
parts.push(`P1..P${premises.length} ${assumptions.length > 0 ? `+ A1..A${assumptions.length} (implicit) ` : ''}⊢ C${conclusions.length > 1 ? '1..C' + conclusions.length : ''}`);
|
|
443
|
+
}
|
|
444
|
+
parts.push(`\n**Sentence count**: ${sentences.length}`);
|
|
445
|
+
parts.push(`**Premise count**: ${premises.length}`);
|
|
446
|
+
parts.push(`**Conclusion count**: ${conclusions.length}`);
|
|
447
|
+
parts.push(`**Assumption count**: ${assumptions.length}`);
|
|
448
|
+
return parts.join('\n');
|
|
449
|
+
}
|
|
450
|
+
function checkFallacies(text) {
|
|
451
|
+
const detected = [];
|
|
452
|
+
for (const f of FALLACY_TAXONOMY) {
|
|
453
|
+
for (const p of f.patterns) {
|
|
454
|
+
const m = text.match(p);
|
|
455
|
+
if (m) {
|
|
456
|
+
detected.push({ fallacy: f, match: m[0] });
|
|
457
|
+
break; // One match per fallacy type is enough
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const parts = ['## Fallacy Analysis\n'];
|
|
462
|
+
if (detected.length === 0) {
|
|
463
|
+
parts.push('No obvious logical fallacies detected via pattern matching.');
|
|
464
|
+
parts.push('\n*Note: This is heuristic pattern matching. Subtle fallacies may not be detected, and some matches may be false positives. Human judgment is essential.*');
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
parts.push(`**${detected.length} potential fallac${detected.length === 1 ? 'y' : 'ies'} detected:**\n`);
|
|
468
|
+
for (const d of detected) {
|
|
469
|
+
parts.push(`### ${d.fallacy.name} *(${d.fallacy.category})*`);
|
|
470
|
+
parts.push(d.fallacy.description);
|
|
471
|
+
parts.push(`> Matched: "${d.match}"`);
|
|
472
|
+
parts.push('');
|
|
473
|
+
}
|
|
474
|
+
parts.push('*Note: Pattern-based detection. Review matches in context — some may be false positives.*');
|
|
475
|
+
}
|
|
476
|
+
return parts.join('\n');
|
|
477
|
+
}
|
|
478
|
+
function checkArgumentValidity(text) {
|
|
479
|
+
const structure = analyzeArgumentStructure(text);
|
|
480
|
+
const fallacies = checkFallacies(text);
|
|
481
|
+
const sentences = sentenceSplit(text);
|
|
482
|
+
const hasConclusion = sentences.some(s => /\b(therefore|thus|hence|so|consequently|it follows|in conclusion)\b/i.test(s));
|
|
483
|
+
const hasPremises = sentences.some(s => /\b(because|since|given that|as|for|the reason is)\b/i.test(s));
|
|
484
|
+
const fallacyCount = (fallacies.match(/###/g) || []).length;
|
|
485
|
+
const parts = ['## Validity Assessment\n'];
|
|
486
|
+
parts.push(structure);
|
|
487
|
+
parts.push('\n---\n');
|
|
488
|
+
parts.push(fallacies);
|
|
489
|
+
parts.push('\n---\n');
|
|
490
|
+
parts.push('### Overall Assessment');
|
|
491
|
+
if (!hasPremises)
|
|
492
|
+
parts.push('- **Warning**: No explicit premise indicators found.');
|
|
493
|
+
if (!hasConclusion)
|
|
494
|
+
parts.push('- **Warning**: No explicit conclusion indicators found.');
|
|
495
|
+
if (fallacyCount > 0)
|
|
496
|
+
parts.push(`- **Warning**: ${fallacyCount} potential fallac${fallacyCount === 1 ? 'y' : 'ies'} detected.`);
|
|
497
|
+
if (hasPremises && hasConclusion && fallacyCount === 0) {
|
|
498
|
+
parts.push('- Argument has identifiable premises and conclusion with no detected fallacies.');
|
|
499
|
+
parts.push('- **Provisional assessment**: Structurally sound (further semantic analysis recommended).');
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
parts.push('- **Provisional assessment**: Argument has structural weaknesses. See above for details.');
|
|
503
|
+
}
|
|
504
|
+
return parts.join('\n');
|
|
505
|
+
}
|
|
506
|
+
function applyUtilitarian(dilemma) {
|
|
507
|
+
return {
|
|
508
|
+
framework: 'Utilitarianism',
|
|
509
|
+
principle: 'Greatest good for the greatest number (Bentham, Mill). Maximize aggregate well-being; minimize aggregate suffering.',
|
|
510
|
+
analysis: `**Utilitarian calculus applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
511
|
+
'1. **Identify stakeholders**: Who is affected? List all parties.\n' +
|
|
512
|
+
'2. **Predict outcomes**: For each possible action, estimate positive and negative consequences.\n' +
|
|
513
|
+
'3. **Quantify well-being**: Consider intensity, duration, certainty, proximity, fecundity, and purity of pleasure/pain (Bentham\'s felicific calculus).\n' +
|
|
514
|
+
'4. **Aggregate**: Sum up well-being across all stakeholders for each option.\n' +
|
|
515
|
+
'5. **Select**: Choose the action that produces the greatest net well-being.',
|
|
516
|
+
likelyConclusion: 'The action that maximizes total well-being across all affected parties is ethically correct, even if it requires sacrifice from some individuals.',
|
|
517
|
+
keyQuestion: 'Which action produces the greatest net benefit for all affected parties?',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function applyDeontological(dilemma) {
|
|
521
|
+
return {
|
|
522
|
+
framework: 'Deontological Ethics (Kant)',
|
|
523
|
+
principle: 'Act only according to maxims you could will to be universal laws. Treat humanity never merely as means but always as ends.',
|
|
524
|
+
analysis: `**Kantian analysis applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
525
|
+
'1. **Formulate the maxim**: What rule are you following? ("I will X in situation Y")\n' +
|
|
526
|
+
'2. **Universalizability test**: Could everyone follow this maxim without contradiction?\n' +
|
|
527
|
+
'3. **Humanity formula**: Does this action treat all persons as ends in themselves, not merely as means?\n' +
|
|
528
|
+
'4. **Kingdom of Ends**: Would this be acceptable legislation in an ideal moral community?\n' +
|
|
529
|
+
'5. **Perfect vs. imperfect duties**: Is this a duty of strict obligation or one allowing latitude?',
|
|
530
|
+
likelyConclusion: 'Actions that violate the categorical imperative are wrong regardless of consequences. Duties are absolute and cannot be overridden by outcomes.',
|
|
531
|
+
keyQuestion: 'Can the maxim of this action be universalized without contradiction? Does it respect the dignity of all persons?',
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function applyVirtueEthics(dilemma) {
|
|
535
|
+
return {
|
|
536
|
+
framework: 'Virtue Ethics (Aristotle)',
|
|
537
|
+
principle: 'Cultivate virtuous character traits (courage, temperance, justice, prudence). Act as a person of practical wisdom (phronesis) would act.',
|
|
538
|
+
analysis: `**Virtue ethics analysis applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
539
|
+
'1. **Character question**: What would a person of good character do?\n' +
|
|
540
|
+
'2. **Relevant virtues**: Which virtues are at stake? (justice, courage, temperance, honesty, compassion, generosity, prudence)\n' +
|
|
541
|
+
'3. **The mean**: Find the golden mean between excess and deficiency for each virtue.\n' +
|
|
542
|
+
'4. **Role models**: What would an exemplar of virtue do in this situation?\n' +
|
|
543
|
+
'5. **Eudaimonia**: Does this action contribute to human flourishing?',
|
|
544
|
+
likelyConclusion: 'The right action is what a practically wise person would do — one who has cultivated virtues through habit and reflects on the good life.',
|
|
545
|
+
keyQuestion: 'What would a person of exemplary character do? Which virtues does each option express?',
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function applyCareEthics(dilemma) {
|
|
549
|
+
return {
|
|
550
|
+
framework: 'Care Ethics (Gilligan, Noddings)',
|
|
551
|
+
principle: 'Prioritize relationships and responsiveness to the needs of particular others. Morality arises from caring connections, not abstract rules.',
|
|
552
|
+
analysis: `**Care ethics analysis applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
553
|
+
'1. **Relationships**: Who are the particular people involved? What are the care relationships?\n' +
|
|
554
|
+
'2. **Needs**: What are the concrete needs of the vulnerable or dependent parties?\n' +
|
|
555
|
+
'3. **Responsiveness**: How can we best respond to these needs while maintaining relationships?\n' +
|
|
556
|
+
'4. **Context**: What does the particular situation demand? (Care ethics rejects one-size-fits-all rules.)\n' +
|
|
557
|
+
'5. **Power dynamics**: Who holds power? Are the voices of the less powerful being heard?',
|
|
558
|
+
likelyConclusion: 'The ethical response prioritizes maintaining caring relationships, attending to the concrete needs of particular others, especially the vulnerable.',
|
|
559
|
+
keyQuestion: 'How can we best maintain caring relationships and attend to the needs of those who depend on us?',
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function applyRightsBased(dilemma) {
|
|
563
|
+
return {
|
|
564
|
+
framework: 'Rights-Based Ethics (Locke, Rawls)',
|
|
565
|
+
principle: 'Every person has fundamental rights (life, liberty, property, equality) that cannot be overridden by aggregate utility or state interest.',
|
|
566
|
+
analysis: `**Rights-based analysis applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
567
|
+
'1. **Rights identification**: What rights are at stake? (life, liberty, privacy, property, equality, due process)\n' +
|
|
568
|
+
'2. **Rights holders**: Whose rights are affected?\n' +
|
|
569
|
+
'3. **Conflicts**: Do rights of different parties conflict? If so, which takes priority?\n' +
|
|
570
|
+
'4. **Rawlsian veil**: Would rational agents behind a veil of ignorance agree to this arrangement?\n' +
|
|
571
|
+
'5. **Negative vs. positive rights**: Are we talking about freedom from interference or entitlement to something?',
|
|
572
|
+
likelyConclusion: 'Actions that violate fundamental rights are wrong even if they produce good outcomes. Rights serve as side constraints on permissible action.',
|
|
573
|
+
keyQuestion: 'Does this action respect the fundamental rights of all parties? Would it be accepted behind a veil of ignorance?',
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function applySocialContract(dilemma) {
|
|
577
|
+
return {
|
|
578
|
+
framework: 'Social Contract Theory (Hobbes, Rousseau, Rawls)',
|
|
579
|
+
principle: 'Moral rules derive from agreements rational agents would make under fair conditions. Justice is what free people would consent to.',
|
|
580
|
+
analysis: `**Social contract analysis applied to**: "${dilemma.slice(0, 100)}..."\n\n` +
|
|
581
|
+
'1. **State of nature**: What would happen without any agreement or rule in this situation?\n' +
|
|
582
|
+
'2. **Rational agreement**: What rule would rational, self-interested parties agree to?\n' +
|
|
583
|
+
'3. **Fairness conditions**: Are all parties negotiating from equal standing? (If not, apply Rawls\' veil of ignorance.)\n' +
|
|
584
|
+
'4. **Enforcement**: Can the agreement be enforced? What happens to defectors?\n' +
|
|
585
|
+
'5. **Consent**: Have affected parties actually (or hypothetically) consented to this arrangement?',
|
|
586
|
+
likelyConclusion: 'The morally correct action is one that rational agents would agree to under fair bargaining conditions, where no party has unfair advantage.',
|
|
587
|
+
keyQuestion: 'Would rational people freely agree to this arrangement under fair conditions?',
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function parseDate(s) {
|
|
591
|
+
// Support various formats: "1776", "1776-07-04", "476 CE", "-500" (500 BCE), "March 15, 44 BCE"
|
|
592
|
+
const bceMatch = s.match(/(\d+)\s*(BCE|BC)/i);
|
|
593
|
+
if (bceMatch)
|
|
594
|
+
return new Date(-parseInt(bceMatch[1], 10), 0, 1);
|
|
595
|
+
const ceMatch = s.match(/(\d+)\s*(CE|AD)/i);
|
|
596
|
+
if (ceMatch)
|
|
597
|
+
return new Date(parseInt(ceMatch[1], 10), 0, 1);
|
|
598
|
+
if (/^-?\d{1,4}$/.test(s.trim())) {
|
|
599
|
+
const year = parseInt(s.trim(), 10);
|
|
600
|
+
return new Date(year, 0, 1);
|
|
601
|
+
}
|
|
602
|
+
const d = new Date(s);
|
|
603
|
+
return isNaN(d.getTime()) ? null : d;
|
|
604
|
+
}
|
|
605
|
+
function formatYear(d) {
|
|
606
|
+
const y = d.getFullYear();
|
|
607
|
+
if (y < 0)
|
|
608
|
+
return `${Math.abs(y)} BCE`;
|
|
609
|
+
return `${y} CE`;
|
|
610
|
+
}
|
|
611
|
+
function yearsBetween(a, b) {
|
|
612
|
+
return Math.abs(a.getFullYear() - b.getFullYear());
|
|
613
|
+
}
|
|
614
|
+
function visualizeTimeline(events) {
|
|
615
|
+
const parsed = events
|
|
616
|
+
.map(e => ({ ...e, parsedDate: parseDate(e.date), parsedEnd: e.end_date ? parseDate(e.end_date) : null }))
|
|
617
|
+
.filter(e => e.parsedDate !== null)
|
|
618
|
+
.sort((a, b) => a.parsedDate.getTime() - b.parsedDate.getTime());
|
|
619
|
+
if (parsed.length === 0)
|
|
620
|
+
return 'No valid dates found.';
|
|
621
|
+
const lines = ['## Timeline\n', '```'];
|
|
622
|
+
const maxEventLen = Math.min(60, Math.max(...parsed.map(e => e.event.length)));
|
|
623
|
+
let prevDate = null;
|
|
624
|
+
for (const e of parsed) {
|
|
625
|
+
const dateStr = e.end_date && e.parsedEnd
|
|
626
|
+
? `${formatYear(e.parsedDate)} – ${formatYear(e.parsedEnd)}`
|
|
627
|
+
: formatYear(e.parsedDate);
|
|
628
|
+
// Show gap indicator
|
|
629
|
+
if (prevDate) {
|
|
630
|
+
const gap = yearsBetween(prevDate, e.parsedDate);
|
|
631
|
+
if (gap > 10) {
|
|
632
|
+
lines.push(` │ ··· ${gap} years ···`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const cat = e.category ? ` [${e.category}]` : '';
|
|
636
|
+
const period = e.parsedEnd ? ` (${yearsBetween(e.parsedDate, e.parsedEnd)} years)` : '';
|
|
637
|
+
lines.push(` ├── ${dateStr.padEnd(20)} ${e.event.slice(0, 60)}${cat}${period}`);
|
|
638
|
+
prevDate = e.parsedEnd || e.parsedDate;
|
|
639
|
+
}
|
|
640
|
+
lines.push(' │');
|
|
641
|
+
lines.push('```');
|
|
642
|
+
return lines.join('\n');
|
|
643
|
+
}
|
|
644
|
+
function analyzeTimeline(events) {
|
|
645
|
+
const parsed = events
|
|
646
|
+
.map(e => ({ ...e, parsedDate: parseDate(e.date), parsedEnd: e.end_date ? parseDate(e.end_date) : null }))
|
|
647
|
+
.filter(e => e.parsedDate !== null)
|
|
648
|
+
.sort((a, b) => a.parsedDate.getTime() - b.parsedDate.getTime());
|
|
649
|
+
if (parsed.length < 2)
|
|
650
|
+
return 'Need at least 2 events for analysis.';
|
|
651
|
+
const parts = ['## Timeline Analysis\n'];
|
|
652
|
+
// Total span
|
|
653
|
+
const first = parsed[0];
|
|
654
|
+
const last = parsed[parsed.length - 1];
|
|
655
|
+
parts.push(`**Total span**: ${formatYear(first.parsedDate)} to ${formatYear(last.parsedDate)} (${yearsBetween(first.parsedDate, last.parsedDate)} years)\n`);
|
|
656
|
+
// Event density
|
|
657
|
+
const totalYears = yearsBetween(first.parsedDate, last.parsedDate);
|
|
658
|
+
if (totalYears > 0) {
|
|
659
|
+
parts.push(`**Event density**: ${(parsed.length / totalYears * 100).toFixed(2)} events per century\n`);
|
|
660
|
+
}
|
|
661
|
+
// Categories
|
|
662
|
+
const cats = new Map();
|
|
663
|
+
for (const e of parsed) {
|
|
664
|
+
const cat = e.category || 'uncategorized';
|
|
665
|
+
cats.set(cat, (cats.get(cat) || 0) + 1);
|
|
666
|
+
}
|
|
667
|
+
if (cats.size > 1) {
|
|
668
|
+
parts.push('**Categories**:');
|
|
669
|
+
for (const [cat, count] of [...cats.entries()].sort((a, b) => b[1] - a[1])) {
|
|
670
|
+
parts.push(`- ${cat}: ${count} events`);
|
|
671
|
+
}
|
|
672
|
+
parts.push('');
|
|
673
|
+
}
|
|
674
|
+
// Gaps between consecutive events
|
|
675
|
+
const gaps = [];
|
|
676
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
677
|
+
const gap = yearsBetween(parsed[i - 1].parsedDate, parsed[i].parsedDate);
|
|
678
|
+
gaps.push({ from: parsed[i - 1].event.slice(0, 40), to: parsed[i].event.slice(0, 40), years: gap });
|
|
679
|
+
}
|
|
680
|
+
gaps.sort((a, b) => b.years - a.years);
|
|
681
|
+
if (gaps.length > 0) {
|
|
682
|
+
parts.push('**Largest gaps**:');
|
|
683
|
+
for (const g of gaps.slice(0, 3)) {
|
|
684
|
+
parts.push(`- ${g.years} years between "${g.from}" and "${g.to}"`);
|
|
685
|
+
}
|
|
686
|
+
parts.push('');
|
|
687
|
+
}
|
|
688
|
+
// Overlapping periods
|
|
689
|
+
const periods = parsed.filter(e => e.parsedEnd);
|
|
690
|
+
if (periods.length >= 2) {
|
|
691
|
+
parts.push('**Overlapping periods**:');
|
|
692
|
+
for (let i = 0; i < periods.length; i++) {
|
|
693
|
+
for (let j = i + 1; j < periods.length; j++) {
|
|
694
|
+
const aStart = periods[i].parsedDate.getTime();
|
|
695
|
+
const aEnd = periods[i].parsedEnd.getTime();
|
|
696
|
+
const bStart = periods[j].parsedDate.getTime();
|
|
697
|
+
const bEnd = periods[j].parsedEnd.getTime();
|
|
698
|
+
if (aStart < bEnd && bStart < aEnd) {
|
|
699
|
+
const overlapStart = Math.max(aStart, bStart);
|
|
700
|
+
const overlapEnd = Math.min(aEnd, bEnd);
|
|
701
|
+
const overlapYears = Math.round((overlapEnd - overlapStart) / (365.25 * 24 * 60 * 60 * 1000));
|
|
702
|
+
parts.push(`- "${periods[i].event.slice(0, 30)}" & "${periods[j].event.slice(0, 30)}" overlap ~${overlapYears} years`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
parts.push('');
|
|
707
|
+
}
|
|
708
|
+
return parts.join('\n');
|
|
709
|
+
}
|
|
710
|
+
function detectPeriods(events) {
|
|
711
|
+
const parsed = events
|
|
712
|
+
.map(e => ({ ...e, parsedDate: parseDate(e.date) }))
|
|
713
|
+
.filter(e => e.parsedDate !== null)
|
|
714
|
+
.sort((a, b) => a.parsedDate.getTime() - b.parsedDate.getTime());
|
|
715
|
+
if (parsed.length < 3)
|
|
716
|
+
return 'Need at least 3 events for periodization.';
|
|
717
|
+
const parts = ['## Periodization\n'];
|
|
718
|
+
// Cluster events by temporal proximity using gap analysis
|
|
719
|
+
const gaps = [];
|
|
720
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
721
|
+
gaps.push(yearsBetween(parsed[i - 1].parsedDate, parsed[i].parsedDate));
|
|
722
|
+
}
|
|
723
|
+
const avgGap = gaps.reduce((a, b) => a + b, 0) / gaps.length;
|
|
724
|
+
const threshold = avgGap * 1.5;
|
|
725
|
+
// Split into periods at large gaps
|
|
726
|
+
const periods = [[parsed[0]]];
|
|
727
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
728
|
+
if (gaps[i - 1] > threshold) {
|
|
729
|
+
periods.push([parsed[i]]);
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
periods[periods.length - 1].push(parsed[i]);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (let i = 0; i < periods.length; i++) {
|
|
736
|
+
const p = periods[i];
|
|
737
|
+
const start = formatYear(p[0].parsedDate);
|
|
738
|
+
const end = formatYear(p[p.length - 1].parsedDate);
|
|
739
|
+
const span = yearsBetween(p[0].parsedDate, p[p.length - 1].parsedDate);
|
|
740
|
+
parts.push(`### Period ${i + 1}: ${start} – ${end} (${span} years)`);
|
|
741
|
+
parts.push(`**Events**: ${p.length}`);
|
|
742
|
+
for (const e of p) {
|
|
743
|
+
parts.push(`- ${formatYear(e.parsedDate)}: ${e.event}`);
|
|
744
|
+
}
|
|
745
|
+
parts.push('');
|
|
746
|
+
}
|
|
747
|
+
parts.push(`**Gap threshold**: ${Math.round(threshold)} years (1.5x average gap of ${Math.round(avgGap)} years)`);
|
|
748
|
+
return parts.join('\n');
|
|
749
|
+
}
|
|
750
|
+
const LANGUAGES = [
|
|
751
|
+
{ name: 'Mandarin Chinese', family: 'Sino-Tibetan', subfamily: 'Sinitic', wordOrder: 'SVO', morphologicalType: 'isolating', writingSystem: 'logographic (Hanzi)', phonemeCount: 35, tonal: true, caseSystem: 'none', speakers: '~920M native' },
|
|
752
|
+
{ name: 'Spanish', family: 'Indo-European', subfamily: 'Romance', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 39, tonal: false, caseSystem: 'none (pronominal)', speakers: '~475M native' },
|
|
753
|
+
{ name: 'English', family: 'Indo-European', subfamily: 'Germanic', wordOrder: 'SVO', morphologicalType: 'fusional (weakly)', writingSystem: 'Latin', phonemeCount: 44, tonal: false, caseSystem: 'pronominal only', speakers: '~380M native' },
|
|
754
|
+
{ name: 'Hindi', family: 'Indo-European', subfamily: 'Indo-Aryan', wordOrder: 'SOV', morphologicalType: 'fusional', writingSystem: 'Devanagari', phonemeCount: 52, tonal: false, caseSystem: '3 cases', speakers: '~345M native' },
|
|
755
|
+
{ name: 'Arabic (Standard)', family: 'Afro-Asiatic', subfamily: 'Semitic', wordOrder: 'VSO/SVO', morphologicalType: 'fusional (root-pattern)', writingSystem: 'Arabic abjad', phonemeCount: 34, tonal: false, caseSystem: '3 cases', speakers: '~310M native' },
|
|
756
|
+
{ name: 'Bengali', family: 'Indo-European', subfamily: 'Indo-Aryan', wordOrder: 'SOV', morphologicalType: 'fusional', writingSystem: 'Bengali script', phonemeCount: 49, tonal: false, caseSystem: '4 cases', speakers: '~230M native' },
|
|
757
|
+
{ name: 'Portuguese', family: 'Indo-European', subfamily: 'Romance', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 37, tonal: false, caseSystem: 'none (pronominal)', speakers: '~220M native' },
|
|
758
|
+
{ name: 'Russian', family: 'Indo-European', subfamily: 'Slavic', wordOrder: 'SVO (flexible)', morphologicalType: 'fusional', writingSystem: 'Cyrillic', phonemeCount: 43, tonal: false, caseSystem: '6 cases', speakers: '~150M native' },
|
|
759
|
+
{ name: 'Japanese', family: 'Japonic', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Kanji + Kana (mixed)', phonemeCount: 24, tonal: false, caseSystem: 'particles (9+)', speakers: '~125M native' },
|
|
760
|
+
{ name: 'Punjabi', family: 'Indo-European', subfamily: 'Indo-Aryan', wordOrder: 'SOV', morphologicalType: 'fusional', writingSystem: 'Gurmukhi/Shahmukhi', phonemeCount: 48, tonal: true, caseSystem: '5 cases', speakers: '~120M native' },
|
|
761
|
+
{ name: 'German', family: 'Indo-European', subfamily: 'Germanic', wordOrder: 'SVO/SOV (V2)', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 44, tonal: false, caseSystem: '4 cases', speakers: '~95M native' },
|
|
762
|
+
{ name: 'Javanese', family: 'Austronesian', subfamily: 'Malayo-Polynesian', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin/Javanese', phonemeCount: 33, tonal: false, caseSystem: 'none', speakers: '~82M native' },
|
|
763
|
+
{ name: 'Korean', family: 'Koreanic', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Hangul (featural)', phonemeCount: 40, tonal: false, caseSystem: 'particles (7+)', speakers: '~77M native' },
|
|
764
|
+
{ name: 'French', family: 'Indo-European', subfamily: 'Romance', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 37, tonal: false, caseSystem: 'none (pronominal)', speakers: '~77M native' },
|
|
765
|
+
{ name: 'Turkish', family: 'Turkic', subfamily: 'Oghuz', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 32, tonal: false, caseSystem: '6 cases', speakers: '~75M native' },
|
|
766
|
+
{ name: 'Vietnamese', family: 'Austroasiatic', subfamily: 'Vietic', wordOrder: 'SVO', morphologicalType: 'isolating', writingSystem: 'Latin (Quoc Ngu)', phonemeCount: 33, tonal: true, caseSystem: 'none', speakers: '~75M native' },
|
|
767
|
+
{ name: 'Tamil', family: 'Dravidian', subfamily: 'South Dravidian', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Tamil script', phonemeCount: 39, tonal: false, caseSystem: '8 cases', speakers: '~70M native' },
|
|
768
|
+
{ name: 'Italian', family: 'Indo-European', subfamily: 'Romance', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 30, tonal: false, caseSystem: 'none (pronominal)', speakers: '~65M native' },
|
|
769
|
+
{ name: 'Urdu', family: 'Indo-European', subfamily: 'Indo-Aryan', wordOrder: 'SOV', morphologicalType: 'fusional', writingSystem: 'Perso-Arabic (Nastaliq)', phonemeCount: 53, tonal: false, caseSystem: '3 cases', speakers: '~65M native' },
|
|
770
|
+
{ name: 'Swahili', family: 'Niger-Congo', subfamily: 'Bantu', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 33, tonal: false, caseSystem: 'none', speakers: '~16M native, ~100M L2' },
|
|
771
|
+
{ name: 'Thai', family: 'Kra-Dai', subfamily: 'Tai', wordOrder: 'SVO', morphologicalType: 'isolating', writingSystem: 'Thai abugida', phonemeCount: 40, tonal: true, caseSystem: 'none', speakers: '~60M native' },
|
|
772
|
+
{ name: 'Polish', family: 'Indo-European', subfamily: 'Slavic', wordOrder: 'SVO (flexible)', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 43, tonal: false, caseSystem: '7 cases', speakers: '~40M native' },
|
|
773
|
+
{ name: 'Ukrainian', family: 'Indo-European', subfamily: 'Slavic', wordOrder: 'SVO (flexible)', morphologicalType: 'fusional', writingSystem: 'Cyrillic', phonemeCount: 42, tonal: false, caseSystem: '7 cases', speakers: '~35M native' },
|
|
774
|
+
{ name: 'Malay/Indonesian', family: 'Austronesian', subfamily: 'Malayo-Polynesian', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 31, tonal: false, caseSystem: 'none', speakers: '~43M native, ~200M L2' },
|
|
775
|
+
{ name: 'Persian (Farsi)', family: 'Indo-European', subfamily: 'Iranian', wordOrder: 'SOV', morphologicalType: 'fusional', writingSystem: 'Perso-Arabic', phonemeCount: 29, tonal: false, caseSystem: 'none', speakers: '~55M native' },
|
|
776
|
+
{ name: 'Dutch', family: 'Indo-European', subfamily: 'Germanic', wordOrder: 'SVO/SOV (V2)', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 40, tonal: false, caseSystem: 'none (archaic)', speakers: '~24M native' },
|
|
777
|
+
{ name: 'Greek', family: 'Indo-European', subfamily: 'Hellenic', wordOrder: 'SVO (flexible)', morphologicalType: 'fusional', writingSystem: 'Greek', phonemeCount: 24, tonal: false, caseSystem: '4 cases', speakers: '~13M native' },
|
|
778
|
+
{ name: 'Hungarian', family: 'Uralic', subfamily: 'Ugric', wordOrder: 'SVO (flexible)', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 40, tonal: false, caseSystem: '18 cases', speakers: '~13M native' },
|
|
779
|
+
{ name: 'Finnish', family: 'Uralic', subfamily: 'Finnic', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 28, tonal: false, caseSystem: '15 cases', speakers: '~5.4M native' },
|
|
780
|
+
{ name: 'Hebrew', family: 'Afro-Asiatic', subfamily: 'Semitic', wordOrder: 'SVO', morphologicalType: 'fusional (root-pattern)', writingSystem: 'Hebrew abjad', phonemeCount: 30, tonal: false, caseSystem: 'none', speakers: '~5M native, ~9M total' },
|
|
781
|
+
{ name: 'Georgian', family: 'Kartvelian', wordOrder: 'SOV (flexible)', morphologicalType: 'agglutinative', writingSystem: 'Georgian (Mkhedruli)', phonemeCount: 34, tonal: false, caseSystem: '7 cases', speakers: '~3.7M native' },
|
|
782
|
+
{ name: 'Basque', family: 'Language isolate', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 30, tonal: false, caseSystem: '12 cases', speakers: '~750K native' },
|
|
783
|
+
{ name: 'Icelandic', family: 'Indo-European', subfamily: 'Germanic', wordOrder: 'SVO (V2)', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 36, tonal: false, caseSystem: '4 cases', speakers: '~330K native' },
|
|
784
|
+
{ name: 'Navajo', family: 'Na-Dene', subfamily: 'Athabaskan', wordOrder: 'SOV', morphologicalType: 'polysynthetic', writingSystem: 'Latin', phonemeCount: 47, tonal: true, caseSystem: 'none (verb-incorporated)', speakers: '~170K native' },
|
|
785
|
+
{ name: 'Quechua', family: 'Quechuan', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 31, tonal: false, caseSystem: '8+ cases', speakers: '~8M native' },
|
|
786
|
+
{ name: 'Yoruba', family: 'Niger-Congo', subfamily: 'Volta-Niger', wordOrder: 'SVO', morphologicalType: 'isolating', writingSystem: 'Latin', phonemeCount: 25, tonal: true, caseSystem: 'none', speakers: '~45M native' },
|
|
787
|
+
{ name: 'Amharic', family: 'Afro-Asiatic', subfamily: 'Semitic', wordOrder: 'SOV', morphologicalType: 'fusional (root-pattern)', writingSystem: 'Ge\'ez (abugida)', phonemeCount: 31, tonal: false, caseSystem: '2 cases', speakers: '~32M native' },
|
|
788
|
+
{ name: 'Zulu', family: 'Niger-Congo', subfamily: 'Bantu', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 47, tonal: true, caseSystem: 'none', speakers: '~12M native' },
|
|
789
|
+
{ name: 'Tibetan', family: 'Sino-Tibetan', subfamily: 'Tibeto-Burman', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Tibetan abugida', phonemeCount: 37, tonal: true, caseSystem: 'particles (5+)', speakers: '~6M native' },
|
|
790
|
+
{ name: 'Tagalog', family: 'Austronesian', subfamily: 'Malayo-Polynesian', wordOrder: 'VSO/VOS', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 28, tonal: false, caseSystem: '3 cases (focus)', speakers: '~28M native' },
|
|
791
|
+
{ name: 'Mongolian', family: 'Mongolic', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Cyrillic/Mongolian', phonemeCount: 35, tonal: false, caseSystem: '8 cases', speakers: '~5.2M native' },
|
|
792
|
+
{ name: 'Estonian', family: 'Uralic', subfamily: 'Finnic', wordOrder: 'SVO', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 36, tonal: false, caseSystem: '14 cases', speakers: '~1.1M native' },
|
|
793
|
+
{ name: 'Burmese', family: 'Sino-Tibetan', subfamily: 'Tibeto-Burman', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Burmese script', phonemeCount: 39, tonal: true, caseSystem: 'particles', speakers: '~33M native' },
|
|
794
|
+
{ name: 'Hausa', family: 'Afro-Asiatic', subfamily: 'Chadic', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin/Ajami', phonemeCount: 42, tonal: true, caseSystem: 'none', speakers: '~45M native' },
|
|
795
|
+
{ name: 'Khmer', family: 'Austroasiatic', subfamily: 'Khmeric', wordOrder: 'SVO', morphologicalType: 'isolating', writingSystem: 'Khmer abugida', phonemeCount: 33, tonal: false, caseSystem: 'none', speakers: '~16M native' },
|
|
796
|
+
{ name: 'Romanian', family: 'Indo-European', subfamily: 'Romance', wordOrder: 'SVO', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 31, tonal: false, caseSystem: '3 cases (with articles)', speakers: '~24M native' },
|
|
797
|
+
{ name: 'Czech', family: 'Indo-European', subfamily: 'Slavic', wordOrder: 'SVO (flexible)', morphologicalType: 'fusional', writingSystem: 'Latin', phonemeCount: 39, tonal: false, caseSystem: '7 cases', speakers: '~10.7M native' },
|
|
798
|
+
{ name: 'Somali', family: 'Afro-Asiatic', subfamily: 'Cushitic', wordOrder: 'SOV', morphologicalType: 'agglutinative', writingSystem: 'Latin', phonemeCount: 37, tonal: true, caseSystem: '4 cases', speakers: '~16M native' },
|
|
799
|
+
{ name: 'Ainu', family: 'Language isolate', wordOrder: 'SOV', morphologicalType: 'polysynthetic', writingSystem: 'Katakana/Latin', phonemeCount: 16, tonal: false, caseSystem: 'particles', speakers: '~10 native (critically endangered)' },
|
|
800
|
+
{ name: 'Inuktitut', family: 'Eskimo-Aleut', subfamily: 'Inuit', wordOrder: 'SOV', morphologicalType: 'polysynthetic', writingSystem: 'Canadian syllabics/Latin', phonemeCount: 18, tonal: false, caseSystem: '8 cases', speakers: '~35K native' },
|
|
801
|
+
];
|
|
802
|
+
const IPA_CHART = [
|
|
803
|
+
// Plosives
|
|
804
|
+
{ symbol: 'p', type: 'consonant', description: 'voiceless bilabial plosive', place: 'bilabial', manner: 'plosive', voicing: 'voiceless' },
|
|
805
|
+
{ symbol: 'b', type: 'consonant', description: 'voiced bilabial plosive', place: 'bilabial', manner: 'plosive', voicing: 'voiced' },
|
|
806
|
+
{ symbol: 't', type: 'consonant', description: 'voiceless alveolar plosive', place: 'alveolar', manner: 'plosive', voicing: 'voiceless' },
|
|
807
|
+
{ symbol: 'd', type: 'consonant', description: 'voiced alveolar plosive', place: 'alveolar', manner: 'plosive', voicing: 'voiced' },
|
|
808
|
+
{ symbol: 'ʈ', type: 'consonant', description: 'voiceless retroflex plosive', place: 'retroflex', manner: 'plosive', voicing: 'voiceless' },
|
|
809
|
+
{ symbol: 'ɖ', type: 'consonant', description: 'voiced retroflex plosive', place: 'retroflex', manner: 'plosive', voicing: 'voiced' },
|
|
810
|
+
{ symbol: 'c', type: 'consonant', description: 'voiceless palatal plosive', place: 'palatal', manner: 'plosive', voicing: 'voiceless' },
|
|
811
|
+
{ symbol: 'ɟ', type: 'consonant', description: 'voiced palatal plosive', place: 'palatal', manner: 'plosive', voicing: 'voiced' },
|
|
812
|
+
{ symbol: 'k', type: 'consonant', description: 'voiceless velar plosive', place: 'velar', manner: 'plosive', voicing: 'voiceless' },
|
|
813
|
+
{ symbol: 'ɡ', type: 'consonant', description: 'voiced velar plosive', place: 'velar', manner: 'plosive', voicing: 'voiced' },
|
|
814
|
+
{ symbol: 'q', type: 'consonant', description: 'voiceless uvular plosive', place: 'uvular', manner: 'plosive', voicing: 'voiceless' },
|
|
815
|
+
{ symbol: 'ɢ', type: 'consonant', description: 'voiced uvular plosive', place: 'uvular', manner: 'plosive', voicing: 'voiced' },
|
|
816
|
+
{ symbol: 'ʔ', type: 'consonant', description: 'glottal stop', place: 'glottal', manner: 'plosive', voicing: 'voiceless' },
|
|
817
|
+
// Nasals
|
|
818
|
+
{ symbol: 'm', type: 'consonant', description: 'voiced bilabial nasal', place: 'bilabial', manner: 'nasal', voicing: 'voiced' },
|
|
819
|
+
{ symbol: 'ɱ', type: 'consonant', description: 'voiced labiodental nasal', place: 'labiodental', manner: 'nasal', voicing: 'voiced' },
|
|
820
|
+
{ symbol: 'n', type: 'consonant', description: 'voiced alveolar nasal', place: 'alveolar', manner: 'nasal', voicing: 'voiced' },
|
|
821
|
+
{ symbol: 'ɳ', type: 'consonant', description: 'voiced retroflex nasal', place: 'retroflex', manner: 'nasal', voicing: 'voiced' },
|
|
822
|
+
{ symbol: 'ɲ', type: 'consonant', description: 'voiced palatal nasal', place: 'palatal', manner: 'nasal', voicing: 'voiced' },
|
|
823
|
+
{ symbol: 'ŋ', type: 'consonant', description: 'voiced velar nasal', place: 'velar', manner: 'nasal', voicing: 'voiced' },
|
|
824
|
+
{ symbol: 'ɴ', type: 'consonant', description: 'voiced uvular nasal', place: 'uvular', manner: 'nasal', voicing: 'voiced' },
|
|
825
|
+
// Trills
|
|
826
|
+
{ symbol: 'ʙ', type: 'consonant', description: 'voiced bilabial trill', place: 'bilabial', manner: 'trill', voicing: 'voiced' },
|
|
827
|
+
{ symbol: 'r', type: 'consonant', description: 'voiced alveolar trill', place: 'alveolar', manner: 'trill', voicing: 'voiced' },
|
|
828
|
+
{ symbol: 'ʀ', type: 'consonant', description: 'voiced uvular trill', place: 'uvular', manner: 'trill', voicing: 'voiced' },
|
|
829
|
+
// Taps/Flaps
|
|
830
|
+
{ symbol: 'ⱱ', type: 'consonant', description: 'voiced labiodental flap', place: 'labiodental', manner: 'flap', voicing: 'voiced' },
|
|
831
|
+
{ symbol: 'ɾ', type: 'consonant', description: 'voiced alveolar tap', place: 'alveolar', manner: 'tap', voicing: 'voiced' },
|
|
832
|
+
{ symbol: 'ɽ', type: 'consonant', description: 'voiced retroflex flap', place: 'retroflex', manner: 'flap', voicing: 'voiced' },
|
|
833
|
+
// Fricatives
|
|
834
|
+
{ symbol: 'ɸ', type: 'consonant', description: 'voiceless bilabial fricative', place: 'bilabial', manner: 'fricative', voicing: 'voiceless' },
|
|
835
|
+
{ symbol: 'β', type: 'consonant', description: 'voiced bilabial fricative', place: 'bilabial', manner: 'fricative', voicing: 'voiced' },
|
|
836
|
+
{ symbol: 'f', type: 'consonant', description: 'voiceless labiodental fricative', place: 'labiodental', manner: 'fricative', voicing: 'voiceless' },
|
|
837
|
+
{ symbol: 'v', type: 'consonant', description: 'voiced labiodental fricative', place: 'labiodental', manner: 'fricative', voicing: 'voiced' },
|
|
838
|
+
{ symbol: 'θ', type: 'consonant', description: 'voiceless dental fricative', place: 'dental', manner: 'fricative', voicing: 'voiceless' },
|
|
839
|
+
{ symbol: 'ð', type: 'consonant', description: 'voiced dental fricative', place: 'dental', manner: 'fricative', voicing: 'voiced' },
|
|
840
|
+
{ symbol: 's', type: 'consonant', description: 'voiceless alveolar fricative', place: 'alveolar', manner: 'fricative', voicing: 'voiceless' },
|
|
841
|
+
{ symbol: 'z', type: 'consonant', description: 'voiced alveolar fricative', place: 'alveolar', manner: 'fricative', voicing: 'voiced' },
|
|
842
|
+
{ symbol: 'ʃ', type: 'consonant', description: 'voiceless postalveolar fricative', place: 'postalveolar', manner: 'fricative', voicing: 'voiceless' },
|
|
843
|
+
{ symbol: 'ʒ', type: 'consonant', description: 'voiced postalveolar fricative', place: 'postalveolar', manner: 'fricative', voicing: 'voiced' },
|
|
844
|
+
{ symbol: 'ʂ', type: 'consonant', description: 'voiceless retroflex fricative', place: 'retroflex', manner: 'fricative', voicing: 'voiceless' },
|
|
845
|
+
{ symbol: 'ʐ', type: 'consonant', description: 'voiced retroflex fricative', place: 'retroflex', manner: 'fricative', voicing: 'voiced' },
|
|
846
|
+
{ symbol: 'ç', type: 'consonant', description: 'voiceless palatal fricative', place: 'palatal', manner: 'fricative', voicing: 'voiceless' },
|
|
847
|
+
{ symbol: 'ʝ', type: 'consonant', description: 'voiced palatal fricative', place: 'palatal', manner: 'fricative', voicing: 'voiced' },
|
|
848
|
+
{ symbol: 'x', type: 'consonant', description: 'voiceless velar fricative', place: 'velar', manner: 'fricative', voicing: 'voiceless' },
|
|
849
|
+
{ symbol: 'ɣ', type: 'consonant', description: 'voiced velar fricative', place: 'velar', manner: 'fricative', voicing: 'voiced' },
|
|
850
|
+
{ symbol: 'χ', type: 'consonant', description: 'voiceless uvular fricative', place: 'uvular', manner: 'fricative', voicing: 'voiceless' },
|
|
851
|
+
{ symbol: 'ʁ', type: 'consonant', description: 'voiced uvular fricative', place: 'uvular', manner: 'fricative', voicing: 'voiced' },
|
|
852
|
+
{ symbol: 'ħ', type: 'consonant', description: 'voiceless pharyngeal fricative', place: 'pharyngeal', manner: 'fricative', voicing: 'voiceless' },
|
|
853
|
+
{ symbol: 'ʕ', type: 'consonant', description: 'voiced pharyngeal fricative', place: 'pharyngeal', manner: 'fricative', voicing: 'voiced' },
|
|
854
|
+
{ symbol: 'h', type: 'consonant', description: 'voiceless glottal fricative', place: 'glottal', manner: 'fricative', voicing: 'voiceless' },
|
|
855
|
+
{ symbol: 'ɦ', type: 'consonant', description: 'voiced glottal fricative', place: 'glottal', manner: 'fricative', voicing: 'voiced' },
|
|
856
|
+
// Lateral fricatives
|
|
857
|
+
{ symbol: 'ɬ', type: 'consonant', description: 'voiceless alveolar lateral fricative', place: 'alveolar', manner: 'lateral fricative', voicing: 'voiceless' },
|
|
858
|
+
{ symbol: 'ɮ', type: 'consonant', description: 'voiced alveolar lateral fricative', place: 'alveolar', manner: 'lateral fricative', voicing: 'voiced' },
|
|
859
|
+
// Approximants
|
|
860
|
+
{ symbol: 'ʋ', type: 'consonant', description: 'voiced labiodental approximant', place: 'labiodental', manner: 'approximant', voicing: 'voiced' },
|
|
861
|
+
{ symbol: 'ɹ', type: 'consonant', description: 'voiced alveolar approximant', place: 'alveolar', manner: 'approximant', voicing: 'voiced' },
|
|
862
|
+
{ symbol: 'ɻ', type: 'consonant', description: 'voiced retroflex approximant', place: 'retroflex', manner: 'approximant', voicing: 'voiced' },
|
|
863
|
+
{ symbol: 'j', type: 'consonant', description: 'voiced palatal approximant', place: 'palatal', manner: 'approximant', voicing: 'voiced' },
|
|
864
|
+
{ symbol: 'ɰ', type: 'consonant', description: 'voiced velar approximant', place: 'velar', manner: 'approximant', voicing: 'voiced' },
|
|
865
|
+
// Lateral approximants
|
|
866
|
+
{ symbol: 'l', type: 'consonant', description: 'voiced alveolar lateral approximant', place: 'alveolar', manner: 'lateral approximant', voicing: 'voiced' },
|
|
867
|
+
{ symbol: 'ɭ', type: 'consonant', description: 'voiced retroflex lateral approximant', place: 'retroflex', manner: 'lateral approximant', voicing: 'voiced' },
|
|
868
|
+
{ symbol: 'ʎ', type: 'consonant', description: 'voiced palatal lateral approximant', place: 'palatal', manner: 'lateral approximant', voicing: 'voiced' },
|
|
869
|
+
{ symbol: 'ʟ', type: 'consonant', description: 'voiced velar lateral approximant', place: 'velar', manner: 'lateral approximant', voicing: 'voiced' },
|
|
870
|
+
// Affricates
|
|
871
|
+
{ symbol: 'ts', type: 'consonant', description: 'voiceless alveolar affricate', place: 'alveolar', manner: 'affricate', voicing: 'voiceless' },
|
|
872
|
+
{ symbol: 'dz', type: 'consonant', description: 'voiced alveolar affricate', place: 'alveolar', manner: 'affricate', voicing: 'voiced' },
|
|
873
|
+
{ symbol: 'tʃ', type: 'consonant', description: 'voiceless postalveolar affricate', place: 'postalveolar', manner: 'affricate', voicing: 'voiceless' },
|
|
874
|
+
{ symbol: 'dʒ', type: 'consonant', description: 'voiced postalveolar affricate', place: 'postalveolar', manner: 'affricate', voicing: 'voiced' },
|
|
875
|
+
// Co-articulated
|
|
876
|
+
{ symbol: 'w', type: 'consonant', description: 'voiced labial-velar approximant', place: 'labial-velar', manner: 'approximant', voicing: 'voiced' },
|
|
877
|
+
{ symbol: 'ɥ', type: 'consonant', description: 'voiced labial-palatal approximant', place: 'labial-palatal', manner: 'approximant', voicing: 'voiced' },
|
|
878
|
+
// Vowels — Cardinal vowels and common additions
|
|
879
|
+
{ symbol: 'i', type: 'vowel', description: 'close front unrounded vowel', height: 'close', backness: 'front', rounded: false },
|
|
880
|
+
{ symbol: 'y', type: 'vowel', description: 'close front rounded vowel', height: 'close', backness: 'front', rounded: true },
|
|
881
|
+
{ symbol: 'ɨ', type: 'vowel', description: 'close central unrounded vowel', height: 'close', backness: 'central', rounded: false },
|
|
882
|
+
{ symbol: 'ʉ', type: 'vowel', description: 'close central rounded vowel', height: 'close', backness: 'central', rounded: true },
|
|
883
|
+
{ symbol: 'ɯ', type: 'vowel', description: 'close back unrounded vowel', height: 'close', backness: 'back', rounded: false },
|
|
884
|
+
{ symbol: 'u', type: 'vowel', description: 'close back rounded vowel', height: 'close', backness: 'back', rounded: true },
|
|
885
|
+
{ symbol: 'ɪ', type: 'vowel', description: 'near-close near-front unrounded vowel', height: 'near-close', backness: 'near-front', rounded: false },
|
|
886
|
+
{ symbol: 'ʏ', type: 'vowel', description: 'near-close near-front rounded vowel', height: 'near-close', backness: 'near-front', rounded: true },
|
|
887
|
+
{ symbol: 'ʊ', type: 'vowel', description: 'near-close near-back rounded vowel', height: 'near-close', backness: 'near-back', rounded: true },
|
|
888
|
+
{ symbol: 'e', type: 'vowel', description: 'close-mid front unrounded vowel', height: 'close-mid', backness: 'front', rounded: false },
|
|
889
|
+
{ symbol: 'ø', type: 'vowel', description: 'close-mid front rounded vowel', height: 'close-mid', backness: 'front', rounded: true },
|
|
890
|
+
{ symbol: 'ɘ', type: 'vowel', description: 'close-mid central unrounded vowel', height: 'close-mid', backness: 'central', rounded: false },
|
|
891
|
+
{ symbol: 'ɵ', type: 'vowel', description: 'close-mid central rounded vowel', height: 'close-mid', backness: 'central', rounded: true },
|
|
892
|
+
{ symbol: 'ɤ', type: 'vowel', description: 'close-mid back unrounded vowel', height: 'close-mid', backness: 'back', rounded: false },
|
|
893
|
+
{ symbol: 'o', type: 'vowel', description: 'close-mid back rounded vowel', height: 'close-mid', backness: 'back', rounded: true },
|
|
894
|
+
{ symbol: 'ə', type: 'vowel', description: 'mid central vowel (schwa)', height: 'mid', backness: 'central', rounded: false },
|
|
895
|
+
{ symbol: 'ɛ', type: 'vowel', description: 'open-mid front unrounded vowel', height: 'open-mid', backness: 'front', rounded: false },
|
|
896
|
+
{ symbol: 'œ', type: 'vowel', description: 'open-mid front rounded vowel', height: 'open-mid', backness: 'front', rounded: true },
|
|
897
|
+
{ symbol: 'ɜ', type: 'vowel', description: 'open-mid central unrounded vowel', height: 'open-mid', backness: 'central', rounded: false },
|
|
898
|
+
{ symbol: 'ɞ', type: 'vowel', description: 'open-mid central rounded vowel', height: 'open-mid', backness: 'central', rounded: true },
|
|
899
|
+
{ symbol: 'ʌ', type: 'vowel', description: 'open-mid back unrounded vowel', height: 'open-mid', backness: 'back', rounded: false },
|
|
900
|
+
{ symbol: 'ɔ', type: 'vowel', description: 'open-mid back rounded vowel', height: 'open-mid', backness: 'back', rounded: true },
|
|
901
|
+
{ symbol: 'æ', type: 'vowel', description: 'near-open front unrounded vowel', height: 'near-open', backness: 'front', rounded: false },
|
|
902
|
+
{ symbol: 'ɐ', type: 'vowel', description: 'near-open central vowel', height: 'near-open', backness: 'central', rounded: false },
|
|
903
|
+
{ symbol: 'a', type: 'vowel', description: 'open front unrounded vowel', height: 'open', backness: 'front', rounded: false },
|
|
904
|
+
{ symbol: 'ɶ', type: 'vowel', description: 'open front rounded vowel', height: 'open', backness: 'front', rounded: true },
|
|
905
|
+
{ symbol: 'ɑ', type: 'vowel', description: 'open back unrounded vowel', height: 'open', backness: 'back', rounded: false },
|
|
906
|
+
{ symbol: 'ɒ', type: 'vowel', description: 'open back rounded vowel', height: 'open', backness: 'back', rounded: true },
|
|
907
|
+
// Suprasegmentals
|
|
908
|
+
{ symbol: 'ˈ', type: 'suprasegmental', description: 'primary stress' },
|
|
909
|
+
{ symbol: 'ˌ', type: 'suprasegmental', description: 'secondary stress' },
|
|
910
|
+
{ symbol: 'ː', type: 'suprasegmental', description: 'long vowel/consonant' },
|
|
911
|
+
{ symbol: '˘', type: 'suprasegmental', description: 'extra-short' },
|
|
912
|
+
// Diacritics
|
|
913
|
+
{ symbol: '̃', type: 'diacritic', description: 'nasalized' },
|
|
914
|
+
{ symbol: '̥', type: 'diacritic', description: 'voiceless (on normally voiced sound)' },
|
|
915
|
+
{ symbol: '̬', type: 'diacritic', description: 'voiced (on normally voiceless sound)' },
|
|
916
|
+
{ symbol: 'ʰ', type: 'diacritic', description: 'aspirated' },
|
|
917
|
+
{ symbol: 'ʷ', type: 'diacritic', description: 'labialized' },
|
|
918
|
+
{ symbol: 'ʲ', type: 'diacritic', description: 'palatalized' },
|
|
919
|
+
];
|
|
920
|
+
// Common English words with IPA transcriptions (General American)
|
|
921
|
+
const ENGLISH_IPA = {
|
|
922
|
+
the: 'ðə', a: 'eɪ (letter) / ə (article)', is: 'ɪz', are: 'ɑːɹ', was: 'wɑːz',
|
|
923
|
+
have: 'hæv', has: 'hæz', had: 'hæd', do: 'duː', does: 'dʌz',
|
|
924
|
+
will: 'wɪl', would: 'wʊd', could: 'kʊd', should: 'ʃʊd',
|
|
925
|
+
hello: 'hɛˈloʊ', world: 'wɜːɹld', water: 'ˈwɔːtəɹ', people: 'ˈpiːpəl',
|
|
926
|
+
language: 'ˈlæŋɡwɪdʒ', phonetics: 'fəˈnɛtɪks', linguistics: 'lɪŋˈɡwɪstɪks',
|
|
927
|
+
about: 'əˈbaʊt', above: 'əˈbʌv', after: 'ˈæftəɹ', again: 'əˈɡɛn',
|
|
928
|
+
all: 'ɔːl', also: 'ˈɔːlsoʊ', always: 'ˈɔːlweɪz', and: 'ænd',
|
|
929
|
+
because: 'bɪˈkɔːz', before: 'bɪˈfɔːɹ', between: 'bɪˈtwiːn',
|
|
930
|
+
boy: 'bɔɪ', girl: 'ɡɜːɹl', man: 'mæn', woman: 'ˈwʊmən',
|
|
931
|
+
child: 'tʃaɪld', children: 'ˈtʃɪldɹən', mother: 'ˈmʌðəɹ', father: 'ˈfɑːðəɹ',
|
|
932
|
+
cat: 'kæt', dog: 'dɔːɡ', house: 'haʊs', book: 'bʊk',
|
|
933
|
+
good: 'ɡʊd', great: 'ɡɹeɪt', beautiful: 'ˈbjuːtɪfəl',
|
|
934
|
+
think: 'θɪŋk', thought: 'θɔːt', through: 'θɹuː', though: 'ðoʊ',
|
|
935
|
+
enough: 'ɪˈnʌf', laugh: 'læf', cough: 'kɔːf', rough: 'ɹʌf',
|
|
936
|
+
knight: 'naɪt', knife: 'naɪf', know: 'noʊ', write: 'ɹaɪt',
|
|
937
|
+
psychology: 'saɪˈkɑːlədʒi', philosophy: 'fɪˈlɑːsəfi',
|
|
938
|
+
university: 'ˌjuːnɪˈvɜːɹsəti', education: 'ˌɛdʒuˈkeɪʃən',
|
|
939
|
+
computer: 'kəmˈpjuːtəɹ', technology: 'tɛkˈnɑːlədʒi',
|
|
940
|
+
international: 'ˌɪntəɹˈnæʃənəl', communication: 'kəˌmjuːnɪˈkeɪʃən',
|
|
941
|
+
information: 'ˌɪnfəɹˈmeɪʃən', government: 'ˈɡʌvəɹnmənt',
|
|
942
|
+
important: 'ɪmˈpɔːɹtənt', different: 'ˈdɪfəɹənt',
|
|
943
|
+
something: 'ˈsʌmθɪŋ', everything: 'ˈɛvɹiθɪŋ', nothing: 'ˈnʌθɪŋ',
|
|
944
|
+
music: 'ˈmjuːzɪk', science: 'ˈsaɪəns', history: 'ˈhɪstəɹi',
|
|
945
|
+
teacher: 'ˈtiːtʃəɹ', student: 'ˈstuːdənt', school: 'skuːl',
|
|
946
|
+
color: 'ˈkʌləɹ', nature: 'ˈneɪtʃəɹ', picture: 'ˈpɪktʃəɹ',
|
|
947
|
+
question: 'ˈkwɛstʃən', answer: 'ˈænsəɹ', example: 'ɪɡˈzæmpəl',
|
|
948
|
+
family: 'ˈfæməli', friend: 'fɹɛnd', together: 'təˈɡɛðəɹ',
|
|
949
|
+
country: 'ˈkʌntɹi', city: 'ˈsɪti', place: 'pleɪs',
|
|
950
|
+
year: 'jɪɹ', time: 'taɪm', day: 'deɪ', night: 'naɪt',
|
|
951
|
+
today: 'təˈdeɪ', tomorrow: 'təˈmɑːɹoʊ', yesterday: 'ˈjɛstəɹdeɪ',
|
|
952
|
+
};
|
|
953
|
+
const FUNCTION_WORDS = new Set([
|
|
954
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'that', 'this',
|
|
955
|
+
'is', 'was', 'are', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
|
956
|
+
'do', 'does', 'did', 'will', 'would', 'shall', 'should', 'can', 'could',
|
|
957
|
+
'may', 'might', 'must', 'of', 'in', 'to', 'for', 'with', 'on', 'at',
|
|
958
|
+
'from', 'by', 'as', 'into', 'through', 'during', 'before', 'after',
|
|
959
|
+
'above', 'below', 'between', 'up', 'down', 'out', 'off', 'over', 'under',
|
|
960
|
+
'not', 'no', 'nor', 'so', 'yet', 'both', 'either', 'neither', 'each',
|
|
961
|
+
'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some', 'such',
|
|
962
|
+
'than', 'too', 'very', 'just', 'also', 'now', 'here', 'there', 'when',
|
|
963
|
+
'where', 'why', 'how', 'what', 'which', 'who', 'whom', 'whose',
|
|
964
|
+
'i', 'me', 'my', 'we', 'us', 'our', 'you', 'your', 'he', 'him', 'his',
|
|
965
|
+
'she', 'her', 'it', 'its', 'they', 'them', 'their', 'about', 'while',
|
|
966
|
+
]);
|
|
967
|
+
function computeYulesK(tokens) {
|
|
968
|
+
// Yule's K = 10^4 * (M2 - N) / N^2 where M2 = sum(i^2 * V_i) and V_i = number of words occurring i times
|
|
969
|
+
const freq = new Map();
|
|
970
|
+
for (const t of tokens)
|
|
971
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
972
|
+
const spectrum = new Map(); // frequency -> how many words have that frequency
|
|
973
|
+
for (const count of freq.values()) {
|
|
974
|
+
spectrum.set(count, (spectrum.get(count) || 0) + 1);
|
|
975
|
+
}
|
|
976
|
+
const N = tokens.length;
|
|
977
|
+
let M2 = 0;
|
|
978
|
+
for (const [i, vi] of spectrum) {
|
|
979
|
+
M2 += i * i * vi;
|
|
980
|
+
}
|
|
981
|
+
if (N === 0)
|
|
982
|
+
return 0;
|
|
983
|
+
return 10000 * (M2 - N) / (N * N);
|
|
984
|
+
}
|
|
985
|
+
function profileText(label, text) {
|
|
986
|
+
const tokens = tokenize(text);
|
|
987
|
+
const sentences = sentenceSplit(text);
|
|
988
|
+
const types = new Set(tokens);
|
|
989
|
+
const hapax = hapaxLegomena(tokens);
|
|
990
|
+
// Function word frequencies (normalized)
|
|
991
|
+
const funcFreqs = new Map();
|
|
992
|
+
for (const fw of FUNCTION_WORDS) {
|
|
993
|
+
const count = tokens.filter(t => t === fw).length;
|
|
994
|
+
funcFreqs.set(fw, tokens.length > 0 ? count / tokens.length : 0);
|
|
995
|
+
}
|
|
996
|
+
// Punctuation frequencies
|
|
997
|
+
const punctFreqs = new Map();
|
|
998
|
+
const punctuation = text.match(/[.,;:!?'"()\-—–…]/g) || [];
|
|
999
|
+
for (const p of punctuation) {
|
|
1000
|
+
punctFreqs.set(p, (punctFreqs.get(p) || 0) + 1);
|
|
1001
|
+
}
|
|
1002
|
+
const totalPunct = punctuation.length || 1;
|
|
1003
|
+
for (const [k, v] of punctFreqs)
|
|
1004
|
+
punctFreqs.set(k, v / totalPunct);
|
|
1005
|
+
// Top N most frequent words
|
|
1006
|
+
const topWords = wordFrequency(tokens, 50);
|
|
1007
|
+
return {
|
|
1008
|
+
label,
|
|
1009
|
+
tokenCount: tokens.length,
|
|
1010
|
+
typeCount: types.size,
|
|
1011
|
+
sentenceCount: sentences.length,
|
|
1012
|
+
avgSentenceLength: sentences.length > 0 ? tokens.length / sentences.length : 0,
|
|
1013
|
+
avgWordLength: tokens.length > 0 ? tokens.reduce((sum, t) => sum + t.length, 0) / tokens.length : 0,
|
|
1014
|
+
ttr: tokens.length > 0 ? types.size / tokens.length : 0,
|
|
1015
|
+
hapaxRatio: tokens.length > 0 ? hapax.length / tokens.length : 0,
|
|
1016
|
+
yulesK: computeYulesK(tokens),
|
|
1017
|
+
functionWordFreqs: funcFreqs,
|
|
1018
|
+
punctuationFreqs: punctFreqs,
|
|
1019
|
+
topWords,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function formatProfile(p) {
|
|
1023
|
+
const lines = [
|
|
1024
|
+
`### "${p.label}"`,
|
|
1025
|
+
'',
|
|
1026
|
+
`| Metric | Value |`,
|
|
1027
|
+
`|--------|-------|`,
|
|
1028
|
+
`| Tokens | ${p.tokenCount} |`,
|
|
1029
|
+
`| Types (unique words) | ${p.typeCount} |`,
|
|
1030
|
+
`| Sentences | ${p.sentenceCount} |`,
|
|
1031
|
+
`| Avg sentence length | ${p.avgSentenceLength.toFixed(1)} words |`,
|
|
1032
|
+
`| Avg word length | ${p.avgWordLength.toFixed(2)} chars |`,
|
|
1033
|
+
`| Type-token ratio | ${p.ttr.toFixed(4)} |`,
|
|
1034
|
+
`| Hapax ratio | ${p.hapaxRatio.toFixed(4)} |`,
|
|
1035
|
+
`| Yule's K | ${p.yulesK.toFixed(2)} |`,
|
|
1036
|
+
'',
|
|
1037
|
+
'**Top function words**:',
|
|
1038
|
+
...[...p.functionWordFreqs.entries()]
|
|
1039
|
+
.filter(([, v]) => v > 0)
|
|
1040
|
+
.sort((a, b) => b[1] - a[1])
|
|
1041
|
+
.slice(0, 10)
|
|
1042
|
+
.map(([w, f]) => `- "${w}": ${(f * 100).toFixed(2)}%`),
|
|
1043
|
+
'',
|
|
1044
|
+
'**Punctuation profile**:',
|
|
1045
|
+
...[...p.punctuationFreqs.entries()]
|
|
1046
|
+
.sort((a, b) => b[1] - a[1])
|
|
1047
|
+
.slice(0, 8)
|
|
1048
|
+
.map(([p2, f]) => `- "${p2}": ${(f * 100).toFixed(1)}%`),
|
|
1049
|
+
];
|
|
1050
|
+
return lines.join('\n');
|
|
1051
|
+
}
|
|
1052
|
+
function burrowsDelta(profiles) {
|
|
1053
|
+
if (profiles.length < 2)
|
|
1054
|
+
return 'Need at least 2 texts for Burrows\' Delta comparison.';
|
|
1055
|
+
// Collect all word frequencies across all texts
|
|
1056
|
+
const allWords = new Map();
|
|
1057
|
+
for (const p of profiles) {
|
|
1058
|
+
for (const [word, count] of p.topWords) {
|
|
1059
|
+
if (!allWords.has(word))
|
|
1060
|
+
allWords.set(word, new Array(profiles.length).fill(0));
|
|
1061
|
+
const idx = profiles.indexOf(p);
|
|
1062
|
+
allWords.get(word)[idx] = count / p.tokenCount;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
// Select the N most frequent words across the corpus
|
|
1066
|
+
const totalFreqs = [...allWords.entries()]
|
|
1067
|
+
.map(([word, freqs]) => ({ word, total: freqs.reduce((a, b) => a + b, 0) }))
|
|
1068
|
+
.sort((a, b) => b.total - a.total)
|
|
1069
|
+
.slice(0, 100);
|
|
1070
|
+
const featureWords = totalFreqs.map(f => f.word);
|
|
1071
|
+
// Build frequency matrix
|
|
1072
|
+
const matrix = profiles.map((p, idx) => featureWords.map(w => allWords.get(w)?.[idx] ?? 0));
|
|
1073
|
+
// Compute z-scores per feature
|
|
1074
|
+
const means = featureWords.map((_, j) => {
|
|
1075
|
+
const col = matrix.map(row => row[j]);
|
|
1076
|
+
return col.reduce((a, b) => a + b, 0) / col.length;
|
|
1077
|
+
});
|
|
1078
|
+
const stds = featureWords.map((_, j) => {
|
|
1079
|
+
const col = matrix.map(row => row[j]);
|
|
1080
|
+
const mean = means[j];
|
|
1081
|
+
const variance = col.reduce((sum, v) => sum + (v - mean) ** 2, 0) / col.length;
|
|
1082
|
+
return Math.sqrt(variance) || 1e-10; // Avoid division by zero
|
|
1083
|
+
});
|
|
1084
|
+
const zMatrix = matrix.map(row => row.map((v, j) => (v - means[j]) / stds[j]));
|
|
1085
|
+
// Compute Manhattan distances between all pairs
|
|
1086
|
+
const parts = ['## Burrows\' Delta Analysis\n'];
|
|
1087
|
+
parts.push(`**Feature words used**: ${featureWords.length}`);
|
|
1088
|
+
parts.push('');
|
|
1089
|
+
parts.push('### Distance Matrix');
|
|
1090
|
+
parts.push('');
|
|
1091
|
+
// Header
|
|
1092
|
+
const header = '| | ' + profiles.map(p => `"${p.label}"`.slice(0, 15)).join(' | ') + ' |';
|
|
1093
|
+
const separator = '|---|' + profiles.map(() => '---').join('|') + '|';
|
|
1094
|
+
parts.push(header);
|
|
1095
|
+
parts.push(separator);
|
|
1096
|
+
const distances = [];
|
|
1097
|
+
for (let i = 0; i < profiles.length; i++) {
|
|
1098
|
+
const row = [];
|
|
1099
|
+
for (let j = 0; j < profiles.length; j++) {
|
|
1100
|
+
if (i === j) {
|
|
1101
|
+
row.push(0);
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
let dist = 0;
|
|
1105
|
+
for (let k = 0; k < featureWords.length; k++) {
|
|
1106
|
+
dist += Math.abs(zMatrix[i][k] - zMatrix[j][k]);
|
|
1107
|
+
}
|
|
1108
|
+
dist /= featureWords.length;
|
|
1109
|
+
row.push(dist);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
distances.push(row);
|
|
1113
|
+
parts.push(`| "${profiles[i].label}".slice(0,12) | ${row.map(d => d.toFixed(3)).join(' | ')} |`);
|
|
1114
|
+
}
|
|
1115
|
+
parts.push('');
|
|
1116
|
+
parts.push('### Interpretation');
|
|
1117
|
+
parts.push('- **Delta < 1.0**: Likely same author');
|
|
1118
|
+
parts.push('- **Delta 1.0-1.5**: Uncertain / similar style');
|
|
1119
|
+
parts.push('- **Delta > 1.5**: Likely different authors');
|
|
1120
|
+
// Find closest pair
|
|
1121
|
+
let minDist = Infinity;
|
|
1122
|
+
let minPair = [0, 1];
|
|
1123
|
+
for (let i = 0; i < profiles.length; i++) {
|
|
1124
|
+
for (let j = i + 1; j < profiles.length; j++) {
|
|
1125
|
+
if (distances[i][j] < minDist) {
|
|
1126
|
+
minDist = distances[i][j];
|
|
1127
|
+
minPair = [i, j];
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
parts.push(`\n**Closest pair**: "${profiles[minPair[0]].label}" & "${profiles[minPair[1]].label}" (Delta = ${minDist.toFixed(3)})`);
|
|
1132
|
+
return parts.join('\n');
|
|
1133
|
+
}
|
|
1134
|
+
const PHILOSOPHICAL_CONCEPTS = [
|
|
1135
|
+
// Metaphysics
|
|
1136
|
+
{ name: 'Substance Dualism', branch: 'metaphysics', period: 'Early Modern', thinkers: ['Descartes'], definition: 'Reality consists of two fundamentally different substances: mind (res cogitans) and matter (res extensa).', relatedConcepts: ['Mind-Body Problem', 'Materialism', 'Property Dualism'], argumentsFor: ['Conceivability argument', 'Divisibility argument', 'Qualia seem non-physical'], argumentsAgainst: ['Interaction problem', 'Causal closure of physics', 'Neural correlates of consciousness'] },
|
|
1137
|
+
{ name: 'Materialism', branch: 'metaphysics', period: 'Ancient/Modern', thinkers: ['Democritus', 'Hobbes', 'Smart', 'Armstrong'], definition: 'Everything that exists is physical matter or supervenes on physical matter. Mental states are identical to or reducible to brain states.', relatedConcepts: ['Physicalism', 'Identity Theory', 'Eliminativism'], argumentsFor: ['Causal closure of physics', 'Neuroscience progress', 'Parsimony'], argumentsAgainst: ['Hard problem of consciousness', 'Knowledge argument (Mary\'s Room)', 'Qualia'] },
|
|
1138
|
+
{ name: 'Free Will', branch: 'metaphysics', period: 'Ancient to Present', thinkers: ['Aristotle', 'Kant', 'Hume', 'Frankfurt', 'van Inwagen'], definition: 'The capacity of agents to choose between different possible courses of action unimpeded.', relatedConcepts: ['Determinism', 'Compatibilism', 'Libertarianism (metaphysical)', 'Moral Responsibility'], argumentsFor: ['Phenomenological experience of choosing', 'Moral responsibility presupposes it', 'Quantum indeterminacy'], argumentsAgainst: ['Causal determinism', 'Neuroscience (Libet experiments)', 'If random, not truly "free"'] },
|
|
1139
|
+
{ name: 'Determinism', branch: 'metaphysics', period: 'Ancient to Present', thinkers: ['Democritus', 'Laplace', 'Spinoza'], definition: 'Every event is necessitated by antecedent events and conditions plus the laws of nature.', relatedConcepts: ['Free Will', 'Compatibilism', 'Fatalism'], argumentsFor: ['Success of physical laws', 'Causal regularity', 'Predictability'], argumentsAgainst: ['Quantum mechanics', 'Chaos theory', 'Subjective experience of choice'] },
|
|
1140
|
+
{ name: 'Personal Identity', branch: 'metaphysics', period: 'Early Modern', thinkers: ['Locke', 'Hume', 'Parfit', 'Nozick'], definition: 'What makes a person the same person over time? Theories include psychological continuity, bodily continuity, and narrative identity.', relatedConcepts: ['Consciousness', 'Memory', 'Ship of Theseus'], argumentsFor: ['Memory continuity (Locke)', 'Bodily continuity', 'Narrative coherence'], argumentsAgainst: ['Circularity of memory criterion', 'Fission cases (Parfit)', 'Buddhist no-self'] },
|
|
1141
|
+
{ name: 'Universals', branch: 'metaphysics', period: 'Ancient', thinkers: ['Plato', 'Aristotle', 'Duns Scotus', 'Armstrong'], definition: 'Abstract properties or relations that can be instantiated by multiple particular things (e.g., "redness" in all red things).', relatedConcepts: ['Nominalism', 'Realism', 'Tropes', 'Forms'], argumentsFor: ['Explains predication', 'Needed for laws of nature', 'Mathematical objects'], argumentsAgainst: ['Parsimony favors nominalism', 'Third Man Argument', 'Location problem'] },
|
|
1142
|
+
{ name: 'Possible Worlds', branch: 'metaphysics', period: 'Modern', thinkers: ['Leibniz', 'Lewis', 'Kripke', 'Plantinga'], definition: 'Complete ways reality could have been. Used to analyze necessity, possibility, and counterfactuals.', relatedConcepts: ['Modal Logic', 'Necessity', 'Contingency', 'Counterpart Theory'], argumentsFor: ['Clarifies modal reasoning', 'Explains counterfactuals', 'Systematic semantics for modal logic'], argumentsAgainst: ['Ontological extravagance (Lewis)', 'Abstracta problem', 'Irrelevance to actual world'] },
|
|
1143
|
+
// Epistemology
|
|
1144
|
+
{ name: 'Empiricism', branch: 'epistemology', period: 'Early Modern', thinkers: ['Locke', 'Berkeley', 'Hume'], definition: 'All knowledge derives from sensory experience. The mind begins as a blank slate (tabula rasa).', relatedConcepts: ['Rationalism', 'Sensory Experience', 'Induction'], argumentsFor: ['Scientific method relies on observation', 'No innate ideas demonstrable', 'Developmental psychology'], argumentsAgainst: ['Mathematical knowledge seems non-empirical', 'Problem of induction (Hume)', 'Linguistic nativism (Chomsky)'] },
|
|
1145
|
+
{ name: 'Rationalism', branch: 'epistemology', period: 'Early Modern', thinkers: ['Descartes', 'Spinoza', 'Leibniz'], definition: 'Some knowledge is innate or can be derived through reason alone, independent of sensory experience.', relatedConcepts: ['Empiricism', 'A Priori', 'Innate Ideas'], argumentsFor: ['Mathematics and logic', 'Cogito argument', 'Universal grammar'], argumentsAgainst: ['Empirical success of science', 'No clear mechanism for innate ideas', 'Cultural variation'] },
|
|
1146
|
+
{ name: 'Skepticism', branch: 'epistemology', period: 'Ancient to Present', thinkers: ['Pyrrho', 'Sextus Empiricus', 'Descartes', 'Hume'], definition: 'The view that knowledge is impossible or that we should suspend judgment. Radical skepticism doubts even basic perceptual beliefs.', relatedConcepts: ['Foundationalism', 'Brain in a Vat', 'Cogito'], argumentsFor: ['Dream argument', 'Evil demon', 'Regress problem'], argumentsAgainst: ['Self-refuting', 'Pragmatic costs', 'Moorean shift'] },
|
|
1147
|
+
{ name: 'Justified True Belief', branch: 'epistemology', period: 'Ancient/Modern', thinkers: ['Plato', 'Gettier'], definition: 'The traditional analysis of knowledge as belief that is both true and justified. Challenged by Gettier cases.', relatedConcepts: ['Gettier Problem', 'Reliabilism', 'Knowledge'], argumentsFor: ['Intuitive analysis', 'Captures core conditions', 'Long philosophical tradition'], argumentsAgainst: ['Gettier counterexamples', 'No agreement on justification', 'Contextual variation'] },
|
|
1148
|
+
{ name: 'Foundationalism', branch: 'epistemology', period: 'Ancient/Modern', thinkers: ['Aristotle', 'Descartes', 'Russell'], definition: 'Knowledge has a structure: some beliefs are self-evident or basic, and all other beliefs rest on them.', relatedConcepts: ['Coherentism', 'Regress Problem', 'Basic Beliefs'], argumentsFor: ['Stops infinite regress', 'Self-evident truths exist', 'Science has axioms'], argumentsAgainst: ['Which beliefs are basic?', 'Basic beliefs may be fallible', 'Coherentist alternatives'] },
|
|
1149
|
+
{ name: 'Pragmatism', branch: 'epistemology', period: 'Modern', thinkers: ['Peirce', 'James', 'Dewey', 'Rorty'], definition: 'The meaning and truth of ideas consist in their practical consequences and usefulness.', relatedConcepts: ['Instrumentalism', 'Verificationism', 'Truth'], argumentsFor: ['Scientific method is pragmatic', 'Avoids fruitless metaphysical debates', 'Successful prediction'], argumentsAgainst: ['Conflates truth with utility', 'False beliefs can be useful', 'Relativism concerns'] },
|
|
1150
|
+
// Ethics
|
|
1151
|
+
{ name: 'Utilitarianism', branch: 'ethics', period: 'Modern', thinkers: ['Bentham', 'Mill', 'Singer', 'Sidgwick'], definition: 'The right action is the one that maximizes overall happiness or well-being for the greatest number.', relatedConcepts: ['Consequentialism', 'Hedonism', 'Felicific Calculus'], argumentsFor: ['Impartial', 'Measurable outcomes', 'Intuitive in many cases'], argumentsAgainst: ['Tyranny of the majority', 'Demandingness', 'Utility monster'] },
|
|
1152
|
+
{ name: 'Categorical Imperative', branch: 'ethics', period: 'Modern', thinkers: ['Kant'], definition: 'Act only according to that maxim by which you can at the same time will that it should become a universal law. Treat persons always as ends, never merely as means.', relatedConcepts: ['Deontology', 'Duty', 'Universalizability'], argumentsFor: ['Respects dignity', 'Non-consequentialist', 'Clear decision procedure'], argumentsAgainst: ['Rigidity', 'Conflicting duties', 'Empty formalism objection'] },
|
|
1153
|
+
{ name: 'Virtue Ethics', branch: 'ethics', period: 'Ancient', thinkers: ['Aristotle', 'Foot', 'MacIntyre', 'Hursthouse'], definition: 'Moral character (virtues like courage, justice, temperance, prudence) is primary. The right action is what a virtuous person would do.', relatedConcepts: ['Eudaimonia', 'Phronesis', 'Golden Mean'], argumentsFor: ['Holistic approach', 'Emphasizes character development', 'Fits moral psychology'], argumentsAgainst: ['Action guidance problem', 'Cultural relativity of virtues', 'Circularity'] },
|
|
1154
|
+
{ name: 'Social Contract', branch: 'ethics', period: 'Early Modern/Modern', thinkers: ['Hobbes', 'Locke', 'Rousseau', 'Rawls'], definition: 'Political and moral rules are justified by the agreement rational agents would make under certain conditions.', relatedConcepts: ['State of Nature', 'Veil of Ignorance', 'Justice as Fairness'], argumentsFor: ['Grounds authority in consent', 'Explains political obligation', 'Rawlsian fairness'], argumentsAgainst: ['Historical fiction', 'Excludes non-contractors', 'Free-rider problem'] },
|
|
1155
|
+
{ name: 'Moral Relativism', branch: 'ethics', period: 'Ancient to Present', thinkers: ['Protagoras', 'Harman', 'Wong'], definition: 'Moral truths are relative to cultures, societies, or individuals. There are no universal moral facts.', relatedConcepts: ['Cultural Relativism', 'Moral Realism', 'Subjectivism'], argumentsFor: ['Cultural diversity of morals', 'No proof of objective morals', 'Tolerance'], argumentsAgainst: ['Self-refuting', 'Can\'t criticize other cultures', 'Reformers would be wrong by definition'] },
|
|
1156
|
+
{ name: 'Existential Ethics', branch: 'ethics', period: 'Modern', thinkers: ['Kierkegaard', 'Sartre', 'de Beauvoir', 'Camus'], definition: 'Humans create their own values through free choice. Authenticity and responsibility are central. Existence precedes essence.', relatedConcepts: ['Authenticity', 'Bad Faith', 'Absurdism'], argumentsFor: ['Radical freedom', 'Personal responsibility', 'Authentic self-creation'], argumentsAgainst: ['Anxiety-inducing', 'No external guidance', 'Ignores social context'] },
|
|
1157
|
+
// Logic
|
|
1158
|
+
{ name: 'Law of Non-Contradiction', branch: 'logic', period: 'Ancient', thinkers: ['Aristotle', 'Leibniz'], definition: 'A proposition cannot be both true and false at the same time and in the same respect. ¬(P ∧ ¬P).', relatedConcepts: ['Law of Excluded Middle', 'Dialetheism', 'Classical Logic'], argumentsFor: ['Foundational for reasoning', 'Undeniable without self-contradiction', 'Required for communication'], argumentsAgainst: ['Dialetheism (Priest)', 'Quantum superposition analogy', 'Liar paradox'] },
|
|
1159
|
+
{ name: 'Validity', branch: 'logic', period: 'Ancient to Present', thinkers: ['Aristotle', 'Frege', 'Tarski'], definition: 'An argument is valid if and only if it is impossible for the premises to all be true and the conclusion false.', relatedConcepts: ['Soundness', 'Deduction', 'Logical Consequence'], argumentsFor: ['Preserves truth', 'Formal and checkable', 'Foundation of proof'], argumentsAgainst: ['Valid arguments can have false premises', 'Relevance logic critiques', 'Explosion problem'] },
|
|
1160
|
+
{ name: 'Induction', branch: 'logic', period: 'Modern', thinkers: ['Hume', 'Goodman', 'Popper'], definition: 'Reasoning from specific observations to general conclusions. Problem of induction: no logical guarantee that the future resembles the past.', relatedConcepts: ['Deduction', 'Abduction', 'Falsificationism'], argumentsFor: ['Basis of scientific method', 'Practically indispensable', 'Bayesian justification'], argumentsAgainst: ['Problem of induction (Hume)', 'New riddle (Goodman/grue)', 'No a priori justification'] },
|
|
1161
|
+
// Aesthetics
|
|
1162
|
+
{ name: 'Aesthetic Judgment', branch: 'aesthetics', period: 'Modern', thinkers: ['Kant', 'Hume', 'Sibley'], definition: 'Judgments about beauty that claim universal agreement despite being based on subjective feeling. Kant: "purposiveness without purpose."', relatedConcepts: ['Sublime', 'Taste', 'Beauty'], argumentsFor: ['Universal structures of perception', 'Intersubjective agreement exists', 'Not mere preference'], argumentsAgainst: ['Cultural variation', 'No objective properties of beauty', 'Evolutionary explanations'] },
|
|
1163
|
+
{ name: 'The Sublime', branch: 'aesthetics', period: 'Modern', thinkers: ['Burke', 'Kant', 'Lyotard'], definition: 'An aesthetic experience of awe, vastness, or power that overwhelms our capacity for comprehension. Contrasted with the merely beautiful.', relatedConcepts: ['Aesthetic Judgment', 'Beauty', 'Terror'], argumentsFor: ['Distinct phenomenology', 'Connects to magnitude/power', 'Art evokes it reliably'], argumentsAgainst: ['Merely psychological', 'Culturally variable', 'Vague category'] },
|
|
1164
|
+
{ name: 'Mimesis', branch: 'aesthetics', period: 'Ancient', thinkers: ['Plato', 'Aristotle'], definition: 'Art as imitation of reality. Plato: art is a copy of a copy (twice removed from truth). Aristotle: art reveals universal truths through particular imitations.', relatedConcepts: ['Representation', 'Catharsis', 'Forms'], argumentsFor: ['Art does represent reality', 'Universal truths in particular stories', 'Catharsis theory'], argumentsAgainst: ['Non-representational art', 'Art creates, not just copies', 'Music/architecture challenge'] },
|
|
1165
|
+
// Political
|
|
1166
|
+
{ name: 'Liberalism', branch: 'political', period: 'Early Modern/Modern', thinkers: ['Locke', 'Mill', 'Rawls', 'Nozick'], definition: 'Individual liberty, equal rights, consent of the governed, and limited government are foundational political values.', relatedConcepts: ['Rights', 'Democracy', 'Social Contract', 'Liberty'], argumentsFor: ['Protects individual freedom', 'Promotes tolerance', 'Economic prosperity'], argumentsAgainst: ['Atomistic individualism', 'Ignores structural inequality', 'Cultural imperialism'] },
|
|
1167
|
+
{ name: 'Justice as Fairness', branch: 'political', period: 'Modern', thinkers: ['Rawls'], definition: 'Justice is what rational agents would agree to behind a veil of ignorance: equal basic liberties and inequalities arranged to benefit the least advantaged.', relatedConcepts: ['Veil of Ignorance', 'Difference Principle', 'Original Position'], argumentsFor: ['Impartial procedure', 'Protects disadvantaged', 'Intuitive fairness'], argumentsAgainst: ['Hypothetical consent', 'Ignores desert', 'Libertarian objections (Nozick)'] },
|
|
1168
|
+
{ name: 'Marxism', branch: 'political', period: 'Modern', thinkers: ['Marx', 'Engels', 'Gramsci', 'Lukacs'], definition: 'History is driven by class struggle and material conditions. Capitalism alienates workers and will be superseded by communism.', relatedConcepts: ['Historical Materialism', 'Alienation', 'Class Struggle', 'Ideology'], argumentsFor: ['Explains economic inequality', 'Critique of exploitation', 'Historical analysis of power'], argumentsAgainst: ['Failed implementations', 'Deterministic', 'Ignores individual agency'] },
|
|
1169
|
+
// Philosophy of Mind
|
|
1170
|
+
{ name: 'Consciousness', branch: 'metaphysics', period: 'Ancient to Present', thinkers: ['Descartes', 'Nagel', 'Chalmers', 'Dennett'], definition: 'Subjective experience — "what it is like" to be a conscious being. The "hard problem": why do physical processes give rise to experience?', relatedConcepts: ['Qualia', 'Hard Problem', 'Functionalism'], argumentsFor: ['Undeniable first-person datum', 'Explanatory gap', 'Zombie argument'], argumentsAgainst: ['Dennett: quining qualia', 'Illusionism', 'No causal role needed'] },
|
|
1171
|
+
{ name: 'Functionalism', branch: 'metaphysics', period: 'Modern', thinkers: ['Putnam', 'Fodor', 'Block', 'Dennett'], definition: 'Mental states are defined by their functional roles — inputs, outputs, and relations to other states — not by their physical substrate.', relatedConcepts: ['Multiple Realizability', 'Turing Machine', 'Chinese Room'], argumentsFor: ['Multiple realizability', 'Explains AI possibility', 'Scientific compatibility'], argumentsAgainst: ['Chinese Room (Searle)', 'Absent qualia', 'Inverted qualia'] },
|
|
1172
|
+
{ name: 'Intentionality', branch: 'metaphysics', period: 'Modern', thinkers: ['Brentano', 'Husserl', 'Searle', 'Dennett'], definition: 'The "aboutness" of mental states — their capacity to be directed at or represent objects and states of affairs.', relatedConcepts: ['Consciousness', 'Representation', 'Chinese Room'], argumentsFor: ['Distinguishes mental from physical', 'Explains meaning', 'Phenomenologically evident'], argumentsAgainst: ['Naturalization problem', 'Thermostats have "aboutness" too?', 'Derived vs. original'] },
|
|
1173
|
+
// Philosophy of Language
|
|
1174
|
+
{ name: 'Meaning (Theory of)', branch: 'logic', period: 'Modern', thinkers: ['Frege', 'Russell', 'Wittgenstein', 'Kripke', 'Putnam'], definition: 'What determines the meaning of words and sentences? Theories include referential, use-based, possible-worlds semantics, and causal theories of reference.', relatedConcepts: ['Reference', 'Sense', 'Language Games', 'Rigid Designators'], argumentsFor: ['Compositionality', 'Truth conditions', 'Communication success'], argumentsAgainst: ['Indeterminacy of translation (Quine)', 'Context-dependence', 'Private language argument'] },
|
|
1175
|
+
{ name: 'Phenomenology', branch: 'epistemology', period: 'Modern', thinkers: ['Husserl', 'Heidegger', 'Merleau-Ponty', 'Sartre'], definition: 'The study of structures of consciousness as experienced from the first-person perspective. "To the things themselves!"', relatedConcepts: ['Intentionality', 'Lived Experience', 'Being-in-the-World', 'Epoché'], argumentsFor: ['Rich descriptions of experience', 'Avoids reductionism', 'Foundation for human sciences'], argumentsAgainst: ['Subjectivity limits intersubjective verification', 'Obscure methodology', 'Anti-scientific tendency'] },
|
|
1176
|
+
{ name: 'Hermeneutics', branch: 'epistemology', period: 'Modern', thinkers: ['Schleiermacher', 'Dilthey', 'Gadamer', 'Ricoeur'], definition: 'The theory and methodology of interpretation, especially of texts. Understanding always involves a "fusion of horizons" between interpreter and interpreted.', relatedConcepts: ['Phenomenology', 'Interpretation', 'Hermeneutic Circle'], argumentsFor: ['Explains interpretive process', 'Historical understanding', 'Text-reader interaction'], argumentsAgainst: ['Relativism concern', 'No single correct interpretation', 'Vagueness'] },
|
|
1177
|
+
{ name: 'Nihilism', branch: 'metaphysics', period: 'Modern', thinkers: ['Nietzsche', 'Turgenev', 'Cioran'], definition: 'Life is without objective meaning, purpose, or intrinsic value. There are no moral truths.', relatedConcepts: ['Existentialism', 'Absurdism', 'Moral Anti-Realism'], argumentsFor: ['No evidence of cosmic purpose', 'Death of God', 'Suffering without redemption'], argumentsAgainst: ['Self-undermining', 'We do experience meaning', 'Leads to paralysis'] },
|
|
1178
|
+
{ name: 'Absurdism', branch: 'ethics', period: 'Modern', thinkers: ['Camus', 'Kierkegaard'], definition: 'The conflict between humans\' tendency to seek meaning and the universe\'s silence. Camus: we must imagine Sisyphus happy.', relatedConcepts: ['Nihilism', 'Existentialism', 'Revolt'], argumentsFor: ['Honest about human condition', 'Motivates creative response', 'Neither denial nor despair'], argumentsAgainst: ['Arbitrary starting point', 'Why revolt rather than resignation?', 'Meaning may exist'] },
|
|
1179
|
+
{ name: 'Epistemic Injustice', branch: 'epistemology', period: 'Contemporary', thinkers: ['Fricker', 'Medina', 'Dotson'], definition: 'Wrongful denial of someone\'s capacity as a knower. Includes testimonial injustice (credibility deficit) and hermeneutical injustice (lack of interpretive resources).', relatedConcepts: ['Social Epistemology', 'Standpoint Theory', 'Testimony'], argumentsFor: ['Explains systematic knowledge suppression', 'Real-world impact', 'Intersects with justice'], argumentsAgainst: ['Scope inflation', 'Hard to operationalize', 'Political rather than epistemic'] },
|
|
1180
|
+
// Philosophy of Science
|
|
1181
|
+
{ name: 'Falsificationism', branch: 'epistemology', period: 'Modern', thinkers: ['Popper'], definition: 'Scientific theories cannot be verified but can be falsified. A theory is scientific only if it makes testable predictions that could prove it wrong.', relatedConcepts: ['Verification', 'Demarcation Problem', 'Paradigm Shift'], argumentsFor: ['Clear demarcation criterion', 'Explains theory revision', 'Promotes bold hypotheses'], argumentsAgainst: ['Duhem-Quine thesis', 'Scientists don\'t actually abandon falsified theories', 'Auxiliary hypotheses'] },
|
|
1182
|
+
{ name: 'Paradigm Shift', branch: 'epistemology', period: 'Modern', thinkers: ['Kuhn'], definition: 'Scientific revolutions occur when an existing paradigm (framework of normal science) is replaced by a new one that is incommensurable with the old.', relatedConcepts: ['Normal Science', 'Anomaly', 'Incommensurability'], argumentsFor: ['Historical examples (Copernicus, Einstein)', 'Explains resistance to change', 'Community structure of science'], argumentsAgainst: ['Incommensurability is overstated', 'Relativism', 'Progress still happens'] },
|
|
1183
|
+
// Additional important concepts
|
|
1184
|
+
{ name: 'Dialectics', branch: 'logic', period: 'Ancient/Modern', thinkers: ['Plato', 'Hegel', 'Marx'], definition: 'A method of argument through dialogue (Plato) or a theory of development through thesis-antithesis-synthesis (Hegel). For Marx, applied to material/social conditions.', relatedConcepts: ['Thesis-Antithesis-Synthesis', 'Historical Materialism', 'Socratic Method'], argumentsFor: ['Captures dynamic development', 'Useful analytical tool', 'Historical explanatory power'], argumentsAgainst: ['Vague/unfalsifiable', 'Teleological assumptions', 'Oversimplifies'] },
|
|
1185
|
+
{ name: 'Utopia', branch: 'political', period: 'Early Modern', thinkers: ['More', 'Plato', 'Marx', 'Nozick'], definition: 'An imagined ideal society. Philosophical utopianism explores what the best possible social arrangement would look like.', relatedConcepts: ['Dystopia', 'Social Contract', 'Justice'], argumentsFor: ['Inspires reform', 'Clarifies values', 'Thought experiment for justice'], argumentsAgainst: ['Impossible to realize', 'Totalitarian risk', 'Ignores human nature'] },
|
|
1186
|
+
{ name: 'Natural Law', branch: 'ethics', period: 'Ancient/Medieval', thinkers: ['Aquinas', 'Aristotle', 'Cicero', 'Finnis'], definition: 'There are objective moral principles discernible through reason that are grounded in human nature or divine order.', relatedConcepts: ['Divine Command Theory', 'Human Rights', 'Virtue Ethics'], argumentsFor: ['Universal basis for rights', 'Transcends positive law', 'Natural teleology'], argumentsAgainst: ['Naturalistic fallacy', 'Relies on contested teleology', 'Cultural variation'] },
|
|
1187
|
+
{ name: 'Panpsychism', branch: 'metaphysics', period: 'Ancient to Present', thinkers: ['Spinoza', 'Leibniz', 'Whitehead', 'Chalmers', 'Goff'], definition: 'Consciousness or experience is a fundamental and ubiquitous feature of the physical world. All matter has some form of inner experience.', relatedConcepts: ['Consciousness', 'Hard Problem', 'Neutral Monism'], argumentsFor: ['Avoids emergence mystery', 'Continuity in nature', 'Addresses hard problem'], argumentsAgainst: ['Combination problem', 'Unfalsifiable', 'Counterintuitive'] },
|
|
1188
|
+
{ name: 'Stoicism', branch: 'ethics', period: 'Ancient', thinkers: ['Zeno', 'Epictetus', 'Seneca', 'Marcus Aurelius'], definition: 'Virtue (living according to reason/nature) is the sole good. External things are "indifferent." Emotional tranquility (apatheia) through accepting what we cannot control.', relatedConcepts: ['Virtue Ethics', 'Determinism', 'Cosmopolitanism'], argumentsFor: ['Psychological resilience', 'Practical wisdom', 'Universal ethics'], argumentsAgainst: ['Emotional suppression', 'Fatalistic', 'Ignores social structures'] },
|
|
1189
|
+
{ name: 'Epicureanism', branch: 'ethics', period: 'Ancient', thinkers: ['Epicurus', 'Lucretius'], definition: 'The highest good is pleasure (especially absence of pain — ataraxia). Seek modest pleasures, friendship, philosophical conversation. Avoid fear of death and gods.', relatedConcepts: ['Hedonism', 'Utilitarianism', 'Atomism'], argumentsFor: ['Death is nothing to us', 'Modest and achievable', 'Friendship-centered'], argumentsAgainst: ['Passive/withdrawn', 'Too focused on individual', 'Pleasure is not all good'] },
|
|
1190
|
+
{ name: 'Social Constructionism', branch: 'epistemology', period: 'Modern/Contemporary', thinkers: ['Berger', 'Luckmann', 'Foucault', 'Butler'], definition: 'Knowledge, categories, and social realities (gender, race, institutions) are constructed through social processes rather than being natural or given.', relatedConcepts: ['Postmodernism', 'Discourse', 'Power/Knowledge'], argumentsFor: ['Explains cultural variation', 'Denaturalizes oppressive categories', 'Historical evidence of change'], argumentsAgainst: ['Self-refuting risk', 'Neglects material reality', 'Everything-is-constructed overreach'] },
|
|
1191
|
+
{ name: 'Deconstruction', branch: 'logic', period: 'Contemporary', thinkers: ['Derrida'], definition: 'A method of reading that reveals internal contradictions and hierarchies within texts. Binary oppositions (speech/writing, nature/culture) are unstable.', relatedConcepts: ['Différance', 'Logocentrism', 'Post-structuralism'], argumentsFor: ['Reveals hidden assumptions', 'Challenges dominant narratives', 'Close reading discipline'], argumentsAgainst: ['Obscurantism', 'Nihilistic', 'Self-undermining'] },
|
|
1192
|
+
{ name: 'Veil of Ignorance', branch: 'political', period: 'Modern', thinkers: ['Rawls'], definition: 'A thought experiment: rational agents choosing principles of justice without knowing their own social position, talents, or values. Ensures impartial principles.', relatedConcepts: ['Justice as Fairness', 'Original Position', 'Social Contract'], argumentsFor: ['Procedural fairness', 'Risk aversion leads to protecting worst-off', 'Powerful thought experiment'], argumentsAgainst: ['Removes relevant information', 'Hypothetical consent is not real', 'Assumes risk aversion'] },
|
|
1193
|
+
{ name: 'Bioethics', branch: 'ethics', period: 'Contemporary', thinkers: ['Beauchamp', 'Childress', 'Singer', 'Kass'], definition: 'The study of ethical issues arising from advances in biology and medicine: autonomy, beneficence, non-maleficence, and justice (the four principles).', relatedConcepts: ['Medical Ethics', 'Autonomy', 'Informed Consent'], argumentsFor: ['Practical guidance', 'Four principles framework', 'Addresses real dilemmas'], argumentsAgainst: ['Principlism is too abstract', 'Cultural blind spots', 'Technology outpaces ethics'] },
|
|
1194
|
+
{ name: 'Philosophy of Technology', branch: 'metaphysics', period: 'Contemporary', thinkers: ['Heidegger', 'Ellul', 'Feenberg', 'Ihde'], definition: 'Critical examination of technology\'s nature and impact on human existence. Heidegger: technology as "enframing" (Gestell) reduces everything to "standing reserve."', relatedConcepts: ['AI Ethics', 'Transhumanism', 'Instrumentalism'], argumentsFor: ['Technology shapes perception', 'Not value-neutral', 'Existential implications'], argumentsAgainst: ['Technophobic bias', 'Vague concepts', 'Ignores benefits'] },
|
|
1195
|
+
{ name: 'AI Ethics', branch: 'ethics', period: 'Contemporary', thinkers: ['Bostrom', 'Russell', 'Floridi', 'Bengio'], definition: 'Ethical issues posed by artificial intelligence: alignment, bias, autonomy, accountability, consciousness, existential risk, and the moral status of AI systems.', relatedConcepts: ['Consciousness', 'Philosophy of Technology', 'Existential Risk'], argumentsFor: ['Urgent real-world implications', 'Alignment problem is genuine', 'Power concentration risk'], argumentsAgainst: ['Premature speculation', 'Anthropomorphism', 'Distracts from current harms'] },
|
|
1196
|
+
];
|
|
1197
|
+
// ─── Registration ───────────────────────────────────────────────────────────
|
|
1198
|
+
export function registerLabHumanitiesTools() {
|
|
1199
|
+
// ── 1. corpus_analyze ──────────────────────────────────────────────────
|
|
1200
|
+
registerTool({
|
|
1201
|
+
name: 'corpus_analyze',
|
|
1202
|
+
description: 'Computational text analysis: word frequency, N-grams, hapax legomena, type-token ratio, vocabulary growth curve (Heaps\' law), and concordance (KWIC). Useful for linguistics, stylistics, and digital humanities.',
|
|
1203
|
+
parameters: {
|
|
1204
|
+
text: { type: 'string', description: 'The text to analyze', required: true },
|
|
1205
|
+
analysis: { type: 'string', description: 'Analysis type: frequency, ngrams, hapax, ttr, concordance, all', required: true },
|
|
1206
|
+
keyword: { type: 'string', description: 'Keyword for concordance (KWIC) — required if analysis is "concordance"' },
|
|
1207
|
+
n: { type: 'number', description: 'N-gram size (default 2)' },
|
|
1208
|
+
top: { type: 'number', description: 'Number of top results to return (default 20)' },
|
|
1209
|
+
},
|
|
1210
|
+
tier: 'free',
|
|
1211
|
+
async execute(args) {
|
|
1212
|
+
const text = String(args.text);
|
|
1213
|
+
const analysis = String(args.analysis || 'all').toLowerCase();
|
|
1214
|
+
const keyword = args.keyword ? String(args.keyword) : undefined;
|
|
1215
|
+
const n = typeof args.n === 'number' ? args.n : 2;
|
|
1216
|
+
const top = typeof args.top === 'number' ? args.top : 20;
|
|
1217
|
+
const tokens = tokenize(text);
|
|
1218
|
+
if (tokens.length === 0)
|
|
1219
|
+
return 'Error: No text to analyze (empty or no words).';
|
|
1220
|
+
const parts = ['## Corpus Analysis\n'];
|
|
1221
|
+
parts.push(`**Token count**: ${tokens.length} | **Type count**: ${new Set(tokens).size}\n`);
|
|
1222
|
+
if (analysis === 'frequency' || analysis === 'all') {
|
|
1223
|
+
parts.push('### Word Frequency (top ' + top + ')');
|
|
1224
|
+
parts.push('');
|
|
1225
|
+
parts.push('| Rank | Word | Count | % |');
|
|
1226
|
+
parts.push('|------|------|-------|---|');
|
|
1227
|
+
let rank = 1;
|
|
1228
|
+
for (const [word, count] of wordFrequency(tokens, top)) {
|
|
1229
|
+
parts.push(`| ${rank++} | ${word} | ${count} | ${(count / tokens.length * 100).toFixed(2)}% |`);
|
|
1230
|
+
}
|
|
1231
|
+
parts.push('');
|
|
1232
|
+
}
|
|
1233
|
+
if (analysis === 'ngrams' || analysis === 'all') {
|
|
1234
|
+
parts.push(`### ${n}-grams (top ${top})`);
|
|
1235
|
+
parts.push('');
|
|
1236
|
+
parts.push('| Rank | N-gram | Count |');
|
|
1237
|
+
parts.push('|------|--------|-------|');
|
|
1238
|
+
let rank = 1;
|
|
1239
|
+
for (const [gram, count] of computeNgrams(tokens, n, top)) {
|
|
1240
|
+
parts.push(`| ${rank++} | ${gram} | ${count} |`);
|
|
1241
|
+
}
|
|
1242
|
+
parts.push('');
|
|
1243
|
+
}
|
|
1244
|
+
if (analysis === 'hapax' || analysis === 'all') {
|
|
1245
|
+
const hapax = hapaxLegomena(tokens);
|
|
1246
|
+
parts.push(`### Hapax Legomena`);
|
|
1247
|
+
parts.push(`**Count**: ${hapax.length} (${(hapax.length / tokens.length * 100).toFixed(2)}% of tokens)`);
|
|
1248
|
+
parts.push('');
|
|
1249
|
+
if (hapax.length <= 30) {
|
|
1250
|
+
parts.push(hapax.join(', '));
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
parts.push(hapax.slice(0, 30).join(', ') + ` ... and ${hapax.length - 30} more`);
|
|
1254
|
+
}
|
|
1255
|
+
parts.push('');
|
|
1256
|
+
}
|
|
1257
|
+
if (analysis === 'ttr' || analysis === 'all') {
|
|
1258
|
+
const ttrResult = typeTokenRatio(tokens);
|
|
1259
|
+
const growth = vocabularyGrowthCurve(tokens);
|
|
1260
|
+
parts.push('### Type-Token Ratio & Vocabulary Growth');
|
|
1261
|
+
parts.push('');
|
|
1262
|
+
parts.push(`| Metric | Value |`);
|
|
1263
|
+
parts.push(`|--------|-------|`);
|
|
1264
|
+
parts.push(`| Types (V) | ${ttrResult.types} |`);
|
|
1265
|
+
parts.push(`| Tokens (N) | ${ttrResult.tokens} |`);
|
|
1266
|
+
parts.push(`| TTR (V/N) | ${ttrResult.ttr.toFixed(4)} |`);
|
|
1267
|
+
parts.push(`| Heaps\' K | ${growth.heapsK.toFixed(4)} |`);
|
|
1268
|
+
parts.push(`| Heaps\' β | ${growth.heapsBeta.toFixed(4)} |`);
|
|
1269
|
+
parts.push(`| Heaps\' law: V ≈ ${growth.heapsK.toFixed(2)} × N^${growth.heapsBeta.toFixed(3)} | |`);
|
|
1270
|
+
parts.push('');
|
|
1271
|
+
// ASCII growth curve (compact)
|
|
1272
|
+
if (growth.curve.length > 5) {
|
|
1273
|
+
parts.push('**Vocabulary growth curve** (N → V):');
|
|
1274
|
+
parts.push('```');
|
|
1275
|
+
const maxV = growth.curve[growth.curve.length - 1][1];
|
|
1276
|
+
const height = 10;
|
|
1277
|
+
for (let row = height; row >= 0; row--) {
|
|
1278
|
+
const threshold = (row / height) * maxV;
|
|
1279
|
+
let line = `${Math.round(threshold).toString().padStart(6)} |`;
|
|
1280
|
+
for (const [, v] of growth.curve) {
|
|
1281
|
+
line += v >= threshold ? '█' : ' ';
|
|
1282
|
+
}
|
|
1283
|
+
parts.push(line);
|
|
1284
|
+
}
|
|
1285
|
+
parts.push(`${''.padStart(7)}${'─'.repeat(growth.curve.length + 1)}`);
|
|
1286
|
+
parts.push(`${''.padStart(7)}N → ${growth.curve[growth.curve.length - 1][0]}`);
|
|
1287
|
+
parts.push('```');
|
|
1288
|
+
}
|
|
1289
|
+
parts.push('');
|
|
1290
|
+
}
|
|
1291
|
+
if (analysis === 'concordance' || analysis === 'all') {
|
|
1292
|
+
if (keyword) {
|
|
1293
|
+
const results = concordance(tokens, keyword);
|
|
1294
|
+
parts.push(`### Concordance (KWIC) for "${keyword}"`);
|
|
1295
|
+
parts.push(`**Occurrences**: ${results.length}`);
|
|
1296
|
+
parts.push('');
|
|
1297
|
+
parts.push('```');
|
|
1298
|
+
for (const line of results.slice(0, 30)) {
|
|
1299
|
+
parts.push(line);
|
|
1300
|
+
}
|
|
1301
|
+
if (results.length > 30)
|
|
1302
|
+
parts.push(`... and ${results.length - 30} more`);
|
|
1303
|
+
parts.push('```');
|
|
1304
|
+
}
|
|
1305
|
+
else if (analysis === 'concordance') {
|
|
1306
|
+
parts.push('*Concordance requires a `keyword` parameter.*');
|
|
1307
|
+
}
|
|
1308
|
+
parts.push('');
|
|
1309
|
+
}
|
|
1310
|
+
return parts.join('\n');
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
// ── 2. formal_logic ────────────────────────────────────────────────────
|
|
1314
|
+
registerTool({
|
|
1315
|
+
name: 'formal_logic',
|
|
1316
|
+
description: 'Propositional logic: parse expressions, build truth tables, check validity/satisfiability/tautology, identify inference rules. Supports operators: ∧/&/AND, ∨/|/OR, ¬/!/NOT, →/->/IMPLIES, ↔/<->/IFF. Up to 6 variables.',
|
|
1317
|
+
parameters: {
|
|
1318
|
+
expression: { type: 'string', description: 'Logical expression, e.g. "(P → Q) ∧ P → Q" or "(P -> Q) & P -> Q"', required: true },
|
|
1319
|
+
operation: { type: 'string', description: 'Operation: truth_table, validity, satisfiability, inference', required: true },
|
|
1320
|
+
},
|
|
1321
|
+
tier: 'free',
|
|
1322
|
+
async execute(args) {
|
|
1323
|
+
const expression = String(args.expression);
|
|
1324
|
+
const operation = String(args.operation || 'truth_table').toLowerCase();
|
|
1325
|
+
if (operation === 'inference') {
|
|
1326
|
+
return checkInference(expression);
|
|
1327
|
+
}
|
|
1328
|
+
let ast;
|
|
1329
|
+
try {
|
|
1330
|
+
const tokens = lexLogic(expression);
|
|
1331
|
+
ast = new LogicParser(tokens).parse();
|
|
1332
|
+
}
|
|
1333
|
+
catch (err) {
|
|
1334
|
+
return `Error parsing expression: ${err instanceof Error ? err.message : String(err)}`;
|
|
1335
|
+
}
|
|
1336
|
+
const formatted = formatLogicNode(ast);
|
|
1337
|
+
const parts = [`## Formal Logic Analysis\n`, `**Parsed**: ${formatted}\n`];
|
|
1338
|
+
try {
|
|
1339
|
+
const table = buildTruthTable(ast);
|
|
1340
|
+
const allTrue = table.rows.every(r => r.result);
|
|
1341
|
+
const anyTrue = table.rows.some(r => r.result);
|
|
1342
|
+
const allFalse = table.rows.every(r => !r.result);
|
|
1343
|
+
if (operation === 'truth_table' || operation === 'validity') {
|
|
1344
|
+
parts.push('### Truth Table\n');
|
|
1345
|
+
const header = '| ' + table.vars.join(' | ') + ' | Result |';
|
|
1346
|
+
const sep = '|' + table.vars.map(() => '---').join('|') + '|--------|';
|
|
1347
|
+
parts.push(header);
|
|
1348
|
+
parts.push(sep);
|
|
1349
|
+
for (const row of table.rows) {
|
|
1350
|
+
const vals = row.assignment.map(v => v ? 'T' : 'F').join(' | ');
|
|
1351
|
+
parts.push(`| ${vals} | ${row.result ? '**T**' : 'F'} |`);
|
|
1352
|
+
}
|
|
1353
|
+
parts.push('');
|
|
1354
|
+
}
|
|
1355
|
+
if (operation === 'validity' || operation === 'truth_table') {
|
|
1356
|
+
parts.push('### Classification\n');
|
|
1357
|
+
if (allTrue)
|
|
1358
|
+
parts.push('- **Tautology** (always true) — logically valid');
|
|
1359
|
+
else if (allFalse)
|
|
1360
|
+
parts.push('- **Contradiction** (always false)');
|
|
1361
|
+
else
|
|
1362
|
+
parts.push('- **Contingent** (true in some valuations, false in others)');
|
|
1363
|
+
parts.push(`- True in ${table.rows.filter(r => r.result).length}/${table.rows.length} valuations`);
|
|
1364
|
+
}
|
|
1365
|
+
if (operation === 'satisfiability') {
|
|
1366
|
+
parts.push('### Satisfiability\n');
|
|
1367
|
+
if (anyTrue) {
|
|
1368
|
+
parts.push('**Satisfiable** — at least one valuation makes this true.\n');
|
|
1369
|
+
parts.push('**Satisfying assignment(s)**:');
|
|
1370
|
+
for (const row of table.rows.filter(r => r.result).slice(0, 5)) {
|
|
1371
|
+
const assignment = table.vars.map((v, i) => `${v}=${row.assignment[i] ? 'T' : 'F'}`).join(', ');
|
|
1372
|
+
parts.push(`- {${assignment}}`);
|
|
1373
|
+
}
|
|
1374
|
+
const satCount = table.rows.filter(r => r.result).length;
|
|
1375
|
+
if (satCount > 5)
|
|
1376
|
+
parts.push(` ... and ${satCount - 5} more`);
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
parts.push('**Unsatisfiable** — no valuation makes this true (contradiction).');
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return parts.join('\n');
|
|
1383
|
+
}
|
|
1384
|
+
catch (err) {
|
|
1385
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
// ── 3. argument_map ────────────────────────────────────────────────────
|
|
1390
|
+
registerTool({
|
|
1391
|
+
name: 'argument_map',
|
|
1392
|
+
description: 'Analyze argument structure: identify premises, conclusions, assumptions, logical form. Detect ~20 common logical fallacies. Check deductive validity.',
|
|
1393
|
+
parameters: {
|
|
1394
|
+
argument: { type: 'string', description: 'Natural language argument text to analyze', required: true },
|
|
1395
|
+
operation: { type: 'string', description: 'Operation: structure, fallacy_check, validity', required: true },
|
|
1396
|
+
},
|
|
1397
|
+
tier: 'free',
|
|
1398
|
+
async execute(args) {
|
|
1399
|
+
const text = String(args.argument);
|
|
1400
|
+
const operation = String(args.operation || 'structure').toLowerCase();
|
|
1401
|
+
if (text.length < 10)
|
|
1402
|
+
return 'Error: Argument text too short for meaningful analysis.';
|
|
1403
|
+
switch (operation) {
|
|
1404
|
+
case 'structure': return analyzeArgumentStructure(text);
|
|
1405
|
+
case 'fallacy_check': return checkFallacies(text);
|
|
1406
|
+
case 'validity': return checkArgumentValidity(text);
|
|
1407
|
+
default: return `Unknown operation "${operation}". Use: structure, fallacy_check, validity.`;
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
});
|
|
1411
|
+
// ── 4. ethics_framework ────────────────────────────────────────────────
|
|
1412
|
+
registerTool({
|
|
1413
|
+
name: 'ethics_framework',
|
|
1414
|
+
description: 'Apply ethical frameworks to a dilemma: utilitarianism, deontological (Kantian), virtue ethics (Aristotelian), care ethics, rights-based, social contract. Generates analysis and likely conclusion for each.',
|
|
1415
|
+
parameters: {
|
|
1416
|
+
dilemma: { type: 'string', description: 'Describe the ethical situation/dilemma', required: true },
|
|
1417
|
+
frameworks: { type: 'string', description: 'Frameworks: all, utilitarian, deontological, virtue, care, rights, social_contract', required: true },
|
|
1418
|
+
},
|
|
1419
|
+
tier: 'free',
|
|
1420
|
+
async execute(args) {
|
|
1421
|
+
const dilemma = String(args.dilemma);
|
|
1422
|
+
const frameworksArg = String(args.frameworks || 'all').toLowerCase();
|
|
1423
|
+
if (dilemma.length < 10)
|
|
1424
|
+
return 'Error: Please provide a more detailed description of the ethical dilemma.';
|
|
1425
|
+
const fMap = {
|
|
1426
|
+
utilitarian: applyUtilitarian,
|
|
1427
|
+
deontological: applyDeontological,
|
|
1428
|
+
virtue: applyVirtueEthics,
|
|
1429
|
+
care: applyCareEthics,
|
|
1430
|
+
rights: applyRightsBased,
|
|
1431
|
+
social_contract: applySocialContract,
|
|
1432
|
+
};
|
|
1433
|
+
const selected = frameworksArg === 'all'
|
|
1434
|
+
? Object.keys(fMap)
|
|
1435
|
+
: frameworksArg.split(',').map(s => s.trim()).filter(s => s in fMap);
|
|
1436
|
+
if (selected.length === 0) {
|
|
1437
|
+
return `Unknown framework(s). Available: ${Object.keys(fMap).join(', ')}, all`;
|
|
1438
|
+
}
|
|
1439
|
+
const parts = [
|
|
1440
|
+
`## Ethical Analysis\n`,
|
|
1441
|
+
`**Dilemma**: ${dilemma.slice(0, 200)}${dilemma.length > 200 ? '...' : ''}\n`,
|
|
1442
|
+
'---\n',
|
|
1443
|
+
];
|
|
1444
|
+
for (const key of selected) {
|
|
1445
|
+
const analysis = fMap[key](dilemma);
|
|
1446
|
+
parts.push(`### ${analysis.framework}`);
|
|
1447
|
+
parts.push(`**Core principle**: ${analysis.principle}\n`);
|
|
1448
|
+
parts.push(analysis.analysis);
|
|
1449
|
+
parts.push(`\n**Likely conclusion**: ${analysis.likelyConclusion}`);
|
|
1450
|
+
parts.push(`\n**Key question**: *${analysis.keyQuestion}*`);
|
|
1451
|
+
parts.push('\n---\n');
|
|
1452
|
+
}
|
|
1453
|
+
if (selected.length > 1) {
|
|
1454
|
+
parts.push('### Comparative Summary');
|
|
1455
|
+
parts.push('Different frameworks may yield different conclusions. Ethical maturity involves understanding these tensions and making reflective judgments that consider multiple perspectives.');
|
|
1456
|
+
}
|
|
1457
|
+
return parts.join('\n');
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
// ── 5. historical_timeline ─────────────────────────────────────────────
|
|
1461
|
+
registerTool({
|
|
1462
|
+
name: 'historical_timeline',
|
|
1463
|
+
description: 'Build and analyze timelines: add events with dates and categories, compute duration/overlap, detect periods, generate ASCII visualization. Dates support CE/BCE/ISO formats.',
|
|
1464
|
+
parameters: {
|
|
1465
|
+
events: { type: 'string', description: 'JSON array of {date, event, category?, end_date?}', required: true },
|
|
1466
|
+
operation: { type: 'string', description: 'Operation: visualize, analyze, period, connections', required: true },
|
|
1467
|
+
},
|
|
1468
|
+
tier: 'free',
|
|
1469
|
+
async execute(args) {
|
|
1470
|
+
let events;
|
|
1471
|
+
try {
|
|
1472
|
+
events = JSON.parse(String(args.events));
|
|
1473
|
+
if (!Array.isArray(events))
|
|
1474
|
+
throw new Error('Must be an array');
|
|
1475
|
+
}
|
|
1476
|
+
catch (err) {
|
|
1477
|
+
return `Error parsing events JSON: ${err instanceof Error ? err.message : String(err)}\n\nExpected format: [{"date": "1776", "event": "Declaration of Independence", "category": "politics"}]`;
|
|
1478
|
+
}
|
|
1479
|
+
if (events.length === 0)
|
|
1480
|
+
return 'Error: No events provided.';
|
|
1481
|
+
const operation = String(args.operation || 'visualize').toLowerCase();
|
|
1482
|
+
switch (operation) {
|
|
1483
|
+
case 'visualize': return visualizeTimeline(events);
|
|
1484
|
+
case 'analyze': return analyzeTimeline(events);
|
|
1485
|
+
case 'period': return detectPeriods(events);
|
|
1486
|
+
case 'connections': {
|
|
1487
|
+
const vis = visualizeTimeline(events);
|
|
1488
|
+
const analysis = analyzeTimeline(events);
|
|
1489
|
+
return `${vis}\n\n---\n\n${analysis}`;
|
|
1490
|
+
}
|
|
1491
|
+
default: return `Unknown operation "${operation}". Use: visualize, analyze, period, connections.`;
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
// ── 6. language_typology ───────────────────────────────────────────────
|
|
1496
|
+
registerTool({
|
|
1497
|
+
name: 'language_typology',
|
|
1498
|
+
description: 'Linguistic typology database: ~50 major languages with family, word order, morphological type, writing system, phoneme count, tonality, case system, speaker numbers. Search by language, family, or feature.',
|
|
1499
|
+
parameters: {
|
|
1500
|
+
query: { type: 'string', description: 'Language name, family name, or feature to search', required: true },
|
|
1501
|
+
search_type: { type: 'string', description: 'Search type: language, family, feature, comparison', required: true },
|
|
1502
|
+
},
|
|
1503
|
+
tier: 'free',
|
|
1504
|
+
async execute(args) {
|
|
1505
|
+
const query = String(args.query).toLowerCase();
|
|
1506
|
+
const searchType = String(args.search_type || 'language').toLowerCase();
|
|
1507
|
+
const formatLang = (lang) => {
|
|
1508
|
+
return [
|
|
1509
|
+
`### ${lang.name}`,
|
|
1510
|
+
'',
|
|
1511
|
+
'| Property | Value |',
|
|
1512
|
+
'|----------|-------|',
|
|
1513
|
+
`| Family | ${lang.family}${lang.subfamily ? ' > ' + lang.subfamily : ''} |`,
|
|
1514
|
+
`| Word order | ${lang.wordOrder} |`,
|
|
1515
|
+
`| Morphological type | ${lang.morphologicalType} |`,
|
|
1516
|
+
`| Writing system | ${lang.writingSystem} |`,
|
|
1517
|
+
`| Phoneme inventory | ~${lang.phonemeCount} |`,
|
|
1518
|
+
`| Tonal | ${lang.tonal ? 'Yes' : 'No'} |`,
|
|
1519
|
+
`| Case system | ${lang.caseSystem} |`,
|
|
1520
|
+
`| Speakers | ${lang.speakers} |`,
|
|
1521
|
+
].join('\n');
|
|
1522
|
+
};
|
|
1523
|
+
switch (searchType) {
|
|
1524
|
+
case 'language': {
|
|
1525
|
+
const matches = LANGUAGES.filter(l => l.name.toLowerCase().includes(query));
|
|
1526
|
+
if (matches.length === 0)
|
|
1527
|
+
return `No language found matching "${query}". Try: ${LANGUAGES.slice(0, 5).map(l => l.name).join(', ')}...`;
|
|
1528
|
+
return `## Language Typology\n\n${matches.map(formatLang).join('\n\n')}`;
|
|
1529
|
+
}
|
|
1530
|
+
case 'family': {
|
|
1531
|
+
const matches = LANGUAGES.filter(l => l.family.toLowerCase().includes(query) ||
|
|
1532
|
+
(l.subfamily?.toLowerCase().includes(query) ?? false));
|
|
1533
|
+
if (matches.length === 0)
|
|
1534
|
+
return `No languages found in family "${query}". Available families: ${[...new Set(LANGUAGES.map(l => l.family))].join(', ')}`;
|
|
1535
|
+
const parts = [`## Languages in "${query}" family\n`];
|
|
1536
|
+
parts.push(`**Count**: ${matches.length}\n`);
|
|
1537
|
+
for (const m of matches) {
|
|
1538
|
+
parts.push(`- **${m.name}** — ${m.wordOrder}, ${m.morphologicalType}, ${m.speakers}`);
|
|
1539
|
+
}
|
|
1540
|
+
return parts.join('\n');
|
|
1541
|
+
}
|
|
1542
|
+
case 'feature': {
|
|
1543
|
+
const parts = [`## Feature Search: "${query}"\n`];
|
|
1544
|
+
// Search across all properties
|
|
1545
|
+
const matches = LANGUAGES.filter(l => l.wordOrder.toLowerCase().includes(query) ||
|
|
1546
|
+
l.morphologicalType.toLowerCase().includes(query) ||
|
|
1547
|
+
l.writingSystem.toLowerCase().includes(query) ||
|
|
1548
|
+
l.caseSystem.toLowerCase().includes(query) ||
|
|
1549
|
+
(query === 'tonal' && l.tonal) ||
|
|
1550
|
+
(query === 'non-tonal' && !l.tonal) ||
|
|
1551
|
+
(query === 'isolating' && l.morphologicalType === 'isolating') ||
|
|
1552
|
+
(query === 'agglutinative' && l.morphologicalType.includes('agglutinative')) ||
|
|
1553
|
+
(query === 'fusional' && l.morphologicalType.includes('fusional')) ||
|
|
1554
|
+
(query === 'polysynthetic' && l.morphologicalType.includes('polysynthetic')));
|
|
1555
|
+
if (matches.length === 0)
|
|
1556
|
+
return `No languages found with feature "${query}". Try: SOV, SVO, tonal, agglutinative, fusional, isolating, polysynthetic, Cyrillic, Latin...`;
|
|
1557
|
+
parts.push(`**Matching languages**: ${matches.length}\n`);
|
|
1558
|
+
for (const m of matches) {
|
|
1559
|
+
parts.push(`- **${m.name}** (${m.family}) — ${m.wordOrder}, ${m.morphologicalType}`);
|
|
1560
|
+
}
|
|
1561
|
+
return parts.join('\n');
|
|
1562
|
+
}
|
|
1563
|
+
case 'comparison': {
|
|
1564
|
+
const names = query.split(/[,;&]/).map(s => s.trim().toLowerCase());
|
|
1565
|
+
const matches = names.map(n => LANGUAGES.find(l => l.name.toLowerCase().includes(n))).filter(Boolean);
|
|
1566
|
+
if (matches.length < 2)
|
|
1567
|
+
return `Need at least 2 languages for comparison. Found: ${matches.map(m => m.name).join(', ')}`;
|
|
1568
|
+
const parts = [`## Language Comparison\n`];
|
|
1569
|
+
const header = '| Feature | ' + matches.map(m => m.name).join(' | ') + ' |';
|
|
1570
|
+
const sep = '|---------|' + matches.map(() => '---').join('|') + '|';
|
|
1571
|
+
parts.push(header);
|
|
1572
|
+
parts.push(sep);
|
|
1573
|
+
parts.push(`| Family | ${matches.map(m => m.family).join(' | ')} |`);
|
|
1574
|
+
parts.push(`| Word order | ${matches.map(m => m.wordOrder).join(' | ')} |`);
|
|
1575
|
+
parts.push(`| Morphology | ${matches.map(m => m.morphologicalType).join(' | ')} |`);
|
|
1576
|
+
parts.push(`| Writing | ${matches.map(m => m.writingSystem).join(' | ')} |`);
|
|
1577
|
+
parts.push(`| Phonemes | ${matches.map(m => '~' + m.phonemeCount).join(' | ')} |`);
|
|
1578
|
+
parts.push(`| Tonal | ${matches.map(m => m.tonal ? 'Yes' : 'No').join(' | ')} |`);
|
|
1579
|
+
parts.push(`| Case system | ${matches.map(m => m.caseSystem).join(' | ')} |`);
|
|
1580
|
+
parts.push(`| Speakers | ${matches.map(m => m.speakers).join(' | ')} |`);
|
|
1581
|
+
return parts.join('\n');
|
|
1582
|
+
}
|
|
1583
|
+
default:
|
|
1584
|
+
return `Unknown search type "${searchType}". Use: language, family, feature, comparison.`;
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
});
|
|
1588
|
+
// ── 7. phonetics_ipa ──────────────────────────────────────────────────
|
|
1589
|
+
registerTool({
|
|
1590
|
+
name: 'phonetics_ipa',
|
|
1591
|
+
description: 'IPA (International Phonetic Alphabet) tools: look up symbols by description, describe articulatory features, transcribe common English words, compare phoneme inventories. Embeds the full IPA consonant and vowel charts.',
|
|
1592
|
+
parameters: {
|
|
1593
|
+
query: { type: 'string', description: 'IPA symbol, description, English word, or language names to compare', required: true },
|
|
1594
|
+
operation: { type: 'string', description: 'Operation: lookup, describe, transcribe, compare', required: true },
|
|
1595
|
+
},
|
|
1596
|
+
tier: 'free',
|
|
1597
|
+
async execute(args) {
|
|
1598
|
+
const query = String(args.query).toLowerCase().trim();
|
|
1599
|
+
const operation = String(args.operation || 'lookup').toLowerCase();
|
|
1600
|
+
switch (operation) {
|
|
1601
|
+
case 'lookup': {
|
|
1602
|
+
// Search by description
|
|
1603
|
+
const matches = IPA_CHART.filter(s => s.description.toLowerCase().includes(query) ||
|
|
1604
|
+
s.place?.toLowerCase().includes(query) ||
|
|
1605
|
+
s.manner?.toLowerCase().includes(query) ||
|
|
1606
|
+
s.voicing?.toLowerCase().includes(query) ||
|
|
1607
|
+
s.height?.toLowerCase().includes(query) ||
|
|
1608
|
+
s.backness?.toLowerCase().includes(query));
|
|
1609
|
+
if (matches.length === 0)
|
|
1610
|
+
return `No IPA symbols found for "${query}". Try: bilabial, fricative, voiceless, close, front, rounded...`;
|
|
1611
|
+
const parts = [`## IPA Lookup: "${query}"\n`];
|
|
1612
|
+
parts.push(`**${matches.length} symbols found:**\n`);
|
|
1613
|
+
for (const m of matches) {
|
|
1614
|
+
let detail = `- **[${m.symbol}]** — ${m.description}`;
|
|
1615
|
+
if (m.type === 'consonant') {
|
|
1616
|
+
detail += ` (${m.voicing} ${m.place} ${m.manner})`;
|
|
1617
|
+
}
|
|
1618
|
+
else if (m.type === 'vowel') {
|
|
1619
|
+
detail += ` (${m.height} ${m.backness}${m.rounded ? ' rounded' : ' unrounded'})`;
|
|
1620
|
+
}
|
|
1621
|
+
parts.push(detail);
|
|
1622
|
+
}
|
|
1623
|
+
return parts.join('\n');
|
|
1624
|
+
}
|
|
1625
|
+
case 'describe': {
|
|
1626
|
+
// Look up specific symbol(s)
|
|
1627
|
+
const symbols = [...query];
|
|
1628
|
+
// Also handle multi-char symbols
|
|
1629
|
+
const found = [];
|
|
1630
|
+
let i = 0;
|
|
1631
|
+
while (i < query.length) {
|
|
1632
|
+
// Try 2-char match first
|
|
1633
|
+
const twoChar = query.slice(i, i + 2);
|
|
1634
|
+
const match2 = IPA_CHART.find(s => s.symbol === twoChar);
|
|
1635
|
+
if (match2) {
|
|
1636
|
+
found.push(match2);
|
|
1637
|
+
i += 2;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
// Single char
|
|
1641
|
+
const match1 = IPA_CHART.find(s => s.symbol === query[i]);
|
|
1642
|
+
if (match1)
|
|
1643
|
+
found.push(match1);
|
|
1644
|
+
i++;
|
|
1645
|
+
}
|
|
1646
|
+
if (found.length === 0)
|
|
1647
|
+
return `No IPA symbol recognized for "${query}". Enter an IPA symbol (e.g., ʃ, ŋ, ə, θ).`;
|
|
1648
|
+
const parts = [`## IPA Description\n`];
|
|
1649
|
+
for (const s of found) {
|
|
1650
|
+
parts.push(`### [${s.symbol}]`);
|
|
1651
|
+
parts.push(`**Description**: ${s.description}`);
|
|
1652
|
+
parts.push(`**Type**: ${s.type}`);
|
|
1653
|
+
if (s.type === 'consonant') {
|
|
1654
|
+
parts.push(`**Place**: ${s.place}`);
|
|
1655
|
+
parts.push(`**Manner**: ${s.manner}`);
|
|
1656
|
+
parts.push(`**Voicing**: ${s.voicing}`);
|
|
1657
|
+
}
|
|
1658
|
+
else if (s.type === 'vowel') {
|
|
1659
|
+
parts.push(`**Height**: ${s.height}`);
|
|
1660
|
+
parts.push(`**Backness**: ${s.backness}`);
|
|
1661
|
+
parts.push(`**Rounded**: ${s.rounded ? 'yes' : 'no'}`);
|
|
1662
|
+
}
|
|
1663
|
+
parts.push('');
|
|
1664
|
+
}
|
|
1665
|
+
return parts.join('\n');
|
|
1666
|
+
}
|
|
1667
|
+
case 'transcribe': {
|
|
1668
|
+
const words = query.split(/[\s,]+/).filter(w => w.length > 0);
|
|
1669
|
+
const parts = [`## IPA Transcription (General American English)\n`];
|
|
1670
|
+
parts.push('| Word | IPA |');
|
|
1671
|
+
parts.push('|------|-----|');
|
|
1672
|
+
for (const word of words) {
|
|
1673
|
+
const ipa = ENGLISH_IPA[word.toLowerCase()];
|
|
1674
|
+
if (ipa) {
|
|
1675
|
+
parts.push(`| ${word} | /${ipa}/ |`);
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
parts.push(`| ${word} | *(not in dictionary — ${Object.keys(ENGLISH_IPA).length} common words available)* |`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return parts.join('\n');
|
|
1682
|
+
}
|
|
1683
|
+
case 'compare': {
|
|
1684
|
+
// Compare phoneme inventories of languages
|
|
1685
|
+
const names = query.split(/[,;&]/).map(s => s.trim().toLowerCase());
|
|
1686
|
+
const langs = names
|
|
1687
|
+
.map(n => LANGUAGES.find(l => l.name.toLowerCase().includes(n)))
|
|
1688
|
+
.filter(Boolean);
|
|
1689
|
+
if (langs.length < 2)
|
|
1690
|
+
return `Need at least 2 languages. Found: ${langs.map(l => l.name).join(', ')}. Try: "english, japanese" or "mandarin, thai, vietnamese".`;
|
|
1691
|
+
const parts = [`## Phoneme Inventory Comparison\n`];
|
|
1692
|
+
parts.push('| Language | Phonemes | Tonal | Family |');
|
|
1693
|
+
parts.push('|----------|----------|-------|--------|');
|
|
1694
|
+
for (const l of langs) {
|
|
1695
|
+
parts.push(`| ${l.name} | ~${l.phonemeCount} | ${l.tonal ? 'Yes' : 'No'} | ${l.family} |`);
|
|
1696
|
+
}
|
|
1697
|
+
parts.push('');
|
|
1698
|
+
const maxPhonemes = Math.max(...langs.map(l => l.phonemeCount));
|
|
1699
|
+
const minPhonemes = Math.min(...langs.map(l => l.phonemeCount));
|
|
1700
|
+
parts.push(`**Range**: ${minPhonemes} – ${maxPhonemes} phonemes`);
|
|
1701
|
+
parts.push(`**Tonal languages**: ${langs.filter(l => l.tonal).map(l => l.name).join(', ') || 'none'}`);
|
|
1702
|
+
return parts.join('\n');
|
|
1703
|
+
}
|
|
1704
|
+
default:
|
|
1705
|
+
return `Unknown operation "${operation}". Use: lookup, describe, transcribe, compare.`;
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
// ── 8. text_stylometry ────────────────────────────────────────────────
|
|
1710
|
+
registerTool({
|
|
1711
|
+
name: 'text_stylometry',
|
|
1712
|
+
description: 'Author attribution and style analysis: sentence length, vocabulary richness (Yule\'s K, hapax ratio), function word frequencies, punctuation patterns, and Burrows\' Delta for author comparison.',
|
|
1713
|
+
parameters: {
|
|
1714
|
+
texts: { type: 'string', description: 'JSON array of {label, text} objects', required: true },
|
|
1715
|
+
operation: { type: 'string', description: 'Operation: profile, compare, delta', required: true },
|
|
1716
|
+
},
|
|
1717
|
+
tier: 'free',
|
|
1718
|
+
async execute(args) {
|
|
1719
|
+
let texts;
|
|
1720
|
+
try {
|
|
1721
|
+
texts = JSON.parse(String(args.texts));
|
|
1722
|
+
if (!Array.isArray(texts))
|
|
1723
|
+
throw new Error('Must be an array');
|
|
1724
|
+
}
|
|
1725
|
+
catch (err) {
|
|
1726
|
+
return `Error parsing texts JSON: ${err instanceof Error ? err.message : String(err)}\n\nExpected format: [{"label": "Author A", "text": "..."}]`;
|
|
1727
|
+
}
|
|
1728
|
+
if (texts.length === 0)
|
|
1729
|
+
return 'Error: No texts provided.';
|
|
1730
|
+
const operation = String(args.operation || 'profile').toLowerCase();
|
|
1731
|
+
const profiles = texts.map(t => profileText(t.label, t.text));
|
|
1732
|
+
switch (operation) {
|
|
1733
|
+
case 'profile': {
|
|
1734
|
+
const parts = ['## Stylometric Profiles\n'];
|
|
1735
|
+
for (const p of profiles) {
|
|
1736
|
+
parts.push(formatProfile(p));
|
|
1737
|
+
parts.push('');
|
|
1738
|
+
}
|
|
1739
|
+
return parts.join('\n');
|
|
1740
|
+
}
|
|
1741
|
+
case 'compare': {
|
|
1742
|
+
if (profiles.length < 2)
|
|
1743
|
+
return 'Need at least 2 texts for comparison.';
|
|
1744
|
+
const parts = ['## Stylometric Comparison\n'];
|
|
1745
|
+
// Comparison table
|
|
1746
|
+
const header = '| Metric | ' + profiles.map(p => `"${p.label}"`.slice(0, 15)).join(' | ') + ' |';
|
|
1747
|
+
const sep = '|--------|' + profiles.map(() => '---').join('|') + '|';
|
|
1748
|
+
parts.push(header);
|
|
1749
|
+
parts.push(sep);
|
|
1750
|
+
parts.push(`| Tokens | ${profiles.map(p => p.tokenCount.toString()).join(' | ')} |`);
|
|
1751
|
+
parts.push(`| Types | ${profiles.map(p => p.typeCount.toString()).join(' | ')} |`);
|
|
1752
|
+
parts.push(`| Avg sentence len | ${profiles.map(p => p.avgSentenceLength.toFixed(1)).join(' | ')} |`);
|
|
1753
|
+
parts.push(`| Avg word len | ${profiles.map(p => p.avgWordLength.toFixed(2)).join(' | ')} |`);
|
|
1754
|
+
parts.push(`| TTR | ${profiles.map(p => p.ttr.toFixed(4)).join(' | ')} |`);
|
|
1755
|
+
parts.push(`| Hapax ratio | ${profiles.map(p => p.hapaxRatio.toFixed(4)).join(' | ')} |`);
|
|
1756
|
+
parts.push(`| Yule's K | ${profiles.map(p => p.yulesK.toFixed(2)).join(' | ')} |`);
|
|
1757
|
+
return parts.join('\n');
|
|
1758
|
+
}
|
|
1759
|
+
case 'delta': {
|
|
1760
|
+
return burrowsDelta(profiles);
|
|
1761
|
+
}
|
|
1762
|
+
default:
|
|
1763
|
+
return `Unknown operation "${operation}". Use: profile, compare, delta.`;
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
});
|
|
1767
|
+
// ── 9. philosophical_concept ──────────────────────────────────────────
|
|
1768
|
+
registerTool({
|
|
1769
|
+
name: 'philosophical_concept',
|
|
1770
|
+
description: 'Encyclopedia of ~80 key philosophical concepts with branch, period, key thinkers, definition, related concepts, and arguments for/against. Search by concept, thinker, or branch.',
|
|
1771
|
+
parameters: {
|
|
1772
|
+
query: { type: 'string', description: 'Concept name, thinker name, or branch (metaphysics/epistemology/ethics/logic/aesthetics/political)', required: true },
|
|
1773
|
+
search_type: { type: 'string', description: 'Search type: concept, thinker, branch', required: true },
|
|
1774
|
+
},
|
|
1775
|
+
tier: 'free',
|
|
1776
|
+
async execute(args) {
|
|
1777
|
+
const query = String(args.query).toLowerCase();
|
|
1778
|
+
const searchType = String(args.search_type || 'concept').toLowerCase();
|
|
1779
|
+
const formatConcept = (c) => {
|
|
1780
|
+
return [
|
|
1781
|
+
`### ${c.name}`,
|
|
1782
|
+
`*${c.branch} — ${c.period}*\n`,
|
|
1783
|
+
`**Key thinkers**: ${c.thinkers.join(', ')}\n`,
|
|
1784
|
+
`**Definition**: ${c.definition}\n`,
|
|
1785
|
+
`**Related concepts**: ${c.relatedConcepts.join(', ')}\n`,
|
|
1786
|
+
'**Arguments for**:',
|
|
1787
|
+
...c.argumentsFor.map(a => `- ${a}`),
|
|
1788
|
+
'',
|
|
1789
|
+
'**Arguments against**:',
|
|
1790
|
+
...c.argumentsAgainst.map(a => `- ${a}`),
|
|
1791
|
+
].join('\n');
|
|
1792
|
+
};
|
|
1793
|
+
switch (searchType) {
|
|
1794
|
+
case 'concept': {
|
|
1795
|
+
const matches = PHILOSOPHICAL_CONCEPTS.filter(c => c.name.toLowerCase().includes(query) ||
|
|
1796
|
+
c.definition.toLowerCase().includes(query));
|
|
1797
|
+
if (matches.length === 0)
|
|
1798
|
+
return `No concept found matching "${query}". Try: consciousness, utilitarianism, free will, empiricism, justice...`;
|
|
1799
|
+
return `## Philosophical Concepts\n\n${matches.slice(0, 5).map(formatConcept).join('\n\n---\n\n')}`;
|
|
1800
|
+
}
|
|
1801
|
+
case 'thinker': {
|
|
1802
|
+
const matches = PHILOSOPHICAL_CONCEPTS.filter(c => c.thinkers.some(t => t.toLowerCase().includes(query)));
|
|
1803
|
+
if (matches.length === 0)
|
|
1804
|
+
return `No concepts found for thinker "${query}". Try: Kant, Aristotle, Hume, Rawls, Descartes...`;
|
|
1805
|
+
const parts = [`## Concepts associated with "${query}"\n`];
|
|
1806
|
+
parts.push(`**${matches.length} concepts found:**\n`);
|
|
1807
|
+
for (const m of matches) {
|
|
1808
|
+
parts.push(`- **${m.name}** (${m.branch}) — ${m.definition.slice(0, 100)}...`);
|
|
1809
|
+
}
|
|
1810
|
+
parts.push('\n---\n');
|
|
1811
|
+
// Show first 3 in detail
|
|
1812
|
+
for (const m of matches.slice(0, 3)) {
|
|
1813
|
+
parts.push(formatConcept(m));
|
|
1814
|
+
parts.push('\n---\n');
|
|
1815
|
+
}
|
|
1816
|
+
return parts.join('\n');
|
|
1817
|
+
}
|
|
1818
|
+
case 'branch': {
|
|
1819
|
+
const matches = PHILOSOPHICAL_CONCEPTS.filter(c => c.branch.toLowerCase().includes(query));
|
|
1820
|
+
if (matches.length === 0)
|
|
1821
|
+
return `No concepts found in branch "${query}". Branches: metaphysics, epistemology, ethics, logic, aesthetics, political.`;
|
|
1822
|
+
const parts = [`## ${query.charAt(0).toUpperCase() + query.slice(1)} — ${matches.length} concepts\n`];
|
|
1823
|
+
// Group by period
|
|
1824
|
+
const periods = [...new Set(matches.map(m => m.period))].sort();
|
|
1825
|
+
for (const period of periods) {
|
|
1826
|
+
const periodMatches = matches.filter(m => m.period === period);
|
|
1827
|
+
parts.push(`### ${period}`);
|
|
1828
|
+
for (const m of periodMatches) {
|
|
1829
|
+
parts.push(`- **${m.name}** (${m.thinkers.slice(0, 3).join(', ')}) — ${m.definition.slice(0, 80)}...`);
|
|
1830
|
+
}
|
|
1831
|
+
parts.push('');
|
|
1832
|
+
}
|
|
1833
|
+
return parts.join('\n');
|
|
1834
|
+
}
|
|
1835
|
+
default:
|
|
1836
|
+
return `Unknown search type "${searchType}". Use: concept, thinker, branch.`;
|
|
1837
|
+
}
|
|
1838
|
+
},
|
|
1839
|
+
});
|
|
1840
|
+
// ── 10. archival_search ────────────────────────────────────────────────
|
|
1841
|
+
registerTool({
|
|
1842
|
+
name: 'archival_search',
|
|
1843
|
+
description: 'Search digital archives and open-access historical collections: Internet Archive, Digital Public Library of America (DPLA), Europeana. Returns titles, descriptions, dates, and links.',
|
|
1844
|
+
parameters: {
|
|
1845
|
+
query: { type: 'string', description: 'Search query for archival materials', required: true },
|
|
1846
|
+
source: { type: 'string', description: 'Source: all, internet_archive, dpla, europeana' },
|
|
1847
|
+
media_type: { type: 'string', description: 'Media type: text, image, audio, all' },
|
|
1848
|
+
},
|
|
1849
|
+
tier: 'free',
|
|
1850
|
+
async execute(args) {
|
|
1851
|
+
const query = String(args.query);
|
|
1852
|
+
const source = String(args.source || 'all').toLowerCase();
|
|
1853
|
+
const mediaType = String(args.media_type || 'all').toLowerCase();
|
|
1854
|
+
const parts = [`## Archival Search: "${query}"\n`];
|
|
1855
|
+
const errors = [];
|
|
1856
|
+
// Internet Archive
|
|
1857
|
+
if (source === 'all' || source === 'internet_archive') {
|
|
1858
|
+
try {
|
|
1859
|
+
const encoded = encodeURIComponent(query);
|
|
1860
|
+
const mtFilter = mediaType !== 'all' ? `&fl[]=mediatype&mediatype=${encodeURIComponent(mediaType === 'text' ? 'texts' : mediaType)}` : '';
|
|
1861
|
+
const url = `https://archive.org/advancedsearch.php?q=${encoded}${mtFilter}&output=json&rows=5&fl[]=identifier&fl[]=title&fl[]=description&fl[]=date&fl[]=mediatype&fl[]=creator`;
|
|
1862
|
+
const res = await fetch(url, {
|
|
1863
|
+
headers: { 'User-Agent': 'KBot/3.0 (Archival Search)' },
|
|
1864
|
+
signal: AbortSignal.timeout(10000),
|
|
1865
|
+
});
|
|
1866
|
+
if (res.ok) {
|
|
1867
|
+
const data = await res.json();
|
|
1868
|
+
const docs = data.response?.docs || [];
|
|
1869
|
+
if (docs.length > 0) {
|
|
1870
|
+
parts.push('### Internet Archive\n');
|
|
1871
|
+
for (const doc of docs) {
|
|
1872
|
+
const title = doc.title || 'Untitled';
|
|
1873
|
+
const id = doc.identifier || '';
|
|
1874
|
+
const date = doc.date || 'n.d.';
|
|
1875
|
+
const creator = doc.creator || 'Unknown';
|
|
1876
|
+
const desc = Array.isArray(doc.description)
|
|
1877
|
+
? (doc.description[0] || '').slice(0, 150)
|
|
1878
|
+
: (doc.description || '').slice(0, 150);
|
|
1879
|
+
const link = id ? `https://archive.org/details/${id}` : '';
|
|
1880
|
+
parts.push(`**${title}**`);
|
|
1881
|
+
parts.push(`*${creator}* — ${date} | ${doc.mediatype || 'unknown'}`);
|
|
1882
|
+
if (desc)
|
|
1883
|
+
parts.push(`> ${desc}${desc.length >= 150 ? '...' : ''}`);
|
|
1884
|
+
if (link)
|
|
1885
|
+
parts.push(`[View on Internet Archive](${link})`);
|
|
1886
|
+
parts.push('');
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
parts.push('### Internet Archive\nNo results found.\n');
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
catch (err) {
|
|
1895
|
+
errors.push(`Internet Archive: ${err instanceof Error ? err.message : 'timeout or network error'}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
// DPLA
|
|
1899
|
+
if (source === 'all' || source === 'dpla') {
|
|
1900
|
+
try {
|
|
1901
|
+
const encoded = encodeURIComponent(query);
|
|
1902
|
+
const url = `https://api.dp.la/v2/items?q=${encoded}&page_size=5&api_key=`;
|
|
1903
|
+
// DPLA requires an API key but has a public demo endpoint
|
|
1904
|
+
const res = await fetch(url, {
|
|
1905
|
+
headers: { 'User-Agent': 'KBot/3.0 (Archival Search)' },
|
|
1906
|
+
signal: AbortSignal.timeout(10000),
|
|
1907
|
+
});
|
|
1908
|
+
if (res.ok) {
|
|
1909
|
+
const data = await res.json();
|
|
1910
|
+
const docs = data.docs || [];
|
|
1911
|
+
if (docs.length > 0) {
|
|
1912
|
+
parts.push('### Digital Public Library of America (DPLA)\n');
|
|
1913
|
+
for (const doc of docs) {
|
|
1914
|
+
const sr = doc.sourceResource || {};
|
|
1915
|
+
const title = Array.isArray(sr.title) ? sr.title[0] : (sr.title || 'Untitled');
|
|
1916
|
+
const desc = Array.isArray(sr.description)
|
|
1917
|
+
? (sr.description[0] || '').slice(0, 150)
|
|
1918
|
+
: (sr.description || '').slice(0, 150);
|
|
1919
|
+
const date = sr.date?.[0]?.displayDate || 'n.d.';
|
|
1920
|
+
const creator = sr.creator?.join(', ') || 'Unknown';
|
|
1921
|
+
const link = doc.isShownAt || '';
|
|
1922
|
+
const provider = doc.provider?.name || '';
|
|
1923
|
+
parts.push(`**${title}**`);
|
|
1924
|
+
parts.push(`*${creator}* — ${date}${provider ? ' | ' + provider : ''}`);
|
|
1925
|
+
if (desc)
|
|
1926
|
+
parts.push(`> ${desc}${desc.length >= 150 ? '...' : ''}`);
|
|
1927
|
+
if (link)
|
|
1928
|
+
parts.push(`[View source](${link})`);
|
|
1929
|
+
parts.push('');
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
else {
|
|
1933
|
+
parts.push('### DPLA\nNo results found.\n');
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
catch (err) {
|
|
1938
|
+
errors.push(`DPLA: ${err instanceof Error ? err.message : 'timeout or network error'}`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
// Europeana
|
|
1942
|
+
if (source === 'all' || source === 'europeana') {
|
|
1943
|
+
try {
|
|
1944
|
+
const encoded = encodeURIComponent(query);
|
|
1945
|
+
// Europeana REST API (public key available)
|
|
1946
|
+
const url = `https://api.europeana.eu/record/v2/search.json?query=${encoded}&rows=5&profile=standard&wskey=api2demo`;
|
|
1947
|
+
const res = await fetch(url, {
|
|
1948
|
+
headers: { 'User-Agent': 'KBot/3.0 (Archival Search)' },
|
|
1949
|
+
signal: AbortSignal.timeout(10000),
|
|
1950
|
+
});
|
|
1951
|
+
if (res.ok) {
|
|
1952
|
+
const data = await res.json();
|
|
1953
|
+
const items = data.items || [];
|
|
1954
|
+
if (items.length > 0) {
|
|
1955
|
+
parts.push('### Europeana\n');
|
|
1956
|
+
for (const item of items) {
|
|
1957
|
+
const title = item.title?.[0] || 'Untitled';
|
|
1958
|
+
const desc = (item.dcDescription?.[0] || '').slice(0, 150);
|
|
1959
|
+
const year = item.year?.[0] || 'n.d.';
|
|
1960
|
+
const creator = item.dcCreator?.[0] || 'Unknown';
|
|
1961
|
+
const link = item.guid || '';
|
|
1962
|
+
const provider = item.dataProvider?.[0] || '';
|
|
1963
|
+
parts.push(`**${title}**`);
|
|
1964
|
+
parts.push(`*${creator}* — ${year}${provider ? ' | ' + provider : ''} | ${item.type || ''}`);
|
|
1965
|
+
if (desc)
|
|
1966
|
+
parts.push(`> ${desc}${desc.length >= 150 ? '...' : ''}`);
|
|
1967
|
+
if (link)
|
|
1968
|
+
parts.push(`[View on Europeana](${link})`);
|
|
1969
|
+
parts.push('');
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
parts.push('### Europeana\nNo results found.\n');
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
catch (err) {
|
|
1978
|
+
errors.push(`Europeana: ${err instanceof Error ? err.message : 'timeout or network error'}`);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (errors.length > 0) {
|
|
1982
|
+
parts.push('---\n**Errors**:');
|
|
1983
|
+
for (const e of errors)
|
|
1984
|
+
parts.push(`- ${e}`);
|
|
1985
|
+
}
|
|
1986
|
+
if (parts.length <= 2) {
|
|
1987
|
+
parts.push('No results found from any source. Try broadening your search terms.');
|
|
1988
|
+
}
|
|
1989
|
+
return parts.join('\n');
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
//# sourceMappingURL=lab-humanities.js.map
|