@polintpro/proposit-core 0.5.2 → 0.5.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 (91) hide show
  1. package/README.md +2 -7
  2. package/dist/cli/commands/sources.d.ts.map +1 -1
  3. package/dist/cli/commands/sources.js +13 -66
  4. package/dist/cli/commands/sources.js.map +1 -1
  5. package/dist/cli/commands/variables.d.ts.map +1 -1
  6. package/dist/cli/commands/variables.js +37 -1
  7. package/dist/cli/commands/variables.js.map +1 -1
  8. package/dist/cli/config.d.ts +0 -2
  9. package/dist/cli/config.d.ts.map +1 -1
  10. package/dist/cli/config.js +0 -6
  11. package/dist/cli/config.js.map +1 -1
  12. package/dist/cli/engine.d.ts.map +1 -1
  13. package/dist/cli/engine.js +7 -34
  14. package/dist/cli/engine.js.map +1 -1
  15. package/dist/cli/import.d.ts.map +1 -1
  16. package/dist/cli/import.js +2 -1
  17. package/dist/cli/import.js.map +1 -1
  18. package/dist/cli/schemata.d.ts +0 -8
  19. package/dist/cli/schemata.d.ts.map +1 -1
  20. package/dist/cli/schemata.js +0 -10
  21. package/dist/cli/schemata.js.map +1 -1
  22. package/dist/cli/storage/sources.d.ts +3 -10
  23. package/dist/cli/storage/sources.d.ts.map +1 -1
  24. package/dist/cli/storage/sources.js +11 -94
  25. package/dist/cli/storage/sources.js.map +1 -1
  26. package/dist/lib/consts.d.ts.map +1 -1
  27. package/dist/lib/consts.js +7 -15
  28. package/dist/lib/consts.js.map +1 -1
  29. package/dist/lib/core/argument-engine.d.ts +15 -27
  30. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  31. package/dist/lib/core/argument-engine.js +236 -242
  32. package/dist/lib/core/argument-engine.js.map +1 -1
  33. package/dist/lib/core/change-collector.d.ts +0 -7
  34. package/dist/lib/core/change-collector.d.ts.map +1 -1
  35. package/dist/lib/core/change-collector.js +0 -18
  36. package/dist/lib/core/change-collector.js.map +1 -1
  37. package/dist/lib/core/claim-source-library.d.ts +27 -0
  38. package/dist/lib/core/claim-source-library.d.ts.map +1 -0
  39. package/dist/lib/core/claim-source-library.js +116 -0
  40. package/dist/lib/core/claim-source-library.js.map +1 -0
  41. package/dist/lib/core/diff.d.ts +3 -7
  42. package/dist/lib/core/diff.d.ts.map +1 -1
  43. package/dist/lib/core/diff.js +14 -79
  44. package/dist/lib/core/diff.js.map +1 -1
  45. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +30 -9
  46. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -1
  47. package/dist/lib/core/interfaces/index.d.ts +1 -2
  48. package/dist/lib/core/interfaces/index.d.ts.map +1 -1
  49. package/dist/lib/core/interfaces/library.interfaces.d.ts +18 -1
  50. package/dist/lib/core/interfaces/library.interfaces.d.ts.map +1 -1
  51. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +1 -29
  52. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -1
  53. package/dist/lib/core/premise-engine.d.ts +7 -8
  54. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  55. package/dist/lib/core/premise-engine.js +66 -82
  56. package/dist/lib/core/premise-engine.js.map +1 -1
  57. package/dist/lib/core/variable-manager.d.ts +3 -4
  58. package/dist/lib/core/variable-manager.d.ts.map +1 -1
  59. package/dist/lib/core/variable-manager.js +11 -1
  60. package/dist/lib/core/variable-manager.js.map +1 -1
  61. package/dist/lib/index.d.ts +2 -3
  62. package/dist/lib/index.d.ts.map +1 -1
  63. package/dist/lib/index.js +2 -2
  64. package/dist/lib/index.js.map +1 -1
  65. package/dist/lib/schemata/propositional.d.ts +36 -2
  66. package/dist/lib/schemata/propositional.d.ts.map +1 -1
  67. package/dist/lib/schemata/propositional.js +30 -4
  68. package/dist/lib/schemata/propositional.js.map +1 -1
  69. package/dist/lib/schemata/source.d.ts +4 -16
  70. package/dist/lib/schemata/source.d.ts.map +1 -1
  71. package/dist/lib/schemata/source.js +7 -17
  72. package/dist/lib/schemata/source.js.map +1 -1
  73. package/dist/lib/types/checksum.d.ts +2 -4
  74. package/dist/lib/types/checksum.d.ts.map +1 -1
  75. package/dist/lib/types/diff.d.ts +1 -5
  76. package/dist/lib/types/diff.d.ts.map +1 -1
  77. package/dist/lib/types/evaluation.d.ts +1 -1
  78. package/dist/lib/types/evaluation.d.ts.map +1 -1
  79. package/dist/lib/types/mutation.d.ts +0 -3
  80. package/dist/lib/types/mutation.d.ts.map +1 -1
  81. package/dist/lib/types/reactive.d.ts +1 -3
  82. package/dist/lib/types/reactive.d.ts.map +1 -1
  83. package/package.json +1 -1
  84. package/dist/lib/core/interfaces/source-management.interfaces.d.ts +0 -63
  85. package/dist/lib/core/interfaces/source-management.interfaces.d.ts.map +0 -1
  86. package/dist/lib/core/interfaces/source-management.interfaces.js +0 -2
  87. package/dist/lib/core/interfaces/source-management.interfaces.js.map +0 -1
  88. package/dist/lib/core/source-manager.d.ts +0 -38
  89. package/dist/lib/core/source-manager.d.ts.map +0 -1
  90. package/dist/lib/core/source-manager.js +0 -266
  91. package/dist/lib/core/source-manager.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { isClaimBound, isPremiseBound, } from "../schemata/index.js";
