@polintpro/proposit-core 0.3.0 → 0.4.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 (100) hide show
  1. package/README.md +13 -0
  2. package/dist/cli/commands/sources.d.ts +3 -0
  3. package/dist/cli/commands/sources.d.ts.map +1 -0
  4. package/dist/cli/commands/sources.js +175 -0
  5. package/dist/cli/commands/sources.js.map +1 -0
  6. package/dist/cli/config.d.ts +2 -0
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +6 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/engine.d.ts.map +1 -1
  11. package/dist/cli/engine.js +39 -1
  12. package/dist/cli/engine.js.map +1 -1
  13. package/dist/cli/schemata.d.ts +8 -0
  14. package/dist/cli/schemata.d.ts.map +1 -1
  15. package/dist/cli/schemata.js +10 -0
  16. package/dist/cli/schemata.js.map +1 -1
  17. package/dist/cli/storage/sources.d.ts +11 -0
  18. package/dist/cli/storage/sources.d.ts.map +1 -0
  19. package/dist/cli/storage/sources.js +105 -0
  20. package/dist/cli/storage/sources.js.map +1 -0
  21. package/dist/cli.js +2 -0
  22. package/dist/cli.js.map +1 -1
  23. package/dist/extensions/ieee/index.d.ts +3 -0
  24. package/dist/extensions/ieee/index.d.ts.map +1 -0
  25. package/dist/extensions/ieee/index.js +3 -0
  26. package/dist/extensions/ieee/index.js.map +1 -0
  27. package/dist/extensions/ieee/references.d.ts +620 -0
  28. package/dist/extensions/ieee/references.d.ts.map +1 -0
  29. package/dist/extensions/ieee/references.js +450 -0
  30. package/dist/extensions/ieee/references.js.map +1 -0
  31. package/dist/extensions/ieee/source.d.ts +286 -0
  32. package/dist/extensions/ieee/source.d.ts.map +1 -0
  33. package/dist/extensions/ieee/source.js +12 -0
  34. package/dist/extensions/ieee/source.js.map +1 -0
  35. package/dist/lib/consts.d.ts.map +1 -1
  36. package/dist/lib/consts.js +19 -0
  37. package/dist/lib/consts.js.map +1 -1
  38. package/dist/lib/core/argument-engine.d.ts +42 -134
  39. package/dist/lib/core/argument-engine.d.ts.map +1 -1
  40. package/dist/lib/core/argument-engine.js +299 -116
  41. package/dist/lib/core/argument-engine.js.map +1 -1
  42. package/dist/lib/core/change-collector.d.ts +12 -2
  43. package/dist/lib/core/change-collector.d.ts.map +1 -1
  44. package/dist/lib/core/change-collector.js +27 -0
  45. package/dist/lib/core/change-collector.js.map +1 -1
  46. package/dist/lib/core/diff.d.ts +8 -2
  47. package/dist/lib/core/diff.d.ts.map +1 -1
  48. package/dist/lib/core/diff.js +58 -0
  49. package/dist/lib/core/diff.js.map +1 -1
  50. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts +334 -0
  51. package/dist/lib/core/interfaces/argument-engine.interfaces.d.ts.map +1 -0
  52. package/dist/lib/core/interfaces/argument-engine.interfaces.js +2 -0
  53. package/dist/lib/core/interfaces/argument-engine.interfaces.js.map +1 -0
  54. package/dist/lib/core/interfaces/index.d.ts +5 -0
  55. package/dist/lib/core/interfaces/index.d.ts.map +1 -0
  56. package/dist/lib/core/interfaces/index.js +2 -0
  57. package/dist/lib/core/interfaces/index.js.map +1 -0
  58. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts +317 -0
  59. package/dist/lib/core/interfaces/premise-engine.interfaces.d.ts.map +1 -0
  60. package/dist/lib/core/interfaces/premise-engine.interfaces.js +2 -0
  61. package/dist/lib/core/interfaces/premise-engine.interfaces.js.map +1 -0
  62. package/dist/lib/core/interfaces/shared.interfaces.d.ts +24 -0
  63. package/dist/lib/core/interfaces/shared.interfaces.d.ts.map +1 -0
  64. package/dist/lib/core/interfaces/shared.interfaces.js +2 -0
  65. package/dist/lib/core/interfaces/shared.interfaces.js.map +1 -0
  66. package/dist/lib/core/interfaces/source-management.interfaces.d.ts +110 -0
  67. package/dist/lib/core/interfaces/source-management.interfaces.d.ts.map +1 -0
  68. package/dist/lib/core/interfaces/source-management.interfaces.js +2 -0
  69. package/dist/lib/core/interfaces/source-management.interfaces.js.map +1 -0
  70. package/dist/lib/core/premise-engine.d.ts +19 -153
  71. package/dist/lib/core/premise-engine.d.ts.map +1 -1
  72. package/dist/lib/core/premise-engine.js +95 -143
  73. package/dist/lib/core/premise-engine.js.map +1 -1
  74. package/dist/lib/core/source-manager.d.ts +109 -0
  75. package/dist/lib/core/source-manager.d.ts.map +1 -0
  76. package/dist/lib/core/source-manager.js +420 -0
  77. package/dist/lib/core/source-manager.js.map +1 -0
  78. package/dist/lib/index.d.ts +4 -1
  79. package/dist/lib/index.d.ts.map +1 -1
  80. package/dist/lib/index.js +2 -1
  81. package/dist/lib/index.js.map +1 -1
  82. package/dist/lib/schemata/index.d.ts +1 -0
  83. package/dist/lib/schemata/index.d.ts.map +1 -1
  84. package/dist/lib/schemata/index.js +1 -0
  85. package/dist/lib/schemata/index.js.map +1 -1
  86. package/dist/lib/schemata/source.d.ts +28 -0
  87. package/dist/lib/schemata/source.d.ts.map +1 -0
  88. package/dist/lib/schemata/source.js +35 -0
  89. package/dist/lib/schemata/source.js.map +1 -0
  90. package/dist/lib/types/checksum.d.ts +6 -0
  91. package/dist/lib/types/checksum.d.ts.map +1 -1
  92. package/dist/lib/types/diff.d.ts +9 -3
  93. package/dist/lib/types/diff.d.ts.map +1 -1
  94. package/dist/lib/types/evaluation.d.ts +1 -1
  95. package/dist/lib/types/evaluation.d.ts.map +1 -1
  96. package/dist/lib/types/mutation.d.ts +7 -3
  97. package/dist/lib/types/mutation.d.ts.map +1 -1
  98. package/dist/lib/types/reactive.d.ts +5 -2
  99. package/dist/lib/types/reactive.d.ts.map +1 -1
  100. package/package.json +5 -1
