calliope-ts 0.0.2 → 0.0.4

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 (160) hide show
  1. package/README.md +32 -20
  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/calliope_src_contents.md +19049 -0
  130. package/src/clio/caesura.ts +145 -0
  131. package/src/clio/depfix.ts +88 -0
  132. package/src/clio/display.ts +1042 -0
  133. package/src/clio/engine.ts +38 -0
  134. package/src/clio/parser.ts +845 -0
  135. package/src/clio/phonological.ts +849 -0
  136. package/src/clio/phrasestress.ts +108 -0
  137. package/src/clio/pipeline.ts +154 -0
  138. package/src/clio/rhyme.ts +740 -0
  139. package/src/clio/scandroid.ts +434 -0
  140. package/src/clio/scansion.ts +1130 -0
  141. package/src/clio/semantics.ts +134 -0
  142. package/src/clio/stress.ts +1731 -0
  143. package/src/clio/tagfix.ts +104 -0
  144. package/src/display.ts +321 -137
  145. package/src/engine.ts +22 -0
  146. package/src/index.ts +176 -32
  147. package/src/parser.ts +346 -682
  148. package/src/phonological.ts +72 -4
  149. package/src/phrasestress.ts +108 -0
  150. package/src/rhyme.ts +428 -8
  151. package/src/scansion.ts +136 -15
  152. package/src/semantics.ts +134 -0
  153. package/src/stress.ts +794 -64
  154. package/src/types.ts +39 -4
  155. package/tests/DataForHayesLinesOnly.txt +364 -0
  156. package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
  157. package/tests/basic.test.ts +443 -37
  158. package/tests/bench-hayes.mjs +72 -0
  159. package/tests/wagner-stress.test.ts +188 -0
  160. package/vitest.config.ts +0 -15
