@proposit/proposit-core 0.12.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +104 -93
  2. package/dist/cli/commands/premises.d.ts.map +1 -1
  3. package/dist/cli/commands/premises.js +28 -24
  4. package/dist/cli/commands/premises.js.map +1 -1
  5. package/dist/cli/commands/repair.d.ts.map +1 -1
  6. package/dist/cli/commands/repair.js +4 -2
  7. package/dist/cli/commands/repair.js.map +1 -1
  8. package/dist/cli/commands/validate.d.ts.map +1 -1
  9. package/dist/cli/commands/validate.js +7 -1
  10. package/dist/cli/commands/validate.js.map +1 -1
  11. package/dist/cli/engine.d.ts.map +1 -1
  12. package/dist/cli/engine.js +7 -25
  13. package/dist/cli/engine.js.map +1 -1
  14. package/dist/cli/import.d.ts.map +1 -1
  15. package/dist/cli/import.js +66 -28
  16. package/dist/cli/import.js.map +1 -1
  17. package/dist/lib/core/argument-engine.d.ts +275 -10
  18. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  19. package/dist/lib/core/argument-engine.js +442 -82
  20. package/dist/lib/core/argument-engine.js.map +1 -1
  21. package/dist/lib/core/argument-library.d.ts +5 -1
  22. package/dist/lib/core/argument-library.d.ts.map +1 -1
  23. package/dist/lib/core/argument-library.js +7 -3
  24. package/dist/lib/core/argument-library.js.map +1 -1
  25. package/dist/lib/core/expression-manager.d.ts +68 -73
  26. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  27. package/dist/lib/core/expression-manager.js +242 -762
  28. package/dist/lib/core/expression-manager.js.map +1 -1
  29. package/dist/lib/core/fork.d.ts +5 -1
  30. package/dist/lib/core/fork.d.ts.map +1 -1
  31. package/dist/lib/core/fork.js +16 -6
  32. package/dist/lib/core/fork.js.map +1 -1
  33. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +68 -7
  34. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  35. package/dist/lib/core/interfaces/library.interfaces.d.ts +8 -3
  36. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  37. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +50 -47
  38. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  39. package/dist/lib/core/premise-engine.d.ts +80 -11
  40. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  41. package/dist/lib/core/premise-engine.js +232 -80
  42. package/dist/lib/core/premise-engine.js.map +1 -1
  43. package/dist/lib/core/proposit-core.d.ts.map +1 -1
  44. package/dist/lib/core/proposit-core.js +13 -3
  45. package/dist/lib/core/proposit-core.js.map +1 -1
  46. package/dist/lib/grammar/an-rules.d.ts +158 -0
  47. package/dist/lib/grammar/an-rules.d.ts.map +1 -0
  48. package/dist/lib/grammar/an-rules.js +778 -0
  49. package/dist/lib/grammar/an-rules.js.map +1 -0
  50. package/dist/lib/grammar/auto-normalize.d.ts +14 -0
  51. package/dist/lib/grammar/auto-normalize.d.ts.map +1 -0
  52. package/dist/lib/grammar/auto-normalize.js +35 -0
  53. package/dist/lib/grammar/auto-normalize.js.map +1 -0
  54. package/dist/lib/grammar/bounded-subtree.d.ts +30 -0
  55. package/dist/lib/grammar/bounded-subtree.d.ts.map +1 -0
  56. package/dist/lib/grammar/bounded-subtree.js +74 -0
  57. package/dist/lib/grammar/bounded-subtree.js.map +1 -0
  58. package/dist/lib/grammar/naked-q.d.ts +20 -0
  59. package/dist/lib/grammar/naked-q.d.ts.map +1 -0
  60. package/dist/lib/grammar/naked-q.js +58 -0
  61. package/dist/lib/grammar/naked-q.js.map +1 -0
  62. package/dist/lib/grammar/normalize.d.ts +12 -0
  63. package/dist/lib/grammar/normalize.d.ts.map +1 -0
  64. package/dist/lib/grammar/normalize.js +45 -0
  65. package/dist/lib/grammar/normalize.js.map +1 -0
  66. package/dist/lib/grammar/populate-from.d.ts +25 -0
  67. package/dist/lib/grammar/populate-from.d.ts.map +1 -0
  68. package/dist/lib/grammar/populate-from.js +252 -0
  69. package/dist/lib/grammar/populate-from.js.map +1 -0
  70. package/dist/lib/grammar/repair.d.ts +65 -0
  71. package/dist/lib/grammar/repair.d.ts.map +1 -0
  72. package/dist/lib/grammar/repair.js +251 -0
  73. package/dist/lib/grammar/repair.js.map +1 -0
  74. package/dist/lib/grammar/types.d.ts +17 -0
  75. package/dist/lib/grammar/types.d.ts.map +1 -0
  76. package/dist/lib/grammar/types.js +82 -0
  77. package/dist/lib/grammar/types.js.map +1 -0
  78. package/dist/lib/grammar/validate.d.ts +4 -0
  79. package/dist/lib/grammar/validate.d.ts.map +1 -0
  80. package/dist/lib/grammar/validate.js +28 -0
  81. package/dist/lib/grammar/validate.js.map +1 -0
  82. package/dist/lib/grammar/validators/context.d.ts +11 -0
  83. package/dist/lib/grammar/validators/context.d.ts.map +1 -0
  84. package/dist/lib/grammar/validators/context.js +5 -0
  85. package/dist/lib/grammar/validators/context.js.map +1 -0
  86. package/dist/lib/grammar/validators/derivable.d.ts +63 -0
  87. package/dist/lib/grammar/validators/derivable.d.ts.map +1 -0
  88. package/dist/lib/grammar/validators/derivable.js +502 -0
  89. package/dist/lib/grammar/validators/derivable.js.map +1 -0
  90. package/dist/lib/grammar/validators/evaluable.d.ts +48 -0
  91. package/dist/lib/grammar/validators/evaluable.d.ts.map +1 -0
  92. package/dist/lib/grammar/validators/evaluable.js +226 -0
  93. package/dist/lib/grammar/validators/evaluable.js.map +1 -0
  94. package/dist/lib/grammar/validators/presentable.d.ts +45 -0
  95. package/dist/lib/grammar/validators/presentable.d.ts.map +1 -0
  96. package/dist/lib/grammar/validators/presentable.js +231 -0
  97. package/dist/lib/grammar/validators/presentable.js.map +1 -0
  98. package/dist/lib/grammar/validators/structural.d.ts +103 -0
  99. package/dist/lib/grammar/validators/structural.d.ts.map +1 -0
  100. package/dist/lib/grammar/validators/structural.js +602 -0
  101. package/dist/lib/grammar/validators/structural.js.map +1 -0
  102. package/dist/lib/index.d.ts +4 -3
  103. package/dist/lib/index.d.ts.map +1 -1
  104. package/dist/lib/index.js +2 -2
  105. package/dist/lib/index.js.map +1 -1
  106. package/dist/lib/parsing/argument-parser.d.ts.map +1 -1
  107. package/dist/lib/parsing/argument-parser.js +52 -10
  108. package/dist/lib/parsing/argument-parser.js.map +1 -1
  109. package/dist/lib/types/evaluation.d.ts +1 -1
  110. package/dist/lib/types/evaluation.d.ts.map +1 -1
  111. package/dist/lib/types/fork.d.ts +12 -3
  112. package/dist/lib/types/fork.d.ts.map +1 -1
  113. package/dist/lib/types/validation.d.ts +0 -6
  114. package/dist/lib/types/validation.d.ts.map +1 -1
  115. package/dist/lib/types/validation.js +23 -6
  116. package/dist/lib/types/validation.js.map +1 -1
  117. package/package.json +1 -1
  118. package/dist/lib/core/managed-derivation-premise-engine.d.ts +0 -172
  119. package/dist/lib/core/managed-derivation-premise-engine.d.ts.map +0 -1
  120. package/dist/lib/core/managed-derivation-premise-engine.js +0 -550
  121. package/dist/lib/core/managed-derivation-premise-engine.js.map +0 -1
  122. package/dist/lib/types/grammar.d.ts +0 -83
  123. package/dist/lib/types/grammar.d.ts.map +0 -1
  124. package/dist/lib/types/grammar.js +0 -24
  125. package/dist/lib/types/grammar.js.map +0 -1