@@ -7,6 +7,7 @@ import { kleeneAnd, kleeneNot } from "./evaluation/kleene.js";
7
7
  import { makeErrorIssue, makeValidationResult, } from "./evaluation/validation.js";
8
8
  import { PremiseEngine } from "./premise-engine.js";
9
9
  import { VariableManager } from "./variable-manager.js";
10
+ import { SourceManager } from "./source-manager.js";
10
11
  /**
11
12
  * Manages a propositional logic argument composed of premises, variable
12
13
  * assignments, and logical roles (supporting premises and a conclusion).
@@ -18,6 +19,7 @@ export class ArgumentEngine {
18
19
  argument;
19
20
  premises;
20
21
  variables;
22
+ sourceManager;
21
23
  conclusionPremiseId;
22
24
  checksumConfig;
23
25
  positionConfig;
@@ -29,6 +31,7 @@ export class ArgumentEngine {
29
31
  argument: true,
30
32
  variables: true,
31
33
  roles: true,
34
+ sources: true,
32
35
  premiseIds: new Set(),
33
36
  allPremises: true,
34
37
  };
@@ -40,15 +43,12 @@ export class ArgumentEngine {
40
43
  checksumConfig: this.checksumConfig,
41
44
  positionConfig: this.positionConfig,
42
45
  });
46
+ this.sourceManager = new SourceManager();
43
47
  this.expressionIndex = new Map();
44
48
  this.conclusionPremiseId = undefined;
45
49
  this.checksumConfig = options?.checksumConfig;
46
50
  this.positionConfig = options?.positionConfig;
47
51
  }
48
- /**
49
- * Registers a listener that is called after every mutation.
50
- * Returns an unsubscribe function.
51
- */
52
52
  subscribe = (listener) => {
53
53
  this.listeners.add(listener);
54
54
  return () => {
@@ -70,6 +70,7 @@ export class ArgumentEngine {
70
70
  !dirty.argument &&
71
71
  !dirty.variables &&
72
72
  !dirty.roles &&
73
+ !dirty.sources &&
73
74
  dirty.premiseIds.size === 0 &&
74
75
  !dirty.allPremises) {
75
76
  return prev;
@@ -108,17 +109,43 @@ export class ArgumentEngine {
108
109
  }
109
110
  }
110
111
  }
112
+ let sourcesRecord;
113
+ let varAssocRecord;
114
+ let exprAssocRecord;
115
+ if (dirty.sources || !prev) {
116
+ sourcesRecord = {};
117
+ for (const s of this.sourceManager.getSources()) {
118
+ sourcesRecord[s.id] = s;
119
+ }
120
+ varAssocRecord = {};
121
+ for (const a of this.sourceManager.getAllVariableSourceAssociations()) {
122
+ varAssocRecord[a.id] = a;
123
+ }
124
+ exprAssocRecord = {};
125
+ for (const a of this.sourceManager.getAllExpressionSourceAssociations()) {
126
+ exprAssocRecord[a.id] = a;
127
+ }
128
+ }
129
+ else {
130
+ sourcesRecord = prev.sources;
131
+ varAssocRecord = prev.variableSourceAssociations;
132
+ exprAssocRecord = prev.expressionSourceAssociations;
133
+ }
111
134
  const snapshot = {
112
135
  argument,
113
136
  variables,
114
137
  premises,
115
138
  roles,
139
+ sources: sourcesRecord,
140
+ variableSourceAssociations: varAssocRecord,
141
+ expressionSourceAssociations: exprAssocRecord,
116
142
  };
117
143
  this.cachedReactiveSnapshot = snapshot;
118
144
  this.reactiveDirty = {
119
145
  argument: false,
120
146
  variables: false,
121
147
  roles: false,
148
+ sources: false,
122
149
  premiseIds: new Set(),
123
150
  allPremises: false,
124
151
  };
@@ -178,12 +205,15 @@ export class ArgumentEngine {
178
205
  this.reactiveDirty.premiseIds.add(p.id);
179
206
  }
180
207
  }
