@proposit/proposit-core 0.12.1 → 1.0.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 (175) hide show
  1. package/README.md +104 -93
  2. package/dist/cli/commands/premises.d.ts.map +1 -1
  3. package/dist/cli/commands/premises.js +28 -24
  4. package/dist/cli/commands/premises.js.map +1 -1
  5. package/dist/cli/commands/repair.d.ts.map +1 -1
  6. package/dist/cli/commands/repair.js +4 -2
  7. package/dist/cli/commands/repair.js.map +1 -1
  8. package/dist/cli/commands/validate.d.ts.map +1 -1
  9. package/dist/cli/commands/validate.js +7 -1
  10. package/dist/cli/commands/validate.js.map +1 -1
  11. package/dist/cli/engine.d.ts.map +1 -1
  12. package/dist/cli/engine.js +7 -25
  13. package/dist/cli/engine.js.map +1 -1
  14. package/dist/cli/import.d.ts.map +1 -1
  15. package/dist/cli/import.js +66 -28
  16. package/dist/cli/import.js.map +1 -1
  17. package/dist/cli/schemata.d.ts.map +1 -1
  18. package/dist/cli/schemata.js +4 -3
  19. package/dist/cli/schemata.js.map +1 -1
  20. package/dist/extensions/basics/schemata.d.ts +56 -2
  21. package/dist/extensions/basics/schemata.d.ts.map +1 -1
  22. package/dist/extensions/basics/schemata.js +54 -23
  23. package/dist/extensions/basics/schemata.js.map +1 -1
  24. package/dist/lib/core/argument-engine.d.ts +275 -10
  25. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  26. package/dist/lib/core/argument-engine.js +442 -82
  27. package/dist/lib/core/argument-engine.js.map +1 -1
  28. package/dist/lib/core/argument-library.d.ts +5 -1
  29. package/dist/lib/core/argument-library.d.ts.map +1 -1
  30. package/dist/lib/core/argument-library.js +7 -3
  31. package/dist/lib/core/argument-library.js.map +1 -1
  32. package/dist/lib/core/claim-axiom-library.d.ts +3 -3
  33. package/dist/lib/core/claim-axiom-library.d.ts.map +1 -1
  34. package/dist/lib/core/claim-axiom-library.js +2 -2
  35. package/dist/lib/core/claim-axiom-library.js.map +1 -1
  36. package/dist/lib/core/claim-citation-library.d.ts +3 -3
  37. package/dist/lib/core/claim-citation-library.d.ts.map +1 -1
  38. package/dist/lib/core/claim-citation-library.js +2 -2
  39. package/dist/lib/core/claim-citation-library.js.map +1 -1
  40. package/dist/lib/core/expression-manager.d.ts +68 -73
  41. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  42. package/dist/lib/core/expression-manager.js +242 -762
  43. package/dist/lib/core/expression-manager.js.map +1 -1
  44. package/dist/lib/core/fork.d.ts +5 -1
  45. package/dist/lib/core/fork.d.ts.map +1 -1
  46. package/dist/lib/core/fork.js +16 -6
  47. package/dist/lib/core/fork.js.map +1 -1
  48. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +68 -7
  49. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  50. package/dist/lib/core/interfaces/library.interfaces.d.ts +9 -6
  51. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  52. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +50 -47
  53. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  54. package/dist/lib/core/premise-engine.d.ts +80 -11
  55. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  56. package/dist/lib/core/premise-engine.js +232 -80
  57. package/dist/lib/core/premise-engine.js.map +1 -1
  58. package/dist/lib/core/proposit-core.d.ts +5 -5
  59. package/dist/lib/core/proposit-core.d.ts.map +1 -1
  60. package/dist/lib/core/proposit-core.js +13 -3
  61. package/dist/lib/core/proposit-core.js.map +1 -1
  62. package/dist/lib/grammar/an-rules.d.ts +158 -0
  63. package/dist/lib/grammar/an-rules.d.ts.map +1 -0
  64. package/dist/lib/grammar/an-rules.js +778 -0
  65. package/dist/lib/grammar/an-rules.js.map +1 -0
  66. package/dist/lib/grammar/auto-normalize.d.ts +14 -0
  67. package/dist/lib/grammar/auto-normalize.d.ts.map +1 -0
  68. package/dist/lib/grammar/auto-normalize.js +35 -0
  69. package/dist/lib/grammar/auto-normalize.js.map +1 -0
  70. package/dist/lib/grammar/bounded-subtree.d.ts +30 -0
  71. package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -0
  72. package/dist/lib/grammar/bounded-subtree.js +74 -0
  73. package/dist/lib/grammar/bounded-subtree.js.map +1 -0
  74. package/dist/lib/grammar/naked-q.d.ts +20 -0
  75. package/dist/lib/grammar/naked-q.d.ts.map +1 -0
  76. package/dist/lib/grammar/naked-q.js +58 -0
  77. package/dist/lib/grammar/naked-q.js.map +1 -0
  78. package/dist/lib/grammar/normalize.d.ts +12 -0
  79. package/dist/lib/grammar/normalize.d.ts.map +1 -0
  80. package/dist/lib/grammar/normalize.js +45 -0
  81. package/dist/lib/grammar/normalize.js.map +1 -0
  82. package/dist/lib/grammar/populate-from.d.ts +25 -0
  83. package/dist/lib/grammar/populate-from.d.ts.map +1 -0
  84. package/dist/lib/grammar/populate-from.js +252 -0
  85. package/dist/lib/grammar/populate-from.js.map +1 -0
  86. package/dist/lib/grammar/repair.d.ts +65 -0
  87. package/dist/lib/grammar/repair.d.ts.map +1 -0
  88. package/dist/lib/grammar/repair.js +251 -0
  89. package/dist/lib/grammar/repair.js.map +1 -0
  90. package/dist/lib/grammar/types.d.ts +17 -0
  91. package/dist/lib/grammar/types.d.ts.map +1 -0
  92. package/dist/lib/grammar/types.js +82 -0
  93. package/dist/lib/grammar/types.js.map +1 -0
  94. package/dist/lib/grammar/validate.d.ts +4 -0
  95. package/dist/lib/grammar/validate.d.ts.map +1 -0
  96. package/dist/lib/grammar/validate.js +28 -0
  97. package/dist/lib/grammar/validate.js.map +1 -0
  98. package/dist/lib/grammar/validators/context.d.ts +11 -0
  99. package/dist/lib/grammar/validators/context.d.ts.map +1 -0
  100. package/dist/lib/grammar/validators/context.js +5 -0
  101. package/dist/lib/grammar/validators/context.js.map +1 -0
  102. package/dist/lib/grammar/validators/derivable.d.ts +63 -0
  103. package/dist/lib/grammar/validators/derivable.d.ts.map +1 -0
  104. package/dist/lib/grammar/validators/derivable.js +502 -0
  105. package/dist/lib/grammar/validators/derivable.js.map +1 -0
  106. package/dist/lib/grammar/validators/evaluable.d.ts +48 -0
  107. package/dist/lib/grammar/validators/evaluable.d.ts.map +1 -0
  108. package/dist/lib/grammar/validators/evaluable.js +226 -0
  109. package/dist/lib/grammar/validators/evaluable.js.map +1 -0
  110. package/dist/lib/grammar/validators/presentable.d.ts +45 -0
  111. package/dist/lib/grammar/validators/presentable.d.ts.map +1 -0
  112. package/dist/lib/grammar/validators/presentable.js +231 -0
  113. package/dist/lib/grammar/validators/presentable.js.map +1 -0
  114. package/dist/lib/grammar/validators/structural.d.ts +103 -0
  115. package/dist/lib/grammar/validators/structural.d.ts.map +1 -0
  116. package/dist/lib/grammar/validators/structural.js +602 -0
  117. package/dist/lib/grammar/validators/structural.js.map +1 -0
  118. package/dist/lib/index.d.ts +4 -3
  119. package/dist/lib/index.d.ts.map +1 -1
  120. package/dist/lib/index.js +2 -2
  121. package/dist/lib/index.js.map +1 -1
  122. package/dist/lib/parsing/argument-parser.d.ts +8 -5
  123. package/dist/lib/parsing/argument-parser.d.ts.map +1 -1
  124. package/dist/lib/parsing/argument-parser.js +204 -42
  125. package/dist/lib/parsing/argument-parser.js.map +1 -1
  126. package/dist/lib/parsing/clamp-max-lengths.d.ts.map +1 -1
  127. package/dist/lib/parsing/clamp-max-lengths.js +10 -4
  128. package/dist/lib/parsing/clamp-max-lengths.js.map +1 -1
  129. package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
  130. package/dist/lib/parsing/prompt-builder.js +13 -15
  131. package/dist/lib/parsing/prompt-builder.js.map +1 -1
  132. package/dist/lib/parsing/schemata.d.ts +0 -3
  133. package/dist/lib/parsing/schemata.d.ts.map +1 -1
  134. package/dist/lib/parsing/schemata.js +25 -13
  135. package/dist/lib/parsing/schemata.js.map +1 -1
  136. package/dist/lib/parsing/types.d.ts +1 -1
  137. package/dist/lib/parsing/types.d.ts.map +1 -1
  138. package/dist/lib/schemata/claim.d.ts +8 -0
  139. package/dist/lib/schemata/claim.d.ts.map +1 -1
  140. package/dist/lib/schemata/claim.js +17 -7
  141. package/dist/lib/schemata/claim.js.map +1 -1
  142. package/dist/lib/schemata/import.d.ts.map +1 -1
  143. package/dist/lib/schemata/import.js +2 -5
  144. package/dist/lib/schemata/import.js.map +1 -1
  145. package/dist/lib/schemata/index.d.ts +0 -2
  146. package/dist/lib/schemata/index.d.ts.map +1 -1
  147. package/dist/lib/schemata/index.js +0 -2
  148. package/dist/lib/schemata/index.js.map +1 -1
  149. package/dist/lib/types/evaluation.d.ts +1 -1
  150. package/dist/lib/types/evaluation.d.ts.map +1 -1
  151. package/dist/lib/types/fork.d.ts +12 -3
  152. package/dist/lib/types/fork.d.ts.map +1 -1
  153. package/dist/lib/types/validation.d.ts +0 -6
  154. package/dist/lib/types/validation.d.ts.map +1 -1
  155. package/dist/lib/types/validation.js +23 -6
  156. package/dist/lib/types/validation.js.map +1 -1
  157. package/dist/lib/utils/lookup.d.ts +2 -2
  158. package/dist/lib/utils/lookup.js +2 -2
  159. package/package.json +1 -1
  160. package/dist/lib/core/managed-derivation-premise-engine.d.ts +0 -172
  161. package/dist/lib/core/managed-derivation-premise-engine.d.ts.map +0 -1
  162. package/dist/lib/core/managed-derivation-premise-engine.js +0 -550
  163. package/dist/lib/core/managed-derivation-premise-engine.js.map +0 -1
  164. package/dist/lib/schemata/claim-axiom.d.ts +0 -11
  165. package/dist/lib/schemata/claim-axiom.d.ts.map +0 -1
  166. package/dist/lib/schemata/claim-axiom.js +0 -9
  167. package/dist/lib/schemata/claim-axiom.js.map +0 -1
  168. package/dist/lib/schemata/claim-citation.d.ts +0 -11
  169. package/dist/lib/schemata/claim-citation.d.ts.map +0 -1
  170. package/dist/lib/schemata/claim-citation.js +0 -9
  171. package/dist/lib/schemata/claim-citation.js.map +0 -1
  172. package/dist/lib/types/grammar.d.ts +0 -83
  173. package/dist/lib/types/grammar.d.ts.map +0 -1
  174. package/dist/lib/types/grammar.js +0 -24
  175. package/dist/lib/types/grammar.js.map +0 -1
