calliope-ts 0.0.4 → 0.1.0

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 (183) hide show
  1. package/README.md +69 -3
  2. package/dist/calliope/bracketing.d.ts.map +1 -1
  3. package/dist/calliope/bracketing.js +59 -5
  4. package/dist/calliope/deps.d.ts +5 -1
  5. package/dist/calliope/deps.d.ts.map +1 -1
  6. package/dist/calliope/deps.js +53 -3
  7. package/dist/calliope/engine.d.ts.map +1 -1
  8. package/dist/calliope/engine.js +6 -1
  9. package/dist/calliope/relstress.d.ts.map +1 -1
  10. package/dist/calliope/relstress.js +411 -15
  11. package/dist/calliope/syntax.d.ts +33 -0
  12. package/dist/calliope/syntax.d.ts.map +1 -1
  13. package/dist/calliope/syntax.js +199 -0
  14. package/dist/data/en_common_words.json +1 -0
  15. package/dist/display.d.ts +15 -1
  16. package/dist/display.d.ts.map +1 -1
  17. package/dist/display.js +112 -10
  18. package/dist/fabbhalle.d.ts +54 -0
  19. package/dist/fabbhalle.d.ts.map +1 -0
  20. package/dist/fabbhalle.js +404 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +86 -33
  23. package/dist/phonological.d.ts.map +1 -1
  24. package/dist/phonological.js +7 -1
  25. package/dist/rewriteEn.d.ts +14 -0
  26. package/dist/rewriteEn.d.ts.map +1 -0
  27. package/dist/rewriteEn.js +392 -0
  28. package/dist/rhyme.d.ts +35 -3
  29. package/dist/rhyme.d.ts.map +1 -1
  30. package/dist/rhyme.js +176 -11
  31. package/dist/russian/accentuator.d.ts +31 -0
  32. package/dist/russian/accentuator.d.ts.map +1 -0
  33. package/dist/russian/accentuator.js +546 -0
  34. package/dist/russian/collocations.d.ts +7 -0
  35. package/dist/russian/collocations.d.ts.map +1 -0
  36. package/dist/russian/collocations.js +38 -0
  37. package/dist/russian/compounds.d.ts +6 -0
  38. package/dist/russian/compounds.d.ts.map +1 -0
  39. package/dist/russian/compounds.js +81 -0
  40. package/dist/russian/download.d.ts +23 -0
  41. package/dist/russian/download.d.ts.map +1 -0
  42. package/dist/russian/download.js +117 -0
  43. package/dist/russian/engine.d.ts +6 -0
  44. package/dist/russian/engine.d.ts.map +1 -0
  45. package/dist/russian/engine.js +1424 -0
  46. package/dist/russian/parser.d.ts +8 -0
  47. package/dist/russian/parser.d.ts.map +1 -0
  48. package/dist/russian/parser.js +92 -0
  49. package/dist/russian/paths.d.ts +55 -0
  50. package/dist/russian/paths.d.ts.map +1 -0
  51. package/dist/russian/paths.js +114 -0
  52. package/dist/russian/rewrite.d.ts +6 -0
  53. package/dist/russian/rewrite.d.ts.map +1 -0
  54. package/dist/russian/rewrite.js +400 -0
  55. package/dist/russian/rhyme.d.ts +18 -0
  56. package/dist/russian/rhyme.d.ts.map +1 -0
  57. package/dist/russian/rhyme.js +230 -0
  58. package/dist/russian/syllabifier.d.ts +14 -0
  59. package/dist/russian/syllabifier.d.ts.map +1 -0
  60. package/dist/russian/syllabifier.js +475 -0
  61. package/dist/russian/types.d.ts +94 -0
  62. package/dist/russian/types.d.ts.map +1 -0
  63. package/dist/russian/types.js +27 -0
  64. package/dist/scandroidNative/constants.d.ts +16 -0
  65. package/dist/scandroidNative/constants.d.ts.map +1 -0
  66. package/dist/scandroidNative/constants.js +60 -0
  67. package/dist/scandroidNative/deduceParameters.d.ts +16 -0
  68. package/dist/scandroidNative/deduceParameters.d.ts.map +1 -0
  69. package/dist/scandroidNative/deduceParameters.js +69 -0
  70. package/dist/scandroidNative/dictionary-data.d.ts +2 -0
  71. package/dist/scandroidNative/dictionary-data.d.ts.map +1 -0
  72. package/dist/scandroidNative/dictionary-data.js +744 -0
  73. package/dist/scandroidNative/dictionary.d.ts +12 -0
  74. package/dist/scandroidNative/dictionary.d.ts.map +1 -0
  75. package/dist/scandroidNative/dictionary.js +50 -0
  76. package/dist/scandroidNative/engine.d.ts +41 -0
  77. package/dist/scandroidNative/engine.d.ts.map +1 -0
  78. package/dist/scandroidNative/engine.js +119 -0
  79. package/dist/scandroidNative/machine.d.ts +93 -0
  80. package/dist/scandroidNative/machine.d.ts.map +1 -0
  81. package/dist/scandroidNative/machine.js +795 -0
  82. package/dist/scandroidNative/positioner.d.ts +32 -0
  83. package/dist/scandroidNative/positioner.d.ts.map +1 -0
  84. package/dist/scandroidNative/positioner.js +208 -0
  85. package/dist/scandroidNative/syllabizer.d.ts +3 -0
  86. package/dist/scandroidNative/syllabizer.d.ts.map +1 -0
  87. package/dist/scandroidNative/syllabizer.js +215 -0
  88. package/dist/scandroidNative/utilities.d.ts +37 -0
  89. package/dist/scandroidNative/utilities.d.ts.map +1 -0
  90. package/dist/scandroidNative/utilities.js +86 -0
  91. package/dist/scansion.d.ts.map +1 -1
  92. package/dist/scansion.js +118 -14
  93. package/dist/stress.d.ts +1 -0
  94. package/dist/stress.d.ts.map +1 -1
  95. package/dist/stress.js +502 -42
  96. package/dist/types.d.ts +3 -0
  97. package/dist/types.d.ts.map +1 -1
  98. package/package.json +27 -6
  99. package/tools/fetch-russian-data.mjs +30 -0
  100. package/webapp/public/app.js +2418 -0
  101. package/webapp/public/assets/GithubLogo.png +0 -0
  102. package/webapp/public/assets/NPM2.png +0 -0
  103. package/webapp/public/assets/fonts/MurenaBold-webfont.woff2 +0 -0
  104. package/webapp/public/assets/fonts/MurenaItalic-webfont.woff2 +0 -0
  105. package/webapp/public/assets/fonts/MurenaRegular-webfont.woff2 +0 -0
  106. package/webapp/public/assets/fonts/Myra_4F_Caps_Bold-webfont.woff2 +0 -0
  107. package/webapp/public/assets/fonts/SinkinSans-700Bold.otf +0 -0
  108. package/webapp/public/assets/fonts/butterflies.ttf +0 -0
  109. package/webapp/public/assets/fonts/midcase_blackline-webfont.woff2 +0 -0
  110. package/webapp/public/assets/fonts/neueral-bold-webfont.woff2 +0 -0
  111. package/webapp/public/assets/fonts/neueral-regular-webfont.woff2 +0 -0
  112. package/webapp/public/assets/fonts/popelka-webfont.woff2 +0 -0
  113. package/webapp/public/assets/fonts/vremenagrotesk-webfont.woff +0 -0
  114. package/webapp/public/assets/fonts/wicky_javick_bold-webfont.woff2 +0 -0
  115. package/webapp/public/assets/logos/Logo_Var_10_Celtic_Knot_SVG.svg +116 -0
  116. package/webapp/public/assets/logos/Logo_Var_11_Graph_SVG.svg +313 -0
  117. package/webapp/public/assets/logos/Logo_Var_12_Limacon_Knot_SVG.svg +3942 -0
  118. package/webapp/public/assets/logos/Logo_Var_13_Crescents_SVG.svg +1030 -0
  119. package/webapp/public/assets/logos/Logo_Var_1_Spiral_SVG.svg +1491 -0
  120. package/webapp/public/assets/logos/Logo_Var_2_Tunnel_SVG.svg +12 -0
  121. package/webapp/public/assets/logos/Logo_Var_3_Triskelion_SVG.svg +10 -0
  122. package/webapp/public/assets/logos/Logo_Var_4_Quinquetra_PNG.png +0 -0
  123. package/webapp/public/assets/logos/Logo_Var_5_Penrose_Pentagram_SVG.svg +37 -0
  124. package/webapp/public/assets/logos/Logo_Var_6_Triskelion2_SVG.svg +10 -0
  125. package/webapp/public/assets/logos/Logo_Var_7_SixfoldSpirals_SVG.svg +20 -0
  126. package/webapp/public/assets/logos/Logo_Var_8_Radial_Labyrinth_SVG.svg +163 -0
  127. package/webapp/public/assets/logos/Logo_Var_9_Radial_Labyrinth_2_SVG.svg +106 -0
  128. package/webapp/public/index.html +316 -0
  129. package/webapp/public/styles.css +1114 -0
  130. package/webapp/server.mjs +680 -0
  131. package/src/caesura.ts +0 -201
  132. package/src/calliope/boundaries.ts +0 -190
  133. package/src/calliope/bracketing.ts +0 -390
  134. package/src/calliope/deps.ts +0 -160
  135. package/src/calliope/engine.ts +0 -77
  136. package/src/calliope/feats.ts +0 -46
  137. package/src/calliope/names.ts +0 -44
  138. package/src/calliope/postag.ts +0 -253
  139. package/src/calliope/prosodic.ts +0 -262
  140. package/src/calliope/relstress.ts +0 -645
  141. package/src/calliope/stressrules.ts +0 -147
  142. package/src/calliope/syntax.ts +0 -218
  143. package/src/calliope/udpos.ts +0 -152
  144. package/src/calliope_src_contents.md +0 -19049
  145. package/src/clio/caesura.ts +0 -145
  146. package/src/clio/depfix.ts +0 -88
  147. package/src/clio/display.ts +0 -1042
  148. package/src/clio/engine.ts +0 -38
  149. package/src/clio/parser.ts +0 -845
  150. package/src/clio/phonological.ts +0 -849
  151. package/src/clio/phrasestress.ts +0 -108
  152. package/src/clio/pipeline.ts +0 -154
  153. package/src/clio/rhyme.ts +0 -740
  154. package/src/clio/scandroid.ts +0 -434
  155. package/src/clio/scansion.ts +0 -1130
  156. package/src/clio/semantics.ts +0 -134
  157. package/src/clio/stress.ts +0 -1731
  158. package/src/clio/tagfix.ts +0 -104
  159. package/src/depfix.ts +0 -88
  160. package/src/display.ts +0 -1138
  161. package/src/engine.ts +0 -22
  162. package/src/index.ts +0 -690
  163. package/src/parser.ts +0 -501
  164. package/src/phonological.ts +0 -917
  165. package/src/phrasestress.ts +0 -108
  166. package/src/rhyme.ts +0 -748
  167. package/src/scandroid.ts +0 -434
  168. package/src/scansion.ts +0 -1174
  169. package/src/semantics.ts +0 -134
  170. package/src/stress.ts +0 -2111
  171. package/src/tagfix.ts +0 -104
  172. package/src/types.ts +0 -265
  173. package/tests/DataForHayesLinesOnly.txt +0 -364
  174. package/tests/DataForHayesStressSymbolsRevised.txt +0 -728
  175. package/tests/basic.test.ts +0 -1102
  176. package/tests/bench-hayes.mjs +0 -72
  177. package/tests/epg64.meter.train.txt +0 -8139
  178. package/tests/litlab-sample-2016.txt +0 -1738
  179. package/tests/prosodic.meter.train.txt +0 -13192
  180. package/tests/wagner-stress.test.ts +0 -188
  181. package/tsconfig.json +0 -29
  182. package/vitest.config.d.ts +0 -2
  183. package/vitest.config.js +0 -14
