@proposit/proposit-core 0.12.3 → 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/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 +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 +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 +232 -80
- 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,47 +950,16 @@ 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
965
|
// Compute child positions (midpoint-spaced for future bisection),
|
|
@@ -1490,39 +985,18 @@ export class ExpressionManager {
|
|
|
1490
985
|
if (leftNodeId !== undefined) {
|
|
1491
986
|
this.reparent(leftNodeId, expression.id, leftPosition);
|
|
1492
987
|
}
|
|
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.
|
|
988
|
+
// Store the new expression in the anchor's slot.
|
|
1503
989
|
const stored = this.attachChecksum({
|
|
1504
990
|
...expression,
|
|
1505
|
-
parentId:
|
|
1506
|
-
position:
|
|
991
|
+
parentId: anchorParentId,
|
|
992
|
+
position: anchorPosition,
|
|
1507
993
|
});
|
|
1508
994
|
this.expressions.set(expression.id, stored);
|
|
1509
995
|
this.collector?.addedExpression({
|
|
1510
996
|
...stored,
|
|
1511
997
|
});
|
|
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
|
-
}
|
|
998
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(expression.id);
|
|
999
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
1526
1000
|
// Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
1527
1001
|
// Note: reparent() already marks children dirty, so this propagates from the new expression up.
|
|
1528
1002
|
this.markExpressionDirty(expression.id);
|
|
@@ -1597,46 +1071,16 @@ export class ExpressionManager {
|
|
|
1597
1071
|
(newSibling.operator === "implies" || newSibling.operator === "iff")) {
|
|
1598
1072
|
throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1599
1073
|
}
|
|
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
|
-
}
|
|
1074
|
+
// D2: the pre-v1.0 P-1 inline buffer-insertion / throw branches
|
|
1075
|
+
// (gated on `grammarConfig.enforceFormulaBetweenOperators` +
|
|
1076
|
+
// `resolveAutoNormalize(_, 'wrapInsertFormula')`) for three
|
|
1077
|
+
// sites — (1) new operator as child of existing node's parent;
|
|
1078
|
+
// (2) existing node as child of new operator;
|
|
1079
|
+
// (3) new sibling as child of new operator — were deleted.
|
|
1080
|
+
// AN-1 (post-mutation hook in assistive mode) inserts the
|
|
1081
|
+
// buffer when any of these sites produces a non-not operator
|
|
1082
|
+
// under operator; permissive mode leaves the un-buffered state
|
|
1083
|
+
// and `validate('presentable')` flags it.
|
|
1640
1084
|
// Save the existing node's slot (the operator will inherit it).
|
|
1641
1085
|
const anchorParentId = existingNode.parentId;
|
|
1642
1086
|
const anchorPosition = existingNode.position;
|
|
@@ -1661,47 +1105,18 @@ export class ExpressionManager {
|
|
|
1661
1105
|
});
|
|
1662
1106
|
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
|
|
1663
1107
|
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.
|
|
1108
|
+
// Store operator in the anchor slot.
|
|
1674
1109
|
const storedOperator = this.attachChecksum({
|
|
1675
1110
|
...operator,
|
|
1676
|
-
parentId:
|
|
1677
|
-
position:
|
|
1111
|
+
parentId: anchorParentId,
|
|
1112
|
+
position: anchorPosition,
|
|
1678
1113
|
});
|
|
1679
1114
|
this.expressions.set(operator.id, storedOperator);
|
|
1680
1115
|
this.collector?.addedExpression({
|
|
1681
1116
|
...storedOperator,
|
|
1682
1117
|
});
|
|
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
|
-
}
|
|
1118
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(operator.id);
|
|
1119
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
1705
1120
|
// Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
|
|
1706
1121
|
// reparent() already marks the existing node dirty; mark the operator and sibling as well.
|
|
1707
1122
|
this.markExpressionDirty(newSibling.id);
|
|
@@ -1717,6 +1132,80 @@ export class ExpressionManager {
|
|
|
1717
1132
|
}
|
|
1718
1133
|
this.reparent(expressionId, newParentId, newPosition);
|
|
1719
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Inserts a new `formula` node between an existing expression and
|
|
1137
|
+
* its current parent atomically. The formula takes the child's
|
|
1138
|
+
* original slot (parentId + position); the child becomes the
|
|
1139
|
+
* formula's sole child at position 0.
|
|
1140
|
+
*
|
|
1141
|
+
* Used by the native AN-1 (formula-buffer insertion) pass in
|
|
1142
|
+
* `src/lib/grammar/an-rules.ts` per spec §5.1. Composing this from
|
|
1143
|
+
* `addExpression` + `reparentExpression` is not possible without
|
|
1144
|
+
* trip-wires: `addExpression(formula, parent, childPosition)` would
|
|
1145
|
+
* throw S-9 (child still occupies that slot), and `assertChildLimit`
|
|
1146
|
+
* would reject the extra child under unary `not` parents and binary
|
|
1147
|
+
* `implies`/`iff` parents even transiently. This primitive sidesteps
|
|
1148
|
+
* both: the net child count of the parent is unchanged by the wrap
|
|
1149
|
+
* (the formula displaces the child), so the limit isn't actually
|
|
1150
|
+
* violated, just transiently if expressed via two atomic mutations.
|
|
1151
|
+
*
|
|
1152
|
+
* Generates the formula's id via the caller-supplied `formulaId`
|
|
1153
|
+
* parameter so the caller (PE) can plug in the engine's
|
|
1154
|
+
* `idGenerator` rather than this manager minting one internally —
|
|
1155
|
+
* keeps id provenance explicit at the PE boundary.
|
|
1156
|
+
*
|
|
1157
|
+
* The new formula inherits the source child's `argumentId`,
|
|
1158
|
+
* `argumentVersion`, and `premiseId` automatically.
|
|
1159
|
+
*
|
|
1160
|
+
* @throws If `childId` does not exist.
|
|
1161
|
+
* @throws If `childId` is at the root (`parentId === null`) — there
|
|
1162
|
+
* is no operator parent to insert a buffer beneath.
|
|
1163
|
+
*/
|
|
1164
|
+
wrapInFormula(childId, formulaId) {
|
|
1165
|
+
const child = this.expressions.get(childId);
|
|
1166
|
+
if (!child) {
|
|
1167
|
+
throw new Error(`Expression "${childId}" does not exist.`);
|
|
1168
|
+
}
|
|
1169
|
+
if (child.parentId === null) {
|
|
1170
|
+
throw new Error(`Cannot wrap root expression "${childId}" in a formula — no parent operator above it.`);
|
|
1171
|
+
}
|
|
1172
|
+
// S-10 entity-ID uniqueness: refuse to mint a formula at an id
|
|
1173
|
+
// that already exists in this premise. Without this check
|
|
1174
|
+
// `registerFormulaBuffer` would silently overwrite the prior
|
|
1175
|
+
// expression via `this.expressions.set`. Today's AN-1 caller
|
|
1176
|
+
// mints via `engine.idGenerator` (crypto UUID v4) so collision
|
|
1177
|
+
// is astronomically unlikely, but `wrapInFormula` is a public
|
|
1178
|
+
// bundled-composite primitive — the API surface promises S-10
|
|
1179
|
+
// enforcement regardless of immediate caller.
|
|
1180
|
+
if (this.expressions.has(formulaId)) {
|
|
1181
|
+
throw new Error(`S-10: formulaId "${formulaId}" already exists in this premise.`);
|
|
1182
|
+
}
|
|
1183
|
+
const childParentId = child.parentId;
|
|
1184
|
+
const childPosition = child.position;
|
|
1185
|
+
// Register the formula at the child's old slot. Bypasses the
|
|
1186
|
+
// S-9 check via direct map insertion — at this transient point
|
|
1187
|
+
// the child still nominally occupies childPosition (its
|
|
1188
|
+
// parentId/position fields are unchanged), but the position
|
|
1189
|
+
// *set* under childParentId already contains childPosition (it
|
|
1190
|
+
// was added when the child was inserted), and Set.add is
|
|
1191
|
+
// idempotent, so re-adding it for the formula is safe. The
|
|
1192
|
+
// very next reparent call moves the child to (formulaId, 0),
|
|
1193
|
+
// freeing childPosition from the child's tracking and leaving
|
|
1194
|
+
// it owned by the formula alone.
|
|
1195
|
+
// `child` is already typed `TExpr` from `this.expressions.get`,
|
|
1196
|
+
// so no cast is needed at this call site.
|
|
1197
|
+
this.registerFormulaBuffer(child, childParentId, childPosition, formulaId);
|
|
1198
|
+
// Move the child under the new formula at position 0. The
|
|
1199
|
+
// child's old slot under childParentId is freed by `reparent`,
|
|
1200
|
+
// then the formula's `add(childPosition)` we did above keeps
|
|
1201
|
+
// childPosition owned by the formula. Result: a single formula
|
|
1202
|
+
// sits where the child was, the child is the formula's only
|
|
1203
|
+
// descendant at position 0.
|
|
1204
|
+
this.reparent(childId, formulaId, 0);
|
|
1205
|
+
// Mark the formula dirty so its descendant/combined checksums
|
|
1206
|
+
// recompute (it now has a child).
|
|
1207
|
+
this.markExpressionDirty(formulaId);
|
|
1208
|
+
}
|
|
1720
1209
|
/**
|
|
1721
1210
|
* Deletes a single expression that has no children.
|
|
1722
1211
|
* Does NOT trigger operator collapse. Caller must ensure children
|
|
@@ -1773,8 +1262,9 @@ export class ExpressionManager {
|
|
|
1773
1262
|
...updated,
|
|
1774
1263
|
});
|
|
1775
1264
|
this.markExpressionDirty(expressionId);
|
|
1776
|
-
//
|
|
1777
|
-
this
|
|
1265
|
+
// D2: AN-4 (post-mutation hook in assistive mode) handles
|
|
1266
|
+
// same-operator absorption through a formula buffer if this
|
|
1267
|
+
// operator change produced one.
|
|
1778
1268
|
return this.expressions.get(expressionId) ?? updated;
|
|
1779
1269
|
}
|
|
1780
1270
|
/**
|
|
@@ -1878,21 +1368,13 @@ export class ExpressionManager {
|
|
|
1878
1368
|
entityId: id,
|
|
1879
1369
|
});
|
|
1880
1370
|
}
|
|
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
|
-
}
|
|
1371
|
+
// D2: the pre-v1.0 3g `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1372
|
+
// legacy-validate() check (gated on
|
|
1373
|
+
// `grammarConfig.enforceFormulaBetweenOperators`) is gone.
|
|
1374
|
+
// P-1 is now surfaced via the grammar-tier validators —
|
|
1375
|
+
// call `engine.validate('presentable')` and look for the
|
|
1376
|
+
// `P-1` code. The `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
|
|
1377
|
+
// engine-error constant was deleted in lockstep.
|
|
1896
1378
|
// Collect positions for uniqueness check
|
|
1897
1379
|
const parentKey = expr.parentId;
|
|
1898
1380
|
let parentPositions = positionsByParent.get(parentKey);
|
|
@@ -1973,7 +1455,7 @@ export class ExpressionManager {
|
|
|
1973
1455
|
};
|
|
1974
1456
|
}
|
|
1975
1457
|
/** Creates a new ExpressionManager from a previously captured snapshot. */
|
|
1976
|
-
static fromSnapshot(snapshot,
|
|
1458
|
+
static fromSnapshot(snapshot, generateId) {
|
|
1977
1459
|
// Normalize checksumConfig in case the snapshot went through a JSON
|
|
1978
1460
|
// round-trip that converted Sets to arrays or empty objects.
|
|
1979
1461
|
const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
|
|
@@ -1983,10 +1465,8 @@ export class ExpressionManager {
|
|
|
1983
1465
|
checksumConfig: normalizedChecksumConfig,
|
|
1984
1466
|
}
|
|
1985
1467
|
: undefined;
|
|
1986
|
-
// During loading: use explicit grammarConfig, falling back to snapshot's config
|
|
1987
1468
|
const loadingConfig = {
|
|
1988
1469
|
...normalizedConfig,
|
|
1989
|
-
grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
|
|
1990
1470
|
generateId: generateId ?? normalizedConfig?.generateId,
|
|
1991
1471
|
};
|
|
1992
1472
|
const em = new ExpressionManager(loadingConfig);
|