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
@@ -0,0 +1,416 @@
1
+ // calliope/bracketing.ts — cyclic Compound + Nuclear Stress Rules, extended with
2
+ // Wagner (2005) Ch. 6 functor/argument geometry (2026-06-29 Wagner/Krifka rebuild).
3
+ //
4
+ // This is the REAL phrase-stress stage McAleese (after Chomsky & Halle's SPE and
5
+ // Hayes 1984b) prescribes — NOT a left-to-right ramp. It PROJECTS a constituent
6
+ // bracketing from the (UD-normalised) dependency tree, then runs the cyclic stress
7
+ // rules over it, innermost cycle out. The classic SPE convention is:
8
+ //
9
+ // • a COMPOUND combination (N+N / NOMD) → Compound Stress Rule: primary on the LEFT
10
+ // element (SLATE roof, ICE cream);
11
+ // • every other (phrasal) combination → Nuclear Stress Rule: primary on the RIGHT
12
+ // element (the rightmost constituent of a phrase is its nuclear).
13
+ //
14
+ // Wagner refines WHICH sister wins at each cycle (the plan's Gaps 2–8):
15
+ // • Complement Prominence (§6.2.2): the ARGUMENT outranks the FUNCTOR;
16
+ // • the Prosodic Asymmetry (§6.1.3): a functor that FOLLOWS its argument is
17
+ // obligatorily subordinated; one that PRECEDES may be on a par;
18
+ // • the specifier restriction (§6.3.2): a BRANCHING functor (a transitive VP) does
19
+ // not subordinate its subject — the two MATCH (co-nuclei);
20
+ // • the modifier asymmetry (§6.5.1): a modifier that PRECEDES its modifiee gets its
21
+ // OWN accentual domain (both project) — unlike an argument, which integrates;
22
+ // • associative domains (§2.2.2): same-coordinator conjuncts MATCH (flat), not NSR.
23
+ //
24
+ // To express MATCH (two co-equal sisters, neither subordinated) the relational grid is
25
+ // generalised: each constituent carries a SET of designated terminal elements (dtes),
26
+ // and
27
+ //
28
+ // level(w) = 1 + #{ enclosing constituents c : w ∈ c.words ∧ w ∉ c.dtes }
29
+ //
30
+ // A normal NSR/CSR cycle has a singleton dte (the winner's), so the formula still
31
+ // reproduces the canonical derivation
32
+ // [Mary [ate [sweet [ice cream]]]] → Mary 2, ate 3, sweet 4, ice 1, cream 5
33
+ // (Krifka 2001 Table 17); a MATCH cycle co-designates BOTH sisters' dtes, so neither is
34
+ // demoted at that cycle (a branching subject + VP give two co-nuclei).
35
+ //
36
+ // Output is per-word `phraseStress` (1 = strongest, punctuation = 0): the structural
37
+ // prominence the relativiser then reads PER PHONOLOGICAL PHRASE to place beats.
38
+ import { isPunct, isVerb, verbHasArgChild, isUnaccusativeOrPassive, isLowLocative, isFrameSetting, isLightNominalHead, } from './syntax.js';
39
+ const NOUN = /^(NN|NNS|NNP|NNPS)$/;
40
+ /** A head-dependent combination is a COMPOUND (Compound Stress Rule → primary LEFT)
41
+ * when the parse labels it NOMD, or when a common-/proper-noun modifier sits
42
+ * immediately to the left of a noun head — the N+N compound the tagger routinely
43
+ * mislabels AMOD ("ICE cream", "SLATE roof", "BIRD nest"). */
44
+ function isCompound(dep, head) {
45
+ if ((dep.canonicalRel ?? '') === 'NOMD')
46
+ return true;
47
+ if ((dep.canonicalRel ?? '') === 'EXT')
48
+ return false;
49
+ return NOUN.test(dep.lexicalClass) && NOUN.test(head.lexicalClass)
50
+ && Math.abs(dep.absoluteIndex - head.absoluteIndex) === 1;
51
+ }
52
+ function bare(w) { return w.word.toLowerCase().replace(/['’]/g, ''); }
53
+ /** The coordinator lemma of a conjunct (its `cc` dependent), or null. */
54
+ function coordinatorLemma(conjunct, words) {
55
+ for (const w of words) {
56
+ if (w.dependency?.governor === conjunct &&
57
+ (w.canonicalRel === 'CC' || w.lexicalClass === 'CC')) {
58
+ return bare(w);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ /** Is the coordination headed by `head` ASSOCIATIVE (all conjuncts share one
64
+ * coordinator: "old and gray and full") rather than ARTICULATED (mixed and/or)?
65
+ * Associative → MATCH (flat, Wagner §2.2.2 / Newman); articulated → NSR. */
66
+ function conjIsAssociative(head, children, words) {
67
+ const conjuncts = (children.get(head.absoluteIndex) ?? [])
68
+ .filter(c => (c.canonicalRel ?? '') === 'CONJ');
69
+ if (conjuncts.length < 1)
70
+ return true;
71
+ const coords = conjuncts.map(c => coordinatorLemma(c, words)).filter(Boolean);
72
+ if (coords.length === 0)
73
+ return true; // no overt coordinator → flat
74
+ return coords.every(c => c === coords[0]);
75
+ }
76
+ /** A child relation that makes a verb BRANCH — carry an internal argument.
77
+ * Used for the specifier-restriction trigger and for distinguishing a bare
78
+ * participle ("little boy lost" — no args, adjective-like) from a participial
79
+ * PHRASE ("gone in the teeth" — has an OBL, a real clause). */
80
+ const ARG_CHILD_LOCAL = new Set(['DOBJ', 'IOBJ', 'OBJ', 'OBL', 'CCOMP', 'XCOMP', 'ADVCL']);
81
+ function hasArgChildren(w, children) {
82
+ return (children.get(w.absoluteIndex) ?? []).some(c => ARG_CHILD_LOCAL.has(c.canonicalRel ?? ''));
83
+ }
84
+ /** Decide which sister of a (dep, head) edge is the constituent's DTE — 'dep',
85
+ * 'head', or 'match' (both co-designated). Encodes Wagner's combination rules. */
86
+ function combineDecision(dep, head, children, words) {
87
+ const rel = dep.canonicalRel ?? '';
88
+ const depBefore = dep.absoluteIndex < head.absoluteIndex;
89
+ const rightWins = depBefore ? 'head' : 'dep'; // NSR rightmost
90
+ const leftWins = depBefore ? 'dep' : 'head'; // CSR leftmost
91
+ // Compound Stress Rule first (handles the backwards-parsed N+N the tagger mislabels).
92
+ if (isCompound(dep, head))
93
+ return leftWins;
94
+ if (rel === 'EXT')
95
+ return rightWins; // proper-name head (New YORK)
96
+ switch (rel) {
97
+ case 'NOMD':
98
+ return leftWins; // CSR: complement (N1) prominent
99
+ case 'DOBJ':
100
+ case 'IOBJ':
101
+ case 'OBJ':
102
+ case 'CCOMP':
103
+ case 'XCOMP':
104
+ return 'dep'; // argument is the DTE
105
+ case 'OBL':
106
+ // A post-nominal / post-adjectival oblique ("a friend OF him", "full OF
107
+ // sleep") is a complement of its NON-verbal head and is subordinated to it
108
+ // (the head stays the local nuclear). Only a VERBAL oblique is an argument
109
+ // / locative of the predicate.
110
+ if (!isVerb(head))
111
+ return 'head';
112
+ if (isLowLocative(dep, head, words))
113
+ return 'match'; // low locative → own accent
114
+ if (isFrameSetting(dep, words))
115
+ return 'match'; // frame-setting → own domain
116
+ return 'dep'; // oblique argument → DTE
117
+ case 'NMOD':
118
+ return 'head'; // post-nominal modifier subordinated
119
+ case 'NSUBJ':
120
+ case 'NSUBJPASS':
121
+ if (verbHasArgChild(head, children))
122
+ return 'match'; // branching VP → specifier restriction
123
+ if (isUnaccusativeOrPassive(dep, head))
124
+ return 'dep'; // underlying object → subject DTE
125
+ return rightWins; // unergative → verb (NSR) DTE
126
+ case 'AMOD':
127
+ return depBefore ? 'match' : 'head'; // pre-modifier → own domain; post → subordinate
128
+ case 'ADVMOD':
129
+ if (/^JJ/.test(head.lexicalClass))
130
+ return 'head'; // degree adv ("very TALL") integrates
131
+ return depBefore ? 'match' : 'head'; // pre-adv → own domain; post → subordinate
132
+ case 'ACL':
133
+ case 'ADVCL':
134
+ // A BARE participial acl (no argument children — "little boy lost",
135
+ // "mission accomplished") is adjective-like: the NSR gives right-stress
136
+ // (the participle is the nuclear: "little boy LOST"). A participial PHRASE
137
+ // ("gone in the teeth") has arguments, so the HEAD NOUN stays the nuclear
138
+ // and the participial phrase is subordinated to it ("old BITCH gone in the
139
+ // teeth").
140
+ if (!hasArgChildren(dep, children))
141
+ return 'dep'; // bare participle → NSR right-stress
142
+ return 'head'; // participial phrase → head noun wins
143
+ case 'CASE':
144
+ return 'head'; // preposition subordinated to its noun
145
+ // (the "OF him" differentiation — prep beat over a given pronoun — is set in
146
+ // relstress.ts, not here, so a function word never becomes the phrase nuclear)
147
+ case 'AUX':
148
+ case 'AUXPASS':
149
+ case 'DET':
150
+ case 'NUMMOD':
151
+ case 'EXPL':
152
+ case 'COMPMARK':
153
+ case 'ADVMARK':
154
+ case 'CC':
155
+ return 'head'; // function-word functor subordinated
156
+ case 'VPRT':
157
+ case 'DISCOURSE':
158
+ case 'INTJ':
159
+ return 'match'; // particle / interjection keep accent
160
+ case 'CONJ':
161
+ return conjIsAssociative(head, children, words) ? 'match' : rightWins;
162
+ default:
163
+ return rightWins; // NSR: rightmost wins
164
+ }
165
+ }
166
+ /**
167
+ * Run the cyclic Compound + Nuclear Stress Rules (Wagner-extended) over the
168
+ * sentence's dependency tree and write each word's `phraseStress` (1 = strongest).
169
+ */
170
+ export function computePhraseStress(sent) {
171
+ for (const w of sent.words)
172
+ if (isPunct(w))
173
+ w.phraseStress = 0;
174
+ const words = sent.words.filter(w => !isPunct(w));
175
+ if (words.length === 0)
176
+ return;
177
+ const childrenOf = new Map();
178
+ const roots = [];
179
+ for (const w of words) {
180
+ const gov = w.dependency?.governor;
181
+ if (gov && !isPunct(gov) && gov !== w) {
182
+ const arr = childrenOf.get(gov.absoluteIndex);
183
+ if (arr)
184
+ arr.push(w);
185
+ else
186
+ childrenOf.set(gov.absoluteIndex, [w]);
187
+ }
188
+ else {
189
+ roots.push(w);
190
+ }
191
+ }
192
+ const cons = [];
193
+ const visited = new Set();
194
+ function combine(left, right, dep, head) {
195
+ const depBefore = dep.absoluteIndex < head.absoluteIndex;
196
+ const depCons = depBefore ? left : right;
197
+ const headCons = depBefore ? right : left;
198
+ const decision = combineDecision(dep, head, childrenOf, words);
199
+ let dtes;
200
+ if (decision === 'match') {
201
+ dtes = [...left.dtes, ...right.dtes];
202
+ // For an ASSOCIATIVE coordination, the conjunct HEAD words are all co-equal
203
+ // accents (old / gray / full), even when a conjunct's own internal nuclear
204
+ // differs ("full of sleep" → sleep) — so co-designate the conjunct heads too,
205
+ // keeping the coordinate accents level instead of letting one sink.
206
+ if ((dep.canonicalRel ?? '') === 'CONJ') {
207
+ if (!dtes.includes(dep))
208
+ dtes.push(dep);
209
+ if (!dtes.includes(head))
210
+ dtes.push(head);
211
+ }
212
+ }
213
+ else if (decision === 'dep') {
214
+ dtes = depCons.dtes;
215
+ }
216
+ else {
217
+ dtes = headCons.dtes;
218
+ }
219
+ const c = { words: [...left.words, ...right.words], dtes };
220
+ cons.push(c);
221
+ return c;
222
+ }
223
+ function project(head) {
224
+ visited.add(head);
225
+ let cur = { words: [head], dtes: [head] };
226
+ const kids = (childrenOf.get(head.absoluteIndex) ?? []).filter(k => !visited.has(k));
227
+ const left = kids.filter(k => k.absoluteIndex < head.absoluteIndex)
228
+ .sort((a, b) => b.absoluteIndex - a.absoluteIndex); // closest-first
229
+ const right = kids.filter(k => k.absoluteIndex > head.absoluteIndex)
230
+ .sort((a, b) => a.absoluteIndex - b.absoluteIndex); // closest-first
231
+ for (const d of right)
232
+ cur = combine(cur, project(d), d, head); // head LEFT, dep RIGHT
233
+ for (const d of left)
234
+ cur = combine(project(d), cur, d, head); // dep LEFT, head RIGHT
235
+ return cur;
236
+ }
237
+ roots.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
238
+ let top = null;
239
+ for (const r of roots) {
240
+ if (visited.has(r))
241
+ continue;
242
+ const pc = project(r);
243
+ if (!top) {
244
+ top = pc;
245
+ }
246
+ else {
247
+ const c = { words: [...top.words, ...pc.words], dtes: pc.dtes }; // NSR: right root wins
248
+ cons.push(c);
249
+ top = c;
250
+ }
251
+ }
252
+ // level(w) = 1 + #{ constituents c : w ∈ c.words ∧ w ∉ c.dtes }
253
+ for (const w of words) {
254
+ let demotions = 0;
255
+ for (const c of cons)
256
+ if (!c.dtes.includes(w) && c.words.includes(w))
257
+ demotions++;
258
+ w.phraseStress = 1 + demotions;
259
+ }
260
+ // Accent-level clash reduction (Wagner §6.4.1): runs BEFORE the syllable-level
261
+ // resolveStressClashes (relstress.ts) and is additional to it. See below.
262
+ reduceAccentClashes(words);
263
+ }
264
+ /** Accent-level clash reduction (Wagner §6.4.1). Operates on which CONTENT words
265
+ * bear a phrasal accent (a local prominence), NOT on syllable stress. A run of
266
+ * three-or-more SURFACE-ADJACENT content words (no intervening function word) that
267
+ * share the SAME phraseStress is a clash plateau: the last is the nuclear (never
268
+ * dropped), the first keeps its accent, and medial accents are thinned by the
269
+ * alternation rule (keep every other one). Dropped accents are demoted one rung
270
+ * (phraseStress += 1), increasing contour differentiation without flattening.
271
+ *
272
+ * This is deliberately narrow — it fires only on a true equal-prominence plateau —
273
+ * so ordinary verse, where the cyclic rules already differentiate, is untouched.
274
+ * The syllable-level resolveStressClashes still runs afterward for cross-φ
275
+ * abutments and the compound-pair special case. */
276
+ function reduceAccentClashes(words) {
277
+ const ordered = [...words].sort((a, b) => a.absoluteIndex - b.absoluteIndex);
278
+ let i = 0;
279
+ while (i < ordered.length) {
280
+ if (!ordered[i].isContent) {
281
+ i++;
282
+ continue;
283
+ }
284
+ // Maximal run of surface-adjacent content words (adjacent absolute indices).
285
+ let j = i;
286
+ while (j + 1 < ordered.length && ordered[j + 1].isContent &&
287
+ ordered[j + 1].absoluteIndex === ordered[j].absoluteIndex + 1) {
288
+ j++;
289
+ }
290
+ const run = ordered.slice(i, j + 1);
291
+ if (run.length >= 3) {
292
+ const minPs = Math.min(...run.map(w => w.phraseStress));
293
+ const plateau = run.filter(w => w.phraseStress === minPs);
294
+ // Only thin when MOST of the run sits on the same prominence (a real plateau).
295
+ if (plateau.length >= 3) {
296
+ // keep first and last; drop alternate medial members.
297
+ for (let k = 1; k < plateau.length - 1; k++) {
298
+ if (k % 2 === 1)
299
+ plateau[k].phraseStress = minPs + 1; // drop every other medial accent
300
+ }
301
+ }
302
+ }
303
+ i = j + 1;
304
+ }
305
+ }
306
+ // ─── ϕ-domain derivation (the phonological-phrase grounding) ──────────────
307
+ //
308
+ // The phonological-phrase (ϕ) layer is derived from the SAME dependency
309
+ // constituent structure the cyclic stress rules use above — not from a parallel
310
+ // POS heuristic. Two adjacent words share a ϕ iff the dependency edge between
311
+ // them is ϕ-INTERNAL; a new ϕ opens at every ϕ-PROJECTING edge.
312
+ const NOMINAL = /^(NN|NNS|NNP|NNPS)$/;
313
+ const COPULA = new Set(['am', 'is', 'are', 'was', 'were']);
314
+ function isCopula(w) {
315
+ return COPULA.has(w.word.toLowerCase().replace(/['’]/g, '')) && /^VB/.test(w.lexicalClass);
316
+ }
317
+ /** Does a nominal `dep` BRANCH — carry its own NOMINAL modifier? An ADVERB child
318
+ * does NOT count. */
319
+ function branchesNominally(dep, children) {
320
+ return (children.get(dep.absoluteIndex) ?? []).some(c => {
321
+ if (!c.isContent)
322
+ return false;
323
+ if (/^RB/.test(c.lexicalClass) || (c.canonicalRel ?? '') === 'ADVMOD')
324
+ return false;
325
+ return true;
326
+ });
327
+ }
328
+ /** Does `dep`'s subtree open its OWN ϕ relative to its governor `head`? */
329
+ function phiProjects(dep, head, children, words) {
330
+ switch (dep.canonicalRel ?? '') {
331
+ case 'CCOMP':
332
+ case 'XCOMP':
333
+ case 'ADVCL':
334
+ case 'CONJ':
335
+ return true;
336
+ case 'ACL':
337
+ // A BARE participial acl ("little boy lost", "mission accomplished" — no
338
+ // argument children) is adjective-like: it stays WITH its head noun in one
339
+ // ϕ, and the NSR gives right-stress (LOST is the nuclear). A participial
340
+ // PHRASE ("gone in the teeth") has arguments and is a clause: it projects
341
+ // its own ϕ, separate from the head noun.
342
+ return hasArgChildren(dep, children);
343
+ case 'OBL':
344
+ // A verbal oblique is its own ϕ — EXCEPT an oblique whose governor is an
345
+ // ACL (a participle): it is a complement of the participial phrase, not an
346
+ // independent locative, so it stays INSIDE the ACL's ϕ ("gone in the teeth"
347
+ // is one ϕ, not two). This prevents the oblique from being stranded alone
348
+ // while the participle projects separately.
349
+ if ((head.canonicalRel ?? '') === 'ACL')
350
+ return false;
351
+ if (!isVerb(head))
352
+ return isLightNominalHead(head); // post-nominal PP of a LIGHT
353
+ // head ("Something | for the modern stage") projects; of a FULL noun ("a mould
354
+ // in plaster") it does not — see the NMOD case for the same modifier asymmetry.
355
+ if (isLowLocative(dep, head, words) || isFrameSetting(dep, words))
356
+ return true;
357
+ return true; // verbal oblique → own ϕ
358
+ case 'NSUBJ':
359
+ case 'NSUBJPASS':
360
+ return NOMINAL.test(dep.lexicalClass);
361
+ case 'DOBJ':
362
+ case 'IOBJ':
363
+ case 'OBJ':
364
+ if (isCopula(head))
365
+ return false;
366
+ return branchesNominally(dep, children);
367
+ case 'NMOD':
368
+ // A post-nominal PP modifier of a LIGHT head (a pronoun / inherently-given
369
+ // indefinite — "Something | for the modern stage") opens its OWN ϕ: the head
370
+ // is too light to host the modifier, so a real phrasing break falls after it
371
+ // (Wagner §6.5.1). A FULL lexical-noun head keeps its post-nominal PP as an
372
+ // internal κ ("a mould in plaster" stays one ϕ), so this does not over-segment.
373
+ return isLightNominalHead(head);
374
+ default:
375
+ return false;
376
+ }
377
+ }
378
+ /**
379
+ * Partition the sentence's words into ϕ-domains over the dependency tree.
380
+ */
381
+ export function computePhiDomains(sent) {
382
+ const words = sent.words.filter(w => !isPunct(w));
383
+ const children = new Map();
384
+ for (const w of words) {
385
+ const g = w.dependency?.governor;
386
+ if (g && !isPunct(g) && g !== w) {
387
+ const a = children.get(g.absoluteIndex);
388
+ if (a)
389
+ a.push(w);
390
+ else
391
+ children.set(g.absoluteIndex, [w]);
392
+ }
393
+ }
394
+ const parent = new Map();
395
+ for (const w of words)
396
+ parent.set(w.absoluteIndex, w.absoluteIndex);
397
+ const find = (x) => {
398
+ while (parent.get(x) !== x) {
399
+ parent.set(x, parent.get(parent.get(x)));
400
+ x = parent.get(x);
401
+ }
402
+ return x;
403
+ };
404
+ const union = (a, b) => { parent.set(find(a), find(b)); };
405
+ for (const w of words) {
406
+ const g = w.dependency?.governor;
407
+ if (!g || isPunct(g) || g === w)
408
+ continue;
409
+ if (!phiProjects(w, g, children, words))
410
+ union(w.absoluteIndex, g.absoluteIndex);
411
+ }
412
+ const dom = new Map();
413
+ for (const w of words)
414
+ dom.set(w, find(w.absoluteIndex));
415
+ return dom;
416
+ }
@@ -0,0 +1,4 @@
1
+ import { ClsSentence } from '../types.js';
2
+ /** Populate `canonicalRel` for every word, then apply label-only repairs. */
3
+ export declare function normalizeDeps(sent: ClsSentence): void;
4
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/calliope/deps.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,WAAW,EAAW,MAAM,aAAa,CAAC;AAkBnD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIrD"}
@@ -0,0 +1,181 @@
1
+ // calliope/deps.ts — canonical dependency normalisation for the Calliope engine.
2
+ //
3
+ // en-parse emits a hybrid Stanford/UD label set, partly unreliable. This pass
4
+ // writes a normalised Scenario relation onto `word.canonicalRel`, the label space
5
+ // the Match-Theory prosodic builder (Stage 2) and the Scenario A–O stress rules
6
+ // (Stage 3) read. It is ADDITIVE — it never mutates the raw `dependency`, so the
7
+ // legacy/Clio passes see exactly the same parse as before.
8
+ //
9
+ // Where en-parse is reliable the mapping is a straight relabel; where it is not —
10
+ // the ditransitive DOBJ/IOBJ swap (probed: "gave John a book" → John=dobj,
11
+ // book=iobj, reversed), or N+N compounds it labels generic `dep` — POS and surface
12
+ // adjacency decide. (Head-changing repairs — coordinate re-heading, fronted
13
+ // adverbial re-root, invocations — are handled where the prosodic builder needs
14
+ // them, Stage 2.)
15
+ const NOUN = /^(NN|NNS|NNP|NNPS)$/;
16
+ const PROPER = /^(NNP|NNPS)$/;
17
+ const VERB = /^VB/;
18
+ const ADJ = /^JJ/;
19
+ function rawRel(w) {
20
+ return (w.dependency?.dependentType ?? '').toLowerCase();
21
+ }
22
+ function gov(w) {
23
+ return w.dependency?.governor;
24
+ }
25
+ /** w immediately precedes its head (a pre-head modifier — the N+N / Adj+N frame). */
26
+ function preHead(w, head) {
27
+ return w.absoluteIndex + 1 === head.absoluteIndex;
28
+ }
29
+ /** Populate `canonicalRel` for every word, then apply label-only repairs. */
30
+ export function normalizeDeps(sent) {
31
+ for (const w of sent.words)
32
+ w.canonicalRel = canonical(w);
33
+ fixDitransitive(sent.words);
34
+ inferPrenominalModifiers(sent.words);
35
+ }
36
+ /** Reliable structural relations whose label adjacency must NOT override. */
37
+ const STRUCTURAL = new Set([
38
+ 'ROOT', 'NSUBJ', 'NSUBJPASS', 'DOBJ', 'IOBJ', 'OBL', 'AUX', 'AUXPASS',
39
+ 'CCOMP', 'XCOMP', 'ADVCL', 'ADVMOD', 'AMOD', 'ACL', 'CC', 'CONJ', 'EXPL',
40
+ 'INTJ', 'DISCOURSE', 'VPRT', 'COMPMARK', 'ADVMARK', 'EXT',
41
+ ]);
42
+ /**
43
+ * Pre-head modifier inference by SURFACE ADJACENCY — independent of en-parse's
44
+ * (often unreliable) head links. An attributive adjective immediately before a
45
+ * noun is AMOD; a noun immediately before a noun is a NOMD noun adjunct (or EXT
46
+ * for a proper+proper name span). This is what lets a POS-corrected adjective
47
+ * (Pale/High/Green, demoted from a spurious NNP by `correctPosWithLexicon`) read
48
+ * as the AMOD it is, rather than collapsing to a bare `dep`. Only fills a word
49
+ * whose current label is non-structural, so deliberate relations are preserved.
50
+ */
51
+ function inferPrenominalModifiers(words) {
52
+ for (let i = 0; i + 1 < words.length; i++) {
53
+ const w = words[i];
54
+ const h = words[i + 1];
55
+ if (w.absoluteIndex + 1 !== h.absoluteIndex)
56
+ continue; // surface-adjacent
57
+ if (!NOUN.test(h.lexicalClass))
58
+ continue; // head is a noun
59
+ if (STRUCTURAL.has(w.canonicalRel ?? ''))
60
+ continue; // keep real relations
61
+ if (ADJ.test(w.lexicalClass)) {
62
+ w.canonicalRel = 'AMOD';
63
+ }
64
+ else if (NOUN.test(w.lexicalClass)) {
65
+ w.canonicalRel = PROPER.test(w.lexicalClass) && PROPER.test(h.lexicalClass) ? 'EXT' : 'NOMD';
66
+ }
67
+ }
68
+ }
69
+ function canonical(w) {
70
+ const rel = rawRel(w);
71
+ const pos = w.lexicalClass;
72
+ const g = gov(w);
73
+ const gpos = g?.lexicalClass ?? '';
74
+ switch (rel) {
75
+ case 'root': return 'ROOT';
76
+ case 'nsubj': return 'NSUBJ';
77
+ case 'nsubjpass':
78
+ case 'nsubj:pass': return 'NSUBJPASS';
79
+ case 'csubj': return 'NSUBJ';
80
+ case 'csubjpass':
81
+ case 'csubj:pass': return 'NSUBJPASS';
82
+ case 'dobj':
83
+ case 'obj': return 'DOBJ';
84
+ case 'iobj': return 'IOBJ';
85
+ case 'aux': return 'AUX';
86
+ case 'auxpass':
87
+ case 'aux:pass': return 'AUXPASS';
88
+ // UD oblique nominal (UDPipe emits `obl`; the old path used Stanford `pobj`).
89
+ case 'obl':
90
+ case 'obl:npmod':
91
+ case 'obl:tmod':
92
+ case 'obl:arg': return 'OBL';
93
+ case 'cop': return 'AUX'; // copula behaves prosodically like an auxiliary
94
+ case 'ccomp': return 'CCOMP';
95
+ case 'xcomp': return 'XCOMP';
96
+ case 'advcl': return 'ADVCL';
97
+ case 'advmod': return 'ADVMOD';
98
+ case 'amod': return 'AMOD';
99
+ case 'acl':
100
+ case 'relcl':
101
+ case 'acl:relcl': return 'ACL';
102
+ case 'det':
103
+ case 'predet': return 'DET';
104
+ case 'nummod': return 'NUMMOD';
105
+ case 'cc': return 'CC';
106
+ case 'conj': return 'CONJ';
107
+ case 'expl': return 'EXPL';
108
+ case 'intj': return 'INTJ';
109
+ case 'discourse': return 'DISCOURSE';
110
+ case 'prt':
111
+ case 'compound:prt': return 'VPRT';
112
+ case 'case': return 'CASE';
113
+ case 'poss':
114
+ case 'possessive':
115
+ case 'nmod:poss': return 'CASE';
116
+ case 'prep': return 'CASE'; // the preposition itself cliticises
117
+ case 'pobj': return 'OBL'; // object of a preposition → oblique
118
+ case 'mark': return markType(w); // complementiser vs adverbial subordinator
119
+ case 'nmod':
120
+ if (NOUN.test(pos) && g && NOUN.test(gpos) && preHead(w, g))
121
+ return 'NOMD';
122
+ return 'OBL';
123
+ case 'compound':
124
+ return 'NOMD';
125
+ case 'flat':
126
+ case 'flat:name':
127
+ case 'name':
128
+ return 'EXT';
129
+ }
130
+ // Generic `dep` / unknown: infer from POS + adjacency.
131
+ if (NOUN.test(pos) && g && NOUN.test(gpos) && preHead(w, g)) {
132
+ // A proper-name span (both proper, adjacent) reads as an EXT extension; a
133
+ // common-noun pre-modifier is a NOMD noun adjunct.
134
+ return PROPER.test(pos) && PROPER.test(gpos) ? 'EXT' : 'NOMD';
135
+ }
136
+ if (ADJ.test(pos) && g && NOUN.test(gpos))
137
+ return 'AMOD';
138
+ if (pos === 'RP')
139
+ return 'VPRT';
140
+ if (pos === 'CC')
141
+ return 'CC';
142
+ if (pos === 'DT' || pos === 'PDT')
143
+ return 'DET';
144
+ if (pos === 'CD')
145
+ return 'NUMMOD';
146
+ if (pos === 'IN' || pos === 'TO')
147
+ return 'CASE';
148
+ return rel ? rel.toUpperCase() : 'DEP';
149
+ }
150
+ /** A `mark` heads a complement clause (COMPMARK: to/that) or an adverbial clause
151
+ * (ADVMARK: as/when/because). Decide by the governed clause's own relation. */
152
+ function markType(w) {
153
+ const clauseVerb = gov(w);
154
+ const crel = clauseVerb ? rawRel(clauseVerb) : '';
155
+ return crel === 'advcl' ? 'ADVMARK' : 'COMPMARK';
156
+ }
157
+ /** Ditransitive correction: a verb governing two bare objects N1 (precedes) N2 is
158
+ * often labelled N1=DOBJ N2=IOBJ — reversed. The first post-verbal object is the
159
+ * recipient (IOBJ), the second the theme (DOBJ). */
160
+ function fixDitransitive(words) {
161
+ const byGov = new Map();
162
+ for (const w of words) {
163
+ if (w.canonicalRel !== 'DOBJ' && w.canonicalRel !== 'IOBJ')
164
+ continue;
165
+ const g = gov(w);
166
+ if (!g || !VERB.test(g.lexicalClass))
167
+ continue;
168
+ const list = byGov.get(g);
169
+ if (list)
170
+ list.push(w);
171
+ else
172
+ byGov.set(g, [w]);
173
+ }
174
+ for (const objs of byGov.values()) {
175
+ if (objs.length !== 2)
176
+ continue;
177
+ objs.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
178
+ objs[0].canonicalRel = 'IOBJ';
179
+ objs[1].canonicalRel = 'DOBJ';
180
+ }
181
+ }
@@ -0,0 +1,3 @@
1
+ import { ProsodyEngine } from '../engine.js';
2
+ export declare const calliopeEngine: ProsodyEngine;
3
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/calliope/engine.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA0D7C,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}