calliope-ts 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +3 -13
  2. package/dist/caesura.d.ts +33 -0
  3. package/dist/caesura.d.ts.map +1 -0
  4. package/dist/caesura.js +202 -0
  5. package/dist/calliope/boundaries.d.ts +19 -0
  6. package/dist/calliope/boundaries.d.ts.map +1 -0
  7. package/dist/calliope/boundaries.js +182 -0
  8. package/dist/calliope/bracketing.d.ts +11 -0
  9. package/dist/calliope/bracketing.d.ts.map +1 -0
  10. package/dist/calliope/bracketing.js +416 -0
  11. package/dist/calliope/deps.d.ts +4 -0
  12. package/dist/calliope/deps.d.ts.map +1 -0
  13. package/dist/calliope/deps.js +181 -0
  14. package/dist/calliope/engine.d.ts +3 -0
  15. package/dist/calliope/engine.d.ts.map +1 -0
  16. package/dist/calliope/engine.js +71 -0
  17. package/dist/calliope/feats.d.ts +9 -0
  18. package/dist/calliope/feats.d.ts.map +1 -0
  19. package/dist/calliope/feats.js +45 -0
  20. package/dist/calliope/names.d.ts +7 -0
  21. package/dist/calliope/names.d.ts.map +1 -0
  22. package/dist/calliope/names.js +42 -0
  23. package/dist/calliope/postag.d.ts +8 -0
  24. package/dist/calliope/postag.d.ts.map +1 -0
  25. package/dist/calliope/postag.js +250 -0
  26. package/dist/calliope/prosodic.d.ts +3 -0
  27. package/dist/calliope/prosodic.d.ts.map +1 -0
  28. package/dist/calliope/prosodic.js +275 -0
  29. package/dist/calliope/relstress.d.ts +4 -0
  30. package/dist/calliope/relstress.d.ts.map +1 -0
  31. package/dist/calliope/relstress.js +688 -0
  32. package/dist/calliope/stressrules.d.ts +28 -0
  33. package/dist/calliope/stressrules.d.ts.map +1 -0
  34. package/dist/calliope/stressrules.js +147 -0
  35. package/dist/calliope/syntax.d.ts +38 -0
  36. package/dist/calliope/syntax.d.ts.map +1 -0
  37. package/dist/calliope/syntax.js +234 -0
  38. package/dist/calliope/udpos.d.ts +13 -0
  39. package/dist/calliope/udpos.d.ts.map +1 -0
  40. package/dist/calliope/udpos.js +156 -0
  41. package/dist/clio/caesura.d.ts +27 -0
  42. package/dist/clio/caesura.d.ts.map +1 -0
  43. package/dist/clio/caesura.js +148 -0
  44. package/dist/clio/depfix.d.ts +13 -0
  45. package/dist/clio/depfix.d.ts.map +1 -0
  46. package/dist/clio/depfix.js +84 -0
  47. package/dist/clio/display.d.ts +32 -0
  48. package/dist/clio/display.d.ts.map +1 -0
  49. package/dist/clio/display.js +976 -0
  50. package/dist/clio/engine.d.ts +3 -0
  51. package/dist/clio/engine.d.ts.map +1 -0
  52. package/dist/clio/engine.js +28 -0
  53. package/dist/clio/parser.d.ts +10 -0
  54. package/dist/clio/parser.d.ts.map +1 -0
  55. package/dist/clio/parser.js +696 -0
  56. package/dist/clio/phonological.d.ts +41 -0
  57. package/dist/clio/phonological.d.ts.map +1 -0
  58. package/dist/clio/phonological.js +788 -0
  59. package/dist/clio/phrasestress.d.ts +6 -0
  60. package/dist/clio/phrasestress.d.ts.map +1 -0
  61. package/dist/clio/phrasestress.js +106 -0
  62. package/dist/clio/pipeline.d.ts +11 -0
  63. package/dist/clio/pipeline.d.ts.map +1 -0
  64. package/dist/clio/pipeline.js +147 -0
  65. package/dist/clio/rhyme.d.ts +65 -0
  66. package/dist/clio/rhyme.d.ts.map +1 -0
  67. package/dist/clio/rhyme.js +761 -0
  68. package/dist/clio/scandroid.d.ts +17 -0
  69. package/dist/clio/scandroid.d.ts.map +1 -0
  70. package/dist/clio/scandroid.js +435 -0
  71. package/dist/clio/scansion.d.ts +46 -0
  72. package/dist/clio/scansion.d.ts.map +1 -0
  73. package/dist/clio/scansion.js +1086 -0
  74. package/dist/clio/semantics.d.ts +44 -0
  75. package/dist/clio/semantics.d.ts.map +1 -0
  76. package/dist/clio/semantics.js +139 -0
  77. package/dist/clio/stress.d.ts +83 -0
  78. package/dist/clio/stress.d.ts.map +1 -0
  79. package/dist/clio/stress.js +1737 -0
  80. package/dist/clio/tagfix.d.ts +6 -0
  81. package/dist/clio/tagfix.d.ts.map +1 -0
  82. package/dist/clio/tagfix.js +101 -0
  83. package/dist/display.d.ts +0 -6
  84. package/dist/display.d.ts.map +1 -1
  85. package/dist/display.js +322 -141
  86. package/dist/engine.d.ts +9 -0
  87. package/dist/engine.d.ts.map +1 -0
  88. package/dist/engine.js +12 -0
  89. package/dist/index.d.ts +4 -3
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +169 -26
  92. package/dist/parser.d.ts +3 -3
  93. package/dist/parser.d.ts.map +1 -1
  94. package/dist/parser.js +320 -563
  95. package/dist/phonological.d.ts +1 -1
  96. package/dist/phonological.d.ts.map +1 -1
  97. package/dist/phonological.js +73 -4
  98. package/dist/phrasestress.d.ts +6 -0
  99. package/dist/phrasestress.d.ts.map +1 -0
  100. package/dist/phrasestress.js +106 -0
  101. package/dist/rhyme.d.ts +40 -1
  102. package/dist/rhyme.d.ts.map +1 -1
  103. package/dist/rhyme.js +435 -7
  104. package/dist/scansion.d.ts +9 -0
  105. package/dist/scansion.d.ts.map +1 -1
  106. package/dist/scansion.js +145 -18
  107. package/dist/semantics.d.ts +44 -0
  108. package/dist/semantics.d.ts.map +1 -0
  109. package/dist/semantics.js +139 -0
  110. package/dist/stress.d.ts +127 -4
  111. package/dist/stress.d.ts.map +1 -1
  112. package/dist/stress.js +834 -61
  113. package/dist/types.d.ts +15 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/package.json +5 -3
  116. package/src/caesura.ts +201 -0
  117. package/src/calliope/boundaries.ts +190 -0
  118. package/src/calliope/bracketing.ts +390 -0
  119. package/src/calliope/deps.ts +160 -0
  120. package/src/calliope/engine.ts +77 -0
  121. package/src/calliope/feats.ts +46 -0
  122. package/src/calliope/names.ts +44 -0
  123. package/src/calliope/postag.ts +253 -0
  124. package/src/calliope/prosodic.ts +262 -0
  125. package/src/calliope/relstress.ts +645 -0
  126. package/src/calliope/stressrules.ts +147 -0
  127. package/src/calliope/syntax.ts +218 -0
  128. package/src/calliope/udpos.ts +152 -0
  129. package/src/clio/caesura.ts +145 -0
  130. package/src/clio/depfix.ts +88 -0
  131. package/src/clio/display.ts +1042 -0
  132. package/src/clio/engine.ts +38 -0
  133. package/src/clio/parser.ts +845 -0
  134. package/src/clio/phonological.ts +849 -0
  135. package/src/clio/phrasestress.ts +108 -0
  136. package/src/clio/pipeline.ts +154 -0
  137. package/src/clio/rhyme.ts +740 -0
  138. package/src/clio/scandroid.ts +434 -0
  139. package/src/clio/scansion.ts +1130 -0
  140. package/src/clio/semantics.ts +134 -0
  141. package/src/clio/stress.ts +1731 -0
  142. package/src/clio/tagfix.ts +104 -0
  143. package/src/display.ts +321 -137
  144. package/src/engine.ts +22 -0
  145. package/src/index.ts +176 -32
  146. package/src/parser.ts +346 -682
  147. package/src/phonological.ts +72 -4
  148. package/src/phrasestress.ts +108 -0
  149. package/src/rhyme.ts +428 -8
  150. package/src/scansion.ts +136 -15
  151. package/src/semantics.ts +134 -0
  152. package/src/stress.ts +794 -64
  153. package/src/types.ts +39 -4
  154. package/tests/DataForHayesLinesOnly.txt +364 -0
  155. package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
  156. package/tests/basic.test.ts +443 -37
  157. package/tests/bench-hayes.mjs +72 -0
  158. package/tests/wagner-stress.test.ts +188 -0
  159. package/vitest.config.ts +0 -15