2
3
  import { DEFAULT_CHECKSUM_CONFIG } from "../consts.js";
3
4
  import { getOrCreate, sortedUnique } from "../utils/collections.js";
4
5
  import { ChangeCollector } from "./change-collector.js";
@@ -7,7 +8,6 @@ import { kleeneAnd, kleeneNot } from "./evaluation/kleene.js";
7
8
  import { makeErrorIssue, makeValidationResult, } from "./evaluation/validation.js";
8
9
  import { PremiseEngine } from "./premise-engine.js";
9
10
  import { VariableManager } from "./variable-manager.js";
10
- import { SourceManager } from "./source-manager.js";
11
11
  /**
12
12
  * Manages a propositional logic argument composed of premises, variable
13
13
  * assignments, and logical roles (supporting premises and a conclusion).
@@ -19,9 +19,9 @@ export class ArgumentEngine {
19
19
  argument;
20
20
  premises;
21
21
  variables;
22
- sourceManager;
23
22
  claimLibrary;
24
23
  sourceLibrary;
24
+ claimSourceLibrary;
25
25
  conclusionPremiseId;
26
26
  checksumConfig;
27
27
  positionConfig;
@@ -33,15 +33,15 @@ export class ArgumentEngine {
33
33
  argument: true,
34
34
  variables: true,
35
35
  roles: true,
36
- sources: true,
37
36
  premiseIds: new Set(),
38
37
  allPremises: true,
39
38
  };
40
39
  cachedReactiveSnapshot;
41
- constructor(argument, claimLibrary, sourceLibrary, options) {
40
+ constructor(argument, claimLibrary, sourceLibrary, claimSourceLibrary, options) {
42
41
  this.argument = { ...argument };
43
42
  this.claimLibrary = claimLibrary;
44
43
  this.sourceLibrary = sourceLibrary;
44
+ this.claimSourceLibrary = claimSourceLibrary;
45
45
  this.premises = new Map();
46
46
  this.checksumConfig = options?.checksumConfig;
47
47
  this.positionConfig = options?.positionConfig;
@@ -49,10 +49,54 @@ export class ArgumentEngine {
49
49
  checksumConfig: this.checksumConfig,
50
50
  positionConfig: this.positionConfig,
51
51
  });
52
- this.sourceManager = new SourceManager();
53
52
  this.expressionIndex = new Map();
54
53
  this.conclusionPremiseId = undefined;
55
54
  }
55
+ createCircularityCheck() {
56
+ return (variableId, targetPremiseId) => {
57
+ return this.wouldCreateCycle(variableId, targetPremiseId, new Set());
58
+ };
59
+ }
60
+ wouldCreateCycle(variableId, targetPremiseId, visited) {
61
+ const variable = this.variables.getVariable(variableId);
62
+ if (!variable)
63
+ return false;
64
+ if (!isPremiseBound(variable))
65
+ return false;
66
+ const bound = variable;
67
+ if (bound.boundPremiseId === targetPremiseId)
68
+ return true;
69
+ if (visited.size >= this.premises.size) {
70
+ throw new Error(`Circularity check depth limit exceeded (visited ${visited.size} premises).`);
71
+ }
72
+ if (visited.has(bound.boundPremiseId))
73
+ return false;
74
+ visited.add(bound.boundPremiseId);
75
+ const boundPremise = this.premises.get(bound.boundPremiseId);
76
+ if (!boundPremise)
77
+ return false;
78
+ for (const expr of boundPremise.getExpressions()) {
79
+ if (expr.type === "variable") {
80
+ if (this.wouldCreateCycle(expr.variableId, targetPremiseId, visited)) {
81
+ return true;
82
+ }
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ wireCircularityCheck(pm) {
88
+ pm.setCircularityCheck(this.createCircularityCheck());
89
+ }
90
+ wireEmptyBoundPremiseCheck(pm) {
91
+ pm.setEmptyBoundPremiseCheck((variableId) => {
92
+ const v = this.variables.getVariable(variableId);
93
+ if (!v ||
94
+ !isPremiseBound(v))
95
+ return false;
96
+ const boundPremise = this.premises.get(v.boundPremiseId);
97
+ return !boundPremise?.getRootExpressionId();
98
+ });
99
+ }
56
100
  subscribe = (listener) => {
57
101
  this.listeners.add(listener);
58
102
  return () => {
@@ -74,7 +118,6 @@ export class ArgumentEngine {
74
118
  !dirty.argument &&
75
119
  !dirty.variables &&
76
120
  !dirty.roles &&
77
- !dirty.sources &&
78
121
  dirty.premiseIds.size === 0 &&
79
122
  !dirty.allPremises) {
80
123
  return prev;
@@ -113,36 +156,17 @@ export class ArgumentEngine {
113
156
  }
114
157
  }
115
158
  }
116
- let varAssocRecord;
117
- let exprAssocRecord;
118
- if (dirty.sources || !prev) {
119
- varAssocRecord = {};
120
- for (const a of this.sourceManager.getAllVariableSourceAssociations()) {
121
- varAssocRecord[a.id] = a;
122
- }
123
- exprAssocRecord = {};
124
- for (const a of this.sourceManager.getAllExpressionSourceAssociations()) {
125
- exprAssocRecord[a.id] = a;
126
- }
127
- }
128
- else {
129
- varAssocRecord = prev.variableSourceAssociations;
130
- exprAssocRecord = prev.expressionSourceAssociations;
131
- }
132
159
  const snapshot = {
133
160
  argument,
134
161
  variables,
135
162
  premises,
136
163
  roles,
137
- variableSourceAssociations: varAssocRecord,
138
- expressionSourceAssociations: exprAssocRecord,
139
164
  };
140
165
  this.cachedReactiveSnapshot = snapshot;
141
166
  this.reactiveDirty = {
142
167
  argument: false,
143
168
  variables: false,
144
169
  roles: false,
145
- sources: false,
146
170
  premiseIds: new Set(),
147
171
  allPremises: false,
148
172
  };
@@ -202,10 +226,6 @@ export class ArgumentEngine {
202
226
  this.reactiveDirty.premiseIds.add(p.id);
203
227
  }
204
228
  }
205
- if (changes.variableSourceAssociations ||
206
- changes.expressionSourceAssociations) {
207
- this.reactiveDirty.sources = true;
208
- }
209
229
  }
210
230
  getArgument() {
211
231
  return { ...this.argument, checksum: this.checksum() };
@@ -249,12 +269,13 @@ export class ArgumentEngine {
249
269
  argument: this.argument,
250
270
  variables: this.variables,
251
271
  expressionIndex: this.expressionIndex,
252
- sourceManager: this.sourceManager,
253
272
  }, {
254
273
  checksumConfig: this.checksumConfig,
255
274
  positionConfig: this.positionConfig,
256
275
  });
257
276
  this.premises.set(id, pm);
277
+ this.wireCircularityCheck(pm);
278
+ this.wireEmptyBoundPremiseCheck(pm);
258
279
  pm.setOnMutate(() => {
259
280
  this.reactiveDirty.premiseIds.add(id);
260
281
  this.notifySubscribers();
@@ -280,13 +301,9 @@ export class ArgumentEngine {
280
301
  return { result: undefined, changes: {} };
281
302
  const data = pm.toPremiseData();
282
303
  const collector = new ChangeCollector();
283
- // Clean up expression index and source associations for removed premise's expressions
304
+ // Clean up expression index for removed premise's expressions
284
305
  for (const expr of pm.getExpressions()) {
285
306
  this.expressionIndex.delete(expr.id);
286
- const sourceResult = this.sourceManager.removeAssociationsForExpression(expr.id);
287
- for (const assoc of sourceResult.removedExpressionAssociations) {
288
- collector.removedExpressionSourceAssociation(assoc);
289
- }
290
307
  }
291
308
  this.premises.delete(premiseId);
292
309
  collector.removedPremise(data);
@@ -294,6 +311,21 @@ export class ArgumentEngine {
294
311
  this.conclusionPremiseId = undefined;
295
312
  collector.setRoles(this.getRoleState());
296
313
  }
314
+ // Cascade: remove variables bound to the deleted premise
315
+ const boundVars = this.getVariablesBoundToPremise(premiseId);
316
+ for (const v of boundVars) {
317
+ const removeResult = this.removeVariable(v.id);
318
+ if (removeResult.changes.variables) {
319
+ for (const rv of removeResult.changes.variables.removed) {
320
+ collector.removedVariable(rv);
321
+ }
322
+ }
323
+ if (removeResult.changes.expressions) {
324
+ for (const re of removeResult.changes.expressions.removed) {
325
+ collector.removedExpression(re);
326
+ }
327
+ }
328
+ }
297
329
  this.markDirty();
298
330
  const changes = collector.toChangeset();
299
331
  this.markReactiveDirty(changes);
@@ -318,6 +350,11 @@ export class ArgumentEngine {
318
350
  .filter((pm) => pm !== undefined);
319
351
  }
320
352
  addVariable(variable) {
353
+ // Only claim-bound variables may be added via addVariable.
354
+ // Premise-bound variables must use bindVariableToPremise.
355
+ if (!isClaimBound(variable)) {
356
+ throw new Error("addVariable only accepts claim-bound variables. Use bindVariableToPremise for premise-bound variables.");
357
+ }
321
358
  if (variable.argumentId !== this.argument.id) {
322
359
  throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
323
360
  }
@@ -328,7 +365,38 @@ export class ArgumentEngine {
328
365
  if (!this.claimLibrary.get(variable.claimId, variable.claimVersion)) {
329
366
  throw new Error(`Claim "${variable.claimId}" version ${variable.claimVersion} does not exist in the claim library.`);
330
367
  }
331
- const withChecksum = this.attachVariableChecksum({ ...variable });
368
+ const withChecksum = this.attachVariableChecksum({
369
+ ...variable,
370
+ });
371
+ this.variables.addVariable(withChecksum);
372
+ const collector = new ChangeCollector();
373
+ collector.addedVariable(withChecksum);
374
+ this.markDirty();
375
+ this.markAllPremisesDirty();
376
+ const changes = collector.toChangeset();
377
+ this.markReactiveDirty(changes);
378
+ this.notifySubscribers();
379
+ return {
380
+ result: withChecksum,
381
+ changes,
382
+ };
383
+ }
384
+ bindVariableToPremise(variable) {
385
+ if (variable.argumentId !== this.argument.id) {
386
+ throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
387
+ }
388
+ if (variable.argumentVersion !== this.argument.version) {
389
+ throw new Error(`Variable argumentVersion "${variable.argumentVersion}" does not match engine argument version "${this.argument.version}".`);
390
+ }
391
+ if (variable.boundArgumentId !== this.argument.id) {
392
+ throw new Error(`Cross-argument bindings are not supported. boundArgumentId "${variable.boundArgumentId}" does not match engine argument ID "${this.argument.id}".`);
393
+ }
394
+ if (!this.premises.has(variable.boundPremiseId)) {
395
+ throw new Error(`Bound premise "${variable.boundPremiseId}" does not exist in this argument.`);
396
+ }
397
+ const withChecksum = this.attachVariableChecksum({
398
+ ...variable,
399
+ });
332
400
  this.variables.addVariable(withChecksum);
333
401
  const collector = new ChangeCollector();
334
402
  collector.addedVariable(withChecksum);
@@ -343,16 +411,50 @@ export class ArgumentEngine {
343
411
  };
344
412
  }
345
413
  updateVariable(variableId, updates) {
346
- // Validate: claimId and claimVersion must be provided together
347
- const hasClaimId = updates.claimId !== undefined;
348
- const hasClaimVersion = updates.claimVersion !== undefined;
349
- if (hasClaimId !== hasClaimVersion) {
350
- throw new Error("claimId and claimVersion must be provided together.");
351
- }
352
- // Validate claim reference if provided
353
- if (hasClaimId && hasClaimVersion) {
354
- if (!this.claimLibrary.get(updates.claimId, updates.claimVersion)) {
355
- throw new Error(`Claim "${updates.claimId}" version ${updates.claimVersion} does not exist in the claim library.`);
414
+ const existing = this.variables.getVariable(variableId);
415
+ if (!existing) {
416
+ return { result: undefined, changes: {} };
417
+ }
418
+ const existingVar = existing;
419
+ const updatesObj = updates;
420
+ // Reject binding-type conversion
421
+ if (isClaimBound(existingVar)) {
422
+ const premiseBoundFields = [
423
+ "boundPremiseId",
424
+ "boundArgumentId",
425
+ "boundArgumentVersion",
426
+ ];
427
+ for (const f of premiseBoundFields) {
428
+ if (updatesObj[f] !== undefined) {
429
+ throw new Error(`Cannot set "${f}" on a claim-bound variable. Delete and re-create to change binding type.`);
430
+ }
431
+ }
432
+ // Validate: claimId and claimVersion must be provided together
433
+ const hasClaimId = updatesObj.claimId !== undefined;
434
+ const hasClaimVersion = updatesObj.claimVersion !== undefined;
435
+ if (hasClaimId !== hasClaimVersion) {
436
+ throw new Error("claimId and claimVersion must be provided together.");
437
+ }
438
+ // Validate claim reference if provided
439
+ if (hasClaimId && hasClaimVersion) {
440
+ if (!this.claimLibrary.get(updatesObj.claimId, updatesObj.claimVersion)) {
441
+ throw new Error(`Claim "${String(updatesObj.claimId)}" version ${String(updatesObj.claimVersion)} does not exist in the claim library.`);
442
+ }
443
+ }
444
+ }
445
+ else if (isPremiseBound(existingVar)) {
446
+ const claimBoundFields = ["claimId", "claimVersion"];
447
+ for (const f of claimBoundFields) {
448
+ if (updatesObj[f] !== undefined) {
449
+ throw new Error(`Cannot set "${f}" on a premise-bound variable. Delete and re-create to change binding type.`);
450
+ }
451
+ }
452
+ // Validate boundPremiseId if provided
453
+ if (updatesObj.boundPremiseId !== undefined) {
454
+ const newPremiseId = updatesObj.boundPremiseId;
455
+ if (!this.premises.has(newPremiseId)) {
456
+ throw new Error(`Bound premise "${newPremiseId}" does not exist in this argument.`);
457
+ }
356
458
  }
357
459
  }
358
460
  const updated = this.variables.updateVariable(variableId, updates);
@@ -386,8 +488,6 @@ export class ArgumentEngine {
386
488
  }
387
489
  const collector = new ChangeCollector();
388
490
  // Cascade: delete referencing expressions in every premise
389
- // (PremiseEngine.removeExpression already cascades expression-source
390
- // associations via the Task 12 logic)
391
491
  for (const pm of this.listPremises()) {
392
492
  const { changes } = pm.deleteExpressionsUsingVariable(variableId);
393
493
  if (changes.expressions) {
@@ -395,16 +495,6 @@ export class ArgumentEngine {
395
495
  collector.removedExpression(e);
396
496
  }
397
497
  }
398
- if (changes.expressionSourceAssociations) {
399
- for (const a of changes.expressionSourceAssociations.removed) {
400
- collector.removedExpressionSourceAssociation(a);
401
- }
402
- }
403
- }
404
- // Cascade: remove variable-source associations
405
- const varAssocResult = this.sourceManager.removeAssociationsForVariable(variableId);
406
- for (const assoc of varAssocResult.removedVariableAssociations) {
407
- collector.removedVariableSourceAssociation(assoc);
408
498
  }
409
499
  this.variables.removeVariable(variableId);
410
500
  collector.removedVariable(variable);
@@ -437,6 +527,12 @@ export class ArgumentEngine {
437
527
  }
438
528
  return map;
439
529
  }
530
+ getVariablesBoundToPremise(premiseId) {
531
+ return this.variables.toArray().filter((v) => {
532
+ const base = v;
533
+ return isPremiseBound(base) && base.boundPremiseId === premiseId;
534
+ });
535
+ }
440
536
  getExpression(expressionId) {
441
537
  const premiseId = this.expressionIndex.get(expressionId);
442
538
  if (premiseId === undefined)
@@ -534,129 +630,6 @@ export class ArgumentEngine {
534
630
  listSupportingPremises() {
535
631
  return this.listPremises().filter((pm) => pm.isInference() && pm.getId() !== this.conclusionPremiseId);
536
632
  }
537
- // -------------------------------------------------------------------------
538
- // Source association management
539
- // -------------------------------------------------------------------------
540
- addVariableSourceAssociation(sourceId, sourceVersion, variableId) {
541
- if (!this.sourceLibrary.get(sourceId, sourceVersion)) {
542
- throw new Error(`Source "${sourceId}" version ${sourceVersion} does not exist in the source library.`);
543
- }
544
- if (!this.variables.hasVariable(variableId)) {
545
- throw new Error(`Variable "${variableId}" does not exist.`);
546
- }
547
- const assoc = {
548
- id: randomUUID(),
549
- sourceId,
550
- sourceVersion,
551
- variableId,
552
- argumentId: this.argument.id,
553
- argumentVersion: this.argument.version,
554
- checksum: "",
555
- };
556
- const fields = this.checksumConfig?.variableSourceAssociationFields ??
557
- DEFAULT_CHECKSUM_CONFIG.variableSourceAssociationFields;
558
- const assocWithChecksum = {
559
- ...assoc,
560
- checksum: entityChecksum(assoc, fields),
561
- };
562
- this.sourceManager.addVariableSourceAssociation(assocWithChecksum);
563
- const collector = new ChangeCollector();
564
- collector.addedVariableSourceAssociation(assocWithChecksum);
565
- this.markDirty();
566
- const changes = collector.toChangeset();
567
- this.markReactiveDirty(changes);
568
- this.notifySubscribers();
569
- return { result: assocWithChecksum, changes };
570
- }
571
- removeVariableSourceAssociation(associationId) {
572
- const allVarAssocs = this.sourceManager.getAllVariableSourceAssociations();
573
- if (!allVarAssocs.some((a) => a.id === associationId)) {
574
- return { result: undefined, changes: {} };
575
- }
576
- const removalResult = this.sourceManager.removeVariableSourceAssociation(associationId);
577
- const collector = new ChangeCollector();
578
- for (const assoc of removalResult.removedVariableAssociations) {
579
- collector.removedVariableSourceAssociation(assoc);
580
- }
581
- this.markDirty();
582
- const changes = collector.toChangeset();
583
- this.markReactiveDirty(changes);
584
- this.notifySubscribers();
585
- return {
586
- result: removalResult.removedVariableAssociations[0],
587
- changes,
588
- };
589
- }
590
- addExpressionSourceAssociation(sourceId, sourceVersion, expressionId, premiseId) {
591
- if (!this.sourceLibrary.get(sourceId, sourceVersion)) {
592
- throw new Error(`Source "${sourceId}" version ${sourceVersion} does not exist in the source library.`);
593
- }
594
- const pm = this.premises.get(premiseId);
595
- if (!pm) {
596
- throw new Error(`Premise "${premiseId}" does not exist.`);
597
- }
598
- if (!pm.getExpression(expressionId)) {
599
- throw new Error(`Expression "${expressionId}" does not exist in premise "${premiseId}".`);
600
- }
601
- const assoc = {
602
- id: randomUUID(),
603
- sourceId,
604
- sourceVersion,
605
- expressionId,
606
- premiseId,
607
- argumentId: this.argument.id,
608
- argumentVersion: this.argument.version,
609
- checksum: "",
610
- };
611
- const fields = this.checksumConfig?.expressionSourceAssociationFields ??
612
- DEFAULT_CHECKSUM_CONFIG.expressionSourceAssociationFields;
613
- const assocWithChecksum = {
614
- ...assoc,
615
- checksum: entityChecksum(assoc, fields),
616
- };
617
- this.sourceManager.addExpressionSourceAssociation(assocWithChecksum);
618
- const collector = new ChangeCollector();
619
- collector.addedExpressionSourceAssociation(assocWithChecksum);
620
- this.markDirty();
621
- const changes = collector.toChangeset();
622
- this.markReactiveDirty(changes);
623
- this.notifySubscribers();
624
- return { result: assocWithChecksum, changes };
625
- }
626
- removeExpressionSourceAssociation(associationId) {
627
- const allExprAssocs = this.sourceManager.getAllExpressionSourceAssociations();
628
- if (!allExprAssocs.some((a) => a.id === associationId)) {
629
- return { result: undefined, changes: {} };
630
- }
631
- const removalResult = this.sourceManager.removeExpressionSourceAssociation(associationId);
632
- const collector = new ChangeCollector();
633
- for (const assoc of removalResult.removedExpressionAssociations) {
634
- collector.removedExpressionSourceAssociation(assoc);
635
- }
636
- this.markDirty();
637
- const changes = collector.toChangeset();
638
- this.markReactiveDirty(changes);
639
- this.notifySubscribers();
640
- return {
641
- result: removalResult.removedExpressionAssociations[0],
642
- changes,
643
- };
644
- }
645
- getAssociationsForSource(sourceId) {
646
- return this.sourceManager.getAssociationsForSource(sourceId);
647
- }
648
- getAssociationsForVariable(variableId) {
649
- return this.sourceManager.getAssociationsForVariable(variableId);
650
- }
651
- getAssociationsForExpression(expressionId) {
652
- return this.sourceManager.getAssociationsForExpression(expressionId);
653
- }
654
- getAllVariableSourceAssociations() {
655
- return this.sourceManager.getAllVariableSourceAssociations();
656
- }
657
- getAllExpressionSourceAssociations() {
658
- return this.sourceManager.getAllExpressionSourceAssociations();
659
- }
660
633
  snapshot() {
661
634
  return {
662
635
  argument: { ...this.argument },
@@ -669,30 +642,34 @@ export class ArgumentEngine {
669
642
  checksumConfig: this.checksumConfig,
670
643
  positionConfig: this.positionConfig,
671
644
  },
672
- sources: this.sourceManager.snapshot(),
673
645
  };
674
646
  }
675
647
  /** Creates a new ArgumentEngine from a previously captured snapshot. */
