@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.
- package/README.md +104 -93
- package/dist/cli/commands/premises.d.ts.map +1 -1
- package/dist/cli/commands/premises.js +28 -24
- package/dist/cli/commands/premises.js.map +1 -1
- package/dist/cli/commands/repair.d.ts.map +1 -1
- package/dist/cli/commands/repair.js +4 -2
- package/dist/cli/commands/repair.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +7 -1
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/engine.d.ts.map +1 -1
- package/dist/cli/engine.js +7 -25
- package/dist/cli/engine.js.map +1 -1
- package/dist/cli/import.d.ts.map +1 -1
- package/dist/cli/import.js +66 -28
- package/dist/cli/import.js.map +1 -1
- package/dist/cli/schemata.d.ts.map +1 -1
- package/dist/cli/schemata.js +4 -3
- package/dist/cli/schemata.js.map +1 -1
- package/dist/extensions/basics/schemata.d.ts +56 -2
- package/dist/extensions/basics/schemata.d.ts.map +1 -1
- package/dist/extensions/basics/schemata.js +54 -23
- package/dist/extensions/basics/schemata.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +275 -10
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +442 -82
- package/dist/lib/core/argument-engine.js.map +1 -1
- package/dist/lib/core/argument-library.d.ts +5 -1
- package/dist/lib/core/argument-library.d.ts.map +1 -1
- package/dist/lib/core/argument-library.js +7 -3
- package/dist/lib/core/argument-library.js.map +1 -1
- package/dist/lib/core/claim-axiom-library.d.ts +3 -3
- package/dist/lib/core/claim-axiom-library.d.ts.map +1 -1
- package/dist/lib/core/claim-axiom-library.js +2 -2
- package/dist/lib/core/claim-axiom-library.js.map +1 -1
- package/dist/lib/core/claim-citation-library.d.ts +3 -3
- package/dist/lib/core/claim-citation-library.d.ts.map +1 -1
- package/dist/lib/core/claim-citation-library.js +2 -2
- package/dist/lib/core/claim-citation-library.js.map +1 -1
- package/dist/lib/core/expression-manager.d.ts +68 -73
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +242 -762
- package/dist/lib/core/expression-manager.js.map +1 -1
- package/dist/lib/core/fork.d.ts +5 -1
- package/dist/lib/core/fork.d.ts.map +1 -1
- package/dist/lib/core/fork.js +16 -6
- package/dist/lib/core/fork.js.map +1 -1
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +68 -7
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/library.interfaces.d.ts +9 -6
- package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +50 -47
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.d.ts +80 -11
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +232 -80
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/core/proposit-core.d.ts +5 -5
- package/dist/lib/core/proposit-core.d.ts.map +1 -1
- package/dist/lib/core/proposit-core.js +13 -3
- package/dist/lib/core/proposit-core.js.map +1 -1
- package/dist/lib/grammar/an-rules.d.ts +158 -0
- package/dist/lib/grammar/an-rules.d.ts.map +1 -0
- package/dist/lib/grammar/an-rules.js +778 -0
- package/dist/lib/grammar/an-rules.js.map +1 -0
- package/dist/lib/grammar/auto-normalize.d.ts +14 -0
- package/dist/lib/grammar/auto-normalize.d.ts.map +1 -0
- package/dist/lib/grammar/auto-normalize.js +35 -0
- package/dist/lib/grammar/auto-normalize.js.map +1 -0
- package/dist/lib/grammar/bounded-subtree.d.ts +30 -0
- package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -0
- package/dist/lib/grammar/bounded-subtree.js +74 -0
- package/dist/lib/grammar/bounded-subtree.js.map +1 -0
- package/dist/lib/grammar/naked-q.d.ts +20 -0
- package/dist/lib/grammar/naked-q.d.ts.map +1 -0
- package/dist/lib/grammar/naked-q.js +58 -0
- package/dist/lib/grammar/naked-q.js.map +1 -0
- package/dist/lib/grammar/normalize.d.ts +12 -0
- package/dist/lib/grammar/normalize.d.ts.map +1 -0
- package/dist/lib/grammar/normalize.js +45 -0
- package/dist/lib/grammar/normalize.js.map +1 -0
- package/dist/lib/grammar/populate-from.d.ts +25 -0
- package/dist/lib/grammar/populate-from.d.ts.map +1 -0
- package/dist/lib/grammar/populate-from.js +252 -0
- package/dist/lib/grammar/populate-from.js.map +1 -0
- package/dist/lib/grammar/repair.d.ts +65 -0
- package/dist/lib/grammar/repair.d.ts.map +1 -0
- package/dist/lib/grammar/repair.js +251 -0
- package/dist/lib/grammar/repair.js.map +1 -0
- package/dist/lib/grammar/types.d.ts +17 -0
- package/dist/lib/grammar/types.d.ts.map +1 -0
- package/dist/lib/grammar/types.js +82 -0
- package/dist/lib/grammar/types.js.map +1 -0
- package/dist/lib/grammar/validate.d.ts +4 -0
- package/dist/lib/grammar/validate.d.ts.map +1 -0
- package/dist/lib/grammar/validate.js +28 -0
- package/dist/lib/grammar/validate.js.map +1 -0
- package/dist/lib/grammar/validators/context.d.ts +11 -0
- package/dist/lib/grammar/validators/context.d.ts.map +1 -0
- package/dist/lib/grammar/validators/context.js +5 -0
- package/dist/lib/grammar/validators/context.js.map +1 -0
- package/dist/lib/grammar/validators/derivable.d.ts +63 -0
- package/dist/lib/grammar/validators/derivable.d.ts.map +1 -0
- package/dist/lib/grammar/validators/derivable.js +502 -0
- package/dist/lib/grammar/validators/derivable.js.map +1 -0
- package/dist/lib/grammar/validators/evaluable.d.ts +48 -0
- package/dist/lib/grammar/validators/evaluable.d.ts.map +1 -0
- package/dist/lib/grammar/validators/evaluable.js +226 -0
- package/dist/lib/grammar/validators/evaluable.js.map +1 -0
- package/dist/lib/grammar/validators/presentable.d.ts +45 -0
- package/dist/lib/grammar/validators/presentable.d.ts.map +1 -0
- package/dist/lib/grammar/validators/presentable.js +231 -0
- package/dist/lib/grammar/validators/presentable.js.map +1 -0
- package/dist/lib/grammar/validators/structural.d.ts +103 -0
- package/dist/lib/grammar/validators/structural.d.ts.map +1 -0
- package/dist/lib/grammar/validators/structural.js +602 -0
- package/dist/lib/grammar/validators/structural.js.map +1 -0
- package/dist/lib/index.d.ts +4 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/parsing/argument-parser.d.ts +8 -5
- package/dist/lib/parsing/argument-parser.d.ts.map +1 -1
- package/dist/lib/parsing/argument-parser.js +204 -42
- package/dist/lib/parsing/argument-parser.js.map +1 -1
- package/dist/lib/parsing/clamp-max-lengths.d.ts.map +1 -1
- package/dist/lib/parsing/clamp-max-lengths.js +10 -4
- package/dist/lib/parsing/clamp-max-lengths.js.map +1 -1
- package/dist/lib/parsing/prompt-builder.d.ts.map +1 -1
- package/dist/lib/parsing/prompt-builder.js +13 -15
- package/dist/lib/parsing/prompt-builder.js.map +1 -1
- package/dist/lib/parsing/schemata.d.ts +0 -3
- package/dist/lib/parsing/schemata.d.ts.map +1 -1
- package/dist/lib/parsing/schemata.js +25 -13
- package/dist/lib/parsing/schemata.js.map +1 -1
- package/dist/lib/parsing/types.d.ts +1 -1
- package/dist/lib/parsing/types.d.ts.map +1 -1
- package/dist/lib/schemata/claim.d.ts +8 -0
- package/dist/lib/schemata/claim.d.ts.map +1 -1
- package/dist/lib/schemata/claim.js +17 -7
- package/dist/lib/schemata/claim.js.map +1 -1
- package/dist/lib/schemata/import.d.ts.map +1 -1
- package/dist/lib/schemata/import.js +2 -5
- package/dist/lib/schemata/import.js.map +1 -1
- package/dist/lib/schemata/index.d.ts +0 -2
- package/dist/lib/schemata/index.d.ts.map +1 -1
- package/dist/lib/schemata/index.js +0 -2
- package/dist/lib/schemata/index.js.map +1 -1
- package/dist/lib/types/evaluation.d.ts +1 -1
- package/dist/lib/types/evaluation.d.ts.map +1 -1
- package/dist/lib/types/fork.d.ts +12 -3
- package/dist/lib/types/fork.d.ts.map +1 -1
- package/dist/lib/types/validation.d.ts +0 -6
- package/dist/lib/types/validation.d.ts.map +1 -1
- package/dist/lib/types/validation.js +23 -6
- package/dist/lib/types/validation.js.map +1 -1
- package/dist/lib/utils/lookup.d.ts +2 -2
- package/dist/lib/utils/lookup.js +2 -2
- package/package.json +1 -1
- package/dist/lib/core/managed-derivation-premise-engine.d.ts +0 -172
- package/dist/lib/core/managed-derivation-premise-engine.d.ts.map +0 -1
- package/dist/lib/core/managed-derivation-premise-engine.js +0 -550
- package/dist/lib/core/managed-derivation-premise-engine.js.map +0 -1
- package/dist/lib/schemata/claim-axiom.d.ts +0 -11
- package/dist/lib/schemata/claim-axiom.d.ts.map +0 -1
- package/dist/lib/schemata/claim-axiom.js +0 -9
- package/dist/lib/schemata/claim-axiom.js.map +0 -1
- package/dist/lib/schemata/claim-citation.d.ts +0 -11
- package/dist/lib/schemata/claim-citation.d.ts.map +0 -1
- package/dist/lib/schemata/claim-citation.js +0 -9
- package/dist/lib/schemata/claim-citation.js.map +0 -1
- package/dist/lib/types/grammar.d.ts +0 -83
- package/dist/lib/types/grammar.d.ts.map +0 -1
- package/dist/lib/types/grammar.js +0 -24
- 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
|