@@ -1,6 +1,5 @@
1
1
  import { isClaimBound, isPremiseBound, } from "../schemata/index.js";
2
- import { DEFAULT_GRAMMAR_CONFIG, PERMISSIVE_GRAMMAR_CONFIG, } from "../types/grammar.js";
3
- import { AXIOM_VARIABLE_ASSIGNMENT_FORBIDDEN, CLAIM_NOT_FOUND, CREATE_DERIVATION_CLAIM_NOT_FOUND, CREATE_DERIVATION_REQUIRES_DERIVED_CLAIM_ID, DERIVATION_STRUCTURE_INVALID_AT_EVALUATION, } from "../types/validation.js";
2
+ import { AXIOM_VARIABLE_ASSIGNMENT_FORBIDDEN, CLAIM_NOT_FOUND, CREATE_DERIVATION_CLAIM_NOT_FOUND, CREATE_DERIVATION_REQUIRES_DERIVED_CLAIM_ID, } from "../types/validation.js";
4
3
  import { validateDerivationStructure } from "../utils/derivation-validation.js";
5
4
  import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
6
5
  import { ChangeCollector } from "./change-collector.js";
@@ -8,6 +7,12 @@ import { canonicalSerialize, computeHash, entityChecksum } from "./checksum.js";
8
7
  import { evaluateArgument as evaluateArgumentStandalone, checkArgumentValidity as checkArgumentValidityStandalone, } from "./evaluation/argument-evaluation.js";
9
8
  import { makeErrorIssue, makeValidationResult, } from "./evaluation/validation.js";
10
9
  import { validateArgument as validateArgumentStandalone, validateArgumentAfterPremiseMutation as validateAfterPremiseMutationStandalone, validateArgumentEvaluability as validateArgumentEvaluabilityStandalone, collectArgumentReferencedVariables as collectArgumentReferencedVariablesStandalone, } from "./argument-validation.js";
10
+ import { normalizeArgument } from "../grammar/normalize.js";
11
+ import { runAssistiveNormalization } from "../grammar/auto-normalize.js";
12
+ import { isNakedQDerivationPremise } from "../grammar/naked-q.js";
13
+ import { populateFromGrounding as populateFromGroundingImpl, } from "../grammar/populate-from.js";
14
+ import { removeUnresolvableVariables as removeUnresolvableVariablesImpl, removeOrphanOperators as removeOrphanOperatorsImpl, removeDuplicateDerivationPremises as removeDuplicateDerivationPremisesImpl, dropAxiomsFromMixedAntecedent as dropAxiomsFromMixedAntecedentImpl, } from "../grammar/repair.js";
15
+ import { validate as validateGrammar } from "../grammar/validate.js";
11
16
  import { InvariantViolationError } from "./invariant-violation-error.js";
12
17
  import { PremiseEngine } from "./premise-engine.js";
13
18
  import { VariableManager } from "./variable-manager.js";
