@sharpee/lang-en-us 1.5.0 → 2.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 (259) hide show
  1. package/actions/about.d.ts +1 -0
  2. package/actions/about.d.ts.map +1 -1
  3. package/actions/about.js +15 -8
  4. package/actions/about.js.map +1 -1
  5. package/actions/again.d.ts.map +1 -1
  6. package/actions/again.js.map +1 -1
  7. package/actions/answering.d.ts.map +1 -1
  8. package/actions/answering.js.map +1 -1
  9. package/actions/asking.d.ts.map +1 -1
  10. package/actions/asking.js +14 -14
  11. package/actions/asking.js.map +1 -1
  12. package/actions/attacking.d.ts.map +1 -1
  13. package/actions/attacking.js +51 -51
  14. package/actions/attacking.js.map +1 -1
  15. package/actions/climbing.d.ts.map +1 -1
  16. package/actions/climbing.js +4 -4
  17. package/actions/climbing.js.map +1 -1
  18. package/actions/closing.d.ts.map +1 -1
  19. package/actions/closing.js +5 -5
  20. package/actions/closing.js.map +1 -1
  21. package/actions/drinking.d.ts.map +1 -1
  22. package/actions/drinking.js +20 -20
  23. package/actions/drinking.js.map +1 -1
  24. package/actions/dropping.d.ts.map +1 -1
  25. package/actions/dropping.js +3 -3
  26. package/actions/dropping.js.map +1 -1
  27. package/actions/eating.d.ts.map +1 -1
  28. package/actions/eating.js +20 -20
  29. package/actions/eating.js.map +1 -1
  30. package/actions/entering.d.ts.map +1 -1
  31. package/actions/entering.js +10 -10
  32. package/actions/entering.js.map +1 -1
  33. package/actions/examining.d.ts.map +1 -1
  34. package/actions/examining.js +28 -23
  35. package/actions/examining.js.map +1 -1
  36. package/actions/exiting.d.ts.map +1 -1
  37. package/actions/exiting.js +4 -4
  38. package/actions/exiting.js.map +1 -1
  39. package/actions/giving.d.ts.map +1 -1
  40. package/actions/giving.js +12 -12
  41. package/actions/giving.js.map +1 -1
  42. package/actions/going.d.ts.map +1 -1
  43. package/actions/going.js +7 -7
  44. package/actions/going.js.map +1 -1
  45. package/actions/help.d.ts +3 -3
  46. package/actions/help.d.ts.map +1 -1
  47. package/actions/help.js +9 -7
  48. package/actions/help.js.map +1 -1
  49. package/actions/hiding.d.ts.map +1 -1
  50. package/actions/hiding.js.map +1 -1
  51. package/actions/index.d.ts +5 -3
  52. package/actions/index.d.ts.map +1 -1
  53. package/actions/index.js.map +1 -1
  54. package/actions/inserting.d.ts.map +1 -1
  55. package/actions/inserting.js +8 -8
  56. package/actions/inserting.js.map +1 -1
  57. package/actions/inventory.d.ts.map +1 -1
  58. package/actions/inventory.js.map +1 -1
  59. package/actions/listening.d.ts.map +1 -1
  60. package/actions/listening.js +7 -7
  61. package/actions/listening.js.map +1 -1
  62. package/actions/locking.d.ts.map +1 -1
  63. package/actions/locking.js +8 -8
  64. package/actions/locking.js.map +1 -1
  65. package/actions/looking.d.ts.map +1 -1
  66. package/actions/looking.js +5 -5
  67. package/actions/looking.js.map +1 -1
  68. package/actions/lowering.d.ts.map +1 -1
  69. package/actions/lowering.js +2 -2
  70. package/actions/lowering.js.map +1 -1
  71. package/actions/opening.d.ts.map +1 -1
  72. package/actions/opening.js +7 -7
  73. package/actions/opening.js.map +1 -1
  74. package/actions/pulling.d.ts.map +1 -1
  75. package/actions/pulling.js +19 -19
  76. package/actions/pulling.js.map +1 -1
  77. package/actions/pushing.d.ts.map +1 -1
  78. package/actions/pushing.js +14 -14
  79. package/actions/pushing.js.map +1 -1
  80. package/actions/putting.d.ts.map +1 -1
  81. package/actions/putting.js +12 -12
  82. package/actions/putting.js.map +1 -1
  83. package/actions/quitting.d.ts.map +1 -1
  84. package/actions/quitting.js.map +1 -1
  85. package/actions/raising.d.ts.map +1 -1
  86. package/actions/raising.js +2 -2
  87. package/actions/raising.js.map +1 -1
  88. package/actions/reading.d.ts.map +1 -1
  89. package/actions/reading.js +10 -8
  90. package/actions/reading.js.map +1 -1
  91. package/actions/removing.d.ts.map +1 -1
  92. package/actions/removing.js +8 -8
  93. package/actions/removing.js.map +1 -1
  94. package/actions/restoring.d.ts.map +1 -1
  95. package/actions/restoring.js +6 -6
  96. package/actions/restoring.js.map +1 -1
  97. package/actions/saving.d.ts.map +1 -1
  98. package/actions/saving.js +6 -6
  99. package/actions/saving.js.map +1 -1
  100. package/actions/scoring.d.ts.map +1 -1
  101. package/actions/scoring.js.map +1 -1
  102. package/actions/searching.d.ts.map +1 -1
  103. package/actions/searching.js +7 -7
  104. package/actions/searching.js.map +1 -1
  105. package/actions/showing.d.ts.map +1 -1
  106. package/actions/showing.js +11 -11
  107. package/actions/showing.js.map +1 -1
  108. package/actions/sleeping.d.ts.map +1 -1
  109. package/actions/sleeping.js.map +1 -1
  110. package/actions/smelling.d.ts.map +1 -1
  111. package/actions/smelling.js +10 -10
  112. package/actions/smelling.js.map +1 -1
  113. package/actions/switching-off.d.ts.map +1 -1
  114. package/actions/switching-off.js +12 -12
  115. package/actions/switching-off.js.map +1 -1
  116. package/actions/switching-on.d.ts.map +1 -1
  117. package/actions/switching-on.js +12 -12
  118. package/actions/switching-on.js.map +1 -1
  119. package/actions/taking-off.d.ts.map +1 -1
  120. package/actions/taking-off.js +4 -4
  121. package/actions/taking-off.js.map +1 -1
  122. package/actions/taking.d.ts +1 -0
  123. package/actions/taking.d.ts.map +1 -1
  124. package/actions/taking.js +7 -5
  125. package/actions/taking.js.map +1 -1
  126. package/actions/talking.d.ts.map +1 -1
  127. package/actions/talking.js +15 -15
  128. package/actions/talking.js.map +1 -1
  129. package/actions/telling.d.ts.map +1 -1
  130. package/actions/telling.js +13 -13
  131. package/actions/telling.js.map +1 -1
  132. package/actions/throwing.d.ts.map +1 -1
  133. package/actions/throwing.js +21 -21
  134. package/actions/throwing.js.map +1 -1
  135. package/actions/touching.d.ts.map +1 -1
  136. package/actions/touching.js +20 -20
  137. package/actions/touching.js.map +1 -1
  138. package/actions/turning.d.ts.map +1 -1
  139. package/actions/turning.js +24 -24
  140. package/actions/turning.js.map +1 -1
  141. package/actions/undoing.d.ts.map +1 -1
  142. package/actions/undoing.js.map +1 -1
  143. package/actions/unlocking.d.ts.map +1 -1
  144. package/actions/unlocking.js +8 -8
  145. package/actions/unlocking.js.map +1 -1
  146. package/actions/version.d.ts.map +1 -1
  147. package/actions/version.js +3 -3
  148. package/actions/version.js.map +1 -1
  149. package/actions/waiting.d.ts.map +1 -1
  150. package/actions/waiting.js +1 -1
  151. package/actions/waiting.js.map +1 -1
  152. package/actions/wearing.d.ts.map +1 -1
  153. package/actions/wearing.js +5 -5
  154. package/actions/wearing.js.map +1 -1
  155. package/assembler/english-assembler.d.ts +66 -0
  156. package/assembler/english-assembler.d.ts.map +1 -0
  157. package/assembler/english-assembler.js +806 -0
  158. package/assembler/english-assembler.js.map +1 -0
  159. package/assembler/errors.d.ts +23 -0
  160. package/assembler/errors.d.ts.map +1 -0
  161. package/assembler/errors.js +35 -0
  162. package/assembler/errors.js.map +1 -0
  163. package/assembler/index.d.ts +10 -0
  164. package/assembler/index.d.ts.map +1 -0
  165. package/assembler/index.js +17 -0
  166. package/assembler/index.js.map +1 -0
  167. package/data/events.d.ts.map +1 -1
  168. package/data/events.js +11 -11
  169. package/data/events.js.map +1 -1
  170. package/data/messages.d.ts.map +1 -1
  171. package/data/messages.js +1 -1
  172. package/data/messages.js.map +1 -1
  173. package/data/verbs.d.ts.map +1 -1
  174. package/data/verbs.js.map +1 -1
  175. package/data/words.d.ts.map +1 -1
  176. package/data/words.js.map +1 -1
  177. package/grammar.d.ts.map +1 -1
  178. package/grammar.js.map +1 -1
  179. package/index.d.ts +2 -1
  180. package/index.d.ts.map +1 -1
  181. package/index.js +4 -1
  182. package/index.js.map +1 -1
  183. package/language-provider.d.ts +50 -24
  184. package/language-provider.d.ts.map +1 -1
  185. package/language-provider.js +96 -40
  186. package/language-provider.js.map +1 -1
  187. package/npc/conversation.d.ts +5 -0
  188. package/npc/conversation.d.ts.map +1 -1
  189. package/npc/conversation.js +24 -19
  190. package/npc/conversation.js.map +1 -1
  191. package/npc/index.d.ts.map +1 -1
  192. package/npc/index.js.map +1 -1
  193. package/npc/influence.d.ts.map +1 -1
  194. package/npc/influence.js +4 -4
  195. package/npc/influence.js.map +1 -1
  196. package/npc/npc.d.ts +6 -0
  197. package/npc/npc.d.ts.map +1 -1
  198. package/npc/npc.js +39 -33
  199. package/npc/npc.js.map +1 -1
  200. package/npc/propagation.d.ts.map +1 -1
  201. package/npc/propagation.js +6 -6
  202. package/npc/propagation.js.map +1 -1
  203. package/number-words.d.ts +18 -0
  204. package/number-words.d.ts.map +1 -1
  205. package/number-words.js +77 -0
  206. package/number-words.js.map +1 -1
  207. package/package.json +3 -3
  208. package/parser/index.d.ts +8 -0
  209. package/parser/index.d.ts.map +1 -0
  210. package/parser/index.js +13 -0
  211. package/parser/index.js.map +1 -0
  212. package/parser/parse-phrase-template.d.ts +62 -0
  213. package/parser/parse-phrase-template.d.ts.map +1 -0
  214. package/parser/parse-phrase-template.js +384 -0
  215. package/parser/parse-phrase-template.js.map +1 -0
  216. package/perspective/index.d.ts.map +1 -1
  217. package/perspective/index.js.map +1 -1
  218. package/perspective/placeholder-resolver.d.ts +1 -1
  219. package/perspective/placeholder-resolver.d.ts.map +1 -1
  220. package/perspective/placeholder-resolver.js +12 -1
  221. package/perspective/placeholder-resolver.js.map +1 -1
  222. package/platform-messages.d.ts +26 -0
  223. package/platform-messages.d.ts.map +1 -0
  224. package/platform-messages.js +31 -0
  225. package/platform-messages.js.map +1 -0
  226. package/pluralize.d.ts.map +1 -1
  227. package/pluralize.js.map +1 -1
  228. package/sound-messages.d.ts +8 -8
  229. package/sound-messages.d.ts.map +1 -1
  230. package/sound-messages.js +8 -8
  231. package/sound-messages.js.map +1 -1
  232. package/formatters/article.d.ts +0 -41
  233. package/formatters/article.d.ts.map +0 -1
  234. package/formatters/article.js +0 -143
  235. package/formatters/article.js.map +0 -1
  236. package/formatters/index.d.ts +0 -19
  237. package/formatters/index.d.ts.map +0 -1
  238. package/formatters/index.js +0 -42
  239. package/formatters/index.js.map +0 -1
  240. package/formatters/list.d.ts +0 -56
  241. package/formatters/list.d.ts.map +0 -1
  242. package/formatters/list.js +0 -178
  243. package/formatters/list.js.map +0 -1
  244. package/formatters/registry.d.ts +0 -49
  245. package/formatters/registry.d.ts.map +0 -1
  246. package/formatters/registry.js +0 -136
  247. package/formatters/registry.js.map +0 -1
  248. package/formatters/text.d.ts +0 -33
  249. package/formatters/text.d.ts.map +0 -1
  250. package/formatters/text.js +0 -80
  251. package/formatters/text.js.map +0 -1
  252. package/formatters/types.d.ts +0 -52
  253. package/formatters/types.d.ts.map +0 -1
  254. package/formatters/types.js +0 -14
  255. package/formatters/types.js.map +0 -1
  256. package/formatters/verb.d.ts +0 -30
  257. package/formatters/verb.d.ts.map +0 -1
  258. package/formatters/verb.js +0 -59
  259. package/formatters/verb.js.map +0 -1
