@proposit/proposit-core 0.12.3 → 1.0.1
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/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/expression-manager.d.ts +68 -73
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +272 -771
- 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 +8 -3
- 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 +245 -83
- package/dist/lib/core/premise-engine.js.map +1 -1
- 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.map +1 -1
- package/dist/lib/parsing/argument-parser.js +52 -10
- package/dist/lib/parsing/argument-parser.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/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/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
|
@@ -4,9 +4,8 @@ import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
|
|
|
4
4
|
import { defaultGenerateId } from "./argument-engine.js";
|
|
5
5
|
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
6
6
|
import { entityChecksum, computeHash, canonicalSerialize } from "./checksum.js";
|
|
7
|
-
import { DEFAULT_GRAMMAR_CONFIG, resolveAutoNormalize, } from "../types/grammar.js";
|
|
8
7
|
import { Value } from "typebox/value";
|
|
9
|
-
import { EXPR_SCHEMA_INVALID, EXPR_DUPLICATE_ID, EXPR_SELF_REFERENTIAL_PARENT, EXPR_PARENT_NOT_FOUND, EXPR_PARENT_NOT_CONTAINER, EXPR_ROOT_ONLY_VIOLATED,
|
|
8
|
+
import { EXPR_SCHEMA_INVALID, EXPR_DUPLICATE_ID, EXPR_SELF_REFERENTIAL_PARENT, EXPR_PARENT_NOT_FOUND, EXPR_PARENT_NOT_CONTAINER, EXPR_ROOT_ONLY_VIOLATED, EXPR_CHILD_LIMIT_EXCEEDED, EXPR_POSITION_DUPLICATE, EXPR_CHECKSUM_MISMATCH, } from "../types/validation.js";
|
|
10
9
|
const PERMITTED_OPERATOR_SWAPS = {
|
|
11
10
|
and: "or",
|
|
12
11
|
or: "and",
|
|
@@ -36,15 +35,6 @@ export class ExpressionManager {
|
|
|
36
35
|
setCollector(collector) {
|
|
37
36
|
this.collector = collector;
|
|
38
37
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Overrides the grammar config used for validation and mutation-time
|
|
41
|
-
* checks. Called by restoration paths (e.g. `fromSnapshot`) when the
|
|
42
|
-
* caller supplies a grammar config that should override whatever was
|
|
43
|
-
* stored in the snapshot.
|
|
44
|
-
*/
|
|
45
|
-
setGrammarConfig(grammarConfig) {
|
|
46
|
-
this.config = { ...this.config, grammarConfig };
|
|
47
|
-
}
|
|
48
38
|
constructor(config) {
|
|
49
39
|
this.expressions = new Map();
|
|
50
40
|
this.childExpressionIdsByParentId = new Map();
|
|
@@ -53,8 +43,17 @@ export class ExpressionManager {
|
|
|
53
43
|
this.config = config;
|
|
54
44
|
this.generateId = config?.generateId ?? defaultGenerateId;
|
|
55
45
|
}
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Returns the position config in effect for this expression manager.
|
|
48
|
+
* Used by in-package helpers (e.g. native AN-4 in
|
|
49
|
+
* `src/lib/grammar/an-rules.ts`) that need the position range
|
|
50
|
+
* boundaries (`min`/`max`) for spacing-algorithm fallbacks when a
|
|
51
|
+
* formula sits at the leftmost or rightmost slot under its parent.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
getPositionConfig() {
|
|
56
|
+
return this.positionConfig;
|
|
58
57
|
}
|
|
59
58
|
attachChecksum(expr) {
|
|
60
59
|
const fields = this.config?.checksumConfig?.expressionFields ??
|
|
@@ -88,9 +87,13 @@ export class ExpressionManager {
|
|
|
88
87
|
* maps (`expressions`, `childExpressionIdsByParentId`,
|
|
89
88
|
* `childPositionsByParentId`) and notifies the change collector.
|
|
90
89
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* As of v1.0 the legacy per-mutation P-1 buffer-insertion branches
|
|
91
|
+
* (`addExpression`/`insertExpression`/`wrapExpression`) are gone —
|
|
92
|
+
* AN-1 (post-mutation hook in assistive mode, see
|
|
93
|
+
* `src/lib/grammar/an-rules.ts`) is the sole formula-buffer
|
|
94
|
+
* insertion path. This helper is invoked from `wrapInFormula` (the
|
|
95
|
+
* public AN-1 primitive on `PremiseEngine`) which calls it once per
|
|
96
|
+
* buffer insertion to materialize the formula node.
|
|
94
97
|
*
|
|
95
98
|
* @returns The generated formula expression ID.
|
|
96
99
|
*/
|
|
@@ -239,7 +242,7 @@ export class ExpressionManager {
|
|
|
239
242
|
* @throws If the position is already occupied under the parent.
|
|
240
243
|
*/
|
|
241
244
|
addExpression(input) {
|
|
242
|
-
|
|
245
|
+
const expression = input;
|
|
243
246
|
if (this.expressions.has(expression.id)) {
|
|
244
247
|
throw new Error(`Expression with ID "${expression.id}" already exists.`);
|
|
245
248
|
}
|
|
@@ -253,36 +256,20 @@ export class ExpressionManager {
|
|
|
253
256
|
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
254
257
|
}
|
|
255
258
|
if (expression.parentId !== null) {
|
|
256
|
-
|
|
259
|
+
const parent = this.expressions.get(expression.parentId);
|
|
257
260
|
if (!parent) {
|
|
258
261
|
throw new Error(`Parent expression "${expression.parentId}" does not exist.`);
|
|
259
262
|
}
|
|
260
263
|
if (parent.type !== "operator" && parent.type !== "formula") {
|
|
261
264
|
throw new Error(`Parent expression "${expression.parentId}" is not an operator expression.`);
|
|
262
265
|
}
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.assertChildLimit(parent.operator, expression.parentId);
|
|
271
|
-
// Auto-insert a formula buffer between parent and expression.
|
|
272
|
-
const formulaId = this.registerFormulaBuffer(expression, expression.parentId, expression.position);
|
|
273
|
-
// Rewrite expression to be child of formula.
|
|
274
|
-
expression = {
|
|
275
|
-
...expression,
|
|
276
|
-
parentId: formulaId,
|
|
277
|
-
position: 0,
|
|
278
|
-
};
|
|
279
|
-
// Update parent reference for subsequent checks.
|
|
280
|
-
parent = this.expressions.get(formulaId);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
266
|
+
// D2: P-1 (non-not operator under operator) is no longer
|
|
267
|
+
// enforced at mutation time. Assistive mode inserts the
|
|
268
|
+
// formula buffer via AN-1 post-hook; permissive mode
|
|
269
|
+
// leaves the un-buffered state and `validate('presentable')`
|
|
270
|
+
// flags it. The pre-v1.0 inline buffer-insertion fallback +
|
|
271
|
+
// throw both lived here under `grammarConfig.enforceFormula
|
|
272
|
+
// BetweenOperators` — deleted in D2.
|
|
286
273
|
if (parent.type === "operator") {
|
|
287
274
|
this.assertChildLimit(parent.operator, expression.parentId);
|
|
288
275
|
}
|
|
@@ -320,12 +307,14 @@ export class ExpressionManager {
|
|
|
320
307
|
const lastChild = children[children.length - 1];
|
|
321
308
|
let position = midpoint(lastChild.position, this.positionConfig.max);
|
|
322
309
|
if (position === lastChild.position) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
310
|
+
// Composite-mutation behavior (spec §8 / S-9): always shift
|
|
311
|
+
// colliding siblings as part of the bundled op. The pre-v1.0
|
|
312
|
+
// `repositionOnCollision` flag gating is gone in D2 — composites
|
|
313
|
+
// never leave a Structural violation by design.
|
|
314
|
+
this.repositionSiblings(parentId, lastChild.position, this.positionConfig.max);
|
|
315
|
+
const updated = this.getChildExpressions(parentId);
|
|
316
|
+
const newLast = updated[updated.length - 1];
|
|
317
|
+
position = midpoint(newLast.position, this.positionConfig.max);
|
|
329
318
|
}
|
|
330
319
|
this.addExpression({
|
|
331
320
|
...expression,
|
|
@@ -352,17 +341,19 @@ export class ExpressionManager {
|
|
|
352
341
|
: this.positionConfig.min;
|
|
353
342
|
position = midpoint(prevPosition, sibling.position);
|
|
354
343
|
if (position === prevPosition || position === sibling.position) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
// Composite-mutation behavior (spec §8 / S-9): always shift
|
|
345
|
+
// colliding siblings as part of the bundled op. The
|
|
346
|
+
// pre-v1.0 `repositionOnCollision` flag gating is gone in
|
|
347
|
+
// D2.
|
|
348
|
+
this.repositionSiblings(sibling.parentId, siblingIndex > 0
|
|
349
|
+
? children[siblingIndex - 1].position
|
|
350
|
+
: this.positionConfig.min, sibling.position);
|
|
351
|
+
const updated = this.getChildExpressions(sibling.parentId);
|
|
352
|
+
const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
|
|
353
|
+
const newPrevPos = newSiblingIdx > 0
|
|
354
|
+
? updated[newSiblingIdx - 1].position
|
|
355
|
+
: this.positionConfig.min;
|
|
356
|
+
position = midpoint(newPrevPos, updated[newSiblingIdx].position);
|
|
366
357
|
}
|
|
367
358
|
}
|
|
368
359
|
else {
|
|
@@ -371,17 +362,17 @@ export class ExpressionManager {
|
|
|
371
362
|
: this.positionConfig.max;
|
|
372
363
|
position = midpoint(sibling.position, nextPosition);
|
|
373
364
|
if (position === sibling.position || position === nextPosition) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
365
|
+
// Composite-mutation behavior (spec §8 / S-9): always
|
|
366
|
+
// shift colliding siblings as part of the bundled op.
|
|
367
|
+
this.repositionSiblings(sibling.parentId, sibling.position, siblingIndex < children.length - 1
|
|
368
|
+
? children[siblingIndex + 1].position
|
|
369
|
+
: this.positionConfig.max);
|
|
370
|
+
const updated = this.getChildExpressions(sibling.parentId);
|
|
371
|
+
const newSiblingIdx = updated.findIndex((c) => c.id === siblingId);
|
|
372
|
+
const newNextPos = newSiblingIdx < updated.length - 1
|
|
373
|
+
? updated[newSiblingIdx + 1].position
|
|
374
|
+
: this.positionConfig.max;
|
|
375
|
+
position = midpoint(updated[newSiblingIdx].position, newNextPos);
|
|
385
376
|
}
|
|
386
377
|
}
|
|
387
378
|
this.addExpression({
|
|
@@ -481,23 +472,26 @@ export class ExpressionManager {
|
|
|
481
472
|
});
|
|
482
473
|
// Mark the updated expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
483
474
|
this.markExpressionDirty(expressionId);
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
475
|
+
// D2: the pre-v1.0 same-operator absorption inline cascade
|
|
476
|
+
// (gated on `absorbSameOperator`) is gone. AN-4 (post-mutation
|
|
477
|
+
// hook in assistive mode) handles same-operator absorption
|
|
478
|
+
// through a formula buffer.
|
|
488
479
|
return this.expressions.get(expressionId) ?? updated;
|
|
489
480
|
}
|
|
490
481
|
/**
|
|
491
482
|
* Removes an expression from the tree.
|
|
492
483
|
*
|
|
493
484
|
* When `deleteSubtree` is `true`, the expression and its entire descendant
|
|
494
|
-
* subtree are removed
|
|
485
|
+
* subtree are removed.
|
|
495
486
|
*
|
|
496
487
|
* When `deleteSubtree` is `false`, the expression is removed but its single
|
|
497
|
-
* child (if any) is promoted into the removed expression's slot.
|
|
498
|
-
* expression has more than one child, an error is thrown.
|
|
499
|
-
*
|
|
500
|
-
*
|
|
488
|
+
* child (if any) is promoted into the removed expression's slot. If the
|
|
489
|
+
* expression has more than one child, an error is thrown.
|
|
490
|
+
*
|
|
491
|
+
* As of v1.0 the pre-removal collapse-cascade (the legacy
|
|
492
|
+
* `collapseIfNeeded` / `simulateCollapseChain`) is gone — AN-3
|
|
493
|
+
* (post-mutation hook in assistive mode) handles 0/1-child
|
|
494
|
+
* operator/formula collapse on the surviving parent.
|
|
501
495
|
*
|
|
502
496
|
* @throws If `deleteSubtree` is `false` and the expression has multiple children.
|
|
503
497
|
* @throws If `deleteSubtree` is `false` and the single child is a root-only
|
|
@@ -551,7 +545,10 @@ export class ExpressionManager {
|
|
|
551
545
|
if (parentId !== null) {
|
|
552
546
|
this.markExpressionDirty(parentId);
|
|
553
547
|
}
|
|
554
|
-
|
|
548
|
+
// D2: the pre-v1.0 `collapseIfNeeded(parentId)` inline cascade
|
|
549
|
+
// (gated on `collapseEmptyFormula`) is gone. AN-3 (post-mutation
|
|
550
|
+
// hook in assistive mode) handles 0/1-child operator/formula
|
|
551
|
+
// collapse.
|
|
555
552
|
return target;
|
|
556
553
|
}
|
|
557
554
|
removeAndPromote(expressionId, target) {
|
|
@@ -560,7 +557,7 @@ export class ExpressionManager {
|
|
|
560
557
|
throw new Error(`Cannot promote: expression "${expressionId}" has multiple children (${children.length}). Use deleteSubtree: true or remove children first.`);
|
|
561
558
|
}
|
|
562
559
|
if (children.length === 0) {
|
|
563
|
-
// Leaf removal
|
|
560
|
+
// Leaf removal.
|
|
564
561
|
const parentId = target.parentId;
|
|
565
562
|
this.collector?.removedExpression({
|
|
566
563
|
...target,
|
|
@@ -571,22 +568,17 @@ export class ExpressionManager {
|
|
|
571
568
|
if (parentId !== null) {
|
|
572
569
|
this.markExpressionDirty(parentId);
|
|
573
570
|
}
|
|
574
|
-
|
|
571
|
+
// D2: AN-3 (post-mutation hook in assistive mode) handles
|
|
572
|
+
// 0/1-child operator/formula collapse on the parent.
|
|
575
573
|
return target;
|
|
576
574
|
}
|
|
577
575
|
// Exactly 1 child — promote it into the target's slot.
|
|
578
576
|
const child = children[0];
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const grandparent = this.expressions.get(target.parentId);
|
|
585
|
-
if (grandparent && grandparent.type === "operator") {
|
|
586
|
-
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
577
|
+
// D2: the P-1 promote-on-remove enforcement throw lived here under
|
|
578
|
+
// `grammarConfig.enforceFormulaBetweenOperators`. AN-1 (post-mutation
|
|
579
|
+
// hook in assistive mode) now inserts the buffer if the promotion
|
|
580
|
+
// produced a non-not operator under operator; permissive mode leaves
|
|
581
|
+
// the un-buffered state and `validate('presentable')` flags it.
|
|
590
582
|
// Validate: root-only operators cannot be promoted into a non-root position.
|
|
591
583
|
if (child.type === "operator" &&
|
|
592
584
|
(child.operator === "implies" || child.operator === "iff") &&
|
|
@@ -623,60 +615,18 @@ export class ExpressionManager {
|
|
|
623
615
|
// (its parentId changed) which also propagates to ancestors.
|
|
624
616
|
this.dirtyExpressionIds.delete(expressionId);
|
|
625
617
|
this.markExpressionDirty(child.id);
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
|
|
618
|
+
// D2: AN-3 (post-mutation hook in assistive mode) handles
|
|
619
|
+
// formula collapse on the target's parent if the promoted child
|
|
620
|
+
// makes the formula unjustified.
|
|
629
621
|
return target;
|
|
630
622
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
// When repositionOnCollision is enabled and the grandparent has multiple
|
|
639
|
-
// children, compute the midpoint between the parent's left and right
|
|
640
|
-
// neighbors for better spacing.
|
|
641
|
-
if (grandparentId !== null &&
|
|
642
|
-
resolveAutoNormalize(this.grammarConfig, "repositionOnCollision")) {
|
|
643
|
-
const siblings = this.getChildExpressions(grandparentId);
|
|
644
|
-
if (siblings.length > 1) {
|
|
645
|
-
const parentIdx = siblings.findIndex((s) => s.id === parentId);
|
|
646
|
-
const leftPos = parentIdx > 0
|
|
647
|
-
? siblings[parentIdx - 1].position
|
|
648
|
-
: this.positionConfig.min;
|
|
649
|
-
const rightPos = parentIdx < siblings.length - 1
|
|
650
|
-
? siblings[parentIdx + 1].position
|
|
651
|
-
: this.positionConfig.max;
|
|
652
|
-
promotedPosition = midpoint(leftPos, rightPos);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
const promoted = this.attachChecksum({
|
|
656
|
-
...child,
|
|
657
|
-
parentId: grandparentId,
|
|
658
|
-
position: promotedPosition,
|
|
659
|
-
});
|
|
660
|
-
this.expressions.set(child.id, promoted);
|
|
661
|
-
this.collector?.modifiedExpression({
|
|
662
|
-
...promoted,
|
|
663
|
-
});
|
|
664
|
-
this.childExpressionIdsByParentId.get(grandparentId)?.delete(parentId);
|
|
665
|
-
getOrCreate(this.childExpressionIdsByParentId, grandparentId, () => new Set()).add(child.id);
|
|
666
|
-
// Update position tracking: remove parent's old position, add promoted position.
|
|
667
|
-
this.childPositionsByParentId
|
|
668
|
-
.get(grandparentId)
|
|
669
|
-
?.delete(parent.position);
|
|
670
|
-
getOrCreate(this.childPositionsByParentId, grandparentId, () => new Set()).add(promotedPosition);
|
|
671
|
-
this.childExpressionIdsByParentId.delete(parentId);
|
|
672
|
-
this.childPositionsByParentId.delete(parentId);
|
|
673
|
-
this.collector?.removedExpression({
|
|
674
|
-
...parent,
|
|
675
|
-
});
|
|
676
|
-
this.expressions.delete(parentId);
|
|
677
|
-
this.dirtyExpressionIds.delete(parentId);
|
|
678
|
-
this.markExpressionDirty(child.id);
|
|
679
|
-
}
|
|
623
|
+
// D2 — `promoteChild` was the helper for the pre-v1.0 inline
|
|
624
|
+
// collapse cascade (`collapseIfNeeded`) and the legacy
|
|
625
|
+
// `ExpressionManager.normalize()` sweep, both of which are gone in
|
|
626
|
+
// D2 (replaced by AN-3 / AN-4 / AN-2 / AN-1 post-mutation hooks in
|
|
627
|
+
// `src/lib/grammar/an-rules.ts`). The remaining
|
|
628
|
+
// `removeAndPromote` 1-child branch in `removeExpression` writes
|
|
629
|
+
// the promoted child directly (no shared helper is needed).
|
|
680
630
|
/**
|
|
681
631
|
* Redistributes the minimal set of sibling positions to create room at
|
|
682
632
|
* an insertion point between `leftPos` and `rightPos` under `parentId`.
|
|
@@ -786,374 +736,21 @@ export class ExpressionManager {
|
|
|
786
736
|
}
|
|
787
737
|
return modified;
|
|
788
738
|
}
|
|
789
|
-
collapseIfNeeded
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
this.detachExpression(operatorId, operator);
|
|
805
|
-
this.dirtyExpressionIds.delete(operatorId);
|
|
806
|
-
if (grandparentId !== null) {
|
|
807
|
-
this.markExpressionDirty(grandparentId);
|
|
808
|
-
}
|
|
809
|
-
this.collapseIfNeeded(grandparentId);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
// 1-child formula: collapse if no binary operator in bounded subtree.
|
|
813
|
-
if (children.length === 1 &&
|
|
814
|
-
!this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
|
|
815
|
-
const grandparentId = operator.parentId;
|
|
816
|
-
this.promoteChild(operatorId, operator, children[0]);
|
|
817
|
-
// Grandparent may also be a formula that now needs collapsing.
|
|
818
|
-
this.collapseIfNeeded(grandparentId);
|
|
819
|
-
}
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
if (operator.type !== "operator")
|
|
823
|
-
return;
|
|
824
|
-
const children = this.getChildExpressions(operatorId);
|
|
825
|
-
if (children.length === 0) {
|
|
826
|
-
const grandparentId = operator.parentId;
|
|
827
|
-
this.collector?.removedExpression({
|
|
828
|
-
...operator,
|
|
829
|
-
});
|
|
830
|
-
this.detachExpression(operatorId, operator);
|
|
831
|
-
// Prune collapsed operator from dirty set and propagate to grandparent.
|
|
832
|
-
this.dirtyExpressionIds.delete(operatorId);
|
|
833
|
-
if (grandparentId !== null) {
|
|
834
|
-
this.markExpressionDirty(grandparentId);
|
|
835
|
-
}
|
|
836
|
-
this.collapseIfNeeded(grandparentId);
|
|
837
|
-
}
|
|
838
|
-
else if (children.length === 1 && operator.operator === "not") {
|
|
839
|
-
// `not` is unary — 1 child is its valid state; skip collapse.
|
|
840
|
-
// Still recurse to grandparent: a formula wrapping this `not` may
|
|
841
|
-
// now qualify for collapse after a descendant change.
|
|
842
|
-
this.collapseIfNeeded(operator.parentId);
|
|
843
|
-
}
|
|
844
|
-
else if (children.length === 1) {
|
|
845
|
-
const child = children[0];
|
|
846
|
-
const grandparentId = operator.parentId;
|
|
847
|
-
// Defense-in-depth: validate promotion doesn't violate nesting or root-only rules.
|
|
848
|
-
if (child.type === "operator") {
|
|
849
|
-
// Root-only — always enforced
|
|
850
|
-
if ((child.operator === "implies" ||
|
|
851
|
-
child.operator === "iff") &&
|
|
852
|
-
grandparentId !== null) {
|
|
853
|
-
throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
|
|
854
|
-
}
|
|
855
|
-
// Nesting — grammar-configurable
|
|
856
|
-
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
857
|
-
if (child.operator !== "not" && grandparentId !== null) {
|
|
858
|
-
const grandparent = this.expressions.get(grandparentId);
|
|
859
|
-
if (grandparent && grandparent.type === "operator") {
|
|
860
|
-
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
this.promoteChild(operatorId, operator, child);
|
|
866
|
-
// Grandparent may be a formula that now needs collapsing after the
|
|
867
|
-
// promoted child replaced the operator.
|
|
868
|
-
this.collapseIfNeeded(grandparentId);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Flag-gated wrapper: absorbs same-operator children through a formula
|
|
873
|
-
* after an operator swap, if `absorbSameOperator` is enabled.
|
|
874
|
-
*/
|
|
875
|
-
absorbSameOperatorIfNeeded(expressionId) {
|
|
876
|
-
if (!resolveAutoNormalize(this.grammarConfig, "absorbSameOperator"))
|
|
877
|
-
return;
|
|
878
|
-
this.absorbSameOperator(expressionId);
|
|
879
|
-
}
|
|
880
|
-
/**
|
|
881
|
-
* Absorbs the inner operator's children into the outer (grandparent)
|
|
882
|
-
* operator when both are the same type, separated by a formula.
|
|
883
|
-
*
|
|
884
|
-
* Example: `AND → formula → AND(B, C)` becomes `AND → [B, C]` (children
|
|
885
|
-
* promoted into the outer AND at positions between the formula's neighbors).
|
|
886
|
-
*
|
|
887
|
-
* No-op if the pattern does not match.
|
|
888
|
-
*/
|
|
889
|
-
absorbSameOperator(expressionId) {
|
|
890
|
-
const inner = this.expressions.get(expressionId);
|
|
891
|
-
if (!inner || inner.type !== "operator")
|
|
892
|
-
return false;
|
|
893
|
-
// Only and/or are absorbable (implies/iff are root-only).
|
|
894
|
-
if (inner.operator !== "and" && inner.operator !== "or")
|
|
895
|
-
return false;
|
|
896
|
-
const formulaId = inner.parentId;
|
|
897
|
-
if (formulaId === null)
|
|
898
|
-
return false;
|
|
899
|
-
const formula = this.expressions.get(formulaId);
|
|
900
|
-
if (!formula || formula.type !== "formula")
|
|
901
|
-
return false;
|
|
902
|
-
const outerId = formula.parentId;
|
|
903
|
-
if (outerId === null)
|
|
904
|
-
return false;
|
|
905
|
-
const outer = this.expressions.get(outerId);
|
|
906
|
-
if (!outer || outer.type !== "operator")
|
|
907
|
-
return false;
|
|
908
|
-
if (outer.operator !== inner.operator)
|
|
909
|
-
return false;
|
|
910
|
-
// Same operator on both sides of a formula — absorb.
|
|
911
|
-
const innerChildren = this.getChildExpressions(expressionId);
|
|
912
|
-
// Determine the position gap available for absorbed children.
|
|
913
|
-
const outerChildren = this.getChildExpressions(outerId);
|
|
914
|
-
const formulaIdx = outerChildren.findIndex((c) => c.id === formulaId);
|
|
915
|
-
const leftPos = formulaIdx > 0
|
|
916
|
-
? outerChildren[formulaIdx - 1].position
|
|
917
|
-
: this.positionConfig.min;
|
|
918
|
-
const rightPos = formulaIdx < outerChildren.length - 1
|
|
919
|
-
? outerChildren[formulaIdx + 1].position
|
|
920
|
-
: this.positionConfig.max;
|
|
921
|
-
// Check if there is enough integer space between neighbors for N children.
|
|
922
|
-
const gap = rightPos - leftPos;
|
|
923
|
-
const count = innerChildren.length;
|
|
924
|
-
const needsRedistribution = gap <= count;
|
|
925
|
-
// Reparent each inner child to the outer operator. Use evenly spaced
|
|
926
|
-
// positions within the gap; these may collide if the gap is too tight,
|
|
927
|
-
// which is fixed by a full redistribution below.
|
|
928
|
-
for (let i = 0; i < count; i++) {
|
|
929
|
-
const newPosition = Math.trunc(leftPos + ((rightPos - leftPos) / (count + 1)) * (i + 1));
|
|
930
|
-
this.reparent(innerChildren[i].id, outerId, newPosition);
|
|
931
|
-
}
|
|
932
|
-
// Remove the inner operator and formula (now childless).
|
|
933
|
-
this.childExpressionIdsByParentId.delete(expressionId);
|
|
934
|
-
this.childPositionsByParentId.delete(expressionId);
|
|
935
|
-
this.collector?.removedExpression({
|
|
936
|
-
...inner,
|
|
937
|
-
});
|
|
938
|
-
this.expressions.delete(expressionId);
|
|
939
|
-
this.dirtyExpressionIds.delete(expressionId);
|
|
940
|
-
this.childExpressionIdsByParentId.get(outerId)?.delete(formulaId);
|
|
941
|
-
this.childPositionsByParentId.get(outerId)?.delete(formula.position);
|
|
942
|
-
this.childExpressionIdsByParentId.delete(formulaId);
|
|
943
|
-
this.childPositionsByParentId.delete(formulaId);
|
|
944
|
-
this.collector?.removedExpression({
|
|
945
|
-
...formula,
|
|
946
|
-
});
|
|
947
|
-
this.expressions.delete(formulaId);
|
|
948
|
-
this.dirtyExpressionIds.delete(formulaId);
|
|
949
|
-
// If the gap was too tight, redistribute all children of the outer
|
|
950
|
-
// operator evenly across the full position range.
|
|
951
|
-
if (needsRedistribution) {
|
|
952
|
-
const allChildren = this.getChildExpressions(outerId);
|
|
953
|
-
const total = allChildren.length;
|
|
954
|
-
const positionSet = this.childPositionsByParentId.get(outerId);
|
|
955
|
-
for (const child of allChildren) {
|
|
956
|
-
positionSet?.delete(child.position);
|
|
957
|
-
}
|
|
958
|
-
for (let i = 0; i < total; i++) {
|
|
959
|
-
const newPos = Math.trunc(this.positionConfig.min +
|
|
960
|
-
((this.positionConfig.max - this.positionConfig.min) /
|
|
961
|
-
(total + 1)) *
|
|
962
|
-
(i + 1));
|
|
963
|
-
const updated = this.attachChecksum({
|
|
964
|
-
...allChildren[i],
|
|
965
|
-
position: newPos,
|
|
966
|
-
});
|
|
967
|
-
this.expressions.set(allChildren[i].id, updated);
|
|
968
|
-
this.collector?.modifiedExpression({
|
|
969
|
-
...updated,
|
|
970
|
-
});
|
|
971
|
-
positionSet?.add(newPos);
|
|
972
|
-
this.markExpressionDirty(allChildren[i].id);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
// Mark the outer operator dirty.
|
|
976
|
-
this.markExpressionDirty(outerId);
|
|
977
|
-
return true;
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Checks whether the subtree rooted at `expressionId` contains a binary
|
|
981
|
-
* operator (`and` or `or`). Traversal stops at formula boundaries — a
|
|
982
|
-
* nested formula owns its own subtree and is not inspected.
|
|
983
|
-
*/
|
|
984
|
-
hasBinaryOperatorInBoundedSubtree(expressionId) {
|
|
985
|
-
const expr = this.expressions.get(expressionId);
|
|
986
|
-
if (!expr)
|
|
987
|
-
return false;
|
|
988
|
-
if (expr.type === "formula")
|
|
989
|
-
return false;
|
|
990
|
-
if (expr.type === "variable")
|
|
991
|
-
return false;
|
|
992
|
-
if (expr.type === "operator" &&
|
|
993
|
-
(expr.operator === "and" || expr.operator === "or")) {
|
|
994
|
-
return true;
|
|
995
|
-
}
|
|
996
|
-
const children = this.getChildExpressions(expressionId);
|
|
997
|
-
return children.some((child) => this.hasBinaryOperatorInBoundedSubtree(child.id));
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Performs a full normalization sweep on the expression tree:
|
|
1001
|
-
* 1. Collapses operators with 0 or 1 children.
|
|
1002
|
-
* 2. Collapses formulas whose bounded subtree has no binary operator.
|
|
1003
|
-
* 3. Inserts formula buffers where `enforceFormulaBetweenOperators` requires them.
|
|
1004
|
-
* 4. Collapses double negation.
|
|
1005
|
-
* 5. Absorbs same-operator nesting through formulas.
|
|
1006
|
-
* 6. Repeats until stable.
|
|
1007
|
-
*
|
|
1008
|
-
* Works regardless of the current `autoNormalize` setting — this is an
|
|
1009
|
-
* explicit on-demand normalization.
|
|
1010
|
-
*/
|
|
1011
|
-
normalize() {
|
|
1012
|
-
let changed = true;
|
|
1013
|
-
while (changed) {
|
|
1014
|
-
changed = false;
|
|
1015
|
-
// Pass 1: Collapse operators with 0 or 1 children (bottom-up).
|
|
1016
|
-
for (const expr of this.toArray()) {
|
|
1017
|
-
if (expr.type !== "operator")
|
|
1018
|
-
continue;
|
|
1019
|
-
if (!this.expressions.has(expr.id))
|
|
1020
|
-
continue;
|
|
1021
|
-
const children = this.getChildExpressions(expr.id);
|
|
1022
|
-
if (children.length === 0) {
|
|
1023
|
-
const grandparentId = expr.parentId;
|
|
1024
|
-
this.collector?.removedExpression({
|
|
1025
|
-
...expr,
|
|
1026
|
-
});
|
|
1027
|
-
this.detachExpression(expr.id, expr);
|
|
1028
|
-
this.dirtyExpressionIds.delete(expr.id);
|
|
1029
|
-
if (grandparentId !== null) {
|
|
1030
|
-
this.markExpressionDirty(grandparentId);
|
|
1031
|
-
}
|
|
1032
|
-
changed = true;
|
|
1033
|
-
}
|
|
1034
|
-
else if (children.length === 1 && expr.operator !== "not") {
|
|
1035
|
-
this.promoteChild(expr.id, expr, children[0]);
|
|
1036
|
-
changed = true;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
// Pass 2: Collapse unjustified formulas (bottom-up).
|
|
1040
|
-
for (const expr of this.toArray()) {
|
|
1041
|
-
if (expr.type !== "formula")
|
|
1042
|
-
continue;
|
|
1043
|
-
if (!this.expressions.has(expr.id))
|
|
1044
|
-
continue;
|
|
1045
|
-
const children = this.getChildExpressions(expr.id);
|
|
1046
|
-
if (children.length === 0) {
|
|
1047
|
-
const grandparentId = expr.parentId;
|
|
1048
|
-
this.collector?.removedExpression({
|
|
1049
|
-
...expr,
|
|
1050
|
-
});
|
|
1051
|
-
this.detachExpression(expr.id, expr);
|
|
1052
|
-
this.dirtyExpressionIds.delete(expr.id);
|
|
1053
|
-
if (grandparentId !== null) {
|
|
1054
|
-
this.markExpressionDirty(grandparentId);
|
|
1055
|
-
}
|
|
1056
|
-
changed = true;
|
|
1057
|
-
}
|
|
1058
|
-
else if (children.length === 1 &&
|
|
1059
|
-
!this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
|
|
1060
|
-
this.promoteChild(expr.id, expr, children[0]);
|
|
1061
|
-
changed = true;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
// Pass 3: Insert formula buffers for operator-under-operator violations.
|
|
1065
|
-
for (const expr of this.toArray()) {
|
|
1066
|
-
if (expr.type !== "operator" || expr.operator === "not")
|
|
1067
|
-
continue;
|
|
1068
|
-
if (!this.expressions.has(expr.id))
|
|
1069
|
-
continue;
|
|
1070
|
-
if (expr.parentId === null)
|
|
1071
|
-
continue;
|
|
1072
|
-
const parent = this.expressions.get(expr.parentId);
|
|
1073
|
-
if (!parent || parent.type !== "operator")
|
|
1074
|
-
continue;
|
|
1075
|
-
// Non-not operator is direct child of operator — insert formula buffer.
|
|
1076
|
-
const formulaPosition = expr.position;
|
|
1077
|
-
const formulaParentId = expr.parentId;
|
|
1078
|
-
const formulaId = this.registerFormulaBuffer(expr, formulaParentId, formulaPosition);
|
|
1079
|
-
// Reparent the operator under the formula. This removes the
|
|
1080
|
-
// operator's old position from the parent's position set, but
|
|
1081
|
-
// the formula now occupies that slot, so re-add it. Also mark
|
|
1082
|
-
// the formula dirty since it now has a child and its
|
|
1083
|
-
// descendant/combined checksums need recomputation.
|
|
1084
|
-
this.reparent(expr.id, formulaId, 0);
|
|
1085
|
-
getOrCreate(this.childPositionsByParentId, formulaParentId, () => new Set()).add(formulaPosition);
|
|
1086
|
-
this.markExpressionDirty(formulaId);
|
|
1087
|
-
changed = true;
|
|
1088
|
-
}
|
|
1089
|
-
// Pass 4: Collapse double negation — NOT(NOT(x)) → x.
|
|
1090
|
-
for (const expr of this.toArray()) {
|
|
1091
|
-
if (expr.type !== "operator" || expr.operator !== "not")
|
|
1092
|
-
continue;
|
|
1093
|
-
if (!this.expressions.has(expr.id))
|
|
1094
|
-
continue;
|
|
1095
|
-
const children = this.getChildExpressions(expr.id);
|
|
1096
|
-
if (children.length !== 1)
|
|
1097
|
-
continue;
|
|
1098
|
-
const child = children[0];
|
|
1099
|
-
// Direct: NOT → NOT → x
|
|
1100
|
-
if (child.type === "operator" && child.operator === "not") {
|
|
1101
|
-
const innerChildren = this.getChildExpressions(child.id);
|
|
1102
|
-
if (innerChildren.length === 1) {
|
|
1103
|
-
// Promote inner child into outer NOT's slot, remove both NOTs.
|
|
1104
|
-
this.promoteChild(child.id, child, innerChildren[0]);
|
|
1105
|
-
this.promoteChild(expr.id,
|
|
1106
|
-
// Re-fetch since promoteChild mutated in place.
|
|
1107
|
-
this.expressions.get(expr.id), innerChildren[0]);
|
|
1108
|
-
changed = true;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
// Buffered: NOT → formula → NOT → x
|
|
1112
|
-
if (child.type === "formula" &&
|
|
1113
|
-
this.expressions.has(child.id)) {
|
|
1114
|
-
const formulaChildren = this.getChildExpressions(child.id);
|
|
1115
|
-
if (formulaChildren.length === 1 &&
|
|
1116
|
-
formulaChildren[0].type === "operator" &&
|
|
1117
|
-
formulaChildren[0].operator === "not") {
|
|
1118
|
-
const innerNot = formulaChildren[0];
|
|
1119
|
-
const innerChildren = this.getChildExpressions(innerNot.id);
|
|
1120
|
-
if (innerChildren.length === 1) {
|
|
1121
|
-
// Remove inner NOT, promote its child into formula.
|
|
1122
|
-
this.promoteChild(innerNot.id, innerNot, innerChildren[0]);
|
|
1123
|
-
// Remove outer NOT, promote formula into its slot.
|
|
1124
|
-
this.promoteChild(expr.id, this.expressions.get(expr.id), child);
|
|
1125
|
-
changed = true;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
// Pass 5: Absorb same-operator through formula — OP → formula → OP → [...] becomes OP → [...].
|
|
1131
|
-
for (const expr of this.toArray()) {
|
|
1132
|
-
if (expr.type !== "formula")
|
|
1133
|
-
continue;
|
|
1134
|
-
if (!this.expressions.has(expr.id))
|
|
1135
|
-
continue;
|
|
1136
|
-
if (expr.parentId === null)
|
|
1137
|
-
continue;
|
|
1138
|
-
const parent = this.expressions.get(expr.parentId);
|
|
1139
|
-
if (!parent || parent.type !== "operator")
|
|
1140
|
-
continue;
|
|
1141
|
-
if (parent.operator !== "and" && parent.operator !== "or")
|
|
1142
|
-
continue;
|
|
1143
|
-
const formulaChildren = this.getChildExpressions(expr.id);
|
|
1144
|
-
if (formulaChildren.length !== 1)
|
|
1145
|
-
continue;
|
|
1146
|
-
const inner = formulaChildren[0];
|
|
1147
|
-
if (inner.type !== "operator" ||
|
|
1148
|
-
inner.operator !== parent.operator)
|
|
1149
|
-
continue;
|
|
1150
|
-
// Same operator on both sides — absorb inner's children into parent.
|
|
1151
|
-
if (this.absorbSameOperator(inner.id)) {
|
|
1152
|
-
changed = true;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
739
|
+
// D2 — the inline AN cascades (`collapseIfNeeded` /
|
|
740
|
+
// `absorbSameOperatorIfNeeded` / `absorbSameOperator`), the legacy
|
|
741
|
+
// `ExpressionManager.normalize()` 5-pass sweep, the
|
|
742
|
+
// `promoteChild` helper, and the private
|
|
743
|
+
// `hasBinaryOperatorInBoundedSubtree` were all deleted here. The
|
|
744
|
+
// first three were gated on the legacy `collapseEmptyFormula` /
|
|
745
|
+
// `absorbSameOperator` flags and ran inline from
|
|
746
|
+
// `removeExpression` / `updateExpression`. The `normalize()` sweep
|
|
747
|
+
// was the underlying engine for `pe.normalizeExpressions()` /
|
|
748
|
+
// `engine.normalizeAllExpressions()`. All of these are subsumed by
|
|
749
|
+
// the four native AN passes in `src/lib/grammar/an-rules.ts`
|
|
750
|
+
// (AN-1..AN-4 — assistive-mode post-mutation hook +
|
|
751
|
+
// `engine.normalize(tier?)`'s explicit pass). The AN-3 module
|
|
752
|
+
// uses its own bounded-subtree helper bound to
|
|
753
|
+
// `pe.getChildExpressions` (see `src/lib/grammar/bounded-subtree.ts`).
|
|
1157
754
|
/** Returns `true` if any expression in the tree references the given variable ID. */
|
|
1158
755
|
hasVariableReference(variableId) {
|
|
1159
756
|
for (const expression of this.expressions.values()) {
|
|
@@ -1189,112 +786,41 @@ export class ExpressionManager {
|
|
|
1189
786
|
}
|
|
1190
787
|
}
|
|
1191
788
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
789
|
+
* Pre-flight before {@link removeExpression} mutates state. As of v1.0
|
|
790
|
+
* the only structural failure mode for `removeAndPromote`'s 1-child
|
|
791
|
+
* branch is the root-only-operator promotion rule (S-5): an
|
|
792
|
+
* `implies`/`iff` child cannot be promoted into a non-root slot. The
|
|
793
|
+
* pre-v1.0 P-1 promote-on-remove check was deleted in D2 along with
|
|
794
|
+
* the rest of the `grammarConfig.enforceFormulaBetweenOperators`
|
|
795
|
+
* machinery; the legacy `collapseEmptyFormula` cascade simulation
|
|
796
|
+
* (`simulateCollapseChain` / `simulatePostPromotionCollapse`) was
|
|
797
|
+
* deleted in lockstep — AN-3 (post-mutation hook in assistive mode)
|
|
798
|
+
* handles every collapse case.
|
|
1194
799
|
*/
|
|
1195
800
|
assertRemovalSafe(expressionId, deleteSubtree) {
|
|
801
|
+
if (deleteSubtree)
|
|
802
|
+
return;
|
|
1196
803
|
const target = this.expressions.get(expressionId);
|
|
1197
804
|
if (!target)
|
|
1198
805
|
return;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
if (children.length === 1) {
|
|
1203
|
-
this.assertPromotionSafe(children[0], target.parentId);
|
|
1204
|
-
// Simulate post-promotion cascade (formula collapse after promotion).
|
|
1205
|
-
if (resolveAutoNormalize(this.grammarConfig, "collapseEmptyFormula")) {
|
|
1206
|
-
this.simulatePostPromotionCollapse(target.parentId, children[0]);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
if (children.length === 0) {
|
|
1210
|
-
this.simulateCollapseChain(target.parentId, expressionId);
|
|
1211
|
-
}
|
|
1212
|
-
return;
|
|
806
|
+
const children = this.getChildExpressions(expressionId);
|
|
807
|
+
if (children.length === 1) {
|
|
808
|
+
this.assertPromotionSafe(children[0], target.parentId);
|
|
1213
809
|
}
|
|
1214
|
-
// deleteSubtree: entire subtree removed, then collapse runs on parent.
|
|
1215
|
-
this.simulateCollapseChain(target.parentId, expressionId);
|
|
1216
810
|
}
|
|
1217
811
|
/**
|
|
1218
812
|
* Checks whether promoting `child` into a slot with the given `newParentId`
|
|
1219
|
-
* would violate the
|
|
813
|
+
* would violate the root-only rule (S-5). The pre-v1.0 nesting check
|
|
814
|
+
* (P-1 / `enforceFormulaBetweenOperators`) was deleted in D2.
|
|
1220
815
|
*/
|
|
1221
816
|
assertPromotionSafe(child, newParentId) {
|
|
1222
817
|
if (child.type !== "operator")
|
|
1223
818
|
return;
|
|
1224
|
-
// Root-only check — always enforced
|
|
819
|
+
// Root-only check — always enforced (S-5)
|
|
1225
820
|
if ((child.operator === "implies" || child.operator === "iff") &&
|
|
1226
821
|
newParentId !== null) {
|
|
1227
822
|
throw new Error(`Cannot remove expression — would promote a root-only operator ("${child.operator}") to a non-root position`);
|
|
1228
823
|
}
|
|
1229
|
-
// Nesting check — grammar-configurable
|
|
1230
|
-
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
1231
|
-
if (child.operator !== "not" && newParentId !== null) {
|
|
1232
|
-
const newParent = this.expressions.get(newParentId);
|
|
1233
|
-
if (newParent && newParent.type === "operator") {
|
|
1234
|
-
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Walks the collapse chain starting from `operatorId` after `removedChildId`
|
|
1241
|
-
* is removed. At each level: if 0 remaining children, operator/formula is deleted
|
|
1242
|
-
* and chain continues up. If 1 remaining child, check promotion safety.
|
|
1243
|
-
*/
|
|
1244
|
-
simulateCollapseChain(operatorId, removedChildId) {
|
|
1245
|
-
if (!resolveAutoNormalize(this.grammarConfig, "collapseEmptyFormula"))
|
|
1246
|
-
return;
|
|
1247
|
-
if (operatorId === null)
|
|
1248
|
-
return;
|
|
1249
|
-
const operator = this.expressions.get(operatorId);
|
|
1250
|
-
if (!operator)
|
|
1251
|
-
return;
|
|
1252
|
-
if (operator.type !== "operator" && operator.type !== "formula")
|
|
1253
|
-
return;
|
|
1254
|
-
const children = this.getChildExpressions(operatorId);
|
|
1255
|
-
const remainingChildren = children.filter((c) => c.id !== removedChildId);
|
|
1256
|
-
if (operator.type === "formula") {
|
|
1257
|
-
if (remainingChildren.length === 0) {
|
|
1258
|
-
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
1259
|
-
}
|
|
1260
|
-
else if (remainingChildren.length === 1 &&
|
|
1261
|
-
!this.hasBinaryOperatorInBoundedSubtree(remainingChildren[0].id)) {
|
|
1262
|
-
// Formula would collapse — child promoted.
|
|
1263
|
-
// Formula collapse promotion is always safe (child is variable, not, or formula).
|
|
1264
|
-
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
1265
|
-
}
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
// operator.type === "operator"
|
|
1269
|
-
if (remainingChildren.length === 0) {
|
|
1270
|
-
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
1271
|
-
}
|
|
1272
|
-
else if (remainingChildren.length === 1) {
|
|
1273
|
-
this.assertPromotionSafe(remainingChildren[0], operator.parentId);
|
|
1274
|
-
// After promotion, simulate further collapse on grandparent.
|
|
1275
|
-
this.simulatePostPromotionCollapse(operator.parentId, remainingChildren[0]);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
/**
|
|
1279
|
-
* After an operator promotion places `promotedChild` into `parentId`'s child set,
|
|
1280
|
-
* check whether the parent (if a formula) would itself collapse. Formula collapse
|
|
1281
|
-
* promotion is always safe (the child can't be a binary operator or root-only operator),
|
|
1282
|
-
* but we need to continue the simulation chain.
|
|
1283
|
-
*/
|
|
1284
|
-
simulatePostPromotionCollapse(parentId, promotedChild) {
|
|
1285
|
-
if (parentId === null)
|
|
1286
|
-
return;
|
|
1287
|
-
const parent = this.expressions.get(parentId);
|
|
1288
|
-
if (!parent)
|
|
1289
|
-
return;
|
|
1290
|
-
if (parent.type === "formula") {
|
|
1291
|
-
if (!this.hasBinaryOperatorInBoundedSubtree(promotedChild.id)) {
|
|
1292
|
-
// Formula would collapse. The promotedChild takes formula's slot.
|
|
1293
|
-
// This is always safe. Continue simulation from formula's parent.
|
|
1294
|
-
this.simulatePostPromotionCollapse(parent.parentId, promotedChild);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
// Operator parents: child count unchanged, no further collapse.
|
|
1298
824
|
}
|
|
1299
825
|
assertChildLimit(operator, parentId) {
|
|
1300
826
|
const childCount = this.childExpressionIdsByParentId.get(parentId)?.size ?? 0;
|
|
@@ -1424,56 +950,33 @@ export class ExpressionManager {
|
|
|
1424
950
|
anchor.parentId !== null) {
|
|
1425
951
|
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
1426
952
|
}
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
if (anchorParent && anchorParent.type === "operator") {
|
|
1438
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1439
|
-
needsParentFormulaBuffer = true;
|
|
1440
|
-
}
|
|
1441
|
-
else {
|
|
1442
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
// Check 2 (Site 2): left/right nodes as children of the new expression.
|
|
1447
|
-
if (expression.type === "operator") {
|
|
1448
|
-
if (leftNode?.type === "operator" &&
|
|
1449
|
-
leftNode.operator !== "not") {
|
|
1450
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1451
|
-
childrenNeedingFormulaBuffer.push(leftNodeId);
|
|
1452
|
-
}
|
|
1453
|
-
else {
|
|
1454
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (rightNode?.type === "operator" &&
|
|
1458
|
-
rightNode.operator !== "not") {
|
|
1459
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1460
|
-
childrenNeedingFormulaBuffer.push(rightNodeId);
|
|
1461
|
-
}
|
|
1462
|
-
else {
|
|
1463
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
953
|
+
// D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
|
|
954
|
+
// (gated on `grammarConfig.enforceFormulaBetweenOperators` +
|
|
955
|
+
// `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
|
|
956
|
+
// sites — (1) new expression as child of anchor's parent;
|
|
957
|
+
// (2) left node as child of new expression; (3) right node as
|
|
958
|
+
// child of new expression — were deleted. AN-1 (post-mutation
|
|
959
|
+
// hook in assistive mode) inserts the buffer when any of these
|
|
960
|
+
// sites produces a non-not operator under operator;
|
|
961
|
+
// permissive mode leaves the un-buffered state and
|
|
962
|
+
// `validate('presentable')` flags it.
|
|
1468
963
|
const anchorParentId = anchor.parentId;
|
|
1469
964
|
const anchorPosition = anchor.position;
|
|
1470
|
-
// Compute child positions
|
|
1471
|
-
//
|
|
965
|
+
// Compute child positions. For `implies`/`iff` the S-8 invariant
|
|
966
|
+
// pins the two children to exact positions [0, 1] — bisection
|
|
967
|
+
// headroom is irrelevant because S-8 also fixes arity at 2. For
|
|
968
|
+
// `and`/`or` (and `formula`, though it's never produced here), use
|
|
969
|
+
// midpoint-spaced positions so future inserts can bisect. Mirrors
|
|
970
|
+
// the wrapExpression branch landed in c303aa4.
|
|
971
|
+
const isBinaryOp = expression.type === "operator" &&
|
|
972
|
+
(expression.operator === "implies" || expression.operator === "iff");
|
|
1472
973
|
let leftPosition;
|
|
1473
974
|
let rightPosition;
|
|
1474
975
|
if (leftNodeId !== undefined && rightNodeId !== undefined) {
|
|
1475
|
-
leftPosition = this.positionConfig.initial;
|
|
1476
|
-
rightPosition =
|
|
976
|
+
leftPosition = isBinaryOp ? 0 : this.positionConfig.initial;
|
|
977
|
+
rightPosition = isBinaryOp
|
|
978
|
+
? 1
|
|
979
|
+
: midpoint(this.positionConfig.initial, this.positionConfig.max);
|
|
1477
980
|
}
|
|
1478
981
|
else if (leftNodeId !== undefined) {
|
|
1479
982
|
leftPosition = this.positionConfig.initial;
|
|
@@ -1490,39 +993,18 @@ export class ExpressionManager {
|
|
|
1490
993
|
if (leftNodeId !== undefined) {
|
|
1491
994
|
this.reparent(leftNodeId, expression.id, leftPosition);
|
|
1492
995
|
}
|
|
1493
|
-
//
|
|
1494
|
-
// is needed, the formula takes the anchor slot and the expression goes under it.
|
|
1495
|
-
let finalParentId = anchorParentId;
|
|
1496
|
-
let finalPosition = anchorPosition;
|
|
1497
|
-
if (needsParentFormulaBuffer) {
|
|
1498
|
-
const formulaId = this.registerFormulaBuffer(expression, anchorParentId, anchorPosition);
|
|
1499
|
-
finalParentId = formulaId;
|
|
1500
|
-
finalPosition = 0;
|
|
1501
|
-
}
|
|
1502
|
-
// Store the new expression in its slot.
|
|
996
|
+
// Store the new expression in the anchor's slot.
|
|
1503
997
|
const stored = this.attachChecksum({
|
|
1504
998
|
...expression,
|
|
1505
|
-
parentId:
|
|
1506
|
-
position:
|
|
999
|
+
parentId: anchorParentId,
|
|
1000
|
+
position: anchorPosition,
|
|
1507
1001
|
});
|
|
1508
1002
|
this.expressions.set(expression.id, stored);
|
|
1509
1003
|
this.collector?.addedExpression({
|
|
1510
1004
|
...stored,
|
|
1511
1005
|
});
|
|
1512
|
-
getOrCreate(this.childExpressionIdsByParentId,
|
|
1513
|
-
getOrCreate(this.childPositionsByParentId,
|
|
1514
|
-
// Site 2: auto-insert formula buffers between the new expression and
|
|
1515
|
-
// any offending operator children.
|
|
1516
|
-
for (const childId of childrenNeedingFormulaBuffer) {
|
|
1517
|
-
const child = this.expressions.get(childId);
|
|
1518
|
-
const childPosition = child.position;
|
|
1519
|
-
// Reparent the child under the formula first. This detaches the child
|
|
1520
|
-
// from expression.id's tracking (removing its position from the set).
|
|
1521
|
-
// registerFormulaBuffer then occupies the freed position.
|
|
1522
|
-
const formulaId = this.generateId();
|
|
1523
|
-
this.reparent(childId, formulaId, 0);
|
|
1524
|
-
this.registerFormulaBuffer(expression, expression.id, childPosition, formulaId);
|
|
1525
|
-
}
|
|
1006
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(expression.id);
|
|
1007
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
1526
1008
|
// Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
1527
1009
|
// Note: reparent() already marks children dirty, so this propagates from the new expression up.
|
|
1528
1010
|
this.markExpressionDirty(expression.id);
|
|
@@ -1597,56 +1079,39 @@ export class ExpressionManager {
|
|
|
1597
1079
|
(newSibling.operator === "implies" || newSibling.operator === "iff")) {
|
|
1598
1080
|
throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1599
1081
|
}
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
if (existingParent && existingParent.type === "operator") {
|
|
1611
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1612
|
-
needsParentFormulaBuffer = true;
|
|
1613
|
-
}
|
|
1614
|
-
else {
|
|
1615
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
// Check 2 (Site 2): existing node as child of new operator.
|
|
1620
|
-
if (existingNode.type === "operator" &&
|
|
1621
|
-
existingNode.operator !== "not") {
|
|
1622
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1623
|
-
existingNodeNeedsFormulaBuffer = true;
|
|
1624
|
-
}
|
|
1625
|
-
else {
|
|
1626
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
// Check 3 (Site 3): new sibling as child of new operator.
|
|
1630
|
-
if (newSibling.type === "operator" &&
|
|
1631
|
-
newSibling.operator !== "not") {
|
|
1632
|
-
if (resolveAutoNormalize(this.grammarConfig, "wrapInsertFormula")) {
|
|
1633
|
-
siblingNeedsFormulaBuffer = true;
|
|
1634
|
-
}
|
|
1635
|
-
else {
|
|
1636
|
-
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1082
|
+
// D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
|
|
1083
|
+
// (gated on `grammarConfig.enforceFormulaBetweenOperators` +
|
|
1084
|
+
// `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
|
|
1085
|
+
// sites — (1) new operator as child of existing node's parent;
|
|
1086
|
+
// (2) existing node as child of new operator;
|
|
1087
|
+
// (3) new sibling as child of new operator — were deleted.
|
|
1088
|
+
// AN-1 (post-mutation hook in assistive mode) inserts the
|
|
1089
|
+
// buffer when any of these sites produces a non-not operator
|
|
1090
|
+
// under operator; permissive mode leaves the un-buffered state
|
|
1091
|
+
// and `validate('presentable')` flags it.
|
|
1640
1092
|
// Save the existing node's slot (the operator will inherit it).
|
|
1641
1093
|
const anchorParentId = existingNode.parentId;
|
|
1642
1094
|
const anchorPosition = existingNode.position;
|
|
1643
|
-
// Determine child positions
|
|
1095
|
+
// Determine child positions. For `implies`/`iff` the S-8 invariant
|
|
1096
|
+
// pins the two children to exact positions [0, 1] — bisection
|
|
1097
|
+
// headroom is irrelevant because S-8 also fixes arity at 2. For
|
|
1098
|
+
// `and`/`or` (and `formula`, though it's never produced here), use
|
|
1099
|
+
// midpoint-spaced positions so future inserts can bisect.
|
|
1100
|
+
const isBinaryOp = operator.operator === "implies" || operator.operator === "iff";
|
|
1644
1101
|
const existingPosition = leftNodeId !== undefined
|
|
1645
|
-
?
|
|
1646
|
-
|
|
1102
|
+
? isBinaryOp
|
|
1103
|
+
? 0
|
|
1104
|
+
: this.positionConfig.initial
|
|
1105
|
+
: isBinaryOp
|
|
1106
|
+
? 1
|
|
1107
|
+
: midpoint(this.positionConfig.initial, this.positionConfig.max);
|
|
1647
1108
|
const siblingPosition = leftNodeId !== undefined
|
|
1648
|
-
?
|
|
1649
|
-
|
|
1109
|
+
? isBinaryOp
|
|
1110
|
+
? 1
|
|
1111
|
+
: midpoint(this.positionConfig.initial, this.positionConfig.max)
|
|
1112
|
+
: isBinaryOp
|
|
1113
|
+
? 0
|
|
1114
|
+
: this.positionConfig.initial;
|
|
1650
1115
|
// Reparent existing node under operator.
|
|
1651
1116
|
this.reparent(existingNodeId, operator.id, existingPosition);
|
|
1652
1117
|
// Store new sibling under operator.
|
|
@@ -1661,47 +1126,18 @@ export class ExpressionManager {
|
|
|
1661
1126
|
});
|
|
1662
1127
|
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
|
|
1663
1128
|
getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(siblingPosition);
|
|
1664
|
-
//
|
|
1665
|
-
// the formula takes the anchor slot and the operator goes under it.
|
|
1666
|
-
let operatorParentId = anchorParentId;
|
|
1667
|
-
let operatorPosition = anchorPosition;
|
|
1668
|
-
if (needsParentFormulaBuffer) {
|
|
1669
|
-
const formulaId = this.registerFormulaBuffer(operator, anchorParentId, anchorPosition);
|
|
1670
|
-
operatorParentId = formulaId;
|
|
1671
|
-
operatorPosition = 0;
|
|
1672
|
-
}
|
|
1673
|
-
// Store operator in its slot.
|
|
1129
|
+
// Store operator in the anchor slot.
|
|
1674
1130
|
const storedOperator = this.attachChecksum({
|
|
1675
1131
|
...operator,
|
|
1676
|
-
parentId:
|
|
1677
|
-
position:
|
|
1132
|
+
parentId: anchorParentId,
|
|
1133
|
+
position: anchorPosition,
|
|
1678
1134
|
});
|
|
1679
1135
|
this.expressions.set(operator.id, storedOperator);
|
|
1680
1136
|
this.collector?.addedExpression({
|
|
1681
1137
|
...storedOperator,
|
|
1682
1138
|
});
|
|
1683
|
-
getOrCreate(this.childExpressionIdsByParentId,
|
|
1684
|
-
getOrCreate(this.childPositionsByParentId,
|
|
1685
|
-
// Site 2: auto-insert formula buffer between operator and existing node.
|
|
1686
|
-
if (existingNodeNeedsFormulaBuffer) {
|
|
1687
|
-
const existingChild = this.expressions.get(existingNodeId);
|
|
1688
|
-
const childPosition = existingChild.position;
|
|
1689
|
-
const formulaId = this.generateId();
|
|
1690
|
-
// Reparent existing node under formula first (frees position in operator's tracking).
|
|
1691
|
-
// registerFormulaBuffer then occupies the freed position.
|
|
1692
|
-
this.reparent(existingNodeId, formulaId, 0);
|
|
1693
|
-
this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
|
|
1694
|
-
}
|
|
1695
|
-
// Site 3: auto-insert formula buffer between operator and new sibling.
|
|
1696
|
-
if (siblingNeedsFormulaBuffer) {
|
|
1697
|
-
const siblingChild = this.expressions.get(newSibling.id);
|
|
1698
|
-
const childPosition = siblingChild.position;
|
|
1699
|
-
const formulaId = this.generateId();
|
|
1700
|
-
// Reparent sibling under formula first (frees position in operator's tracking).
|
|
1701
|
-
// registerFormulaBuffer then occupies the freed position.
|
|
1702
|
-
this.reparent(newSibling.id, formulaId, 0);
|
|
1703
|
-
this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
|
|
1704
|
-
}
|
|
1139
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(operator.id);
|
|
1140
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
1705
1141
|
// Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
|
|
1706
1142
|
// reparent() already marks the existing node dirty; mark the operator and sibling as well.
|
|
1707
1143
|
this.markExpressionDirty(newSibling.id);
|
|
@@ -1717,6 +1153,80 @@ export class ExpressionManager {
|
|
|
1717
1153
|
}
|
|
1718
1154
|
this.reparent(expressionId, newParentId, newPosition);
|
|
1719
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Inserts a new `formula` node between an existing expression and
|
|
1158
|
+
* its current parent atomically. The formula takes the child's
|
|
1159
|
+
* original slot (parentId + position); the child becomes the
|
|
1160
|
+
* formula's sole child at position 0.
|
|
1161
|
+
*
|
|
1162
|
+
* Used by the native AN-1 (formula-buffer insertion) pass in
|
|
1163
|
+
* `src/lib/grammar/an-rules.ts` per spec §5.1. Composing this from
|
|
1164
|
+
* `addExpression` + `reparentExpression` is not possible without
|
|
1165
|
+
* trip-wires: `addExpression(formula, parent, childPosition)` would
|
|
1166
|
+
* throw S-9 (child still occupies that slot), and `assertChildLimit`
|
|
1167
|
+
* would reject the extra child under unary `not` parents and binary
|
|
1168
|
+
* `implies`/`iff` parents even transiently. This primitive sidesteps
|
|
1169
|
+
* both: the net child count of the parent is unchanged by the wrap
|
|
1170
|
+
* (the formula displaces the child), so the limit isn't actually
|
|
1171
|
+
* violated, just transiently if expressed via two atomic mutations.
|
|
1172
|
+
*
|
|
1173
|
+
* Generates the formula's id via the caller-supplied `formulaId`
|
|
1174
|
+
* parameter so the caller (PE) can plug in the engine's
|
|
1175
|
+
* `idGenerator` rather than this manager minting one internally —
|
|
1176
|
+
* keeps id provenance explicit at the PE boundary.
|
|
1177
|
+
*
|
|
1178
|
+
* The new formula inherits the source child's `argumentId`,
|
|
1179
|
+
* `argumentVersion`, and `premiseId` automatically.
|
|
1180
|
+
*
|
|
1181
|
+
* @throws If `childId` does not exist.
|
|
1182
|
+
* @throws If `childId` is at the root (`parentId === null`) — there
|
|
1183
|
+
* is no operator parent to insert a buffer beneath.
|
|
1184
|
+
*/
|
|
1185
|
+
wrapInFormula(childId, formulaId) {
|
|
1186
|
+
const child = this.expressions.get(childId);
|
|
1187
|
+
if (!child) {
|
|
1188
|
+
throw new Error(`Expression "${childId}" does not exist.`);
|
|
1189
|
+
}
|
|
1190
|
+
if (child.parentId === null) {
|
|
1191
|
+
throw new Error(`Cannot wrap root expression "${childId}" in a formula — no parent operator above it.`);
|
|
1192
|
+
}
|
|
1193
|
+
// S-10 entity-ID uniqueness: refuse to mint a formula at an id
|
|
1194
|
+
// that already exists in this premise. Without this check
|
|
1195
|
+
// `registerFormulaBuffer` would silently overwrite the prior
|
|
1196
|
+
// expression via `this.expressions.set`. Today's AN-1 caller
|
|
1197
|
+
// mints via `engine.idGenerator` (crypto UUID v4) so collision
|
|
1198
|
+
// is astronomically unlikely, but `wrapInFormula` is a public
|
|
1199
|
+
// bundled-composite primitive — the API surface promises S-10
|
|
1200
|
+
// enforcement regardless of immediate caller.
|
|
1201
|
+
if (this.expressions.has(formulaId)) {
|
|
1202
|
+
throw new Error(`S-10: formulaId "${formulaId}" already exists in this premise.`);
|
|
1203
|
+
}
|
|
1204
|
+
const childParentId = child.parentId;
|
|
1205
|
+
const childPosition = child.position;
|
|
1206
|
+
// Register the formula at the child's old slot. Bypasses the
|
|
1207
|
+
// S-9 check via direct map insertion — at this transient point
|
|
1208
|
+
// the child still nominally occupies childPosition (its
|
|
1209
|
+
// parentId/position fields are unchanged), but the position
|
|
1210
|
+
// *set* under childParentId already contains childPosition (it
|
|
1211
|
+
// was added when the child was inserted), and Set.add is
|
|
1212
|
+
// idempotent, so re-adding it for the formula is safe. The
|
|
1213
|
+
// very next reparent call moves the child to (formulaId, 0),
|
|
1214
|
+
// freeing childPosition from the child's tracking and leaving
|
|
1215
|
+
// it owned by the formula alone.
|
|
1216
|
+
// `child` is already typed `TExpr` from `this.expressions.get`,
|
|
1217
|
+
// so no cast is needed at this call site.
|
|
1218
|
+
this.registerFormulaBuffer(child, childParentId, childPosition, formulaId);
|
|
1219
|
+
// Move the child under the new formula at position 0. The
|
|
1220
|
+
// child's old slot under childParentId is freed by `reparent`,
|
|
1221
|
+
// then the formula's `add(childPosition)` we did above keeps
|
|
1222
|
+
// childPosition owned by the formula. Result: a single formula
|
|
1223
|
+
// sits where the child was, the child is the formula's only
|
|
1224
|
+
// descendant at position 0.
|
|
1225
|
+
this.reparent(childId, formulaId, 0);
|
|
1226
|
+
// Mark the formula dirty so its descendant/combined checksums
|
|
1227
|
+
// recompute (it now has a child).
|
|
1228
|
+
this.markExpressionDirty(formulaId);
|
|
1229
|
+
}
|
|
1720
1230
|
/**
|
|
1721
1231
|
* Deletes a single expression that has no children.
|
|
1722
1232
|
* Does NOT trigger operator collapse. Caller must ensure children
|
|
@@ -1773,8 +1283,9 @@ export class ExpressionManager {
|
|
|
1773
1283
|
...updated,
|
|
1774
1284
|
});
|
|
1775
1285
|
this.markExpressionDirty(expressionId);
|
|
1776
|
-
//
|
|
1777
|
-
this
|
|
1286
|
+
// D2: AN-4 (post-mutation hook in assistive mode) handles
|
|
1287
|
+
// same-operator absorption through a formula buffer if this
|
|
1288
|
+
// operator change produced one.
|
|
1778
1289
|
return this.expressions.get(expressionId) ?? updated;
|
|
1779
1290
|
}
|
|
1780
1291
|
/**
|
|
@@ -1878,21 +1389,13 @@ export class ExpressionManager {
|
|
|
1878
1389
|
entityId: id,
|
|
1879
1390
|
});
|
|
1880
1391
|
}
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
violations.push({
|
|
1889
|
-
code: EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED,
|
|
1890
|
-
message: `Non-not operator "${expr.operator}" expression "${id}" is a direct child of operator "${expr.parentId}".`,
|
|
1891
|
-
entityType: "expression",
|
|
1892
|
-
entityId: id,
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1392
|
+
// D2: the pre-v1.0 3g `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1393
|
+
// legacy-validate() check (gated on
|
|
1394
|
+
// `grammarConfig.enforceFormulaBetweenOperators`) is gone.
|
|
1395
|
+
// P-1 is now surfaced via the grammar-tier validators —
|
|
1396
|
+
// call `engine.validate('presentable')` and look for the
|
|
1397
|
+
// `P-1` code. The `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1398
|
+
// engine-error constant was deleted in lockstep.
|
|
1896
1399
|
// Collect positions for uniqueness check
|
|
1897
1400
|
const parentKey = expr.parentId;
|
|
1898
1401
|
let parentPositions = positionsByParent.get(parentKey);
|
|
@@ -1973,7 +1476,7 @@ export class ExpressionManager {
|
|
|
1973
1476
|
};
|
|
1974
1477
|
}
|
|
1975
1478
|
/** Creates a new ExpressionManager from a previously captured snapshot. */
|
|
1976
|
-
static fromSnapshot(snapshot,
|
|
1479
|
+
static fromSnapshot(snapshot, generateId) {
|
|
1977
1480
|
// Normalize checksumConfig in case the snapshot went through a JSON
|
|
1978
1481
|
// round-trip that converted Sets to arrays or empty objects.
|
|
1979
1482
|
const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
|
|
@@ -1983,10 +1486,8 @@ export class ExpressionManager {
|
|
|
1983
1486
|
checksumConfig: normalizedChecksumConfig,
|
|
1984
1487
|
}
|
|
1985
1488
|
: undefined;
|
|
1986
|
-
// During loading: use explicit grammarConfig, falling back to snapshot's config
|
|
1987
1489
|
const loadingConfig = {
|
|
1988
1490
|
...normalizedConfig,
|
|
1989
|
-
grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
|
|
1990
1491
|
generateId: generateId ?? normalizedConfig?.generateId,
|
|
1991
1492
|
};
|
|
1992
1493
|
const em = new ExpressionManager(loadingConfig);
|