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/dist/engine.js ADDED
@@ -0,0 +1,12 @@
1
+ // engine.ts — the prosody-engine abstraction (type-only; no runtime deps, so
2
+ // the concrete engines can import this without a cycle).
3
+ //
4
+ // Two engines produce the SAME shape — a per-sentence prosodic hierarchy with
5
+ // lexical/phrase/relative stress populated on the words:
6
+ // • "calliope" — the faithful, default, syntax-driven rebuild (Match-Theory
7
+ // hierarchy + Scenario A–O relation-keyed stress);
8
+ // • "clio" — a frozen snapshot of the prior pipeline, the legacy /
9
+ // alternative parse, selectable via the CLI.
10
+ // Everything downstream (metrical scoring, rhyme/form, display, synopsis) is
11
+ // shared and engine-agnostic.
12
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import { ProsodyEngine } from './engine.js';
2
3
  import { type ReadingStanza } from './display.js';
3
4
  import type { LineResult } from './types.js';
4
- export declare function analyzeStanzas(text: string, useScandroid?: boolean): LineResult[][];
5
+ export declare function analyzeStanzas(text: string, useScandroid?: boolean, engine?: ProsodyEngine): LineResult[][];
5
6
  /**
6
7
  * Convenience wrapper: analyse a multi‑line text, ignore stanza breaks,
7
8
  * return a flat list of LineResult for each line.
8
9
  */
9
- export declare function analyzeText(text: string, useScandroid?: boolean): LineResult[];
10
+ export declare function analyzeText(text: string, useScandroid?: boolean, engine?: ProsodyEngine): LineResult[];
10
11
  /**
11
12
  * Analyse a document for the reading view: like analyzeStanzas, but retains
12
13
  * each original input line alongside its (1+) parsed sentence results so the
13
14
  * stress gradient can be projected back over the verbatim text.
14
15
  */
15
- export declare function analyzeReadingDocument(text: string): ReadingStanza[];
16
+ export declare function analyzeReadingDocument(text: string, engine?: ProsodyEngine): ReadingStanza[];
16
17
  export declare const ML_IDLE_MS = 120;
