@proposit/proposit-core 0.8.9
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 +1032 -0
- package/dist/cli/commands/analysis.d.ts +3 -0
- package/dist/cli/commands/analysis.d.ts.map +1 -0
- package/dist/cli/commands/analysis.js +504 -0
- package/dist/cli/commands/analysis.js.map +1 -0
- package/dist/cli/commands/arguments.d.ts +3 -0
- package/dist/cli/commands/arguments.d.ts.map +1 -0
- package/dist/cli/commands/arguments.js +187 -0
- package/dist/cli/commands/arguments.js.map +1 -0
- package/dist/cli/commands/claims.d.ts +3 -0
- package/dist/cli/commands/claims.d.ts.map +1 -0
- package/dist/cli/commands/claims.js +120 -0
- package/dist/cli/commands/claims.js.map +1 -0
- package/dist/cli/commands/diff.d.ts +3 -0
- package/dist/cli/commands/diff.d.ts.map +1 -0
- package/dist/cli/commands/diff.js +61 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/expressions.d.ts +3 -0
- package/dist/cli/commands/expressions.d.ts.map +1 -0
- package/dist/cli/commands/expressions.js +344 -0
- package/dist/cli/commands/expressions.js.map +1 -0
- package/dist/cli/commands/graph.d.ts +13 -0
- package/dist/cli/commands/graph.d.ts.map +1 -0
- package/dist/cli/commands/graph.js +382 -0
- package/dist/cli/commands/graph.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +3 -0
- package/dist/cli/commands/meta.d.ts.map +1 -0
- package/dist/cli/commands/meta.js +14 -0
- package/dist/cli/commands/meta.js.map +1 -0
- package/dist/cli/commands/parse.d.ts +3 -0
- package/dist/cli/commands/parse.d.ts.map +1 -0
- package/dist/cli/commands/parse.js +171 -0
- package/dist/cli/commands/parse.js.map +1 -0
- package/dist/cli/commands/premises.d.ts +3 -0
- package/dist/cli/commands/premises.d.ts.map +1 -0
- package/dist/cli/commands/premises.js +261 -0
- package/dist/cli/commands/premises.js.map +1 -0
- package/dist/cli/commands/render.d.ts +3 -0
- package/dist/cli/commands/render.d.ts.map +1 -0
- package/dist/cli/commands/render.js +109 -0
- package/dist/cli/commands/render.js.map +1 -0
- package/dist/cli/commands/repair.d.ts +3 -0
- package/dist/cli/commands/repair.d.ts.map +1 -0
- package/dist/cli/commands/repair.js +53 -0
- package/dist/cli/commands/repair.js.map +1 -0
- package/dist/cli/commands/roles.d.ts +3 -0
- package/dist/cli/commands/roles.d.ts.map +1 -0
- package/dist/cli/commands/roles.js +64 -0
- package/dist/cli/commands/roles.js.map +1 -0
- package/dist/cli/commands/sources.d.ts +3 -0
- package/dist/cli/commands/sources.d.ts.map +1 -0
- package/dist/cli/commands/sources.js +103 -0
- package/dist/cli/commands/sources.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +27 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/commands/variables.d.ts +3 -0
- package/dist/cli/commands/variables.d.ts.map +1 -0
- package/dist/cli/commands/variables.js +206 -0
- package/dist/cli/commands/variables.js.map +1 -0
- package/dist/cli/commands/version-show.d.ts +3 -0
- package/dist/cli/commands/version-show.d.ts.map +1 -0
- package/dist/cli/commands/version-show.js +31 -0
- package/dist/cli/commands/version-show.js.map +1 -0
- package/dist/cli/config.d.ts +8 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +24 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/engine.d.ts +19 -0
- package/dist/cli/engine.d.ts.map +1 -0
- package/dist/cli/engine.js +173 -0
- package/dist/cli/engine.js.map +1 -0
- package/dist/cli/import.d.ts +22 -0
- package/dist/cli/import.d.ts.map +1 -0
- package/dist/cli/import.js +242 -0
- package/dist/cli/import.js.map +1 -0
- package/dist/cli/llm/index.d.ts +6 -0
- package/dist/cli/llm/index.d.ts.map +1 -0
- package/dist/cli/llm/index.js +26 -0
- package/dist/cli/llm/index.js.map +1 -0
- package/dist/cli/llm/openai.d.ts +4 -0
- package/dist/cli/llm/openai.d.ts.map +1 -0
- package/dist/cli/llm/openai.js +44 -0
- package/dist/cli/llm/openai.js.map +1 -0
- package/dist/cli/llm/types.d.ts +14 -0
- package/dist/cli/llm/types.d.ts.map +1 -0
- package/dist/cli/llm/types.js +2 -0
- package/dist/cli/llm/types.js.map +1 -0
- package/dist/cli/logging.d.ts +8 -0
- package/dist/cli/logging.d.ts.map +1 -0
- package/dist/cli/logging.js +23 -0
- package/dist/cli/logging.js.map +1 -0
- package/dist/cli/output/diff-renderer.d.ts +4 -0
- package/dist/cli/output/diff-renderer.d.ts.map +1 -0
- package/dist/cli/output/diff-renderer.js +89 -0
- package/dist/cli/output/diff-renderer.js.map +1 -0
- package/dist/cli/output.d.ts +6 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +41 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/router.d.ts +14 -0
- package/dist/cli/router.d.ts.map +1 -0
- package/dist/cli/router.js +59 -0
- package/dist/cli/router.js.map +1 -0
- package/dist/cli/schemata.d.ts +74 -0
- package/dist/cli/schemata.d.ts.map +1 -0
- package/dist/cli/schemata.js +89 -0
- package/dist/cli/schemata.js.map +1 -0
- package/dist/cli/storage/analysis.d.ts +9 -0
- package/dist/cli/storage/analysis.d.ts.map +1 -0
- package/dist/cli/storage/analysis.js +108 -0
- package/dist/cli/storage/analysis.js.map +1 -0
- package/dist/cli/storage/arguments.d.ts +12 -0
- package/dist/cli/storage/arguments.d.ts.map +1 -0
- package/dist/cli/storage/arguments.js +80 -0
- package/dist/cli/storage/arguments.js.map +1 -0
- package/dist/cli/storage/libraries.d.ts +14 -0
- package/dist/cli/storage/libraries.d.ts.map +1 -0
- package/dist/cli/storage/libraries.js +80 -0
- package/dist/cli/storage/libraries.js.map +1 -0
- package/dist/cli/storage/premises.d.ts +9 -0
- package/dist/cli/storage/premises.d.ts.map +1 -0
- package/dist/cli/storage/premises.js +67 -0
- package/dist/cli/storage/premises.js.map +1 -0
- package/dist/cli/storage/roles.d.ts +4 -0
- package/dist/cli/storage/roles.d.ts.map +1 -0
- package/dist/cli/storage/roles.js +26 -0
- package/dist/cli/storage/roles.js.map +1 -0
- package/dist/cli/storage/variables.d.ts +4 -0
- package/dist/cli/storage/variables.d.ts.map +1 -0
- package/dist/cli/storage/variables.js +36 -0
- package/dist/cli/storage/variables.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +65 -0
- package/dist/cli.js.map +1 -0
- package/dist/extensions/basics/argument-parser.d.ts +12 -0
- package/dist/extensions/basics/argument-parser.d.ts.map +1 -0
- package/dist/extensions/basics/argument-parser.js +27 -0
- package/dist/extensions/basics/argument-parser.js.map +1 -0
- package/dist/extensions/basics/index.d.ts +4 -0
- package/dist/extensions/basics/index.d.ts.map +1 -0
- package/dist/extensions/basics/index.js +3 -0
- package/dist/extensions/basics/index.js.map +1 -0
- package/dist/extensions/basics/schemata.d.ts +35 -0
- package/dist/extensions/basics/schemata.d.ts.map +1 -0
- package/dist/extensions/basics/schemata.js +55 -0
- package/dist/extensions/basics/schemata.js.map +1 -0
- package/dist/extensions/ieee/formatting.d.ts +18 -0
- package/dist/extensions/ieee/formatting.d.ts.map +1 -0
- package/dist/extensions/ieee/formatting.js +57 -0
- package/dist/extensions/ieee/formatting.js.map +1 -0
- package/dist/extensions/ieee/index.d.ts +6 -0
- package/dist/extensions/ieee/index.d.ts.map +1 -0
- package/dist/extensions/ieee/index.js +6 -0
- package/dist/extensions/ieee/index.js.map +1 -0
- package/dist/extensions/ieee/references.d.ts +1379 -0
- package/dist/extensions/ieee/references.d.ts.map +1 -0
- package/dist/extensions/ieee/references.js +929 -0
- package/dist/extensions/ieee/references.js.map +1 -0
- package/dist/extensions/ieee/relaxed.d.ts +1371 -0
- package/dist/extensions/ieee/relaxed.d.ts.map +1 -0
- package/dist/extensions/ieee/relaxed.js +160 -0
- package/dist/extensions/ieee/relaxed.js.map +1 -0
- package/dist/extensions/ieee/segment-builder.d.ts +9 -0
- package/dist/extensions/ieee/segment-builder.d.ts.map +1 -0
- package/dist/extensions/ieee/segment-builder.js +98 -0
- package/dist/extensions/ieee/segment-builder.js.map +1 -0
- package/dist/extensions/ieee/segment-templates.d.ts +58 -0
- package/dist/extensions/ieee/segment-templates.d.ts.map +1 -0
- package/dist/extensions/ieee/segment-templates.js +1618 -0
- package/dist/extensions/ieee/segment-templates.js.map +1 -0
- package/dist/extensions/ieee/source.d.ts +434 -0
- package/dist/extensions/ieee/source.d.ts.map +1 -0
- package/dist/extensions/ieee/source.js +12 -0
- package/dist/extensions/ieee/source.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/consts.d.ts +21 -0
- package/dist/lib/consts.d.ts.map +1 -0
- package/dist/lib/consts.js +117 -0
- package/dist/lib/consts.js.map +1 -0
- package/dist/lib/core/argument-engine.d.ts +181 -0
- package/dist/lib/core/argument-engine.d.ts.map +1 -0
- package/dist/lib/core/argument-engine.js +1294 -0
- package/dist/lib/core/argument-engine.js.map +1 -0
- package/dist/lib/core/argument-library.d.ts +84 -0
- package/dist/lib/core/argument-library.d.ts.map +1 -0
- package/dist/lib/core/argument-library.js +122 -0
- package/dist/lib/core/argument-library.js.map +1 -0
- package/dist/lib/core/argument-validation.d.ts +74 -0
- package/dist/lib/core/argument-validation.d.ts.map +1 -0
- package/dist/lib/core/argument-validation.js +315 -0
- package/dist/lib/core/argument-validation.js.map +1 -0
- package/dist/lib/core/change-collector.d.ts +24 -0
- package/dist/lib/core/change-collector.d.ts.map +1 -0
- package/dist/lib/core/change-collector.js +63 -0
- package/dist/lib/core/change-collector.js.map +1 -0
- package/dist/lib/core/checksum.d.ts +15 -0
- package/dist/lib/core/checksum.d.ts.map +1 -0
- package/dist/lib/core/checksum.js +43 -0
- package/dist/lib/core/checksum.js.map +1 -0
- package/dist/lib/core/claim-library.d.ts +23 -0
- package/dist/lib/core/claim-library.d.ts.map +1 -0
- package/dist/lib/core/claim-library.js +21 -0
- package/dist/lib/core/claim-library.js.map +1 -0
- package/dist/lib/core/claim-source-library.d.ts +32 -0
- package/dist/lib/core/claim-source-library.d.ts.map +1 -0
- package/dist/lib/core/claim-source-library.js +193 -0
- package/dist/lib/core/claim-source-library.js.map +1 -0
- package/dist/lib/core/diff.d.ts +20 -0
- package/dist/lib/core/diff.d.ts.map +1 -0
- package/dist/lib/core/diff.js +260 -0
- package/dist/lib/core/diff.js.map +1 -0
- package/dist/lib/core/evaluation/argument-evaluation.d.ts +53 -0
- package/dist/lib/core/evaluation/argument-evaluation.d.ts.map +1 -0
- package/dist/lib/core/evaluation/argument-evaluation.js +535 -0
- package/dist/lib/core/evaluation/argument-evaluation.js.map +1 -0
- package/dist/lib/core/evaluation/grading.d.ts +28 -0
- package/dist/lib/core/evaluation/grading.d.ts.map +1 -0
- package/dist/lib/core/evaluation/grading.js +44 -0
- package/dist/lib/core/evaluation/grading.js.map +1 -0
- package/dist/lib/core/evaluation/kleene.d.ts +12 -0
- package/dist/lib/core/evaluation/kleene.d.ts.map +1 -0
- package/dist/lib/core/evaluation/kleene.js +29 -0
- package/dist/lib/core/evaluation/kleene.js.map +1 -0
- package/dist/lib/core/evaluation/validation.d.ts +10 -0
- package/dist/lib/core/evaluation/validation.d.ts.map +1 -0
- package/dist/lib/core/evaluation/validation.js +28 -0
- package/dist/lib/core/evaluation/validation.js.map +1 -0
- package/dist/lib/core/expression-manager.d.ts +278 -0
- package/dist/lib/core/expression-manager.d.ts.map +1 -0
- package/dist/lib/core/expression-manager.js +1622 -0
- package/dist/lib/core/expression-manager.js.map +1 -0
- package/dist/lib/core/fork-library.d.ts +26 -0
- package/dist/lib/core/fork-library.d.ts.map +1 -0
- package/dist/lib/core/fork-library.js +71 -0
- package/dist/lib/core/fork-library.js.map +1 -0
- package/dist/lib/core/fork-namespace.d.ts +32 -0
- package/dist/lib/core/fork-namespace.d.ts.map +1 -0
- package/dist/lib/core/fork-namespace.js +99 -0
- package/dist/lib/core/fork-namespace.js.map +1 -0
- package/dist/lib/core/fork.d.ts +30 -0
- package/dist/lib/core/fork.d.ts.map +1 -0
- package/dist/lib/core/fork.js +125 -0
- package/dist/lib/core/fork.js.map +1 -0
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +366 -0
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -0
- package/dist/lib/core/interfaces/argument-engine.interfaces.js +2 -0
- package/dist/lib/core/interfaces/argument-engine.interfaces.js.map +1 -0
- package/dist/lib/core/interfaces/index.d.ts +5 -0
- package/dist/lib/core/interfaces/index.d.ts.map +1 -0
- package/dist/lib/core/interfaces/index.js +2 -0
- package/dist/lib/core/interfaces/index.js.map +1 -0
- package/dist/lib/core/interfaces/library.interfaces.d.ts +347 -0
- package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -0
- package/dist/lib/core/interfaces/library.interfaces.js +2 -0
- package/dist/lib/core/interfaces/library.interfaces.js.map +1 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +401 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.js +2 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.js.map +1 -0
- package/dist/lib/core/interfaces/shared.interfaces.d.ts +28 -0
- package/dist/lib/core/interfaces/shared.interfaces.d.ts.map +1 -0
- package/dist/lib/core/interfaces/shared.interfaces.js +2 -0
- package/dist/lib/core/interfaces/shared.interfaces.js.map +1 -0
- package/dist/lib/core/invariant-violation-error.d.ts +10 -0
- package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
- package/dist/lib/core/invariant-violation-error.js +16 -0
- package/dist/lib/core/invariant-violation-error.js.map +1 -0
- package/dist/lib/core/parser/formula-gen.js +923 -0
- package/dist/lib/core/parser/formula.d.ts +24 -0
- package/dist/lib/core/parser/formula.d.ts.map +1 -0
- package/dist/lib/core/parser/formula.js +8 -0
- package/dist/lib/core/parser/formula.js.map +1 -0
- package/dist/lib/core/premise-engine.d.ts +122 -0
- package/dist/lib/core/premise-engine.d.ts.map +1 -0
- package/dist/lib/core/premise-engine.js +1362 -0
- package/dist/lib/core/premise-engine.js.map +1 -0
- package/dist/lib/core/proposit-core.d.ts +111 -0
- package/dist/lib/core/proposit-core.d.ts.map +1 -0
- package/dist/lib/core/proposit-core.js +365 -0
- package/dist/lib/core/proposit-core.js.map +1 -0
- package/dist/lib/core/relationships.d.ts +15 -0
- package/dist/lib/core/relationships.d.ts.map +1 -0
- package/dist/lib/core/relationships.js +319 -0
- package/dist/lib/core/relationships.js.map +1 -0
- package/dist/lib/core/source-library.d.ts +23 -0
- package/dist/lib/core/source-library.d.ts.map +1 -0
- package/dist/lib/core/source-library.js +21 -0
- package/dist/lib/core/source-library.js.map +1 -0
- package/dist/lib/core/variable-manager.d.ts +68 -0
- package/dist/lib/core/variable-manager.d.ts.map +1 -0
- package/dist/lib/core/variable-manager.js +200 -0
- package/dist/lib/core/variable-manager.js.map +1 -0
- package/dist/lib/core/versioned-library.d.ts +52 -0
- package/dist/lib/core/versioned-library.d.ts.map +1 -0
- package/dist/lib/core/versioned-library.js +192 -0
- package/dist/lib/core/versioned-library.js.map +1 -0
- package/dist/lib/index.d.ts +54 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +39 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/parsing/argument-parser.d.ts +49 -0
- package/dist/lib/parsing/argument-parser.d.ts.map +1 -0
- package/dist/lib/parsing/argument-parser.js +403 -0
- package/dist/lib/parsing/argument-parser.js.map +1 -0
- package/dist/lib/parsing/clamp-max-lengths.d.ts +11 -0
- package/dist/lib/parsing/clamp-max-lengths.d.ts.map +1 -0
- package/dist/lib/parsing/clamp-max-lengths.js +56 -0
- package/dist/lib/parsing/clamp-max-lengths.js.map +1 -0
- package/dist/lib/parsing/index.d.ts +7 -0
- package/dist/lib/parsing/index.d.ts.map +1 -0
- package/dist/lib/parsing/index.js +4 -0
- package/dist/lib/parsing/index.js.map +1 -0
- package/dist/lib/parsing/prompt-builder.d.ts +11 -0
- package/dist/lib/parsing/prompt-builder.d.ts.map +1 -0
- package/dist/lib/parsing/prompt-builder.js +229 -0
- package/dist/lib/parsing/prompt-builder.js.map +1 -0
- package/dist/lib/parsing/schemata.d.ts +79 -0
- package/dist/lib/parsing/schemata.d.ts.map +1 -0
- package/dist/lib/parsing/schemata.js +92 -0
- package/dist/lib/parsing/schemata.js.map +1 -0
- package/dist/lib/parsing/types.d.ts +24 -0
- package/dist/lib/parsing/types.d.ts.map +1 -0
- package/dist/lib/parsing/types.js +2 -0
- package/dist/lib/parsing/types.js.map +1 -0
- package/dist/lib/schemata/analysis.d.ts +9 -0
- package/dist/lib/schemata/analysis.d.ts.map +1 -0
- package/dist/lib/schemata/analysis.js +13 -0
- package/dist/lib/schemata/analysis.js.map +1 -0
- package/dist/lib/schemata/argument.d.ts +14 -0
- package/dist/lib/schemata/argument.d.ts.map +1 -0
- package/dist/lib/schemata/argument.js +24 -0
- package/dist/lib/schemata/argument.js.map +1 -0
- package/dist/lib/schemata/claim.d.ts +9 -0
- package/dist/lib/schemata/claim.d.ts.map +1 -0
- package/dist/lib/schemata/claim.js +18 -0
- package/dist/lib/schemata/claim.js.map +1 -0
- package/dist/lib/schemata/fork.d.ts +76 -0
- package/dist/lib/schemata/fork.d.ts.map +1 -0
- package/dist/lib/schemata/fork.js +55 -0
- package/dist/lib/schemata/fork.js.map +1 -0
- package/dist/lib/schemata/import.d.ts +33 -0
- package/dist/lib/schemata/import.d.ts.map +1 -0
- package/dist/lib/schemata/import.js +18 -0
- package/dist/lib/schemata/import.js.map +1 -0
- package/dist/lib/schemata/index.d.ts +8 -0
- package/dist/lib/schemata/index.d.ts.map +1 -0
- package/dist/lib/schemata/index.js +8 -0
- package/dist/lib/schemata/index.js.map +1 -0
- package/dist/lib/schemata/propositional.d.ts +142 -0
- package/dist/lib/schemata/propositional.d.ts.map +1 -0
- package/dist/lib/schemata/propositional.js +120 -0
- package/dist/lib/schemata/propositional.js.map +1 -0
- package/dist/lib/schemata/shared.d.ts +41 -0
- package/dist/lib/schemata/shared.d.ts.map +1 -0
- package/dist/lib/schemata/shared.js +66 -0
- package/dist/lib/schemata/shared.js.map +1 -0
- package/dist/lib/schemata/source.d.ts +18 -0
- package/dist/lib/schemata/source.d.ts.map +1 -0
- package/dist/lib/schemata/source.js +35 -0
- package/dist/lib/schemata/source.js.map +1 -0
- package/dist/lib/types/checksum.d.ts +20 -0
- package/dist/lib/types/checksum.d.ts.map +1 -0
- package/dist/lib/types/checksum.js +2 -0
- package/dist/lib/types/checksum.js.map +1 -0
- package/dist/lib/types/diff.d.ts +60 -0
- package/dist/lib/types/diff.d.ts.map +1 -0
- package/dist/lib/types/diff.js +2 -0
- package/dist/lib/types/diff.js.map +1 -0
- package/dist/lib/types/evaluation.d.ts +164 -0
- package/dist/lib/types/evaluation.d.ts.map +1 -0
- package/dist/lib/types/evaluation.js +2 -0
- package/dist/lib/types/evaluation.js.map +1 -0
- package/dist/lib/types/fork.d.ts +25 -0
- package/dist/lib/types/fork.d.ts.map +1 -0
- package/dist/lib/types/fork.js +2 -0
- package/dist/lib/types/fork.js.map +1 -0
- package/dist/lib/types/grammar.d.ts +38 -0
- package/dist/lib/types/grammar.d.ts.map +1 -0
- package/dist/lib/types/grammar.js +11 -0
- package/dist/lib/types/grammar.js.map +1 -0
- package/dist/lib/types/mutation.d.ts +31 -0
- package/dist/lib/types/mutation.d.ts.map +1 -0
- package/dist/lib/types/mutation.js +2 -0
- package/dist/lib/types/mutation.js.map +1 -0
- package/dist/lib/types/reactive.d.ts +14 -0
- package/dist/lib/types/reactive.d.ts.map +1 -0
- package/dist/lib/types/reactive.js +2 -0
- package/dist/lib/types/reactive.js.map +1 -0
- package/dist/lib/types/relationships.d.ts +36 -0
- package/dist/lib/types/relationships.d.ts.map +1 -0
- package/dist/lib/types/relationships.js +2 -0
- package/dist/lib/types/relationships.js.map +1 -0
- package/dist/lib/types/validation.d.ts +47 -0
- package/dist/lib/types/validation.d.ts.map +1 -0
- package/dist/lib/types/validation.js +43 -0
- package/dist/lib/types/validation.js.map +1 -0
- package/dist/lib/utils/changeset.d.ts +124 -0
- package/dist/lib/utils/changeset.d.ts.map +1 -0
- package/dist/lib/utils/changeset.js +221 -0
- package/dist/lib/utils/changeset.js.map +1 -0
- package/dist/lib/utils/collections.d.ts +12 -0
- package/dist/lib/utils/collections.d.ts.map +1 -0
- package/dist/lib/utils/collections.js +24 -0
- package/dist/lib/utils/collections.js.map +1 -0
- package/dist/lib/utils/default-map.d.ts +17 -0
- package/dist/lib/utils/default-map.d.ts.map +1 -0
- package/dist/lib/utils/default-map.js +33 -0
- package/dist/lib/utils/default-map.js.map +1 -0
- package/dist/lib/utils/lookup.d.ts +47 -0
- package/dist/lib/utils/lookup.d.ts.map +1 -0
- package/dist/lib/utils/lookup.js +62 -0
- package/dist/lib/utils/lookup.js.map +1 -0
- package/dist/lib/utils/position.d.ts +12 -0
- package/dist/lib/utils/position.d.ts.map +1 -0
- package/dist/lib/utils/position.js +13 -0
- package/dist/lib/utils/position.js.map +1 -0
- package/package.json +82 -0
- package/skills/proposit-core/SKILL.md +35 -0
- package/skills/proposit-core/docs/api-usage.md +442 -0
- package/skills/proposit-core/docs/architecture.md +256 -0
- package/skills/proposit-core/docs/cli.md +304 -0
- package/skills/proposit-core/docs/testing.md +281 -0
- package/skills/proposit-core/docs/types-schemas.md +648 -0
|
@@ -0,0 +1,1622 @@
|
|
|
1
|
+
import { CorePropositionalExpressionSchema } from "../schemata/index.js";
|
|
2
|
+
import { getOrCreate } from "../utils/collections.js";
|
|
3
|
+
import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
|
|
4
|
+
import { defaultGenerateId } from "./argument-engine.js";
|
|
5
|
+
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
6
|
+
import { entityChecksum, computeHash, canonicalSerialize } from "./checksum.js";
|
|
7
|
+
import { DEFAULT_GRAMMAR_CONFIG, } from "../types/grammar.js";
|
|
8
|
+
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, EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED, EXPR_CHILD_LIMIT_EXCEEDED, EXPR_POSITION_DUPLICATE, EXPR_CHECKSUM_MISMATCH, } from "../types/validation.js";
|
|
10
|
+
const PERMITTED_OPERATOR_SWAPS = {
|
|
11
|
+
and: "or",
|
|
12
|
+
or: "and",
|
|
13
|
+
implies: "iff",
|
|
14
|
+
iff: "implies",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Low-level manager for a flat-stored expression tree.
|
|
18
|
+
*
|
|
19
|
+
* Expressions are immutable value objects stored in three maps: the main
|
|
20
|
+
* expression store, a parent-to-children ID index, and a parent-to-positions
|
|
21
|
+
* index. Structural invariants (child limits, root-only operators, position
|
|
22
|
+
* uniqueness) are enforced on every mutation.
|
|
23
|
+
*
|
|
24
|
+
* This class is an internal building block used by {@link PremiseEngine}
|
|
25
|
+
* and is not part of the public API.
|
|
26
|
+
*/
|
|
27
|
+
export class ExpressionManager {
|
|
28
|
+
expressions;
|
|
29
|
+
childExpressionIdsByParentId;
|
|
30
|
+
childPositionsByParentId;
|
|
31
|
+
positionConfig;
|
|
32
|
+
config;
|
|
33
|
+
generateId;
|
|
34
|
+
collector = null;
|
|
35
|
+
dirtyExpressionIds = new Set();
|
|
36
|
+
setCollector(collector) {
|
|
37
|
+
this.collector = collector;
|
|
38
|
+
}
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.expressions = new Map();
|
|
41
|
+
this.childExpressionIdsByParentId = new Map();
|
|
42
|
+
this.childPositionsByParentId = new Map();
|
|
43
|
+
this.positionConfig = config?.positionConfig ?? DEFAULT_POSITION_CONFIG;
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.generateId = config?.generateId ?? defaultGenerateId;
|
|
46
|
+
}
|
|
47
|
+
get grammarConfig() {
|
|
48
|
+
return this.config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
|
|
49
|
+
}
|
|
50
|
+
attachChecksum(expr) {
|
|
51
|
+
const fields = this.config?.checksumConfig?.expressionFields ??
|
|
52
|
+
DEFAULT_CHECKSUM_CONFIG.expressionFields;
|
|
53
|
+
const checksum = entityChecksum(expr, fields);
|
|
54
|
+
return {
|
|
55
|
+
...expr,
|
|
56
|
+
checksum,
|
|
57
|
+
descendantChecksum: null,
|
|
58
|
+
combinedChecksum: checksum,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates and registers a formula-buffer expression in the three internal
|
|
63
|
+
* maps (`expressions`, `childExpressionIdsByParentId`,
|
|
64
|
+
* `childPositionsByParentId`) and notifies the change collector.
|
|
65
|
+
*
|
|
66
|
+
* Used by `addExpression`, `insertExpression`, and `wrapExpression` to
|
|
67
|
+
* auto-insert formula nodes between operators when
|
|
68
|
+
* `grammarConfig.autoNormalize` is enabled.
|
|
69
|
+
*
|
|
70
|
+
* @returns The generated formula expression ID.
|
|
71
|
+
*/
|
|
72
|
+
registerFormulaBuffer(sourceExpr, parentId, position, formulaId) {
|
|
73
|
+
formulaId ??= this.generateId();
|
|
74
|
+
const formulaExpr = this.attachChecksum({
|
|
75
|
+
id: formulaId,
|
|
76
|
+
type: "formula",
|
|
77
|
+
argumentId: sourceExpr.argumentId,
|
|
78
|
+
argumentVersion: sourceExpr.argumentVersion,
|
|
79
|
+
premiseId: sourceExpr
|
|
80
|
+
.premiseId,
|
|
81
|
+
parentId,
|
|
82
|
+
position,
|
|
83
|
+
});
|
|
84
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
85
|
+
this.collector?.addedExpression({
|
|
86
|
+
...formulaExpr,
|
|
87
|
+
});
|
|
88
|
+
getOrCreate(this.childExpressionIdsByParentId, parentId, () => new Set()).add(formulaId);
|
|
89
|
+
getOrCreate(this.childPositionsByParentId, parentId, () => new Set()).add(position);
|
|
90
|
+
return formulaId;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Removes an expression from the three internal maps: deletes it from
|
|
94
|
+
* the main `expressions` store, removes it from its parent's child-id
|
|
95
|
+
* and position indexes, and deletes its own child-id and position
|
|
96
|
+
* indexes.
|
|
97
|
+
*
|
|
98
|
+
* Callers remain responsible for collector notification, dirty-set
|
|
99
|
+
* cleanup, and parent dirtying — timing for those varies by call site.
|
|
100
|
+
*/
|
|
101
|
+
detachExpression(expressionId, expression) {
|
|
102
|
+
this.expressions.delete(expressionId);
|
|
103
|
+
this.childExpressionIdsByParentId
|
|
104
|
+
.get(expression.parentId)
|
|
105
|
+
?.delete(expressionId);
|
|
106
|
+
this.childPositionsByParentId
|
|
107
|
+
.get(expression.parentId)
|
|
108
|
+
?.delete(expression.position);
|
|
109
|
+
this.childExpressionIdsByParentId.delete(expressionId);
|
|
110
|
+
this.childPositionsByParentId.delete(expressionId);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Marks an expression and all its ancestors as dirty for hierarchical
|
|
114
|
+
* checksum recomputation. Stops early when it reaches an expression
|
|
115
|
+
* already in the dirty set (since its ancestors are already marked).
|
|
116
|
+
*/
|
|
117
|
+
markExpressionDirty(exprId) {
|
|
118
|
+
let current = exprId;
|
|
119
|
+
while (current !== null) {
|
|
120
|
+
if (this.dirtyExpressionIds.has(current))
|
|
121
|
+
break; // ancestors already dirty
|
|
122
|
+
this.dirtyExpressionIds.add(current);
|
|
123
|
+
const expr = this.expressions.get(current);
|
|
124
|
+
current = expr ? expr.parentId : null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Recomputes `descendantChecksum` and `combinedChecksum` for all dirty
|
|
129
|
+
* expressions, processing bottom-up (deepest first) so that children
|
|
130
|
+
* are up-to-date before their parents are computed.
|
|
131
|
+
*/
|
|
132
|
+
flushExpressionChecksums() {
|
|
133
|
+
if (this.dirtyExpressionIds.size === 0)
|
|
134
|
+
return;
|
|
135
|
+
// Sort dirty expressions by depth (deepest first) for bottom-up processing
|
|
136
|
+
const dirtyIds = [...this.dirtyExpressionIds];
|
|
137
|
+
const depthOf = (id) => {
|
|
138
|
+
let depth = 0;
|
|
139
|
+
let current = this.expressions.get(id);
|
|
140
|
+
while (current && current.parentId !== null) {
|
|
141
|
+
depth++;
|
|
142
|
+
current = this.expressions.get(current.parentId);
|
|
143
|
+
}
|
|
144
|
+
return depth;
|
|
145
|
+
};
|
|
146
|
+
dirtyIds.sort((a, b) => depthOf(b) - depthOf(a));
|
|
147
|
+
const fields = this.config?.checksumConfig?.expressionFields ??
|
|
148
|
+
DEFAULT_CHECKSUM_CONFIG.expressionFields;
|
|
149
|
+
for (const id of dirtyIds) {
|
|
150
|
+
const expr = this.expressions.get(id);
|
|
151
|
+
if (!expr)
|
|
152
|
+
continue;
|
|
153
|
+
const oldChecksum = expr.checksum;
|
|
154
|
+
const oldDescendantChecksum = expr.descendantChecksum;
|
|
155
|
+
const oldCombinedChecksum = expr.combinedChecksum;
|
|
156
|
+
const metaChecksum = entityChecksum(expr, fields);
|
|
157
|
+
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
158
|
+
let descendantChecksum = null;
|
|
159
|
+
if (childIds && childIds.size > 0) {
|
|
160
|
+
const childMap = {};
|
|
161
|
+
for (const childId of childIds) {
|
|
162
|
+
const child = this.expressions.get(childId);
|
|
163
|
+
if (child) {
|
|
164
|
+
childMap[childId] = child.combinedChecksum;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
descendantChecksum = computeHash(canonicalSerialize(childMap));
|
|
168
|
+
}
|
|
169
|
+
const combinedChecksum = descendantChecksum === null
|
|
170
|
+
? metaChecksum
|
|
171
|
+
: computeHash(metaChecksum + descendantChecksum);
|
|
172
|
+
this.expressions.set(id, {
|
|
173
|
+
...expr,
|
|
174
|
+
checksum: metaChecksum,
|
|
175
|
+
descendantChecksum,
|
|
176
|
+
combinedChecksum,
|
|
177
|
+
});
|
|
178
|
+
if (this.collector &&
|
|
179
|
+
!this.collector.isExpressionAdded(expr.id) &&
|
|
180
|
+
(metaChecksum !== oldChecksum ||
|
|
181
|
+
descendantChecksum !== oldDescendantChecksum ||
|
|
182
|
+
combinedChecksum !== oldCombinedChecksum)) {
|
|
183
|
+
this.collector.modifiedExpression({
|
|
184
|
+
...expr,
|
|
185
|
+
checksum: metaChecksum,
|
|
186
|
+
descendantChecksum,
|
|
187
|
+
combinedChecksum,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
this.dirtyExpressionIds.clear();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Removes deleted expression IDs from the dirty set so that flush
|
|
195
|
+
* doesn't attempt to process expressions that no longer exist.
|
|
196
|
+
*/
|
|
197
|
+
pruneDeletedFromDirtySet(deletedIds) {
|
|
198
|
+
for (const id of deletedIds) {
|
|
199
|
+
this.dirtyExpressionIds.delete(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Returns all expressions sorted by ID for deterministic output. */
|
|
203
|
+
toArray() {
|
|
204
|
+
return Array.from(this.expressions.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Adds an expression to the tree.
|
|
208
|
+
*
|
|
209
|
+
* @throws If the expression ID already exists.
|
|
210
|
+
* @throws If the expression references itself as parent.
|
|
211
|
+
* @throws If `implies`/`iff` operators have a non-null parentId (they must be roots).
|
|
212
|
+
* @throws If the parent does not exist or is not an operator/formula.
|
|
213
|
+
* @throws If the parent's child limit would be exceeded.
|
|
214
|
+
* @throws If the position is already occupied under the parent.
|
|
215
|
+
*/
|
|
216
|
+
addExpression(input) {
|
|
217
|
+
let expression = input;
|
|
218
|
+
if (this.expressions.has(expression.id)) {
|
|
219
|
+
throw new Error(`Expression with ID "${expression.id}" already exists.`);
|
|
220
|
+
}
|
|
221
|
+
if (expression.parentId === expression.id) {
|
|
222
|
+
throw new Error(`Expression "${expression.id}" cannot be its own parent.`);
|
|
223
|
+
}
|
|
224
|
+
if (expression.type === "operator" &&
|
|
225
|
+
(expression.operator === "implies" ||
|
|
226
|
+
expression.operator === "iff") &&
|
|
227
|
+
expression.parentId !== null) {
|
|
228
|
+
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
229
|
+
}
|
|
230
|
+
if (expression.parentId !== null) {
|
|
231
|
+
let parent = this.expressions.get(expression.parentId);
|
|
232
|
+
if (!parent) {
|
|
233
|
+
throw new Error(`Parent expression "${expression.parentId}" does not exist.`);
|
|
234
|
+
}
|
|
235
|
+
if (parent.type !== "operator" && parent.type !== "formula") {
|
|
236
|
+
throw new Error(`Parent expression "${expression.parentId}" is not an operator expression.`);
|
|
237
|
+
}
|
|
238
|
+
// Non-not operators cannot be direct children of operators.
|
|
239
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators &&
|
|
240
|
+
parent.type === "operator" &&
|
|
241
|
+
expression.type === "operator" &&
|
|
242
|
+
expression.operator !== "not") {
|
|
243
|
+
if (this.grammarConfig.autoNormalize) {
|
|
244
|
+
// Check original parent can accept the formula as a new child.
|
|
245
|
+
this.assertChildLimit(parent.operator, expression.parentId);
|
|
246
|
+
// Auto-insert a formula buffer between parent and expression.
|
|
247
|
+
const formulaId = this.registerFormulaBuffer(expression, expression.parentId, expression.position);
|
|
248
|
+
// Rewrite expression to be child of formula.
|
|
249
|
+
expression = {
|
|
250
|
+
...expression,
|
|
251
|
+
parentId: formulaId,
|
|
252
|
+
position: 0,
|
|
253
|
+
};
|
|
254
|
+
// Update parent reference for subsequent checks.
|
|
255
|
+
parent = this.expressions.get(formulaId);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (parent.type === "operator") {
|
|
262
|
+
this.assertChildLimit(parent.operator, expression.parentId);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const childCount = this.childExpressionIdsByParentId.get(expression.parentId)
|
|
266
|
+
?.size ?? 0;
|
|
267
|
+
if (childCount >= 1) {
|
|
268
|
+
throw new Error(`Formula expression "${expression.parentId}" can only have one child.`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const occupiedPositions = getOrCreate(this.childPositionsByParentId, expression.parentId, () => new Set());
|
|
273
|
+
if (occupiedPositions.has(expression.position)) {
|
|
274
|
+
throw new Error(`Position ${expression.position} is already used under parent "${expression.parentId}".`);
|
|
275
|
+
}
|
|
276
|
+
occupiedPositions.add(expression.position);
|
|
277
|
+
const withChecksum = this.attachChecksum(expression);
|
|
278
|
+
this.expressions.set(expression.id, withChecksum);
|
|
279
|
+
this.collector?.addedExpression({
|
|
280
|
+
...withChecksum,
|
|
281
|
+
});
|
|
282
|
+
getOrCreate(this.childExpressionIdsByParentId, expression.parentId, () => new Set()).add(expression.id);
|
|
283
|
+
// Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
284
|
+
this.markExpressionDirty(expression.id);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Adds an expression as the last child of the given parent.
|
|
288
|
+
*
|
|
289
|
+
* If the parent has no children, the expression gets `POSITION_INITIAL`.
|
|
290
|
+
* Otherwise it gets a midpoint between the last child's position and
|
|
291
|
+
* `POSITION_MAX`.
|
|
292
|
+
*/
|
|
293
|
+
appendExpression(parentId, expression) {
|
|
294
|
+
const children = this.getChildExpressions(parentId);
|
|
295
|
+
const position = children.length === 0
|
|
296
|
+
? this.positionConfig.initial
|
|
297
|
+
: midpoint(children[children.length - 1].position, this.positionConfig.max);
|
|
298
|
+
this.addExpression({
|
|
299
|
+
...expression,
|
|
300
|
+
parentId,
|
|
301
|
+
position,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Adds an expression immediately before or after an existing sibling.
|
|
306
|
+
*
|
|
307
|
+
* @throws If the sibling does not exist.
|
|
308
|
+
*/
|
|
309
|
+
addExpressionRelative(siblingId, relativePosition, expression) {
|
|
310
|
+
const sibling = this.expressions.get(siblingId);
|
|
311
|
+
if (!sibling) {
|
|
312
|
+
throw new Error(`Expression "${siblingId}" not found.`);
|
|
313
|
+
}
|
|
314
|
+
const children = this.getChildExpressions(sibling.parentId);
|
|
315
|
+
const siblingIndex = children.findIndex((c) => c.id === siblingId);
|
|
316
|
+
let position;
|
|
317
|
+
if (relativePosition === "before") {
|
|
318
|
+
const prevPosition = siblingIndex > 0
|
|
319
|
+
? children[siblingIndex - 1].position
|
|
320
|
+
: this.positionConfig.min;
|
|
321
|
+
position = midpoint(prevPosition, sibling.position);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const nextPosition = siblingIndex < children.length - 1
|
|
325
|
+
? children[siblingIndex + 1].position
|
|
326
|
+
: this.positionConfig.max;
|
|
327
|
+
position = midpoint(sibling.position, nextPosition);
|
|
328
|
+
}
|
|
329
|
+
this.addExpression({
|
|
330
|
+
...expression,
|
|
331
|
+
parentId: sibling.parentId,
|
|
332
|
+
position,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Updates mutable fields of an existing expression in-place.
|
|
337
|
+
*
|
|
338
|
+
* Only `position`, `variableId`, and `operator` may be updated. Structural
|
|
339
|
+
* fields (`id`, `parentId`, `type`, `argumentId`, `argumentVersion`,
|
|
340
|
+
* `checksum`) are forbidden.
|
|
341
|
+
*
|
|
342
|
+
* Operator changes are restricted to permitted swaps: `and`/`or` and
|
|
343
|
+
* `implies`/`iff`. Variable ID changes require the expression to be of
|
|
344
|
+
* type `"variable"`.
|
|
345
|
+
*
|
|
346
|
+
* @throws If the expression does not exist.
|
|
347
|
+
* @throws If a forbidden field is present in `updates`.
|
|
348
|
+
* @throws If an operator change is not permitted.
|
|
349
|
+
* @throws If `variableId` is set on a non-variable expression.
|
|
350
|
+
* @throws If the new position collides with a sibling.
|
|
351
|
+
*/
|
|
352
|
+
updateExpression(expressionId, updates) {
|
|
353
|
+
const expression = this.expressions.get(expressionId);
|
|
354
|
+
if (!expression) {
|
|
355
|
+
throw new Error(`Expression "${expressionId}" not found.`);
|
|
356
|
+
}
|
|
357
|
+
// Reject forbidden fields passed via `as any`.
|
|
358
|
+
const FORBIDDEN_KEYS = [
|
|
359
|
+
"id",
|
|
360
|
+
"argumentId",
|
|
361
|
+
"argumentVersion",
|
|
362
|
+
"premiseId",
|
|
363
|
+
"checksum",
|
|
364
|
+
"parentId",
|
|
365
|
+
"type",
|
|
366
|
+
];
|
|
367
|
+
for (const key of FORBIDDEN_KEYS) {
|
|
368
|
+
if (key in updates) {
|
|
369
|
+
throw new Error(`Field "${key}" is forbidden in expression updates.`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// If no actual mutable fields are set, return the expression as-is.
|
|
373
|
+
if (updates.position === undefined &&
|
|
374
|
+
updates.variableId === undefined &&
|
|
375
|
+
updates.operator === undefined) {
|
|
376
|
+
return expression;
|
|
377
|
+
}
|
|
378
|
+
// Validate operator change.
|
|
379
|
+
if (updates.operator !== undefined) {
|
|
380
|
+
if (expression.type !== "operator") {
|
|
381
|
+
throw new Error(`Expression "${expressionId}" is not an operator expression; cannot update operator.`);
|
|
382
|
+
}
|
|
383
|
+
const permitted = PERMITTED_OPERATOR_SWAPS[expression.operator];
|
|
384
|
+
if (permitted !== updates.operator) {
|
|
385
|
+
throw new Error(`Changing operator from "${expression.operator}" to "${updates.operator}" is not a permitted operator change. Permitted: and↔or, implies↔iff.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Validate variableId change.
|
|
389
|
+
if (updates.variableId !== undefined) {
|
|
390
|
+
if (expression.type !== "variable") {
|
|
391
|
+
throw new Error(`Expression "${expressionId}" is not a variable expression; cannot update variableId.`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Validate position change.
|
|
395
|
+
if (updates.position !== undefined) {
|
|
396
|
+
const positionSet = this.childPositionsByParentId.get(expression.parentId);
|
|
397
|
+
if (positionSet) {
|
|
398
|
+
positionSet.delete(expression.position);
|
|
399
|
+
if (positionSet.has(updates.position)) {
|
|
400
|
+
// Restore old position before throwing.
|
|
401
|
+
positionSet.add(expression.position);
|
|
402
|
+
throw new Error(`Position ${updates.position} is already used under parent "${expression.parentId}".`);
|
|
403
|
+
}
|
|
404
|
+
positionSet.add(updates.position);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Build an updated copy and replace in the map.
|
|
408
|
+
const updated = this.attachChecksum({
|
|
409
|
+
...expression,
|
|
410
|
+
...(updates.position !== undefined
|
|
411
|
+
? { position: updates.position }
|
|
412
|
+
: {}),
|
|
413
|
+
...(updates.variableId !== undefined
|
|
414
|
+
? { variableId: updates.variableId }
|
|
415
|
+
: {}),
|
|
416
|
+
...(updates.operator !== undefined
|
|
417
|
+
? { operator: updates.operator }
|
|
418
|
+
: {}),
|
|
419
|
+
});
|
|
420
|
+
this.expressions.set(expressionId, updated);
|
|
421
|
+
this.collector?.modifiedExpression({
|
|
422
|
+
...updated,
|
|
423
|
+
});
|
|
424
|
+
// Mark the updated expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
425
|
+
this.markExpressionDirty(expressionId);
|
|
426
|
+
return updated;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Removes an expression from the tree.
|
|
430
|
+
*
|
|
431
|
+
* When `deleteSubtree` is `true`, the expression and its entire descendant
|
|
432
|
+
* subtree are removed, then {@link collapseIfNeeded} runs on the parent.
|
|
433
|
+
*
|
|
434
|
+
* When `deleteSubtree` is `false`, the expression is removed but its single
|
|
435
|
+
* child (if any) is promoted into the removed expression's slot. If the
|
|
436
|
+
* expression has more than one child, an error is thrown. Leaf removal
|
|
437
|
+
* (0 children) still triggers {@link collapseIfNeeded} on the parent.
|
|
438
|
+
* Promotion does **not** trigger collapse.
|
|
439
|
+
*
|
|
440
|
+
* @throws If `deleteSubtree` is `false` and the expression has multiple children.
|
|
441
|
+
* @throws If `deleteSubtree` is `false` and the single child is a root-only
|
|
442
|
+
* operator (`implies`/`iff`) that would be placed in a non-root position.
|
|
443
|
+
* @returns The removed expression, or `undefined` if not found.
|
|
444
|
+
*/
|
|
445
|
+
removeExpression(expressionId, deleteSubtree) {
|
|
446
|
+
const target = this.expressions.get(expressionId);
|
|
447
|
+
if (!target) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
// Pre-flight: simulate collapse chain to detect nesting/root-only violations.
|
|
451
|
+
this.assertRemovalSafe(expressionId, deleteSubtree);
|
|
452
|
+
if (deleteSubtree) {
|
|
453
|
+
return this.removeSubtree(expressionId, target);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
return this.removeAndPromote(expressionId, target);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
removeSubtree(expressionId, target) {
|
|
460
|
+
const parentId = target.parentId;
|
|
461
|
+
const toRemove = new Set();
|
|
462
|
+
const stack = [expressionId];
|
|
463
|
+
while (stack.length > 0) {
|
|
464
|
+
const currentId = stack.pop();
|
|
465
|
+
if (!currentId || toRemove.has(currentId)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
toRemove.add(currentId);
|
|
469
|
+
const children = this.childExpressionIdsByParentId.get(currentId);
|
|
470
|
+
if (!children) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
for (const childId of children) {
|
|
474
|
+
stack.push(childId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const id of toRemove) {
|
|
478
|
+
const expression = this.expressions.get(id);
|
|
479
|
+
if (!expression) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
this.collector?.removedExpression({
|
|
483
|
+
...expression,
|
|
484
|
+
});
|
|
485
|
+
this.detachExpression(id, expression);
|
|
486
|
+
}
|
|
487
|
+
// Prune deleted expressions from the dirty set and mark the surviving parent dirty.
|
|
488
|
+
this.pruneDeletedFromDirtySet(toRemove);
|
|
489
|
+
if (parentId !== null) {
|
|
490
|
+
this.markExpressionDirty(parentId);
|
|
491
|
+
}
|
|
492
|
+
this.collapseIfNeeded(parentId);
|
|
493
|
+
return target;
|
|
494
|
+
}
|
|
495
|
+
removeAndPromote(expressionId, target) {
|
|
496
|
+
const children = this.getChildExpressions(expressionId);
|
|
497
|
+
if (children.length > 1) {
|
|
498
|
+
throw new Error(`Cannot promote: expression "${expressionId}" has multiple children (${children.length}). Use deleteSubtree: true or remove children first.`);
|
|
499
|
+
}
|
|
500
|
+
if (children.length === 0) {
|
|
501
|
+
// Leaf removal — same as removing a single node, then collapse parent.
|
|
502
|
+
const parentId = target.parentId;
|
|
503
|
+
this.collector?.removedExpression({
|
|
504
|
+
...target,
|
|
505
|
+
});
|
|
506
|
+
this.detachExpression(expressionId, target);
|
|
507
|
+
// Prune deleted expression from dirty set and mark surviving parent dirty.
|
|
508
|
+
this.dirtyExpressionIds.delete(expressionId);
|
|
509
|
+
if (parentId !== null) {
|
|
510
|
+
this.markExpressionDirty(parentId);
|
|
511
|
+
}
|
|
512
|
+
this.collapseIfNeeded(parentId);
|
|
513
|
+
return target;
|
|
514
|
+
}
|
|
515
|
+
// Exactly 1 child — promote it into the target's slot.
|
|
516
|
+
const child = children[0];
|
|
517
|
+
// Validate: non-not operators cannot be promoted into an operator parent.
|
|
518
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
519
|
+
if (child.type === "operator" &&
|
|
520
|
+
child.operator !== "not" &&
|
|
521
|
+
target.parentId !== null) {
|
|
522
|
+
const grandparent = this.expressions.get(target.parentId);
|
|
523
|
+
if (grandparent && grandparent.type === "operator") {
|
|
524
|
+
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Validate: root-only operators cannot be promoted into a non-root position.
|
|
529
|
+
if (child.type === "operator" &&
|
|
530
|
+
(child.operator === "implies" || child.operator === "iff") &&
|
|
531
|
+
target.parentId !== null) {
|
|
532
|
+
throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
|
|
533
|
+
}
|
|
534
|
+
// Promote child into the target's slot.
|
|
535
|
+
const promoted = this.attachChecksum({
|
|
536
|
+
...child,
|
|
537
|
+
parentId: target.parentId,
|
|
538
|
+
position: target.position,
|
|
539
|
+
});
|
|
540
|
+
this.expressions.set(child.id, promoted);
|
|
541
|
+
// Update parent's child-id set: remove target, add promoted child.
|
|
542
|
+
this.childExpressionIdsByParentId
|
|
543
|
+
.get(target.parentId)
|
|
544
|
+
?.delete(expressionId);
|
|
545
|
+
getOrCreate(this.childExpressionIdsByParentId, target.parentId, () => new Set()).add(child.id);
|
|
546
|
+
// The parent's position set is unchanged: target.position was already
|
|
547
|
+
// tracked and continues to be occupied by the promoted child.
|
|
548
|
+
// Clean up target's own tracking entries.
|
|
549
|
+
this.childExpressionIdsByParentId.delete(expressionId);
|
|
550
|
+
this.childPositionsByParentId.delete(expressionId);
|
|
551
|
+
// Notify collector.
|
|
552
|
+
this.collector?.removedExpression({
|
|
553
|
+
...target,
|
|
554
|
+
});
|
|
555
|
+
this.collector?.modifiedExpression({
|
|
556
|
+
...promoted,
|
|
557
|
+
});
|
|
558
|
+
// Remove target from expressions map.
|
|
559
|
+
this.expressions.delete(expressionId);
|
|
560
|
+
// Prune deleted expression from dirty set and mark promoted child dirty
|
|
561
|
+
// (its parentId changed) which also propagates to ancestors.
|
|
562
|
+
this.dirtyExpressionIds.delete(expressionId);
|
|
563
|
+
this.markExpressionDirty(child.id);
|
|
564
|
+
// After promotion, the target's parent may be a formula that now needs collapsing
|
|
565
|
+
// (e.g., if the promoted child has no binary operator in its bounded subtree).
|
|
566
|
+
this.collapseIfNeeded(target.parentId);
|
|
567
|
+
return target;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Promotes `child` into the slot occupied by `parent` and removes `parent`.
|
|
571
|
+
* Used by `collapseIfNeeded` and `normalize()`.
|
|
572
|
+
*/
|
|
573
|
+
promoteChild(parentId, parent, child) {
|
|
574
|
+
const grandparentId = parent.parentId;
|
|
575
|
+
const grandparentPosition = parent.position;
|
|
576
|
+
const promoted = this.attachChecksum({
|
|
577
|
+
...child,
|
|
578
|
+
parentId: grandparentId,
|
|
579
|
+
position: grandparentPosition,
|
|
580
|
+
});
|
|
581
|
+
this.expressions.set(child.id, promoted);
|
|
582
|
+
this.collector?.modifiedExpression({
|
|
583
|
+
...promoted,
|
|
584
|
+
});
|
|
585
|
+
this.childExpressionIdsByParentId.get(grandparentId)?.delete(parentId);
|
|
586
|
+
getOrCreate(this.childExpressionIdsByParentId, grandparentId, () => new Set()).add(child.id);
|
|
587
|
+
this.childExpressionIdsByParentId.delete(parentId);
|
|
588
|
+
this.childPositionsByParentId.delete(parentId);
|
|
589
|
+
this.collector?.removedExpression({
|
|
590
|
+
...parent,
|
|
591
|
+
});
|
|
592
|
+
this.expressions.delete(parentId);
|
|
593
|
+
this.dirtyExpressionIds.delete(parentId);
|
|
594
|
+
this.markExpressionDirty(child.id);
|
|
595
|
+
}
|
|
596
|
+
collapseIfNeeded(operatorId) {
|
|
597
|
+
if (!this.grammarConfig.autoNormalize)
|
|
598
|
+
return;
|
|
599
|
+
if (operatorId === null)
|
|
600
|
+
return;
|
|
601
|
+
const operator = this.expressions.get(operatorId);
|
|
602
|
+
if (!operator)
|
|
603
|
+
return;
|
|
604
|
+
if (operator.type === "formula") {
|
|
605
|
+
const children = this.getChildExpressions(operatorId);
|
|
606
|
+
if (children.length === 0) {
|
|
607
|
+
const grandparentId = operator.parentId;
|
|
608
|
+
this.collector?.removedExpression({
|
|
609
|
+
...operator,
|
|
610
|
+
});
|
|
611
|
+
this.detachExpression(operatorId, operator);
|
|
612
|
+
this.dirtyExpressionIds.delete(operatorId);
|
|
613
|
+
if (grandparentId !== null) {
|
|
614
|
+
this.markExpressionDirty(grandparentId);
|
|
615
|
+
}
|
|
616
|
+
this.collapseIfNeeded(grandparentId);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// 1-child formula: collapse if no binary operator in bounded subtree.
|
|
620
|
+
if (children.length === 1 &&
|
|
621
|
+
!this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
|
|
622
|
+
const grandparentId = operator.parentId;
|
|
623
|
+
this.promoteChild(operatorId, operator, children[0]);
|
|
624
|
+
// Grandparent may also be a formula that now needs collapsing.
|
|
625
|
+
this.collapseIfNeeded(grandparentId);
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (operator.type !== "operator")
|
|
630
|
+
return;
|
|
631
|
+
const children = this.getChildExpressions(operatorId);
|
|
632
|
+
if (children.length === 0) {
|
|
633
|
+
const grandparentId = operator.parentId;
|
|
634
|
+
this.collector?.removedExpression({
|
|
635
|
+
...operator,
|
|
636
|
+
});
|
|
637
|
+
this.detachExpression(operatorId, operator);
|
|
638
|
+
// Prune collapsed operator from dirty set and propagate to grandparent.
|
|
639
|
+
this.dirtyExpressionIds.delete(operatorId);
|
|
640
|
+
if (grandparentId !== null) {
|
|
641
|
+
this.markExpressionDirty(grandparentId);
|
|
642
|
+
}
|
|
643
|
+
this.collapseIfNeeded(grandparentId);
|
|
644
|
+
}
|
|
645
|
+
else if (children.length === 1 && operator.operator === "not") {
|
|
646
|
+
// `not` is unary — 1 child is its valid state; skip collapse.
|
|
647
|
+
// Still recurse to grandparent: a formula wrapping this `not` may
|
|
648
|
+
// now qualify for collapse after a descendant change.
|
|
649
|
+
this.collapseIfNeeded(operator.parentId);
|
|
650
|
+
}
|
|
651
|
+
else if (children.length === 1) {
|
|
652
|
+
const child = children[0];
|
|
653
|
+
const grandparentId = operator.parentId;
|
|
654
|
+
// Defense-in-depth: validate promotion doesn't violate nesting or root-only rules.
|
|
655
|
+
if (child.type === "operator") {
|
|
656
|
+
// Root-only — always enforced
|
|
657
|
+
if ((child.operator === "implies" ||
|
|
658
|
+
child.operator === "iff") &&
|
|
659
|
+
grandparentId !== null) {
|
|
660
|
+
throw new Error(`Cannot promote: child "${child.id}" is a root-only operator ("${child.operator}") and would be placed in a non-root position.`);
|
|
661
|
+
}
|
|
662
|
+
// Nesting — grammar-configurable
|
|
663
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
664
|
+
if (child.operator !== "not" && grandparentId !== null) {
|
|
665
|
+
const grandparent = this.expressions.get(grandparentId);
|
|
666
|
+
if (grandparent && grandparent.type === "operator") {
|
|
667
|
+
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
this.promoteChild(operatorId, operator, child);
|
|
673
|
+
// Grandparent may be a formula that now needs collapsing after the
|
|
674
|
+
// promoted child replaced the operator.
|
|
675
|
+
this.collapseIfNeeded(grandparentId);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Checks whether the subtree rooted at `expressionId` contains a binary
|
|
680
|
+
* operator (`and` or `or`). Traversal stops at formula boundaries — a
|
|
681
|
+
* nested formula owns its own subtree and is not inspected.
|
|
682
|
+
*/
|
|
683
|
+
hasBinaryOperatorInBoundedSubtree(expressionId) {
|
|
684
|
+
const expr = this.expressions.get(expressionId);
|
|
685
|
+
if (!expr)
|
|
686
|
+
return false;
|
|
687
|
+
if (expr.type === "formula")
|
|
688
|
+
return false;
|
|
689
|
+
if (expr.type === "variable")
|
|
690
|
+
return false;
|
|
691
|
+
if (expr.type === "operator" &&
|
|
692
|
+
(expr.operator === "and" || expr.operator === "or")) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const children = this.getChildExpressions(expressionId);
|
|
696
|
+
return children.some((child) => this.hasBinaryOperatorInBoundedSubtree(child.id));
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Performs a full normalization sweep on the expression tree:
|
|
700
|
+
* 1. Collapses operators with 0 or 1 children.
|
|
701
|
+
* 2. Collapses formulas whose bounded subtree has no binary operator.
|
|
702
|
+
* 3. Inserts formula buffers where `enforceFormulaBetweenOperators` requires them.
|
|
703
|
+
* 4. Repeats until stable.
|
|
704
|
+
*
|
|
705
|
+
* Works regardless of the current `autoNormalize` setting — this is an
|
|
706
|
+
* explicit on-demand normalization.
|
|
707
|
+
*/
|
|
708
|
+
normalize() {
|
|
709
|
+
let changed = true;
|
|
710
|
+
while (changed) {
|
|
711
|
+
changed = false;
|
|
712
|
+
// Pass 1: Collapse operators with 0 or 1 children (bottom-up).
|
|
713
|
+
for (const expr of this.toArray()) {
|
|
714
|
+
if (expr.type !== "operator")
|
|
715
|
+
continue;
|
|
716
|
+
if (!this.expressions.has(expr.id))
|
|
717
|
+
continue;
|
|
718
|
+
const children = this.getChildExpressions(expr.id);
|
|
719
|
+
if (children.length === 0) {
|
|
720
|
+
const grandparentId = expr.parentId;
|
|
721
|
+
this.collector?.removedExpression({
|
|
722
|
+
...expr,
|
|
723
|
+
});
|
|
724
|
+
this.detachExpression(expr.id, expr);
|
|
725
|
+
this.dirtyExpressionIds.delete(expr.id);
|
|
726
|
+
if (grandparentId !== null) {
|
|
727
|
+
this.markExpressionDirty(grandparentId);
|
|
728
|
+
}
|
|
729
|
+
changed = true;
|
|
730
|
+
}
|
|
731
|
+
else if (children.length === 1 && expr.operator !== "not") {
|
|
732
|
+
this.promoteChild(expr.id, expr, children[0]);
|
|
733
|
+
changed = true;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Pass 2: Collapse unjustified formulas (bottom-up).
|
|
737
|
+
for (const expr of this.toArray()) {
|
|
738
|
+
if (expr.type !== "formula")
|
|
739
|
+
continue;
|
|
740
|
+
if (!this.expressions.has(expr.id))
|
|
741
|
+
continue;
|
|
742
|
+
const children = this.getChildExpressions(expr.id);
|
|
743
|
+
if (children.length === 0) {
|
|
744
|
+
const grandparentId = expr.parentId;
|
|
745
|
+
this.collector?.removedExpression({
|
|
746
|
+
...expr,
|
|
747
|
+
});
|
|
748
|
+
this.detachExpression(expr.id, expr);
|
|
749
|
+
this.dirtyExpressionIds.delete(expr.id);
|
|
750
|
+
if (grandparentId !== null) {
|
|
751
|
+
this.markExpressionDirty(grandparentId);
|
|
752
|
+
}
|
|
753
|
+
changed = true;
|
|
754
|
+
}
|
|
755
|
+
else if (children.length === 1 &&
|
|
756
|
+
!this.hasBinaryOperatorInBoundedSubtree(children[0].id)) {
|
|
757
|
+
this.promoteChild(expr.id, expr, children[0]);
|
|
758
|
+
changed = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Pass 3: Insert formula buffers for operator-under-operator violations.
|
|
762
|
+
for (const expr of this.toArray()) {
|
|
763
|
+
if (expr.type !== "operator" || expr.operator === "not")
|
|
764
|
+
continue;
|
|
765
|
+
if (!this.expressions.has(expr.id))
|
|
766
|
+
continue;
|
|
767
|
+
if (expr.parentId === null)
|
|
768
|
+
continue;
|
|
769
|
+
const parent = this.expressions.get(expr.parentId);
|
|
770
|
+
if (!parent || parent.type !== "operator")
|
|
771
|
+
continue;
|
|
772
|
+
// Non-not operator is direct child of operator — insert formula buffer.
|
|
773
|
+
const formulaId = this.registerFormulaBuffer(expr, expr.parentId, expr.position);
|
|
774
|
+
// Reparent the operator under the formula.
|
|
775
|
+
this.reparent(expr.id, formulaId, 0);
|
|
776
|
+
changed = true;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/** Returns `true` if any expression in the tree references the given variable ID. */
|
|
781
|
+
hasVariableReference(variableId) {
|
|
782
|
+
for (const expression of this.expressions.values()) {
|
|
783
|
+
if (expression.type === "variable" &&
|
|
784
|
+
expression.variableId === variableId) {
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
/** Returns the expression with the given ID, or `undefined` if not found. */
|
|
791
|
+
getExpression(expressionId) {
|
|
792
|
+
return this.expressions.get(expressionId);
|
|
793
|
+
}
|
|
794
|
+
/** Returns the children of the given parent, sorted by position. */
|
|
795
|
+
getChildExpressions(parentId) {
|
|
796
|
+
const childIds = this.childExpressionIdsByParentId.get(parentId);
|
|
797
|
+
if (!childIds || childIds.size === 0) {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
const children = [];
|
|
801
|
+
for (const childId of childIds) {
|
|
802
|
+
const child = this.expressions.get(childId);
|
|
803
|
+
if (child) {
|
|
804
|
+
children.push(child);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return children.sort((a, b) => a.position - b.position);
|
|
808
|
+
}
|
|
809
|
+
loadInitialExpressions(initialExpressions) {
|
|
810
|
+
if (initialExpressions.length === 0) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const pending = new Map(initialExpressions.map((expression) => [expression.id, expression]));
|
|
814
|
+
let progressed = true;
|
|
815
|
+
while (pending.size > 0 && progressed) {
|
|
816
|
+
progressed = false;
|
|
817
|
+
for (const [id, expression] of Array.from(pending.entries())) {
|
|
818
|
+
if (expression.parentId !== null &&
|
|
819
|
+
!this.expressions.has(expression.parentId)) {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
this.addExpression(expression);
|
|
823
|
+
pending.delete(id);
|
|
824
|
+
progressed = true;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (pending.size > 0) {
|
|
828
|
+
const unresolved = Array.from(pending.keys()).join(", ");
|
|
829
|
+
throw new Error(`Could not resolve parent relationships for expressions: ${unresolved}.`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Simulates the collapse chain that would result from removing an expression.
|
|
834
|
+
* Throws if any promotion would violate nesting or root-only rules.
|
|
835
|
+
*/
|
|
836
|
+
assertRemovalSafe(expressionId, deleteSubtree) {
|
|
837
|
+
const target = this.expressions.get(expressionId);
|
|
838
|
+
if (!target)
|
|
839
|
+
return;
|
|
840
|
+
if (!deleteSubtree) {
|
|
841
|
+
const children = this.getChildExpressions(expressionId);
|
|
842
|
+
// >1 children: removeAndPromote throws before any mutation, no nesting concern.
|
|
843
|
+
if (children.length === 1) {
|
|
844
|
+
this.assertPromotionSafe(children[0], target.parentId);
|
|
845
|
+
// Simulate post-promotion cascade (formula collapse after promotion).
|
|
846
|
+
if (this.grammarConfig.autoNormalize) {
|
|
847
|
+
this.simulatePostPromotionCollapse(target.parentId, children[0]);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (children.length === 0) {
|
|
851
|
+
this.simulateCollapseChain(target.parentId, expressionId);
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
// deleteSubtree: entire subtree removed, then collapse runs on parent.
|
|
856
|
+
this.simulateCollapseChain(target.parentId, expressionId);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Checks whether promoting `child` into a slot with the given `newParentId`
|
|
860
|
+
* would violate the nesting rule or root-only rule.
|
|
861
|
+
*/
|
|
862
|
+
assertPromotionSafe(child, newParentId) {
|
|
863
|
+
if (child.type !== "operator")
|
|
864
|
+
return;
|
|
865
|
+
// Root-only check — always enforced
|
|
866
|
+
if ((child.operator === "implies" || child.operator === "iff") &&
|
|
867
|
+
newParentId !== null) {
|
|
868
|
+
throw new Error(`Cannot remove expression — would promote a root-only operator ("${child.operator}") to a non-root position`);
|
|
869
|
+
}
|
|
870
|
+
// Nesting check — grammar-configurable
|
|
871
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
872
|
+
if (child.operator !== "not" && newParentId !== null) {
|
|
873
|
+
const newParent = this.expressions.get(newParentId);
|
|
874
|
+
if (newParent && newParent.type === "operator") {
|
|
875
|
+
throw new Error(`Cannot remove expression — would promote a non-not operator as a direct child of another operator`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Walks the collapse chain starting from `operatorId` after `removedChildId`
|
|
882
|
+
* is removed. At each level: if 0 remaining children, operator/formula is deleted
|
|
883
|
+
* and chain continues up. If 1 remaining child, check promotion safety.
|
|
884
|
+
*/
|
|
885
|
+
simulateCollapseChain(operatorId, removedChildId) {
|
|
886
|
+
if (!this.grammarConfig.autoNormalize)
|
|
887
|
+
return;
|
|
888
|
+
if (operatorId === null)
|
|
889
|
+
return;
|
|
890
|
+
const operator = this.expressions.get(operatorId);
|
|
891
|
+
if (!operator)
|
|
892
|
+
return;
|
|
893
|
+
if (operator.type !== "operator" && operator.type !== "formula")
|
|
894
|
+
return;
|
|
895
|
+
const children = this.getChildExpressions(operatorId);
|
|
896
|
+
const remainingChildren = children.filter((c) => c.id !== removedChildId);
|
|
897
|
+
if (operator.type === "formula") {
|
|
898
|
+
if (remainingChildren.length === 0) {
|
|
899
|
+
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
900
|
+
}
|
|
901
|
+
else if (remainingChildren.length === 1 &&
|
|
902
|
+
!this.hasBinaryOperatorInBoundedSubtree(remainingChildren[0].id)) {
|
|
903
|
+
// Formula would collapse — child promoted.
|
|
904
|
+
// Formula collapse promotion is always safe (child is variable, not, or formula).
|
|
905
|
+
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// operator.type === "operator"
|
|
910
|
+
if (remainingChildren.length === 0) {
|
|
911
|
+
this.simulateCollapseChain(operator.parentId, operatorId);
|
|
912
|
+
}
|
|
913
|
+
else if (remainingChildren.length === 1) {
|
|
914
|
+
this.assertPromotionSafe(remainingChildren[0], operator.parentId);
|
|
915
|
+
// After promotion, simulate further collapse on grandparent.
|
|
916
|
+
this.simulatePostPromotionCollapse(operator.parentId, remainingChildren[0]);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* After an operator promotion places `promotedChild` into `parentId`'s child set,
|
|
921
|
+
* check whether the parent (if a formula) would itself collapse. Formula collapse
|
|
922
|
+
* promotion is always safe (the child can't be a binary operator or root-only operator),
|
|
923
|
+
* but we need to continue the simulation chain.
|
|
924
|
+
*/
|
|
925
|
+
simulatePostPromotionCollapse(parentId, promotedChild) {
|
|
926
|
+
if (parentId === null)
|
|
927
|
+
return;
|
|
928
|
+
const parent = this.expressions.get(parentId);
|
|
929
|
+
if (!parent)
|
|
930
|
+
return;
|
|
931
|
+
if (parent.type === "formula") {
|
|
932
|
+
if (!this.hasBinaryOperatorInBoundedSubtree(promotedChild.id)) {
|
|
933
|
+
// Formula would collapse. The promotedChild takes formula's slot.
|
|
934
|
+
// This is always safe. Continue simulation from formula's parent.
|
|
935
|
+
this.simulatePostPromotionCollapse(parent.parentId, promotedChild);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// Operator parents: child count unchanged, no further collapse.
|
|
939
|
+
}
|
|
940
|
+
assertChildLimit(operator, parentId) {
|
|
941
|
+
const childCount = this.childExpressionIdsByParentId.get(parentId)?.size ?? 0;
|
|
942
|
+
if (operator === "not" && childCount >= 1) {
|
|
943
|
+
throw new Error(`Operator expression "${parentId}" with "not" can only have one child.`);
|
|
944
|
+
}
|
|
945
|
+
if ((operator === "implies" || operator === "iff") && childCount >= 2) {
|
|
946
|
+
throw new Error(`Operator expression "${parentId}" with "${operator}" can only have two children.`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
reparent(expressionId, newParentId, newPosition) {
|
|
950
|
+
const expression = this.expressions.get(expressionId);
|
|
951
|
+
const oldParentId = expression.parentId;
|
|
952
|
+
// Detach from old parent.
|
|
953
|
+
this.childExpressionIdsByParentId
|
|
954
|
+
.get(expression.parentId)
|
|
955
|
+
?.delete(expressionId);
|
|
956
|
+
this.childPositionsByParentId
|
|
957
|
+
.get(expression.parentId)
|
|
958
|
+
?.delete(expression.position);
|
|
959
|
+
// Replace the stored value (expressions are immutable value objects).
|
|
960
|
+
const updated = this.attachChecksum({
|
|
961
|
+
...expression,
|
|
962
|
+
parentId: newParentId,
|
|
963
|
+
position: newPosition,
|
|
964
|
+
});
|
|
965
|
+
this.expressions.set(expressionId, updated);
|
|
966
|
+
this.collector?.modifiedExpression({
|
|
967
|
+
...updated,
|
|
968
|
+
});
|
|
969
|
+
// Attach to new parent.
|
|
970
|
+
getOrCreate(this.childExpressionIdsByParentId, newParentId, () => new Set()).add(expressionId);
|
|
971
|
+
getOrCreate(this.childPositionsByParentId, newParentId, () => new Set()).add(newPosition);
|
|
972
|
+
// Mark both old and new parent chains dirty for hierarchical checksum recomputation.
|
|
973
|
+
this.markExpressionDirty(expressionId);
|
|
974
|
+
if (oldParentId !== null && oldParentId !== newParentId) {
|
|
975
|
+
this.markExpressionDirty(oldParentId);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Inserts a new expression between existing nodes in the tree.
|
|
980
|
+
*
|
|
981
|
+
* The new expression inherits the tree slot of the anchor node
|
|
982
|
+
* (`leftNodeId ?? rightNodeId`). The anchor and optional second node
|
|
983
|
+
* become children of the new expression at positions 0 and 1.
|
|
984
|
+
*
|
|
985
|
+
* Right node is reparented before left node to handle the case where
|
|
986
|
+
* the right node is a descendant of the left node's subtree.
|
|
987
|
+
*
|
|
988
|
+
* @throws If neither leftNodeId nor rightNodeId is provided.
|
|
989
|
+
* @throws If the expression ID already exists.
|
|
990
|
+
* @throws If leftNodeId and rightNodeId are the same.
|
|
991
|
+
* @throws If either referenced node does not exist.
|
|
992
|
+
* @throws If a unary operator/formula is given two children.
|
|
993
|
+
* @throws If either child is an `implies`/`iff` operator (cannot be subordinated).
|
|
994
|
+
* @throws If an `implies`/`iff` expression would be inserted at a non-root position.
|
|
995
|
+
*/
|
|
996
|
+
insertExpression(expression, leftNodeId, rightNodeId) {
|
|
997
|
+
// 1. At least one child node must be provided.
|
|
998
|
+
if (leftNodeId === undefined && rightNodeId === undefined) {
|
|
999
|
+
throw new Error(`insertExpression requires at least one of leftNodeId or rightNodeId.`);
|
|
1000
|
+
}
|
|
1001
|
+
// 2. The new expression's ID must not already exist.
|
|
1002
|
+
if (this.expressions.has(expression.id)) {
|
|
1003
|
+
throw new Error(`Expression with ID "${expression.id}" already exists.`);
|
|
1004
|
+
}
|
|
1005
|
+
// 3. An expression cannot be its own parent.
|
|
1006
|
+
if (expression.parentId === expression.id) {
|
|
1007
|
+
throw new Error(`Expression "${expression.id}" cannot be its own parent.`);
|
|
1008
|
+
}
|
|
1009
|
+
// 4. Left and right nodes must be distinct.
|
|
1010
|
+
if (leftNodeId !== undefined &&
|
|
1011
|
+
rightNodeId !== undefined &&
|
|
1012
|
+
leftNodeId === rightNodeId) {
|
|
1013
|
+
throw new Error(`leftNodeId and rightNodeId must be different.`);
|
|
1014
|
+
}
|
|
1015
|
+
// 5. The left node must exist if provided.
|
|
1016
|
+
// Cast to base TExpressionInput for validation access — deferred conditional
|
|
1017
|
+
// types (TExpressionInput<TExpr>) cannot be narrowed by TS control flow.
|
|
1018
|
+
const leftNode = leftNodeId !== undefined
|
|
1019
|
+
? this.expressions.get(leftNodeId)
|
|
1020
|
+
: undefined;
|
|
1021
|
+
if (leftNodeId !== undefined && !leftNode) {
|
|
1022
|
+
throw new Error(`Expression "${leftNodeId}" does not exist.`);
|
|
1023
|
+
}
|
|
1024
|
+
// 6. The right node must exist if provided.
|
|
1025
|
+
const rightNode = rightNodeId !== undefined
|
|
1026
|
+
? this.expressions.get(rightNodeId)
|
|
1027
|
+
: undefined;
|
|
1028
|
+
if (rightNodeId !== undefined && !rightNode) {
|
|
1029
|
+
throw new Error(`Expression "${rightNodeId}" does not exist.`);
|
|
1030
|
+
}
|
|
1031
|
+
// 7a. A variable expression cannot have children.
|
|
1032
|
+
if (expression.type === "variable") {
|
|
1033
|
+
throw new Error(`Variable expression "${expression.id}" cannot have children.`);
|
|
1034
|
+
}
|
|
1035
|
+
// 7. The "not" operator is unary and cannot take two children.
|
|
1036
|
+
if (expression.type === "operator" &&
|
|
1037
|
+
expression.operator === "not" &&
|
|
1038
|
+
leftNodeId !== undefined &&
|
|
1039
|
+
rightNodeId !== undefined) {
|
|
1040
|
+
throw new Error(`Operator expression "${expression.id}" with "not" can only have one child.`);
|
|
1041
|
+
}
|
|
1042
|
+
// 7b. A formula expression is also unary and cannot take two children.
|
|
1043
|
+
if (expression.type === "formula" &&
|
|
1044
|
+
leftNodeId !== undefined &&
|
|
1045
|
+
rightNodeId !== undefined) {
|
|
1046
|
+
throw new Error(`Formula expression "${expression.id}" can only have one child.`);
|
|
1047
|
+
}
|
|
1048
|
+
// 8. The left node must not be an implies/iff expression (which must remain a root).
|
|
1049
|
+
if (leftNode?.type === "operator" &&
|
|
1050
|
+
(leftNode.operator === "implies" || leftNode.operator === "iff")) {
|
|
1051
|
+
throw new Error(`Expression "${leftNodeId}" with "${leftNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1052
|
+
}
|
|
1053
|
+
// 9. The right node must not be an implies/iff expression (which must remain a root).
|
|
1054
|
+
if (rightNode?.type === "operator" &&
|
|
1055
|
+
(rightNode.operator === "implies" || rightNode.operator === "iff")) {
|
|
1056
|
+
throw new Error(`Expression "${rightNodeId}" with "${rightNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1057
|
+
}
|
|
1058
|
+
// The anchor is the node whose current tree slot the new expression will inherit.
|
|
1059
|
+
const anchor = (leftNode ?? rightNode);
|
|
1060
|
+
// 10. implies/iff expressions may only be inserted at the root of the tree.
|
|
1061
|
+
if (expression.type === "operator" &&
|
|
1062
|
+
(expression.operator === "implies" ||
|
|
1063
|
+
expression.operator === "iff") &&
|
|
1064
|
+
anchor.parentId !== null) {
|
|
1065
|
+
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
1066
|
+
}
|
|
1067
|
+
// 10a. Non-not operators cannot be direct children of operators.
|
|
1068
|
+
// Track which children need formula buffers (Site 2) for post-reparent insertion.
|
|
1069
|
+
let needsParentFormulaBuffer = false;
|
|
1070
|
+
const childrenNeedingFormulaBuffer = [];
|
|
1071
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
1072
|
+
// Check 1 (Site 1): new expression as child of anchor's parent.
|
|
1073
|
+
if (anchor.parentId !== null &&
|
|
1074
|
+
expression.type === "operator" &&
|
|
1075
|
+
expression.operator !== "not") {
|
|
1076
|
+
const anchorParent = this.expressions.get(anchor.parentId);
|
|
1077
|
+
if (anchorParent && anchorParent.type === "operator") {
|
|
1078
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1079
|
+
needsParentFormulaBuffer = true;
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Check 2 (Site 2): left/right nodes as children of the new expression.
|
|
1087
|
+
if (expression.type === "operator") {
|
|
1088
|
+
if (leftNode?.type === "operator" &&
|
|
1089
|
+
leftNode.operator !== "not") {
|
|
1090
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1091
|
+
childrenNeedingFormulaBuffer.push(leftNodeId);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (rightNode?.type === "operator" &&
|
|
1098
|
+
rightNode.operator !== "not") {
|
|
1099
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1100
|
+
childrenNeedingFormulaBuffer.push(rightNodeId);
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const anchorParentId = anchor.parentId;
|
|
1109
|
+
const anchorPosition = anchor.position;
|
|
1110
|
+
// Reparent rightNode first in case it is a descendant of leftNode.
|
|
1111
|
+
if (rightNodeId !== undefined) {
|
|
1112
|
+
this.reparent(rightNodeId, expression.id, 1);
|
|
1113
|
+
}
|
|
1114
|
+
if (leftNodeId !== undefined) {
|
|
1115
|
+
this.reparent(leftNodeId, expression.id, 0);
|
|
1116
|
+
}
|
|
1117
|
+
// Determine the slot for the new expression. If a parent formula buffer
|
|
1118
|
+
// is needed, the formula takes the anchor slot and the expression goes under it.
|
|
1119
|
+
let finalParentId = anchorParentId;
|
|
1120
|
+
let finalPosition = anchorPosition;
|
|
1121
|
+
if (needsParentFormulaBuffer) {
|
|
1122
|
+
const formulaId = this.registerFormulaBuffer(expression, anchorParentId, anchorPosition);
|
|
1123
|
+
finalParentId = formulaId;
|
|
1124
|
+
finalPosition = 0;
|
|
1125
|
+
}
|
|
1126
|
+
// Store the new expression in its slot.
|
|
1127
|
+
const stored = this.attachChecksum({
|
|
1128
|
+
...expression,
|
|
1129
|
+
parentId: finalParentId,
|
|
1130
|
+
position: finalPosition,
|
|
1131
|
+
});
|
|
1132
|
+
this.expressions.set(expression.id, stored);
|
|
1133
|
+
this.collector?.addedExpression({
|
|
1134
|
+
...stored,
|
|
1135
|
+
});
|
|
1136
|
+
getOrCreate(this.childExpressionIdsByParentId, finalParentId, () => new Set()).add(expression.id);
|
|
1137
|
+
getOrCreate(this.childPositionsByParentId, finalParentId, () => new Set()).add(finalPosition);
|
|
1138
|
+
// Site 2: auto-insert formula buffers between the new expression and
|
|
1139
|
+
// any offending operator children.
|
|
1140
|
+
for (const childId of childrenNeedingFormulaBuffer) {
|
|
1141
|
+
const child = this.expressions.get(childId);
|
|
1142
|
+
const childPosition = child.position;
|
|
1143
|
+
// Reparent the child under the formula first. This detaches the child
|
|
1144
|
+
// from expression.id's tracking (removing its position from the set).
|
|
1145
|
+
// registerFormulaBuffer then occupies the freed position.
|
|
1146
|
+
const formulaId = this.generateId();
|
|
1147
|
+
this.reparent(childId, formulaId, 0);
|
|
1148
|
+
this.registerFormulaBuffer(expression, expression.id, childPosition, formulaId);
|
|
1149
|
+
}
|
|
1150
|
+
// Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
1151
|
+
// Note: reparent() already marks children dirty, so this propagates from the new expression up.
|
|
1152
|
+
this.markExpressionDirty(expression.id);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Wraps an existing expression with a new operator and a new sibling.
|
|
1156
|
+
*
|
|
1157
|
+
* The operator takes the existing node's slot in the tree. Both the
|
|
1158
|
+
* existing node and the new sibling become children of the operator.
|
|
1159
|
+
*
|
|
1160
|
+
* Exactly one of `leftNodeId` / `rightNodeId` must be provided — it
|
|
1161
|
+
* identifies the existing node and which child slot (position 0 or 1)
|
|
1162
|
+
* it occupies. The new sibling fills the other slot.
|
|
1163
|
+
*
|
|
1164
|
+
* @throws If neither or both of leftNodeId/rightNodeId are provided.
|
|
1165
|
+
* @throws If the operator or sibling expression ID already exists.
|
|
1166
|
+
* @throws If operator and sibling IDs are the same.
|
|
1167
|
+
* @throws If the existing node does not exist.
|
|
1168
|
+
* @throws If the operator is not of type `"operator"`.
|
|
1169
|
+
* @throws If the operator is unary (`not`).
|
|
1170
|
+
* @throws If the operator is `implies`/`iff` and the existing node is not at root.
|
|
1171
|
+
* @throws If the existing node is an `implies`/`iff` operator (cannot be subordinated).
|
|
1172
|
+
* @throws If the new sibling is an `implies`/`iff` operator (cannot be subordinated).
|
|
1173
|
+
*/
|
|
1174
|
+
wrapExpression(operator, newSibling, leftNodeId, rightNodeId) {
|
|
1175
|
+
// 1. Exactly one of leftNodeId / rightNodeId must be provided.
|
|
1176
|
+
if (leftNodeId === undefined && rightNodeId === undefined) {
|
|
1177
|
+
throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId.`);
|
|
1178
|
+
}
|
|
1179
|
+
if (leftNodeId !== undefined && rightNodeId !== undefined) {
|
|
1180
|
+
throw new Error(`wrapExpression requires exactly one of leftNodeId or rightNodeId, not both.`);
|
|
1181
|
+
}
|
|
1182
|
+
// 2. Operator expression ID must not already exist.
|
|
1183
|
+
if (this.expressions.has(operator.id)) {
|
|
1184
|
+
throw new Error(`Expression with ID "${operator.id}" already exists.`);
|
|
1185
|
+
}
|
|
1186
|
+
// 3. New sibling expression ID must not already exist.
|
|
1187
|
+
if (this.expressions.has(newSibling.id)) {
|
|
1188
|
+
throw new Error(`Expression with ID "${newSibling.id}" already exists.`);
|
|
1189
|
+
}
|
|
1190
|
+
// 4. Operator and sibling IDs must be different.
|
|
1191
|
+
if (operator.id === newSibling.id) {
|
|
1192
|
+
throw new Error(`Operator and sibling expression IDs must be different.`);
|
|
1193
|
+
}
|
|
1194
|
+
// 5. The existing node must exist.
|
|
1195
|
+
const existingNodeId = (leftNodeId ?? rightNodeId);
|
|
1196
|
+
const existingNode = this.expressions.get(existingNodeId);
|
|
1197
|
+
if (!existingNode) {
|
|
1198
|
+
throw new Error(`Expression "${existingNodeId}" does not exist.`);
|
|
1199
|
+
}
|
|
1200
|
+
// 6. Operator expression must have type "operator".
|
|
1201
|
+
if (operator.type !== "operator") {
|
|
1202
|
+
throw new Error(`Wrap operator expression "${operator.id}" must have type "operator", got "${operator.type}".`);
|
|
1203
|
+
}
|
|
1204
|
+
// 7. Operator must not be unary ("not").
|
|
1205
|
+
if (operator.operator === "not") {
|
|
1206
|
+
throw new Error(`Operator expression "${operator.id}" with "not" cannot wrap (it is unary and wrapping always produces two children).`);
|
|
1207
|
+
}
|
|
1208
|
+
// 8. implies/iff operator only allowed if existing node is at root.
|
|
1209
|
+
if ((operator.operator === "implies" || operator.operator === "iff") &&
|
|
1210
|
+
existingNode.parentId !== null) {
|
|
1211
|
+
throw new Error(`Operator expression "${operator.id}" with "${operator.operator}" must be a root expression (parentId must be null).`);
|
|
1212
|
+
}
|
|
1213
|
+
// 9. Existing node must not be implies/iff (cannot be subordinated).
|
|
1214
|
+
if (existingNode.type === "operator" &&
|
|
1215
|
+
(existingNode.operator === "implies" ||
|
|
1216
|
+
existingNode.operator === "iff")) {
|
|
1217
|
+
throw new Error(`Expression "${existingNodeId}" with "${existingNode.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1218
|
+
}
|
|
1219
|
+
// 10. New sibling must not be implies/iff (cannot be subordinated).
|
|
1220
|
+
if (newSibling.type === "operator" &&
|
|
1221
|
+
(newSibling.operator === "implies" || newSibling.operator === "iff")) {
|
|
1222
|
+
throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1223
|
+
}
|
|
1224
|
+
// 10a. Non-not operators cannot be direct children of operators.
|
|
1225
|
+
// Track which sites need formula buffers for post-mutation insertion.
|
|
1226
|
+
let needsParentFormulaBuffer = false;
|
|
1227
|
+
let existingNodeNeedsFormulaBuffer = false;
|
|
1228
|
+
let siblingNeedsFormulaBuffer = false;
|
|
1229
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
1230
|
+
// Check 1 (Site 1): new operator as child of existing node's parent.
|
|
1231
|
+
// Note: step 7 already rejects `not`, so operator.operator is always non-not here.
|
|
1232
|
+
if (existingNode.parentId !== null) {
|
|
1233
|
+
const existingParent = this.expressions.get(existingNode.parentId);
|
|
1234
|
+
if (existingParent && existingParent.type === "operator") {
|
|
1235
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1236
|
+
needsParentFormulaBuffer = true;
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// Check 2 (Site 2): existing node as child of new operator.
|
|
1244
|
+
if (existingNode.type === "operator" &&
|
|
1245
|
+
existingNode.operator !== "not") {
|
|
1246
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1247
|
+
existingNodeNeedsFormulaBuffer = true;
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// Check 3 (Site 3): new sibling as child of new operator.
|
|
1254
|
+
if (newSibling.type === "operator" &&
|
|
1255
|
+
newSibling.operator !== "not") {
|
|
1256
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1257
|
+
siblingNeedsFormulaBuffer = true;
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// Save the existing node's slot (the operator will inherit it).
|
|
1265
|
+
const anchorParentId = existingNode.parentId;
|
|
1266
|
+
const anchorPosition = existingNode.position;
|
|
1267
|
+
// Determine child positions (midpoint-spaced for future bisection).
|
|
1268
|
+
const existingPosition = leftNodeId !== undefined
|
|
1269
|
+
? this.positionConfig.initial
|
|
1270
|
+
: midpoint(this.positionConfig.initial, this.positionConfig.max);
|
|
1271
|
+
const siblingPosition = leftNodeId !== undefined
|
|
1272
|
+
? midpoint(this.positionConfig.initial, this.positionConfig.max)
|
|
1273
|
+
: this.positionConfig.initial;
|
|
1274
|
+
// Reparent existing node under operator.
|
|
1275
|
+
this.reparent(existingNodeId, operator.id, existingPosition);
|
|
1276
|
+
// Store new sibling under operator.
|
|
1277
|
+
const storedSibling = this.attachChecksum({
|
|
1278
|
+
...newSibling,
|
|
1279
|
+
parentId: operator.id,
|
|
1280
|
+
position: siblingPosition,
|
|
1281
|
+
});
|
|
1282
|
+
this.expressions.set(newSibling.id, storedSibling);
|
|
1283
|
+
this.collector?.addedExpression({
|
|
1284
|
+
...storedSibling,
|
|
1285
|
+
});
|
|
1286
|
+
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
|
|
1287
|
+
getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(siblingPosition);
|
|
1288
|
+
// Determine the operator's slot. If a parent formula buffer is needed,
|
|
1289
|
+
// the formula takes the anchor slot and the operator goes under it.
|
|
1290
|
+
let operatorParentId = anchorParentId;
|
|
1291
|
+
let operatorPosition = anchorPosition;
|
|
1292
|
+
if (needsParentFormulaBuffer) {
|
|
1293
|
+
const formulaId = this.registerFormulaBuffer(operator, anchorParentId, anchorPosition);
|
|
1294
|
+
operatorParentId = formulaId;
|
|
1295
|
+
operatorPosition = 0;
|
|
1296
|
+
}
|
|
1297
|
+
// Store operator in its slot.
|
|
1298
|
+
const storedOperator = this.attachChecksum({
|
|
1299
|
+
...operator,
|
|
1300
|
+
parentId: operatorParentId,
|
|
1301
|
+
position: operatorPosition,
|
|
1302
|
+
});
|
|
1303
|
+
this.expressions.set(operator.id, storedOperator);
|
|
1304
|
+
this.collector?.addedExpression({
|
|
1305
|
+
...storedOperator,
|
|
1306
|
+
});
|
|
1307
|
+
getOrCreate(this.childExpressionIdsByParentId, operatorParentId, () => new Set()).add(operator.id);
|
|
1308
|
+
getOrCreate(this.childPositionsByParentId, operatorParentId, () => new Set()).add(operatorPosition);
|
|
1309
|
+
// Site 2: auto-insert formula buffer between operator and existing node.
|
|
1310
|
+
if (existingNodeNeedsFormulaBuffer) {
|
|
1311
|
+
const existingChild = this.expressions.get(existingNodeId);
|
|
1312
|
+
const childPosition = existingChild.position;
|
|
1313
|
+
const formulaId = this.generateId();
|
|
1314
|
+
// Reparent existing node under formula first (frees position in operator's tracking).
|
|
1315
|
+
// registerFormulaBuffer then occupies the freed position.
|
|
1316
|
+
this.reparent(existingNodeId, formulaId, 0);
|
|
1317
|
+
this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
|
|
1318
|
+
}
|
|
1319
|
+
// Site 3: auto-insert formula buffer between operator and new sibling.
|
|
1320
|
+
if (siblingNeedsFormulaBuffer) {
|
|
1321
|
+
const siblingChild = this.expressions.get(newSibling.id);
|
|
1322
|
+
const childPosition = siblingChild.position;
|
|
1323
|
+
const formulaId = this.generateId();
|
|
1324
|
+
// Reparent sibling under formula first (frees position in operator's tracking).
|
|
1325
|
+
// registerFormulaBuffer then occupies the freed position.
|
|
1326
|
+
this.reparent(newSibling.id, formulaId, 0);
|
|
1327
|
+
this.registerFormulaBuffer(operator, operator.id, childPosition, formulaId);
|
|
1328
|
+
}
|
|
1329
|
+
// Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
|
|
1330
|
+
// reparent() already marks the existing node dirty; mark the operator and sibling as well.
|
|
1331
|
+
this.markExpressionDirty(newSibling.id);
|
|
1332
|
+
this.markExpressionDirty(operator.id);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Reparents an expression to a new parent at a given position.
|
|
1336
|
+
*/
|
|
1337
|
+
reparentExpression(expressionId, newParentId, newPosition) {
|
|
1338
|
+
const expression = this.expressions.get(expressionId);
|
|
1339
|
+
if (!expression) {
|
|
1340
|
+
throw new Error(`Expression "${expressionId}" does not exist.`);
|
|
1341
|
+
}
|
|
1342
|
+
this.reparent(expressionId, newParentId, newPosition);
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Deletes a single expression that has no children.
|
|
1346
|
+
* Does NOT trigger operator collapse. Caller must ensure children
|
|
1347
|
+
* have been reparented away first.
|
|
1348
|
+
*/
|
|
1349
|
+
deleteExpression(expressionId) {
|
|
1350
|
+
const expression = this.expressions.get(expressionId);
|
|
1351
|
+
if (!expression)
|
|
1352
|
+
return undefined;
|
|
1353
|
+
const children = this.getChildExpressions(expressionId);
|
|
1354
|
+
if (children.length > 0) {
|
|
1355
|
+
throw new Error(`Cannot delete expression "${expressionId}" — it still has ${children.length} children. Reparent them first.`);
|
|
1356
|
+
}
|
|
1357
|
+
this.detachExpression(expressionId, expression);
|
|
1358
|
+
// Notify collector
|
|
1359
|
+
this.collector?.removedExpression({
|
|
1360
|
+
...expression,
|
|
1361
|
+
});
|
|
1362
|
+
// Clean up dirty set and mark parent dirty
|
|
1363
|
+
this.dirtyExpressionIds.delete(expressionId);
|
|
1364
|
+
if (expression.parentId !== null) {
|
|
1365
|
+
this.markExpressionDirty(expression.parentId);
|
|
1366
|
+
}
|
|
1367
|
+
return expression;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Changes the operator type of an operator expression without the swap
|
|
1371
|
+
* restriction enforced by {@link updateExpression}. Only validates that
|
|
1372
|
+
* the target expression is an operator, the new operator is not `"not"`,
|
|
1373
|
+
* and root-only constraints are satisfied.
|
|
1374
|
+
*/
|
|
1375
|
+
changeOperatorType(expressionId, newOperator) {
|
|
1376
|
+
const expression = this.expressions.get(expressionId);
|
|
1377
|
+
if (!expression) {
|
|
1378
|
+
throw new Error(`Expression "${expressionId}" does not exist.`);
|
|
1379
|
+
}
|
|
1380
|
+
if (expression.type !== "operator") {
|
|
1381
|
+
throw new Error(`Expression "${expressionId}" is not an operator (type: "${expression.type}").`);
|
|
1382
|
+
}
|
|
1383
|
+
if (newOperator === "not") {
|
|
1384
|
+
throw new Error(`Cannot change operator to "not". Use toggleNegation instead.`);
|
|
1385
|
+
}
|
|
1386
|
+
// Root-only: implies/iff must be at root
|
|
1387
|
+
if ((newOperator === "implies" || newOperator === "iff") &&
|
|
1388
|
+
expression.parentId !== null) {
|
|
1389
|
+
throw new Error(`Operator "${newOperator}" must be a root expression (parentId must be null).`);
|
|
1390
|
+
}
|
|
1391
|
+
const updated = this.attachChecksum({
|
|
1392
|
+
...expression,
|
|
1393
|
+
operator: newOperator,
|
|
1394
|
+
});
|
|
1395
|
+
this.expressions.set(expressionId, updated);
|
|
1396
|
+
this.collector?.modifiedExpression({
|
|
1397
|
+
...updated,
|
|
1398
|
+
});
|
|
1399
|
+
this.markExpressionDirty(expressionId);
|
|
1400
|
+
return updated;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Loads expressions in BFS order, respecting the current grammar config.
|
|
1404
|
+
* Used by restoration paths (fromData, rollback) that load existing data.
|
|
1405
|
+
*/
|
|
1406
|
+
loadExpressions(expressions) {
|
|
1407
|
+
this.loadInitialExpressions(expressions);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Performs a comprehensive validation sweep on all managed expressions.
|
|
1411
|
+
*
|
|
1412
|
+
* Collects ALL violations rather than failing on the first one. Checks:
|
|
1413
|
+
* schema validity, duplicate IDs, self-referential parents, parent
|
|
1414
|
+
* existence, parent container type, root-only operators, formula-between-
|
|
1415
|
+
* operators (when enabled), child limits, position uniqueness, and
|
|
1416
|
+
* checksum integrity.
|
|
1417
|
+
*/
|
|
1418
|
+
validate() {
|
|
1419
|
+
const violations = [];
|
|
1420
|
+
const seenIds = new Set();
|
|
1421
|
+
// ── 1. Save pre-flush checksums for later comparison ──
|
|
1422
|
+
const preFlushChecksums = new Map();
|
|
1423
|
+
for (const [id, expr] of this.expressions) {
|
|
1424
|
+
if (expr.checksum != null) {
|
|
1425
|
+
preFlushChecksums.set(id, {
|
|
1426
|
+
checksum: expr.checksum,
|
|
1427
|
+
descendantChecksum: expr.descendantChecksum,
|
|
1428
|
+
combinedChecksum: expr.combinedChecksum,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
// ── 2. Flush checksums to get fresh values ──
|
|
1433
|
+
// Mark all expressions dirty so flush recomputes everything
|
|
1434
|
+
for (const id of this.expressions.keys()) {
|
|
1435
|
+
this.dirtyExpressionIds.add(id);
|
|
1436
|
+
}
|
|
1437
|
+
this.flushExpressionChecksums();
|
|
1438
|
+
// ── 3. Per-expression checks ──
|
|
1439
|
+
// Build a sibling-position map for position uniqueness checks
|
|
1440
|
+
const positionsByParent = new Map();
|
|
1441
|
+
for (const [id, expr] of this.expressions) {
|
|
1442
|
+
// 3a. Schema check
|
|
1443
|
+
if (!Value.Check(CorePropositionalExpressionSchema, expr)) {
|
|
1444
|
+
violations.push({
|
|
1445
|
+
code: EXPR_SCHEMA_INVALID,
|
|
1446
|
+
message: `Expression "${id}" does not conform to CorePropositionalExpressionSchema.`,
|
|
1447
|
+
entityType: "expression",
|
|
1448
|
+
entityId: id,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
// 3b. Duplicate ID
|
|
1452
|
+
if (seenIds.has(id)) {
|
|
1453
|
+
violations.push({
|
|
1454
|
+
code: EXPR_DUPLICATE_ID,
|
|
1455
|
+
message: `Duplicate expression ID "${id}".`,
|
|
1456
|
+
entityType: "expression",
|
|
1457
|
+
entityId: id,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
seenIds.add(id);
|
|
1461
|
+
// 3c. Self-referential parent
|
|
1462
|
+
if (expr.parentId === id) {
|
|
1463
|
+
violations.push({
|
|
1464
|
+
code: EXPR_SELF_REFERENTIAL_PARENT,
|
|
1465
|
+
message: `Expression "${id}" references itself as parent.`,
|
|
1466
|
+
entityType: "expression",
|
|
1467
|
+
entityId: id,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
// 3d. Parent existence
|
|
1471
|
+
if (expr.parentId !== null &&
|
|
1472
|
+
!this.expressions.has(expr.parentId)) {
|
|
1473
|
+
violations.push({
|
|
1474
|
+
code: EXPR_PARENT_NOT_FOUND,
|
|
1475
|
+
message: `Expression "${id}" references non-existent parent "${expr.parentId}".`,
|
|
1476
|
+
entityType: "expression",
|
|
1477
|
+
entityId: id,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
// 3e. Parent is container (operator or formula)
|
|
1481
|
+
if (expr.parentId !== null && this.expressions.has(expr.parentId)) {
|
|
1482
|
+
const parent = this.expressions.get(expr.parentId);
|
|
1483
|
+
if (parent.type !== "operator" && parent.type !== "formula") {
|
|
1484
|
+
violations.push({
|
|
1485
|
+
code: EXPR_PARENT_NOT_CONTAINER,
|
|
1486
|
+
message: `Expression "${id}" has parent "${expr.parentId}" of type "${parent.type}" (expected operator or formula).`,
|
|
1487
|
+
entityType: "expression",
|
|
1488
|
+
entityId: id,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// 3f. Root-only: implies/iff must have parentId === null
|
|
1493
|
+
if (expr.type === "operator" &&
|
|
1494
|
+
(expr.operator === "implies" || expr.operator === "iff") &&
|
|
1495
|
+
expr.parentId !== null) {
|
|
1496
|
+
violations.push({
|
|
1497
|
+
code: EXPR_ROOT_ONLY_VIOLATED,
|
|
1498
|
+
message: `Root-only operator "${expr.operator}" expression "${id}" has non-null parentId "${expr.parentId}".`,
|
|
1499
|
+
entityType: "expression",
|
|
1500
|
+
entityId: id,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
// 3g. Formula-between-operators
|
|
1504
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators &&
|
|
1505
|
+
expr.parentId !== null &&
|
|
1506
|
+
expr.type === "operator" &&
|
|
1507
|
+
expr.operator !== "not") {
|
|
1508
|
+
const parent = this.expressions.get(expr.parentId);
|
|
1509
|
+
if (parent && parent.type === "operator") {
|
|
1510
|
+
violations.push({
|
|
1511
|
+
code: EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED,
|
|
1512
|
+
message: `Non-not operator "${expr.operator}" expression "${id}" is a direct child of operator "${expr.parentId}".`,
|
|
1513
|
+
entityType: "expression",
|
|
1514
|
+
entityId: id,
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
// Collect positions for uniqueness check
|
|
1519
|
+
const parentKey = expr.parentId;
|
|
1520
|
+
let parentPositions = positionsByParent.get(parentKey);
|
|
1521
|
+
if (!parentPositions) {
|
|
1522
|
+
parentPositions = new Map();
|
|
1523
|
+
positionsByParent.set(parentKey, parentPositions);
|
|
1524
|
+
}
|
|
1525
|
+
const idsAtPosition = parentPositions.get(expr.position);
|
|
1526
|
+
if (idsAtPosition) {
|
|
1527
|
+
idsAtPosition.push(id);
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
parentPositions.set(expr.position, [id]);
|
|
1531
|
+
}
|
|
1532
|
+
// 3j. Checksum comparison
|
|
1533
|
+
const pre = preFlushChecksums.get(id);
|
|
1534
|
+
if (pre) {
|
|
1535
|
+
const fresh = this.expressions.get(id);
|
|
1536
|
+
if (pre.checksum !== fresh.checksum ||
|
|
1537
|
+
pre.descendantChecksum !== fresh.descendantChecksum ||
|
|
1538
|
+
pre.combinedChecksum !== fresh.combinedChecksum) {
|
|
1539
|
+
violations.push({
|
|
1540
|
+
code: EXPR_CHECKSUM_MISMATCH,
|
|
1541
|
+
message: `Expression "${id}" checksum mismatch: stored does not match recomputed.`,
|
|
1542
|
+
entityType: "expression",
|
|
1543
|
+
entityId: id,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// ── 4. Child limit checks (not/formula: max 1 child) ──
|
|
1549
|
+
for (const [id, expr] of this.expressions) {
|
|
1550
|
+
if ((expr.type === "operator" && expr.operator === "not") ||
|
|
1551
|
+
expr.type === "formula") {
|
|
1552
|
+
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
1553
|
+
const childCount = childIds?.size ?? 0;
|
|
1554
|
+
if (childCount > 1) {
|
|
1555
|
+
const label = expr.type === "formula" ? "Formula" : `Operator "not"`;
|
|
1556
|
+
violations.push({
|
|
1557
|
+
code: EXPR_CHILD_LIMIT_EXCEEDED,
|
|
1558
|
+
message: `${label} expression "${id}" has ${childCount} children (max 1).`,
|
|
1559
|
+
entityType: "expression",
|
|
1560
|
+
entityId: id,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
// ── 5. Position uniqueness ──
|
|
1566
|
+
for (const [, posMap] of positionsByParent) {
|
|
1567
|
+
for (const [position, ids] of posMap) {
|
|
1568
|
+
if (ids.length > 1) {
|
|
1569
|
+
for (const id of ids) {
|
|
1570
|
+
violations.push({
|
|
1571
|
+
code: EXPR_POSITION_DUPLICATE,
|
|
1572
|
+
message: `Position ${position} is shared by expressions [${ids.join(", ")}].`,
|
|
1573
|
+
entityType: "expression",
|
|
1574
|
+
entityId: id,
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return {
|
|
1581
|
+
ok: violations.length === 0,
|
|
1582
|
+
violations,
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
/** Returns a serializable snapshot of the current state. */
|
|
1586
|
+
snapshot() {
|
|
1587
|
+
return {
|
|
1588
|
+
expressions: this.toArray(),
|
|
1589
|
+
config: this.config
|
|
1590
|
+
? {
|
|
1591
|
+
...this.config,
|
|
1592
|
+
checksumConfig: serializeChecksumConfig(this.config.checksumConfig),
|
|
1593
|
+
}
|
|
1594
|
+
: this.config,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
/** Creates a new ExpressionManager from a previously captured snapshot. */
|
|
1598
|
+
static fromSnapshot(snapshot, grammarConfig, generateId) {
|
|
1599
|
+
// Normalize checksumConfig in case the snapshot went through a JSON
|
|
1600
|
+
// round-trip that converted Sets to arrays or empty objects.
|
|
1601
|
+
const normalizedChecksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
|
|
1602
|
+
const normalizedConfig = snapshot.config
|
|
1603
|
+
? {
|
|
1604
|
+
...snapshot.config,
|
|
1605
|
+
checksumConfig: normalizedChecksumConfig,
|
|
1606
|
+
}
|
|
1607
|
+
: undefined;
|
|
1608
|
+
// During loading: use explicit grammarConfig, falling back to snapshot's config
|
|
1609
|
+
const loadingConfig = {
|
|
1610
|
+
...normalizedConfig,
|
|
1611
|
+
grammarConfig: grammarConfig ?? normalizedConfig?.grammarConfig,
|
|
1612
|
+
generateId: generateId ?? normalizedConfig?.generateId,
|
|
1613
|
+
};
|
|
1614
|
+
const em = new ExpressionManager(loadingConfig);
|
|
1615
|
+
em.loadInitialExpressions(snapshot.expressions);
|
|
1616
|
+
// After loading: restore the normalized config for ongoing mutations
|
|
1617
|
+
// (generateId is preserved via the em.generateId field set in constructor)
|
|
1618
|
+
em.config = normalizedConfig;
|
|
1619
|
+
return em;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
//# sourceMappingURL=expression-manager.js.map
|