@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.
Files changed (81) hide show
  1. package/README.md +194 -0
  2. package/dist/extensions/basics/schemata.d.ts +5 -0
  3. package/dist/extensions/basics/schemata.d.ts.map +1 -1
  4. package/dist/lib/consts.d.ts.map +1 -1
  5. package/dist/lib/consts.js +21 -2
  6. package/dist/lib/consts.js.map +1 -1
  7. package/dist/lib/core/argument-engine.d.ts +51 -2
  8. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  9. package/dist/lib/core/argument-engine.js +764 -227
  10. package/dist/lib/core/argument-engine.js.map +1 -1
  11. package/dist/lib/core/change-collector.d.ts +1 -0
  12. package/dist/lib/core/change-collector.d.ts.map +1 -1
  13. package/dist/lib/core/change-collector.js +3 -0
  14. package/dist/lib/core/change-collector.js.map +1 -1
  15. package/dist/lib/core/claim-library.d.ts +4 -0
  16. package/dist/lib/core/claim-library.d.ts.map +1 -1
  17. package/dist/lib/core/claim-library.js +126 -59
  18. package/dist/lib/core/claim-library.js.map +1 -1
  19. package/dist/lib/core/claim-source-library.d.ts +4 -0
  20. package/dist/lib/core/claim-source-library.d.ts.map +1 -1
  21. package/dist/lib/core/claim-source-library.js +114 -38
  22. package/dist/lib/core/claim-source-library.js.map +1 -1
  23. package/dist/lib/core/diff.d.ts +10 -0
  24. package/dist/lib/core/diff.d.ts.map +1 -1
  25. package/dist/lib/core/diff.js +114 -21
  26. package/dist/lib/core/diff.js.map +1 -1
  27. package/dist/lib/core/expression-manager.d.ts +11 -0
  28. package/dist/lib/core/expression-manager.d.ts.map +1 -1
  29. package/dist/lib/core/expression-manager.js +379 -20
  30. package/dist/lib/core/expression-manager.js.map +1 -1
  31. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +9 -2
  32. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  33. package/dist/lib/core/interfaces/library.interfaces.d.ts +19 -0
  34. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  35. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +22 -0
  36. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  37. package/dist/lib/core/invariant-violation-error.d.ts +6 -0
  38. package/dist/lib/core/invariant-violation-error.d.ts.map +1 -0
  39. package/dist/lib/core/invariant-violation-error.js +12 -0
  40. package/dist/lib/core/invariant-violation-error.js.map +1 -0
  41. package/dist/lib/core/parser/formula.d.ts.map +1 -1
  42. package/dist/lib/core/parser/formula.js +2 -2
  43. package/dist/lib/core/parser/formula.js.map +1 -1
  44. package/dist/lib/core/premise-engine.d.ts +10 -0
  45. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  46. package/dist/lib/core/premise-engine.js +699 -536
  47. package/dist/lib/core/premise-engine.js.map +1 -1
  48. package/dist/lib/core/source-library.d.ts +4 -0
  49. package/dist/lib/core/source-library.d.ts.map +1 -1
  50. package/dist/lib/core/source-library.js +126 -59
  51. package/dist/lib/core/source-library.js.map +1 -1
  52. package/dist/lib/core/variable-manager.d.ts +7 -0
  53. package/dist/lib/core/variable-manager.d.ts.map +1 -1
  54. package/dist/lib/core/variable-manager.js +65 -1
  55. package/dist/lib/core/variable-manager.js.map +1 -1
  56. package/dist/lib/index.d.ts +4 -1
  57. package/dist/lib/index.d.ts.map +1 -1
  58. package/dist/lib/index.js +4 -1
  59. package/dist/lib/index.js.map +1 -1
  60. package/dist/lib/schemata/argument.d.ts +2 -0
  61. package/dist/lib/schemata/argument.d.ts.map +1 -1
  62. package/dist/lib/schemata/argument.js +6 -0
  63. package/dist/lib/schemata/argument.js.map +1 -1
  64. package/dist/lib/schemata/propositional.d.ts +41 -0
  65. package/dist/lib/schemata/propositional.d.ts.map +1 -1
  66. package/dist/lib/schemata/propositional.js +34 -0
  67. package/dist/lib/schemata/propositional.js.map +1 -1
  68. package/dist/lib/types/diff.d.ts +6 -0
  69. package/dist/lib/types/diff.d.ts.map +1 -1
  70. package/dist/lib/types/fork.d.ts +32 -0
  71. package/dist/lib/types/fork.d.ts.map +1 -0
  72. package/dist/lib/types/fork.js +2 -0
  73. package/dist/lib/types/fork.js.map +1 -0
  74. package/dist/lib/types/grammar.d.ts +5 -4
  75. package/dist/lib/types/grammar.d.ts.map +1 -1
  76. package/dist/lib/types/grammar.js.map +1 -1
  77. package/dist/lib/types/validation.d.ts +46 -0
  78. package/dist/lib/types/validation.d.ts.map +1 -0
  79. package/dist/lib/types/validation.js +41 -0
  80. package/dist/lib/types/validation.js.map +1 -0
  81. package/package.json +1 -1
@@ -1,12 +1,15 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { isClaimBound, isPremiseBound, } from "../schemata/index.js";
3
- import { PERMISSIVE_GRAMMAR_CONFIG, } from "../types/grammar.js";
2
+ import { Value } from "typebox/value";
3
+ import { CoreArgumentSchema, isClaimBound, isPremiseBound, } from "../schemata/index.js";
4
+ import { DEFAULT_GRAMMAR_CONFIG, PERMISSIVE_GRAMMAR_CONFIG, } from "../types/grammar.js";
5
+ import { ARG_SCHEMA_INVALID, ARG_OWNERSHIP_MISMATCH, ARG_CLAIM_REF_NOT_FOUND, ARG_PREMISE_REF_NOT_FOUND, ARG_CIRCULARITY_DETECTED, ARG_CONCLUSION_NOT_FOUND, ARG_CHECKSUM_MISMATCH, } from "../types/validation.js";
4
6
  import { DEFAULT_CHECKSUM_CONFIG, normalizeChecksumConfig, serializeChecksumConfig, } from "../consts.js";
5
7
  import { getOrCreate, sortedUnique } from "../utils/collections.js";
6
8
  import { ChangeCollector } from "./change-collector.js";
7
9
  import { canonicalSerialize, computeHash, entityChecksum } from "./checksum.js";
8
10
  import { kleeneAnd, kleeneNot } from "./evaluation/kleene.js";
9
11
  import { makeErrorIssue, makeValidationResult, } from "./evaluation/validation.js";
12
+ import { InvariantViolationError } from "./invariant-violation-error.js";
10
13
  import { PremiseEngine } from "./premise-engine.js";
11
14
  import { VariableManager } from "./variable-manager.js";