@@ -0,0 +1,778 @@
1
+ // AN rule set — native implementations of AN-1..AN-4.
2
+ //
3
+ // Per spec §5.1 the auto-normalization rule set consists of four local
4
+ // cleanup rules:
5
+ //
6
+ // AN-1 Insert formula buffer when a non-`not` operator becomes a
7
+ // direct child of another operator. Preserves P-1.
8
+ // AN-2 Collapse double negation (NOT(NOT(x)) → x). Preserves P-2.
9
+ // AN-3 Collapse 0-child operator/formula (recursing to grandparent);
10
+ // promote single child where the parent is non-meaningful.
11
+ // Preserves P-3 and P-4 (incidentally E-1).
12
+ // AN-4 Absorb same-operator adjacency through a formula. Preserves
13
+ // P-5.
14
+ //
15
+ // This module is the **native home** of the rule set for v1.0. Phase D
16
+ // owns this module: D0 (this file) lifts the rules out of
17
+ // `ExpressionManager.normalize()` and into the grammar module so the AN
18
+ // pass is no longer coupled to the legacy `grammarConfig` machinery; D2
19
+ // removes the legacy plumbing entirely (the per-mutation AN inside
20
+ // `ExpressionManager` mutation methods + the 11 P-1 throw sites — see
21
+ // the plan's Phase D summary).
22
+ //
23
+ // **D0e — AN-1 + AN-2 + AN-3 + AN-4 all native.** `applyAN1`,
24
+ // `applyAN2`, `applyAN3`, and `applyAN4` implement their rules
25
+ // directly against the public `PremiseEngine` mutation API. AN-1 uses
26
+ // `pe.wrapInFormula(childOpId, formulaId)` to atomically insert a
27
+ // formula buffer between an operator parent and a non-`not` operator
28
+ // child (composing this from `addExpression` + `reparentExpression`
29
+ // would trip S-9 transiently and violate parent child-limits under
30
+ // unary `not` and binary `implies`/`iff` parents — see the
31
+ // `wrapInFormula` JSDoc on PE for the full argument). AN-2 and AN-3
32
+ // use `pe.removeExpression(id, false)`. AN-4 uses
33
+ // `pe.reparentExpression(c_i, outerId, position_i)` + a final
34
+ // `pe.removeExpression(formula, false)` cleanup. The four `applyAN*`
35
+ // exports no longer delegate to the legacy `pe.normalizeExpressions()`
36
+ // full sweep.
37
+ //
38
+ // **D0f.** `applyANToFixedPoint` switched from a `||` short-circuit
39
+ // chain (one rule per outer iteration) to a reduce-or accumulator (all
40
+ // four rules per outer iteration, OR'd into the changed flag) —
41
+ // synthesis P2 #2 carry-over. The PERMISSIVE config-swap that disarms
42
+ // the 11 legacy inline P-1 enforcement throws moved from `normalize.ts`
43
+ // into `applyANToFixedPoint` itself so both `runAssistiveNormalization`
44
+ // (post-mutation hook in assistive mode) and `normalizeArgument`
45
+ // (`engine.normalize()`) benefit automatically. D2 removes the legacy
46
+ // per-flag `grammarConfig` machinery and the 11 P-1 throw sites the
47
+ // swap currently works around.
48
+ //
49
+ // The per-rule tests (`test/grammar/an-rules.test.ts`) assert behavior
50
+ // the native implementation must preserve once it lands; today they
51
+ // pass because the delegated implementation already produces that
52
+ // behavior. Each test is a regression guard for the eventual native
53
+ // rewrite.
54
+ import { hasBinaryOperatorInBoundedSubtree } from "./bounded-subtree.js";
55
+ /**
56
+ * Convergence safety cap — typically AN converges in ≤ 3 iterations
57
+ * because the rules are local and idempotent in combination. The cap is
58
+ * a defense against pathological inputs (e.g. a malformed Structural
59
+ * state that would otherwise oscillate). When the cap is hit the
60
+ * implementation throws an InvariantViolationError-shaped Error so
61
+ * regressions surface loudly rather than silently truncating.
62
+ */
63
+ const MAX_AN_ITERATIONS = 10;
64
+ /**
65
+ * Run AN-2 (collapse double negation) on every premise of `engine`.
66
+ * Returns `true` iff any mutation occurred.
67
+ *
68
+ * **D0b: native rewrite.** Walks each premise's expression tree
69
+ * looking for NOT(NOT(x)) — both the direct form (`NOT_outer →
70
+ * NOT_inner → x`) and the buffered form (`NOT_outer → formula →
71
+ * NOT_inner → x`). For each match issues two
72
+ * `pe.removeExpression(id, false)` calls that promote the
73
+ * grandchild (and, in the buffered case, the residual formula)
74
+ * through the two NOT layers.
75
+ *
76
+ * The buffered case leaves an unjustified `formula(x)` residue
77
+ * which AN-3 cleans up in a subsequent iteration of
78
+ * `applyANToFixedPoint`. AN-2 stays focused on the NOT-NOT
79
+ * collapse itself — no formula bookkeeping.
80
+ *
81
+ * Behavior parity with the legacy `ExpressionManager.normalize()`
82
+ * pass 4 is asserted by the regression-guard tests in
83
+ * `test/grammar/an-rules.test.ts` and the broader 1598-test
84
+ * baseline (which exercises double-negation collapse via
85
+ * `pe.normalizeExpressions()` and `engine.normalize()`).
86
+ *
87
+ * @since 1.0.0
88
+ */
89
+ export function applyAN2(engine) {
90
+ let anyChanged = false;
91
+ for (const pe of engine.listPremises()) {
92
+ // Loop until no AN-2 pattern remains in this premise. Cascading
93
+ // NOT chains (NOT-NOT-NOT-NOT-x) need multiple sweeps to fully
94
+ // collapse; doing them in one call keeps the outer
95
+ // fixed-point driver simple.
96
+ let premiseChanged = true;
97
+ while (premiseChanged) {
98
+ premiseChanged = collapseOneDoubleNegationInPremise(pe);
99
+ if (premiseChanged)
100
+ anyChanged = true;
101
+ }
102
+ }
103
+ return anyChanged;
104
+ }
105
+ /**
106
+ * Find one NOT(NOT(x)) pattern in `pe`'s tree (direct or buffered)
107
+ * and collapse it. Returns `true` iff a collapse occurred.
108
+ *
109
+ * Each call collapses exactly one pattern. The caller loops until
110
+ * no patterns remain.
111
+ */
112
+ function collapseOneDoubleNegationInPremise(pe) {
113
+ for (const expr of pe.getExpressions()) {
114
+ if (expr.type !== "operator" || expr.operator !== "not")
115
+ continue;
116
+ const children = pe.getChildExpressions(expr.id);
117
+ if (children.length !== 1)
118
+ continue;
119
+ const child = children[0];
120
+ // Direct: NOT_outer → NOT_inner → x
121
+ if (child.type === "operator" && child.operator === "not") {
122
+ const innerChildren = pe.getChildExpressions(child.id);
123
+ if (innerChildren.length !== 1)
124
+ continue;
125
+ // Promote x into inner NOT's slot, then promote x into
126
+ // outer NOT's slot. Two removeExpression(_, false) calls.
127
+ pe.removeExpression(child.id, false);
128
+ pe.removeExpression(expr.id, false);
129
+ return true;
130
+ }
131
+ // Buffered: NOT_outer → formula → NOT_inner → x
132
+ if (child.type === "formula") {
133
+ const formulaChildren = pe.getChildExpressions(child.id);
134
+ if (formulaChildren.length !== 1)
135
+ continue;
136
+ const innerNot = formulaChildren[0];
137
+ if (innerNot.type !== "operator" || innerNot.operator !== "not") {
138
+ continue;
139
+ }
140
+ const innerChildren = pe.getChildExpressions(innerNot.id);
141
+ if (innerChildren.length !== 1)
142
+ continue;
143
+ // Promote x into inner NOT's slot (formula now wraps x),
144
+ // then promote formula into outer NOT's slot. The
145
+ // residual `formula(x)` is unjustified (no binary
146
+ // operator in its bounded subtree) and AN-3 collapses
147
+ // it in a subsequent fixed-point iteration.
148
+ pe.removeExpression(innerNot.id, false);
149
+ pe.removeExpression(expr.id, false);
150
+ return true;
151
+ }
152
+ }
153
+ return false;
154
+ }
155
+ /**
156
+ * Run AN-3 (collapse 0/1-child operator/formula) on every premise of
157
+ * `engine`. Returns `true` iff any mutation occurred.
158
+ *
159
+ * **D0c: native rewrite.** Walks each premise's expression tree and
160
+ * collapses four sub-cases via `pe.removeExpression(id, false)`:
161
+ *
162
+ * 1. Operator with 0 children → removed (leaf removal).
163
+ * 2. Operator with 1 child (non-`not`) → child promoted into the
164
+ * operator's slot. `not` is unary so 1-child `not` is its
165
+ * Presentable form and is NOT collapsed by AN-3.
166
+ * 3. Formula with 0 children → removed.
167
+ * 4. Formula with 1 child whose bounded subtree contains no binary
168
+ * operator (`and`/`or`) → child promoted (the formula is
169
+ * unjustified per P-3, so it disappears).
170
+ *
171
+ * Bounded-subtree traversal stops at nested formulas (each formula
172
+ * is a separate P-3 scope). The shared
173
+ * `hasBinaryOperatorInBoundedSubtree` helper in
174
+ * `src/lib/grammar/bounded-subtree.ts` is used by both this rule and
175
+ * the P-3 validator; AN-3 binds its lookup function to
176
+ * `pe.getChildExpressions(id)` so it sees live mid-mutation reads,
177
+ * while the validator binds to a snapshot `TChildMap`. The lift
178
+ * resolves the pre-D0e duplication (D0a P2 #3 / D0d P2 #3).
179
+ *
180
+ * Behavior parity with the legacy `ExpressionManager.normalize()`
181
+ * passes 1 + 2 is asserted by the regression-guard tests in
182
+ * `test/grammar/an-rules.test.ts` and the broader 1603-test
183
+ * baseline.
184
+ *
185
+ * @since 1.0.0
186
+ */
187
+ export function applyAN3(engine) {
188
+ let anyChanged = false;
189
+ for (const pe of engine.listPremises()) {
190
+ let premiseChanged = true;
191
+ while (premiseChanged) {
192
+ premiseChanged = collapseOneAN3InPremise(pe);
193
+ if (premiseChanged)
194
+ anyChanged = true;
195
+ }
196
+ }
197
+ return anyChanged;
198
+ }
199
+ /**
200
+ * Find one AN-3 candidate in `pe`'s tree and collapse it. Returns
201
+ * `true` iff a collapse occurred.
202
+ *
203
+ * Each call collapses exactly one node. The caller loops until the
204
+ * tree is stable. The single-collapse-per-call shape mirrors AN-2 so
205
+ * cascading mutations don't trip mid-iteration tree-walk invariants.
206
+ */
207
+ function collapseOneAN3InPremise(pe) {
208
+ for (const expr of pe.getExpressions()) {
209
+ // Sub-case 1 & 2: operator collapse.
210
+ if (expr.type === "operator") {
211
+ const children = pe.getChildExpressions(expr.id);
212
+ if (children.length === 0) {
213
+ // 0-child operator — remove wholesale.
214
+ pe.removeExpression(expr.id, false);
215
+ return true;
216
+ }
217
+ if (children.length === 1 && expr.operator !== "not") {
218
+ // 1-child non-not operator — promote single child.
219
+ pe.removeExpression(expr.id, false);
220
+ return true;
221
+ }
222
+ continue;
223
+ }
224
+ // Sub-case 3 & 4: formula collapse.
225
+ if (expr.type === "formula") {
226
+ const children = pe.getChildExpressions(expr.id);
227
+ if (children.length === 0) {
228
+ // 0-child formula — remove wholesale.
229
+ pe.removeExpression(expr.id, false);
230
+ return true;
231
+ }
232
+ if (children.length === 1 &&
233
+ !hasBinaryOperatorInBoundedSubtree(children[0].id, (id) => pe.getChildExpressions(id), (id) => pe.getExpression(id))) {
234
+ // Unjustified formula (no binary operator in bounded
235
+ // subtree) — promote single child.
236
+ pe.removeExpression(expr.id, false);
237
+ return true;
238
+ }
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+ /**
244
+ * Run AN-4 (absorb same-operator through formula) on every premise of
245
+ * `engine`. Returns `true` iff any mutation occurred.
246
+ *
247
+ * **D0e: native rewrite.** Walks each premise's expression tree for
248
+ * the absorption shape `OUTER_OP → (..., ) formula → INNER_OP (same
249
+ * operator) → [c1, c2, …, cN]`, with both operators being `and` or
250
+ * `or` (S-5 restricts `implies`/`iff` to roots, so they never appear in
251
+ * AN-4-firing positions). For each match:
252
+ *
253
+ * 1. Compute target positions for the N inner children under the
254
+ * outer operator using the legacy spacing algorithm from
255
+ * `ExpressionManager.absorbSameOperator` (em.ts:1240-1349):
256
+ * `leftPos + ((rightPos - leftPos) / (count + 1)) * (i + 1)`,
257
+ * truncated to integers. `leftPos` and `rightPos` are the
258
+ * formula's outer neighbors (or `positionConfig.min`/`max` at
259
+ * boundaries).
260
+ * 2. If `gap = rightPos - leftPos <= count` (tight neighborhood
261
+ * where N evenly-spaced positions would collide), fall back to a
262
+ * **full redistribution**: renumber every existing sibling of the
263
+ * formula evenly across `positionConfig.min..max`, then redo the
264
+ * spacing on the refreshed boundaries before reparenting.
265
+ * 3. Reparent each inner child to the outer operator at its computed
266
+ * target position via `pe.reparentExpression`.
267
+ * 4. Remove the now-empty inner operator via
268
+ * `pe.removeExpression(inner.id, true)` (deleteSubtree is fine —
269
+ * it has zero children).
270
+ * 5. Remove the now-empty formula wrapper via
271
+ * `pe.removeExpression(formula.id, false)`. Hits the 0-child
272
+ * leaf-removal branch of `removeAndPromote` so the inline P-1
273
+ * enforcement throw is not reached.
274
+ *
275
+ * Identity preservation: each absorbed child's expression id survives
276
+ * the operation — `reparentExpression` mutates the position/parentId
277
+ * fields atomically without minting new ids. Asserted by the contract
278
+ * regression-guard tests in `test/grammar/an-rules.test.ts:559-893`.
279
+ *
280
+ * @since 1.0.0
281
+ */
282
+ export function applyAN4(engine) {
283
+ let anyChanged = false;
284
+ for (const pe of engine.listPremises()) {
285
+ let premiseChanged = true;
286
+ while (premiseChanged) {
287
+ premiseChanged = absorbOneSameOperatorInPremise(pe);
288
+ if (premiseChanged)
289
+ anyChanged = true;
290
+ }
291
+ }
292
+ return anyChanged;
293
+ }
294
+ /**
295
+ * Find one AN-4 candidate (inner OP whose parent is a formula whose
296
+ * parent is the same-typed outer OP) in `pe`'s tree and absorb it.
297
+ * Returns `true` iff an absorption occurred.
298
+ *
299
+ * Each call absorbs exactly one inner operator. The caller loops until
300
+ * the tree is stable. The single-absorption-per-call shape matches
301
+ * AN-2/AN-3 so cascading mutations don't trip mid-iteration tree-walk
302
+ * invariants.
303
+ */
304
+ function absorbOneSameOperatorInPremise(pe) {
305
+ for (const inner of pe.getExpressions()) {
306
+ // Only and/or inner operators absorb (S-5 keeps implies/iff at
307
+ // root; not is unary so absorption doesn't apply).
308
+ if (inner.type !== "operator")
309
+ continue;
310
+ if (inner.operator !== "and" && inner.operator !== "or")
311
+ continue;
312
+ const formulaId = inner.parentId;
313
+ if (formulaId === null)
314
+ continue;
315
+ const formula = pe.getExpression(formulaId);
316
+ if (!formula || formula.type !== "formula")
317
+ continue;
318
+ const outerId = formula.parentId;
319
+ if (outerId === null)
320
+ continue;
321
+ const outer = pe.getExpression(outerId);
322
+ if (!outer || outer.type !== "operator")
323
+ continue;
324
+ if (outer.operator !== inner.operator)
325
+ continue;
326
+ // Pattern matches — absorb.
327
+ absorbSameOperatorMatch(pe, inner.id, formulaId, outerId);
328
+ return true;
329
+ }
330
+ return false;
331
+ }
332
+ /**
333
+ * Execute one AN-4 absorption: move every child of `innerId` to be a
334
+ * direct child of `outerId` at spacing-algorithm positions between the
335
+ * formula's outer neighbors; remove `innerId` and `formulaId`.
336
+ *
337
+ * Ports the position-spacing + redistribution-fallback algorithm from
338
+ * `ExpressionManager.absorbSameOperator` (em.ts:1240-1349) to the
339
+ * public PE-mutation surface.
340
+ */
341
+ function absorbSameOperatorMatch(pe, innerId, formulaId, outerId) {
342
+ const positionConfig = pe.getPositionConfig();
343
+ // Capture inner children up front — we'll reparent each one. Use
344
+ // the live ordering (getChildExpressions returns sorted by
345
+ // position) so the target ordering under `outerId` matches the
346
+ // inner's pre-absorption ordering.
347
+ const innerChildren = pe.getChildExpressions(innerId);
348
+ const count = innerChildren.length;
349
+ // Determine left/right neighbors of the formula under `outerId`.
350
+ const outerChildren = pe.getChildExpressions(outerId);
351
+ const formulaIdx = outerChildren.findIndex((c) => c.id === formulaId);
352
+ const leftPos = formulaIdx > 0
353
+ ? outerChildren[formulaIdx - 1].position
354
+ : positionConfig.min;
355
+ const rightPos = formulaIdx < outerChildren.length - 1
356
+ ? outerChildren[formulaIdx + 1].position
357
+ : positionConfig.max;
358
+ // Tight-neighborhood guard: if `rightPos - leftPos <= count`, even
359
+ // spacing collides. Redistribute all of outer's children evenly
360
+ // across the full positionConfig range first, then re-compute the
361
+ // formula's neighbors on the refreshed positions.
362
+ const gap = rightPos - leftPos;
363
+ let effectiveLeftPos = leftPos;
364
+ let effectiveRightPos = rightPos;
365
+ if (gap <= count) {
366
+ redistributeChildrenEvenly(pe, outerId, positionConfig);
367
+ const refreshedOuter = pe.getChildExpressions(outerId);
368
+ const refreshedFormulaIdx = refreshedOuter.findIndex((c) => c.id === formulaId);
369
+ effectiveLeftPos =
370
+ refreshedFormulaIdx > 0
371
+ ? refreshedOuter[refreshedFormulaIdx - 1].position
372
+ : positionConfig.min;
373
+ effectiveRightPos =
374
+ refreshedFormulaIdx < refreshedOuter.length - 1
375
+ ? refreshedOuter[refreshedFormulaIdx + 1].position
376
+ : positionConfig.max;
377
+ }
378
+ // Reparent each inner child into the outer at its computed
379
+ // position. Naive position formula (from the legacy AN-4):
380
+ // `leftPos + ((rightPos - leftPos) / (count + 1)) * (i + 1)`,
381
+ // truncated to integer.
382
+ //
383
+ // **D2 — D1 P2 #1 carry-over: forbidden-set walk.** A naive
384
+ // computed target can collide with two kinds of obstacles, both
385
+ // of which would trip S-9 inside `pe.reparentExpression`:
386
+ //
387
+ // (a) **The formula's current position.** The formula is still
388
+ // a child of `outerId` at this point — we only remove it
389
+ // *after* the inner reparents. After
390
+ // `redistributeChildrenEvenly` fires the formula sits at
391
+ // one of the redistributed slots between `effectiveLeftPos`
392
+ // and `effectiveRightPos`; on the non-redistribute path
393
+ // the formula keeps its pre-mutation position. Either way,
394
+ // any naive target equal to the formula's position would
395
+ // collide.
396
+ //
397
+ // (b) **Already-placed inner-child target positions.** When
398
+ // `gap = count + 1` (integer spacing 1) the D1 ±1 shift
399
+ // moved a colliding target to the slot reserved for the
400
+ // *next* planned target — the next iteration then tripped
401
+ // S-9 against the just-placed inner child.
402
+ //
403
+ // The fix tracks both obstacles in a single `forbidden` set: the
404
+ // formula's current position seeds the set, and each chosen
405
+ // target gets added as it's planned. For any computed target
406
+ // already in `forbidden`, scan outward (±1, ±2, ...) for the
407
+ // first free integer strictly inside `(effectiveLeftPos,
408
+ // effectiveRightPos)`. The band-exhaustion case (no free integer
409
+ // anywhere in the band) is structurally reachable only when the
410
+ // position keyspace is deeply pathological — throw with a
411
+ // diagnostic that names the forbidden set so the next dev can
412
+ // triage.
413
+ const refreshedFormula = pe.getExpression(formulaId);
414
+ const formulaCurrentPosition = refreshedFormula
415
+ ? refreshedFormula.position
416
+ : null;
417
+ const forbidden = new Set();
418
+ if (formulaCurrentPosition !== null) {
419
+ forbidden.add(formulaCurrentPosition);
420
+ }
421
+ for (let i = 0; i < count; i++) {
422
+ const naiveTarget = Math.trunc(effectiveLeftPos +
423
+ ((effectiveRightPos - effectiveLeftPos) / (count + 1)) * (i + 1));
424
+ let chosen = naiveTarget;
425
+ if (forbidden.has(chosen)) {
426
+ // Scan outward in expanding ±k rings for a free integer
427
+ // strictly inside `(effectiveLeftPos, effectiveRightPos)`.
428
+ // Down-first then up-first per ring matches the prior ±1
429
+ // shift's "shift toward effectiveLeftPos when target is
430
+ // left of midpoint" bias without depending on the
431
+ // midpoint calculation (the integer-truncation boundary
432
+ // doesn't change ring search behavior).
433
+ const bandWidth = effectiveRightPos - effectiveLeftPos;
434
+ let found = false;
435
+ for (let k = 1; k < bandWidth; k++) {
436
+ const down = naiveTarget - k;
437
+ if (down > effectiveLeftPos && !forbidden.has(down)) {
438
+ chosen = down;
439
+ found = true;
440
+ break;
441
+ }
442
+ const up = naiveTarget + k;
443
+ if (up < effectiveRightPos && !forbidden.has(up)) {
444
+ chosen = up;
445
+ found = true;
446
+ break;
447
+ }
448
+ }
449
+ if (!found) {
450
+ const sortedForbidden = [...forbidden]
451
+ .sort((a, b) => a - b)
452
+ .join(", ");
453
+ throw new Error(`AN-4 absorbSameOperatorMatch: no free position in ` +
454
+ `(${effectiveLeftPos}, ${effectiveRightPos}) for ` +
455
+ `inner child #${i} of "${innerId}" under "${outerId}" ` +
456
+ `(forbidden: [${sortedForbidden}]). ` +
457
+ `Position keyspace is over-saturated — argument ` +
458
+ `may be in a structurally pathological state.`);
459
+ }
460
+ }
461
+ forbidden.add(chosen);
462
+ pe.reparentExpression(innerChildren[i].id, outerId, chosen);
463
+ }
464
+ // Inner is now empty. Remove with deleteSubtree=true (no subtree
465
+ // remains, so this is a leaf removal).
466
+ pe.removeExpression(innerId, true);
467
+ // Formula is now empty (inner was its only child). Removing with
468
+ // deleteSubtree=false routes through removeAndPromote's
469
+ // 0-child leaf-removal branch — no promotion attempted, so the
470
+ // inline P-1 enforcement throw at em.ts:863-876 cannot fire.
471
+ pe.removeExpression(formulaId, false);
472
+ }
473
+ /**
474
+ * Redistribute every existing child of `parentId` evenly across the
475
+ * full position range (`positionConfig.min..max`). Used by AN-4's
476
+ * tight-neighborhood fallback when the formula's left/right neighbors
477
+ * sit so close that N evenly-spaced positions would collide.
478
+ *
479
+ * Implementation routes through `pe.reparentExpression(child.id,
480
+ * parentId, newPos)` so the operation is observable via the public
481
+ * mutation API and respects S-9 (which tolerates same-parent moves —
482
+ * see `PremiseEngine.reparentExpression` JSDoc).
483
+ *
484
+ * **Two-phase ordering** required to avoid transient S-9 collisions:
485
+ * the bulk move uses scratch positions that are guaranteed disjoint
486
+ * from (a) every currently-occupied position under `parentId` and (b)
487
+ * the final target positions, then phase 2 places each child at its
488
+ * target. Scratch positions are picked by scanning from `max`
489
+ * downward, skipping any position currently held by a child of
490
+ * `parentId`. This is robust against the pathological case where some
491
+ * children sit close to `POSITION_MAX` — the prior fixed-scratch range
492
+ * `[max - total, max - 1]` could collide with existing children near
493
+ * the top, tripping the S-9 check inside `pe.reparentExpression`. The
494
+ * D0e review (P2 #1) flagged this as the redistribute-fallback's own
495
+ * raison d'être was pathological position-keyspace clustering, so the
496
+ * collision case was structurally reachable.
497
+ *
498
+ * Edge case: total saturation (every position from `min` to `max`
499
+ * occupied) is astronomical — `total > range` (≈ 4.3B) — and is not
500
+ * defended against here. If the scratch scan can't find `total`
501
+ * disjoint positions an Error is thrown with diagnostic context.
502
+ */
503
+ function redistributeChildrenEvenly(pe, parentId, positionConfig) {
504
+ const children = pe.getChildExpressions(parentId);
505
+ const total = children.length;
506
+ if (total === 0)
507
+ return;
508
+ const min = positionConfig.min;
509
+ const max = positionConfig.max;
510
+ const range = max - min;
511
+ // Compute the final target positions for each child.
512
+ const targets = [];
513
+ for (let i = 0; i < total; i++) {
514
+ targets.push(Math.trunc(min + (range / (total + 1)) * (i + 1)));
515
+ }
516
+ // Build the forbidden-position set: union of (a) current
517
+ // positions held by children of parentId and (b) the final
518
+ // target positions. Phase-1 scratches must avoid both:
519
+ //
520
+ // (a) avoids S-9 on a not-yet-moved sibling during phase 1;
521
+ // (b) avoids S-9 in phase 2, where targets land on positions
522
+ // still held by not-yet-moved children carrying scratch
523
+ // positions from phase 1.
524
+ //
525
+ // The pre-D0f hard-coded scratch window `[max - total, max - 1]`
526
+ // happened to cover both concerns *only* when current positions
527
+ // were well below the top of the range — making the bug
528
+ // structurally reachable for clustered-near-max inputs (which is
529
+ // the redistribute-fallback's own raison d'être).
530
+ const forbidden = new Set();
531
+ for (const c of children) {
532
+ forbidden.add(c.position);
533
+ }
534
+ for (const t of targets) {
535
+ forbidden.add(t);
536
+ }
537
+ // D1 — P2 #2: skip children that are already at their target
538
+ // position. The pre-D1 code unconditionally scratched and back-
539
+ // moved every child, emitting 2 reparent change records per
540
+ // already-at-target child. Skipping is correctness-equivalent:
541
+ // - The skipped child's current position equals its target, so it
542
+ // sits inside `forbidden` already (both via the `current` and
543
+ // the `targets` contributions) — phase-1 scratches still avoid
544
+ // it, and other children's phase-2 targets are pairwise
545
+ // distinct from this child's target.
546
+ // Only count + reserve scratches for children that actually move.
547
+ const needsMove = [];
548
+ let movingCount = 0;
549
+ for (let i = 0; i < total; i++) {
550
+ const moves = children[i].position !== targets[i];
551
+ needsMove.push(moves);
552
+ if (moves)
553
+ movingCount++;
554
+ }
555
+ // Phase 1: scan downward from `max` selecting `movingCount`
556
+ // distinct scratch positions not in `forbidden`. The resulting
557
+ // scratches are pairwise distinct (the scan never revisits) and
558
+ // disjoint from all current child positions AND from all target
559
+ // positions, so neither phase-1 nor phase-2 reparents can trip
560
+ // S-9.
561
+ const scratches = [];
562
+ let cursor = max;
563
+ while (scratches.length < movingCount && cursor >= min) {
564
+ if (!forbidden.has(cursor)) {
565
+ scratches.push(cursor);
566
+ }
567
+ cursor--;
568
+ }
569
+ if (scratches.length < movingCount) {
570
+ throw new Error(`AN-4 redistributeChildrenEvenly: cannot find ${movingCount} ` +
571
+ `disjoint scratch positions in [${min}, ${max}] for ` +
572
+ `parent "${parentId}" (current+target occupied: ${forbidden.size}).`);
573
+ }
574
+ let scratchIdx = 0;
575
+ for (let i = 0; i < total; i++) {
576
+ if (!needsMove[i])
577
+ continue;
578
+ pe.reparentExpression(children[i].id, parentId, scratches[scratchIdx++]);
579
+ }
580
+ // Phase 2: move each child to its final target position. Targets
581
+ // are pairwise distinct by `range / (total + 1)` construction and
582
+ // disjoint from scratches by the `forbidden` exclusion above, so
583
+ // no phase-2 reparent collides.
584
+ for (let i = 0; i < total; i++) {
585
+ if (!needsMove[i])
586
+ continue;
587
+ pe.reparentExpression(children[i].id, parentId, targets[i]);
588
+ }
589
+ }
590
+ /**
591
+ * Run AN-1 (insert formula buffer between operators) on every premise
592
+ * of `engine`. Returns `true` iff any mutation occurred.
593
+ *
594
+ * **D0e: native rewrite.** Walks each premise's expression tree
595
+ * looking for non-`not` operators whose parent is also an operator —
596
+ * i.e., the P-1 violation shape `parent-op → child-op (non-not)`. For
597
+ * each match, calls `pe.wrapInFormula(childOpId, formulaId)` which
598
+ * atomically inserts a freshly-minted `formula` between parent and
599
+ * child. The formula takes the child's original slot; the child
600
+ * becomes the formula's sole child at position 0. Per spec §5.1 the
601
+ * result preserves P-1.
602
+ *
603
+ * Why a dedicated `wrapInFormula` primitive rather than composing
604
+ * `addExpression(formula)` + `reparentExpression(child)`:
605
+ *
606
+ * - `addExpression(formula, parent, childPosition)` would throw S-9
607
+ * because the child still occupies that slot.
608
+ * - For unary `not` parents and binary `implies`/`iff` parents,
609
+ * `assertChildLimit` would reject the formula even transiently —
610
+ * even though the *net* child count of the parent is unchanged
611
+ * after the wrap (the formula displaces the child).
612
+ *
613
+ * `pe.wrapInFormula` sidesteps both by performing the insertion +
614
+ * reparent as one bundled-composite mutation per spec §8 (see the PE
615
+ * method's JSDoc for the atomicity contract).
616
+ *
617
+ * The new formula's id is minted via `engine.idGenerator` so id
618
+ * provenance stays at the engine boundary (matches the
619
+ * `populateFromGrounding` factory pattern in `populate-from.ts`).
620
+ *
621
+ * @since 1.0.0
622
+ */
623
+ export function applyAN1(engine) {
624
+ let anyChanged = false;
625
+ const gen = engine.idGenerator;
626
+ for (const pe of engine.listPremises()) {
627
+ let premiseChanged = true;
628
+ while (premiseChanged) {
629
+ premiseChanged = insertOneFormulaBufferInPremise(pe, gen);
630
+ if (premiseChanged)
631
+ anyChanged = true;
632
+ }
633
+ }
634
+ return anyChanged;
635
+ }
636
+ /**
637
+ * Find one P-1 violation in `pe`'s tree (non-`not` operator whose
638
+ * parent is also an operator) and wrap the child in a formula buffer.
639
+ * Returns `true` iff a wrap occurred.
640
+ *
641
+ * Single-fire-per-call shape matches AN-2/AN-3/AN-4 so cascading
642
+ * mutations don't trip mid-iteration tree-walk invariants.
643
+ */
644
+ function insertOneFormulaBufferInPremise(pe, gen) {
645
+ for (const expr of pe.getExpressions()) {
646
+ // P-1 fires on a non-`not` operator that is a direct child of
647
+ // another operator.
648
+ if (expr.type !== "operator")
649
+ continue;
650
+ if (expr.operator === "not")
651
+ continue;
652
+ if (expr.parentId === null)
653
+ continue;
654
+ const parent = pe.getExpression(expr.parentId);
655
+ if (!parent || parent.type !== "operator")
656
+ continue;
657
+ // Wrap the child in a fresh formula. The primitive is atomic:
658
+ // formula takes the child's slot, child becomes formula's sole
659
+ // child at position 0.
660
+ pe.wrapInFormula(expr.id, gen());
661
+ return true;
662
+ }
663
+ return false;
664
+ }
665
+ /**
666
+ * Run AN-1..AN-4 to fixed point on every premise of `engine`.
667
+ *
668
+ * **D0e state: all four rules are native.** The driver issues
669
+ * single-rule passes in order — AN-2, AN-3, AN-4, AN-1 — so buffer
670
+ * insertion sees the post-collapse tree (avoids inserting a buffer
671
+ * that would then need to be collapsed by AN-3).
672
+ *
673
+ * **D0f state: reduce-or accumulator** (replaces the prior `||`
674
+ * short-circuit chain). Every outer iteration fires all four rules
675
+ * and records whether ANY produced a mutation. This reduces outer
676
+ * iterations by ~4x in the worst case versus the short-circuit
677
+ * pattern, and pulls the iteration count back well within
678
+ * MAX_AN_ITERATIONS for previously-borderline inputs.
679
+ *
680
+ * Convergence cap: `MAX_AN_ITERATIONS = 10`. Typical convergence is ≤ 3
681
+ * iterations (spec §5.1); the cap protects against pathological inputs
682
+ * (e.g. malformed Structural state that would otherwise oscillate).
683
+ *
684
+ * @since 1.0.0
685
+ */
686
+ export function applyANToFixedPoint(engine) {
687
+ // D2: the PERMISSIVE swap that disarmed the legacy inline P-1
688
+ // enforcement throws is gone — the 11 P-1 throw sites + the
689
+ // surrounding `grammarConfig.enforceFormulaBetweenOperators`
690
+ // machinery were deleted in D2. AN-2 and AN-3 can now run
691
+ // unconditionally without tripping a P-1 throw mid-pass.
692
+ //
693
+ // D2b: re-entrance guard. AN's own mutations
694
+ // (`pe.removeExpression` / `pe.reparentExpression` /
695
+ // `pe.wrapInFormula`) re-fire `setOnMutate` on the engine, which
696
+ // calls `runAssistiveNormalization(this)` → `applyANToFixedPoint`
697
+ // again. The guard short-circuits nested entries so the outer
698
+ // sweep runs uninterrupted. `beginApplyAN` returns `false` when
699
+ // AN is already in progress; we no-op in that case.
700
+ if (!engine.beginApplyAN())
701
+ return;
702
+ try {
703
+ applyANRulesToConvergence(engine);
704
+ }
705
+ finally {
706
+ engine.endApplyAN();
707
+ }
708
+ }
709
+ function applyANRulesToConvergence(engine) {
710
+ let lastChangedRule = null;
711
+ for (let i = 0; i < MAX_AN_ITERATIONS; i++) {
712
+ // Order: AN-2/3/4 before AN-1 so buffer insertion (AN-1)
713
+ // sees the post-collapse tree (avoids inserting a buffer
714
+ // that AN-3 would then collapse). All four rules are now
715
+ // native single-rule passes (D0e).
716
+ //
717
+ // **D0f: reduce-or accumulator** (was `||` short-circuit
718
+ // pre-D0f). The short-circuit fired at most one rule per
719
+ // outer iteration — if AN-2 fired, AN-3/4/1 were skipped
720
+ // this iteration and the loop went back to the top. The
721
+ // accumulator runs ALL four rules per iteration and records
722
+ // whether ANY made a change. This reduces outer iterations
723
+ // by ~4x in the worst case where multiple rules have
724
+ // independent firing sites in the same premise tree, and
725
+ // pulls the outer loop's iteration count back well within
726
+ // MAX_AN_ITERATIONS for inputs that previously approached
727
+ // the cap. `lastChangedRule` records the most recently
728
+ // changed rule (kept stable across the iteration in
729
+ // priority order AN-2 → AN-3 → AN-4 → AN-1 so the
730
+ // diagnostic still tells the next dev which rule was
731
+ // active last when the cap trips).
732
+ let changed = false;
733
+ if (applyAN2(engine)) {
734
+ lastChangedRule = "AN-2";
735
+ changed = true;
736
+ }
737
+ if (applyAN3(engine)) {
738
+ lastChangedRule = "AN-3";
739
+ changed = true;
740
+ }
741
+ if (applyAN4(engine)) {
742
+ lastChangedRule = "AN-4";
743
+ changed = true;
744
+ }
745
+ if (applyAN1(engine)) {
746
+ lastChangedRule = "AN-1";
747
+ changed = true;
748
+ }
749
+ if (!changed)
750
+ return;
751
+ }
752
+ // Diagnostic context: include the iteration count and the
753
+ // last-changed rule + a representative premise id. Helps the
754
+ // next dev triage where the loop is oscillating when the cap
755
+ // trips. As of D0e the outer loop is the actual convergence
756
+ // driver — all four rules are native single-rule passes — so
757
+ // a cap-trip indicates a real oscillation, not a budgeting
758
+ // artifact of the prior delegation pattern.
759
+ const premises = engine.listPremises();
760
+ const representativePremiseId = premises.length > 0 ? premises[0].getId() : "<no premises>";
761
+ throw new Error(`AN convergence cap reached (${MAX_AN_ITERATIONS} iterations; ` +
762
+ `last-changed rule: ${lastChangedRule ?? "<none>"}; ` +
763
+ `representative premise: ${representativePremiseId}). ` +
764
+ `Argument may be in a malformed Structural state — ` +
765
+ `investigate before re-running.`);
766
+ }
767
+ // `runLegacyNormalizeAndReportChange` lived here during D0a-D0d as a
768
+ // shared delegation helper for applyAN1 / applyAN4. With AN-1 and AN-4
769
+ // natively implemented in D0e, no caller remains and the helper was
770
+ // removed in D0e. D0f converted the `||` short-circuit chain in
771
+ // `applyANToFixedPoint` to a reduce-or accumulator (synthesis P2 #2)
772
+ // so all four rules fire per outer iteration, and moved the PERMISSIVE
773
+ // config swap inside `applyANToFixedPoint`. D2 then deleted the
774
+ // PERMISSIVE swap entirely along with the legacy per-flag
775
+ // `grammarConfig` machinery + the 11 P-1 throw sites the swap worked
776
+ // around — `applyANToFixedPoint` is now a thin try-free wrapper around
777
+ // the convergence loop.
778
+ //# sourceMappingURL=an-rules.js.map