@@ -0,0 +1,145 @@
1
+ // caesura.ts — Caesura placement, shared by the display and rhyme layers.
2
+ //
3
+ // A caesura is the line's medial pause. Two kinds are distinguished:
4
+ // • 'hard' — an overt break at an Intonational-Unit boundary (comma, dash,
5
+ // colon, semicolon …): the punctuation projection.
6
+ // • 'soft' — a single INFERRED medial caesura for a punctuation-free line
7
+ // (Kiparsky 1975, via McAleese: "phonological phrasing determines the
8
+ // location of caesurae in verse").
9
+ //
10
+ // Extracted from display.ts (2026-06-13) so the rhyme layer can find the words
11
+ // that immediately precede caesurae (for pre-caesural internal-rhyme detection)
12
+ // without importing the display module. Pure analysis — no colour/chalk here.
13
+
14
+ import { ClsWord, IntonationalUnit } from '../types.js';
15
+ import { isPunctuation } from './parser.js';
16
+
17
+ export type CaesuraKind = 'hard' | 'soft';
18
+
19
+ /** Foot-boundary syllable indices from a scansion string (cumulative syllable
20
+ * count after each foot; silent beats '-' are not syllables). */
21
+ function footBoundarySet(scansion: string): Set<number> {
22
+ const set = new Set<number>();
23
+ let c = 0;
24
+ for (const foot of scansion.split('|')) {
25
+ for (const ch of foot) if ('xwnms'.includes(ch)) c++;
26
+ set.add(c);
27
+ }
28
+ return set;
29
+ }
30
+
31
+ // A new phonological/syntactic phrase opens at these POS tags: prepositions &
32
+ // subordinators (IN), infinitival "to" (TO), coordinators (CC), wh/relativizers,
33
+ // verb-particles (RP), and the predicate's verb/modal. The major medial caesura
34
+ // of a line falls immediately BEFORE such a word — far more reliably read off the
35
+ // (robust) POS tags than off FinNLP's (noisy) phonological-phrase grouping, which
36
+ // mis-bracketed e.g. "The epic | feast…". Determiners/articles are excluded (they
37
+ // continue a phrase a preposition already opened: "as | one empty bag").
38
+ const PHRASE_ONSET_POS = new Set([
39
+ 'IN', 'TO', 'CC', 'WDT', 'WP', 'WP$', 'WRB', 'RP',
40
+ 'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'MD',
41
+ ]);
42
+
43
+ // Directional/spatial adverbs that open a phrase ("snowed-in OUT of many routes",
44
+ // "drifting DOWN to sleep"). FinNLP often tags these RB rather than RP/IN, so the
45
+ // POS set alone misses them; a small curated lemma list recovers them with low
46
+ // over-fire risk (a generic RB like "very"/"quickly" is NOT a phrase onset).
47
+ const DIRECTIONAL_ONSET = new Set([
48
+ 'out', 'in', 'up', 'down', 'off', 'away', 'back', 'forth', 'over',
49
+ 'around', 'along', 'through', 'apart', 'aside', 'onward', 'onwards',
50
+ ]);
51
+
52
+ /** True if a word opens a new phonological/syntactic phrase (a caesura candidate). */
53
+ function isPhraseOnset(w: ClsWord): boolean {
54
+ if (PHRASE_ONSET_POS.has(w.lexicalClass)) return true;
55
+ return w.lexicalClass === 'RB' && DIRECTIONAL_ONSET.has(w.word.toLowerCase());
56
+ }
57
+
58
+ /**
59
+ * Caesura positions for a line, keyed by the syllable index AFTER which the pause
60
+ * falls (= number of syllables to its left).
61
+ * • 'hard' — an overt break at an Intonational-Unit boundary.
62
+ * • 'soft' — a single INFERRED medial caesura for a punctuation-free line.
63
+ * Candidates are the boundaries just before a phrase/clause onset
64
+ * (PHRASE_ONSET_POS); the one nearest the line's midpoint that lies in the
65
+ * central third AND coincides with a foot boundary wins — so it is medial,
66
+ * never mid-foot, and consistent across structurally-parallel lines. Read in
67
+ * LINEAR order (robust to clitic-group reordering); needs a line of ≥ 8
68
+ * syllables.
69
+ */
70
+ export function computeCaesurae(words: ClsWord[], ius: IntonationalUnit[], scansion?: string): Map<number, CaesuraKind> {
71
+ const caes = new Map<number, CaesuraKind>();
72
+ // Caesurae must sit on FOOT BOUNDARIES (when the scansion is known). The line's
73
+ // metrical feet ARE the scansion's own segmentation, so a break that falls mid-
74
+ // foot has no metrical reality — it merely fragments a foot (often into a lone
75
+ // monosyllable: "But ‖ Oh ‖ ye…") and it made the reading view disagree with the
76
+ // detailed "Feet:" view, which already marks foot-edge breaks only. Punctuation
77
+ // is a caesura CANDIDATE, never an override: an IU boundary that does not land on
78
+ // a foot edge is dropped (the soft-caesura fallback below then offers one medial,
79
+ // foot-aligned break if the line is long enough).
80
+ const footEdges = scansion ? footBoundarySet(scansion) : null;
81
+ const iuOf = new Map<ClsWord, number>();
82
+ for (let i = 0; i < ius.length; i++) {
83
+ for (const pp of ius[i].phonologicalPhrases) {
84
+ for (const cg of pp.cliticGroups) for (const tok of cg.tokens) iuOf.set(tok, i);
85
+ }
86
+ }
87
+ let cum = 0;
88
+ let prevIu: number | undefined;
89
+ let prevWasContentful = false;
90
+ const onsetPositions: number[] = [];
91
+ for (const w of words) {
92
+ if (isPunctuation(w.lexicalClass) || w.syllables.length === 0) continue;
93
+ const iu = iuOf.get(w);
94
+ if (prevIu !== undefined && iu !== undefined && iu !== prevIu
95
+ && (!footEdges || footEdges.has(cum))) {
96
+ caes.set(cum, 'hard'); // IU boundary landing on a foot edge → hard caesura
97
+ }
98
+ // A phrase-onset word that is NOT the line's first word opens a candidate
99
+ // caesura immediately before it.
100
+ if (prevWasContentful && isPhraseOnset(w)) onsetPositions.push(cum);
101
+ cum += w.syllables.length;
102
+ prevIu = iu;
103
+ prevWasContentful = true;
104
+ }
105
+ const total = cum;
106
+
107
+ // Infer ONE medial caesura only when the line carries no overt (hard) break.
108
+ if (caes.size === 0 && total >= 8 && onsetPositions.length > 0) {
109
+ const mid = total / 2;
110
+ const lo = Math.max(2, Math.ceil(total / 3));
111
+ const hi = Math.floor((2 * total) / 3);
112
+ let best = -1, bestDist = Infinity;
113
+ for (const c of onsetPositions) {
114
+ if (c < lo || c > hi) continue; // medial third only
115
+ if (footEdges && !footEdges.has(c)) continue; // align to a foot boundary
116
+ const d = Math.abs(c - mid);
117
+ if (d < bestDist) { bestDist = d; best = c; }
118
+ }
119
+ if (best > 0) caes.set(best, 'soft');
120
+ }
121
+ return caes;
122
+ }
123
+
124
+ /**
125
+ * The words that immediately PRECEDE a caesura in a line — i.e. each word whose
126
+ * cumulative syllable count lands exactly on a caesura position. Used by the
127
+ * rhyme layer for pre-caesural internal-rhyme detection. Returned in linear
128
+ * (reading) order; the caesura kind is paired so callers can weight hard vs
129
+ * inferred breaks if they wish.
130
+ */
131
+ export function preCaesuralWords(
132
+ words: ClsWord[], ius: IntonationalUnit[], scansion?: string,
133
+ ): { word: ClsWord; kind: CaesuraKind }[] {
134
+ const caes = computeCaesurae(words, ius, scansion);
135
+ if (caes.size === 0) return [];
136
+ const out: { word: ClsWord; kind: CaesuraKind }[] = [];
137
+ let cum = 0;
138
+ for (const w of words) {
139
+ if (isPunctuation(w.lexicalClass) || w.syllables.length === 0) continue;
140
+ cum += w.syllables.length;
141
+ const kind = caes.get(cum);
142
+ if (kind) out.push({ word: w, kind });
143
+ }
144
+ return out;
145
+ }
@@ -0,0 +1,88 @@
1
+ // depfix.ts — Post-parse dependency repair via DepEdit rules (the `depedits`
2
+ // npm package, the maintainer's TypeScript port of DepEdit).
3
+ //
4
+ // Runs AFTER en-parse, complementing the pre-parse tag corrections in
5
+ // tagfix.ts: tagfix repairs what the tagger got wrong before the tree is
6
+ // built; this pass repairs systematic attachment errors en-parse makes even
7
+ // with correct tags. Rules are written in DepEdit's declarative format
8
+ // (definitions ⟶ relations ⟶ actions, tab-separated) over en-parse's own
9
+ // label space (DOBJ/NSUBJ/DEP/…), so the round-trip is lossless and every
10
+ // rule is independently testable.
11
+ //
12
+ // The rule set is deliberately small and evidence-based — each rule cites the
13
+ // observed failure it corrects. `depedits` is ESM-only; it is loaded lazily
14
+ // and failures degrade gracefully (the unrepaired parse is still a parse).
15
+
16
+ import { createRequire } from 'module';
17
+
18
+ interface FinDepNode {
19
+ label: string; // dependency label, e.g. "NSUBJ", "ROOT"
20
+ type: string; // phrase type, e.g. "NP", "VP"
21
+ parent: number; // 0-based index of governor token; -1 for root
22
+ }
23
+
24
+ // Observed failure (probe: "I had quit the programming paradigm"): en-parse
25
+ // attaches BOTH nouns of a noun compound to the verb as parallel objects
26
+ // ("programming ←DOBJ← quit", "paradigm ←DOBJ← quit"), and leaves the
27
+ // determiner dangling on the first noun as generic DEP. The repairs:
28
+ // 1. Two adjacent common nouns sharing a governor with the same object
29
+ // relation → the first is a compound modifier (AMOD) of the second.
30
+ // 2. A determiner left as DEP on a noun that has become a modifier →
31
+ // re-attach it as DET to that noun's head (the true NP head).
32
+ const CALLIOPE_DEP_FIXES = [
33
+ 'xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/VB.*/\t#3>#1;#3>#2;#1.#2\t#2>#1;#1:func=AMOD',
34
+ 'xpos=/DT/&func=/DEP|EXT/;xpos=/NNS?/&func=/AMOD/;xpos=/NNS?.*/\t#2>#1;#3>#2\t#3>#1;#1:func=DET',
35
+ ].join('\n');
36
+
37
+ let engine: { process(conllu: string): string } | null | undefined;
38
+
39
+ function loadEngine(): typeof engine {
40
+ if (engine !== undefined) return engine;
41
+ try {
42
+ // This package compiles to ESM, where bare `require` does not exist, and
43
+ // the parse path is synchronous, so dynamic import() is not an option:
44
+ // createRequire gives a sync loader, and since `depedits` is itself
45
+ // ESM-only this resolves via Node's require(esm) (≥20.17 / ≥22.12). On
46
+ // older runtimes it throws and the repair pass degrades to a no-op (the
47
+ // unrepaired parse is still a parse).
48
+ const req = createRequire(import.meta.url);
49
+ const { DepEditEngine } = req('depedits');
50
+ const e = new DepEditEngine();
51
+ e.loadIniString(CALLIOPE_DEP_FIXES);
52
+ engine = e;
53
+ } catch {
54
+ engine = null;
55
+ }
56
+ return engine;
57
+ }
58
+
59
+ /**
60
+ * Repair systematic en-parse attachment errors. Returns a new deps array
61
+ * (same shape as en-parse's `toArray` output); on any failure returns the
62
+ * input unchanged.
63
+ */
64
+ export function applyDepFixes(tokens: string[], tags: string[], deps: FinDepNode[]): FinDepNode[] {
65
+ const e = loadEngine();
66
+ if (!e || tokens.length === 0 || deps.length !== tokens.length) return deps;
67
+ try {
68
+ const conllu = tokens.map((tok, i) => {
69
+ const head = deps[i].parent >= 0 ? deps[i].parent + 1 : 0;
70
+ const safe = tok.replace(/\s/g, '_') || '_';
71
+ return `${i + 1}\t${safe}\t${safe}\t_\t${tags[i] || '_'}\t_\t${head}\t${deps[i].label || 'DEP'}\t_\t_`;
72
+ }).join('\n') + '\n\n';
73
+ const out = e.process(conllu);
74
+ const fixed: FinDepNode[] = deps.map(d => ({ ...d }));
75
+ for (const row of out.split('\n')) {
76
+ const cols = row.split('\t');
77
+ if (cols.length < 10) continue;
78
+ const idx = parseInt(cols[0], 10) - 1;
79
+ if (!(idx >= 0 && idx < fixed.length)) continue;
80
+ const head = parseInt(cols[6], 10);
81
+ fixed[idx].parent = Number.isFinite(head) ? head - 1 : fixed[idx].parent;
82
+ if (cols[7] && cols[7] !== '_') fixed[idx].label = cols[7];
83
+ }
84
+ return fixed;
85
+ } catch {
86
+ return deps;
87
+ }
88
+ }