12
15
  /**
@@ -27,6 +30,7 @@ export class ArgumentEngine {
27
30
  checksumConfig;
28
31
  positionConfig;
29
32
  grammarConfig;
33
+ restoringFromSnapshot = false;
30
34
  checksumDirty = true;
31
35
  cachedMetaChecksum;
32
36
  cachedDescendantChecksum;
@@ -104,6 +108,15 @@ export class ArgumentEngine {
104
108
  return !boundPremise?.getRootExpressionId();
105
109
  });
106
110
  }
111
+ generateUniqueSymbol() {
112
+ let n = this.premises.size - 1;
113
+ let candidate = `P${n}`;
114
+ while (this.variables.getVariableBySymbol(candidate) !== undefined) {
115
+ n++;
116
+ candidate = `P${n}`;
117
+ }
118
+ return candidate;
119
+ }
107
120
  subscribe = (listener) => {
108
121
  this.listeners.add(listener);
109
122
  return () => {
@@ -115,6 +128,47 @@ export class ArgumentEngine {
115
128
  listener();
116
129
  }
117
130
  }
131
+ static skipValidationResult = {
132
+ ok: true,
133
+ violations: [],
134
+ };
135
+ suppressPremiseValidation() {
136
+ for (const pe of this.premises.values()) {
137
+ pe.setArgumentValidateCallback(() => ArgumentEngine.skipValidationResult);
138
+ }
139
+ }
140
+ restorePremiseValidation() {
141
+ for (const pe of this.premises.values()) {
142
+ pe.setArgumentValidateCallback(() => this.validateAfterPremiseMutation());
143
+ }
144
+ }
145
+ withValidation(fn) {
146
+ if (this.restoringFromSnapshot) {
147
+ return fn();
148
+ }
149
+ const snap = this.snapshot();
150
+ // Suppress PremiseEngine-level validation during ArgumentEngine
151
+ // mutations. The ArgumentEngine will do its own validation at the end.
152
+ this.suppressPremiseValidation();
153
+ try {
154
+ const result = fn();
155
+ const validation = this.validate();
156
+ if (!validation.ok) {
157
+ this.rollbackInternal(snap);
158
+ throw new InvariantViolationError(validation.violations);
159
+ }
160
+ return result;
161
+ }
162
+ catch (e) {
163
+ if (!(e instanceof InvariantViolationError)) {
164
+ this.rollbackInternal(snap);
165
+ }
166
+ throw e;
167
+ }
168
+ finally {
169
+ this.restorePremiseValidation();
170
+ }
171
+ }
118
172
  getSnapshot = () => {
119
173
  return this.buildReactiveSnapshot();
120
174
  };
@@ -265,90 +319,115 @@ export class ArgumentEngine {
265
319
  }
266
320
  return lines.join("\n");
267
321
  }
268
- createPremise(extras) {
269
- return this.createPremiseWithId(randomUUID(), extras);
322
+ createPremise(extras, symbol) {
323
+ return this.createPremiseWithId(randomUUID(), extras, symbol);
270
324
  }
271
- createPremiseWithId(id, extras) {
272
- if (this.premises.has(id)) {
273
- throw new Error(`Premise "${id}" already exists.`);
274
- }
275
- const premiseData = {
276
- ...extras,
277
- id,
278
- argumentId: this.argument.id,
279
- argumentVersion: this.argument.version,
280
- };
281
- const pm = new PremiseEngine(premiseData, {
282
- argument: this.argument,
283
- variables: this.variables,
284
- expressionIndex: this.expressionIndex,
285
- }, {
286
- checksumConfig: this.checksumConfig,
287
- positionConfig: this.positionConfig,
288
- grammarConfig: this.grammarConfig,
289
- });
290
- this.premises.set(id, pm);
291
- this.wireCircularityCheck(pm);
292
- this.wireEmptyBoundPremiseCheck(pm);
293
- pm.setOnMutate(() => {
325
+ createPremiseWithId(id, extras, symbol) {
326
+ return this.withValidation(() => {
327
+ if (this.premises.has(id)) {
328
+ throw new Error(`Premise "${id}" already exists.`);
329
+ }
330
+ const premiseData = {
331
+ ...extras,
332
+ id,
333
+ argumentId: this.argument.id,
334
+ argumentVersion: this.argument.version,
335
+ };
336
+ const pm = new PremiseEngine(premiseData, {
337
+ argument: this.argument,
338
+ variables: this.variables,
339
+ expressionIndex: this.expressionIndex,
340
+ }, {
341
+ checksumConfig: this.checksumConfig,
342
+ positionConfig: this.positionConfig,
343
+ grammarConfig: this.grammarConfig,
344
+ });
345
+ this.premises.set(id, pm);
346
+ this.wireCircularityCheck(pm);
347
+ this.wireEmptyBoundPremiseCheck(pm);
348
+ pm.setVariableIdsCallback(() => new Set(this.variables.toArray().map((v) => v.id)));
349
+ pm.setArgumentValidateCallback(() => this.validateAfterPremiseMutation());
350
+ pm.setOnMutate(() => {
351
+ this.markDirty();
352
+ this.reactiveDirty.premiseIds.add(id);
353
+ this.notifySubscribers();
354
+ });
355
+ const collector = new ChangeCollector();
356
+ collector.addedPremise(pm.toPremiseData());
294
357
  this.markDirty();
295
- this.reactiveDirty.premiseIds.add(id);
358
+ if (this.conclusionPremiseId === undefined) {
359
+ this.conclusionPremiseId = id;
360
+ collector.setRoles(this.getRoleState());
361
+ }
362
+ // Auto-create a premise-bound variable for this premise
363
+ if (!this.restoringFromSnapshot) {
364
+ const autoSymbol = symbol ?? this.generateUniqueSymbol();
365
+ const autoVariable = {
366
+ id: randomUUID(),
367
+ argumentId: this.argument.id,
368
+ argumentVersion: this.argument.version,
369
+ symbol: autoSymbol,
370
+ boundPremiseId: id,
371
+ boundArgumentId: this.argument.id,
372
+ boundArgumentVersion: this.argument.version,
373
+ };
374
+ const withChecksum = this.attachVariableChecksum({
375
+ ...autoVariable,
376
+ });
377
+ this.variables.addVariable(withChecksum);
378
+ collector.addedVariable(withChecksum);
379
+ this.markAllPremisesDirty();
380
+ }
381
+ const changes = collector.toChangeset();
382
+ this.markReactiveDirty(changes);
296
383
  this.notifySubscribers();
384
+ return {
385
+ result: pm,
386
+ changes,
387
+ };
297
388
  });
298
- const collector = new ChangeCollector();
299
- collector.addedPremise(pm.toPremiseData());
300
- this.markDirty();
301
- if (this.conclusionPremiseId === undefined) {
302
- this.conclusionPremiseId = id;
303
- collector.setRoles(this.getRoleState());
304
- }
305
- const changes = collector.toChangeset();
306
- this.markReactiveDirty(changes);
307
- this.notifySubscribers();
308
- return {
309
- result: pm,
310
- changes,
311
- };
312
389
  }
313
390
  removePremise(premiseId) {
314
- const pm = this.premises.get(premiseId);
315
- if (!pm)
316
- return { result: undefined, changes: {} };
317
- const data = pm.toPremiseData();
318
- const collector = new ChangeCollector();
319
- // Clean up expression index for removed premise's expressions
320
- for (const expr of pm.getExpressions()) {
321
- this.expressionIndex.delete(expr.id);
322
- }
323
- this.premises.delete(premiseId);
324
- collector.removedPremise(data);
325
- if (this.conclusionPremiseId === premiseId) {
326
- this.conclusionPremiseId = undefined;
327
- collector.setRoles(this.getRoleState());
328
- }
329
- // Cascade: remove variables bound to the deleted premise
330
- const boundVars = this.getVariablesBoundToPremise(premiseId);
331
- for (const v of boundVars) {
332
- const removeResult = this.removeVariable(v.id);
333
- if (removeResult.changes.variables) {
334
- for (const rv of removeResult.changes.variables.removed) {
335
- collector.removedVariable(rv);
336
- }
391
+ return this.withValidation(() => {
392
+ const pm = this.premises.get(premiseId);
393
+ if (!pm)
394
+ return { result: undefined, changes: {} };
395
+ const data = pm.toPremiseData();
396
+ const collector = new ChangeCollector();
397
+ // Clean up expression index for removed premise's expressions
398
+ for (const expr of pm.getExpressions()) {
399
+ this.expressionIndex.delete(expr.id);
337
400
  }
338
- if (removeResult.changes.expressions) {
339
- for (const re of removeResult.changes.expressions.removed) {
340
- collector.removedExpression(re);
401
+ this.premises.delete(premiseId);
402
+ collector.removedPremise(data);
403
+ if (this.conclusionPremiseId === premiseId) {
404
+ this.conclusionPremiseId = undefined;
405
+ collector.setRoles(this.getRoleState());
406
+ }
407
+ // Cascade: remove variables bound to the deleted premise
408
+ const boundVars = this.getVariablesBoundToPremise(premiseId);
409
+ for (const v of boundVars) {
410
+ const removeResult = this.removeVariableCore(v.id);
411
+ if (removeResult.changes.variables) {
412
+ for (const rv of removeResult.changes.variables.removed) {
413
+ collector.removedVariable(rv);
414
+ }
415
+ }
416
+ if (removeResult.changes.expressions) {
417
+ for (const re of removeResult.changes.expressions.removed) {
418
+ collector.removedExpression(re);
419
+ }
341
420
  }
342
421
  }
343
- }
344
- this.markDirty();
345
- const changes = collector.toChangeset();
346
- this.markReactiveDirty(changes);
347
- this.notifySubscribers();
348
- return {
349
- result: data,
350
- changes,
351
- };
422
+ this.markDirty();
423
+ const changes = collector.toChangeset();
424
+ this.markReactiveDirty(changes);
425
+ this.notifySubscribers();
426
+ return {
427
+ result: data,
428
+ changes,
429
+ };
430
+ });
352
431
  }
353
432
  getPremise(premiseId) {
354
433
  return this.premises.get(premiseId);
@@ -365,122 +444,90 @@ export class ArgumentEngine {
365
444
  .filter((pm) => pm !== undefined);
366
445
  }
367
446
  addVariable(variable) {
368
- // Only claim-bound variables may be added via addVariable.
369
- // Premise-bound variables must use bindVariableToPremise.
370
- if (!isClaimBound(variable)) {
371
- throw new Error("addVariable only accepts claim-bound variables. Use bindVariableToPremise for premise-bound variables.");
372
- }
373
- if (variable.argumentId !== this.argument.id) {
374
- throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
375
- }
376
- if (variable.argumentVersion !== this.argument.version) {
377
- throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
378
- }
379
- // Validate claim reference
380
- if (!this.claimLibrary.get(variable.claimId, variable.claimVersion)) {
381
- throw new Error(`Claim "${variable.claimId}" version ${variable.claimVersion} does not exist in the claim library.`);
382
- }
383
- const withChecksum = this.attachVariableChecksum({
384
- ...variable,
447
+ return this.withValidation(() => {
448
+ // Only claim-bound variables may be added via addVariable.
449
+ // Premise-bound variables must use bindVariableToPremise.
450
+ if (!isClaimBound(variable)) {
451
+ throw new Error("addVariable only accepts claim-bound variables. Use bindVariableToPremise for premise-bound variables.");
452
+ }
453
+ if (variable.argumentId !== this.argument.id) {
454
+ throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
455
+ }
456
+ if (variable.argumentVersion !== this.argument.version) {
457
+ throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
458
+ }
459
+ // Validate claim reference
460
+ if (!this.claimLibrary.get(variable.claimId, variable.claimVersion)) {
461
+ throw new Error(`Claim "${variable.claimId}" version ${variable.claimVersion} does not exist in the claim library.`);
462
+ }
463
+ const withChecksum = this.attachVariableChecksum({
464
+ ...variable,
465
+ });
466
+ this.variables.addVariable(withChecksum);
467
+ const collector = new ChangeCollector();
468
+ collector.addedVariable(withChecksum);
469
+ this.markDirty();
470
+ this.markAllPremisesDirty();
471
+ const changes = collector.toChangeset();
472
+ this.markReactiveDirty(changes);
473
+ this.notifySubscribers();
474
+ return {
475
+ result: withChecksum,
476
+ changes,
477
+ };
385
478
  });
386
- this.variables.addVariable(withChecksum);
387
- const collector = new ChangeCollector();
388
- collector.addedVariable(withChecksum);
389
- this.markDirty();
390
- this.markAllPremisesDirty();
391
- const changes = collector.toChangeset();
392
- this.markReactiveDirty(changes);
393
- this.notifySubscribers();
394
- return {
395
- result: withChecksum,
396
- changes,
397
- };
398
479
  }
399
480
  bindVariableToPremise(variable) {
400
- if (variable.argumentId !== this.argument.id) {
401
- throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
402
- }
403
- if (variable.argumentVersion !== this.argument.version) {
404
- throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
405
- }
406
- if (variable.boundArgumentId !== this.argument.id) {
407
- throw new Error(`Cross-argument bindings are not supported. boundArgumentId "${variable.boundArgumentId}" does not match engine argument ID "${this.argument.id}".`);
408
- }
409
- if (!this.premises.has(variable.boundPremiseId)) {
410
- throw new Error(`Bound premise "${variable.boundPremiseId}" does not exist in this argument.`);
411
- }
412
- const withChecksum = this.attachVariableChecksum({
413
- ...variable,
481
+ return this.withValidation(() => {
482
+ if (variable.argumentId !== this.argument.id) {
483
+ throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
484
+ }
485
+ if (variable.argumentVersion !== this.argument.version) {
486
+ throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
487
+ }
488
+ if (variable.boundArgumentId !== this.argument.id) {
489
+ throw new Error(`Cross-argument bindings are not supported. boundArgumentId "${variable.boundArgumentId}" does not match engine argument ID "${this.argument.id}".`);
490
+ }
491
+ if (!this.premises.has(variable.boundPremiseId)) {
492
+ throw new Error(`Bound premise "${variable.boundPremiseId}" does not exist in this argument.`);
493
+ }
494
+ const withChecksum = this.attachVariableChecksum({
495
+ ...variable,
496
+ });
497
+ this.variables.addVariable(withChecksum);
498
+ const collector = new ChangeCollector();
499
+ collector.addedVariable(withChecksum);
500
+ this.markDirty();
501
+ this.markAllPremisesDirty();
502
+ const changes = collector.toChangeset();
503
+ this.markReactiveDirty(changes);
504
+ this.notifySubscribers();
505
+ return {
506
+ result: withChecksum,
507
+ changes,
508
+ };
414
509
  });
415
- this.variables.addVariable(withChecksum);
416
- const collector = new ChangeCollector();
417
- collector.addedVariable(withChecksum);
418
- this.markDirty();
419
- this.markAllPremisesDirty();
420
- const changes = collector.toChangeset();
421
- this.markReactiveDirty(changes);
422
- this.notifySubscribers();
423
- return {
424
- result: withChecksum,
425
- changes,
426
- };
427
510
  }
428
- updateVariable(variableId, updates) {
429
- const existing = this.variables.getVariable(variableId);
430
- if (!existing) {
431
- return { result: undefined, changes: {} };
432
- }
433
- const existingVar = existing;
434
- const updatesObj = updates;
435
- // Reject binding-type conversion
436
- if (isClaimBound(existingVar)) {
437
- const premiseBoundFields = [
438
- "boundPremiseId",
439
- "boundArgumentId",
440
- "boundArgumentVersion",
441
- ];
442
- for (const f of premiseBoundFields) {
443
- if (updatesObj[f] !== undefined) {
444
- throw new Error(`Cannot set "${f}" on a claim-bound variable. Delete and re-create to change binding type.`);
445
- }
446
- }
447
- // Validate: claimId and claimVersion must be provided together
448
- const hasClaimId = updatesObj.claimId !== undefined;
449
- const hasClaimVersion = updatesObj.claimVersion !== undefined;
450
- if (hasClaimId !== hasClaimVersion) {
451
- throw new Error("claimId and claimVersion must be provided together.");
511
+ bindVariableToExternalPremise(variable) {
512
+ return this.withValidation(() => {
513
+ if (variable.argumentId !== this.argument.id) {
514
+ throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
452
515
  }
453
- // Validate claim reference if provided
454
- if (hasClaimId && hasClaimVersion) {
455
- if (!this.claimLibrary.get(updatesObj.claimId, updatesObj.claimVersion)) {
456
- throw new Error(`Claim "${String(updatesObj.claimId)}" version ${String(updatesObj.claimVersion)} does not exist in the claim library.`);
457
- }
516
+ if (variable.argumentVersion !== this.argument.version) {
517
+ throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
458
518
  }
459
- }
460
- else if (isPremiseBound(existingVar)) {
461
- const claimBoundFields = ["claimId", "claimVersion"];
462
- for (const f of claimBoundFields) {
463
- if (updatesObj[f] !== undefined) {
464
- throw new Error(`Cannot set "${f}" on a premise-bound variable. Delete and re-create to change binding type.`);
465
- }
519
+ if (variable.boundArgumentId === this.argument.id) {
520
+ throw new Error(`boundArgumentId matches this engine's argument — use bindVariableToPremise for internal bindings.`);
466
521
  }
467
- // Validate boundPremiseId if provided
468
- if (updatesObj.boundPremiseId !== undefined) {
469
- const newPremiseId = updatesObj.boundPremiseId;
470
- if (!this.premises.has(newPremiseId)) {
471
- throw new Error(`Bound premise "${newPremiseId}" does not exist in this argument.`);
472
- }
522
+ if (!this.canBind(variable.boundArgumentId, variable.boundArgumentVersion)) {
523
+ throw new Error(`Binding to argument "${variable.boundArgumentId}" version ${variable.boundArgumentVersion} is not allowed.`);
473
524
  }
474
- }
475
- const updated = this.variables.updateVariable(variableId, updates);
476
- const collector = new ChangeCollector();
477
- if (updated) {
478
- const withChecksum = this.attachVariableChecksum({ ...updated });
479
- // Re-store with updated checksum so VariableManager always holds
480
- // variables with correct checksums.
481
- this.variables.removeVariable(variableId);
525
+ const withChecksum = this.attachVariableChecksum({
526
+ ...variable,
527
+ });
482
528
  this.variables.addVariable(withChecksum);
483
- collector.modifiedVariable(withChecksum);
529
+ const collector = new ChangeCollector();
530
+ collector.addedVariable(withChecksum);
484
531
  this.markDirty();
485
532
  this.markAllPremisesDirty();
486
533
  const changes = collector.toChangeset();
@@ -490,13 +537,90 @@ export class ArgumentEngine {
490
537
  result: withChecksum,
491
538
  changes,
492
539
  };
493
- }
494
- return {
495
- result: undefined,
496
- changes: collector.toChangeset(),
497
- };
540
+ });
498
541
  }
499
- removeVariable(variableId) {
542
+ bindVariableToArgument(variable, conclusionPremiseId) {
543
+ return this.bindVariableToExternalPremise({
544
+ ...variable,
545
+ boundPremiseId: conclusionPremiseId,
546
+ });
547
+ }
548
+ updateVariable(variableId, updates) {
549
+ return this.withValidation(() => {
550
+ const existing = this.variables.getVariable(variableId);
551
+ if (!existing) {
552
+ return { result: undefined, changes: {} };
553
+ }
554
+ const existingVar = existing;
555
+ const updatesObj = updates;
556
+ // Reject binding-type conversion
557
+ if (isClaimBound(existingVar)) {
558
+ const premiseBoundFields = [
559
+ "boundPremiseId",
560
+ "boundArgumentId",
561
+ "boundArgumentVersion",
562
+ ];
563
+ for (const f of premiseBoundFields) {
564
+ if (updatesObj[f] !== undefined) {
565
+ throw new Error(`Cannot set "${f}" on a claim-bound variable. Delete and re-create to change binding type.`);
566
+ }
567
+ }
568
+ // Validate: claimId and claimVersion must be provided together
569
+ const hasClaimId = updatesObj.claimId !== undefined;
570
+ const hasClaimVersion = updatesObj.claimVersion !== undefined;
571
+ if (hasClaimId !== hasClaimVersion) {
572
+ throw new Error("claimId and claimVersion must be provided together.");
573
+ }
574
+ // Validate claim reference if provided
575
+ if (hasClaimId && hasClaimVersion) {
576
+ if (!this.claimLibrary.get(updatesObj.claimId, updatesObj.claimVersion)) {
577
+ throw new Error(`Claim "${String(updatesObj.claimId)}" version ${String(updatesObj.claimVersion)} does not exist in the claim library.`);
578
+ }
579
+ }
580
+ }
581
+ else if (isPremiseBound(existingVar)) {
582
+ const claimBoundFields = ["claimId", "claimVersion"];
583
+ for (const f of claimBoundFields) {
584
+ if (updatesObj[f] !== undefined) {
585
+ throw new Error(`Cannot set "${f}" on a premise-bound variable. Delete and re-create to change binding type.`);
586
+ }
587
+ }
588
+ // Validate boundPremiseId if provided
589
+ if (updatesObj.boundPremiseId !== undefined) {
590
+ const newPremiseId = updatesObj.boundPremiseId;
591
+ if (!this.premises.has(newPremiseId)) {
592
+ throw new Error(`Bound premise "${newPremiseId}" does not exist in this argument.`);
593
+ }
594
+ }
595
+ }
596
+ const updated = this.variables.updateVariable(variableId, updates);
597
+ const collector = new ChangeCollector();
598
+ if (updated) {
599
+ const withChecksum = this.attachVariableChecksum({
600
+ ...updated,
601
+ });
602
+ // Re-store with updated checksum so VariableManager always holds
603
+ // variables with correct checksums.
604
+ this.variables.removeVariable(variableId);
605
+ this.variables.addVariable(withChecksum);
606
+ collector.modifiedVariable(withChecksum);
607
+ this.markDirty();
608
+ this.markAllPremisesDirty();
609
+ const changes = collector.toChangeset();
610
+ this.markReactiveDirty(changes);
611
+ this.notifySubscribers();
612
+ return {
613
+ result: withChecksum,
614
+ changes,
615
+ };
616
+ }
617
+ return {
618
+ result: undefined,
619
+ changes: collector.toChangeset(),
620
+ };
621
+ });
622
+ }
623
+ removeVariableCore(variableId) {
500
624
  const variable = this.variables.getVariable(variableId);
501
625
  if (!variable) {
502
626
  return { result: undefined, changes: {} };
@@ -523,6 +647,11 @@ export class ArgumentEngine {
523
647
  changes,
524
648
  };
525
649
  }
650
+ removeVariable(variableId) {
651
+ return this.withValidation(() => {
652
+ return this.removeVariableCore(variableId);
653
+ });
654
+ }
526
655
  getVariables() {
527
656
  return this.variables.toArray();
528
657
  }
@@ -605,36 +734,40 @@ export class ArgumentEngine {
605
734
  };
606
735
  }
607
736
  setConclusionPremise(premiseId) {
608
- const premise = this.premises.get(premiseId);
609
- if (!premise) {
610
- throw new Error(`Premise "${premiseId}" does not exist.`);
611
- }
612
- this.conclusionPremiseId = premiseId;
613
- const roles = this.getRoleState();
614
- const collector = new ChangeCollector();
615
- collector.setRoles(roles);
616
- this.markDirty();
617
- const changes = collector.toChangeset();
618
- this.markReactiveDirty(changes);
619
- this.notifySubscribers();
620
- return {
621
- result: roles,
622
- changes,
623
- };
737
+ return this.withValidation(() => {
738
+ const premise = this.premises.get(premiseId);
739
+ if (!premise) {
740
+ throw new Error(`Premise "${premiseId}" does not exist.`);
741
+ }
742
+ this.conclusionPremiseId = premiseId;
743
+ const roles = this.getRoleState();
744
+ const collector = new ChangeCollector();
745
+ collector.setRoles(roles);
746
+ this.markDirty();
747
+ const changes = collector.toChangeset();
748
+ this.markReactiveDirty(changes);
749
+ this.notifySubscribers();
750
+ return {
751
+ result: roles,
752
+ changes,
753
+ };
754
+ });
624
755
  }
625
756
  clearConclusionPremise() {
626
- this.conclusionPremiseId = undefined;
627
- const roles = this.getRoleState();
628
- const collector = new ChangeCollector();
629
- collector.setRoles(roles);
630
- this.markDirty();
631
- const changes = collector.toChangeset();
632
- this.markReactiveDirty(changes);
633
- this.notifySubscribers();
634
- return {
635
- result: roles,
636
- changes,
637
- };
757
+ return this.withValidation(() => {
758
+ this.conclusionPremiseId = undefined;
759
+ const roles = this.getRoleState();
760
+ const collector = new ChangeCollector();
761
+ collector.setRoles(roles);
762
+ this.markDirty();
763
+ const changes = collector.toChangeset();
764
+ this.markReactiveDirty(changes);
765
+ this.notifySubscribers();
766
+ return {
767
+ result: roles,
768
+ changes,
769
+ };
770
+ });
638
771
  }
639
772
  getConclusionPremise() {
640
773
  if (this.conclusionPremiseId === undefined) {
@@ -674,12 +807,15 @@ export class ArgumentEngine {
674
807
  checksumConfig: normalizeChecksumConfig(snapshot.config.checksumConfig),
675
808
  }
676
809
  : undefined);
810
+ engine.restoringFromSnapshot = true;
677
811
  // Restore premises first (premise-bound variables reference them)
678
812
  for (const premiseSnap of snapshot.premises) {
679
813
  const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex, grammarConfig);
680
814
  engine.premises.set(pe.getId(), pe);
681
815
  engine.wireCircularityCheck(pe);
682
816
  engine.wireEmptyBoundPremiseCheck(pe);
817
+ pe.setVariableIdsCallback(() => new Set(engine.variables.toArray().map((v) => v.id)));
818
+ pe.setArgumentValidateCallback(() => engine.validateAfterPremiseMutation());
683
819
  const premiseId = pe.getId();
684
820
  pe.setOnMutate(() => {
685
821
  engine.markDirty();
@@ -695,15 +831,26 @@ export class ArgumentEngine {
695
831
  }
696
832
  for (const v of snapshot.variables.variables) {
697
833
  if (isPremiseBound(v)) {
698
- engine.bindVariableToPremise(v);
834
+ const pbv = v;
835
+ if (pbv.boundArgumentId === engine.argument.id) {
836
+ engine.bindVariableToPremise(v);
837
+ }
838
+ else {
839
+ engine.bindVariableToExternalPremise(v);
840
+ }
699
841
  }
700
842
  }
701
843
  // Restore conclusion role (don't use setConclusionPremise to avoid auto-assign logic)
702
844
  engine.conclusionPremiseId = snapshot.conclusionPremiseId;
845
+ engine.restoringFromSnapshot = false;
703
846
  if (checksumVerification === "strict") {
704
847
  engine.flushChecksums();
705
848
  ArgumentEngine.verifySnapshotChecksums(engine, snapshot);
706
849
  }
850
+ const validation = engine.validate();
851
+ if (!validation.ok) {
852
+ throw new InvariantViolationError(validation.violations);
853
+ }
707
854
  return engine;
708
855
  }
709
856
  /**
@@ -713,7 +860,7 @@ export class ArgumentEngine {
713
860
  * of already-added nodes) to satisfy parent-existence requirements.
714
861
  */