@@ -0,0 +1,806 @@
1
+ "use strict";
2
+ /**
3
+ * @file English Assembler — realizes a phrase tree to text blocks (ADR-192 §4).
4
+ *
5
+ * Purpose: the single English-locale component that walks a `Phrase` tree and
6
+ * is the SOLE authority for every cross-cutting correctness concern — article,
7
+ * agreement, punctuation, whitespace, reference, and case. It replaces the
8
+ * left-to-right formatter chain (`applyFormatters`) whose early collapse to a
9
+ * bare string lost the metadata neighbours need to agree against.
10
+ *
11
+ * Public interface: `EnglishAssembler` (implements `Assembler`),
12
+ * `ASSEMBLER_DEFAULT_BLOCK_KEY`, and the standalone `capitalizeSentenceStart`
13
+ * case-authority helper.
14
+ *
15
+ * Owner context: `@sharpee/lang-en-us` — English realization. The ADR-190 list
16
+ * formatter logic is reproduced here in the `PhraseList` case (the old
17
+ * `formatters/list.ts` is retired in Phase 3, not called from here).
18
+ *
19
+ * INVARIANT (ADR-192 §7): `realize` is a pure function of `(tree, ctx)` GIVEN the
20
+ * `textState` snapshot — the same tree, context, and counters yield byte-identical
21
+ * output. No clocks, no `Math.random`. The one declared state transition is a
22
+ * `Choice` advancing its `textState` counter (ADR-196 §3) — seeded, deterministic.
23
+ *
24
+ * All if-domain kinds are realized here: the foundational kinds (Literal,
25
+ * NounPhrase, PhraseList, Sequence, Empty) plus the `Verb` (199), `Verbatim`
26
+ * (200), `Numeral` (198), `Pronoun` (197), `Contents` (194), `Slot` (195), and
27
+ * `Optional` / `Choice` (196) atoms. `PhraseNotImplementedError` is now only a
28
+ * defensive guard against a future unhandled kind.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.EnglishAssembler = exports.ASSEMBLER_DEFAULT_BLOCK_KEY = void 0;
32
+ exports.capitalizeSentenceStart = capitalizeSentenceStart;
33
+ const if_domain_1 = require("@sharpee/if-domain");
34
+ const text_blocks_1 = require("@sharpee/text-blocks");
35
+ const pluralize_js_1 = require("../pluralize.js");
36
+ const number_words_js_1 = require("../number-words.js");
37
+ const errors_js_1 = require("./errors.js");
38
+ /**
39
+ * Default channel key for a realized tree. Phase 2 emits one block; the report
40
+ * layer (ADR-192 §6, Phase 4) assigns real channel keys (room.name, …) as it
41
+ * wires per-event trees.
42
+ */
43
+ exports.ASSEMBLER_DEFAULT_BLOCK_KEY = text_blocks_1.CORE_BLOCK_KEYS.ACTION_RESULT;
44
+ // ===========================================================================
45
+ // Article authority — a/an/the/some/∅ agreed over the realized head
46
+ // ===========================================================================
47
+ /** Indefinite article for a head phrase, agreed over its leading sound. */
48
+ function indefiniteArticle(head) {
49
+ if (!head)
50
+ return 'a';
51
+ const lower = head.toLowerCase();
52
+ // Silent-h and vowel-sound exceptions (ported from formatters/article.ts).
53
+ if (lower.startsWith('hour'))
54
+ return 'an';
55
+ if (lower.startsWith('honest'))
56
+ return 'an';
57
+ if (lower.startsWith('heir'))
58
+ return 'an';
59
+ if (lower.startsWith('uni'))
60
+ return 'a'; // "a university"
61
+ if (lower.startsWith('one'))
62
+ return 'a'; // "a one-way street"
63
+ const vowels = ['a', 'e', 'i', 'o', 'u'];
64
+ return vowels.includes(lower[0]) ? 'an' : 'a';
65
+ }
66
+ /** The article surface for a noun phrase, agreed over its rendered head. */
67
+ function articleSurface(np, head) {
68
+ if (np.properName)
69
+ return ''; // proper names suppress the article
70
+ switch (np.articleType) {
71
+ case 'none':
72
+ return '';
73
+ case 'definite':
74
+ return 'the';
75
+ case 'some':
76
+ return 'some';
77
+ case 'indefinite':
78
+ if (np.number === 'plural')
79
+ return ''; // "swords", not "a swords"
80
+ if (np.number === 'mass')
81
+ return 'some'; // "some sand"
82
+ return indefiniteArticle(head);
83
+ }
84
+ }
85
+ // ===========================================================================
86
+ // Agreement authority — number / pluralization
87
+ // ===========================================================================
88
+ /**
89
+ * The noun's surface form for its grammatical number (Agreement authority).
90
+ *
91
+ * An intrinsically-plural NounPhrase (`number: 'plural'`) carries its plural
92
+ * surface directly in `name` — the producer maps it from `IdentityTrait.name`,
93
+ * which an author marking an entity `.plural()` writes already-plural ("pygmy
94
+ * goats", "direction signs"). So the name is used as-is; only an explicit
95
+ * `pluralForm` overrides it. Re-running `pluralize` here would double-pluralize
96
+ * ("goats" → "goatses"). The count-group path (N identical *singular* entities →
97
+ * "two goats") pluralizes singular names itself and does not pass through here.
98
+ */
99
+ function nounSurface(np) {
100
+ if (np.number === 'plural')
101
+ return np.pluralForm ?? np.name;
102
+ return np.name; // singular and mass use the base name
103
+ }
104
+ /** The head (adjectives + noun) the article agrees over. */
105
+ function headWithAdjectives(np) {
106
+ const adjectives = np.adjectives && np.adjectives.length ? `${np.adjectives.join(' ')} ` : '';
107
+ return `${adjectives}${nounSurface(np)}`;
108
+ }
109
+ /** Realize a single noun phrase: article + adjectives + noun, agreed as a whole. */
110
+ function renderNoun(np) {
111
+ const head = headWithAdjectives(np);
112
+ const article = articleSurface(np, head);
113
+ const text = article ? `${article} ${head}` : head;
114
+ // Case authority: the {capitalize …} hint upper-cases the rendered head.
115
+ return np.capitalize ? capitalizeSentenceStart(text) : text;
116
+ }
117
+ // ===========================================================================
118
+ // Agreement authority — verb conjugation (ADR-199)
119
+ // ===========================================================================
120
+ /**
121
+ * Suppletive verbs whose plural / 1st-singular forms are not the regular `-s`
122
+ * strip. Keyed by the 3rd-person-singular `lemma` the author types. This is the
123
+ * table lifted out of the deleted `formatters/verb.ts`, extended with do/go.
124
+ */
125
+ const IRREGULAR_VERBS = {
126
+ is: { plural: 'are', firstSingular: 'am' }, // be (present)
127
+ was: { plural: 'were', firstSingular: 'was' }, // be (past): I was / you were
128
+ has: { plural: 'have' }, // have
129
+ does: { plural: 'do' }, // do
130
+ goes: { plural: 'go' }, // go
131
+ // `-ie` stems (ADR-204): 3sg adds only `-s`, so they surface as `-ies` and would be
132
+ // mis-handled by the `ies`→`y` rule (which is correct for the common `-y` stems). This
133
+ // closed set is the exception; add longer derivatives (unties, belies) on demand.
134
+ dies: { plural: 'die' }, // die
135
+ lies: { plural: 'lie' }, // lie
136
+ ties: { plural: 'tie' }, // tie
137
+ vies: { plural: 'vie' }, // vie
138
+ };
139
+ /**
140
+ * Regular rule: the 3rd-singular `lemma` ends in `-s`; the plain form strips it (ADR-199,
141
+ * refined ADR-204). The `-es` strip applies only to genuine `-es` inflections — a doubled
142
+ * sibilant (`ss`/`zz`) or `x`/`ch`/`sh` — NOT to a single `-se`/`-ze` stem, which added only
143
+ * `-s` (use→uses, refuse→refuses). Single-`s` stems (focus/bus/gas, a rare closed set) that
144
+ * legitimately take `-es` are the residual ambiguity; add them to `IRREGULAR_VERBS` on demand.
145
+ */
146
+ function regularPluralVerb(lemma) {
147
+ if (lemma.endsWith('ies') && lemma.length > 3)
148
+ return `${lemma.slice(0, -3)}y`; // carries → carry
149
+ if (/(?:ss|zz|x|ch|sh)es$/.test(lemma))
150
+ return lemma.slice(0, -2); // kisses → kiss, boxes → box, watches → watch
151
+ if (lemma.endsWith('s'))
152
+ return lemma.slice(0, -1); // opens → open, uses → use, refuses → refuse
153
+ return lemma; // no -s to strip — leave as authored
154
+ }
155
+ /**
156
+ * The grammatical person of a subject noun phrase. The player subject (matched
157
+ * by `referableId` against `narrative.playerId`) takes the narrative person
158
+ * ("you are" in 2nd-person narration); otherwise an explicit `person` stamp, or
159
+ * undefined (→ third). ADR-199 §4 B, resolved at realize time where the
160
+ * narrative context is reliably present.
161
+ */
162
+ function nounPerson(np, ctx) {
163
+ if (np.referableId !== undefined && np.referableId === ctx.narrative.playerId) {
164
+ return ctx.narrative.person;
165
+ }
166
+ return np.person;
167
+ }
168
+ /** Read the agreement surface (number, optional person) off a bound subject value. */
169
+ function subjectAgreement(subject, ctx) {
170
+ if (subject !== null && typeof subject === 'object' && typeof subject.kind === 'string') {
171
+ const phrase = subject;
172
+ if ((0, if_domain_1.isNounPhrase)(phrase))
173
+ return { number: phrase.number, person: nounPerson(phrase, ctx) };
174
+ if ((0, if_domain_1.isPhraseList)(phrase)) {
175
+ const present = phrase.items.filter((item) => !(0, if_domain_1.isEmpty)(item));
176
+ if (present.length > 1)
177
+ return { number: 'plural' }; // "the troll and the goats" → are
178
+ const only = present[0];
179
+ if (present.length === 1 && only && (0, if_domain_1.isNounPhrase)(only))
180
+ return { number: only.number, person: nounPerson(only, ctx) };
181
+ return { number: 'singular' };
182
+ }
183
+ }
184
+ // No agreement surface (Literal, Empty, scalar, unbound) → unmarked default (§4 C).
185
+ return { number: 'singular' };
186
+ }
187
+ /** Conjugate a lemma for the resolved subject number/person (Agreement authority). */
188
+ function conjugateVerb(lemma, number, person) {
189
+ // 3rd-person singular (and mass, which agrees singular) is the authored lemma.
190
+ if (person === 'third' && number !== 'plural')
191
+ return lemma;
192
+ const irregular = IRREGULAR_VERBS[lemma];
193
+ // 1st-person singular suppletive ("I am", "I was").
194
+ if (number !== 'plural' && person === 'first' && irregular?.firstSingular)
195
+ return irregular.firstSingular;
196
+ // Everything else (plural, or 1st/2nd person) takes the non-3rd-singular form.
197
+ if (irregular)
198
+ return irregular.plural;
199
+ return regularPluralVerb(lemma);
200
+ }
201
+ /** Realize a `Numeral` (ADR-198): digits, spelled words, or numeric ordinal. */
202
+ function renderNumeral(value, format) {
203
+ if (Number.isNaN(value))
204
+ return ''; // bound to a non-number — authoring error
205
+ switch (format) {
206
+ case 'words':
207
+ return (0, number_words_js_1.numberToWords)(value);
208
+ case 'ordinal':
209
+ return (0, number_words_js_1.ordinalString)(value);
210
+ default:
211
+ return String(value);
212
+ }
213
+ }
214
+ /** Realize a `Verb` by agreeing it with its referenced subject's resolved surface. */
215
+ function renderVerb(verb, ctx) {
216
+ const agreement = subjectAgreement(ctx.params[verb.subjectRef], ctx);
217
+ // The subject's own person wins; else the Verb's declared person; else third.
218
+ const person = agreement.person ?? verb.person ?? 'third';
219
+ return conjugateVerb(verb.lemma, agreement.number, person);
220
+ }
221
+ // ===========================================================================
222
+ // Punctuation authority — serial commas, final and/or, no dangling comma
223
+ // ===========================================================================
224
+ /** Join already-rendered parts with the conjunction, honouring the serial comma. */
225
+ function joinParts(parts, conj, serialComma) {
226
+ if (parts.length === 0)
227
+ return 'nothing';
228
+ if (parts.length === 1)
229
+ return parts[0];
230
+ if (parts.length === 2)
231
+ return `${parts[0]} ${conj} ${parts[1]}`;
232
+ const head = parts.slice(0, -1).join(', ');
233
+ const last = parts[parts.length - 1];
234
+ return serialComma ? `${head}, ${conj} ${last}` : `${head} ${conj} ${last}`;
235
+ }
236
+ /** A noun phrase groups with identical siblings only if indefinite, singular, common. */
237
+ function isGroupable(np) {
238
+ return np.articleType === 'indefinite' && np.number === 'singular' && !np.properName;
239
+ }
240
+ /**
241
+ * Realize a list: group identical indefinite common nouns ("two goats"),
242
+ * pluralize, absorb `Empty`, and join under the punctuation authority. This is
243
+ * the ADR-190 list formatter, ported to operate on phrases (not `EntityInfo`).
244
+ */
245
+ function renderList(items, conj, ctx) {
246
+ const present = items.filter((item) => !(0, if_domain_1.isEmpty)(item)); // Empty absorbed — no dangling comma
247
+ if (present.length === 0)
248
+ return 'nothing';
249
+ const parts = [];
250
+ const groupIndex = new Map();
251
+ for (const item of present) {
252
+ if ((0, if_domain_1.isNounPhrase)(item) && isGroupable(item)) {
253
+ const key = JSON.stringify([item.name, item.adjectives ?? []]);
254
+ const existing = groupIndex.get(key);
255
+ if (existing !== undefined) {
256
+ parts[existing].count++;
257
+ continue;
258
+ }
259
+ groupIndex.set(key, parts.length);
260
+ parts.push({ count: 1, np: item, phrase: item });
261
+ }
262
+ else {
263
+ parts.push({ count: 1, phrase: item });
264
+ }
265
+ }
266
+ const serialComma = ctx.settings.serialComma ?? true;
267
+ const rendered = parts
268
+ .map((part) => {
269
+ if (part.count > 1 && part.np) {
270
+ const adjectives = part.np.adjectives?.length ? `${part.np.adjectives.join(' ')} ` : '';
271
+ const plural = part.np.pluralForm ?? (0, pluralize_js_1.pluralize)(part.np.name);
272
+ return `${(0, number_words_js_1.countWord)(part.count)} ${adjectives}${plural}`;
273
+ }
274
+ return renderToString(part.phrase, ctx);
275
+ })
276
+ // ADR-196: an Optional-absent / Choice→Empty item realizes to "" — absorb it
277
+ // like Empty so it leaves no dangling comma (extends ADR-192 AC-6 to modifiers).
278
+ .filter((s) => s.length > 0);
279
+ if (rendered.length === 0)
280
+ return 'nothing';
281
+ return joinParts(rendered, conj, serialComma);
282
+ }
283
+ /**
284
+ * Realize a `Contents` (ADR-194): read the container's live contents from the
285
+ * world, bridge each entity to a `NounPhrase`, and render as a grouped list.
286
+ * Graceful — an unresolved container or missing bridge renders "nothing".
287
+ */
288
+ function renderContents(contents, ctx) {
289
+ const ref = ctx.params[contents.containerRef];
290
+ let containerId;
291
+ if (typeof ref === 'string') {
292
+ containerId = ref;
293
+ }
294
+ else if (ref !== null && typeof ref === 'object' && ref.kind === 'noun') {
295
+ containerId = ref.referableId;
296
+ }
297
+ const bridge = ctx.world.nounPhraseFor;
298
+ if (containerId === undefined || !bridge)
299
+ return 'nothing';
300
+ const items = ctx.world
301
+ .getEntityContents(containerId)
302
+ .map((entity) => bridge(entity.id))
303
+ .filter((np) => np !== undefined);
304
+ return renderList(items, contents.conj ?? 'and', ctx);
305
+ }
306
+ /**
307
+ * Realize a `Slot` (ADR-195 §4): peek the turn's contributions for `slotKey`,
308
+ * realize each, absorb any that render empty (AC-4), and join the survivors. The
309
+ * SLOT owns the connective grammar — the contribution is bare content:
310
+ *
311
+ * - zero survivors → `''` (the slot is `Empty`; the stem + its terminator stay
312
+ * clean, no dangling space/comma — AC-3);
313
+ * - `sentence` mode (default) → a leading space then the survivors space-joined,
314
+ * so they follow the stem's terminator as independent sentences (AC-1);
315
+ * - `clause` mode → a leading `", "` then the survivors joined through the
316
+ * punctuation authority (`joinParts`: serial comma + final `conj`), so they
317
+ * attach as clauses before the stem's terminator (AC-2).
318
+ *
319
+ * The accessor is optional (`?.`): a context that never wired the store yields no
320
+ * contributions, so the slot simply realizes empty (ADR-195 §2).
321
+ */
322
+ function renderSlot(slot, ctx) {
323
+ const contributions = ctx.slotContributions?.(slot.slotKey) ?? [];
324
+ const surfaces = contributions
325
+ .map((phrase) => renderToString(phrase, ctx))
326
+ .filter((s) => s.length > 0); // absorb Empty / empty-rendering contributions (AC-4)
327
+ if (surfaces.length === 0)
328
+ return ''; // zero contributions → Empty, clean stem (AC-3)
329
+ if ((slot.mode ?? 'sentence') === 'clause') {
330
+ const serialComma = ctx.settings.serialComma ?? true;
331
+ return `, ${joinParts(surfaces, slot.conj ?? 'and', serialComma)}`;
332
+ }
333
+ return ` ${surfaces.join(' ')}`;
334
+ }
335
+ // ===========================================================================
336
+ // Reference authority — last-mentioned tracking (placeholder; ADR-197)
337
+ // ===========================================================================
338
+ /**
339
+ * Record a realized noun phrase as last-mentioned so a later `Pronoun` can refer
340
+ * to it. The real resolution lands in ADR-197; here the Assembler only feeds the
341
+ * seam so the contract is exercised end-to-end.
342
+ */
343
+ function noteReference(np, ctx) {
344
+ if (np.referableId !== undefined) {
345
+ ctx.reference.note({ referableId: np.referableId, number: np.number, pronounSet: np.pronounSet });
346
+ }
347
+ }
348
+ // ===========================================================================
349
+ // Pronoun authority — case × number × gender (ADR-197)
350
+ // ===========================================================================
351
+ /** he/she/it/they forms keyed by case. */
352
+ const PRONOUNS = {
353
+ he: { subject: 'he', object: 'him', possessive: 'his', 'possessive-pronoun': 'his', reflexive: 'himself' },
354
+ she: { subject: 'she', object: 'her', possessive: 'her', 'possessive-pronoun': 'hers', reflexive: 'herself' },
355
+ it: { subject: 'it', object: 'it', possessive: 'its', 'possessive-pronoun': 'its', reflexive: 'itself' },
356
+ they: { subject: 'they', object: 'them', possessive: 'their', 'possessive-pronoun': 'theirs', reflexive: 'themselves' },
357
+ };
358
+ /** Pick the gender row for a referent: explicit pronounSet, else by number. */
359
+ function genderOf(ref) {
360
+ const set = ref.pronounSet?.toLowerCase();
361
+ if (set === 'he' || set === 'masculine')
362
+ return 'he';
363
+ if (set === 'she' || set === 'feminine')
364
+ return 'she';
365
+ if (set === 'they' || set === 'plural' || set === 'nonbinary')
366
+ return 'they';
367
+ if (set === 'it' || set === 'neuter')
368
+ return 'it';
369
+ return ref.number === 'plural' ? 'they' : 'it'; // no set → by number (mass agrees singular)
370
+ }
371
+ /** Realize a `Pronoun` (ADR-197): the last-mentioned referent in the requested case. */
372
+ function renderPronoun(pronoun, ctx) {
373
+ const ref = ctx.reference.lastMentioned();
374
+ // Graceful: no antecedent → neuter singular for the case (no throw).
375
+ const gender = ref ? genderOf(ref) : 'it';
376
+ return PRONOUNS[gender][pronoun.case];
377
+ }
378
+ // ===========================================================================
379
+ // Whitespace authority — collapse, verbatim-exempt
380
+ // ===========================================================================
381
+ /**
382
+ * Collapse runs of whitespace in non-verbatim runs to a single space, drop
383
+ * whitespace at run boundaries, and trim the block ends. Verbatim runs pass
384
+ * through untouched (ADR-183 whitespace authority, now Assembler-owned).
385
+ */
386
+ function collapseWhitespace(runs) {
387
+ const out = [];
388
+ let prevEndedWithSpace = true; // seeded true so leading whitespace is trimmed
389
+ for (const run of runs) {
390
+ if (run.verbatim) {
391
+ out.push(run);
392
+ if (run.text.length > 0)
393
+ prevEndedWithSpace = /\s$/.test(run.text);
394
+ continue;
395
+ }
396
+ let text = run.text.replace(/\s+/g, ' ');
397
+ if (prevEndedWithSpace)
398
+ text = text.replace(/^ /, '');
399
+ if (text.length === 0)
400
+ continue;
401
+ out.push({ ...run, text });
402
+ prevEndedWithSpace = / $/.test(text);
403
+ }
404
+ // Trim trailing space on the final non-verbatim run.
405
+ for (let i = out.length - 1; i >= 0; i--) {
406
+ if (out[i].verbatim)
407
+ break;
408
+ out[i] = { ...out[i], text: out[i].text.replace(/ $/, '') };
409
+ if (out[i].text.length > 0)
410
+ break;
411
+ }
412
+ return out.filter((r) => r.verbatim || r.text.length > 0);
413
+ }
414
+ // ===========================================================================
415
+ // Case authority — sentence-start capitalization
416
+ // ===========================================================================
417
+ /**
418
+ * Capitalize the first alphabetic character of a sentence. Exposed as the Case
419
+ * authority; the explicit `{capitalize …}` template hint is wired to it by the
420
+ * parser in Phase 3 (ADR-192 §5). Not auto-applied — realization preserves the
421
+ * author's case unless capitalization is requested.
422
+ *
423
+ * @param text the realized text
424
+ * @returns the text with its first letter upper-cased
425
+ */
426
+ function capitalizeSentenceStart(text) {
427
+ const index = text.search(/[a-z]/i);
428
+ if (index < 0)
429
+ return text;
430
+ return text.slice(0, index) + text[index].toUpperCase() + text.slice(index + 1);
431
+ }
432
+ // ===========================================================================
433
+ // Choice selection (ADR-196 §2/§3) — deterministic, persistent variation
434
+ // ===========================================================================
435
+ /**
436
+ * FNV-1a 32-bit hash of a seed string. Deterministic; used only to seed the PRNG.
437
+ */
438
+ function hashSeed(s) {
439
+ let h = 2166136261 >>> 0;
440
+ for (let i = 0; i < s.length; i++) {
441
+ h ^= s.charCodeAt(i);
442
+ h = Math.imul(h, 16777619);
443
+ }
444
+ return h >>> 0;
445
+ }
446
+ /**
447
+ * `mulberry32` — a tiny deterministic PRNG. Seeded from `(entityId, messageKey,
448
+ * counter)`, never `Math.random`/`Date.now`, so `random`/`sticky` selection
449
+ * reproduces byte-identically across runs and after save/restore (ADR-196 §3).
450
+ */
451
+ function seededUnitFloat(entityId, messageKey, counter) {
452
+ let a = hashSeed(`${entityId}\0${messageKey}\0${counter}`) >>> 0;
453
+ a = (a + 0x6d2b79f5) | 0;
454
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
455
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
456
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
457
+ }
458
+ /**
459
+ * Select a `Choice`'s alternative from its persisted counter and advance the
460
+ * counter (ADR-196 §2). This is the one place the Assembler writes `ctx.textState`
461
+ * — the declared realize-time mutation (ADR-192 §7 / ADR-196 §4).
462
+ *
463
+ * Stored-number encoding: a trigger count for cycling/stopping/firstTime/random;
464
+ * the chosen index + 1 (sentinel 0/undefined = unchosen) for sticky.
465
+ *
466
+ * @returns the selected alternative phrase (caller realizes it; may be Empty).
467
+ */
468
+ function selectChoice(choice, ctx) {
469
+ const { alternatives, selector, entityId, messageKey } = choice;
470
+ const len = alternatives.length;
471
+ if (len === 0)
472
+ return { kind: 'empty' }; // defensive — the contract requires ≥ 1
473
+ const stored = ctx.textState.get(entityId, messageKey);
474
+ switch (selector) {
475
+ case 'cycling': {
476
+ const n = stored ?? 0;
477
+ ctx.textState.set(entityId, messageKey, n + 1);
478
+ return alternatives[n % len];
479
+ }
480
+ case 'stopping': {
481
+ const n = stored ?? 0;
482
+ ctx.textState.set(entityId, messageKey, n + 1);
483
+ return alternatives[Math.min(n, len - 1)];
484
+ }
485
+ case 'firstTime': {
486
+ const n = stored ?? 0;
487
+ ctx.textState.set(entityId, messageKey, Math.min(n + 1, 1));
488
+ return alternatives[n === 0 ? 0 : Math.min(1, len - 1)];
489
+ }
490
+ case 'random': {
491
+ const n = stored ?? 0;
492
+ ctx.textState.set(entityId, messageKey, n + 1);
493
+ return alternatives[Math.floor(seededUnitFloat(entityId, messageKey, n) * len)];
494
+ }
495
+ case 'sticky': {
496
+ // Already chosen — replay the persisted index (stored = index + 1).
497
+ if (stored && stored > 0)
498
+ return alternatives[Math.min(stored - 1, len - 1)];
499
+ const i = Math.floor(seededUnitFloat(entityId, messageKey, 0) * len);
500
+ ctx.textState.set(entityId, messageKey, i + 1);
501
+ return alternatives[i];
502
+ }
503
+ }
504
+ }
505
+ // ===========================================================================
506
+ // Realization core
507
+ // ===========================================================================
508
+ /** Push a phrase's own decorations onto the inherited stack. */
509
+ function extendDeco(base, decorations) {
510
+ if (!decorations || decorations.length === 0)
511
+ return base;
512
+ return [
513
+ ...base,
514
+ ...decorations.map((d) => (d.value !== undefined ? { className: d.className, value: d.value } : { className: d.className })),
515
+ ];
516
+ }
517
+ // ===========================================================================
518
+ // Sentence/Quote edge metadata (ADR-201 §3) — emitted here, resolved by the
519
+ // reconciliation pass. All reads below are a NODE'S OWN realized surface
520
+ // (its child/utterance runs), never neighbours' or whole-output prose (ADR-202).
521
+ // ===========================================================================
522
+ /** A node's own last glyph already terminates the clause (so no auto-terminal). */
523
+ function endsWithTerminalOrEllipsis(text) {
524
+ const t = text.trimEnd();
525
+ // `.` already covers a `...` ellipsis; `…` is the single-glyph form.
526
+ return t.endsWith('.') || t.endsWith('?') || t.endsWith('!') || t.endsWith('…');
527
+ }
528
+ /** True if the last non-empty run of a realized child ends in terminal punctuation. */
529
+ function lastContentRunEndsTerminal(runs) {
530
+ for (let i = runs.length - 1; i >= 0; i--) {
531
+ if (runs[i].text.length > 0)
532
+ return endsWithTerminalOrEllipsis(runs[i].text);
533
+ }
534
+ return false;
535
+ }
536
+ /**
537
+ * Flag the first non-empty run as sentence-initial + cap-eligible, so the
538
+ * reconciliation pass capitalizes a sentence's / quote's first word. An explicit
539
+ * `capEligible: false` (author opt-out) is preserved, not overridden.
540
+ */
541
+ function markFirstSentenceInitial(runs) {
542
+ const i = runs.findIndex((r) => r.text.length > 0);
543
+ if (i < 0)
544
+ return runs;
545
+ const copy = runs.slice();
546
+ copy[i] = { ...copy[i], sentenceInitial: true, capEligible: copy[i].capEligible === false ? false : true };
547
+ return copy;
548
+ }
549
+ /** Mark the last non-empty run as owning a trailing terminal mark (Sentence close). */
550
+ function markLastTrailingTerminal(runs, terminal) {
551
+ for (let i = runs.length - 1; i >= 0; i--) {
552
+ if (runs[i].text.length > 0) {
553
+ const copy = runs.slice();
554
+ copy[i] = { ...copy[i], ownsTrailingPunct: terminal };
555
+ return copy;
556
+ }
557
+ }
558
+ return runs;
559
+ }
560
+ /** Realize a phrase to flat runs, threading the decoration stack through composition. */
561
+ function realizeToRuns(phrase, ctx, deco) {
562
+ if ((0, if_domain_1.isEmpty)(phrase))
563
+ return [];
564
+ if ((0, if_domain_1.isLiteral)(phrase)) {
565
+ const own = extendDeco(deco, phrase.decorations);
566
+ return [{ text: phrase.text, verbatim: phrase.whitespace === 'verbatim', deco: own }];
567
+ }
568
+ if ((0, if_domain_1.isNounPhrase)(phrase)) {
569
+ const own = extendDeco(deco, phrase.decorations);
570
+ noteReference(phrase, ctx);
571
+ return [{ text: renderNoun(phrase), verbatim: false, deco: own }];
572
+ }
573
+ if ((0, if_domain_1.isPhraseList)(phrase)) {
574
+ const own = extendDeco(deco, phrase.decorations);
575
+ return [{ text: renderList(phrase.items, phrase.conj, ctx), verbatim: false, deco: own }];
576
+ }
577
+ if ((0, if_domain_1.isVerb)(phrase)) {
578
+ const own = extendDeco(deco, phrase.decorations);
579
+ return [{ text: renderVerb(phrase, ctx), verbatim: false, deco: own }];
580
+ }
581
+ if ((0, if_domain_1.isVerbatim)(phrase)) {
582
+ // Opaque pass-through (ADR-200): exempt from whitespace collapse.
583
+ const own = extendDeco(deco, phrase.decorations);
584
+ return [{ text: phrase.text, verbatim: true, deco: own }];
585
+ }
586
+ if ((0, if_domain_1.isNumeral)(phrase)) {
587
+ // Numeral (ADR-198): digits / spelled words / ordinal.
588
+ const own = extendDeco(deco, phrase.decorations);
589
+ return [{ text: renderNumeral(phrase.value, phrase.format), verbatim: false, deco: own }];
590
+ }
591
+ if ((0, if_domain_1.isPronoun)(phrase)) {
592
+ // Pronoun (ADR-197): the last-mentioned referent in the requested case.
593
+ // ADR-201 §2 (Q1) capitalization: `true` ⇒ cap now (own-glyph rule); `false`
594
+ // ⇒ never (opt out of sentence-start cap); absent ⇒ defer to position — if it
595
+ // lands sentence-initial, `markFirstSentenceInitial` flags it for the pass.
596
+ const own = extendDeco(deco, phrase.decorations);
597
+ const text = renderPronoun(phrase, ctx);
598
+ if (phrase.capitalize === true) {
599
+ return [{ text: capitalizeSentenceStart(text), verbatim: false, deco: own }];
600
+ }
601
+ if (phrase.capitalize === false) {
602
+ return [{ text, verbatim: false, deco: own, capEligible: false }];
603
+ }
604
+ return [{ text, verbatim: false, deco: own }];
605
+ }
606
+ if ((0, if_domain_1.isContents)(phrase)) {
607
+ // Contents (ADR-194): the container's live contents as a grouped list.
608
+ const own = extendDeco(deco, phrase.decorations);
609
+ return [{ text: renderContents(phrase, ctx), verbatim: false, deco: own }];
610
+ }
611
+ if ((0, if_domain_1.isSlot)(phrase)) {
612
+ // Slot (ADR-195): the turn's contributions for this key, joined under the
613
+ // slot-owned connective grammar. Zero survivors → no run (absorbed as Empty).
614
+ const own = extendDeco(deco, phrase.decorations);
615
+ const text = renderSlot(phrase, ctx);
616
+ return text ? [{ text, verbatim: false, deco: own }] : [];
617
+ }
618
+ if ((0, if_domain_1.isSequence)(phrase)) {
619
+ const own = extendDeco(deco, phrase.decorations);
620
+ return phrase.parts.flatMap((part) => realizeToRuns(part, ctx, own));
621
+ }
622
+ if ((0, if_domain_1.isOptional)(phrase)) {
623
+ // Optional (ADR-196 §1): the producer resolved `present`; realize the child or
624
+ // nothing. Absent → no runs, absorbed by the enclosing combinator like Empty.
625
+ const own = extendDeco(deco, phrase.decorations);
626
+ return phrase.present ? realizeToRuns(phrase.child, ctx, own) : [];
627
+ }
628
+ if ((0, if_domain_1.isChoice)(phrase)) {
629
+ // Choice (ADR-196 §2): select one alternative from the persisted counter,
630
+ // advance the counter, and realize the winner. A winner that realizes to
631
+ // Empty leaves no runs (once-only text).
632
+ const own = extendDeco(deco, phrase.decorations);
633
+ return realizeToRuns(selectChoice(phrase, ctx), ctx, own);
634
+ }
635
+ if ((0, if_domain_1.isSentence)(phrase)) {
636
+ // Sentence (ADR-201 §2): realize the child as a sentence — cap its first word
637
+ // and emit a terminal mark at its close, suppressed if the child already ends
638
+ // in terminal punctuation or an ellipsis (no double-punctuation).
639
+ const own = extendDeco(deco, phrase.decorations);
640
+ let runs = markFirstSentenceInitial(realizeToRuns(phrase.child, ctx, own));
641
+ if (!lastContentRunEndsTerminal(runs)) {
642
+ runs = markLastTrailingTerminal(runs, phrase.terminal ?? '.');
643
+ }
644
+ return runs;
645
+ }
646
+ if ((0, if_domain_1.isQuote)(phrase)) {
647
+ // Quote (ADR-201 §2): wrap the utterance in locale glyphs, cap its first word,
648
+ // and place terminal punctuation INSIDE the closing glyph (suppressed if the
649
+ // utterance already ends terminal/ellipsis). The attributive comma is the
650
+ // template's in v1 (explicit composition, ADR §5) — the Quote does not own it.
651
+ // An utterance that absorbs to nothing absorbs the whole quote (no stray `""`).
652
+ const own = extendDeco(deco, phrase.decorations);
653
+ const utterance = markFirstSentenceInitial(realizeToRuns(phrase.utterance, ctx, own));
654
+ if (!utterance.some((r) => r.text.length > 0))
655
+ return [];
656
+ const open = { text: ctx.settings.openQuote ?? '"', verbatim: false, deco: own, quoteOpen: true };
657
+ const close = { text: ctx.settings.closeQuote ?? '"', verbatim: false, deco: own, quoteClose: true };
658
+ if (!lastContentRunEndsTerminal(utterance)) {
659
+ close.ownsTrailingPunct = phrase.terminal ?? '.';
660
+ }
661
+ return [open, ...utterance, close];
662
+ }
663
+ // Defensive: every if-domain kind is realized above, so `phrase` narrows to
664
+ // `never` here — TypeScript proves exhaustiveness. The cast keeps the guard
665
+ // live at runtime: a future kind added without a case is refused loudly,
666
+ // naming it, rather than silently dropping text.
667
+ throw new errors_js_1.PhraseNotImplementedError(phrase.kind);
668
+ }
669
+ /**
670
+ * The single structural reconciliation pass (ADR-201 §3.2). Walks the realized
671
+ * runs once and resolves, using run metadata only (never prose scanning, ADR-202):
672
+ * 1. Capitalization — upper-case the first glyph of each `sentenceInitial`
673
+ * `capEligible` run (the case authority's glyph helper does the upper-casing).
674
+ * 2. Terminal punctuation — materialize each `ownsTrailingPunct`: a closing-quote
675
+ * run places it INSIDE the glyph (`."`); any other run appends it.
676
+ * Whitespace collapse (§3.2 step 4) stays the final per-segment step in `realize`.
677
+ */
678
+ function reconciliationPass(runs) {
679
+ return runs.map((run) => {
680
+ let out = run;
681
+ if (out.sentenceInitial && out.capEligible === true && out.text.length > 0) {
682
+ out = { ...out, text: capitalizeSentenceStart(out.text) };
683
+ }
684
+ if (out.ownsTrailingPunct) {
685
+ out = out.quoteClose
686
+ ? { ...out, text: out.ownsTrailingPunct + out.text } // terminal inside the closing glyph
687
+ : { ...out, text: out.text + out.ownsTrailingPunct }; // sentence terminal appended
688
+ }
689
+ return out;
690
+ });
691
+ }
692
+ /** Realize a phrase to a plain string (used for list items and nested phrases). */
693
+ function renderToString(phrase, ctx) {
694
+ return collapseWhitespace(reconciliationPass(realizeToRuns(phrase, ctx, [])))
695
+ .map((r) => r.text)
696
+ .join('');
697
+ }
698
+ /** Build text content from collapsed runs, nesting decorations where present. */
699
+ function runsToContent(runs) {
700
+ const content = [];
701
+ let buffer = '';
702
+ for (const run of runs) {
703
+ if (run.deco.length === 0) {
704
+ buffer += run.text;
705
+ continue;
706
+ }
707
+ if (buffer) {
708
+ content.push(buffer);
709
+ buffer = '';
710
+ }
711
+ content.push(wrapDecorations(run.text, run.deco));
712
+ }
713
+ if (buffer)
714
+ content.push(buffer);
715
+ if (content.length === 0)
716
+ content.push('');
717
+ return content;
718
+ }
719
+ /** Wrap text in nested decorations, outermost first. */
720
+ function wrapDecorations(text, stack) {
721
+ let node = text;
722
+ for (let i = stack.length - 1; i >= 0; i--) {
723
+ const layer = stack[i];
724
+ node =
725
+ layer.value !== undefined
726
+ ? { className: layer.className, content: [node], value: layer.value }
727
+ : { className: layer.className, content: [node] };
728
+ }
729
+ return node;
730
+ }
731
+ /**
732
+ * Split a run stream into per-block segments at newline boundaries (Whitespace
733
+ * authority — the block-structure half ADR-183/`createBlocks` owned). A single
734
+ * `\n` makes the next block a `tight` continuation; a blank line (`\n\n+`) starts
735
+ * a fresh paragraph. Newlines split every run — verbatim runs keep their
736
+ * *horizontal* whitespace but still break into blocks, matching the legacy
737
+ * `createBlocks` behaviour. No block's content carries a `\n`.
738
+ */
739
+ function splitRunsOnNewlines(runs) {
740
+ const segments = [];
741
+ let current = [];
742
+ let tight = false; // the first segment is never a tight continuation
743
+ for (const run of runs) {
744
+ const pieces = run.text.split(/(\n+)/); // keep the newline groups as separators
745
+ for (const piece of pieces) {
746
+ if (piece === '')
747
+ continue;
748
+ if (/^\n+$/.test(piece)) {
749
+ segments.push({ runs: current, tight });
750
+ current = [];
751
+ tight = piece.length === 1; // single \n → tight; blank line → paragraph
752
+ }
753
+ else {
754
+ current.push({ ...run, text: piece });
755
+ }
756
+ }
757
+ }
758
+ segments.push({ runs: current, tight });
759
+ return segments;
760
+ }
761
+ /**
762
+ * The English Assembler. Realizes a phrase tree to text blocks under the default
763
+ * channel key; the report layer re-keys per channel.
764
+ */
765
+ class EnglishAssembler {
766
+ /**
767
+ * Realize a phrase tree to text blocks.
768
+ *
769
+ * Newlines in the realized text are lifted to block boundaries (no block's
770
+ * content carries `\n`); horizontal whitespace is collapsed per block, except
771
+ * in verbatim runs. A single-line tree yields one block.
772
+ *
773
+ * @param tree the phrase tree to realize
774
+ * @param ctx the render context (world, params, settings, seams)
775
+ * @returns the realized text blocks
776
+ * @throws PhraseNotImplementedError when a reserved stub kind is encountered
777
+ */
778
+ realize(tree, ctx) {
779
+ let runs = realizeToRuns(tree, ctx, []);
780
+ // Honor the optional position seam (ADR-201 §4): a context that declares it
781
+ // starts sentence-initial flags the first word for capitalization. Absent →
782
+ // not sentence-initial (today's behavior), so existing render paths are unaffected.
783
+ if (ctx.position?.sentenceInitial)
784
+ runs = markFirstSentenceInitial(runs);
785
+ const segments = splitRunsOnNewlines(reconciliationPass(runs));
786
+ const blocks = [];
787
+ for (const seg of segments) {
788
+ const content = runsToContent(collapseWhitespace(seg.runs));
789
+ // Drop blank lines — they leave no block (paragraph spacing comes from
790
+ // the `tight` flag on the following block).
791
+ if (content.length === 1 && content[0] === '')
792
+ continue;
793
+ blocks.push({
794
+ key: exports.ASSEMBLER_DEFAULT_BLOCK_KEY,
795
+ content,
796
+ ...(seg.tight ? { tight: true } : {}),
797
+ });
798
+ }
799
+ if (blocks.length === 0) {
800
+ blocks.push({ key: exports.ASSEMBLER_DEFAULT_BLOCK_KEY, content: [''] });
801
+ }
802
+ return blocks;
803
+ }
804
+ }
805
+ exports.EnglishAssembler = EnglishAssembler;
806
+ //# sourceMappingURL=english-assembler.js.map