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.
- package/README.md +32 -20
- package/dist/caesura.d.ts +33 -0
- package/dist/caesura.d.ts.map +1 -0
- package/dist/caesura.js +202 -0
- package/dist/calliope/boundaries.d.ts +19 -0
- package/dist/calliope/boundaries.d.ts.map +1 -0
- package/dist/calliope/boundaries.js +182 -0
- package/dist/calliope/bracketing.d.ts +11 -0
- package/dist/calliope/bracketing.d.ts.map +1 -0
- package/dist/calliope/bracketing.js +416 -0
- package/dist/calliope/deps.d.ts +4 -0
- package/dist/calliope/deps.d.ts.map +1 -0
- package/dist/calliope/deps.js +181 -0
- package/dist/calliope/engine.d.ts +3 -0
- package/dist/calliope/engine.d.ts.map +1 -0
- package/dist/calliope/engine.js +71 -0
- package/dist/calliope/feats.d.ts +9 -0
- package/dist/calliope/feats.d.ts.map +1 -0
- package/dist/calliope/feats.js +45 -0
- package/dist/calliope/names.d.ts +7 -0
- package/dist/calliope/names.d.ts.map +1 -0
- package/dist/calliope/names.js +42 -0
- package/dist/calliope/postag.d.ts +8 -0
- package/dist/calliope/postag.d.ts.map +1 -0
- package/dist/calliope/postag.js +250 -0
- package/dist/calliope/prosodic.d.ts +3 -0
- package/dist/calliope/prosodic.d.ts.map +1 -0
- package/dist/calliope/prosodic.js +275 -0
- package/dist/calliope/relstress.d.ts +4 -0
- package/dist/calliope/relstress.d.ts.map +1 -0
- package/dist/calliope/relstress.js +688 -0
- package/dist/calliope/stressrules.d.ts +28 -0
- package/dist/calliope/stressrules.d.ts.map +1 -0
- package/dist/calliope/stressrules.js +147 -0
- package/dist/calliope/syntax.d.ts +38 -0
- package/dist/calliope/syntax.d.ts.map +1 -0
- package/dist/calliope/syntax.js +234 -0
- package/dist/calliope/udpos.d.ts +13 -0
- package/dist/calliope/udpos.d.ts.map +1 -0
- package/dist/calliope/udpos.js +156 -0
- package/dist/clio/caesura.d.ts +27 -0
- package/dist/clio/caesura.d.ts.map +1 -0
- package/dist/clio/caesura.js +148 -0
- package/dist/clio/depfix.d.ts +13 -0
- package/dist/clio/depfix.d.ts.map +1 -0
- package/dist/clio/depfix.js +84 -0
- package/dist/clio/display.d.ts +32 -0
- package/dist/clio/display.d.ts.map +1 -0
- package/dist/clio/display.js +976 -0
- package/dist/clio/engine.d.ts +3 -0
- package/dist/clio/engine.d.ts.map +1 -0
- package/dist/clio/engine.js +28 -0
- package/dist/clio/parser.d.ts +10 -0
- package/dist/clio/parser.d.ts.map +1 -0
- package/dist/clio/parser.js +696 -0
- package/dist/clio/phonological.d.ts +41 -0
- package/dist/clio/phonological.d.ts.map +1 -0
- package/dist/clio/phonological.js +788 -0
- package/dist/clio/phrasestress.d.ts +6 -0
- package/dist/clio/phrasestress.d.ts.map +1 -0
- package/dist/clio/phrasestress.js +106 -0
- package/dist/clio/pipeline.d.ts +11 -0
- package/dist/clio/pipeline.d.ts.map +1 -0
- package/dist/clio/pipeline.js +147 -0
- package/dist/clio/rhyme.d.ts +65 -0
- package/dist/clio/rhyme.d.ts.map +1 -0
- package/dist/clio/rhyme.js +761 -0
- package/dist/clio/scandroid.d.ts +17 -0
- package/dist/clio/scandroid.d.ts.map +1 -0
- package/dist/clio/scandroid.js +435 -0
- package/dist/clio/scansion.d.ts +46 -0
- package/dist/clio/scansion.d.ts.map +1 -0
- package/dist/clio/scansion.js +1086 -0
- package/dist/clio/semantics.d.ts +44 -0
- package/dist/clio/semantics.d.ts.map +1 -0
- package/dist/clio/semantics.js +139 -0
- package/dist/clio/stress.d.ts +83 -0
- package/dist/clio/stress.d.ts.map +1 -0
- package/dist/clio/stress.js +1737 -0
- package/dist/clio/tagfix.d.ts +6 -0
- package/dist/clio/tagfix.d.ts.map +1 -0
- package/dist/clio/tagfix.js +101 -0
- package/dist/display.d.ts +0 -6
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +322 -141
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +12 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +169 -26
- package/dist/parser.d.ts +3 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +320 -563
- package/dist/phonological.d.ts +1 -1
- package/dist/phonological.d.ts.map +1 -1
- package/dist/phonological.js +73 -4
- package/dist/phrasestress.d.ts +6 -0
- package/dist/phrasestress.d.ts.map +1 -0
- package/dist/phrasestress.js +106 -0
- package/dist/rhyme.d.ts +40 -1
- package/dist/rhyme.d.ts.map +1 -1
- package/dist/rhyme.js +435 -7
- package/dist/scansion.d.ts +9 -0
- package/dist/scansion.d.ts.map +1 -1
- package/dist/scansion.js +145 -18
- package/dist/semantics.d.ts +44 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +139 -0
- package/dist/stress.d.ts +127 -4
- package/dist/stress.d.ts.map +1 -1
- package/dist/stress.js +834 -61
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/caesura.ts +201 -0
- package/src/calliope/boundaries.ts +190 -0
- package/src/calliope/bracketing.ts +390 -0
- package/src/calliope/deps.ts +160 -0
- package/src/calliope/engine.ts +77 -0
- package/src/calliope/feats.ts +46 -0
- package/src/calliope/names.ts +44 -0
- package/src/calliope/postag.ts +253 -0
- package/src/calliope/prosodic.ts +262 -0
- package/src/calliope/relstress.ts +645 -0
- package/src/calliope/stressrules.ts +147 -0
- package/src/calliope/syntax.ts +218 -0
- package/src/calliope/udpos.ts +152 -0
- package/src/calliope_src_contents.md +19049 -0
- package/src/clio/caesura.ts +145 -0
- package/src/clio/depfix.ts +88 -0
- package/src/clio/display.ts +1042 -0
- package/src/clio/engine.ts +38 -0
- package/src/clio/parser.ts +845 -0
- package/src/clio/phonological.ts +849 -0
- package/src/clio/phrasestress.ts +108 -0
- package/src/clio/pipeline.ts +154 -0
- package/src/clio/rhyme.ts +740 -0
- package/src/clio/scandroid.ts +434 -0
- package/src/clio/scansion.ts +1130 -0
- package/src/clio/semantics.ts +134 -0
- package/src/clio/stress.ts +1731 -0
- package/src/clio/tagfix.ts +104 -0
- package/src/display.ts +321 -137
- package/src/engine.ts +22 -0
- package/src/index.ts +176 -32
- package/src/parser.ts +346 -682
- package/src/phonological.ts +72 -4
- package/src/phrasestress.ts +108 -0
- package/src/rhyme.ts +428 -8
- package/src/scansion.ts +136 -15
- package/src/semantics.ts +134 -0
- package/src/stress.ts +794 -64
- package/src/types.ts +39 -4
- package/tests/DataForHayesLinesOnly.txt +364 -0
- package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
- package/tests/basic.test.ts +443 -37
- package/tests/bench-hayes.mjs +72 -0
- package/tests/wagner-stress.test.ts +188 -0
- package/vitest.config.ts +0 -15
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// bench-hayes.mjs — relative-stress benchmark against the maintainer's revised
|
|
2
|
+
// Hayes English-folk-song corpus (tests/DataForHayesStressSymbolsRevised.txt),
|
|
3
|
+
// whose per-syllable x/w/n/m/s patterns were re-marked BY EAR (sung aloud) in our
|
|
4
|
+
// gradience nomenclature. This is ground truth for the Calliope relative-stress
|
|
5
|
+
// engine — not a meter fixture.
|
|
6
|
+
//
|
|
7
|
+
// node tests/bench-hayes.mjs # summary
|
|
8
|
+
// node tests/bench-hayes.mjs --worst 20 # + the N worst-disagreeing lines
|
|
9
|
+
// node tests/bench-hayes.mjs --clio # benchmark the frozen Clio engine
|
|
10
|
+
//
|
|
11
|
+
// Metrics (on lines whose engine syllable count matches the marking):
|
|
12
|
+
// BEAT agreement = fraction of syllables where engine and ear agree on
|
|
13
|
+
// is-this-a-beat (relative stress >= m). The headline number.
|
|
14
|
+
// exact / within-1 = tier-level agreement on x<w<n<m<s.
|
|
15
|
+
// strong-adjacencies = engine count of abutting beats (the ear's corpus has 0).
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'fs';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const useClio = process.argv.includes('--clio');
|
|
23
|
+
const worstN = (() => { const i = process.argv.indexOf('--worst'); return i >= 0 ? +process.argv[i + 1] || 10 : 0; })();
|
|
24
|
+
|
|
25
|
+
const { analyzeText } = await import(join(here, '..', 'dist', 'index.js'));
|
|
26
|
+
const { analyzeTextClio } = await import(join(here, '..', 'dist', 'clio', 'pipeline.js'));
|
|
27
|
+
const analyze = useClio ? analyzeTextClio : analyzeText;
|
|
28
|
+
|
|
29
|
+
const RANK = { x: 0, w: 1, n: 2, m: 3, s: 4 };
|
|
30
|
+
const isBeat = (c) => RANK[c] >= 3;
|
|
31
|
+
|
|
32
|
+
const lines = readFileSync(join(here, 'DataForHayesStressSymbolsRevised.txt'), 'utf8').split(/\r?\n/);
|
|
33
|
+
const rows = [];
|
|
34
|
+
let curText = null;
|
|
35
|
+
for (const ln of lines) {
|
|
36
|
+
const t = ln.match(/line text\s+(.*\S)\s*$/);
|
|
37
|
+
const p = ln.match(/stress pattern\s+([xwnms=]+)\s*$/);
|
|
38
|
+
if (t) curText = t[1];
|
|
39
|
+
if (p && curText) { rows.push({ text: curText, pat: p[1].replace(/[^xwnms]/g, '') }); curText = null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let aligned = 0, mismatch = 0, beatAgree = 0, beatTot = 0, exact = 0, within1 = 0, tierTot = 0, strongAdj = 0;
|
|
43
|
+
const diffs = [];
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
const res = analyze(r.text, false);
|
|
46
|
+
const seq = res.flatMap(x => x.sentence.words.flatMap(w => w.syllables.map(s => s.relativeStress ?? 'w')));
|
|
47
|
+
const wrd = res.flatMap(x => x.sentence.words.flatMap(w => w.syllables.map(() => w.word)));
|
|
48
|
+
if (seq.length !== r.pat.length) { mismatch++; continue; }
|
|
49
|
+
aligned++;
|
|
50
|
+
for (let i = 0; i + 1 < seq.length; i++) if (isBeat(seq[i]) && isBeat(seq[i + 1])) strongAdj++;
|
|
51
|
+
let mism = 0; const detail = [];
|
|
52
|
+
for (let i = 0; i < seq.length; i++) {
|
|
53
|
+
const e = RANK[seq[i]], u = RANK[r.pat[i]];
|
|
54
|
+
tierTot++; if (e === u) exact++; if (Math.abs(e - u) <= 1) within1++;
|
|
55
|
+
beatTot++; if (isBeat(seq[i]) === isBeat(r.pat[i])) beatAgree++;
|
|
56
|
+
else { mism++; detail.push((isBeat(seq[i]) ? '+' : '-') + wrd[i] + '(' + seq[i] + '/' + r.pat[i] + ')'); }
|
|
57
|
+
}
|
|
58
|
+
if (mism) diffs.push({ mism, text: r.text, detail });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pct = (a, b) => (100 * a / b).toFixed(1) + '%';
|
|
62
|
+
console.log(`engine: ${useClio ? 'Clio (frozen)' : 'Calliope'}`);
|
|
63
|
+
console.log(`rows ${rows.length} · aligned ${aligned} · syllable-count mismatch ${mismatch}`);
|
|
64
|
+
console.log(`BEAT agreement ${pct(beatAgree, beatTot)}`);
|
|
65
|
+
console.log(`exact tier ${pct(exact, tierTot)}`);
|
|
66
|
+
console.log(`within-1 tier ${pct(within1, tierTot)}`);
|
|
67
|
+
console.log(`engine strong-adjacencies ${strongAdj} (ear corpus: 0)`);
|
|
68
|
+
if (worstN) {
|
|
69
|
+
console.log(`\nworst ${worstN} lines (engine-tier/ear-tier; + = engine over-beats, - = engine misses):`);
|
|
70
|
+
diffs.sort((a, b) => b.mism - a.mism);
|
|
71
|
+
for (const d of diffs.slice(0, worstN)) console.log(`${d.mism} ${d.text}\n ${d.detail.join(' ')}`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// tests/wagner-stress.test.ts — Wagner (2005) / Krifka (2001) phrase-stress rebuild
|
|
2
|
+
// (2026-06-29). Tests the functor/argument cyclic stress, boundary strength, and the
|
|
3
|
+
// relativiser refinements. Assertions are RELATIONAL (A more prominent than B), not
|
|
4
|
+
// exact golden strings, because UDPipe's parse of a bare fragment is noisy — the
|
|
5
|
+
// theory is verified on the structure the parser actually returns, and where a parse
|
|
6
|
+
// defeats the rule that is the parser's limit, not the stress logic's.
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { parseDocument, isPunctuation } from '../src/parser.js';
|
|
10
|
+
import { calliopeEngine } from '../src/calliope/engine.js';
|
|
11
|
+
import { computeBoundaries } from '../src/calliope/boundaries.js';
|
|
12
|
+
import { feat } from '../src/calliope/feats.js';
|
|
13
|
+
import type { ClsWord, ClsSentence } from '../src/types.js';
|
|
14
|
+
|
|
15
|
+
/** Parse one line and run the Calliope per-sentence passes; return the merged words
|
|
16
|
+
* and the IU hierarchy. */
|
|
17
|
+
function analyse(line: string): { words: ClsWord[]; sents: ClsSentence[]; ius: ReturnType<typeof calliopeEngine.analyzeSentence> } {
|
|
18
|
+
const doc = parseDocument(line);
|
|
19
|
+
const ius = doc.sentences.flatMap(s => calliopeEngine.analyzeSentence(s));
|
|
20
|
+
const words = doc.sentences.flatMap(s => s.words).filter(w => !isPunctuation(w.lexicalClass));
|
|
21
|
+
return { words, sents: doc.sentences, ius };
|
|
22
|
+
}
|
|
23
|
+
const byWord = (words: ClsWord[], surf: string) =>
|
|
24
|
+
words.find(w => w.word.toLowerCase().replace(/['’]/g, '') === surf.toLowerCase());
|
|
25
|
+
const contour = (w: ClsWord | undefined) => (w?.syllables ?? []).map(s => s.relativeStress).join('');
|
|
26
|
+
const RANK: Record<string, number> = { x: 0, w: 1, n: 2, m: 3, s: 4 };
|
|
27
|
+
const peakRel = (w: ClsWord | undefined): number =>
|
|
28
|
+
Math.max(0, ...(w?.syllables ?? []).map(s => RANK[s.relativeStress ?? 'w']));
|
|
29
|
+
|
|
30
|
+
describe('Wagner/Krifka phrase stress (2026-06-29)', () => {
|
|
31
|
+
it('Phase 1: FEATS are parsed onto featsMap', () => {
|
|
32
|
+
const { words } = analyse('The woman wore the hat');
|
|
33
|
+
const wore = byWord(words, 'wore');
|
|
34
|
+
expect(wore).toBeTruthy();
|
|
35
|
+
// UDPipe GUM marks a finite verb VerbForm=Fin; the parser must expose it.
|
|
36
|
+
expect(feat(wore!, 'VerbForm')).toBeTruthy();
|
|
37
|
+
const the = byWord(words, 'the');
|
|
38
|
+
expect(feat(the!, 'PronType')).toBe('Art');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('Phase 4: specifier restriction — a transitive subject is not crushed below the verb', () => {
|
|
42
|
+
// "The woman wore the hat": the VP branches (object "hat"), so the subject "woman"
|
|
43
|
+
// is NOT subordinated below the verb — it stays a co-prominence with the nuclear.
|
|
44
|
+
const { words } = analyse('The woman wore the hat');
|
|
45
|
+
const woman = byWord(words, 'woman')!;
|
|
46
|
+
const wore = byWord(words, 'wore')!;
|
|
47
|
+
const hat = byWord(words, 'hat')!;
|
|
48
|
+
expect(hat.phraseStress).toBe(1); // object is the utterance nuclear
|
|
49
|
+
expect(woman.phraseStress).toBeLessThanOrEqual(wore.phraseStress); // subject ≥ verb in prominence
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('Phase 4: modifier-precedes gets its own accentual domain (the invisible worm)', () => {
|
|
53
|
+
// Wagner §6.5.1: a pre-nominal modifier is NOT integrated — both "invisible" and
|
|
54
|
+
// "worm" project (both bear a beat in the contour).
|
|
55
|
+
const { words } = analyse('the invisible worm');
|
|
56
|
+
const invisible = byWord(words, 'invisible')!;
|
|
57
|
+
const worm = byWord(words, 'worm')!;
|
|
58
|
+
expect(peakRel(invisible)).toBeGreaterThanOrEqual(RANK.m); // modifier bears a beat
|
|
59
|
+
expect(peakRel(worm)).toBeGreaterThanOrEqual(RANK.m); // head bears a beat
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('Phase 4: unergative intransitive — the verb is the nuclear (a child cried)', () => {
|
|
63
|
+
const { words } = analyse('A child cried');
|
|
64
|
+
const child = byWord(words, 'child')!;
|
|
65
|
+
const cried = byWord(words, 'cried')!;
|
|
66
|
+
expect(cried.phraseStress).toBeLessThanOrEqual(child.phraseStress); // verb ≥ subject
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('Phase 6: a post-posed relative clause is subordinated below its head', () => {
|
|
70
|
+
// "the cat that caught the rat": the head noun "cat" outranks the clause verb.
|
|
71
|
+
const { words } = analyse('the cat that caught the rat');
|
|
72
|
+
const cat = byWord(words, 'cat')!;
|
|
73
|
+
const caught = byWord(words, 'caught')!;
|
|
74
|
+
expect(cat.phraseStress).toBeLessThan(caught.phraseStress);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('Phase 7: a goal-argument oblique integrates (single accent on the NP)', () => {
|
|
78
|
+
// "she walked to the store": the goal argument "store" is the nuclear; the verb is
|
|
79
|
+
// subordinated to it (Krifka §4.5.1 argument integration).
|
|
80
|
+
const { words } = analyse('She walked to the store');
|
|
81
|
+
const walked = byWord(words, 'walked')!;
|
|
82
|
+
const store = byWord(words, 'store')!;
|
|
83
|
+
expect(store.phraseStress).toBeLessThanOrEqual(walked.phraseStress);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('Phase 7: a low locative adjunct keeps its own accent (he smoked in the tent)', () => {
|
|
87
|
+
// Larson 2005 / Wagner §6.5.1: a VP-final place oblique is NOT a functor — both
|
|
88
|
+
// the verb and the locative noun bear a beat.
|
|
89
|
+
const { words } = analyse('He smoked in the tent');
|
|
90
|
+
const smoked = byWord(words, 'smoked')!;
|
|
91
|
+
const tent = byWord(words, 'tent')!;
|
|
92
|
+
expect(peakRel(smoked)).toBeGreaterThanOrEqual(RANK.m);
|
|
93
|
+
expect(peakRel(tent)).toBeGreaterThanOrEqual(RANK.m);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('Phase 9: a stranded preposition keeps a beat (what are you waiting for)', () => {
|
|
97
|
+
// Wagner §6.5.5: only complement-taking (transitive) prepositions reduce to x; a
|
|
98
|
+
// stranded "for" retains an overt beat (≥ w), not the zero-provision clitic tier.
|
|
99
|
+
const { words } = analyse('what are you waiting for');
|
|
100
|
+
const forW = byWord(words, 'for')!;
|
|
101
|
+
expect(peakRel(forW)).toBeGreaterThanOrEqual(RANK.w);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('Phase 3: a comma boundary is stronger than a phrase-internal break', () => {
|
|
105
|
+
// Boundary strength scales with punctuation + clause separation (Wagner Ch.5).
|
|
106
|
+
const { words, ius } = analyse('In Pakistan, Tuesday is a holiday');
|
|
107
|
+
const b = computeBoundaries(words, ius);
|
|
108
|
+
const maxStrength = Math.max(0, ...b.phi.map(p => p.strength));
|
|
109
|
+
expect(maxStrength).toBeGreaterThan(0.5); // a real graded break exists
|
|
110
|
+
expect(b.phi.every(p => p.strength >= 0 && p.strength <= 1)).toBe(true); // normalised 0..1
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('Lock-in: a clean line stays differentiated (not a plateau of nuclei)', () => {
|
|
114
|
+
// The relativiser must keep a per-φ beat structure, not flatten every content
|
|
115
|
+
// word to s. (A degenerate noun-pile PARSE can still give one nuclear per φ;
|
|
116
|
+
// this checks a cleanly-parsing pentameter keeps secondary beats below the nuclei.)
|
|
117
|
+
const { words } = analyse('Rough winds do shake the darling buds of May');
|
|
118
|
+
const sCount = words.filter(w => peakRel(w) === RANK.s).length;
|
|
119
|
+
const contentCount = words.filter(w => w.isContent).length;
|
|
120
|
+
expect(sCount).toBeGreaterThanOrEqual(1); // at least one nuclear beat
|
|
121
|
+
expect(sCount).toBeLessThan(contentCount); // differentiated, not all-s
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('a SECONDARY stress surfaces as n, distinct from unstressed w (accelerated)', () => {
|
|
125
|
+
// "accelerated" = AE0 K S EH1 L ER0 EY2 T IH0 D → lexicalStress [0,2,0,1,0]:
|
|
126
|
+
// primary on "cel", a strong (EY2) secondary on "at". The relativiser must keep
|
|
127
|
+
// BOTH — primary on a beat tier, secondary on 'n' — never collapsing the secondary
|
|
128
|
+
// to the unstressed 'w' run.
|
|
129
|
+
const { words } = analyse('Of its accelerated grimace');
|
|
130
|
+
const acc = byWord(words, 'accelerated')!;
|
|
131
|
+
const rel = acc.syllables.map(s => s.relativeStress);
|
|
132
|
+
const primaryIdx = acc.syllables.findIndex(s => (s.lexicalStress ?? 0) === 2);
|
|
133
|
+
const secondaryIdx = acc.syllables.findIndex(s => (s.lexicalStress ?? 0) === 1);
|
|
134
|
+
expect(RANK[rel[secondaryIdx]!]).toBe(RANK.n); // secondary → n
|
|
135
|
+
expect(RANK[rel[primaryIdx]!]).toBeGreaterThan(RANK[rel[secondaryIdx]!]); // primary > secondary
|
|
136
|
+
// and the secondary must outrank the genuinely unstressed syllables
|
|
137
|
+
const unstressed = acc.syllables.filter(s => (s.lexicalStress ?? 0) === 0);
|
|
138
|
+
expect(unstressed.every(s => RANK[s.relativeStress!] < RANK.n)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('Givenness: an indefinite-pronoun head yields the nuclear + opens a ϕ (Something for the modern stage)', () => {
|
|
142
|
+
// Wagner §6.5.1 + §7.2.3: the post-nominal PP of the LIGHT, inherently-given head
|
|
143
|
+
// "Something" opens its own ϕ (a phrasing break after it), and "Something" keeps
|
|
144
|
+
// its lexical contour (SOME=m) but yields the utterance nuclear to "stage".
|
|
145
|
+
const { words, ius } = analyse('Something for the modern stage');
|
|
146
|
+
const something = byWord(words, 'something')!;
|
|
147
|
+
const stage = byWord(words, 'stage')!;
|
|
148
|
+
const modern = byWord(words, 'modern')!;
|
|
149
|
+
expect(peakRel(stage)).toBe(RANK.s); // stage is the nuclear
|
|
150
|
+
expect(peakRel(something)).toBe(RANK.m); // given head: a beat, not the nuclear, not crushed
|
|
151
|
+
expect(peakRel(modern)).toBeLessThan(peakRel(stage)); // modern < stage (tilted, not flat)
|
|
152
|
+
expect(ius[0].phonologicalPhrases.length).toBeGreaterThanOrEqual(2); // pause after "Something"
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('Negator: "no" keeps a beat, never the reduced determiner tier (with no loss of time)', () => {
|
|
156
|
+
// The negative determiner is a negator (n-tier), not a reducible article (x).
|
|
157
|
+
const { words } = analyse('Made with no loss of time');
|
|
158
|
+
const no = byWord(words, 'no')!;
|
|
159
|
+
expect(peakRel(no)).toBeGreaterThanOrEqual(RANK.n);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('OOV secondary: a genuinely out-of-vocabulary polysyllable gets its rhythmic secondary', () => {
|
|
163
|
+
// The English Stress Rule must not leave a long OOV word with one stress and a flat
|
|
164
|
+
// tail — pretonic alternation gives it a real secondary ('n'-tier) beat.
|
|
165
|
+
const { words } = analyse('Apalachicola flows on');
|
|
166
|
+
const w = byWord(words, 'apalachicola')!;
|
|
167
|
+
const primary = w.syllables.some(s => (s.lexicalStress ?? 0) === 2);
|
|
168
|
+
const secondary = w.syllables.filter(s => (s.lexicalStress ?? 0) === 1).length;
|
|
169
|
+
expect(primary).toBe(true);
|
|
170
|
+
expect(secondary).toBeGreaterThanOrEqual(1); // not a single lone stress
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('clash invariant holds: no two adjacent syllables share n / m / s', () => {
|
|
174
|
+
for (const line of [
|
|
175
|
+
'The woman wore the hat', 'the invisible worm that flies in the night',
|
|
176
|
+
'old and gray and full of sleep', 'Rough winds do shake the darling buds of May',
|
|
177
|
+
]) {
|
|
178
|
+
const { words } = analyse(line);
|
|
179
|
+
const syls = words.flatMap(w => w.syllables.map(s => s.relativeStress ?? 'w'));
|
|
180
|
+
for (let i = 0; i + 1 < syls.length; i++) {
|
|
181
|
+
const a = RANK[syls[i]], b = RANK[syls[i + 1]];
|
|
182
|
+
const bothBeat = a >= RANK.m && b >= RANK.m;
|
|
183
|
+
const bothN = a === RANK.n && b === RANK.n;
|
|
184
|
+
expect(bothBeat || bothN, `adjacent ${syls[i]}${syls[i + 1]} in "${line}"`).toBe(false);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
package/vitest.config.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// vitest.config.ts
|
|
2
|
-
import { defineConfig } from 'vitest/config';
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
test: {
|
|
6
|
-
globals: true,
|
|
7
|
-
environment: 'node',
|
|
8
|
-
include: ['tests/**/*.test.ts'],
|
|
9
|
-
testTimeout: 10000,
|
|
10
|
-
deps: {
|
|
11
|
-
// required for esm packages like finnlp
|
|
12
|
-
interopDefault: true,
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
});
|