676
- static fromSnapshot(snapshot, claimLibrary, sourceLibrary) {
677
- const engine = new ArgumentEngine(snapshot.argument, claimLibrary, sourceLibrary, snapshot.config);
678
- // Restore variables
679
- for (const v of snapshot.variables.variables) {
680
- engine.addVariable(v);
681
- }
682
- // Restore source manager (before premises, so PremiseEngines get the correct reference)
683
- if (snapshot.sources) {
684
- engine.sourceManager = SourceManager.fromSnapshot(snapshot.sources);
685
- }
686
- // Restore premises using PremiseEngine.fromSnapshot
648
+ static fromSnapshot(snapshot, claimLibrary, sourceLibrary, claimSourceLibrary) {
649
+ const engine = new ArgumentEngine(snapshot.argument, claimLibrary, sourceLibrary, claimSourceLibrary, snapshot.config);
650
+ // Restore premises first (premise-bound variables reference them)
687
651
  for (const premiseSnap of snapshot.premises) {
688
- const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex, engine.sourceManager);
652
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex);
689
653
  engine.premises.set(pe.getId(), pe);
654
+ engine.wireCircularityCheck(pe);
655
+ engine.wireEmptyBoundPremiseCheck(pe);
690
656
  const premiseId = pe.getId();
691
657
  pe.setOnMutate(() => {
692
658
  engine.reactiveDirty.premiseIds.add(premiseId);
693
659
  engine.notifySubscribers();
694
660
  });
695
661
  }