package/src/index.ts CHANGED
@@ -3,20 +3,27 @@ import * as fs from 'fs';
3
3
  import * as readline from 'readline';
4
4
  import chalk from 'chalk';
5
5
  import { fileURLToPath } from 'url';
6
- import { parseDocument } from './parser.js';
6
+ import { parseDocument, isPunctuation } from './parser.js';
7
+ import { ProsodyEngine } from './engine.js';
8
+ import { calliopeEngine } from './calliope/engine.js';
9
+ import { clioEngine } from './clio/engine.js';
10
+ // The FROZEN Clio pipeline + renderers (its own copy of the whole computational
11
+ // tree). When Clio is active the CLI runs entirely through these, so live changes
12
+ // to the regular (Calliope) pipeline — scorer included — cannot affect Clio.
7
13
  import {
8
- assignLexicalStress,
9
- applyCompoundStress,
10
- applyNuclearStress,
11
- assignRelativeStresses,
12
- } from './stress.js';
14
+ analyzeReadingDocumentClio,
15
+ renderUnifiedDisplay as clioRenderUnifiedDisplay,
16
+ renderReadingView as clioRenderReadingView,
17
+ renderFullLegend as clioRenderFullLegend,
18
+ parseDocument as clioParseDocument,
19
+ isPunctuation as clioIsPunctuation,
20
+ } from './clio/pipeline.js';
13
21
  import {
14
- buildPhonologicalHierarchy,
15
22
  renderHierarchy,
16
23
  renderKeyStresses,
17
24
  flattenDisplayEntries,
18
25
  } from './phonological.js';