package/dist/display.d.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  import { ClsWord, LineResult } from './types.js';
2
- export declare function renderUnifiedDisplay(result: LineResult, rawLine?: string): string;
2
+ import { type FHSyllable } from './fabbhalle.js';
3
+ /** The Fabb–Halle projection of a line — one FHSyllable per syllable of every
4
+ * non-punctuation word, carrying LEXICAL stress only. Exported so the CLI
5
+ * can run F&H's poem-level rule-set discovery over a whole document before
6
+ * rendering (only lex/poly matter for discovery; text is cosmetic). */
7
+ export declare function fabbHalleSyllablesOf(result: LineResult): FHSyllable[];
8
+ export interface RenderOptions {
9
+ /** Rule set discovered by F&H's own poem-level grid construction
10
+ * (buildFabbHalleGridsForPoem over the whole document). When present,
11
+ * the F&H section scans under it and reports agreement with the engine's
12
+ * verdict; when absent, the section falls back to stipulating the
13
+ * engine's meter (legacy single-line behaviour). */
14
+ fhSchema?: string | null;
15
+ }
16
+ export declare function renderUnifiedDisplay(result: LineResult, rawLine?: string, opts?: RenderOptions): string;
3
17
  export declare function renderLegend(): string;
4
18
  /**
5
19
  * The long-form legend triggered from the main menu's "Display Legend" option:
@@ -1 +1 @@
1
- {"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;AAgNpB,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAySjF;AAMD,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAuHD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsBzC;AAqCD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAuC/E;AA2OD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CA0FlE"}
1
+ {"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA8MrE;;;wEAGwE;AACxE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,EAAE,CAerE;AAED,MAAM,WAAW,aAAa;IAC5B;;;;yDAIqD;IACrD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAwXvG;AAMD,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAuHD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsBzC;AAqCD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAuC/E;AA2OD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CA0FlE"}
package/dist/display.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import chalk from 'chalk';
4
4
  import { isPunctuation } from './parser.js';
5
5
  import { syllabifyWord, syllableVowelLengths } from './phonological.js';
6
+ import { buildFabbHalleGrid } from './fabbhalle.js';
6
7
  import { computeCaesurae } from './caesura.js';
7
8
  import { computeBoundaries } from './calliope/boundaries.js';
8
9
  import { summarizePoem, analyzePhonopoetics } from './rhyme.js';
@@ -171,7 +172,28 @@ function buildColSyls(words, ius) {
171
172
  // ═══════════════════════════════════════════════════════════════════════
172
173
  // UNIFIED DISPLAY — All layers integrated
173
174
  // ═══════════════════════════════════════════════════════════════════════
174
- export function renderUnifiedDisplay(result, rawLine) {
175
+ /** The Fabb–Halle projection of a line — one FHSyllable per syllable of every
176
+ * non-punctuation word, carrying LEXICAL stress only. Exported so the CLI
177
+ * can run F&H's poem-level rule-set discovery over a whole document before
178
+ * rendering (only lex/poly matter for discovery; text is cosmetic). */
179
+ export function fabbHalleSyllablesOf(result) {
180
+ const out = [];
181
+ for (const w of result.sentence.words) {
182
+ if (isPunctuation(w.lexicalClass))
183
+ continue;
184
+ const poly = w.syllables.length > 1;
185
+ // Monosyllabic FUNCTION words are clitics with no lexical stress in
186
+ // F&H's sense (condition (16): a maximum's neighbours must "not carry
187
+ // lexical stress" — "were", "in", "like" never block a maximum, while
188
+ // content monosyllables like "wolf" legitimately do).
189
+ const cliticMono = !poly && !w.isContent;
190
+ for (const s of w.syllables) {
191
+ out.push({ text: s.text, lex: cliticMono ? 0 : (s.lexicalStress ?? s.stress ?? 0), poly });
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+ export function renderUnifiedDisplay(result, rawLine, opts) {
175
197
  const words = result.sentence.words;
176
198
  const ius = result.phonologicalHierarchy;
177
199
  const detail = result.phonologicalScansion;
@@ -445,18 +467,98 @@ export function renderUnifiedDisplay(result, rawLine) {
445
467
  lines.push(' ' + H2('Scansion: ') + detail.scansion);
446
468
  lines.push(' ' + H2('Summary: ') + detail.summary);
447
469
  lines.push('');
448
- // ── Layer 9: Scandroid comparison (if available) ────────────────
449
- if (result.scandroidCorral || result.scandroidMaximise) {
450
- lines.push(H1('Scandroid Comparison'));
470
+ // ── Layer 9: Scandroid Hartman's own, fully independent second opinion ──
471
+ // Its own dictionary, its own syllabifier, its own algorithms; reads only
472
+ // the raw line text. Never touches or is touched by anything above.
473
+ if (result.scandroidNative) {
474
+ const sn = result.scandroidNative;
475
+ lines.push(H1('Scandroid (independent)'));
451
476
  lines.push('');
452
- if (result.scandroidCorral) {
453
- lines.push(' ' + H2('CW: ') + result.scandroidCorral.scansion);
477
+ const lengthTxt = sn.lineFeetSet ? sn.lineLengthName : `${sn.lineLengthName} (variable length)`;
478
+ lines.push(' ' + H2('Metron: ') + `${sn.metronName} ${lengthTxt}`);
479
+ if (sn.verdict) {
480
+ const failTxt = sn.verdict.ok ? '' : chalk.red(' FAIL' + (sn.verdict.failReason ? ` (${sn.verdict.failReason})` : ''));
481
+ lines.push(' ' + H2('Verdict: ') + `${sn.verdict.algorithm} — ${sn.verdict.scanString}` + failTxt);
482
+ lines.push(' ' + H2('Marks: ') + sn.verdict.marksString + ' ' + chalk.dim(`${sn.verdict.substitutions} substitution(s)`));
454
483
  }
455
- if (result.scandroidMaximise) {
456
- lines.push(' ' + H2('MN: ') + result.scandroidMaximise.scansion);
484
+ else {
485
+ lines.push(' ' + H2('Verdict: ') + chalk.red('FAIL (no scannable resolution)'));
457
486
  }
487
+ if (sn.corralTheWeird)
488
+ lines.push(' ' + H2('CW: ') + sn.corralTheWeird.scanString + (sn.corralTheWeird.ok ? '' : chalk.red(' FAIL')));
489
+ if (sn.maximizeTheNormal)
490
+ lines.push(' ' + H2('MN: ') + sn.maximizeTheNormal.scanString + (sn.maximizeTheNormal.ok ? '' : chalk.red(' FAIL')));
458
491
  lines.push('');
459
492
  }
493
+ // ── Layer 9b: Fabb–Halle bracketed grid (independent second opinion) ──
494
+ // When the caller supplies opts.fhSchema (the rule set F&H's OWN poem-level
495
+ // grid construction discovered — see buildFabbHalleGridsForPoem), the line
496
+ // is scanned under that; otherwise it falls back to stipulating the
497
+ // engine's final meter name. Never fed back into the main scansion.
498
+ {
499
+ const fhSyls = linearSyls.map(ls => ({
500
+ text: ls.chunk,
501
+ lex: 0,
502
+ poly: ls.wordRef.syllables.length > 1,
503
+ }));
504
+ // Lexical stresses assigned positionally, walking words in surface order
505
+ // (mirrors how linearSyls itself was built). Monosyllabic function
506
+ // words stay lex 0 — clitics carry no lexical stress in F&H's sense.
507
+ {
508
+ let k = 0;
509
+ for (const w of words) {
510
+ if (isPunctuation(w.lexicalClass))
511
+ continue;
512
+ const cliticMono = w.syllables.length === 1 && !w.isContent;
513
+ for (const s of w.syllables) {
514
+ if (k < fhSyls.length)
515
+ fhSyls[k].lex = cliticMono ? 0 : (s.lexicalStress ?? s.stress ?? 0);
516
+ k++;
517
+ }
518
+ }
519
+ }
520
+ let fh;
521
+ let discoveredNote = '';
522
+ if (opts?.fhSchema) {
523
+ const size = opts.fhSchema === 'loose' ? 0
524
+ : opts.fhSchema === 'iambic' || opts.fhSchema === 'trochaic' ? 2 : 3;
525
+ fh = buildFabbHalleGrid(fhSyls, opts.fhSchema, size > 0 ? Math.floor(fhSyls.length / size) : undefined);
526
+ const engineMeter = detail.meter || '';
527
+ const agrees = engineMeter.includes(opts.fhSchema)
528
+ || (opts.fhSchema === 'loose' && /accentual|loose|dolnik|free/i.test(engineMeter));
529
+ discoveredNote = agrees
530
+ ? ' · poem-discovered by grid construction, agreeing with the engine'
531
+ : ` · poem-discovered by grid construction (the engine read ${engineMeter || 'nothing'})`;
532
+ }
533
+ else {
534
+ fh = buildFabbHalleGrid(fhSyls, detail.meter || '', detail.footCount);
535
+ }
536
+ if (fh) {
537
+ lines.push(H1('Fabb–Halle Grid') + chalk.dim(' — bracketed-grid scansion (Meter in Poetry, 2008); * projects, ( groups rightward, ) leftward'));
538
+ lines.push('');
539
+ lines.push(' ' + chalk.dim(fh.ruleLabel + discoveredNote));
540
+ const maximaSet = new Set(fh.maxima);
541
+ const violSet = new Set(fh.violations);
542
+ const colW = fhSyls.map((s, i) => Math.max(s.text.length, ...fh.rows.map(r => (r[i] || '').length)) + 1);
543
+ const pad = (t, wd) => t + ' '.repeat(Math.max(0, wd - t.length));
544
+ lines.push(' ' + chalk.dim('Syl: ') + fhSyls.map((s, i) => {
545
+ const t = pad(s.text, colW[i]);
546
+ return violSet.has(i) ? chalk.red(t) : maximaSet.has(i) ? chalk.yellow(t) : t;
547
+ }).join(''));
548
+ fh.rows.forEach((row, g) => {
549
+ lines.push(' ' + chalk.dim(`G${g}: `) + row.map((m, i) => pad(m, colW[i])).join(''));
550
+ });
551
+ const verdict = fh.looseFeet != null
552
+ ? `${fh.looseFeet} feet — in a loose meter the foot count IS the meter; maxima anchor the feet, unfooted syllables are free`
553
+ : fh.maxima.length === 0
554
+ ? 'no maxima in this line — vacuously metrical'
555
+ : fh.violations.length === 0
556
+ ? `${fh.maxima.length} maxim${fh.maxima.length === 1 ? 'um' : 'a'}, all project to gridline 1 → metrical`
557
+ : `${fh.violations.length} of ${fh.maxima.length} maxima fail to project → unmetrical under these rules`;
558
+ lines.push(' ' + (fh.metrical ? chalk.green(verdict) : chalk.red(verdict)));
559
+ lines.push('');
560
+ }
561
+ }
460
562
  // ── Layer 10: Reading projection (stress gradient over the input) ──
461
563
  // A reading-view-style colourisation of the verbatim input, so the finalised
462
564
  // analysis always shows "something that looks like the input". Falls back to
@@ -657,7 +759,7 @@ function renderLineGlossary(words) {
657
759
  }
658
760
  /** Surface form reduced to bare lowercase letters (drops apostrophes/hyphens). */
659
761
  function normWordForm(s) {
660
- return s.toLowerCase().replace(/[^a-z]/g, '');
762
+ return s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z]/g, '');
661
763
  }
662
764
  /** Colour each orthographic syllable of an original token by its relative stress. */
663
765
  function colourToken(tokenText, word) {
@@ -702,7 +804,7 @@ function colourToken(tokenText, word) {
702
804
  * the rest of the line. No original character is ever dropped.
703
805
  */
704
806
  export function projectStressOntoLine(rawLine, words) {
705
- const tokenRe = /[A-Za-z]+(?:['’\-][A-Za-z]+)*/g;
807
+ const tokenRe = /[A-Za-z\u00C0-\u024F]+(?:['’\-][A-Za-z\u00C0-\u024F]+)*/g; // accented Latin included (Milésien)
706
808
  let out = '';
707
809
  let cursor = 0;
708
810
  let wi = 0;
@@ -0,0 +1,54 @@
1
+ export interface FHSyllable {
2
+ text: string;
3
+ lex: number;
4
+ poly: boolean;
5
+ }
6
+ export interface FabbHalleResult {
7
+ ruleLabel: string;
8
+ rows: string[][];
9
+ maxima: number[];
10
+ violations: number[];
11
+ metrical: boolean;
12
+ complete: boolean;
13
+ looseFeet?: number;
14
+ }
15
+ /** How maxima are defined. 'flanked' is the English strict-meter condition
16
+ * (F&H 2006 (16)): primary stress in a polysyllable, preceded AND followed
17
+ * in the line by syllables without lexical stress — line-edge syllables
18
+ * never qualify. 'polysyllabic' drops the flanking clause: any polysyllable
19
+ * primary stress is a maximum. That is the correct parameter for RUSSIAN
20
+ * strict meters, where the empirical law (Taranovsky/Jakobson; F&H treat
21
+ * Russian with the same parametric latitude their fn. 12 grants) is that a
22
+ * polysyllabic word's stress may fall only on an ictus, with no adjacency
23
+ * escape hatch — Russian classical verse simply does not license the
24
+ * clash-adjacent inversions English does. */
25
+ export type MaximaMode = 'flanked' | 'polysyllabic';
26
+ export interface FabbHalleOptions {
27
+ maximaMode?: MaximaMode;
28
+ }
29
+ /**
30
+ * Build the bracketed grid for one line. `meterName` selects the rule set —
31
+ * a single derivation VERIFIES a line against a stipulated rule set; meter
32
+ * DISCOVERY is a poem-level matter (see buildFabbHalleGridsForPoem below).
33
+ * `footCount`, when the caller supplies it, fixes the expected G0 count so
34
+ * that OVERLONG lines exercise
35
+ * F&H's optional NON-PROJECTION (their underlined syllables: synaloepha,
36
+ * "Heav'n"-type syncope, feminine endings — §1.1 example (2)). Only
37
+ * lexically stressless syllables may fail to project; the choice minimizes
38
+ * maximum-condition violations, ties resolving to the EARLIEST position
39
+ * (King James VI's observation that projection is likelier near the line's
40
+ * end). ACCENTUAL / dol'nik verdicts route to the LOOSE-meter rules
41
+ * instead. Returns null when the line's meter has no F&H rule set.
42
+ */
43
+ export declare function buildFabbHalleGrid(syls: FHSyllable[], meterName: string, footCount?: number, options?: FabbHalleOptions): FabbHalleResult | null;
44
+ export interface FabbHallePoemResult {
45
+ schema: string;
46
+ grids: (FabbHalleResult | null)[];
47
+ cost: number;
48
+ runnerUp?: {
49
+ schema: string;
50
+ cost: number;
51
+ };
52
+ }
53
+ export declare function buildFabbHalleGridsForPoem(lines: FHSyllable[][], options?: FabbHalleOptions): FabbHallePoemResult | null;
54
+ //# sourceMappingURL=fabbhalle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fabbhalle.d.ts","sourceRoot":"","sources":["../src/fabbhalle.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0FD;;;;;;;;;8CAS8C;AAC9C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,cAAc,CAAC;AAEpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AA6FD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GACpF,eAAe,GAAG,IAAI,CAkGxB;AAoBD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,eAAe,GAAG,IAAI,CAAC,EAAE,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAiBD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAChD,mBAAmB,GAAG,IAAI,CAiC5B"}
@@ -0,0 +1,404 @@
1
+ // fabbhalle.ts — Fabb & Halle (2008, *Meter in Poetry*) Bracketed-Grid
2
+ // scansion: an INDEPENDENT second-opinion module beside Scandroid.
3
+ //
4
+ // F&H hold that the fundamental principle of metrical verse is COUNTING, not
5
+ // rhythm: each syllable projects an asterisk on gridline 0; iterative rules
6
+ // insert parentheses (a left paren groups rightward, a right paren leftward),
7
+ // each group projects its HEAD (edgemost asterisk) to the next gridline, and
8
+ // a well-formed grid terminates with EXACTLY ONE asterisk on the bottom
9
+ // gridline (their condition (11)). Rhythm falls out as a condition stated
10
+ // over the grid — for English strict meters, condition (7)/(8):
11
+ //
12
+ // A MAXIMUM — a syllable carrying primary stress in a polysyllabic word,
13
+ // preceded AND followed in the same line by syllables without lexical
14
+ // stress — must project to gridline 1.
15
+ //
16
+ // Monosyllables are unregulated (their placement is a *tendency*, which Fabb
17
+ // 2004 argues is implied form, explained pragmatically — not constitutive
18
+ // form, not part of the generative rules). This is deliberately a far more
19
+ // LINEAR and textocentric method than the Calliope pipeline (McAleese's
20
+ // critique); it is offered as an extra signal, never feeding the main
21
+ // scansion.
22
+ //
23
+ // Rule sets follow the worked examples: iambic pentameter (F&H 2006 (5)-(14):
24
+ // G0 R→L binary heads-right; G1 R→L ternary, final metron binary; all higher
25
+ // gridlines likewise R→L — rules (11) and (13) both begin "at the right
26
+ // edge", and the head is always the edgemost asterisk in the direction of
27
+ // iteration), trochaic tetrameter (Keats 'Fancy': all gridlines L→R binary,
28
+ // heads left), anapestic tetrameter (Byron 'Sennacherib': G0 R→L ternary
29
+ // heads-right, initial group may be incomplete). Amphibrachic lines fall
30
+ // out of the ANAPESTIC rules for free: R→L ternary grouping of an
31
+ // 11-syllable amphibrachic tetrameter yields groups |2|3|3|3| whose right
32
+ // heads sit at positions 2·5·8·11 — exactly the amphibrach's medial beats
33
+ // (F&H have no medial-headed groups; none are needed).
34
+ /** Group the asterisks at `cols` per `spec`; return groups in left-to-right
35
+ * order, each an array of member columns. The group at the far end of the
36
+ * iteration may be incomplete (F&H allow non-maximal edge groups). */
37
+ function group(cols, spec) {
38
+ const groups = [];
39
+ if (spec.dir === 'RL') {
40
+ for (let end = cols.length; end > 0; end -= spec.size) {
41
+ const start = Math.max(0, end - spec.size);
42
+ groups.unshift(cols.slice(start, end));
43
+ }
44
+ }
45
+ else {
46
+ for (let start = 0; start < cols.length; start += spec.size) {
47
+ groups.push(cols.slice(start, Math.min(cols.length, start + spec.size)));
48
+ }
49
+ }
50
+ return groups;
51
+ }
52
+ /** Head column of a group: rightmost for RL ')' groups, leftmost for LR '(' . */
53
+ function headOf(g, spec) {
54
+ return spec.dir === 'RL' ? g[g.length - 1] : g[0];
55
+ }
56
+ /** Pick the grouping rule for a HIGHER gridline holding `n` asterisks,
57
+ * following the paper's worked examples where they exist. Pentameter (F&H
58
+ * 2006 rule (11)/(12)): G1 ternary R→L with the final (last-constructed,
59
+ * leftmost) metron binary. Trimeter ("Up-Hill" (33), "Yes, to the very
60
+ * end"): G1 BINARY — `*) * *)`, two metra {1},{2,3} — NOT one ternary
61
+ * group. Hexameter (fn. 9): two full ternary metra. Tetrameter: binary.
62
+ * Direction: every gridline of a meter iterates the same way as its G0 —
63
+ * rules (11) and (13) both begin "at the right edge" for iambic, and the
64
+ * head is the edgemost asterisk in the direction of iteration ("the
65
+ * rightmost asterisk in each group projects"), so heads-right throughout
66
+ * R→L meters and, mirror-image, heads-left throughout L→R meters. */
67
+ function higherSpec(n, g0dir) {
68
+ const dir = g0dir;
69
+ if (n <= 4)
70
+ return { dir, size: 2 }; // incl. n=3 → {1},{2,3} per (33)
71
+ if (n % 3 === 0)
72
+ return { dir, size: 3 };
73
+ if (n % 2 === 0)
74
+ return { dir, size: 2 };
75
+ return { dir, size: 3 }; // odd ≥ 5 → ternary w/ final binary
76
+ }
77
+ /** G0 rule set from the main engine's meter family name. Returns null for
78
+ * meters outside F&H's English inventory (accentual, free…). */
79
+ function g0Spec(meterName) {
80
+ const m = meterName.toLowerCase();
81
+ if (m.includes('iamb'))
82
+ return { spec: { dir: 'RL', size: 2 }, label: 'iambic: G0 Right-to-Left binary, heads right' };
83
+ if (m.includes('troch'))
84
+ return { spec: { dir: 'LR', size: 2 }, label: 'trochaic: G0 Left-to-Right binary, heads left' };
85
+ if (m.includes('amph'))
86
+ return { spec: { dir: 'LR', size: 3 }, label: 'amphibrachic: G0 Left-to-Right ternary, heads left (initial ∆ unprojected)', dropFirst: true };
87
+ if (m.includes('anap'))
88
+ return { spec: { dir: 'RL', size: 3 }, label: 'anapestic: G0 Right-to-Left ternary, heads right' };
89
+ if (m.includes('dact'))
90
+ return { spec: { dir: 'LR', size: 3 }, label: 'dactylic: G0 Left-to-Right ternary, heads left' };
91
+ return null;
92
+ }
93
+ /** Construct rows + G0 head set for a given projection (the columns that
94
+ * project to gridline 0), leaving unprojected columns blank on every row. */
95
+ function construct(syls, projected, g0) {
96
+ const rows = [];
97
+ let cols = projected.slice();
98
+ let g1cols = new Set();
99
+ let spec = g0.spec;
100
+ for (let g = 0; g < 4 && cols.length > 1; g++) {
101
+ if (g > 0)
102
+ spec = higherSpec(cols.length, g0.spec.dir);
103
+ const groups = group(cols, spec);
104
+ const row = syls.map(() => '');
105
+ for (const c of cols)
106
+ row[c] = '*';
107
+ for (const grp of groups) {
108
+ if (spec.dir === 'RL')
109
+ row[grp[grp.length - 1]] = '*)';
110
+ else
111
+ row[grp[0]] = '(*';
112
+ }
113
+ rows.push(row);
114
+ const heads = groups.map(grp => headOf(grp, spec));
115
+ if (g === 0)
116
+ g1cols = new Set(heads);
117
+ cols = heads;
118
+ }
119
+ const bottom = syls.map(() => '');
120
+ if (cols.length === 1)
121
+ bottom[cols[0]] = '*';
122
+ rows.push(bottom);
123
+ return { rows, g1cols, complete: cols.length === 1 };
124
+ }
125
+ /** Maxima per condition (8)/(16), or the flanking-free Russian variant. */
126
+ function findMaxima(syls, mode) {
127
+ const maxima = [];
128
+ if (mode === 'polysyllabic') {
129
+ for (let i = 0; i < syls.length; i++) {
130
+ if (syls[i].poly && syls[i].lex >= 2)
131
+ maxima.push(i);
132
+ }
133
+ return maxima;
134
+ }
135
+ for (let i = 1; i < syls.length - 1; i++) {
136
+ if (!syls[i].poly || syls[i].lex < 2)
137
+ continue;
138
+ if (syls[i - 1].lex === 0 && syls[i + 1].lex === 0)
139
+ maxima.push(i);
140
+ }
141
+ return maxima;
142
+ }
143
+ /**
144
+ * LOOSE iambic meter (F&H 2006 §3, "Spring Quiet"): in loose meters maxima
145
+ * DETERMINE grid construction rather than being checked against it.
146
+ * (21) maximum = any syllable carrying primary stress in a polysyllable
147
+ * (no flanking condition);
148
+ * (22) insert a right parenthesis after each maximum's asterisk, FIRST;
149
+ * (23) then iterate R→L: skip one asterisk, insert a LEFT parenthesis to
150
+ * the left of the next — but an already-parenthesized asterisk
151
+ * refuses the insertion and the procedure reverts to the skip step
152
+ * (the (27) walkthrough: "Arching high over" → *) * (* *) *).
153
+ * Feet are asterisk runs preceded by "(" or followed by ")" (definition (6),
154
+ * Idsardi's one-boundary feet); syllables between feet stay UNFOOTED (the
155
+ * "-ching" of "Arching"). Line length = FEET, so 4–7-syllable lines all
156
+ * count as dimeter — the meter of folk verse, ballads, dol'nik. Heads
157
+ * project right. Condition (17) does not apply: metricality is the foot
158
+ * count itself.
159
+ */
160
+ function buildLooseGrid(syls) {
161
+ const n = syls.length;
162
+ const rightP = new Array(n).fill(false);
163
+ const leftP = new Array(n).fill(false);
164
+ const maxima = [];
165
+ for (let i = 0; i < n; i++) {
166
+ if (syls[i].poly && syls[i].lex >= 2) {
167
+ maxima.push(i);
168
+ rightP[i] = true;
169
+ }
170
+ }
171
+ // Iterative footing (23): skip-first, R→L.
172
+ let p = n - 1;
173
+ let state = 'skip';
174
+ while (p >= 0) {
175
+ if (state === 'skip') {
176
+ p--;
177
+ state = 'insert';
178
+ continue;
179
+ }
180
+ if (rightP[p] || leftP[p]) {
181
+ state = 'skip';
182
+ continue;
183
+ } // insertion refused
184
+ leftP[p] = true;
185
+ p--;
186
+ state = 'skip';
187
+ }
188
+ // Feet: "(…" runs and bare "…)" singletons; unfooted syllables in neither.
189
+ const feet = [];
190
+ let open = null;
191
+ for (let i = 0; i < n; i++) {
192
+ if (leftP[i]) {
193
+ if (open)
194
+ feet.push(open);
195
+ open = [i];
196
+ }
197
+ else if (open) {
198
+ open.push(i);
199
+ }
200
+ if (rightP[i]) {
201
+ if (open) {
202
+ feet.push(open);
203
+ open = null;
204
+ }
205
+ else
206
+ feet.push([i]);
207
+ }
208
+ }
209
+ if (open)
210
+ feet.push(open);
211
+ // Display: G0 marks + G1 heads (rightmost per foot) — the loose grid stops
212
+ // at the foot count, which IS the meter (F&H display no deeper gridlines
213
+ // for Spring Quiet).
214
+ const rows = [];
215
+ rows.push(syls.map((_, i) => {
216
+ if (leftP[i] && rightP[i])
217
+ return '(*)';
218
+ if (leftP[i])
219
+ return '(*';
220
+ if (rightP[i])
221
+ return '*)';
222
+ return '*';
223
+ }));
224
+ const g1 = syls.map(() => '');
225
+ for (const f of feet)
226
+ g1[f[f.length - 1]] = '*';
227
+ rows.push(g1);
228
+ return {
229
+ ruleLabel: `loose iambic (maxima-anchored, F&H 2006 §3): pre-paren maxima, then Right-to-Left skip-first left parens — ${feet.length} feet`,
230
+ rows,
231
+ maxima,
232
+ violations: [],
233
+ metrical: true,
234
+ complete: true,
235
+ looseFeet: feet.length,
236
+ };
237
+ }
238
+ /**
239
+ * Build the bracketed grid for one line. `meterName` selects the rule set —
240
+ * a single derivation VERIFIES a line against a stipulated rule set; meter
241
+ * DISCOVERY is a poem-level matter (see buildFabbHalleGridsForPoem below).
242
+ * `footCount`, when the caller supplies it, fixes the expected G0 count so
243
+ * that OVERLONG lines exercise
244
+ * F&H's optional NON-PROJECTION (their underlined syllables: synaloepha,
245
+ * "Heav'n"-type syncope, feminine endings — §1.1 example (2)). Only
246
+ * lexically stressless syllables may fail to project; the choice minimizes
247
+ * maximum-condition violations, ties resolving to the EARLIEST position
248
+ * (King James VI's observation that projection is likelier near the line's
249
+ * end). ACCENTUAL / dol'nik verdicts route to the LOOSE-meter rules
250
+ * instead. Returns null when the line's meter has no F&H rule set.
251
+ */
252
+ export function buildFabbHalleGrid(syls, meterName, footCount, options) {
253
+ if (syls.length < 2)
254
+ return null;
255
+ if (meterName === 'auto') {
256
+ // Fallback only — F&H's parse is relative to a STIPULATED meter, so
257
+ // callers that know the line's meter must pass it. Kept for lines whose
258
+ // verdict names no meter at all.
259
+ const schemas = ['iambic', 'trochaic', 'dactylic', 'amphibrachic', 'anapestic', 'loose'];
260
+ let bestRes = null;
261
+ let bestScore = -Infinity;
262
+ for (const schema of schemas) {
263
+ const res = buildFabbHalleGrid(syls, schema, footCount, options);
264
+ if (!res)
265
+ continue;
266
+ // Score: heavily penalize violations; loose meter gets a constant penalty so strict meters win if perfect.
267
+ let score = 0;
268
+ if (schema === 'loose') {
269
+ score = -5; // Loose meter base penalty
270
+ }
271
+ else {
272
+ score -= res.violations.length * 10;
273
+ if (!res.metrical)
274
+ score -= 100;
275
+ }
276
+ if (score > bestScore) {
277
+ bestScore = score;
278
+ bestRes = res;
279
+ }
280
+ }
281
+ return bestRes;
282
+ }
283
+ if (/accentual|dolnik|dol'nik|loose/i.test(meterName))
284
+ return buildLooseGrid(syls);
285
+ const g0 = g0Spec(meterName);
286
+ if (!g0)
287
+ return null;
288
+ const maxima = findMaxima(syls, options?.maximaMode ?? 'flanked');
289
+ const all = syls.map((_, i) => i);
290
+ if (g0.dropFirst && all.length > 0) {
291
+ all.shift();
292
+ }
293
+ // Candidate projections:
294
+ // F&H allow dropping syllables that don't project to G0 (e.g. feminine endings, synaloepha).
295
+ // If footCount is provided, we use it to calculate exactly how many to drop.
296
+ // Otherwise, we guess by trying to drop up to 2 final stressless syllables.
297
+ let candidates = [all];
298
+ const expected = footCount && footCount > 0 ? footCount * g0.spec.size : 0;
299
+ if (expected > 0) {
300
+ const extra = all.length - expected;
301
+ if (extra === 1 || extra === 2) {
302
+ const droppable = all.filter(i => syls[i].lex === 0);
303
+ const sets = [];
304
+ if (extra === 1) {
305
+ for (const d of droppable)
306
+ sets.push(all.filter(i => i !== d));
307
+ }
308
+ else {
309
+ for (let a = 0; a < droppable.length; a++) {
310
+ for (let b = a + 1; b < droppable.length; b++) {
311
+ sets.push(all.filter(i => i !== droppable[a] && i !== droppable[b]));
312
+ }
313
+ }
314
+ }
315
+ // The FULL projection stays a candidate alongside the drop sets: a
316
+ // "long" line may equally be a CATALECTIC line of the next foot count
317
+ // (amphibrachic 11 = 4 feet with a short final group, not 3 feet + 2
318
+ // extrametricals), and F&H's incomplete-final-group allowance handles
319
+ // it without non-projection. Violation-minimisation picks per line.
320
+ candidates = [all, ...sets];
321
+ }
322
+ }
323
+ else {
324
+ // No expected count: try dropping 1 or 2 final stressless syllables
325
+ // (feminine/dactylic endings) alongside the full projection.
326
+ if (all.length > 0 && syls[all[all.length - 1]].lex === 0) {
327
+ const c2 = all.slice(0, -1);
328
+ candidates.push(c2);
329
+ if (c2.length > 0 && syls[c2[c2.length - 1]].lex === 0) {
330
+ candidates.push(all.slice(0, -2));
331
+ }
332
+ }
333
+ }
334
+ let best = null;
335
+ let bestViol = Infinity;
336
+ for (const proj of candidates) {
337
+ const c = construct(syls, proj, g0);
338
+ const viol = maxima.filter(i => proj.includes(i) && !c.g1cols.has(i)).length
339
+ + (c.complete ? 0 : 10);
340
+ if (viol < bestViol) {
341
+ bestViol = viol;
342
+ best = c;
343
+ }
344
+ if (bestViol === 0)
345
+ break; // earliest zero-violation wins
346
+ }
347
+ if (!best)
348
+ return null;
349
+ const violations = maxima.filter(i => !best.g1cols.has(i));
350
+ return {
351
+ ruleLabel: g0.label,
352
+ rows: best.rows,
353
+ maxima,
354
+ violations,
355
+ metrical: violations.length === 0 && best.complete,
356
+ complete: best.complete,
357
+ };
358
+ }
359
+ const DISCOVERY_SCHEMAS = ['iambic', 'trochaic', 'anapestic', 'amphibrachic', 'dactylic'];
360
+ // The condition on maxima is INVIOLABLE in a strict meter ("MAXIMA must
361
+ // project to gridline 1") — so a violation costs far more than the loose
362
+ // reading's flat per-line rate: a poem whose lines keep violating under
363
+ // every strict setting (a dol'nik) tips to loose, while sporadic violations
364
+ // (tagger noise on a homograph, a genuinely transgressive line) don't flip
365
+ // an otherwise-clean strict poem.
366
+ const VIOLATION_COST = 2.0;
367
+ const INCOMPLETE_COST = 6.0;
368
+ const LOOSE_COST_PER_LINE = 0.9;
369
+ function schemaFootSize(schema) {
370
+ return schema === 'iambic' || schema === 'trochaic' ? 2 : 3;
371
+ }
372
+ export function buildFabbHalleGridsForPoem(lines, options) {
373
+ const usable = lines.filter(l => l.length >= 2);
374
+ if (usable.length === 0)
375
+ return null;
376
+ const ranked = [];
377
+ for (const schema of DISCOVERY_SCHEMAS) {
378
+ const size = schemaFootSize(schema);
379
+ let cost = 0;
380
+ for (const syls of usable) {
381
+ const fh = buildFabbHalleGrid(syls, schema, Math.floor(syls.length / size), options);
382
+ if (!fh) {
383
+ cost += 8;
384
+ continue;
385
+ }
386
+ cost += fh.violations.length * VIOLATION_COST + (fh.complete ? 0 : INCOMPLETE_COST);
387
+ }
388
+ ranked.push({ schema, cost });
389
+ }
390
+ ranked.push({ schema: 'loose', cost: usable.length * LOOSE_COST_PER_LINE });
391
+ // Stable sort: ties resolve to the DISCOVERY_SCHEMAS order (a frequency
392
+ // prior over the tradition's meters — iambic first), loose last.
393
+ ranked.sort((a, b) => a.cost - b.cost);
394
+ const winner = ranked[0];
395
+ const size = winner.schema === 'loose' ? 0 : schemaFootSize(winner.schema);
396
+ const grids = lines.map(syls => syls.length < 2 ? null :
397
+ buildFabbHalleGrid(syls, winner.schema, size > 0 ? Math.floor(syls.length / size) : undefined, options));
398
+ return {
399
+ schema: winner.schema,
400
+ grids,
401
+ cost: winner.cost,
402
+ runnerUp: ranked[1] ? { schema: ranked[1].schema, cost: ranked[1].cost } : undefined,
403
+ };
404
+ }
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsB5C,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAKV,UAAU,EAMX,MAAM,YAAY,CAAC;AAyLpB,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,EAAE,MAAM,GAAE,aAA4B,GAAG,UAAU,EAAE,EAAE,CAuDtH;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,CAkC1G;AAqDD,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"}