17
18
  export type MLEvent = {
18
19
  kind: 'char';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAyBA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAMV,UAAU,EAMX,MAAM,YAAY,CAAC;AAmIpB,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,GAAG,UAAU,EAAE,EAAE,CAoChF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,GAAG,UAAU,EAAE,CAG3E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBpE;AAoCD,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpB,MAAM,WAAW,OAAO;IAAG,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;CAAE;AAC7E,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,wBAAgB,UAAU,IAAI,OAAO,CAAoD;AAMzF;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,QAAQ,CAoCrE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0B5C,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAOV,UAAU,EAMX,MAAM,YAAY,CAAC;AAkLpB,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,EAAE,MAAM,GAAE,aAA4B,GAAG,UAAU,EAAE,EAAE,CA2CtH;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,EAAE,MAAM,GAAE,aAA4B,GAAG,UAAU,EAAE,CAGjH;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,aAA4B,GAAG,aAAa,EAAE,CA0B1G;AAwCD,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpB,MAAM,WAAW,OAAO;IAAG,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;CAAE;AAC7E,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,wBAAgB,UAAU,IAAI,OAAO,CAAoD;AAMzF;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,QAAQ,CAoCrE"}
package/dist/index.js CHANGED
@@ -3,13 +3,73 @@ 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';
7
- import { assignLexicalStress, applyCompoundStress, applyNuclearStress, assignRelativeStresses, } from './stress.js';
8
- import { buildPhonologicalHierarchy, renderHierarchy, renderKeyStresses, } from './phonological.js';
9
- import { extractKeyStresses, scoreMeters, applyStanzaConsensus, applyRhythmLayer } from './scansion.js';
6
+ import { parseDocument, isPunctuation } from './parser.js';
7
+ import { calliopeEngine } from './calliope/engine.js';
8
+ import { clioEngine } from './clio/engine.js';
9
+ // The FROZEN Clio pipeline + renderers (its own copy of the whole computational
10
+ // tree). When Clio is active the CLI runs entirely through these, so live changes
11
+ // to the regular (Calliope) pipeline — scorer included — cannot affect Clio.
12
+ import { analyzeReadingDocumentClio, renderUnifiedDisplay as clioRenderUnifiedDisplay, renderReadingView as clioRenderReadingView, renderFullLegend as clioRenderFullLegend, parseDocument as clioParseDocument, isPunctuation as clioIsPunctuation, } from './clio/pipeline.js';
13
+ import { renderHierarchy, renderKeyStresses, } from './phonological.js';
14
+ import { extractKeyStresses, scoreMeters, applyStanzaConsensus, applyRhythmLayer, applyMetricalityLayer } from './scansion.js';
10
15
  import { applyRhymeAndForm } from './rhyme.js';
11
16
  import { scandroidCorralWeird, scandroidMaximizeNormal, stressToMarks, } from './scandroid.js';
12
17
  import { renderUnifiedDisplay, renderFullLegend, renderReadingView, } from './display.js';
18
+ // The active prosody engine for this process: Calliope (faithful, default) or
19
+ // Clio (the frozen legacy / alternative parse), chosen by `--clio` or the REPL
20
+ // menu. The exported analysis functions default their `engine` parameter to
21
+ // this, so library callers (tests, trials, benchmark) transparently use the
22
+ // active engine without signature churn.
23
+ let activeEngine = calliopeEngine;
24
+ /**
25
+ * Stanza-level discourse givenness (Wagner Ch.7, the plan's Gap 13) — CAREFUL and
26
+ * cross-line only. A CONTENT word repeated from an EARLIER line of the same stanza
27
+ * is discourse-given, so it may be subordinated relative to a new-information sister
28
+ * (the relativiser reads `discourseGiven`). Constraints that protect single-line
29
+ * scanning and thematically-focused words:
30
+ * • the first line is never marked (no prior context);
31
+ * • a lemma repeated WITHIN a single line is FOCUSED, not given → never marked
32
+ * (Eliot's "Nothing … nothing", a refrain);
33
+ * • only the 2nd-and-later occurrence (across lines) is marked, never the first;
34
+ * • single-line input has no previous line, so nothing is marked — the standout
35
+ * isolated-line feature is untouched.
36
+ * Mutates `discourseGiven` on the words; must run BEFORE the per-line relativisation.
37
+ */
38
+ function markStanzaGivenness(docPerLine) {
39
+ if (docPerLine.length < 2)
40
+ return; // single line → no givenness
41
+ const key = (w) => w.word.toLowerCase().replace(/['’]/g, '');
42
+ // Lemmas that appear ≥2× within ANY one line are focal (refrain/emphasis) — exempt.
43
+ const focal = new Set();
44
+ for (const sents of docPerLine) {
45
+ const counts = new Map();
46
+ for (const sent of sents)
47
+ for (const w of sent.words) {
48
+ if (!w.isContent)
49
+ continue;
50
+ const k = key(w);
51
+ counts.set(k, (counts.get(k) ?? 0) + 1);
52
+ }
53
+ for (const [k, n] of counts)
54
+ if (n >= 2)
55
+ focal.add(k);
56
+ }
57
+ const seen = new Set(); // content lemmas in PRIOR lines
58
+ for (const sents of docPerLine) {
59
+ const thisLine = new Set();
60
+ for (const sent of sents)
61
+ for (const w of sent.words) {
62
+ if (!w.isContent)
63
+ continue;
64
+ const k = key(w);
65
+ if (seen.has(k) && !focal.has(k))
66
+ w.discourseGiven = true;
67
+ thisLine.add(k);
68
+ }
69
+ for (const k of thisLine)
70
+ seen.add(k);
71
+ }
72
+ }
13
73
  /**
14
74
  * Scan one VERSE LINE (which may parse into several grammatical sentences).
15
75
  *
@@ -23,16 +83,15 @@ import { renderUnifiedDisplay, renderFullLegend, renderReadingView, } from './di
23
83
  * line's full concatenated syllable stream, with each sentence's IUs preserved
24
84
  * as IU boundaries (→ hard caesurae) inside the line.
25
85
  */
26
- function processLine(sents) {
86
+ function processLine(sents, engine = activeEngine) {
27
87
  if (sents.length === 0)
28
88
  return null;
29
89
  const iusPerSent = [];
30
90
  for (const sent of sents) {
31
- assignLexicalStress(sent.words);
32
- const sentIus = buildPhonologicalHierarchy(sent);
33
- applyCompoundStress(sentIus);
34
- applyNuclearStress(sentIus);
35
- assignRelativeStresses(sent.words, sentIus);
91
+ // The selected engine runs the per-sentence linguistic passes (lexical →
92
+ // hierarchy compound/nuclear → phrase → relative stress) and returns the
93
+ // prosodic hierarchy. Calliope (default) and Clio differ only here.
94
+ const sentIus = engine.analyzeSentence(sent);
36
95
  iusPerSent.push(sentIus);
37
96
  }
38
97
  // Merge the sentences' streams into the line-level scansion domain.
@@ -132,15 +191,19 @@ function applyContinuityRename(results) {
132
191
  d.consensusMeter = undefined;
133
192
  }
134
193
  }
135
- export function analyzeStanzas(text, useScandroid = true) {
194
+ export function analyzeStanzas(text, useScandroid = true, engine = activeEngine) {
136
195
  const stanzas = text.split(/\n\s*\n/);
137
196
  const results = [];
138
197
  for (const stanza of stanzas) {
139
198
  const lines = stanza.split('\n').filter(l => l.trim() !== '');
140
199
  const stanzaResults = [];
141
- for (const line of lines) {
142
- const doc = parseDocument(line);
143
- const res = processLine(doc.sentences);
200
+ // Parse every line first, then mark cross-line discourse givenness BEFORE the
201
+ // per-line stress passes (relativisation reads `discourseGiven`). Only the
202
+ // default (Calliope) relativiser consults it; Clio ignores the flag.
203
+ const docs = lines.map(line => parseDocument(line));
204
+ markStanzaGivenness(docs.map(d => d.sentences));
205
+ for (const doc of docs) {
206
+ const res = processLine(doc.sentences, engine);
144
207
  if (res)
145
208
  stanzaResults.push(res);
146
209
  }
@@ -166,6 +229,9 @@ export function analyzeStanzas(text, useScandroid = true) {
166
229
  for (const st of results)
167
230
  applyRhythmLayer(st.map(r => r.phonologicalScansion));
168
231
  }
232
+ // Prose-likeness hedge (Option 0): advisory, runs after the rhythm layer so it
233
+ // can defer to any accentual/dolnik verdict; annotation-only (metricalityNote).
234
+ applyMetricalityLayer(results.flatMap(st => st.map(r => r.phonologicalScansion)));
169
235
  // Rhyme scheme + poetic-form identification spans stanzas (sonnets, terza
170
236
  // rima); annotation-only (rhyme/formNote on each line's detail).
171
237
  applyRhymeAndForm(results);
@@ -175,8 +241,8 @@ export function analyzeStanzas(text, useScandroid = true) {
175
241
  * Convenience wrapper: analyse a multi‑line text, ignore stanza breaks,
176
242
  * return a flat list of LineResult for each line.
177
243
  */
178
- export function analyzeText(text, useScandroid = true) {
179
- const stanzaResults = analyzeStanzas(text, useScandroid);
244
+ export function analyzeText(text, useScandroid = true, engine = activeEngine) {
245
+ const stanzaResults = analyzeStanzas(text, useScandroid, engine);
180
246
  return stanzaResults.flat();
181
247
  }
182
248
  /**
@@ -184,16 +250,17 @@ export function analyzeText(text, useScandroid = true) {
184
250
  * each original input line alongside its (1+) parsed sentence results so the
185
251
  * stress gradient can be projected back over the verbatim text.
186
252
  */
187
- export function analyzeReadingDocument(text) {
253
+ export function analyzeReadingDocument(text, engine = activeEngine) {
188
254
  const stanzas = text.split(/\n\s*\n/);
189
255
  const out = [];
190
256
  for (const stanza of stanzas) {
191
257
  const rawLines = stanza.split('\n').filter(l => l.trim() !== '');
192
258
  if (rawLines.length === 0)
193
259
  continue;
194
- const lines = rawLines.map(raw => {
195
- const doc = parseDocument(raw);
196
- const res = processLine(doc.sentences);
260
+ const rawDocs = rawLines.map(raw => ({ raw, doc: parseDocument(raw) }));
261
+ markStanzaGivenness(rawDocs.map(rd => rd.doc.sentences));
262
+ const lines = rawDocs.map(({ raw, doc }) => {
263
+ const res = processLine(doc.sentences, engine);
197
264
  return { raw, results: res ? [res] : [] };
198
265
  });
199
266
  applyStanzaConsensus(lines.flatMap(l => l.results.map(r => r.phonologicalScansion)));
@@ -208,6 +275,7 @@ export function analyzeReadingDocument(text) {
208
275
  for (const st of out)
209
276
  applyRhythmLayer(st.lines.flatMap(l => l.results.map(r => r.phonologicalScansion)));
210
277
  }
278
+ applyMetricalityLayer(out.flatMap(st => st.lines.flatMap(l => l.results.map(r => r.phonologicalScansion))));
211
279
  applyRhymeAndForm(out.map(st => st.lines.flatMap(l => l.results)));
212
280
  return out;
213
281
  }
@@ -215,21 +283,25 @@ export function analyzeReadingDocument(text) {
215
283
  function showResults(text) {
216
284
  // Use the reading-document analysis so each LineResult keeps its verbatim input
217
285
  // line (for the tail Reading Projection) and the stanza-consensus annotation.
218
- const stanzas = analyzeReadingDocument(text);
286
+ // Clio routes through its own frozen pipeline + renderer.
287
+ const isClio = activeEngine.name === 'clio';
288
+ const stanzas = (isClio ? analyzeReadingDocumentClio : analyzeReadingDocument)(text);
289
+ const render = isClio ? clioRenderUnifiedDisplay : renderUnifiedDisplay;
219
290
  for (let s = 0; s < stanzas.length; s++) {
220
291
  if (stanzas.length > 1) {
221
292
  console.log('\n' + chalk.bold('═══ Stanza ' + (s + 1) + ' ═══'));
222
293
  }
223
294
  for (const ln of stanzas[s].lines) {
224
295
  for (const res of ln.results) {
225
- console.log(renderUnifiedDisplay(res, ln.raw));
296
+ console.log(render(res, ln.raw));
226
297
  }
227
298
  }
228
299
  }
229
300
  }
230
301
  function showReadingView(text) {
231
- const stanzas = analyzeReadingDocument(text);
232
- console.log(renderReadingView(stanzas));
302
+ const isClio = activeEngine.name === 'clio';
303
+ const stanzas = (isClio ? analyzeReadingDocumentClio : analyzeReadingDocument)(text);
304
+ console.log((isClio ? clioRenderReadingView : renderReadingView)(stanzas));
233
305
  }
234
306
  // ─── MULTI-LINE INPUT (paste-friendly) ──────────────────────────
235
307
  // Goal: the user pastes a whole poem (stanza breaks and all) and presses Enter
@@ -396,6 +468,7 @@ async function replMode() {
396
468
  { title: 'Line-by-Line Analysis (detailed view)', value: 'multiline' },
397
469
  { title: 'Parse & Scan from File (reading view)', value: 'reading-file' },
398
470
  { title: 'Analyze from File (detailed view)', value: 'file' },
471
+ { title: 'Ask Clio instead (alternative parse) — toggle engine', value: 'engine' },
399
472
  { title: 'Display Legend', value: 'legend' },
400
473
  { title: 'Exit', value: 'exit' },
401
474
  ],
@@ -405,8 +478,20 @@ async function replMode() {
405
478
  console.log(chalk.dim('\nGoodbye.\n'));
406
479
  break;
407
480
  }
481
+ if (response.action === 'engine') {
482
+ // Toggle between Calliope (faithful, default) and Clio (the legacy /
483
+ // alternative parse). Clio is Calliope's historian sister — sometimes
484
+ // on point, but not the primary poetic voice.
485
+ activeEngine = activeEngine.name === 'calliope' ? clioEngine : calliopeEngine;
486
+ const label = activeEngine.name === 'clio'
487
+ ? 'Clio — legacy / alternative parse'
488
+ : 'Calliope — faithful, default';
489
+ console.log(chalk.dim(`\n Active engine: ${chalk.bold(label)}\n`));
490
+ continue;
491
+ }
408
492
  if (response.action === 'legend') {
409
- console.log('\n' + renderFullLegend() + '\n');
493
+ const legend = activeEngine.name === 'clio' ? clioRenderFullLegend : renderFullLegend;
494
+ console.log('\n' + legend() + '\n');
410
495
  continue;
411
496
  }
412
497
  if (response.action === 'scan') {
@@ -462,6 +547,50 @@ async function replMode() {
462
547
  }
463
548
  }
464
549
  }
550
+ // ─── PARSE-AUDIT DIAGNOSTIC ──────────────────────────────────────
551
+ //
552
+ // `--debug-parse` dumps, per word, the full chain the scansion rests on: POS
553
+ // tag, dependency role + governor, prosodic membership (IU.PP.CP), and the
554
+ // lexical / phrase / relative stress. This is the audit instrument for the
555
+ // POS + dependency + correction layers — read alongside trials/parse_audit.mjs,
556
+ // which tabulates tag/dependency distributions and anomalies over a corpus.
557
+ function debugParse(text) {
558
+ // Clio audits through its own frozen parser; Calliope through the live one.
559
+ const isClio = activeEngine.name === 'clio';
560
+ const parse = isClio ? clioParseDocument : parseDocument;
561
+ const isPunct = isClio ? clioIsPunctuation : isPunctuation;
562
+ const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
563
+ for (const line of lines) {
564
+ const doc = parse(line);
565
+ console.log('\n' + chalk.bold.cyan('› ' + line));
566
+ for (const sent of doc.sentences) {
567
+ // Route the audit dump through the active engine so --debug-parse reflects
568
+ // whichever engine (Calliope / Clio) is selected.
569
+ const ius = activeEngine.analyzeSentence(sent);
570
+ const loc = new Map();
571
+ ius.forEach((iu, ii) => iu.phonologicalPhrases.forEach((pp, pi) => pp.cliticGroups.forEach((cg, ci) => cg.tokens.forEach(t => loc.set(t, `${ii + 1}.${pi + 1}.${ci + 1}`)))));
572
+ console.log(chalk.dim(' word POS C/F dep ←governor IU.PP.CP lex phr rel canon name'));
573
+ for (const w of sent.words) {
574
+ if (isPunctuation(w.lexicalClass))
575
+ continue;
576
+ const d = w.dependency;
577
+ const gov = d && d.governor ? d.governor.word : '—';
578
+ const cf = w.isContent ? 'C' : 'f';
579
+ const lex = w.syllables.map(s => (s.lexicalStress ?? s.stress)).join('');
580
+ const rel = w.syllables.map(s => s.relativeStress).join('');
581
+ // Calliope substrate (Stage 1): normalised relation + person/place flags.
582
+ const canon = w.canonicalRel ?? '-';
583
+ const name = w.isPersonName && w.isPlaceName ? 'P+C'
584
+ : w.isPersonName ? 'person' : w.isPlaceName ? 'place' : '';
585
+ console.log(' ' + w.word.padEnd(12) + w.lexicalClass.padEnd(6) + ' ' + cf + ' '
586
+ + (d ? d.dependentType : '?').padEnd(14) + ' ' + String(gov).padEnd(13) + ' '
587
+ + (loc.get(w) ?? '-').padEnd(9) + ' ' + lex.padEnd(5) + ' '
588
+ + String(w.phraseStress).padEnd(4) + ' ' + rel.padEnd(5) + ' '
589
+ + canon.padEnd(10) + ' ' + name);
590
+ }
591
+ }
592
+ }
593
+ }
465
594
  // ─── CLI ENTRY POINT ─────────────────────────────────────────────
466
595
  async function main() {
467
596
  let rawArgs = process.argv.slice(2);
@@ -469,7 +598,15 @@ async function main() {
469
598
  // syllables stress-coloured, + per-line stress maps) instead of the full dump.
470
599
  const reading = rawArgs.includes('--reading') || rawArgs.includes('-r');
471
600
  rawArgs = rawArgs.filter(a => a !== '--reading' && a !== '-r');
472
- const show = reading ? showReadingView : showResults;
601
+ // --debug-parse : dump the per-word POS / dependency / prosody / stress chain.
602
+ const debugParseMode = rawArgs.includes('--debug-parse');
603
+ rawArgs = rawArgs.filter(a => a !== '--debug-parse');
604
+ // --clio : run the frozen legacy / alternative parse engine instead of the
605
+ // default faithful Calliope engine.
606
+ if (rawArgs.includes('--clio'))
607
+ activeEngine = clioEngine;
608
+ rawArgs = rawArgs.filter(a => a !== '--clio');
609
+ const show = debugParseMode ? debugParse : reading ? showReadingView : showResults;
473
610
  // Explicit arguments take precedence over piped stdin — otherwise running
474
611
  // `calliope_ts "some line"` from a script/CI (where stdin is a non-TTY but
475
612
  // empty) silently analysed the empty pipe and ignored the argument.
@@ -495,6 +632,12 @@ async function main() {
495
632
  // No arguments: launch interactive REPL
496
633
  await replMode();
497
634
  }
635
+ // Is this module being run directly as the CLI, or imported as a library?
636
+ // A plain `process.argv[1] === fileURLToPath(import.meta.url)` check breaks under
637
+ // `npm install -g`: npm invokes us through a symlink in its bin directory, so
638
+ // process.argv[1] is that symlink's path, not the real dist/index.js. The two
639
+ // never match, `main()` never fires, and the command exits silently. Resolve
640
+ // symlinks on BOTH sides before comparing so the global command actually runs.
498
641
  let isMain = false;
499
642
  if (process.argv[1]) {
500
643
  try {
package/dist/parser.d.ts CHANGED
@@ -2,9 +2,9 @@ import { ClsDocument } from './types.js';
2
2
  export declare function isPunctuation(tag: string): boolean;
3
3
  export declare function isQuoteTag(tag: string): boolean;
4
4
  /**
5
- * Parse a multi‑sentence text string and return a ClsDocument whose
6
- * internal structure mirrors the Antelope NLP (aka Universal Dependency type)
7
- * output from McAleese’s original Calliope implementation.
5
+ * Lowercase the first alphabetic character of every line. Kept available but
6
+ * NOT called by default empirically net-negative with UDPipe (see parseDocument).
8
7
  */
8
+ export declare function lowerLineInitials(text: string): string;
9
9
  export declare function parseDocument(text: string): ClsDocument;
10
10
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,WAAW,EAKZ,MAAM,YAAY,CAAC;AA6TpB,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AAUD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAgED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAiPvD"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,WAAW,EAKZ,MAAM,YAAY,CAAC;AAkCpB,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AASD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD;AAmJD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAsJvD"}