715
862
  static fromData(argument, claimLibrary, sourceLibrary, claimSourceLibrary, variables, premises, expressions, roles, config, grammarConfig, checksumVerification) {
716
- const loadingGrammarConfig = grammarConfig ?? PERMISSIVE_GRAMMAR_CONFIG;
863
+ const loadingGrammarConfig = grammarConfig ?? config?.grammarConfig ?? DEFAULT_GRAMMAR_CONFIG;
717
864
  const normalizedConfig = config
718
865
  ? {
719
866
  ...config,
@@ -725,6 +872,7 @@ export class ArgumentEngine {
725
872
  grammarConfig: loadingGrammarConfig,
726
873
  };
727
874
  const engine = new ArgumentEngine(argument, claimLibrary, sourceLibrary, claimSourceLibrary, loadingConfig);
875
+ engine.restoringFromSnapshot = true;
728
876
  // Register claim-bound variables first (no dependencies)
729
877
  for (const v of variables) {
730
878
  if (isClaimBound(v)) {
@@ -741,7 +889,13 @@ export class ArgumentEngine {
741
889
  // Register premise-bound variables (depend on premises)
742
890
  for (const v of variables) {
743
891
  if (isPremiseBound(v)) {
744
- engine.bindVariableToPremise(v);
892
+ const pbv = v;
893
+ if (pbv.boundArgumentId === engine.argument.id) {
894
+ engine.bindVariableToPremise(v);
895
+ }
896
+ else {
897
+ engine.bindVariableToExternalPremise(v);
898
+ }
745
899
  }
746
900
  }
747
901
  // Group expressions by premiseId
@@ -767,10 +921,15 @@ export class ArgumentEngine {
767
921
  }
768
922
  // After loading: restore the caller's intended grammar config
769
923
  engine.grammarConfig = config?.grammarConfig;
924
+ engine.restoringFromSnapshot = false;
770
925
  if (checksumVerification === "strict") {
771
926
  engine.flushChecksums();
772
927
  ArgumentEngine.verifyDataChecksums(engine, argument, variables, premises);
773
928
  }
929
+ const validation = engine.validate();
930
+ if (!validation.ok) {
931
+ throw new InvariantViolationError(validation.violations);
932
+ }
774
933
  return engine;
775
934
  }
776
935
  /**
@@ -882,6 +1041,15 @@ export class ArgumentEngine {
882
1041
  }
883
1042
  }
884
1043
  rollback(snapshot) {
1044
+ const preRollbackSnap = this.snapshot();
1045
+ this.rollbackInternal(snapshot);
1046
+ const validation = this.validate();
1047
+ if (!validation.ok) {
1048
+ this.rollbackInternal(preRollbackSnap);
1049
+ throw new InvariantViolationError(validation.violations);
1050
+ }
1051
+ }
1052
+ rollbackInternal(snapshot) {
885
1053
  this.argument = { ...snapshot.argument };
886
1054
  this.checksumConfig = normalizeChecksumConfig(snapshot.config?.checksumConfig);
887
1055
  this.positionConfig = snapshot.config?.positionConfig;
@@ -897,6 +1065,8 @@ export class ArgumentEngine {
897
1065
  for (const pe of this.premises.values()) {
898
1066
  this.wireCircularityCheck(pe);
899
1067
  this.wireEmptyBoundPremiseCheck(pe);
1068
+ pe.setVariableIdsCallback(() => new Set(this.variables.toArray().map((v) => v.id)));
1069
+ pe.setArgumentValidateCallback(() => this.validateAfterPremiseMutation());
900
1070
  const premiseId = pe.getId();
901
1071
  pe.setOnMutate(() => {
902
1072
  this.markDirty();
@@ -999,6 +1169,11 @@ export class ArgumentEngine {
999
1169
  }
1000
1170
  markDirty() {
1001
1171
  this.checksumDirty = true;
1172
+ this.cachedMetaChecksum = undefined;
1173
+ this.cachedDescendantChecksum = undefined;
1174
+ this.cachedCombinedChecksum = undefined;
1175
+ this.cachedPremisesCollectionChecksum = undefined;
1176
+ this.cachedVariablesCollectionChecksum = undefined;
1002
1177
  }
1003
1178
  /** Invalidate all premise checksums (e.g. after variable changes). */
1004
1179
  markAllPremisesDirty() {
@@ -1060,6 +1235,207 @@ export class ArgumentEngine {
1060
1235
  bySymbol,
1061
1236
  };
1062
1237
  }
1238
+ /**
1239
+ * Validates after a PremiseEngine mutation. Identical to `validate()` but
1240
+ * clears cached argument-level checksums first so the checksum-stability
1241
+ * check is skipped (checksums are known to be dirty after a premise
1242
+ * mutation).
1243
+ */
1244
+ /**
1245
+ * Lightweight validation triggered after a PremiseEngine mutation.
1246
+ * Skips per-premise deep validation (which is O(n) over all premises)
1247
+ * and argument-level checksum stability checks (checksums are known to
1248
+ * be dirty). Only checks argument-level cross-references that a
1249
+ * PremiseEngine mutation could affect.
1250
+ */
1251
+ validateAfterPremiseMutation() {
1252
+ const violations = [];
1253
+ // Variable references: ensure all variable expressions in the
1254
+ // mutated premise still reference known variables (this is the main
1255
+ // cross-cutting invariant a premise mutation can break).
1256
+ for (const v of this.variables.toArray()) {
1257
+ const base = v;
1258
+ if (isPremiseBound(base)) {
1259
+ const pb = base;
1260
+ if (pb.boundArgumentId === this.argument.id) {
1261
+ if (!this.premises.has(pb.boundPremiseId)) {
1262
+ violations.push({
1263
+ code: ARG_PREMISE_REF_NOT_FOUND,
1264
+ message: `Premise-bound variable "${pb.id}" references non-existent premise "${pb.boundPremiseId}".`,
1265
+ entityType: "variable",
1266
+ entityId: pb.id,
1267
+ });
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+ // Conclusion premise reference
1273
+ if (this.conclusionPremiseId !== undefined &&
1274
+ !this.premises.has(this.conclusionPremiseId)) {
1275
+ violations.push({
1276
+ code: ARG_CONCLUSION_NOT_FOUND,
1277
+ message: `Conclusion premise "${this.conclusionPremiseId}" does not exist in this argument.`,
1278
+ entityType: "argument",
1279
+ entityId: this.argument.id,
1280
+ });
1281
+ }
1282
+ return {
1283
+ ok: violations.length === 0,
1284
+ violations,
1285
+ };
1286
+ }
1287
+ validate() {
1288
+ const violations = [];
1289
+ // 1. Schema check — flush checksums first so fields are populated
1290
+ const savedMeta = this.cachedMetaChecksum;
1291
+ const savedDescendant = this.cachedDescendantChecksum;
1292
+ const savedCombined = this.cachedCombinedChecksum;
1293
+ this.flushChecksums();
1294
+ const arg = this.getArgument();
1295
+ if (!Value.Check(CoreArgumentSchema, arg)) {
1296
+ violations.push({
1297
+ code: ARG_SCHEMA_INVALID,
1298
+ message: `Argument "${arg.id}" does not conform to CoreArgumentSchema.`,
1299
+ entityType: "argument",
1300
+ entityId: arg.id,
1301
+ });
1302
+ }
1303
+ // 2. Delegate to VariableManager.validate()
1304
+ const varResult = this.variables.validate();
1305
+ violations.push(...varResult.violations);
1306
+ // 3. Delegate to each PremiseEngine.validate()
1307
+ for (const pe of this.listPremises()) {
1308
+ const premiseResult = pe.validate();
1309
+ violations.push(...premiseResult.violations);
1310
+ }
1311
+ // 4. Variable ownership: all variables must belong to this argument
1312
+ for (const v of this.variables.toArray()) {
1313
+ const base = v;
1314
+ if (base.argumentId !== this.argument.id ||
1315
+ base.argumentVersion !== this.argument.version) {
1316
+ violations.push({
1317
+ code: ARG_OWNERSHIP_MISMATCH,
1318
+ message: `Variable "${base.id}" has argumentId/version "${base.argumentId}/${base.argumentVersion}" but engine is "${this.argument.id}/${this.argument.version}".`,
1319
+ entityType: "variable",
1320
+ entityId: base.id,
1321
+ });
1322
+ }
1323
+ }
1324
+ // 5. Claim-bound variable references
1325
+ for (const v of this.variables.toArray()) {
1326
+ const base = v;
1327
+ if (isClaimBound(base)) {
1328
+ const cb = base;
1329
+ if (!this.claimLibrary.get(cb.claimId, cb.claimVersion)) {
1330
+ violations.push({
1331
+ code: ARG_CLAIM_REF_NOT_FOUND,
1332
+ message: `Variable "${cb.id}" references claim "${cb.claimId}" version ${cb.claimVersion} which does not exist in the claim library.`,
1333
+ entityType: "variable",
1334
+ entityId: cb.id,
1335
+ });
1336
+ }
1337
+ }
1338
+ }
1339
+ // 6. Premise-bound internal variable references
1340
+ for (const v of this.variables.toArray()) {
1341
+ const base = v;
1342
+ if (isPremiseBound(base)) {
1343
+ const pb = base;
1344
+ if (pb.boundArgumentId === this.argument.id) {
1345
+ if (!this.premises.has(pb.boundPremiseId)) {
1346
+ violations.push({
1347
+ code: ARG_PREMISE_REF_NOT_FOUND,
1348
+ message: `Premise-bound variable "${pb.id}" references non-existent premise "${pb.boundPremiseId}".`,
1349
+ entityType: "variable",
1350
+ entityId: pb.id,
1351
+ });
1352
+ }
1353
+ }
1354
+ }
1355
+ }
1356
+ // 7. Circularity detection for internal premise-bound variables.
1357
+ // A cycle exists when a premise-bound variable's bound premise
1358
+ // transitively references back to itself through other
1359
+ // premise-bound variables.
1360
+ for (const v of this.variables.toArray()) {
1361
+ const base = v;
1362
+ if (isPremiseBound(base)) {
1363
+ const pb = base;
1364
+ if (pb.boundArgumentId === this.argument.id) {
1365
+ // Trace from the bound premise through expressions'
1366
+ // variable references to see if we reach back to the
1367
+ // same premise.
1368
+ const boundPremise = this.premises.get(pb.boundPremiseId);
1369
+ if (boundPremise) {
1370
+ let hasCycle = false;
1371
+ for (const expr of boundPremise.getExpressions()) {
1372
+ if (expr.type === "variable") {
1373
+ try {
1374
+ if (this.wouldCreateCycle(expr.variableId, pb.boundPremiseId, new Set())) {
1375
+ hasCycle = true;
1376
+ break;
1377
+ }
1378
+ }
1379
+ catch {
1380
+ hasCycle = true;
1381
+ break;
1382
+ }
1383
+ }
1384
+ }
1385
+ if (hasCycle) {
1386
+ violations.push({
1387
+ code: ARG_CIRCULARITY_DETECTED,
1388
+ message: `Premise-bound variable "${pb.id}" creates a circular dependency through premise "${pb.boundPremiseId}".`,
1389
+ entityType: "variable",
1390
+ entityId: pb.id,
1391
+ });
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ }
1397
+ // 8. Conclusion premise reference
1398
+ if (this.conclusionPremiseId !== undefined &&
1399
+ !this.premises.has(this.conclusionPremiseId)) {
1400
+ violations.push({
1401
+ code: ARG_CONCLUSION_NOT_FOUND,
1402
+ message: `Conclusion premise "${this.conclusionPremiseId}" does not exist in this argument.`,
1403
+ entityType: "argument",
1404
+ entityId: this.argument.id,
1405
+ });
1406
+ }
1407
+ // 9. Argument-level checksum verification
1408
+ if (savedMeta !== undefined && savedMeta !== this.cachedMetaChecksum) {
1409
+ violations.push({
1410
+ code: ARG_CHECKSUM_MISMATCH,
1411
+ message: `Argument "${this.argument.id}" meta checksum changed after flush: "${savedMeta}" → "${this.cachedMetaChecksum}".`,
1412
+ entityType: "argument",
1413
+ entityId: this.argument.id,
1414
+ });
1415
+ }
1416
+ if (savedDescendant !== undefined &&
1417
+ savedDescendant !== this.cachedDescendantChecksum) {
1418
+ violations.push({
1419
+ code: ARG_CHECKSUM_MISMATCH,
1420
+ message: `Argument "${this.argument.id}" descendant checksum changed after flush: "${String(savedDescendant)}" → "${String(this.cachedDescendantChecksum)}".`,
1421
+ entityType: "argument",
1422
+ entityId: this.argument.id,
1423
+ });
1424
+ }
1425
+ if (savedCombined !== undefined &&
1426
+ savedCombined !== this.cachedCombinedChecksum) {
1427
+ violations.push({
1428
+ code: ARG_CHECKSUM_MISMATCH,
1429
+ message: `Argument "${this.argument.id}" combined checksum changed after flush: "${savedCombined}" → "${this.cachedCombinedChecksum}".`,
1430
+ entityType: "argument",
1431
+ entityId: this.argument.id,
1432
+ });
1433
+ }
1434
+ return {
1435
+ ok: violations.length === 0,
1436
+ violations,
1437
+ };
1438
+ }
1063
1439
  validateEvaluability() {
1064
1440
  const issues = [];
1065
1441
  if (this.conclusionPremiseId === undefined) {
@@ -1150,11 +1526,17 @@ export class ArgumentEngine {
1150
1526
  .filter((expr) => expr.type === "variable")
1151
1527
  .map((expr) => expr.variableId))),
1152
1528
  ].sort();
1153
- // Only claim-bound variables get truth-table columns;
1154
- // premise-bound variables are resolved lazily from their bound premise.
1529
+ // Claim-bound and externally-bound premise variables get truth-table columns;
1530
+ // internally-bound premise variables are resolved lazily.
1155
1531
  const referencedVariableIds = allVariableIds.filter((vid) => {
1156
1532
  const v = this.variables.getVariable(vid);
1157
- return v != null && isClaimBound(v);
1533
+ if (v == null)
1534
+ return false;
1535
+ if (isClaimBound(v))
1536
+ return true;
1537
+ if (isPremiseBound(v) && v.boundArgumentId !== this.argument.id)
1538
+ return true;
1539
+ return false;
1158
1540
  });
1159
1541
  try {
1160
1542
  // Build a resolver that lazily evaluates premise-bound variables
@@ -1166,9 +1548,13 @@ export class ArgumentEngine {
1166
1548
  return resolverCache.get(variableId);
1167
1549
  }
1168
1550
  const variable = this.variables.getVariable(variableId);
1169
- if (!variable || !isPremiseBound(variable)) {
1551
+ if (!variable ||
1552
+ !isPremiseBound(variable) ||
1553
+ variable.boundArgumentId !== this.argument.id) {
1554
+ // Claim-bound or externally-bound: read from assignment
1170
1555
  return assignment.variables[variableId] ?? null;
1171
1556
  }
1557
+ // Internal premise-bound: lazy resolution
1172
1558
  const boundPremiseId = variable.boundPremiseId;
1173
1559
  const boundPremise = this.premises.get(boundPremiseId);
1174
1560
  if (!boundPremise) {
@@ -1274,11 +1660,17 @@ export class ArgumentEngine {
1274
1660
  .filter((expr) => expr.type === "variable")
1275
1661
  .map((expr) => expr.variableId))),
1276
1662
  ].sort();
1277
- // Only claim-bound variables get truth-table columns;
1278
- // premise-bound variables are resolved lazily from their bound premise.
1663
+ // Claim-bound and externally-bound premise variables get truth-table columns;
1664
+ // internally-bound premise variables are resolved lazily.
1279
1665
  const checkedVariableIds = allVariableIdsForCheck.filter((vid) => {
1280
1666
  const v = this.variables.getVariable(vid);
1281
- return v != null && isClaimBound(v);
1667
+ if (v == null)
1668
+ return false;
1669
+ if (isClaimBound(v))
1670
+ return true;
1671
+ if (isPremiseBound(v) && v.boundArgumentId !== this.argument.id)
1672
+ return true;
1673
+ return false;
1282
1674
  });
1283
1675
  if (options?.maxVariables !== undefined &&
1284
1676
  checkedVariableIds.length > options.maxVariables) {
@@ -1355,5 +1747,150 @@ export class ArgumentEngine {
1355
1747
  truncated,
1356
1748
  };
1357
1749
  }
1750
+ // -----------------------------------------------------------------
1751
+ // Forking
1752
+ // -----------------------------------------------------------------
1753
+ /**
1754
+ * Override point for subclasses to prevent forking. When this returns
1755
+ * `false`, `forkArgument` will throw.
1756
+ */
1757
+ canFork() {
1758
+ return true;
1759
+ }
1760
+ /**
1761
+ * Override point for subclasses to restrict cross-argument bindings.
1762
+ * When this returns `false`, `bindVariableToExternalPremise` will throw.
1763
+ */
1764
+ canBind(_boundArgumentId, _boundArgumentVersion) {
1765
+ return true;
1766
+ }
1767
+ /**
1768
+ * Creates an independent copy of this argument under a new argument ID.
1769
+ *
1770
+ * Every premise, expression, and variable receives a fresh ID (via
1771
+ * `options.generateId`, defaulting to `crypto.randomUUID`). All internal
1772
+ * cross-references are remapped to the new IDs. Each forked entity carries
1773
+ * `forkedFrom*` metadata pointing back to the originals.
1774
+ *
1775
+ * The returned engine is fully independent — mutations on either the
1776
+ * source or the fork do not affect the other.
1777
+ */
1778
+ forkArgument(newArgumentId, claimLibrary, sourceLibrary, claimSourceLibrary, options) {
1779
+ // 1. Guard
1780
+ if (!this.canFork()) {
1781
+ throw new Error("Forking is not allowed for this engine.");
1782
+ }
1783
+ const generateId = options?.generateId ?? randomUUID;
1784
+ // 2. Snapshot the current engine state
1785
+ const snap = this.snapshot();
1786
+ const originalArgumentId = snap.argument.id;
1787
+ const originalArgumentVersion = snap.argument.version;
1788
+ // 3. Build remap tables (old ID → new ID)
1789
+ const premiseRemap = new Map();
1790
+ const expressionRemap = new Map();
1791
+ const variableRemap = new Map();
1792
+ for (const ps of snap.premises) {
1793
+ premiseRemap.set(ps.premise.id, generateId());
1794
+ for (const expr of ps.expressions.expressions) {
1795
+ expressionRemap.set(expr.id, generateId());
1796
+ }
1797
+ }
1798
+ for (const v of snap.variables.variables) {
1799
+ variableRemap.set(v.id, generateId());
1800
+ }
1801
+ const remapTable = {
1802
+ argumentId: { from: originalArgumentId, to: newArgumentId },
1803
+ premises: premiseRemap,
1804
+ expressions: expressionRemap,
1805
+ variables: variableRemap,
1806
+ };
1807
+ // 4. Remap the argument entity
1808
+ snap.argument = {
1809
+ ...snap.argument,
1810
+ id: newArgumentId,
1811
+ version: 0,
1812
+ forkedFromArgumentId: originalArgumentId,
1813
+ forkedFromArgumentVersion: originalArgumentVersion,
1814
+ };
1815
+ // 5. Remap premises and their expressions
1816
+ for (const ps of snap.premises) {
1817
+ const originalPremiseId = ps.premise.id;
1818
+ const newPremiseId = premiseRemap.get(originalPremiseId);
1819
+ ps.premise = {
1820
+ ...ps.premise,
1821
+ id: newPremiseId,
1822
+ argumentId: newArgumentId,
1823
+ argumentVersion: 0,
1824
+ forkedFromPremiseId: originalPremiseId,
1825
+ forkedFromArgumentId: originalArgumentId,
1826
+ forkedFromArgumentVersion: originalArgumentVersion,
1827
+ };
1828
+ // Remap rootExpressionId
1829
+ if (ps.rootExpressionId) {
1830
+ ps.rootExpressionId = expressionRemap.get(ps.rootExpressionId);
1831
+ }
1832
+ // Remap each expression
1833
+ ps.expressions.expressions = ps.expressions.expressions.map((expr) => {
1834
+ const originalExprId = expr.id;
1835
+ const newExprId = expressionRemap.get(originalExprId);
1836
+ const remapped = {
1837
+ ...expr,
1838
+ id: newExprId,
1839
+ argumentId: newArgumentId,
1840
+ argumentVersion: 0,
1841
+ premiseId: newPremiseId,
1842
+ parentId: expr.parentId
1843
+ ? (expressionRemap.get(expr.parentId) ?? null)
1844
+ : null,
1845
+ forkedFromExpressionId: originalExprId,
1846
+ forkedFromPremiseId: originalPremiseId,
1847
+ forkedFromArgumentId: originalArgumentId,
1848
+ forkedFromArgumentVersion: originalArgumentVersion,
1849
+ };
1850
+ // Remap variableId on variable-type expressions
1851
+ if (remapped.type === "variable" &&
1852
+ "variableId" in remapped) {
1853
+ const origVarId = remapped.variableId;
1854
+ remapped.variableId = variableRemap.get(origVarId);
1855
+ }
1856
+ return remapped;
1857
+ });
1858
+ }
1859
+ // 6. Remap variables
1860
+ snap.variables.variables = snap.variables.variables.map((v) => {
1861
+ const originalVarId = v.id;
1862
+ const newVarId = variableRemap.get(originalVarId);
1863
+ const remapped = {
1864
+ ...v,
1865
+ id: newVarId,
1866
+ argumentId: newArgumentId,
1867
+ argumentVersion: 0,
1868
+ forkedFromVariableId: originalVarId,
1869
+ forkedFromArgumentId: originalArgumentId,
1870
+ forkedFromArgumentVersion: originalArgumentVersion,
1871
+ };
1872
+ // Remap premise-bound variable references
1873
+ if (isPremiseBound(remapped)) {
1874
+ const premiseBound = remapped;
1875
+ premiseBound.boundPremiseId = premiseRemap.get(premiseBound.boundPremiseId);
1876
+ premiseBound.boundArgumentId = newArgumentId;
1877
+ premiseBound.boundArgumentVersion = 0;
1878
+ }
1879
+ return remapped;
1880
+ });
1881
+ // 7. Remap conclusion role
1882
+ if (snap.conclusionPremiseId) {
1883
+ snap.conclusionPremiseId = premiseRemap.get(snap.conclusionPremiseId);
1884
+ }
1885
+ // 8. Carry config from options or source engine
1886
+ snap.config = {
1887
+ checksumConfig: serializeChecksumConfig(options?.checksumConfig ?? this.checksumConfig),
1888
+ positionConfig: options?.positionConfig ?? this.positionConfig,
1889
+ grammarConfig: options?.grammarConfig ?? this.grammarConfig,
1890
+ };
1891
+ // 9. Construct the new engine from the remapped snapshot
1892
+ const engine = ArgumentEngine.fromSnapshot(snap, claimLibrary, sourceLibrary, claimSourceLibrary, options?.grammarConfig ?? this.grammarConfig, "ignore");
1893
+ return { engine, remapTable };
1894
+ }
1358
1895
  }
1359
1896
  //# sourceMappingURL=argument-engine.js.map