208
+ if (changes.sources ||
209
+ changes.variableSourceAssociations ||
210
+ changes.expressionSourceAssociations) {
211
+ this.reactiveDirty.sources = true;
212
+ }
181
213
  }
182
- /** Returns a shallow copy of the argument metadata with checksum attached. */
183
214
  getArgument() {
184
215
  return { ...this.argument, checksum: this.checksum() };
185
216
  }
186
- /** Renders the argument as a multi-line string with role labels for each premise. */
187
217
  toDisplayString() {
188
218
  const lines = [];
189
219
  const arg = this.getArgument();
@@ -206,19 +236,9 @@ export class ArgumentEngine {
206
236
  }
207
237
  return lines.join("\n");
208
238
  }
209
- /**
210
- * Creates a new premise with an auto-generated UUID and registers it
211
- * with this engine.
212
- */
213
239
  createPremise(extras) {
214
240
  return this.createPremiseWithId(randomUUID(), extras);
215
241
  }
216
- /**
217
- * Creates a premise with a caller-supplied ID and registers it with
218
- * this engine.
219
- *
220
- * @throws If a premise with the given ID already exists.
221
- */
222
242
  createPremiseWithId(id, extras) {
223
243
  if (this.premises.has(id)) {
224
244
  throw new Error(`Premise "${id}" already exists.`);
@@ -233,6 +253,7 @@ export class ArgumentEngine {
233
253
  argument: this.argument,
234
254
  variables: this.variables,
235
255
  expressionIndex: this.expressionIndex,
256
+ sourceManager: this.sourceManager,
236
257
  }, {
237
258
  checksumConfig: this.checksumConfig,
238
259
  positionConfig: this.positionConfig,
@@ -257,21 +278,24 @@ export class ArgumentEngine {
257
278
  changes,
258
279
  };
259
280
  }
260
- /**
261
- * Removes a premise and clears any role assignments that reference it.
262
- * Returns the removed premise data, or `undefined` if not found.
263
- */
264
281
  removePremise(premiseId) {
265
282
  const pm = this.premises.get(premiseId);
266
283
  if (!pm)
267
284
  return { result: undefined, changes: {} };
268
285
  const data = pm.toPremiseData();
269
- // Clean up expression index for removed premise's expressions
286
+ const collector = new ChangeCollector();
287
+ // Clean up expression index and source associations for removed premise's expressions
270
288
  for (const expr of pm.getExpressions()) {
271
289
  this.expressionIndex.delete(expr.id);
290
+ const sourceResult = this.sourceManager.removeAssociationsForExpression(expr.id);
291
+ for (const assoc of sourceResult.removedExpressionAssociations) {
292
+ collector.removedExpressionSourceAssociation(assoc);
293
+ }
294
+ for (const orphan of sourceResult.removedOrphanSources) {
295
+ collector.removedSource(orphan);
296
+ }
272
297
  }
273
298
  this.premises.delete(premiseId);
274
- const collector = new ChangeCollector();
275
299
  collector.removedPremise(data);
276
300
  if (this.conclusionPremiseId === premiseId) {
277
301
  this.conclusionPremiseId = undefined;
@@ -286,31 +310,20 @@ export class ArgumentEngine {
286
310
  changes,
287
311
  };
288
312
  }
289
- /** Returns the premise with the given ID, or `undefined` if not found. */
290
313
  getPremise(premiseId) {
291
314
  return this.premises.get(premiseId);
292
315
  }
293
- /** Returns `true` if a premise with the given ID exists. */
294
316
  hasPremise(premiseId) {
295
317
  return this.premises.has(premiseId);
296
318
  }
297
- /** Returns all premise IDs in lexicographic order. */
298
319
  listPremiseIds() {
299
320
  return Array.from(this.premises.keys()).sort((a, b) => a.localeCompare(b));
300
321
  }
301
- /** Returns all premises in lexicographic ID order. */
302
322
  listPremises() {
303
323
  return this.listPremiseIds()
304
324
  .map((id) => this.premises.get(id))
305
325
  .filter((pm) => pm !== undefined);
306
326
  }
307
- /**
308
- * Registers a propositional variable for use across all premises.
309
- *
310
- * @throws If `variable.symbol` is already in use.
311
- * @throws If `variable.id` already exists.
312
- * @throws If the variable does not belong to this argument.
313
- */
314
327
  addVariable(variable) {
315
328
  if (variable.argumentId !== this.argument.id) {
316
329
  throw new Error(`Variable argumentId "${variable.argumentId}" does not match engine argument ID "${this.argument.id}".`);
@@ -332,12 +345,6 @@ export class ArgumentEngine {
332
345
  changes,
333
346
  };
334
347
  }
335
- /**
336
- * Updates fields on an existing variable. Since all premises share the
337
- * same VariableManager, the update is immediately visible everywhere.
338
- *
339
- * @throws If the new symbol is already in use by a different variable.
340
- */
341
348
  updateVariable(variableId, updates) {
342
349
  const updated = this.variables.updateVariable(variableId, updates);
343
350
  const collector = new ChangeCollector();
@@ -363,10 +370,6 @@ export class ArgumentEngine {
363
370
  changes: collector.toChangeset(),
364
371
  };
365
372
  }
366
- /**
367
- * Removes a variable and cascade-deletes all expressions referencing it
368
- * across every premise (including subtrees and operator collapse).
369
- */
370
373
  removeVariable(variableId) {
371
374
  const variable = this.variables.getVariable(variableId);
372
375
  if (!variable) {
@@ -374,6 +377,8 @@ export class ArgumentEngine {
374
377
  }
375
378
  const collector = new ChangeCollector();
376
379
  // Cascade: delete referencing expressions in every premise
380
+ // (PremiseEngine.removeExpression already cascades expression-source
381
+ // associations via the Task 12 logic)
377
382
  for (const pm of this.listPremises()) {
378
383
  const { changes } = pm.deleteExpressionsUsingVariable(variableId);
379
384
  if (changes.expressions) {
@@ -381,6 +386,24 @@ export class ArgumentEngine {
381
386
  collector.removedExpression(e);
382
387
  }
383
388
  }
389
+ if (changes.expressionSourceAssociations) {
390
+ for (const a of changes.expressionSourceAssociations.removed) {
391
+ collector.removedExpressionSourceAssociation(a);
392
+ }
393
+ }
394
+ if (changes.sources) {
395
+ for (const s of changes.sources.removed) {
396
+ collector.removedSource(s);
397
+ }
398
+ }
399
+ }
400
+ // Cascade: remove variable-source associations
401
+ const varAssocResult = this.sourceManager.removeAssociationsForVariable(variableId);
402
+ for (const assoc of varAssocResult.removedVariableAssociations) {
403
+ collector.removedVariableSourceAssociation(assoc);
404
+ }
405
+ for (const orphan of varAssocResult.removedOrphanSources) {
406
+ collector.removedSource(orphan);
384
407
  }
385
408
  this.variables.removeVariable(variableId);
386
409
  collector.removedVariable(variable);
@@ -394,27 +417,18 @@ export class ArgumentEngine {
394
417
  changes,
395
418
  };
396
419
  }
397
- /** Returns all registered variables sorted by ID. */
398
420
  getVariables() {
399
421
  return this.variables.toArray();
400
422
  }
401
- /** Returns the variable with the given ID, or `undefined` if not found. */
402
423
  getVariable(variableId) {
403
424
  return this.variables.getVariable(variableId);
404
425
  }
405
- /** Returns `true` if a variable with the given ID exists. */
406
426
  hasVariable(variableId) {
407
427
  return this.variables.hasVariable(variableId);
408
428
  }
409
- /** Returns the variable with the given symbol, or `undefined` if not found. */
410
429
  getVariableBySymbol(symbol) {
411
430
  return this.variables.getVariableBySymbol(symbol);
412
431
  }
413
- /**
414
- * Builds a Map keyed by a caller-supplied function over all variables.
415
- * Useful for indexing by extension fields (e.g. statementId).
416
- * The caller should cache the result — this is O(n) per call.
417
- */
418
432
  buildVariableIndex(keyFn) {
419
433
  const map = new Map();
420
434
  for (const v of this.variables.toArray()) {
@@ -422,29 +436,24 @@ export class ArgumentEngine {
422
436
  }
423
437
  return map;
424
438
  }
425
- /** Returns an expression by ID from any premise, or `undefined` if not found. */
426
439
  getExpression(expressionId) {
427
440
  const premiseId = this.expressionIndex.get(expressionId);
428
441
  if (premiseId === undefined)
429
442
  return undefined;
430
443
  return this.premises.get(premiseId)?.getExpression(expressionId);
431
444
  }
432
- /** Returns `true` if an expression with the given ID exists in any premise. */
433
445
  hasExpression(expressionId) {
434
446
  return this.expressionIndex.has(expressionId);
435
447
  }
436
- /** Returns the premise ID that contains the given expression, or `undefined`. */
437
448
  getExpressionPremiseId(expressionId) {
438
449
  return this.expressionIndex.get(expressionId);
439
450
  }
440
- /** Returns the PremiseEngine containing the given expression, or `undefined`. */
441
451
  findPremiseByExpressionId(expressionId) {
442
452
  const premiseId = this.expressionIndex.get(expressionId);
443
453
  if (premiseId === undefined)
444
454
  return undefined;
445
455
  return this.premises.get(premiseId);
446
456
  }
447
- /** Returns all expressions across all premises, sorted by ID. */
448
457
  getAllExpressions() {
449
458
  const all = [];
450
459
  for (const pe of this.listPremises()) {
@@ -452,10 +461,6 @@ export class ArgumentEngine {
452
461
  }
453
462
  return all.sort((a, b) => a.id.localeCompare(b.id));
454
463
  }
455
- /**
456
- * Returns all expressions that reference the given variable ID,
457
- * across all premises.
458
- */
459
464
  getExpressionsByVariableId(variableId) {
460
465
  const result = [];
461
466
  for (const pe of this.listPremises()) {
@@ -471,7 +476,6 @@ export class ArgumentEngine {
471
476
  }
472
477
  return result;
473
478
  }
474
- /** Returns the root expression from each premise that has one. */
475
479
  listRootExpressions() {
476
480
  const roots = [];
477
481
  for (const pe of this.listPremises()) {
@@ -481,7 +485,6 @@ export class ArgumentEngine {
481
485
  }
482
486
  return roots;
483
487
  }
484
- /** Returns the current role assignments (conclusion premise ID only; supporting is derived). */
485
488
  getRoleState() {
486
489
  return {
487
490
  ...(this.conclusionPremiseId !== undefined
@@ -489,11 +492,6 @@ export class ArgumentEngine {
489
492
  : {}),
490
493
  };
491
494
  }
492
- /**
493
- * Designates a premise as the argument's conclusion.
494
- *
495
- * @throws If the premise does not exist.
496
- */
497
495
  setConclusionPremise(premiseId) {
498
496
  const premise = this.premises.get(premiseId);
499
497
  if (!premise) {
@@ -512,7 +510,6 @@ export class ArgumentEngine {
512
510
  changes,
513
511
  };
514
512
  }
515
- /** Clears the conclusion designation. */
516
513
  clearConclusionPremise() {
517
514
  this.conclusionPremiseId = undefined;
518
515
  const roles = this.getRoleState();
@@ -527,21 +524,190 @@ export class ArgumentEngine {
527
524
  changes,
528
525
  };
529
526
  }
530
- /** Returns the conclusion premise, or `undefined` if none is set. */
531
527
  getConclusionPremise() {
532
528
  if (this.conclusionPremiseId === undefined) {
533
529
  return undefined;
534
530
  }
535
531
  return this.premises.get(this.conclusionPremiseId);
536
532
  }
537
- /**
538
- * Returns all supporting premises (derived: inference premises that are
539
- * not the conclusion) in lexicographic ID order.
540
- */
541
533
  listSupportingPremises() {
542
534
  return this.listPremises().filter((pm) => pm.isInference() && pm.getId() !== this.conclusionPremiseId);
543
535
  }
544
- /** Returns a serializable snapshot of the full engine state. */
536
+ // -------------------------------------------------------------------------
537
+ // Source management
538
+ // -------------------------------------------------------------------------
539
+ addSource(source) {
540
+ if (source.argumentId !== this.argument.id) {
541
+ throw new Error(`Source argumentId "${source.argumentId}" does not match engine argument ID "${this.argument.id}".`);
542
+ }
543
+ if (source.argumentVersion !== this.argument.version) {
544
+ throw new Error(`Source argumentVersion ${source.argumentVersion} does not match engine argument version ${this.argument.version}.`);
545
+ }
546
+ const fields = this.checksumConfig?.sourceFields ??
547
+ DEFAULT_CHECKSUM_CONFIG.sourceFields;
548
+ const sourceWithChecksum = {
549
+ ...source,
550
+ checksum: entityChecksum(source, fields),
551
+ };
552
+ this.sourceManager.addSource(sourceWithChecksum);
553
+ const collector = new ChangeCollector();
554
+ collector.addedSource(sourceWithChecksum);
555
+ this.markDirty();
556
+ const changes = collector.toChangeset();
557
+ this.markReactiveDirty(changes);
558
+ this.notifySubscribers();
559
+ return { result: sourceWithChecksum, changes };
560
+ }
561
+ removeSource(sourceId) {
562
+ const source = this.sourceManager.getSource(sourceId);
563
+ if (!source) {
564
+ return { result: undefined, changes: {} };
565
+ }
566
+ const removalResult = this.sourceManager.removeSource(sourceId);
567
+ const collector = new ChangeCollector();
568
+ collector.removedSource(source);
569
+ for (const assoc of removalResult.removedVariableAssociations) {
570
+ collector.removedVariableSourceAssociation(assoc);
571
+ }
572
+ for (const assoc of removalResult.removedExpressionAssociations) {
573
+ collector.removedExpressionSourceAssociation(assoc);
574
+ }
575
+ this.markDirty();
576
+ const changes = collector.toChangeset();
577
+ this.markReactiveDirty(changes);
578
+ this.notifySubscribers();
579
+ return { result: source, changes };
580
+ }
581
+ addVariableSourceAssociation(sourceId, variableId) {
582
+ if (!this.sourceManager.getSource(sourceId)) {
583
+ throw new Error(`Source "${sourceId}" does not exist.`);
584
+ }
585
+ if (!this.variables.hasVariable(variableId)) {
586
+ throw new Error(`Variable "${variableId}" does not exist.`);
587
+ }
588
+ const assoc = {
589
+ id: randomUUID(),
590
+ sourceId,
591
+ variableId,
592
+ argumentId: this.argument.id,
593
+ argumentVersion: this.argument.version,
594
+ checksum: "",
595
+ };
596
+ const fields = this.checksumConfig?.variableSourceAssociationFields ??
597
+ DEFAULT_CHECKSUM_CONFIG.variableSourceAssociationFields;
598
+ const assocWithChecksum = {
599
+ ...assoc,
600
+ checksum: entityChecksum(assoc, fields),
601
+ };
602
+ this.sourceManager.addVariableSourceAssociation(assocWithChecksum);
603
+ const collector = new ChangeCollector();
604
+ collector.addedVariableSourceAssociation(assocWithChecksum);
605
+ this.markDirty();
606
+ const changes = collector.toChangeset();
607
+ this.markReactiveDirty(changes);
608
+ this.notifySubscribers();
609
+ return { result: assocWithChecksum, changes };
610
+ }
611
+ removeVariableSourceAssociation(associationId) {
612
+ const allVarAssocs = this.sourceManager.getAllVariableSourceAssociations();
613
+ if (!allVarAssocs.some((a) => a.id === associationId)) {
614
+ return { result: undefined, changes: {} };
615
+ }
616
+ const removalResult = this.sourceManager.removeVariableSourceAssociation(associationId);
617
+ const collector = new ChangeCollector();
618
+ for (const assoc of removalResult.removedVariableAssociations) {
619
+ collector.removedVariableSourceAssociation(assoc);
620
+ }
621
+ for (const orphan of removalResult.removedOrphanSources) {
622
+ collector.removedSource(orphan);
623
+ }
624
+ this.markDirty();
625
+ const changes = collector.toChangeset();
626
+ this.markReactiveDirty(changes);
627
+ this.notifySubscribers();
628
+ return {
629
+ result: removalResult.removedVariableAssociations[0],
630
+ changes,
631
+ };
632
+ }
633
+ addExpressionSourceAssociation(sourceId, expressionId, premiseId) {
634
+ if (!this.sourceManager.getSource(sourceId)) {
635
+ throw new Error(`Source "${sourceId}" does not exist.`);
636
+ }
637
+ const pm = this.premises.get(premiseId);
638
+ if (!pm) {
639
+ throw new Error(`Premise "${premiseId}" does not exist.`);
640
+ }
641
+ if (!pm.getExpression(expressionId)) {
642
+ throw new Error(`Expression "${expressionId}" does not exist in premise "${premiseId}".`);
643
+ }
644
+ const assoc = {
645
+ id: randomUUID(),
646
+ sourceId,
647
+ expressionId,
648
+ premiseId,
649
+ argumentId: this.argument.id,
650
+ argumentVersion: this.argument.version,
651
+ checksum: "",
652
+ };
653
+ const fields = this.checksumConfig?.expressionSourceAssociationFields ??
654
+ DEFAULT_CHECKSUM_CONFIG.expressionSourceAssociationFields;
655
+ const assocWithChecksum = {
656
+ ...assoc,
657
+ checksum: entityChecksum(assoc, fields),
658
+ };
659
+ this.sourceManager.addExpressionSourceAssociation(assocWithChecksum);
660
+ const collector = new ChangeCollector();
661
+ collector.addedExpressionSourceAssociation(assocWithChecksum);
662
+ this.markDirty();
663
+ const changes = collector.toChangeset();
664
+ this.markReactiveDirty(changes);
665
+ this.notifySubscribers();
666
+ return { result: assocWithChecksum, changes };
667
+ }
668
+ removeExpressionSourceAssociation(associationId) {
669
+ const allExprAssocs = this.sourceManager.getAllExpressionSourceAssociations();
670
+ if (!allExprAssocs.some((a) => a.id === associationId)) {
671
+ return { result: undefined, changes: {} };
672
+ }
673
+ const removalResult = this.sourceManager.removeExpressionSourceAssociation(associationId);
674
+ const collector = new ChangeCollector();
675
+ for (const assoc of removalResult.removedExpressionAssociations) {
676
+ collector.removedExpressionSourceAssociation(assoc);
677
+ }
678
+ for (const orphan of removalResult.removedOrphanSources) {
679
+ collector.removedSource(orphan);
680
+ }
681
+ this.markDirty();
682
+ const changes = collector.toChangeset();
683
+ this.markReactiveDirty(changes);
684
+ this.notifySubscribers();
685
+ return {
686
+ result: removalResult.removedExpressionAssociations[0],
687
+ changes,
688
+ };
689
+ }
690
+ getSources() {
691
+ return this.sourceManager.getSources();
692
+ }
693
+ getSource(sourceId) {
694
+ return this.sourceManager.getSource(sourceId);
695
+ }
696
+ getAssociationsForSource(sourceId) {
697
+ return this.sourceManager.getAssociationsForSource(sourceId);
698
+ }
699
+ getAssociationsForVariable(variableId) {
700
+ return this.sourceManager.getAssociationsForVariable(variableId);
701
+ }
702
+ getAssociationsForExpression(expressionId) {
703
+ return this.sourceManager.getAssociationsForExpression(expressionId);
704
+ }
705
+ getAllVariableSourceAssociations() {
706
+ return this.sourceManager.getAllVariableSourceAssociations();
707
+ }
708
+ getAllExpressionSourceAssociations() {
709
+ return this.sourceManager.getAllExpressionSourceAssociations();
710
+ }
545
711
  snapshot() {
546
712
  return {
547
713
  argument: { ...this.argument },
@@ -554,6 +720,7 @@ export class ArgumentEngine {
554
720
  checksumConfig: this.checksumConfig,
555
721
  positionConfig: this.positionConfig,
556
722
  },
723
+ sources: this.sourceManager.snapshot(),
557
724
  };
558
725
  }
559
726
  /** Creates a new ArgumentEngine from a previously captured snapshot. */
@@ -563,9 +730,13 @@ export class ArgumentEngine {
563
730
  for (const v of snapshot.variables.variables) {
564
731
  engine.addVariable(v);
565
732
  }
733
+ // Restore source manager (before premises, so PremiseEngines get the correct reference)
734
+ if (snapshot.sources) {
735
+ engine.sourceManager = SourceManager.fromSnapshot(snapshot.sources);
736
+ }
566
737
  // Restore premises using PremiseEngine.fromSnapshot
567
738
  for (const premiseSnap of snapshot.premises) {
568
- const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex);
739
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, snapshot.argument, engine.variables, engine.expressionIndex, engine.sourceManager);
569
740
  engine.premises.set(pe.getId(), pe);
570
741
  const premiseId = pe.getId();
571
742
  pe.setOnMutate(() => {
@@ -632,16 +803,18 @@ export class ArgumentEngine {
632
803
  }
633
804
  return engine;
634
805
  }
635
- /** Restores the engine to a previously captured snapshot state. */
636
806
  rollback(snapshot) {
637
807
  this.argument = { ...snapshot.argument };
638
808
  this.checksumConfig = snapshot.config?.checksumConfig;
639
809
  this.positionConfig = snapshot.config?.positionConfig;
640
810
  this.variables = VariableManager.fromSnapshot(snapshot.variables);
811
+ this.sourceManager = snapshot.sources
812
+ ? SourceManager.fromSnapshot(snapshot.sources)
813
+ : new SourceManager();
641
814
  this.premises = new Map();
642
815
  this.expressionIndex = new Map();
643
816
  for (const premiseSnap of snapshot.premises) {
644
- const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex);
817
+ const pe = PremiseEngine.fromSnapshot(premiseSnap, this.argument, this.variables, this.expressionIndex, this.sourceManager);
645
818
  this.premises.set(pe.getId(), pe);
646
819
  }
647
820
  this.conclusionPremiseId = snapshot.conclusionPremiseId;
@@ -657,16 +830,12 @@ export class ArgumentEngine {
657
830
  argument: true,
658
831
  variables: true,
659
832
  roles: true,
833
+ sources: true,
660
834
  premiseIds: new Set(),
661
835
  allPremises: true,
662
836
  };
663
837
  this.notifySubscribers();
664
838
  }
665
- /**
666
- * Returns an argument-level checksum combining argument metadata, role
667
- * state, and all premise checksums. Computed lazily -- only recalculated
668
- * when the engine's own state has changed.
669
- */
670
839
  checksum() {
671
840
  if (this.checksumDirty || this.cachedChecksum === undefined) {
672
841
  this.cachedChecksum = this.computeChecksum();
@@ -689,6 +858,17 @@ export class ArgumentEngine {
689
858
  for (const pe of this.listPremises()) {
690
859
  checksumMap[pe.getId()] = pe.checksum();
691
860
  }
861
+ // Source checksums
862
+ for (const s of this.sourceManager.getSources()) {
863
+ checksumMap[s.id] = s.checksum;
864
+ }
865
+ // Association checksums
866
+ for (const a of this.sourceManager.getAllVariableSourceAssociations()) {
867
+ checksumMap[a.id] = a.checksum;
868
+ }
869
+ for (const a of this.sourceManager.getAllExpressionSourceAssociations()) {
870
+ checksumMap[a.id] = a.checksum;
871
+ }
692
872
  return computeHash(canonicalSerialize(checksumMap));
693
873
  }
694
874
  markDirty() {
@@ -708,10 +888,6 @@ export class ArgumentEngine {
708
888
  checksum: entityChecksum(v, fields),
709
889
  };
710
890
  }
711
- /**
712
- * Collects all variables referenced by expressions across all premises,
713
- * indexed both by variable ID and by symbol.
714
- */
715
891
  collectReferencedVariables() {
716
892
  const byIdTmp = new Map();
717
893
  const bySymbolTmp = new Map();
@@ -758,12 +934,6 @@ export class ArgumentEngine {
758
934
  bySymbol,
759
935
  };
760
936
  }
761
- /**
762
- * Validates that this argument is structurally ready for evaluation:
763
- * a conclusion must be set, all role references must point to existing
764
- * premises, variable ID/symbol mappings must be consistent, and every
765
- * premise must be individually evaluable.
766
- */
767
937
  validateEvaluability() {
768
938
  const issues = [];
769
939
  if (this.conclusionPremiseId === undefined) {
@@ -814,21 +984,44 @@ export class ArgumentEngine {
814
984
  const premiseValidation = premise.validateEvaluability();
815
985
  issues.push(...premiseValidation.issues);
816
986
  }
987
+ // Source validation
988
+ for (const assoc of this.sourceManager.getAllVariableSourceAssociations()) {
989
+ if (!this.variables.hasVariable(assoc.variableId)) {
990
+ issues.push(makeErrorIssue({
991
+ code: "SOURCE_VARIABLE_ASSOCIATION_INVALID_VARIABLE",
992
+ message: `Variable-source association "${assoc.id}" references non-existent variable "${assoc.variableId}".`,
993
+ }));
994
+ }
995
+ }
996
+ for (const assoc of this.sourceManager.getAllExpressionSourceAssociations()) {
997
+ const premise = this.premises.get(assoc.premiseId);
998
+ if (!premise) {
999
+ issues.push(makeErrorIssue({
1000
+ code: "SOURCE_EXPRESSION_ASSOCIATION_INVALID_PREMISE",
1001
+ message: `Expression-source association "${assoc.id}" references non-existent premise "${assoc.premiseId}".`,
1002
+ }));
1003
+ }
1004
+ else if (!premise.getExpression(assoc.expressionId)) {
1005
+ issues.push(makeErrorIssue({
1006
+ code: "SOURCE_EXPRESSION_ASSOCIATION_INVALID_EXPRESSION",
1007
+ message: `Expression-source association "${assoc.id}" references non-existent expression "${assoc.expressionId}" in premise "${assoc.premiseId}".`,
1008
+ }));
1009
+ }
1010
+ }
1011
+ // Orphaned sources (warning, not error)
1012
+ for (const source of this.sourceManager.getSources()) {
1013
+ const assocs = this.sourceManager.getAssociationsForSource(source.id);
1014
+ if (assocs.variable.length === 0 &&
1015
+ assocs.expression.length === 0) {
1016
+ issues.push({
1017
+ severity: "warning",
1018
+ code: "SOURCE_ORPHANED",
1019
+ message: `Source "${source.id}" has no associations.`,
1020
+ });
1021
+ }
1022
+ }
817
1023
  return makeValidationResult(issues);
818
1024
  }
819
- /**
820
- * Evaluates the argument under a three-valued expression assignment.
821
- *
822
- * Variables may be `true`, `false`, or `null` (unknown). Expressions
823
- * listed in `rejectedExpressionIds` evaluate to `false` (children
824
- * skipped). All result flags (`isAdmissibleAssignment`,
825
- * `allSupportingPremisesTrue`, `conclusionTrue`, `isCounterexample`,
826
- * `preservesTruthUnderAssignment`) are three-valued: `null` means
827
- * the result is indeterminate due to unknown variable values.
828
- *
829
- * Returns `{ ok: false }` with validation details if the argument is
830
- * not structurally evaluable.
831
- */
832
1025
  evaluate(assignment, options) {
833
1026
  const validateFirst = options?.validateFirst ?? true;
834
1027
  if (validateFirst) {
@@ -922,16 +1115,6 @@ export class ArgumentEngine {
922
1115
  };
923
1116
  }
924
1117
  }
925
- /**
926
- * Enumerates all 2^n variable assignments and checks for counterexamples.
927
- *
928
- * A counterexample is an admissible assignment where all supporting
929
- * premises are true but the conclusion is false. The argument is valid
930
- * if no counterexamples exist.
931
- *
932
- * Supports early termination (`firstCounterexample` mode) and
933
- * configurable limits on variables and assignments checked.
934
- */
935
1118
  checkValidity(options) {
936
1119
  const validateFirst = options?.validateFirst ?? true;
937
1120
  if (validateFirst) {