@polintpro/proposit-core 0.6.6 → 0.7.3
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 +194 -0
- package/dist/extensions/basics/schemata.d.ts +5 -0
- package/dist/extensions/basics/schemata.d.ts.map +1 -1
- package/dist/lib/consts.d.ts.map +1 -1
- package/dist/lib/consts.js +21 -2
- package/dist/lib/consts.js.map +1 -1
- package/dist/lib/core/argument-engine.d.ts +51 -2
- package/dist/lib/core/argument-engine.d.ts.map +1 -1
- package/dist/lib/core/argument-engine.js +764 -227
- package/dist/lib/core/argument-engine.js.map +1 -1
- package/dist/lib/core/change-collector.d.ts +1 -0
- package/dist/lib/core/change-collector.d.ts.map +1 -1
- package/dist/lib/core/change-collector.js +3 -0
- package/dist/lib/core/change-collector.js.map +1 -1
- package/dist/lib/core/claim-library.d.ts +4 -0
- package/dist/lib/core/claim-library.d.ts.map +1 -1
- package/dist/lib/core/claim-library.js +126 -59
- package/dist/lib/core/claim-library.js.map +1 -1
- package/dist/lib/core/claim-source-library.d.ts +4 -0
- package/dist/lib/core/claim-source-library.d.ts.map +1 -1
- package/dist/lib/core/claim-source-library.js +114 -38
- package/dist/lib/core/claim-source-library.js.map +1 -1
- package/dist/lib/core/diff.d.ts +10 -0
- package/dist/lib/core/diff.d.ts.map +1 -1
- package/dist/lib/core/diff.js +114 -21
- package/dist/lib/core/diff.js.map +1 -1
- package/dist/lib/core/expression-manager.d.ts +11 -0
- package/dist/lib/core/expression-manager.d.ts.map +1 -1
- package/dist/lib/core/expression-manager.js +379 -20
- package/dist/lib/core/expression-manager.js.map +1 -1
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +9 -2
- package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/library.interfaces.d.ts +19 -0
- package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +22 -0
- package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
- package/dist/lib/core/invariant-violation-error.d.ts +6 -0
- package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
- package/dist/lib/core/invariant-violation-error.js +12 -0
- package/dist/lib/core/invariant-violation-error.js.map +1 -0
- package/dist/lib/core/parser/formula.d.ts.map +1 -1
- package/dist/lib/core/parser/formula.js +2 -2
- package/dist/lib/core/parser/formula.js.map +1 -1
- package/dist/lib/core/premise-engine.d.ts +10 -0
- package/dist/lib/core/premise-engine.d.ts.map +1 -1
- package/dist/lib/core/premise-engine.js +699 -536
- package/dist/lib/core/premise-engine.js.map +1 -1
- package/dist/lib/core/source-library.d.ts +4 -0
- package/dist/lib/core/source-library.d.ts.map +1 -1
- package/dist/lib/core/source-library.js +126 -59
- package/dist/lib/core/source-library.js.map +1 -1
- package/dist/lib/core/variable-manager.d.ts +7 -0
- package/dist/lib/core/variable-manager.d.ts.map +1 -1
- package/dist/lib/core/variable-manager.js +65 -1
- package/dist/lib/core/variable-manager.js.map +1 -1
- package/dist/lib/index.d.ts +4 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +4 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/schemata/argument.d.ts +2 -0
- package/dist/lib/schemata/argument.d.ts.map +1 -1
- package/dist/lib/schemata/argument.js +6 -0
- package/dist/lib/schemata/argument.js.map +1 -1
- package/dist/lib/schemata/propositional.d.ts +41 -0
- package/dist/lib/schemata/propositional.d.ts.map +1 -1
- package/dist/lib/schemata/propositional.js +34 -0
- package/dist/lib/schemata/propositional.js.map +1 -1
- package/dist/lib/types/diff.d.ts +6 -0
- package/dist/lib/types/diff.d.ts.map +1 -1
- package/dist/lib/types/fork.d.ts +32 -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 +5 -4
- package/dist/lib/types/grammar.d.ts.map +1 -1
- package/dist/lib/types/grammar.js.map +1 -1
- package/dist/lib/types/validation.d.ts +46 -0
- package/dist/lib/types/validation.d.ts.map +1 -0
- package/dist/lib/types/validation.js +41 -0
- package/dist/lib/types/validation.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CorePropositionalExpressionSchema } from "../schemata/index.js";
|
|
2
3
|
import { getOrCreate } from "../utils/collections.js";
|
|
3
4
|
import { DEFAULT_POSITION_CONFIG, midpoint, } from "../utils/position.js";
|
|
4
5
|
import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
|
|
5
6
|
import { entityChecksum, computeHash, canonicalSerialize } from "./checksum.js";
|
|
6
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";
|
|
7
10
|
const PERMITTED_OPERATOR_SWAPS = {
|
|
8
11
|
and: "or",
|
|
9
12
|
or: "and",
|
|
@@ -94,6 +97,9 @@ export class ExpressionManager {
|
|
|
94
97
|
const expr = this.expressions.get(id);
|
|
95
98
|
if (!expr)
|
|
96
99
|
continue;
|
|
100
|
+
const oldChecksum = expr.checksum;
|
|
101
|
+
const oldDescendantChecksum = expr.descendantChecksum;
|
|
102
|
+
const oldCombinedChecksum = expr.combinedChecksum;
|
|
97
103
|
const metaChecksum = entityChecksum(expr, fields);
|
|
98
104
|
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
99
105
|
let descendantChecksum = null;
|
|
@@ -116,6 +122,18 @@ export class ExpressionManager {
|
|
|
116
122
|
descendantChecksum,
|
|
117
123
|
combinedChecksum,
|
|
118
124
|
});
|
|
125
|
+
if (this.collector &&
|
|
126
|
+
!this.collector.isExpressionAdded(expr.id) &&
|
|
127
|
+
(metaChecksum !== oldChecksum ||
|
|
128
|
+
descendantChecksum !== oldDescendantChecksum ||
|
|
129
|
+
combinedChecksum !== oldCombinedChecksum)) {
|
|
130
|
+
this.collector.modifiedExpression({
|
|
131
|
+
...expr,
|
|
132
|
+
checksum: metaChecksum,
|
|
133
|
+
descendantChecksum,
|
|
134
|
+
combinedChecksum,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
119
137
|
}
|
|
120
138
|
this.dirtyExpressionIds.clear();
|
|
121
139
|
}
|
|
@@ -887,25 +905,43 @@ export class ExpressionManager {
|
|
|
887
905
|
throw new Error(`Operator expression "${expression.id}" with "${expression.operator}" must be a root expression (parentId must be null).`);
|
|
888
906
|
}
|
|
889
907
|
// 10a. Non-not operators cannot be direct children of operators.
|
|
908
|
+
// Track which children need formula buffers (Site 2) for post-reparent insertion.
|
|
909
|
+
let needsParentFormulaBuffer = false;
|
|
910
|
+
const childrenNeedingFormulaBuffer = [];
|
|
890
911
|
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
891
|
-
// Check 1: new expression as child of anchor's parent.
|
|
912
|
+
// Check 1 (Site 1): new expression as child of anchor's parent.
|
|
892
913
|
if (anchor.parentId !== null &&
|
|
893
914
|
expression.type === "operator" &&
|
|
894
915
|
expression.operator !== "not") {
|
|
895
916
|
const anchorParent = this.expressions.get(anchor.parentId);
|
|
896
917
|
if (anchorParent && anchorParent.type === "operator") {
|
|
897
|
-
|
|
918
|
+
if (this.grammarConfig.autoNormalize) {
|
|
919
|
+
needsParentFormulaBuffer = true;
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
923
|
+
}
|
|
898
924
|
}
|
|
899
925
|
}
|
|
900
|
-
// Check 2: left/right nodes as children of the new expression.
|
|
926
|
+
// Check 2 (Site 2): left/right nodes as children of the new expression.
|
|
901
927
|
if (expression.type === "operator") {
|
|
902
928
|
if (leftNode?.type === "operator" &&
|
|
903
929
|
leftNode.operator !== "not") {
|
|
904
|
-
|
|
930
|
+
if (this.grammarConfig.autoNormalize) {
|
|
931
|
+
childrenNeedingFormulaBuffer.push(leftNodeId);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
935
|
+
}
|
|
905
936
|
}
|
|
906
937
|
if (rightNode?.type === "operator" &&
|
|
907
938
|
rightNode.operator !== "not") {
|
|
908
|
-
|
|
939
|
+
if (this.grammarConfig.autoNormalize) {
|
|
940
|
+
childrenNeedingFormulaBuffer.push(rightNodeId);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
944
|
+
}
|
|
909
945
|
}
|
|
910
946
|
}
|
|
911
947
|
}
|
|
@@ -918,18 +954,70 @@ export class ExpressionManager {
|
|
|
918
954
|
if (leftNodeId !== undefined) {
|
|
919
955
|
this.reparent(leftNodeId, expression.id, 0);
|
|
920
956
|
}
|
|
921
|
-
//
|
|
957
|
+
// Determine the slot for the new expression. If a parent formula buffer
|
|
958
|
+
// is needed, the formula takes the anchor slot and the expression goes under it.
|
|
959
|
+
let finalParentId = anchorParentId;
|
|
960
|
+
let finalPosition = anchorPosition;
|
|
961
|
+
if (needsParentFormulaBuffer) {
|
|
962
|
+
const formulaId = randomUUID();
|
|
963
|
+
const formulaExpr = this.attachChecksum({
|
|
964
|
+
id: formulaId,
|
|
965
|
+
type: "formula",
|
|
966
|
+
argumentId: expression.argumentId,
|
|
967
|
+
argumentVersion: expression.argumentVersion,
|
|
968
|
+
premiseId: expression
|
|
969
|
+
.premiseId,
|
|
970
|
+
parentId: anchorParentId,
|
|
971
|
+
position: anchorPosition,
|
|
972
|
+
});
|
|
973
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
974
|
+
this.collector?.addedExpression({
|
|
975
|
+
...formulaExpr,
|
|
976
|
+
});
|
|
977
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(formulaId);
|
|
978
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
979
|
+
finalParentId = formulaId;
|
|
980
|
+
finalPosition = 0;
|
|
981
|
+
}
|
|
982
|
+
// Store the new expression in its slot.
|
|
922
983
|
const stored = this.attachChecksum({
|
|
923
984
|
...expression,
|
|
924
|
-
parentId:
|
|
925
|
-
position:
|
|
985
|
+
parentId: finalParentId,
|
|
986
|
+
position: finalPosition,
|
|
926
987
|
});
|
|
927
988
|
this.expressions.set(expression.id, stored);
|
|
928
989
|
this.collector?.addedExpression({
|
|
929
990
|
...stored,
|
|
930
991
|
});
|
|
931
|
-
getOrCreate(this.childExpressionIdsByParentId,
|
|
932
|
-
getOrCreate(this.childPositionsByParentId,
|
|
992
|
+
getOrCreate(this.childExpressionIdsByParentId, finalParentId, () => new Set()).add(expression.id);
|
|
993
|
+
getOrCreate(this.childPositionsByParentId, finalParentId, () => new Set()).add(finalPosition);
|
|
994
|
+
// Site 2: auto-insert formula buffers between the new expression and
|
|
995
|
+
// any offending operator children.
|
|
996
|
+
for (const childId of childrenNeedingFormulaBuffer) {
|
|
997
|
+
const child = this.expressions.get(childId);
|
|
998
|
+
const childPosition = child.position;
|
|
999
|
+
const formulaId = randomUUID();
|
|
1000
|
+
// Reparent the child under the formula first. This detaches the child
|
|
1001
|
+
// from expression.id's tracking (removing its position from the set).
|
|
1002
|
+
this.reparent(childId, formulaId, 0);
|
|
1003
|
+
// Now create and register the formula at the freed position under expression.id.
|
|
1004
|
+
const formulaExpr = this.attachChecksum({
|
|
1005
|
+
id: formulaId,
|
|
1006
|
+
type: "formula",
|
|
1007
|
+
argumentId: expression.argumentId,
|
|
1008
|
+
argumentVersion: expression.argumentVersion,
|
|
1009
|
+
premiseId: expression
|
|
1010
|
+
.premiseId,
|
|
1011
|
+
parentId: expression.id,
|
|
1012
|
+
position: childPosition,
|
|
1013
|
+
});
|
|
1014
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
1015
|
+
this.collector?.addedExpression({
|
|
1016
|
+
...formulaExpr,
|
|
1017
|
+
});
|
|
1018
|
+
getOrCreate(this.childExpressionIdsByParentId, expression.id, () => new Set()).add(formulaId);
|
|
1019
|
+
getOrCreate(this.childPositionsByParentId, expression.id, () => new Set()).add(childPosition);
|
|
1020
|
+
}
|
|
933
1021
|
// Mark the new expression and its ancestors dirty for hierarchical checksum recomputation.
|
|
934
1022
|
// Note: reparent() already marks children dirty, so this propagates from the new expression up.
|
|
935
1023
|
this.markExpressionDirty(expression.id);
|
|
@@ -1005,23 +1093,43 @@ export class ExpressionManager {
|
|
|
1005
1093
|
throw new Error(`Sibling expression "${newSibling.id}" with "${newSibling.operator}" cannot be subordinated (it must remain a root expression).`);
|
|
1006
1094
|
}
|
|
1007
1095
|
// 10a. Non-not operators cannot be direct children of operators.
|
|
1096
|
+
// Track which sites need formula buffers for post-mutation insertion.
|
|
1097
|
+
let needsParentFormulaBuffer = false;
|
|
1098
|
+
let existingNodeNeedsFormulaBuffer = false;
|
|
1099
|
+
let siblingNeedsFormulaBuffer = false;
|
|
1008
1100
|
if (this.grammarConfig.enforceFormulaBetweenOperators) {
|
|
1009
|
-
// Check 1: new operator as child of existing node's parent.
|
|
1101
|
+
// Check 1 (Site 1): new operator as child of existing node's parent.
|
|
1010
1102
|
// Note: step 7 already rejects `not`, so operator.operator is always non-not here.
|
|
1011
1103
|
if (existingNode.parentId !== null) {
|
|
1012
1104
|
const existingParent = this.expressions.get(existingNode.parentId);
|
|
1013
1105
|
if (existingParent && existingParent.type === "operator") {
|
|
1014
|
-
|
|
1106
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1107
|
+
needsParentFormulaBuffer = true;
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1111
|
+
}
|
|
1015
1112
|
}
|
|
1016
1113
|
}
|
|
1017
|
-
// Check 2: existing node
|
|
1114
|
+
// Check 2 (Site 2): existing node as child of new operator.
|
|
1018
1115
|
if (existingNode.type === "operator" &&
|
|
1019
1116
|
existingNode.operator !== "not") {
|
|
1020
|
-
|
|
1117
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1118
|
+
existingNodeNeedsFormulaBuffer = true;
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1122
|
+
}
|
|
1021
1123
|
}
|
|
1124
|
+
// Check 3 (Site 3): new sibling as child of new operator.
|
|
1022
1125
|
if (newSibling.type === "operator" &&
|
|
1023
1126
|
newSibling.operator !== "not") {
|
|
1024
|
-
|
|
1127
|
+
if (this.grammarConfig.autoNormalize) {
|
|
1128
|
+
siblingNeedsFormulaBuffer = true;
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
throw new Error(`Non-not operator expressions cannot be direct children of operator expressions — wrap in a formula node`);
|
|
1132
|
+
}
|
|
1025
1133
|
}
|
|
1026
1134
|
}
|
|
1027
1135
|
// Save the existing node's slot (the operator will inherit it).
|
|
@@ -1048,18 +1156,93 @@ export class ExpressionManager {
|
|
|
1048
1156
|
});
|
|
1049
1157
|
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(newSibling.id);
|
|
1050
1158
|
getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(siblingPosition);
|
|
1051
|
-
//
|
|
1159
|
+
// Determine the operator's slot. If a parent formula buffer is needed,
|
|
1160
|
+
// the formula takes the anchor slot and the operator goes under it.
|
|
1161
|
+
let operatorParentId = anchorParentId;
|
|
1162
|
+
let operatorPosition = anchorPosition;
|
|
1163
|
+
if (needsParentFormulaBuffer) {
|
|
1164
|
+
const formulaId = randomUUID();
|
|
1165
|
+
const formulaExpr = this.attachChecksum({
|
|
1166
|
+
id: formulaId,
|
|
1167
|
+
type: "formula",
|
|
1168
|
+
argumentId: operator.argumentId,
|
|
1169
|
+
argumentVersion: operator.argumentVersion,
|
|
1170
|
+
premiseId: operator
|
|
1171
|
+
.premiseId,
|
|
1172
|
+
parentId: anchorParentId,
|
|
1173
|
+
position: anchorPosition,
|
|
1174
|
+
});
|
|
1175
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
1176
|
+
this.collector?.addedExpression({
|
|
1177
|
+
...formulaExpr,
|
|
1178
|
+
});
|
|
1179
|
+
getOrCreate(this.childExpressionIdsByParentId, anchorParentId, () => new Set()).add(formulaId);
|
|
1180
|
+
getOrCreate(this.childPositionsByParentId, anchorParentId, () => new Set()).add(anchorPosition);
|
|
1181
|
+
operatorParentId = formulaId;
|
|
1182
|
+
operatorPosition = 0;
|
|
1183
|
+
}
|
|
1184
|
+
// Store operator in its slot.
|
|
1052
1185
|
const storedOperator = this.attachChecksum({
|
|
1053
1186
|
...operator,
|
|
1054
|
-
parentId:
|
|
1055
|
-
position:
|
|
1187
|
+
parentId: operatorParentId,
|
|
1188
|
+
position: operatorPosition,
|
|
1056
1189
|
});
|
|
1057
1190
|
this.expressions.set(operator.id, storedOperator);
|
|
1058
1191
|
this.collector?.addedExpression({
|
|
1059
1192
|
...storedOperator,
|
|
1060
1193
|
});
|
|
1061
|
-
getOrCreate(this.childExpressionIdsByParentId,
|
|
1062
|
-
getOrCreate(this.childPositionsByParentId,
|
|
1194
|
+
getOrCreate(this.childExpressionIdsByParentId, operatorParentId, () => new Set()).add(operator.id);
|
|
1195
|
+
getOrCreate(this.childPositionsByParentId, operatorParentId, () => new Set()).add(operatorPosition);
|
|
1196
|
+
// Site 2: auto-insert formula buffer between operator and existing node.
|
|
1197
|
+
if (existingNodeNeedsFormulaBuffer) {
|
|
1198
|
+
const existingChild = this.expressions.get(existingNodeId);
|
|
1199
|
+
const childPosition = existingChild.position;
|
|
1200
|
+
const formulaId = randomUUID();
|
|
1201
|
+
// Reparent existing node under formula first (frees position in operator's tracking).
|
|
1202
|
+
this.reparent(existingNodeId, formulaId, 0);
|
|
1203
|
+
// Register formula at the freed position under operator.
|
|
1204
|
+
const formulaExpr = this.attachChecksum({
|
|
1205
|
+
id: formulaId,
|
|
1206
|
+
type: "formula",
|
|
1207
|
+
argumentId: operator.argumentId,
|
|
1208
|
+
argumentVersion: operator.argumentVersion,
|
|
1209
|
+
premiseId: operator
|
|
1210
|
+
.premiseId,
|
|
1211
|
+
parentId: operator.id,
|
|
1212
|
+
position: childPosition,
|
|
1213
|
+
});
|
|
1214
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
1215
|
+
this.collector?.addedExpression({
|
|
1216
|
+
...formulaExpr,
|
|
1217
|
+
});
|
|
1218
|
+
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(formulaId);
|
|
1219
|
+
getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(childPosition);
|
|
1220
|
+
}
|
|
1221
|
+
// Site 3: auto-insert formula buffer between operator and new sibling.
|
|
1222
|
+
if (siblingNeedsFormulaBuffer) {
|
|
1223
|
+
const siblingChild = this.expressions.get(newSibling.id);
|
|
1224
|
+
const childPosition = siblingChild.position;
|
|
1225
|
+
const formulaId = randomUUID();
|
|
1226
|
+
// Reparent sibling under formula first (frees position in operator's tracking).
|
|
1227
|
+
this.reparent(newSibling.id, formulaId, 0);
|
|
1228
|
+
// Register formula at the freed position under operator.
|
|
1229
|
+
const formulaExpr = this.attachChecksum({
|
|
1230
|
+
id: formulaId,
|
|
1231
|
+
type: "formula",
|
|
1232
|
+
argumentId: operator.argumentId,
|
|
1233
|
+
argumentVersion: operator.argumentVersion,
|
|
1234
|
+
premiseId: operator
|
|
1235
|
+
.premiseId,
|
|
1236
|
+
parentId: operator.id,
|
|
1237
|
+
position: childPosition,
|
|
1238
|
+
});
|
|
1239
|
+
this.expressions.set(formulaId, formulaExpr);
|
|
1240
|
+
this.collector?.addedExpression({
|
|
1241
|
+
...formulaExpr,
|
|
1242
|
+
});
|
|
1243
|
+
getOrCreate(this.childExpressionIdsByParentId, operator.id, () => new Set()).add(formulaId);
|
|
1244
|
+
getOrCreate(this.childPositionsByParentId, operator.id, () => new Set()).add(childPosition);
|
|
1245
|
+
}
|
|
1063
1246
|
// Mark the new operator (and ancestors), the new sibling, and the reparented existing node dirty.
|
|
1064
1247
|
// reparent() already marks the existing node dirty; mark the operator and sibling as well.
|
|
1065
1248
|
this.markExpressionDirty(newSibling.id);
|
|
@@ -1151,6 +1334,182 @@ export class ExpressionManager {
|
|
|
1151
1334
|
loadExpressions(expressions) {
|
|
1152
1335
|
this.loadInitialExpressions(expressions);
|
|
1153
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Performs a comprehensive validation sweep on all managed expressions.
|
|
1339
|
+
*
|
|
1340
|
+
* Collects ALL violations rather than failing on the first one. Checks:
|
|
1341
|
+
* schema validity, duplicate IDs, self-referential parents, parent
|
|
1342
|
+
* existence, parent container type, root-only operators, formula-between-
|
|
1343
|
+
* operators (when enabled), child limits, position uniqueness, and
|
|
1344
|
+
* checksum integrity.
|
|
1345
|
+
*/
|
|
1346
|
+
validate() {
|
|
1347
|
+
const violations = [];
|
|
1348
|
+
const seenIds = new Set();
|
|
1349
|
+
// ── 1. Save pre-flush checksums for later comparison ──
|
|
1350
|
+
const preFlushChecksums = new Map();
|
|
1351
|
+
for (const [id, expr] of this.expressions) {
|
|
1352
|
+
if (expr.checksum != null) {
|
|
1353
|
+
preFlushChecksums.set(id, {
|
|
1354
|
+
checksum: expr.checksum,
|
|
1355
|
+
descendantChecksum: expr.descendantChecksum,
|
|
1356
|
+
combinedChecksum: expr.combinedChecksum,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
// ── 2. Flush checksums to get fresh values ──
|
|
1361
|
+
// Mark all expressions dirty so flush recomputes everything
|
|
1362
|
+
for (const id of this.expressions.keys()) {
|
|
1363
|
+
this.dirtyExpressionIds.add(id);
|
|
1364
|
+
}
|
|
1365
|
+
this.flushExpressionChecksums();
|
|
1366
|
+
// ── 3. Per-expression checks ──
|
|
1367
|
+
// Build a sibling-position map for position uniqueness checks
|
|
1368
|
+
const positionsByParent = new Map();
|
|
1369
|
+
for (const [id, expr] of this.expressions) {
|
|
1370
|
+
// 3a. Schema check
|
|
1371
|
+
if (!Value.Check(CorePropositionalExpressionSchema, expr)) {
|
|
1372
|
+
violations.push({
|
|
1373
|
+
code: EXPR_SCHEMA_INVALID,
|
|
1374
|
+
message: `Expression "${id}" does not conform to CorePropositionalExpressionSchema.`,
|
|
1375
|
+
entityType: "expression",
|
|
1376
|
+
entityId: id,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
// 3b. Duplicate ID
|
|
1380
|
+
if (seenIds.has(id)) {
|
|
1381
|
+
violations.push({
|
|
1382
|
+
code: EXPR_DUPLICATE_ID,
|
|
1383
|
+
message: `Duplicate expression ID "${id}".`,
|
|
1384
|
+
entityType: "expression",
|
|
1385
|
+
entityId: id,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
seenIds.add(id);
|
|
1389
|
+
// 3c. Self-referential parent
|
|
1390
|
+
if (expr.parentId === id) {
|
|
1391
|
+
violations.push({
|
|
1392
|
+
code: EXPR_SELF_REFERENTIAL_PARENT,
|
|
1393
|
+
message: `Expression "${id}" references itself as parent.`,
|
|
1394
|
+
entityType: "expression",
|
|
1395
|
+
entityId: id,
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
// 3d. Parent existence
|
|
1399
|
+
if (expr.parentId !== null &&
|
|
1400
|
+
!this.expressions.has(expr.parentId)) {
|
|
1401
|
+
violations.push({
|
|
1402
|
+
code: EXPR_PARENT_NOT_FOUND,
|
|
1403
|
+
message: `Expression "${id}" references non-existent parent "${expr.parentId}".`,
|
|
1404
|
+
entityType: "expression",
|
|
1405
|
+
entityId: id,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
// 3e. Parent is container (operator or formula)
|
|
1409
|
+
if (expr.parentId !== null && this.expressions.has(expr.parentId)) {
|
|
1410
|
+
const parent = this.expressions.get(expr.parentId);
|
|
1411
|
+
if (parent.type !== "operator" && parent.type !== "formula") {
|
|
1412
|
+
violations.push({
|
|
1413
|
+
code: EXPR_PARENT_NOT_CONTAINER,
|
|
1414
|
+
message: `Expression "${id}" has parent "${expr.parentId}" of type "${parent.type}" (expected operator or formula).`,
|
|
1415
|
+
entityType: "expression",
|
|
1416
|
+
entityId: id,
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// 3f. Root-only: implies/iff must have parentId === null
|
|
1421
|
+
if (expr.type === "operator" &&
|
|
1422
|
+
(expr.operator === "implies" || expr.operator === "iff") &&
|
|
1423
|
+
expr.parentId !== null) {
|
|
1424
|
+
violations.push({
|
|
1425
|
+
code: EXPR_ROOT_ONLY_VIOLATED,
|
|
1426
|
+
message: `Root-only operator "${expr.operator}" expression "${id}" has non-null parentId "${expr.parentId}".`,
|
|
1427
|
+
entityType: "expression",
|
|
1428
|
+
entityId: id,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
// 3g. Formula-between-operators
|
|
1432
|
+
if (this.grammarConfig.enforceFormulaBetweenOperators &&
|
|
1433
|
+
expr.parentId !== null &&
|
|
1434
|
+
expr.type === "operator" &&
|
|
1435
|
+
expr.operator !== "not") {
|
|
1436
|
+
const parent = this.expressions.get(expr.parentId);
|
|
1437
|
+
if (parent && parent.type === "operator") {
|
|
1438
|
+
violations.push({
|
|
1439
|
+
code: EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED,
|
|
1440
|
+
message: `Non-not operator "${expr.operator}" expression "${id}" is a direct child of operator "${expr.parentId}".`,
|
|
1441
|
+
entityType: "expression",
|
|
1442
|
+
entityId: id,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
// Collect positions for uniqueness check
|
|
1447
|
+
const parentKey = expr.parentId;
|
|
1448
|
+
let parentPositions = positionsByParent.get(parentKey);
|
|
1449
|
+
if (!parentPositions) {
|
|
1450
|
+
parentPositions = new Map();
|
|
1451
|
+
positionsByParent.set(parentKey, parentPositions);
|
|
1452
|
+
}
|
|
1453
|
+
const idsAtPosition = parentPositions.get(expr.position);
|
|
1454
|
+
if (idsAtPosition) {
|
|
1455
|
+
idsAtPosition.push(id);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
parentPositions.set(expr.position, [id]);
|
|
1459
|
+
}
|
|
1460
|
+
// 3j. Checksum comparison
|
|
1461
|
+
const pre = preFlushChecksums.get(id);
|
|
1462
|
+
if (pre) {
|
|
1463
|
+
const fresh = this.expressions.get(id);
|
|
1464
|
+
if (pre.checksum !== fresh.checksum ||
|
|
1465
|
+
pre.descendantChecksum !== fresh.descendantChecksum ||
|
|
1466
|
+
pre.combinedChecksum !== fresh.combinedChecksum) {
|
|
1467
|
+
violations.push({
|
|
1468
|
+
code: EXPR_CHECKSUM_MISMATCH,
|
|
1469
|
+
message: `Expression "${id}" checksum mismatch: stored does not match recomputed.`,
|
|
1470
|
+
entityType: "expression",
|
|
1471
|
+
entityId: id,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
// ── 4. Child limit checks (not/formula: max 1 child) ──
|
|
1477
|
+
for (const [id, expr] of this.expressions) {
|
|
1478
|
+
if ((expr.type === "operator" && expr.operator === "not") ||
|
|
1479
|
+
expr.type === "formula") {
|
|
1480
|
+
const childIds = this.childExpressionIdsByParentId.get(id);
|
|
1481
|
+
const childCount = childIds?.size ?? 0;
|
|
1482
|
+
if (childCount > 1) {
|
|
1483
|
+
const label = expr.type === "formula" ? "Formula" : `Operator "not"`;
|
|
1484
|
+
violations.push({
|
|
1485
|
+
code: EXPR_CHILD_LIMIT_EXCEEDED,
|
|
1486
|
+
message: `${label} expression "${id}" has ${childCount} children (max 1).`,
|
|
1487
|
+
entityType: "expression",
|
|
1488
|
+
entityId: id,
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// ── 5. Position uniqueness ──
|
|
1494
|
+
for (const [, posMap] of positionsByParent) {
|
|
1495
|
+
for (const [position, ids] of posMap) {
|
|
1496
|
+
if (ids.length > 1) {
|
|
1497
|
+
for (const id of ids) {
|
|
1498
|
+
violations.push({
|
|
1499
|
+
code: EXPR_POSITION_DUPLICATE,
|
|
1500
|
+
message: `Position ${position} is shared by expressions [${ids.join(", ")}].`,
|
|
1501
|
+
entityType: "expression",
|
|
1502
|
+
entityId: id,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
ok: violations.length === 0,
|
|
1510
|
+
violations,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1154
1513
|
/** Returns a serializable snapshot of the current state. */
|
|
1155
1514
|
snapshot() {
|
|
1156
1515
|
return {
|