import type { TCoreLogicalOperatorType, TCorePropositionalExpression } from "../schemata/index.js"; import type { ChangeCollector } from "./change-collector.js"; import { type TCorePositionConfig } from "../utils/position.js"; import type { TLogicEngineOptions } from "./argument-engine.js"; import type { TInvariantValidationResult } from "../types/validation.js"; export type TExpressionInput = TExpr extends infer U ? U extends TCorePropositionalExpression ? Omit : never : never; export type TExpressionWithoutPosition = TExpr extends infer U ? U extends TCorePropositionalExpression ? Omit : never : never; export type TExpressionManagerSnapshot = { expressions: TExpr[]; config?: TLogicEngineOptions; }; /** Fields that may be updated on an existing expression. */ export type TExpressionUpdate = { position?: number; variableId?: string; operator?: TCoreLogicalOperatorType; }; /** * Low-level manager for a flat-stored expression tree. * * Expressions are immutable value objects stored in three maps: the main * expression store, a parent-to-children ID index, and a parent-to-positions * index. Structural invariants (child limits, root-only operators, position * uniqueness) are enforced on every mutation. * * This class is an internal building block used by {@link PremiseEngine} * and is not part of the public API. */ export declare class ExpressionManager { private expressions; private childExpressionIdsByParentId; private childPositionsByParentId; private positionConfig; private config?; private generateId; private collector; private dirtyExpressionIds; setCollector(collector: ChangeCollector | null): void; constructor(config?: TLogicEngineOptions); /** * Returns the position config in effect for this expression manager. * Used by in-package helpers (e.g. native AN-4 in * `src/lib/grammar/an-rules.ts`) that need the position range * boundaries (`min`/`max`) for spacing-algorithm fallbacks when a * formula sits at the leftmost or rightmost slot under its parent. * * @internal */ getPositionConfig(): TCorePositionConfig; private attachChecksum; /** * Registers an expression in the internal data structures without any * grammar validation or normalization. This is the mechanical * bookkeeping that both `addExpression` (after validation) and * `loadInitialExpressions` (direct bulk load) share. */ private registerExpression; /** * Creates and registers a formula-buffer expression in the three internal * maps (`expressions`, `childExpressionIdsByParentId`, * `childPositionsByParentId`) and notifies the change collector. * * As of v1.0 the legacy per-mutation P-1 buffer-insertion branches * (`addExpression`/`insertExpression`/`wrapExpression`) are gone — * AN-1 (post-mutation hook in assistive mode, see * `src/lib/grammar/an-rules.ts`) is the sole formula-buffer * insertion path. This helper is invoked from `wrapInFormula` (the * public AN-1 primitive on `PremiseEngine`) which calls it once per * buffer insertion to materialize the formula node. * * @returns The generated formula expression ID. */ private registerFormulaBuffer; /** * Removes an expression from the three internal maps: deletes it from * the main `expressions` store, removes it from its parent's child-id * and position indexes, and deletes its own child-id and position * indexes. * * Callers remain responsible for collector notification, dirty-set * cleanup, and parent dirtying — timing for those varies by call site. */ private detachExpression; /** * Marks an expression and all its ancestors as dirty for hierarchical * checksum recomputation. Stops early when it reaches an expression * already in the dirty set (since its ancestors are already marked). */ markExpressionDirty(exprId: string): void; /** * Recomputes `descendantChecksum` and `combinedChecksum` for all dirty * expressions, processing bottom-up (deepest first) so that children * are up-to-date before their parents are computed. */ flushExpressionChecksums(): void; /** * Removes deleted expression IDs from the dirty set so that flush * doesn't attempt to process expressions that no longer exist. */ pruneDeletedFromDirtySet(deletedIds: Set): void; /** Returns all expressions sorted by ID for deterministic output. */ toArray(): TExpr[]; /** * Adds an expression to the tree. * * @throws If the expression ID already exists. * @throws If the expression references itself as parent. * @throws If `implies`/`iff` operators have a non-null parentId (they must be roots). * @throws If the parent does not exist or is not an operator/formula. * @throws If the parent's child limit would be exceeded. * @throws If the position is already occupied under the parent. */ addExpression(input: TExpressionInput): void; /** * Adds an expression as the last child of the given parent. * * If the parent has no children, the expression gets `POSITION_INITIAL`. * Otherwise it gets a midpoint between the last child's position and * `POSITION_MAX`. */ appendExpression(parentId: string | null, expression: TExpressionWithoutPosition): void; /** * Adds an expression immediately before or after an existing sibling. * * @throws If the sibling does not exist. */ addExpressionRelative(siblingId: string, relativePosition: "before" | "after", expression: TExpressionWithoutPosition): void; /** * Updates mutable fields of an existing expression in-place. * * Only `position`, `variableId`, and `operator` may be updated. Structural * fields (`id`, `parentId`, `type`, `argumentId`, `argumentVersion`, * `checksum`) are forbidden. * * Operator changes are restricted to swaps within an arity class: * variadic (`and`, `or`, `xor`) or binary (`implies`, `iff`). `not` is * unary and belongs to neither class, so it can be neither the source * nor the target of a swap. Variable ID changes require the expression * to be of type `"variable"`. * * @throws If the expression does not exist. * @throws If a forbidden field is present in `updates`. * @throws If an operator change is not permitted. * @throws If `variableId` is set on a non-variable expression. * @throws If the new position collides with a sibling. */ updateExpression(expressionId: string, updates: TExpressionUpdate): TExpr; /** * Removes an expression from the tree. * * When `deleteSubtree` is `true`, the expression and its entire descendant * subtree are removed. * * When `deleteSubtree` is `false`, the expression is removed but its single * child (if any) is promoted into the removed expression's slot. If the * expression has more than one child, an error is thrown. * * As of v1.0 the pre-removal collapse-cascade (the legacy * `collapseIfNeeded` / `simulateCollapseChain`) is gone — AN-3 * (post-mutation hook in assistive mode) handles 0/1-child * operator/formula collapse on the surviving parent. * * @throws If `deleteSubtree` is `false` and the expression has multiple children. * @throws If `deleteSubtree` is `false` and the single child is a root-only * operator (`implies`/`iff`) that would be placed in a non-root position. * @returns The removed expression, or `undefined` if not found. */ removeExpression(expressionId: string, deleteSubtree: boolean): TExpr | undefined; private removeSubtree; private removeAndPromote; /** * Redistributes the minimal set of sibling positions to create room at * an insertion point between `leftPos` and `rightPos` under `parentId`. * * When `leftPos` or `rightPos` is a boundary value (positionConfig.min/max) * rather than a real node position, the corresponding chain has 0 nodes. */ private repositionSiblings; /** Returns `true` if any expression in the tree references the given variable ID. */ hasVariableReference(variableId: string): boolean; /** Returns the expression with the given ID, or `undefined` if not found. */ getExpression(expressionId: string): TExpr | undefined; /** Returns the children of the given parent, sorted by position. */ getChildExpressions(parentId: string | null): TExpr[]; private loadInitialExpressions; /** * Pre-flight before {@link removeExpression} mutates state. As of v1.0 * the only structural failure mode for `removeAndPromote`'s 1-child * branch is the root-only-operator promotion rule (S-5): an * `implies`/`iff` child cannot be promoted into a non-root slot. The * pre-v1.0 P-1 promote-on-remove check is gone, along with the rest * of the `grammarConfig.enforceFormulaBetweenOperators` machinery; * the legacy `collapseEmptyFormula` cascade simulation * (`simulateCollapseChain` / `simulatePostPromotionCollapse`) is * gone in lockstep — AN-3 (post-mutation hook in assistive mode) * handles every collapse case. */ private assertRemovalSafe; /** * Checks whether promoting `child` into a slot with the given `newParentId` * would violate the root-only rule (S-5). The pre-v1.0 nesting check * (P-1 / `enforceFormulaBetweenOperators`) is gone. */ private assertPromotionSafe; private assertChildLimit; private reparent; /** * Inserts a new expression between existing nodes in the tree. * * The new expression inherits the tree slot of the anchor node * (`leftNodeId ?? rightNodeId`). The anchor and optional second node * become children of the new expression at midpoint-spaced positions * (`POSITION_INITIAL` and `midpoint(POSITION_INITIAL, POSITION_MAX)`). * * Right node is reparented before left node to handle the case where * the right node is a descendant of the left node's subtree. * * @throws If neither leftNodeId nor rightNodeId is provided. * @throws If the expression ID already exists. * @throws If leftNodeId and rightNodeId are the same. * @throws If either referenced node does not exist. * @throws If a unary operator/formula is given two children. * @throws If either child is an `implies`/`iff` operator (cannot be subordinated). * @throws If an `implies`/`iff` expression would be inserted at a non-root position. */ insertExpression(expression: TExpressionInput, leftNodeId?: string, rightNodeId?: string): void; /** * Wraps an existing expression with a new operator and a new sibling. * * The operator takes the existing node's slot in the tree. Both the * existing node and the new sibling become children of the operator. * * Exactly one of `leftNodeId` / `rightNodeId` must be provided — it * identifies the existing node and which child slot (position 0 or 1) * it occupies. The new sibling fills the other slot. * * @throws If neither or both of leftNodeId/rightNodeId are provided. * @throws If the operator or sibling expression ID already exists. * @throws If operator and sibling IDs are the same. * @throws If the existing node does not exist. * @throws If the operator is not of type `"operator"`. * @throws If the operator is unary (`not`). * @throws If the operator is `implies`/`iff` and the existing node is not at root. * @throws If the existing node is an `implies`/`iff` operator (cannot be subordinated). * @throws If the new sibling is an `implies`/`iff` operator (cannot be subordinated). */ wrapExpression(operator: TExpressionWithoutPosition, newSibling: TExpressionWithoutPosition, leftNodeId?: string, rightNodeId?: string): void; /** * Reparents an expression to a new parent at a given position. */ reparentExpression(expressionId: string, newParentId: string | null, newPosition: number): void; /** * Inserts a new `formula` node between an existing expression and * its current parent atomically. The formula takes the child's * original slot (parentId + position); the child becomes the * formula's sole child at position 0. * * Used by the native AN-1 (formula-buffer insertion) pass in * `src/lib/grammar/an-rules.ts` per spec §5.1. Composing this from * `addExpression` + `reparentExpression` is not possible without * trip-wires: `addExpression(formula, parent, childPosition)` would * throw S-9 (child still occupies that slot), and `assertChildLimit` * would reject the extra child under unary `not` parents and binary * `implies`/`iff` parents even transiently. This primitive sidesteps * both: the net child count of the parent is unchanged by the wrap * (the formula displaces the child), so the limit isn't actually * violated, just transiently if expressed via two atomic mutations. * * Generates the formula's id via the caller-supplied `formulaId` * parameter so the caller (PE) can plug in the engine's * `idGenerator` rather than this manager minting one internally — * keeps id provenance explicit at the PE boundary. * * The new formula inherits the source child's `argumentId`, * `argumentVersion`, and `premiseId` automatically. * * @throws If `childId` does not exist. * @throws If `childId` is at the root (`parentId === null`) — there * is no operator parent to insert a buffer beneath. */ wrapInFormula(childId: string, formulaId: string): void; /** * Deletes a single expression that has no children. * Does NOT trigger operator collapse. Caller must ensure children * have been reparented away first. */ deleteExpression(expressionId: string): TExpr | undefined; /** * Changes the operator type of an operator expression without the swap * restriction enforced by {@link updateExpression}. Only validates that * the target expression is an operator, the new operator is not `"not"`, * and root-only constraints are satisfied. */ changeOperatorType(expressionId: string, newOperator: TCoreLogicalOperatorType): TExpr; /** * Loads expressions in BFS order, respecting the current grammar config. * Used by restoration paths (fromData, rollback) that load existing data. */ loadExpressions(expressions: TExpressionInput[]): void; /** * Performs a comprehensive validation sweep on all managed expressions. * * Collects ALL violations rather than failing on the first one. Checks: * schema validity, duplicate IDs, self-referential parents, parent * existence, parent container type, root-only operators, child limits, * position uniqueness, and checksum integrity. */ validate(): TInvariantValidationResult; /** Returns a serializable snapshot of the current state. */ snapshot(): TExpressionManagerSnapshot; /** Creates a new ExpressionManager from a previously captured snapshot. */ static fromSnapshot(snapshot: TExpressionManagerSnapshot, generateId?: () => string): ExpressionManager; } //# sourceMappingURL=expression-manager.d.ts.map