19
- import { extractKeyStresses, scoreMeters, applyStanzaConsensus, applyRhythmLayer } from './scansion.js';
26
+ import { extractKeyStresses, scoreMeters, applyStanzaConsensus, applyRhythmLayer, applyMetricalityLayer } from './scansion.js';
20
27
  import { applyRhymeAndForm } from './rhyme.js';
21
28
  import {
22
29
  scandroidCorralWeird,
@@ -31,6 +38,7 @@ import {
31
38
  } from './display.js';
32
39
  import type {
33
40
  ClsSentence,
41
+ ClsWord,
34
42
  MetreName,
35
43
  IntonationalUnit,
36
44
  StressLevel,
@@ -43,6 +51,54 @@ import type {
43
51
  DisplayOptions,
44
52
  } from './types.js';
45
53
 
54
+ // The active prosody engine for this process: Calliope (faithful, default) or
55
+ // Clio (the frozen legacy / alternative parse), chosen by `--clio` or the REPL
56
+ // menu. The exported analysis functions default their `engine` parameter to
57
+ // this, so library callers (tests, trials, benchmark) transparently use the
58
+ // active engine without signature churn.
59
+ let activeEngine: ProsodyEngine = calliopeEngine;
60
+
61
+ /**
62
+ * Stanza-level discourse givenness (Wagner Ch.7, the plan's Gap 13) — CAREFUL and
63
+ * cross-line only. A CONTENT word repeated from an EARLIER line of the same stanza
64
+ * is discourse-given, so it may be subordinated relative to a new-information sister
65
+ * (the relativiser reads `discourseGiven`). Constraints that protect single-line
66
+ * scanning and thematically-focused words:
67
+ * • the first line is never marked (no prior context);
68
+ * • a lemma repeated WITHIN a single line is FOCUSED, not given → never marked
69
+ * (Eliot's "Nothing … nothing", a refrain);
70
+ * • only the 2nd-and-later occurrence (across lines) is marked, never the first;
71
+ * • single-line input has no previous line, so nothing is marked — the standout
72
+ * isolated-line feature is untouched.
73
+ * Mutates `discourseGiven` on the words; must run BEFORE the per-line relativisation.
74
+ */
75
+ function markStanzaGivenness(docPerLine: ClsSentence[][]): void {
76
+ if (docPerLine.length < 2) return; // single line → no givenness
77
+ const key = (w: ClsWord) => w.word.toLowerCase().replace(/['’]/g, '');
78
+ // Lemmas that appear ≥2× within ANY one line are focal (refrain/emphasis) — exempt.
79
+ const focal = new Set<string>();
80
+ for (const sents of docPerLine) {
81
+ const counts = new Map<string, number>();
82
+ for (const sent of sents) for (const w of sent.words) {
83
+ if (!w.isContent) continue;
84
+ const k = key(w);
85
+ counts.set(k, (counts.get(k) ?? 0) + 1);
86
+ }
87
+ for (const [k, n] of counts) if (n >= 2) focal.add(k);
88
+ }
89
+ const seen = new Set<string>(); // content lemmas in PRIOR lines
90
+ for (const sents of docPerLine) {
91
+ const thisLine = new Set<string>();
92
+ for (const sent of sents) for (const w of sent.words) {
93
+ if (!w.isContent) continue;
94
+ const k = key(w);
95
+ if (seen.has(k) && !focal.has(k)) w.discourseGiven = true;
96
+ thisLine.add(k);
97
+ }
98
+ for (const k of thisLine) seen.add(k);
99
+ }
100
+ }
101
+
46
102
  /**
47
103
  * Scan one VERSE LINE (which may parse into several grammatical sentences).
48
104
  *
@@ -56,16 +112,15 @@ import type {
56
112
  * line's full concatenated syllable stream, with each sentence's IUs preserved
57
113
  * as IU boundaries (→ hard caesurae) inside the line.
58
114
  */
59
- function processLine(sents: ClsSentence[]): LineResult | null {
115
+ function processLine(sents: ClsSentence[], engine: ProsodyEngine = activeEngine): LineResult | null {
60
116
  if (sents.length === 0) return null;
61
117
 
62
118
  const iusPerSent: IntonationalUnit[][] = [];
63
119
  for (const sent of sents) {
64
- assignLexicalStress(sent.words);
65
- const sentIus = buildPhonologicalHierarchy(sent);
66
- applyCompoundStress(sentIus);
67
- applyNuclearStress(sentIus);
68
- assignRelativeStresses(sent.words, sentIus);
120
+ // The selected engine runs the per-sentence linguistic passes (lexical →
121
+ // hierarchy compound/nuclear → phrase → relative stress) and returns the
122
+ // prosodic hierarchy. Calliope (default) and Clio differ only here.
123
+ const sentIus = engine.analyzeSentence(sent);
69
124
  iusPerSent.push(sentIus);
70
125
  }
71
126
 
@@ -172,15 +227,19 @@ function applyContinuityRename(results: LineResult[]): void {
172
227
  }
173
228
  }
174
229
 
175
- export function analyzeStanzas(text: string, useScandroid = true): LineResult[][] {
230
+ export function analyzeStanzas(text: string, useScandroid = true, engine: ProsodyEngine = activeEngine): LineResult[][] {
176
231
  const stanzas = text.split(/\n\s*\n/);
177
232
  const results: LineResult[][] = [];
178
233
  for (const stanza of stanzas) {
179
234
  const lines = stanza.split('\n').filter(l => l.trim() !== '');
180
235
  const stanzaResults: LineResult[] = [];
181
- for (const line of lines) {
182
- const doc = parseDocument(line);
183
- const res = processLine(doc.sentences);
236
+ // Parse every line first, then mark cross-line discourse givenness BEFORE the
237
+ // per-line stress passes (relativisation reads `discourseGiven`). Only the
238
+ // default (Calliope) relativiser consults it; Clio ignores the flag.
239
+ const docs = lines.map(line => parseDocument(line));
240
+ markStanzaGivenness(docs.map(d => d.sentences));
241
+ for (const doc of docs) {
242
+ const res = processLine(doc.sentences, engine);
184
243
  if (res) stanzaResults.push(res);
185
244
  }
186
245
  // Resolve near-tie lines toward the stanza's dominant meter (explicit, non-
@@ -204,6 +263,9 @@ export function analyzeStanzas(text: string, useScandroid = true): LineResult[][
204
263
  applyContinuityRename(all);
205
264
  for (const st of results) applyRhythmLayer(st.map(r => r.phonologicalScansion));
206
265
  }
266
+ // Prose-likeness hedge (Option 0): advisory, runs after the rhythm layer so it
267
+ // can defer to any accentual/dolnik verdict; annotation-only (metricalityNote).
268
+ applyMetricalityLayer(results.flatMap(st => st.map(r => r.phonologicalScansion)));
207
269
  // Rhyme scheme + poetic-form identification spans stanzas (sonnets, terza
208
270
  // rima); annotation-only (rhyme/formNote on each line's detail).
209
271
  applyRhymeAndForm(results);
@@ -214,8 +276,8 @@ export function analyzeStanzas(text: string, useScandroid = true): LineResult[][
214
276
  * Convenience wrapper: analyse a multi‑line text, ignore stanza breaks,
215
277
  * return a flat list of LineResult for each line.
216
278
  */
217
- export function analyzeText(text: string, useScandroid = true): LineResult[] {
218
- const stanzaResults = analyzeStanzas(text, useScandroid);
279
+ export function analyzeText(text: string, useScandroid = true, engine: ProsodyEngine = activeEngine): LineResult[] {
280
+ const stanzaResults = analyzeStanzas(text, useScandroid, engine);
219
281
  return stanzaResults.flat();
220
282
  }
221
283
 
@@ -224,15 +286,16 @@ export function analyzeText(text: string, useScandroid = true): LineResult[] {
224
286
  * each original input line alongside its (1+) parsed sentence results so the
225
287
  * stress gradient can be projected back over the verbatim text.
226
288
  */
227
- export function analyzeReadingDocument(text: string): ReadingStanza[] {
289
+ export function analyzeReadingDocument(text: string, engine: ProsodyEngine = activeEngine): ReadingStanza[] {
228
290
  const stanzas = text.split(/\n\s*\n/);
229
291
  const out: ReadingStanza[] = [];
230
292
  for (const stanza of stanzas) {
231
293
  const rawLines = stanza.split('\n').filter(l => l.trim() !== '');
232
294
  if (rawLines.length === 0) continue;
233
- const lines = rawLines.map(raw => {
234
- const doc = parseDocument(raw);
235
- const res = processLine(doc.sentences);
295
+ const rawDocs = rawLines.map(raw => ({ raw, doc: parseDocument(raw) }));
296
+ markStanzaGivenness(rawDocs.map(rd => rd.doc.sentences));
297
+ const lines = rawDocs.map(({ raw, doc }) => {
298
+ const res = processLine(doc.sentences, engine);
236
299
  return { raw, results: res ? [res] : [] };
237
300
  });
238
301
  applyStanzaConsensus(lines.flatMap(l => l.results.map(r => r.phonologicalScansion)));
@@ -246,6 +309,7 @@ export function analyzeReadingDocument(text: string): ReadingStanza[] {
246
309
  applyContinuityRename(all);
247
310
  for (const st of out) applyRhythmLayer(st.lines.flatMap(l => l.results.map(r => r.phonologicalScansion)));
248
311
  }
312
+ applyMetricalityLayer(out.flatMap(st => st.lines.flatMap(l => l.results.map(r => r.phonologicalScansion))));
249
313
  applyRhymeAndForm(out.map(st => st.lines.flatMap(l => l.results)));
250
314
  return out;
251
315
  }
@@ -255,7 +319,10 @@ export function analyzeReadingDocument(text: string): ReadingStanza[] {
255
319
  function showResults(text: string): void {
256
320
  // Use the reading-document analysis so each LineResult keeps its verbatim input
257
321
  // line (for the tail Reading Projection) and the stanza-consensus annotation.
258
- const stanzas = analyzeReadingDocument(text);
322
+ // Clio routes through its own frozen pipeline + renderer.
323
+ const isClio = activeEngine.name === 'clio';
324
+ const stanzas = (isClio ? analyzeReadingDocumentClio : analyzeReadingDocument)(text);
325
+ const render = isClio ? clioRenderUnifiedDisplay : renderUnifiedDisplay;
259
326
 
260
327
  for (let s = 0; s < stanzas.length; s++) {
261
328
  if (stanzas.length > 1) {
@@ -263,15 +330,16 @@ function showResults(text: string): void {
263
330
  }
264
331
  for (const ln of stanzas[s].lines) {
265
332
  for (const res of ln.results) {
266
- console.log(renderUnifiedDisplay(res, ln.raw));
333
+ console.log(render(res, ln.raw));
267
334
  }
268
335
  }
269
336
  }
270
337
  }
271
338
 
272
339
  function showReadingView(text: string): void {
273
- const stanzas = analyzeReadingDocument(text);
274
- console.log(renderReadingView(stanzas));
340
+ const isClio = activeEngine.name === 'clio';
341
+ const stanzas = (isClio ? analyzeReadingDocumentClio : analyzeReadingDocument)(text);
342
+ console.log((isClio ? clioRenderReadingView : renderReadingView)(stanzas));
275
343
  }
276
344
 
277
345
  // ─── MULTI-LINE INPUT (paste-friendly) ──────────────────────────
@@ -426,6 +494,7 @@ async function replMode(): Promise<void> {
426
494
  { title: 'Line-by-Line Analysis (detailed view)', value: 'multiline' },
427
495
  { title: 'Parse & Scan from File (reading view)', value: 'reading-file' },
428
496
  { title: 'Analyze from File (detailed view)', value: 'file' },
497
+ { title: 'Ask Clio instead (alternative parse) — toggle engine', value: 'engine' },
429
498
  { title: 'Display Legend', value: 'legend' },
430
499
  { title: 'Exit', value: 'exit' },
431
500
  ],
@@ -437,8 +506,21 @@ async function replMode(): Promise<void> {
437
506
  break;
438
507
  }
439
508
 
509
+ if (response.action === 'engine') {
510
+ // Toggle between Calliope (faithful, default) and Clio (the legacy /
511
+ // alternative parse). Clio is Calliope's historian sister — sometimes
512
+ // on point, but not the primary poetic voice.
513
+ activeEngine = activeEngine.name === 'calliope' ? clioEngine : calliopeEngine;
514
+ const label = activeEngine.name === 'clio'
515
+ ? 'Clio — legacy / alternative parse'
516
+ : 'Calliope — faithful, default';
517
+ console.log(chalk.dim(`\n Active engine: ${chalk.bold(label)}\n`));
518
+ continue;
519
+ }
520
+
440
521
  if (response.action === 'legend') {
441
- console.log('\n' + renderFullLegend() + '\n');
522
+ const legend = activeEngine.name === 'clio' ? clioRenderFullLegend : renderFullLegend;
523
+ console.log('\n' + legend() + '\n');
442
524
  continue;
443
525
  }
444
526
 
@@ -494,6 +576,53 @@ async function replMode(): Promise<void> {
494
576
  }
495
577
  }
496
578
 
579
+ // ─── PARSE-AUDIT DIAGNOSTIC ──────────────────────────────────────
580
+ //
581
+ // `--debug-parse` dumps, per word, the full chain the scansion rests on: POS
582
+ // tag, dependency role + governor, prosodic membership (IU.PP.CP), and the
583
+ // lexical / phrase / relative stress. This is the audit instrument for the
584
+ // POS + dependency + correction layers — read alongside trials/parse_audit.mjs,
585
+ // which tabulates tag/dependency distributions and anomalies over a corpus.
586
+ function debugParse(text: string): void {
587
+ // Clio audits through its own frozen parser; Calliope through the live one.
588
+ const isClio = activeEngine.name === 'clio';
589
+ const parse = isClio ? clioParseDocument : parseDocument;
590
+ const isPunct = isClio ? clioIsPunctuation : isPunctuation;
591
+ const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
592
+ for (const line of lines) {
593
+ const doc = parse(line);
594
+ console.log('\n' + chalk.bold.cyan('› ' + line));
595
+ for (const sent of doc.sentences) {
596
+ // Route the audit dump through the active engine so --debug-parse reflects
597
+ // whichever engine (Calliope / Clio) is selected.
598
+ const ius = activeEngine.analyzeSentence(sent);
599
+ const loc = new Map<ClsWord, string>();
600
+ ius.forEach((iu, ii) =>
601
+ iu.phonologicalPhrases.forEach((pp, pi) =>
602
+ pp.cliticGroups.forEach((cg, ci) =>
603
+ cg.tokens.forEach(t => loc.set(t, `${ii + 1}.${pi + 1}.${ci + 1}`)))));
604
+ console.log(chalk.dim(' word POS C/F dep ←governor IU.PP.CP lex phr rel canon name'));
605
+ for (const w of sent.words) {
606
+ if (isPunctuation(w.lexicalClass)) continue;
607
+ const d = w.dependency;
608
+ const gov = d && d.governor ? d.governor.word : '—';
609
+ const cf = w.isContent ? 'C' : 'f';
610
+ const lex = w.syllables.map(s => (s.lexicalStress ?? s.stress)).join('');
611
+ const rel = w.syllables.map(s => s.relativeStress).join('');
612
+ // Calliope substrate (Stage 1): normalised relation + person/place flags.
613
+ const canon = w.canonicalRel ?? '-';
614
+ const name = w.isPersonName && w.isPlaceName ? 'P+C'
615
+ : w.isPersonName ? 'person' : w.isPlaceName ? 'place' : '';
616
+ console.log(' ' + w.word.padEnd(12) + w.lexicalClass.padEnd(6) + ' ' + cf + ' '
617
+ + (d ? d.dependentType : '?').padEnd(14) + ' ' + String(gov).padEnd(13) + ' '
618
+ + (loc.get(w) ?? '-').padEnd(9) + ' ' + lex.padEnd(5) + ' '
619
+ + String(w.phraseStress).padEnd(4) + ' ' + rel.padEnd(5) + ' '
620
+ + canon.padEnd(10) + ' ' + name);
621
+ }
622
+ }
623
+ }
624
+ }
625
+
497
626
  // ─── CLI ENTRY POINT ─────────────────────────────────────────────
498
627
 
499
628
  async function main(): Promise<void> {
@@ -502,7 +631,14 @@ async function main(): Promise<void> {
502
631
  // syllables stress-coloured, + per-line stress maps) instead of the full dump.
503
632
  const reading = rawArgs.includes('--reading') || rawArgs.includes('-r');
504
633
  rawArgs = rawArgs.filter(a => a !== '--reading' && a !== '-r');
505
- const show = reading ? showReadingView : showResults;
634
+ // --debug-parse : dump the per-word POS / dependency / prosody / stress chain.
635
+ const debugParseMode = rawArgs.includes('--debug-parse');
636
+ rawArgs = rawArgs.filter(a => a !== '--debug-parse');
637
+ // --clio : run the frozen legacy / alternative parse engine instead of the
638
+ // default faithful Calliope engine.
639
+ if (rawArgs.includes('--clio')) activeEngine = clioEngine;
640
+ rawArgs = rawArgs.filter(a => a !== '--clio');
641
+ const show = debugParseMode ? debugParse : reading ? showReadingView : showResults;
506
642
 
507
643
  // Explicit arguments take precedence over piped stdin — otherwise running
508
644
  // `calliope_ts "some line"` from a script/CI (where stdin is a non-TTY but
@@ -532,15 +668,23 @@ async function main(): Promise<void> {
532
668
  await replMode();
533
669
  }
534
670
 
671
+ // Is this module being run directly as the CLI, or imported as a library?
672
+ // A plain `process.argv[1] === fileURLToPath(import.meta.url)` check breaks under
673
+ // `npm install -g`: npm invokes us through a symlink in its bin directory, so
674
+ // process.argv[1] is that symlink's path, not the real dist/index.js. The two
675
+ // never match, `main()` never fires, and the command exits silently. Resolve
676
+ // symlinks on BOTH sides before comparing so the global command actually runs.
535
677
  let isMain = false;
536
678
  if (process.argv[1]) {
537
679
  try {
538
680
  isMain = fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
539
- } catch { isMain = false; }
681
+ } catch {
682
+ isMain = false;
683
+ }
540
684
  }
541
685
  if (isMain) {
542
686
  main().catch(err => {
543
687
  console.error(chalk.red('Fatal error:'), err);
544
688
  process.exit(1);
545
689
  });
546
- }
690
+ }