662
+ // Restore claim-bound variables first, then premise-bound variables
663
+ for (const v of snapshot.variables.variables) {
664
+ if (isClaimBound(v)) {
665
+ engine.addVariable(v);
666
+ }
667
+ }
668
+ for (const v of snapshot.variables.variables) {
669
+ if (isPremiseBound(v)) {
670
+ engine.bindVariableToPremise(v);
671
+ }
672
+ }
696
673
  // Restore conclusion role (don't use setConclusionPremise to avoid auto-assign logic)
697
674
  engine.conclusionPremiseId = snapshot.conclusionPremiseId;
698
675
  return engine;
@@ -703,11 +680,26 @@ export class ArgumentEngine {
703
680
  * `premiseId` field and loaded in BFS order (roots first, then children
704
681
  * of already-added nodes) to satisfy parent-existence requirements.
705
682
  */
706
- static fromData(argument, claimLibrary, sourceLibrary, variables, premises, expressions, roles, config) {
707
- const engine = new ArgumentEngine(argument, claimLibrary, sourceLibrary, config);
708
- // Register variables
683
+ static fromData(argument, claimLibrary, sourceLibrary, claimSourceLibrary, variables, premises, expressions, roles, config) {
684
+ const engine = new ArgumentEngine(argument, claimLibrary, sourceLibrary, claimSourceLibrary, config);
685
+ // Register claim-bound variables first (no dependencies)
686
+ for (const v of variables) {
687
+ if (isClaimBound(v)) {
688
+ engine.addVariable(v);
689
+ }
690
+ }
691
+ // Create premises (premise-bound variables reference them)
692
+ const premiseEngines = new Map();
693
+ for (const premise of premises) {
694
+ const { id: _id, argumentId: _argumentId, argumentVersion: _argumentVersion, checksum: _checksum, ...extras } = premise;
695
+ const { result: pe } = engine.createPremiseWithId(premise.id, extras);
696
+ premiseEngines.set(premise.id, pe);
697
+ }
698
+ // Register premise-bound variables (depend on premises)
709
699
  for (const v of variables) {
710
- engine.addVariable(v);
700
+ if (isPremiseBound(v)) {
701
+ engine.bindVariableToPremise(v);
702
+ }
711
703
  }
712
704
  // Group expressions by premiseId
713
705
  const exprsByPremise = new Map();
@@ -721,12 +713,9 @@ export class ArgumentEngine {
721
713
  }
722
714
  group.push(expr);
723
715
  }
724
- // Create premises and load their expressions in BFS order
725
- for (const premise of premises) {
726
- const { id: _id, argumentId: _argumentId, argumentVersion: _argumentVersion, checksum: _checksum, ...extras } = premise;
727
- const { result: pe } = engine.createPremiseWithId(premise.id, extras);
728
- // Add expressions in BFS order (roots first, then children)
729
- const premiseExprs = exprsByPremise.get(premise.id) ?? [];
716
+ // Add expressions in BFS order (roots first, then children)
717
+ for (const [premiseId, pe] of premiseEngines) {
718
+ const premiseExprs = exprsByPremise.get(premiseId) ?? [];
730
719
  const pending = new Map(premiseExprs.map((e) => [e.id, e]));
731
720
  let progressed = true;
732
721
  while (pending.size > 0 && progressed) {
@@ -757,17 +746,16 @@ export class ArgumentEngine {
757
746
  this.checksumConfig = snapshot.config?.checksumConfig;
758
747
  this.positionConfig = snapshot.config?.positionConfig;
759
748
  this.variables = VariableManager.fromSnapshot(snapshot.variables);
760
- this.sourceManager = snapshot.sources
761
- ? SourceManager.fromSnapshot(snapshot.sources)
762
- : new SourceManager();
763
749
  this.premises = new Map();
764
750
  this.expressionIndex = new Map();
765
751
  for (const premiseSnap of snapshot.premises) {
766
- const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex, this.sourceManager);
752
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex);
767
753
  this.premises.set(pe.getId(), pe);
768
754
  }
769
755
  this.conclusionPremiseId = snapshot.conclusionPremiseId;
770
756
  for (const pe of this.premises.values()) {
757
+ this.wireCircularityCheck(pe);
758
+ this.wireEmptyBoundPremiseCheck(pe);
771
759
  const premiseId = pe.getId();
772
760
  pe.setOnMutate(() => {
773
761
  this.reactiveDirty.premiseIds.add(premiseId);
@@ -779,7 +767,6 @@ export class ArgumentEngine {
779
767
  argument: true,
780
768
  variables: true,
781
769
  roles: true,
782
- sources: true,
783
770
  premiseIds: new Set(),
784
771
  allPremises: true,
785
772
  };
@@ -807,13 +794,6 @@ export class ArgumentEngine {
807
794
  for (const pe of this.listPremises()) {
808
795
  checksumMap[pe.getId()] = pe.checksum();
809
796
  }
810
- // Association checksums
811
- for (const a of this.sourceManager.getAllVariableSourceAssociations()) {
812
- checksumMap[a.id] = a.checksum;
813
- }
814
- for (const a of this.sourceManager.getAllExpressionSourceAssociations()) {
815
- checksumMap[a.id] = a.checksum;
816
- }
817
797
  return computeHash(canonicalSerialize(checksumMap));
818
798
  }
819
799
  markDirty() {
@@ -929,30 +909,6 @@ export class ArgumentEngine {
929
909
  const premiseValidation = premise.validateEvaluability();
930
910
  issues.push(...premiseValidation.issues);
931
911
  }
932
- // Source validation
933
- for (const assoc of this.sourceManager.getAllVariableSourceAssociations()) {
934
- if (!this.variables.hasVariable(assoc.variableId)) {
935
- issues.push(makeErrorIssue({
936
- code: "SOURCE_VARIABLE_ASSOCIATION_INVALID_VARIABLE",
937
- message: `Variable-source association "${assoc.id}" references non-existent variable "${assoc.variableId}".`,
938
- }));
939
- }
940
- }
941
- for (const assoc of this.sourceManager.getAllExpressionSourceAssociations()) {
942
- const premise = this.premises.get(assoc.premiseId);
943
- if (!premise) {
944
- issues.push(makeErrorIssue({
945
- code: "SOURCE_EXPRESSION_ASSOCIATION_INVALID_PREMISE",
946
- message: `Expression-source association "${assoc.id}" references non-existent premise "${assoc.premiseId}".`,
947
- }));
948
- }
949
- else if (!premise.getExpression(assoc.expressionId)) {
950
- issues.push(makeErrorIssue({
951
- code: "SOURCE_EXPRESSION_ASSOCIATION_INVALID_EXPRESSION",
952
- message: `Expression-source association "${assoc.id}" references non-existent expression "${assoc.expressionId}" in premise "${assoc.premiseId}".`,
953
- }));
954
- }
955
- }
956
912
  return makeValidationResult(issues);
957
913
  }
958
914
  evaluate(assignment, options) {
@@ -987,15 +943,47 @@ export class ArgumentEngine {
987
943
  ...supportingPremises,
988
944
  ...constraintPremises,
989
945
  ];
990
- const referencedVariableIds = [
946
+ const allVariableIds = [
991
947
  ...new Set(allRelevantPremises.flatMap((pm) => pm
992
948
  .getExpressions()
993
949
  .filter((expr) => expr.type === "variable")
994
950
  .map((expr) => expr.variableId))),
995
951
  ].sort();
952
+ // Only claim-bound variables get truth-table columns;
953
+ // premise-bound variables are resolved lazily from their bound premise.
954
+ const referencedVariableIds = allVariableIds.filter((vid) => {
955
+ const v = this.variables.getVariable(vid);
956
+ return v != null && isClaimBound(v);
957
+ });
996
958
  try {
959
+ // Build a resolver that lazily evaluates premise-bound variables
960
+ // by evaluating their bound premise's expression tree under the
961
+ // same assignment. Results are cached per-variable per-evaluate call.
962
+ const resolverCache = new Map();
963
+ const resolver = (variableId) => {
964
+ if (resolverCache.has(variableId)) {
965
+ return resolverCache.get(variableId);
966
+ }
967
+ const variable = this.variables.getVariable(variableId);
968
+ if (!variable || !isPremiseBound(variable)) {
969
+ return assignment.variables[variableId] ?? null;
970
+ }
971
+ const boundPremiseId = variable.boundPremiseId;
972
+ const boundPremise = this.premises.get(boundPremiseId);
973
+ if (!boundPremise) {
974
+ resolverCache.set(variableId, null);
975
+ return null;
976
+ }
977
+ const premiseResult = boundPremise.evaluate(assignment, {
978
+ resolver,
979
+ });
980
+ const value = premiseResult?.rootValue ?? null;
981
+ resolverCache.set(variableId, value);
982
+ return value;
983
+ };
997
984
  const evalOpts = {
998
985
  strictUnknownKeys: options?.strictUnknownAssignmentKeys ?? false,
986
+ resolver,
999
987
  };
1000
988
  const conclusionEvaluation = conclusion.evaluate(assignment, evalOpts);
1001
989
  const supportingEvaluations = supportingPremises.map((pm) => pm.evaluate(assignment, evalOpts));
@@ -1075,7 +1063,7 @@ export class ArgumentEngine {
1075
1063
  const supportingIds = new Set(supportingPremises.map((pm) => pm.getId()));
1076
1064
  const constraintPremises = this.listPremises().filter((pm) => pm.getId() !== this.conclusionPremiseId &&
1077
1065
  !supportingIds.has(pm.getId()));
1078
- const checkedVariableIds = [
1066
+ const allVariableIdsForCheck = [
1079
1067
  ...new Set([
1080
1068
  conclusion,
1081
1069
  ...supportingPremises,
@@ -1085,6 +1073,12 @@ export class ArgumentEngine {
1085
1073
  .filter((expr) => expr.type === "variable")
1086
1074
  .map((expr) => expr.variableId))),
1087
1075
  ].sort();
1076
+ // Only claim-bound variables get truth-table columns;
1077
+ // premise-bound variables are resolved lazily from their bound premise.
1078
+ const checkedVariableIds = allVariableIdsForCheck.filter((vid) => {
1079
+ const v = this.variables.getVariable(vid);
1080
+ return v != null && isClaimBound(v);
1081
+ });
1088
1082
  if (options?.maxVariables !== undefined &&
1089
1083
  checkedVariableIds.length > options.maxVariables) {
1090
1084
  return {