@@ -28,9 +33,24 @@ export class ArgumentEngine {
28
33
  conclusionPremiseId;
29
34
  checksumConfig;
30
35
  positionConfig;
31
- grammarConfig;
36
+ engineBehavior;
32
37
  generateId;
33
38
  restoringFromSnapshot = false;
39
+ // D2b — re-entrance guard for the AN post-mutation hook. The
40
+ // `setOnMutate` callbacks (3 sites: createPremise, fromSnapshot,
41
+ // restoreFromSnapshot) fire `runAssistiveNormalization(this)` after
42
+ // every successful mutation when `behavior === 'assistive'`. AN
43
+ // itself mutates premises (removeExpression / reparentExpression /
44
+ // wrapInFormula), which re-fires `setOnMutate`. Without a guard the
45
+ // outer mutation would trigger nested AN sweeps. The guard is
46
+ // toggled by `_beginApplyAN()` / `_endApplyAN()` (see below); the
47
+ // chokepoint is `applyANToFixedPoint` in
48
+ // `src/lib/grammar/an-rules.ts`, so both
49
+ // `runAssistiveNormalization` (post-hook) and `normalizeArgument`
50
+ // (`engine.normalize()`) are covered by the same gate. The
51
+ // accessor pair is `beginApplyAN()` / `endApplyAN()` below
52
+ // (marked `@internal` — not part of the public API).
53
+ applyingAN = false;
34
54
  checksumDirty = true;
35
55
  cachedMetaChecksum;
36
56
  cachedDescendantChecksum;
@@ -53,7 +73,7 @@ export class ArgumentEngine {
53
73
  this.premises = new Map();
54
74
  this.checksumConfig = options?.checksumConfig;
55
75
  this.positionConfig = options?.positionConfig;
56
- this.grammarConfig = options?.grammarConfig;
76
+ this.engineBehavior = options?.behavior ?? "assistive";
57
77
  this.generateId = options?.generateId ?? defaultGenerateId;
58
78
  this.variables = new VariableManager({
59
79
  checksumConfig: this.checksumConfig,
@@ -159,7 +179,7 @@ export class ArgumentEngine {
159
179
  this.suppressPremiseValidation();
160
180
  try {
161
181
  const result = fn();
162
- const validation = this.validate();
182
+ const validation = this.validateInvariants();
163
183
  if (!validation.ok) {
164
184
  this.rollbackInternal(snap);
165
185
  throw new InvariantViolationError(validation.violations);
@@ -295,6 +315,92 @@ export class ArgumentEngine {
295
315
  }
296
316
  }
297
317
  }
318
+ /**
319
+ * Current engine behavior setting. Controls whether the
320
+ * auto-normalization (AN) rule set runs as a post-hook after every
321
+ * successful Structural mutation. See the JSDoc on
322
+ * `TLogicEngineOptions.behavior` for the full contract.
323
+ *
324
+ * @since 1.0.0
325
+ */
326
+ get behavior() {
327
+ return this.engineBehavior;
328
+ }
329
+ /**
330
+ * Access to the engine's ID generator function. Used by in-package
331
+ * helpers that build new entity trees and need fresh IDs (e.g. the
332
+ * `populateFromGrounding` factory in
333
+ * `src/lib/grammar/populate-from.ts`). The generator is captured
334
+ * once at construction (default `crypto.randomUUID`) and stays
335
+ * immutable for the engine's lifetime; this accessor returns the
336
+ * same function reference on every call.
337
+ *
338
+ * Replaces the prior `(engine as unknown as { generateId: () =>
339
+ * string }).generateId` cast in `populate-from.ts`. The accessor is
340
+ * marked `@internal` so it is not surfaced in generated API docs /
341
+ * type bundles — the in-package factory callers are its intended
342
+ * consumers; external programmatic-construction use cases should
343
+ * supply their own generator rather than borrowing the engine's.
344
+ *
345
+ * @internal
346
+ * @since 1.0.0
347
+ */
348
+ get idGenerator() {
349
+ return this.generateId;
350
+ }
351
+ /**
352
+ * Switches the engine's behavior at runtime. Going `permissive →
353
+ * assistive` does **not** auto-run a global `normalize()` pass; the
354
+ * UI is expected to prompt the user before invoking `normalize()`
355
+ * explicitly.
356
+ *
357
+ * As of v1.0 (Phase D2) behavior is enforced entirely via the AN
358
+ * post-mutation hook in `runAssistiveNormalization` — the legacy
359
+ * per-flag `grammarConfig` plumbing that bridged behavior to
360
+ * premise-level enforcement is gone. Switching `permissive →
361
+ * assistive` makes the next successful Structural mutation trigger
362
+ * the AN pass; switching the other direction stops the AN pass
363
+ * from running until the user opts back in.
364
+ *
365
+ * @since 1.0.0
366
+ */
367
+ setBehavior(b) {
368
+ this.engineBehavior = b;
369
+ }
370
+ /**
371
+ * Acquire the AN re-entrance guard. Returns `true` iff the guard
372
+ * was acquired (i.e. AN is not already running for this engine);
373
+ * the caller is then obligated to call `endApplyAN()` after the
374
+ * AN sweep. Returns `false` if AN is already in progress, in
375
+ * which case the caller short-circuits to avoid nested AN
376
+ * sweeps.
377
+ *
378
+ * Used by `applyANToFixedPoint` in `src/lib/grammar/an-rules.ts`
379
+ * (the single chokepoint for both `runAssistiveNormalization`
380
+ * and `normalizeArgument`). The post-mutation hook in
381
+ * `setOnMutate` calls `runAssistiveNormalization(this)` which
382
+ * delegates to `applyANToFixedPoint`; AN's own mutations re-fire
383
+ * `setOnMutate`, which would otherwise recurse. This guard
384
+ * breaks the recursion.
385
+ *
386
+ * @internal
387
+ * @since 1.0.0
388
+ */
389
+ beginApplyAN() {
390
+ if (this.applyingAN)
391
+ return false;
392
+ this.applyingAN = true;
393
+ return true;
394
+ }
395
+ /**
396
+ * Release the AN re-entrance guard. Pairs with `beginApplyAN()`.
397
+ *
398
+ * @internal
399
+ * @since 1.0.0
400
+ */
401
+ endApplyAN() {
402
+ this.applyingAN = false;
403
+ }
298
404
  getArgument() {
299
405
  this.flushChecksums();
300
406
  return {
@@ -429,7 +535,6 @@ export class ArgumentEngine {
429
535
  }, {
430
536
  checksumConfig: this.checksumConfig,
431
537
  positionConfig: this.positionConfig,
432
- grammarConfig: this.grammarConfig,
433
538
  generateId: this.generateId,
434
539
  });
435
540
  this.premises.set(id, pm);
@@ -441,6 +546,16 @@ export class ArgumentEngine {
441
546
  this.markDirty();
442
547
  this.reactiveDirty.premiseIds.add(id);
443
548
  this.notifySubscribers();
549
+ // D2b — AN post-mutation hook per spec §5. Skipped
550
+ // during snapshot restoration (PE.fromSnapshot bypasses
551
+ // mutations anyway, but the guard is defensive).
552
+ // `runAssistiveNormalization` is a no-op in permissive
553
+ // mode; re-entrance from AN's own mutations is guarded
554
+ // inside `applyANToFixedPoint` via the engine's
555
+ // `_beginApplyAN()` flag.
556
+ if (!this.restoringFromSnapshot) {
557
+ runAssistiveNormalization(this);
558
+ }
444
559
  });
445
560
  const collector = new ChangeCollector();
446
561
  collector.addedPremise(pm.toPremiseData());
@@ -813,6 +928,18 @@ export class ArgumentEngine {
813
928
  getVariable(variableId) {
814
929
  return this.variables.getVariable(variableId);
815
930
  }
931
+ /**
932
+ * Look up a claim by `(id, version)` in the engine's claim library.
933
+ * Returns `undefined` if the claim is not present. Exposed for
934
+ * repair primitives and other tooling that needs to inspect a
935
+ * claim's `type` discriminator at a particular version pinned by
936
+ * a claim-bound variable.
937
+ *
938
+ * @since 1.0.0
939
+ */
940
+ getClaim(claimId, claimVersion) {
941
+ return this.claimLibrary.get(claimId, claimVersion);
942
+ }
816
943
  hasVariable(variableId) {
817
944
  return this.variables.hasVariable(variableId);
818
945
  }
@@ -882,27 +1009,144 @@ export class ArgumentEngine {
882
1009
  return roots;
883
1010
  }
884
1011
  /**
885
- * Normalizes expression trees across all premises. Collapses unjustified
886
- * formulas, operators with 0/1 children, and inserts formula buffers where
887
- * needed. Works regardless of `autoNormalize` setting.
1012
+ * Construct (or no-op on) the per-claim derivation premise's
1013
+ * antecedent from a citation lookup. Factory + naked-Q-only:
1014
+ *
1015
+ * - 0 connections → no-op (naked-Q stays).
1016
+ * - 1 connection → `IMPLIES(citation-var, Q)`.
1017
+ * - ≥ 2 connections → `IMPLIES(OR(c1, …, cn), Q)`. In
1018
+ * `'assistive'` mode the per-mutation AN-1 post-hook inserts a
1019
+ * formula buffer between IMPLIES and OR; in `'permissive'` the
1020
+ * OR sits directly under IMPLIES (a P-1 violation surfaces via
1021
+ * `validate('presentable')`).
1022
+ *
1023
+ * **No throw on already-populated.** Per the Structural-only
1024
+ * mutation throw rule, if the target derivation premise is not in
1025
+ * the naked-Q form the factory returns `{ kind: 'no-op', state:
1026
+ * <existing> }` without mutating. UI/caller is responsible for
1027
+ * explicit user consent + clearing the antecedent via a repair
1028
+ * primitive before re-calling. Preserves the no-changes-without-
1029
+ * consent principle.
1030
+ *
1031
+ * Throws only when no derivation premise exists for the given
1032
+ * `derivedClaimId` (legitimate entity-not-found Structural check).
1033
+ *
1034
+ * @since 1.0.0
888
1035
  */
889
- normalizeAllExpressions() {
890
- const merged = {};
891
- for (const pe of this.premises.values()) {
892
- const { changes } = pe.normalizeExpressions();
893
- if (changes.expressions) {
894
- merged.expressions ??= { added: [], modified: [], removed: [] };
895
- merged.expressions.added.push(...changes.expressions.added);
896
- merged.expressions.modified.push(...changes.expressions.modified);
897
- merged.expressions.removed.push(...changes.expressions.removed);
898
- }
899
- if (changes.premises) {
900
- merged.premises ??= { added: [], modified: [], removed: [] };
901
- merged.premises.modified.push(...changes.premises.modified);
902
- }
903
- }
904
- return { result: undefined, changes: merged };
1036
+ populateFromCitations(derivedClaimId, citationLookup) {
1037
+ return populateFromGroundingImpl(this, derivedClaimId, citationLookup);
1038
+ }
1039
+ /**
1040
+ * Mirror of `populateFromCitations` for axiom connections. Same
1041
+ * factory contract: naked-Q-only, no throw on already-populated.
1042
+ *
1043
+ * @since 1.0.0
1044
+ */
1045
+ populateFromAxioms(derivedClaimId, axiomLookup) {
1046
+ return populateFromGroundingImpl(this, derivedClaimId, axiomLookup);
1047
+ }
1048
+ /**
1049
+ * Repair primitive: resolve E-3 violations by deleting each
1050
+ * unresolvable claim- or premise-bound variable, cascading the
1051
+ * removal across all premises. Returns the violations resolved
1052
+ * (for UX confirmation / undo / "we made N changes" feedback).
1053
+ *
1054
+ * **User-initiated; never auto-runs.** Respects `behavior`: in
1055
+ * `'assistive'` mode, the AN post-hook fires after each cascade
1056
+ * mutation; in `'permissive'` no AN runs.
1057
+ *
1058
+ * @since 1.0.0
1059
+ */
1060
+ removeUnresolvableVariables() {
1061
+ return removeUnresolvableVariablesImpl(this);
1062
+ }
1063
+ /**
1064
+ * Repair primitive: resolve E-1 violations (operators with < 2
1065
+ * children) by running the AN-3 cleanup pass globally. Returns the
1066
+ * violations resolved. The repair is non-meaning-changing — it
1067
+ * only removes empty operators and promotes single-child operators
1068
+ * — but lives alongside `normalize()` so the UI can present a
1069
+ * focused "Remove N orphan operators" action with a precise return
1070
+ * value.
1071
+ *
1072
+ * **User-initiated; never auto-runs.** Bypasses `behavior` —
1073
+ * cleanup runs even in permissive mode (the user has already
1074
+ * accepted the action by clicking the repair button).
1075
+ *
1076
+ * @since 1.0.0
1077
+ */
1078
+ removeOrphanOperators() {
1079
+ return removeOrphanOperatorsImpl(this);
1080
+ }
1081
+ /**
1082
+ * Repair primitive: resolve E-6 violations (claim has > 1
1083
+ * derivation premise) by keeping one premise per `derivedClaimId`
1084
+ * and deleting the rest. Strategy controls which premise is kept:
1085
+ *
1086
+ * - `'keep-first'` (default): keep the premise with the
1087
+ * lexicographically smallest id; delete the rest. Deterministic
1088
+ * and snapshot-stable.
1089
+ * - `'keep-largest-antecedent'`: keep the premise whose antecedent
1090
+ * subtree has the most claim-bound variable expressions; tie-break
1091
+ * by id.
1092
+ *
1093
+ * **User-initiated; never auto-runs.** Respects `behavior`.
1094
+ *
1095
+ * @since 1.0.0
1096
+ */
1097
+ removeDuplicateDerivationPremises(strategy = "keep-first") {
1098
+ return removeDuplicateDerivationPremisesImpl(this, strategy);
1099
+ }
1100
+ /**
1101
+ * Repair primitive: resolve D-3 violations (mixed-grounding
1102
+ * antecedent — axioms + citations in one derivation) by deleting
1103
+ * every axiom-bound variable expression from the offending
1104
+ * antecedent subtree. The remaining citation-bound variables stay,
1105
+ * giving the derivation a homogeneous citation-grounded antecedent.
1106
+ *
1107
+ * **User-initiated; never auto-runs.** Respects `behavior`. In
1108
+ * `'assistive'` mode, AN may collapse a resulting single-child OR
1109
+ * via AN-3; in `'permissive'` the OR may persist with one child
1110
+ * (a downstream D-2 violation — follow up with
1111
+ * `removeOrphanOperators()` if desired).
1112
+ *
1113
+ * @since 1.0.0
1114
+ */
1115
+ dropAxiomsFromMixedAntecedent() {
1116
+ return dropAxiomsFromMixedAntecedentImpl(this);
905
1117
  }
1118
+ /**
1119
+ * Global normalize pass per spec §6. Runs the AN rule set
1120
+ * (AN-1..AN-4) everywhere it can fire, converging the argument
1121
+ * toward `tier` (defaults to `'presentable'`).
1122
+ *
1123
+ * `normalize` is non-destructive in the logical-meaning sense — it
1124
+ * does not delete variables, change claim references, or modify
1125
+ * operator semantics. Recovery from Evaluable or Derivable violations
1126
+ * requires user intent and is exposed via the repair primitives
1127
+ * (Phase C4).
1128
+ *
1129
+ * In v1.0 every AN rule targets a Presentable invariant, so calls
1130
+ * with `tier` ∈ {'structural', 'evaluable', 'derivable'} are
1131
+ * effectively no-ops. The parameter exists as forward-compatible
1132
+ * API surface for a future submit/finalize gate.
1133
+ *
1134
+ * **Bypasses `behavior`.** `normalize()` is user-initiated (the UI
1135
+ * invokes it after the user confirms a Tidy / Normalize action), so
1136
+ * cleanup runs regardless of whether the engine is in `'assistive'`
1137
+ * or `'permissive'` mode. The engine's `behavior` setting is not
1138
+ * mutated by this call.
1139
+ *
1140
+ * @since 1.0.0
1141
+ */
1142
+ normalize(tier = "presentable") {
1143
+ normalizeArgument(this, tier);
1144
+ }
1145
+ // D2 — `normalizeAllExpressions` was the per-engine wrapper that
1146
+ // delegated to `pe.normalizeExpressions()` on every premise.
1147
+ // Both methods are deleted in D2. Callers migrate to
1148
+ // `engine.normalize(tier?)` (Phase C3), which routes through the
1149
+ // four native AN passes in `src/lib/grammar/an-rules.ts`.
906
1150
  getRoleState() {
907
1151
  return {
908
1152
  ...(this.conclusionPremiseId !== undefined
@@ -966,12 +1210,23 @@ export class ArgumentEngine {
966
1210
  config: {
967
1211
  checksumConfig: serializeChecksumConfig(this.checksumConfig),
968
1212
  positionConfig: this.positionConfig,
969
- grammarConfig: this.grammarConfig,
1213
+ // `behavior` is intentionally omitted from the snapshot.
1214
+ // Consumers re-supply it at restore time via
1215
+ // `new ArgumentEngine(...)` options or `setBehavior()`;
1216
+ // a restored engine defaults to `'assistive'`. The fork
1217
+ // path (`forkArgumentEngine` / `PropositCore.forkArgument`)
1218
+ // explicitly threads the source engine's `behavior` into
1219
+ // the forked engine's config (see D5 — `fork.ts`), so
1220
+ // fork callers don't lose the setting.
1221
+ //
1222
+ // D2: the legacy `grammarConfig` field is gone — all
1223
+ // P-1 / AN behavior is driven by `engine.behavior` +
1224
+ // the AN post-mutation hook.
970
1225
  },
971
1226
  };
972
1227
  }
973
1228
  /** Creates a new ArgumentEngine from a previously captured snapshot. */
974
- static fromSnapshot(snapshot, claimLibrary, grammarConfig, checksumVerification, generateId) {
1229
+ static fromSnapshot(snapshot, claimLibrary, checksumVerification, generateId) {
975
1230
  const engine = new ArgumentEngine(snapshot.argument, claimLibrary, snapshot.config
976
1231
  ? {
977
1232
  ...snapshot.config,
@@ -984,7 +1239,7 @@ export class ArgumentEngine {
984
1239
  engine.restoringFromSnapshot = true;
985
1240
  // Restore premises first (premise-bound variables reference them)
986
1241
  for (const premiseSnap of snapshot.premises) {
987
- const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex, grammarConfig, generateId);
1242
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex, generateId);
988
1243
  engine.premises.set(pe.getId(), pe);
989
1244
  engine.wireCircularityCheck(pe);
990
1245
  engine.wireEmptyBoundPremiseCheck(pe);
@@ -995,6 +1250,13 @@ export class ArgumentEngine {
995
1250
  engine.markDirty();
996
1251
  engine.reactiveDirty.premiseIds.add(premiseId);
997
1252
  engine.notifySubscribers();
1253
+ // D2b — AN post-mutation hook per spec §5. See the
1254
+ // matching comment in `createPremise`'s setOnMutate
1255
+ // callback for the re-entrance / snapshot-restore
1256
+ // rationale.
1257
+ if (!engine.restoringFromSnapshot) {
1258
+ runAssistiveNormalization(engine);
1259
+ }
998
1260
  });
999
1261
  }
1000
1262
  // Restore claim-bound variables first, then premise-bound variables
@@ -1017,31 +1279,25 @@ export class ArgumentEngine {
1017
1279
  // Restore conclusion role (don't use setConclusionPremise to avoid auto-assign logic)
1018
1280
  engine.conclusionPremiseId = snapshot.conclusionPremiseId;
1019
1281
  engine.restoringFromSnapshot = false;
1020
- // Apply the caller's grammarConfig override to the engine and all
1021
- // premise engines so that validate() and subsequent mutations use the
1022
- // caller's grammar rules instead of whatever was stored in the snapshot.
1023
- if (grammarConfig) {
1024
- engine.grammarConfig = grammarConfig;
1025
- for (const pe of engine.premises.values()) {
1026
- pe.setGrammarConfig(grammarConfig);
1027
- }
1028
- }
1029
- // Post-load normalization: only run full normalize when autoNormalize
1030
- // is `true` (boolean). When it is a granular config object, individual
1031
- // flags control in-operation behavior — loading should not mutate data.
1032
- const restoredGrammarConfig = grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
1033
- if (restoredGrammarConfig.autoNormalize === true) {
1034
- for (const pe of engine.premises.values()) {
1035
- pe.normalizeExpressions();
1036
- }
1037
- }
1282
+ // C7: No post-load normalization. The snapshot loads as-is; any
1283
+ // lower-tier (Evaluable / Derivable / Presentable) violations
1284
+ // are queryable post-load via `engine.validate(tier)`.
1038
1285
  if (checksumVerification === "strict") {
1039
1286
  engine.flushChecksums();
1040
1287
  ArgumentEngine.verifySnapshotChecksums(engine, snapshot);
1041
1288
  }
1042
- const validation = engine.validate();
1043
- if (!validation.ok) {
1044
- throw new InvariantViolationError(validation.violations);
1289
+ // D2: load-time invariant validation no longer needs the
1290
+ // PERMISSIVE swap — the legacy `EXPR_FORMULA_BETWEEN_OPERATORS_VIOLATED`
1291
+ // check was deleted alongside the rest of `grammarConfig`. P-1
1292
+ // is now surfaced via `engine.validate('presentable')`
1293
+ // post-load. Non-grammar invariants (schema conformance,
1294
+ // reference integrity, conclusion ref, circularity, etc.)
1295
+ // still throw at load time. D4 inlined the
1296
+ // `runLoadTimeValidationCore` wrapper and routes through the
1297
+ // public `validateInvariants()` method.
1298
+ const loadValidation = engine.validateInvariants();
1299
+ if (!loadValidation.ok) {
1300
+ throw new InvariantViolationError(loadValidation.violations);
1045
1301
  }
1046
1302
  return engine;
1047
1303
  }
@@ -1051,19 +1307,14 @@ export class ArgumentEngine {
1051
1307
  * `premiseId` field and loaded in BFS order (roots first, then children
1052
1308
  * of already-added nodes) to satisfy parent-existence requirements.
1053
1309
  */
1054
- static fromData(argument, claimLibrary, variables, premises, expressions, roles, config, grammarConfig, checksumVerification) {
1055
- const loadingGrammarConfig = grammarConfig ?? config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
1310
+ static fromData(argument, claimLibrary, variables, premises, expressions, roles, config, checksumVerification) {
1056
1311
  const normalizedConfig = config
1057
1312
  ? {
1058
1313
  ...config,
1059
1314
  checksumConfig: normalizeChecksumConfig(config.checksumConfig),
1060
1315
  }
1061
1316
  : undefined;
1062
- const loadingConfig = {
1063
- ...normalizedConfig,
1064
- grammarConfig: loadingGrammarConfig,
1065
- };
1066
- const engine = new ArgumentEngine(argument, claimLibrary, loadingConfig);
1317
+ const engine = new ArgumentEngine(argument, claimLibrary, normalizedConfig);
1067
1318
  engine.restoringFromSnapshot = true;
1068
1319
  // Register claim-bound variables first (no dependencies)
1069
1320
  for (const v of variables) {
@@ -1121,24 +1372,24 @@ export class ArgumentEngine {
1121
1372
  if (roles.conclusionPremiseId !== undefined) {
1122
1373
  engine.setConclusionPremise(roles.conclusionPremiseId);
1123
1374
  }
1124
- // After loading: restore the caller's intended grammar config
1125
- engine.grammarConfig = config?.grammarConfig;
1126
1375
  engine.restoringFromSnapshot = false;
1127
- // Post-load normalization: only run full normalize when autoNormalize
1128
- // is `true` (boolean). Granular config objects skip post-load normalization.
1129
- const restoredGrammarConfig = config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
1130
- if (restoredGrammarConfig.autoNormalize === true) {
1131
- for (const pe of engine.premises.values()) {
1132
- pe.normalizeExpressions();
1133
- }
1134
- }
1376
+ // C7: No post-load normalization. See the matched note in
1377
+ // `fromSnapshot` above. Load is non-mutating; lower-tier
1378
+ // violations surface via `engine.validate(tier)`. Phase D
1379
+ // removes the legacy grammarConfig parameter entirely.
1135
1380
  if (checksumVerification === "strict") {
1136
1381
  engine.flushChecksums();
1137
1382
  ArgumentEngine.verifyDataChecksums(engine, argument, variables, premises);
1138
1383
  }
1139
- const validation = engine.validate();
1140
- if (!validation.ok) {
1141
- throw new InvariantViolationError(validation.violations);
1384
+ // C7: PERMISSIVE-gated load-time validation (see matched comment
1385
+ // in `fromSnapshot` above). Non-grammar invariants still throw at
1386
+ // load; lower-tier grammar violations surface post-load via
1387
+ // `engine.validate(tier)`. D4 inlined the
1388
+ // `runLoadTimeValidationCore` wrapper and routes through the
1389
+ // public `validateInvariants()` method.
1390
+ const loadValidation = engine.validateInvariants();
1391
+ if (!loadValidation.ok) {
1392
+ throw new InvariantViolationError(loadValidation.violations);
1142
1393
  }
1143
1394
  return engine;
1144
1395
  }
@@ -1253,7 +1504,7 @@ export class ArgumentEngine {
1253
1504
  rollback(snapshot) {
1254
1505
  const preRollbackSnap = this.snapshot();
1255
1506
  this.rollbackInternal(snapshot);
1256
- const validation = this.validate();
1507
+ const validation = this.validateInvariants();
1257
1508
  if (!validation.ok) {
1258
1509
  this.rollbackInternal(preRollbackSnap);
1259
1510
  throw new InvariantViolationError(validation.violations);
@@ -1263,12 +1514,11 @@ export class ArgumentEngine {
1263
1514
  this.argument = { ...snapshot.argument };
1264
1515
  this.checksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
1265
1516
  this.positionConfig = snapshot.config?.positionConfig;
1266
- this.grammarConfig = snapshot.config?.grammarConfig;
1267
1517
  this.variables = VariableManager.fromSnapshot(snapshot.variables);
1268
1518
  this.premises = new Map();
1269
1519
  this.expressionIndex = new Map();
1270
1520
  for (const premiseSnap of snapshot.premises) {
1271
- const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex, PERMISSIVE_GRAMMAR_CONFIG);
1521
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex);
1272
1522
  this.premises.set(pe.getId(), pe);
1273
1523
  }
1274
1524
  this.conclusionPremiseId = snapshot.conclusionPremiseId;
@@ -1282,6 +1532,13 @@ export class ArgumentEngine {
1282
1532
  this.markDirty();
1283
1533
  this.reactiveDirty.premiseIds.add(premiseId);
1284
1534
  this.notifySubscribers();
1535
+ // D2b — AN post-mutation hook per spec §5. See the
1536
+ // matching comment in `createPremise`'s setOnMutate
1537
+ // callback for the re-entrance / snapshot-restore
1538
+ // rationale.
1539
+ if (!this.restoringFromSnapshot) {
1540
+ runAssistiveNormalization(this);
1541
+ }
1285
1542
  });
1286
1543
  }
1287
1544
  this.markDirty();
@@ -1405,9 +1662,85 @@ export class ArgumentEngine {
1405
1662
  validateAfterPremiseMutation() {
1406
1663
  return validateAfterPremiseMutationStandalone(this.asValidationContext());
1407
1664
  }
1408
- validate() {
1665
+ /**
1666
+ * Four-tier grammar validation per spec §4. Returns the union of
1667
+ * violations from Structural up through `tier` — `'structural'`
1668
+ * returns S-rule violations only, `'evaluable'` returns S + E,
1669
+ * `'derivable'` returns S + E + D, `'presentable'` returns the full
1670
+ * union. Empty array means the argument is at the requested tier
1671
+ * or stricter. Never throws on grammar issues.
1672
+ *
1673
+ * For the legacy pre-1.0 invariant sweep (schema conformance,
1674
+ * reference integrity, ownership, conclusion ref, circularity,
1675
+ * checksums) use {@link validateInvariants} instead. The pre-1.0
1676
+ * no-arg overload of `validate()` was removed in Phase D4.
1677
+ */
1678
+ validate(tier) {
1679
+ return validateGrammar(tier, this.asGrammarValidatorContext());
1680
+ }
1681
+ /**
1682
+ * Legacy invariant sweep — schema conformance, reference integrity,
1683
+ * ownership, conclusion-ref + circularity, checksum stability, and
1684
+ * per-premise validation. Returns a `TInvariantValidationResult`.
1685
+ * Used internally by mutation-rollback and snapshot-load paths and
1686
+ * exposed publicly for library-wide invariant checks (see
1687
+ * `ArgumentLibrary.validate` and `PropositCore.validate`).
1688
+ *
1689
+ * Distinct from {@link validate}, which runs the four-tier grammar
1690
+ * validator (`Structural ⊇ Evaluable ⊇ Derivable ⊇ Presentable`)
1691
+ * and returns a `readonly TViolation[]`. The two are
1692
+ * complementary — grammar tiers cover AST-shape rules; this method
1693
+ * covers schema/reference/structural-bookkeeping invariants that
1694
+ * sit outside the tier hierarchy.
1695
+ *
1696
+ * @since 1.0.0 — replaces the legacy `validate()` no-arg overload
1697
+ * removed in Phase D4 of the `grammar-tiers/core` branch.
1698
+ */
1699
+ validateInvariants() {
1409
1700
  return validateArgumentStandalone(this.asValidationContext());
1410
1701
  }
1702
+ /**
1703
+ * Construct the pure-data `TValidatorContext` consumed by the
1704
+ * grammar-tier validators. Claims are gathered by walking the
1705
+ * engine's claim-bound variables and looking each one up in the
1706
+ * claim library — the `TClaimLookup` contract doesn't expose
1707
+ * iteration, so we materialize the referenced subset only.
1708
+ */
1709
+ asGrammarValidatorContext() {
1710
+ const argument = this.getArgument();
1711
+ const premises = [];
1712
+ const expressions = [];
1713
+ for (const pe of this.listPremises()) {
1714
+ premises.push(pe.toPremiseData());
1715
+ expressions.push(...pe.getExpressions());
1716
+ }
1717
+ const variables = this.variables.toArray();
1718
+ // Gather referenced claims via claim-bound variables. Duplicate
1719
+ // (id, version) pairs are deduped via a Set on the composite key.
1720
+ const seen = new Set();
1721
+ const claims = [];
1722
+ for (const v of variables) {
1723
+ if (!isClaimBound(v))
1724
+ continue;
1725
+ const cb = v;
1726
+ const key = `${cb.claimId}:${cb.claimVersion}`;
1727
+ if (seen.has(key))
1728
+ continue;
1729
+ seen.add(key);
1730
+ const claim = this.claimLibrary.get(cb.claimId, cb.claimVersion);
1731
+ if (claim !== undefined) {
1732
+ claims.push(claim);
1733
+ }
1734
+ }
1735
+ return {
1736
+ argument,
1737
+ premises,
1738
+ expressions,
1739
+ variables,
1740
+ claims,
1741
+ roleState: this.getRoleState(),
1742
+ };
1743
+ }
1411
1744
  validateEvaluability() {
1412
1745
  const base = validateArgumentEvaluabilityStandalone(this.asValidationContext());
1413
1746
  const derivationIssues = this.collectDerivationStructureIssues();
@@ -1420,15 +1753,19 @@ export class ArgumentEngine {
1420
1753
  * Apps can pre-check derivation premise structures before invoking the full
1421
1754
  * evaluation pipeline.
1422
1755
  *
1756
+ * Violations carry the underlying `DERIVATION_STRUCTURE_INVALID` code
1757
+ * (per the derivation-validation utility). The pre-1.0
1758
+ * `DERIVATION_STRUCTURE_INVALID_AT_EVALUATION` override was removed in
1759
+ * Phase D4 alongside the legacy `validate()` no-arg overload — naked-Q
1760
+ * is a valid Derivable state (per spec §4.2) and is skipped by
1761
+ * evaluation rather than thrown.
1762
+ *
1423
1763
  * @since 0.11.0
1424
1764
  */
1425
1765
  validateDerivationStructures() {
1426
1766
  const violations = [];
1427
1767
  for (const { violation } of this.collectDerivationViolations()) {
1428
- violations.push({
1429
- ...violation,
1430
- code: DERIVATION_STRUCTURE_INVALID_AT_EVALUATION,
1431
- });
1768
+ violations.push(violation);
1432
1769
  }
1433
1770
  return { ok: violations.length === 0, violations };
1434
1771
  }
@@ -1436,7 +1773,7 @@ export class ArgumentEngine {
1436
1773
  const issues = [];
1437
1774
  for (const { violation } of this.collectDerivationViolations()) {
1438
1775
  issues.push(makeErrorIssue({
1439
- code: DERIVATION_STRUCTURE_INVALID_AT_EVALUATION,
1776
+ code: "DERIVATION_STRUCTURE_INVALID",
1440
1777
  message: violation.message,
1441
1778
  premiseId: violation.entityId,
1442
1779
  }));
@@ -1486,14 +1823,37 @@ export class ArgumentEngine {
1486
1823
  };
1487
1824
  }
1488
1825
  asEvaluationContext() {
1826
+ // C8: naked-Q derivation premises (single variable expression at
1827
+ // root, type='derivation') contribute nothing to evaluation. The
1828
+ // evaluator-context's premise listings filter them out so they
1829
+ // are entirely invisible to evaluate() and checkValidity(). This
1830
+ // replaces the pre-1.0 DERIVATION_STRUCTURE_INVALID_AT_EVALUATION
1831
+ // throw on naked-Q. Filter applies uniformly to conclusion,
1832
+ // supporting, and full premise listings. The predicate lives in
1833
+ // `src/lib/grammar/naked-q.ts` so the C6 factory and this filter
1834
+ // share one definition.
1489
1835
  return {
1490
1836
  argumentId: this.argument.id,
1491
1837
  conclusionPremiseId: this.conclusionPremiseId,
1492
- getConclusionPremise: () => this.getConclusionPremise(),
1493
- listSupportingPremises: () => this.listSupportingPremises(),
1494
- listPremises: () => this.listPremises(),
1838
+ getConclusionPremise: () => {
1839
+ const c = this.getConclusionPremise();
1840
+ if (c === undefined)
1841
+ return undefined;
1842
+ if (isNakedQDerivationPremise(c))
1843
+ return undefined;
1844
+ return c;
1845
+ },
1846
+ listSupportingPremises: () => this.listSupportingPremises().filter((pm) => !isNakedQDerivationPremise(pm)),
1847
+ listPremises: () => this.listPremises().filter((pm) => !isNakedQDerivationPremise(pm)),
1495
1848
  getVariable: (id) => this.variables.getVariable(id),
1496
- getPremise: (id) => this.premises.get(id),
1849
+ getPremise: (id) => {
1850
+ const pe = this.premises.get(id);
1851
+ if (pe === undefined)
1852
+ return undefined;
1853
+ if (isNakedQDerivationPremise(pe))
1854
+ return undefined;
1855
+ return pe;
1856
+ },
1497
1857
  validateEvaluability: () => this.validateEvaluability(),
1498
1858
  };
1499
1859
  }