pr-1-feedback #2
@ -1,8 +1,11 @@
|
|||||||
|
import Debug from 'debug';
|
||||||
import { Delta } from '../src/core';
|
import { Delta } from '../src/core';
|
||||||
import { NegationHelper } from '../src/features';
|
import { NegationHelper } from '../src/features';
|
||||||
import { RhizomeNode } from '../src/node';
|
import { RhizomeNode } from '../src/node';
|
||||||
import { Lossless } from '../src/views';
|
import { Lossless } from '../src/views';
|
||||||
|
|
||||||
|
const debug = Debug('rz:negation:test');
|
||||||
|
|
||||||
describe('Negation System', () => {
|
describe('Negation System', () => {
|
||||||
let node: RhizomeNode;
|
let node: RhizomeNode;
|
||||||
let lossless: Lossless;
|
let lossless: Lossless;
|
||||||
@ -442,32 +445,6 @@ describe('Negation System', () => {
|
|||||||
expect(stats.negationDeltas).toBe(0); // No negations for this entity
|
expect(stats.negationDeltas).toBe(0); // No negations for this entity
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple negations and un-negations', () => {
|
|
||||||
const originalDelta = new Delta({
|
|
||||||
creator: 'user1',
|
|
||||||
host: 'host1',
|
|
||||||
pointers: [
|
|
||||||
{ localContext: 'visible', target: 'item1', targetContext: 'visible' },
|
|
||||||
{ localContext: 'value', target: true }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1');
|
|
||||||
const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1');
|
|
||||||
|
|
||||||
lossless.ingestDelta(originalDelta);
|
|
||||||
lossless.ingestDelta(negation1);
|
|
||||||
lossless.ingestDelta(negation2);
|
|
||||||
|
|
||||||
// Delta should be thoroughly negated
|
|
||||||
const view = lossless.view(['item1']);
|
|
||||||
expect(view.item1).toBeUndefined();
|
|
||||||
|
|
||||||
const stats = lossless.getNegationStats('item1');
|
|
||||||
expect(stats.negatedDeltas).toBe(1);
|
|
||||||
expect(stats.negationDeltas).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle self-referential entities in negations', () => {
|
it('should handle self-referential entities in negations', () => {
|
||||||
// Create a delta that references itself
|
// Create a delta that references itself
|
||||||
const selfRefDelta = new Delta({
|
const selfRefDelta = new Delta({
|
||||||
@ -487,5 +464,165 @@ describe('Negation System', () => {
|
|||||||
const view = lossless.view(['node1']);
|
const view = lossless.view(['node1']);
|
||||||
expect(view.node1).toBeUndefined(); // Should be negated
|
expect(view.node1).toBeUndefined(); // Should be negated
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle multiple direct negations of the same delta', () => {
|
||||||
|
const testNode = new RhizomeNode();
|
||||||
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
|
// Create the original delta
|
||||||
|
const originalDelta = new Delta({
|
||||||
|
creator: 'user1',
|
||||||
|
host: 'host1',
|
||||||
|
pointers: [
|
||||||
|
{ localContext: 'title', target: 'entity2', targetContext: 'title' },
|
||||||
|
{ localContext: 'status', target: 'Draft' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create two negations of the same delta
|
||||||
|
const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1');
|
||||||
|
const negation2 = NegationHelper.createNegation(originalDelta.id, 'user3', 'host1');
|
||||||
|
|
||||||
|
// Process all deltas
|
||||||
|
testLossless.ingestDelta(originalDelta);
|
||||||
|
testLossless.ingestDelta(negation1);
|
||||||
|
testLossless.ingestDelta(negation2);
|
||||||
|
|
||||||
|
// Get the view after processing all deltas
|
||||||
|
const view = testLossless.view(['entity2']);
|
||||||
|
|
||||||
|
// The original delta should be negated (not in view) because it has two direct negations
|
||||||
|
expect(view.entity2).toBeUndefined();
|
||||||
|
|
||||||
|
// Verify the stats
|
||||||
|
const stats = testLossless.getNegationStats('entity2');
|
||||||
|
expect(stats.negationDeltas).toBe(2);
|
||||||
|
expect(stats.negatedDeltas).toBe(1);
|
||||||
|
expect(stats.effectiveDeltas).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex negation chains', () => {
|
||||||
|
const testNode = new RhizomeNode();
|
||||||
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
|
// Create the original delta
|
||||||
|
const deltaA = new Delta({
|
||||||
|
creator: 'user1',
|
||||||
|
host: 'host1',
|
||||||
|
pointers: [
|
||||||
|
{ localContext: 'content', target: 'entity3', targetContext: 'content' },
|
||||||
|
{ localContext: 'text', target: 'Hello World' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a chain of negations: B negates A, C negates B, D negates C
|
||||||
|
const deltaB = NegationHelper.createNegation(deltaA.id, 'user2', 'host1');
|
||||||
|
const deltaC = NegationHelper.createNegation(deltaB.id, 'user3', 'host1');
|
||||||
|
const deltaD = NegationHelper.createNegation(deltaC.id, 'user4', 'host1');
|
||||||
|
|
||||||
|
debug('Delta A (original): %s', deltaA.id);
|
||||||
|
debug('Delta B (negates A): %s', deltaB.id);
|
||||||
|
debug('Delta C (negates B): %s', deltaC.id);
|
||||||
|
debug('Delta D (negates C): %s', deltaD.id);
|
||||||
|
|
||||||
|
// Process all deltas in order
|
||||||
|
testLossless.ingestDelta(deltaA);
|
||||||
|
testLossless.ingestDelta(deltaB);
|
||||||
|
testLossless.ingestDelta(deltaC);
|
||||||
|
testLossless.ingestDelta(deltaD);
|
||||||
|
|
||||||
|
// Get the view after processing all deltas
|
||||||
|
const view = testLossless.view(['entity3']);
|
||||||
|
|
||||||
|
// The original delta should be negated because:
|
||||||
|
// - B negates A
|
||||||
|
// - C negates B (so A is no longer negated)
|
||||||
|
// - D negates C (so B is no longer negated, and A is negated again by B)
|
||||||
|
expect(view.entity3).toBeUndefined();
|
||||||
|
|
||||||
|
// Get all deltas for the entity
|
||||||
|
const allDeltas = [deltaA, deltaB, deltaC, deltaD];
|
||||||
|
|
||||||
|
// Get the stats
|
||||||
|
const stats = testLossless.getNegationStats('entity3');
|
||||||
|
const isANegated = NegationHelper.isDeltaNegated(deltaA.id, allDeltas);
|
||||||
|
const isBNegated = NegationHelper.isDeltaNegated(deltaB.id, allDeltas);
|
||||||
|
const isCNegated = NegationHelper.isDeltaNegated(deltaC.id, allDeltas);
|
||||||
|
const isDNegated = NegationHelper.isDeltaNegated(deltaD.id, allDeltas);
|
||||||
|
|
||||||
|
debug('Delta statuses:');
|
||||||
|
debug('- A (%s): %s', deltaA.id, isANegated ? 'NEGATED' : 'ACTIVE');
|
||||||
|
debug('- B (%s): %s, negates: %s', deltaB.id, isBNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaB));
|
||||||
|
debug('- C (%s): %s, negates: %s', deltaC.id, isCNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaC));
|
||||||
|
debug('- D (%s): %s, negates: %s', deltaD.id, isDNegated ? 'NEGATED' : 'ACTIVE', NegationHelper.getNegatedDeltaId(deltaD));
|
||||||
|
|
||||||
|
debug('Negation stats: %O', {
|
||||||
|
totalDeltas: stats.totalDeltas,
|
||||||
|
negationDeltas: stats.negationDeltas,
|
||||||
|
negatedDeltas: stats.negatedDeltas,
|
||||||
|
effectiveDeltas: stats.effectiveDeltas,
|
||||||
|
negationsByProperty: stats.negationsByProperty
|
||||||
|
});
|
||||||
|
|
||||||
|
// B, C, D are negation deltas
|
||||||
|
expect(stats.negationDeltas).toBe(3);
|
||||||
|
|
||||||
|
// A and C are effectively negated
|
||||||
|
expect(isANegated).toBe(true);
|
||||||
|
expect(isCNegated).toBe(true);
|
||||||
|
|
||||||
|
// B and D are not negated (they are negation deltas that are not themselves negated)
|
||||||
|
expect(isBNegated).toBe(false);
|
||||||
|
expect(isDNegated).toBe(false);
|
||||||
|
|
||||||
|
// No deltas remain unnegated
|
||||||
|
expect(stats.effectiveDeltas).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple independent negations', () => {
|
||||||
|
const testNode = new RhizomeNode();
|
||||||
|
const testLossless = new Lossless(testNode);
|
||||||
|
|
||||||
|
// Create two independent deltas
|
||||||
|
const delta1 = new Delta({
|
||||||
|
creator: 'user1',
|
||||||
|
host: 'host1',
|
||||||
|
pointers: [
|
||||||
|
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
|
||||||
|
{ localContext: 'name', target: 'Item 1' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const delta2 = new Delta({
|
||||||
|
creator: 'user2',
|
||||||
|
host: 'host1',
|
||||||
|
pointers: [
|
||||||
|
{ localContext: 'item', target: 'entity4', targetContext: 'item' },
|
||||||
|
{ localContext: 'name', target: 'Item 2' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create negations for both deltas
|
||||||
|
const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1');
|
||||||
|
const negation2 = NegationHelper.createNegation(delta2.id, 'user4', 'host1');
|
||||||
|
|
||||||
|
// Process all deltas
|
||||||
|
testLossless.ingestDelta(delta1);
|
||||||
|
testLossless.ingestDelta(delta2);
|
||||||
|
testLossless.ingestDelta(negation1);
|
||||||
|
testLossless.ingestDelta(negation2);
|
||||||
|
|
||||||
|
// Get the view after processing all deltas
|
||||||
|
const view = testLossless.view(['entity4']);
|
||||||
|
|
||||||
|
// Both deltas should be negated
|
||||||
|
expect(view.entity4).toBeUndefined();
|
||||||
|
|
||||||
|
// Verify the stats
|
||||||
|
const stats = testLossless.getNegationStats('entity4');
|
||||||
|
expect(stats.negationDeltas).toBe(2);
|
||||||
|
expect(stats.negatedDeltas).toBe(2);
|
||||||
|
expect(stats.effectiveDeltas).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -81,38 +81,140 @@ export class NegationHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a delta is negated by any negation deltas
|
* Check if a delta is negated by any negation deltas
|
||||||
|
* @param deltaId The ID of the delta to check
|
||||||
|
* @param deltas The list of all deltas to consider
|
||||||
|
* @returns True if the delta is effectively negated, false otherwise
|
||||||
*/
|
*/
|
||||||
static isDeltaNegated(deltaId: DeltaID, deltas: Delta[]): boolean {
|
static isDeltaNegated(deltaId: DeltaID, deltas: Delta[]): boolean {
|
||||||
return this.findNegationsFor(deltaId, deltas).length > 0;
|
// Create a map of delta ID to its negation status
|
||||||
}
|
const deltaStatus = new Map<DeltaID, boolean>();
|
||||||
|
// Create a map of delta ID to its negation deltas
|
||||||
|
const deltaToNegations = new Map<DeltaID, Delta[]>();
|
||||||
|
|
||||||
/**
|
// First pass: collect all deltas and their negations
|
||||||
* Filter out negated deltas from a list
|
|
||||||
* Returns deltas that are not negated by any negation deltas in the list
|
|
||||||
*/
|
|
||||||
static filterNegatedDeltas(deltas: Delta[]): Delta[] {
|
|
||||||
const negatedDeltaIds = new Set<DeltaID>();
|
|
||||||
|
|
||||||
// First pass: collect all negated delta IDs
|
|
||||||
for (const delta of deltas) {
|
for (const delta of deltas) {
|
||||||
if (this.isNegationDelta(delta)) {
|
if (this.isNegationDelta(delta)) {
|
||||||
const negatedId = this.getNegatedDeltaId(delta);
|
const negatedId = this.getNegatedDeltaId(delta);
|
||||||
if (negatedId) {
|
if (negatedId) {
|
||||||
negatedDeltaIds.add(negatedId);
|
if (!deltaToNegations.has(negatedId)) {
|
||||||
|
deltaToNegations.set(negatedId, []);
|
||||||
|
}
|
||||||
|
deltaToNegations.get(negatedId)!.push(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: filter out negated deltas and negation deltas themselves
|
// Function to determine if a delta is effectively negated
|
||||||
|
const isEffectivelyNegated = (currentDeltaId: DeltaID, visited: Set<DeltaID> = new Set()): boolean => {
|
||||||
|
// Avoid infinite recursion in case of cycles
|
||||||
|
if (visited.has(currentDeltaId)) {
|
||||||
|
return false; // If we've seen this delta before, assume it's not negated to break the cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've already determined this delta's status
|
||||||
|
if (deltaStatus.has(currentDeltaId)) {
|
||||||
|
return deltaStatus.get(currentDeltaId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all negations targeting this delta
|
||||||
|
const negations = deltaToNegations.get(currentDeltaId) || [];
|
||||||
|
|
||||||
|
// If there are no negations, the delta is not negated
|
||||||
|
if (negations.length === 0) {
|
||||||
|
deltaStatus.set(currentDeltaId, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each negation to see if it's effectively applied
|
||||||
|
// A negation is effective if it's not itself negated
|
||||||
|
for (const negation of negations) {
|
||||||
|
// If the negation delta is not itself negated, then the target is negated
|
||||||
|
if (!isEffectivelyNegated(negation.id, new Set([...visited, currentDeltaId]))) {
|
||||||
|
deltaStatus.set(currentDeltaId, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all negations are themselves negated, the delta is not negated
|
||||||
|
deltaStatus.set(currentDeltaId, false);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the target delta is negated
|
||||||
|
return isEffectivelyNegated(deltaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out negated deltas from a list, handling both direct and indirect negations
|
||||||
|
* Returns deltas that are not effectively negated by any chain of negations
|
||||||
|
*/
|
||||||
|
static filterNegatedDeltas(deltas: Delta[]): Delta[] {
|
||||||
|
// Create a map of delta ID to its negation status
|
||||||
|
const deltaStatus = new Map<DeltaID, boolean>();
|
||||||
|
// Create a map of delta ID to its negation deltas
|
||||||
|
const deltaToNegations = new Map<DeltaID, NegationDelta[]>();
|
||||||
|
|
||||||
|
// First pass: collect all deltas and their negations
|
||||||
|
for (const delta of deltas) {
|
||||||
|
if (this.isNegationDelta(delta)) {
|
||||||
|
const negatedId = this.getNegatedDeltaId(delta);
|
||||||
|
if (negatedId) {
|
||||||
|
if (!deltaToNegations.has(negatedId)) {
|
||||||
|
deltaToNegations.set(negatedId, []);
|
||||||
|
}
|
||||||
|
deltaToNegations.get(negatedId)!.push(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to determine if a delta is effectively negated
|
||||||
|
const isEffectivelyNegated = (deltaId: DeltaID, visited: Set<DeltaID> = new Set()): boolean => {
|
||||||
|
// Avoid infinite recursion in case of cycles
|
||||||
|
if (visited.has(deltaId)) {
|
||||||
|
return false; // If we've seen this delta before, assume it's not negated to break the cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've already determined this delta's status
|
||||||
|
if (deltaStatus.has(deltaId)) {
|
||||||
|
return deltaStatus.get(deltaId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all negations targeting this delta
|
||||||
|
const negations = deltaToNegations.get(deltaId) || [];
|
||||||
|
|
||||||
|
// If there are no negations, the delta is not negated
|
||||||
|
if (negations.length === 0) {
|
||||||
|
deltaStatus.set(deltaId, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each negation to see if it's effectively applied
|
||||||
|
// A negation is effective if it's not itself negated
|
||||||
|
for (const negation of negations) {
|
||||||
|
// If the negation delta is not itself negated, then the target is negated
|
||||||
|
if (!isEffectivelyNegated(negation.id, new Set([...visited, deltaId]))) {
|
||||||
|
deltaStatus.set(deltaId, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all negations are themselves negated, the delta is not negated
|
||||||
|
deltaStatus.set(deltaId, false);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second pass: filter out effectively negated deltas and all negation deltas
|
||||||
return deltas.filter(delta => {
|
return deltas.filter(delta => {
|
||||||
// Exclude negation deltas themselves (they're metadata)
|
// Always exclude negation deltas (they're metadata)
|
||||||
if (this.isNegationDelta(delta)) {
|
if (this.isNegationDelta(delta)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude deltas that have been negated
|
// Check if this delta is effectively negated
|
||||||
if (negatedDeltaIds.has(delta.id)) {
|
const isNegated = isEffectivelyNegated(delta.id);
|
||||||
debug(`Filtering out negated delta ${delta.id}`);
|
|
||||||
|
if (isNegated) {
|
||||||
|
debug(`Filtering out effectively negated delta ${delta.id}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,37 +230,158 @@ export class NegationHelper {
|
|||||||
negationDeltas: number;
|
negationDeltas: number;
|
||||||
negatedDeltas: number;
|
negatedDeltas: number;
|
||||||
effectiveDeltas: number;
|
effectiveDeltas: number;
|
||||||
negatedDeltaIds: DeltaID[];
|
negationsByProperty: { [key: string]: { negated: number; total: number } };
|
||||||
negationMap: Map<DeltaID, DeltaID[]>; // negated -> [negating deltas]
|
negatedDeltaIds: string[];
|
||||||
|
negationMap: Map<DeltaID, DeltaID[]>;
|
||||||
} {
|
} {
|
||||||
const negationDeltas = deltas.filter(d => this.isNegationDelta(d));
|
const negationDeltas = deltas.filter(d => this.isNegationDelta(d));
|
||||||
const negatedDeltaIds = new Set<DeltaID>();
|
|
||||||
const negationMap = new Map<DeltaID, DeltaID[]>();
|
const negationMap = new Map<DeltaID, DeltaID[]>();
|
||||||
|
const deltaById = new Map<DeltaID, Delta>();
|
||||||
|
const properties = new Set<string>();
|
||||||
|
const negatedDeltaIds = new Set<string>();
|
||||||
|
|
||||||
for (const negDelta of negationDeltas) {
|
// Build maps and collect properties
|
||||||
const negatedId = this.getNegatedDeltaId(negDelta);
|
for (const delta of deltas) {
|
||||||
|
deltaById.set(delta.id, delta);
|
||||||
|
|
||||||
|
// Collect all properties referenced in the delta
|
||||||
|
if (delta.pointers) {
|
||||||
|
for (const pointer of delta.pointers) {
|
||||||
|
if (pointer.targetContext) {
|
||||||
|
properties.add(pointer.targetContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNegationDelta(delta)) {
|
||||||
|
const negatedId = this.getNegatedDeltaId(delta);
|
||||||
if (negatedId) {
|
if (negatedId) {
|
||||||
negatedDeltaIds.add(negatedId);
|
|
||||||
|
|
||||||
if (!negationMap.has(negatedId)) {
|
if (!negationMap.has(negatedId)) {
|
||||||
negationMap.set(negatedId, []);
|
negationMap.set(negatedId, []);
|
||||||
}
|
}
|
||||||
negationMap.get(negatedId)!.push(negDelta.id);
|
negationMap.get(negatedId)!.push(delta.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveDeltas = deltas.length - negationDeltas.length - negatedDeltaIds.size;
|
// Track which deltas are effectively negated
|
||||||
|
const deltaStatus = new Map<DeltaID, boolean>();
|
||||||
|
|
||||||
|
// Function to determine if a delta is effectively negated
|
||||||
|
const isEffectivelyNegated = (deltaId: DeltaID, visited: Set<DeltaID> = new Set()): boolean => {
|
||||||
|
// Avoid infinite recursion in case of cycles
|
||||||
|
if (visited.has(deltaId)) {
|
||||||
|
return false; // If we've seen this delta before, assume it's not negated to break the cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've already determined this delta's status
|
||||||
|
if (deltaStatus.has(deltaId)) {
|
||||||
|
return deltaStatus.get(deltaId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all negations targeting this delta
|
||||||
|
const negations = negationMap.get(deltaId) || [];
|
||||||
|
|
||||||
|
// If there are no negations, the delta is not negated
|
||||||
|
if (negations.length === 0) {
|
||||||
|
deltaStatus.set(deltaId, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each negation to see if it's effectively applied
|
||||||
|
// A negation is effective if it's not itself negated
|
||||||
|
for (const negationId of negations) {
|
||||||
|
// If the negation delta is not itself negated, then the target is negated
|
||||||
|
if (!isEffectivelyNegated(negationId, new Set([...visited, deltaId]))) {
|
||||||
|
deltaStatus.set(deltaId, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all negations are themselves negated, the delta is not negated
|
||||||
|
deltaStatus.set(deltaId, false);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First pass: determine status of all deltas
|
||||||
|
for (const delta of deltas) {
|
||||||
|
isEffectivelyNegated(delta.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate statistics
|
||||||
|
let effectiveDeltas = 0;
|
||||||
|
const negationsByProperty: { [key: string]: { negated: number; total: number } } = {};
|
||||||
|
|
||||||
|
// Initialize property counters
|
||||||
|
for (const prop of properties) {
|
||||||
|
negationsByProperty[prop] = { negated: 0, total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: count negated and effective deltas
|
||||||
|
for (const delta of deltas) {
|
||||||
|
const isNegation = this.isNegationDelta(delta);
|
||||||
|
const isNegated = deltaStatus.get(delta.id) || false;
|
||||||
|
|
||||||
|
if (isNegated) {
|
||||||
|
// For non-negation deltas, add them to the negated set
|
||||||
|
if (!isNegation) {
|
||||||
|
negatedDeltaIds.add(delta.id);
|
||||||
|
} else {
|
||||||
|
// For negation deltas, add the delta they negate (if it's not a negation delta)
|
||||||
|
const negatedId = this.getNegatedDeltaId(delta);
|
||||||
|
if (negatedId) {
|
||||||
|
const negatedDelta = deltaById.get(negatedId);
|
||||||
|
if (negatedDelta && !this.isNegationDelta(negatedDelta)) {
|
||||||
|
negatedDeltaIds.add(negatedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNegation) {
|
||||||
|
if (isNegated) {
|
||||||
|
// Already counted in negatedDeltaIds
|
||||||
|
} else {
|
||||||
|
effectiveDeltas++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update property-based statistics
|
||||||
|
for (const delta of deltas) {
|
||||||
|
const isNegated = deltaStatus.get(delta.id) || false;
|
||||||
|
|
||||||
|
if (delta.pointers) {
|
||||||
|
for (const pointer of delta.pointers) {
|
||||||
|
if (pointer.targetContext && negationsByProperty[pointer.targetContext] !== undefined) {
|
||||||
|
negationsByProperty[pointer.targetContext].total++;
|
||||||
|
if (isNegated) {
|
||||||
|
negationsByProperty[pointer.targetContext].negated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalDeltas: deltas.length,
|
totalDeltas: deltas.length,
|
||||||
negationDeltas: negationDeltas.length,
|
negationDeltas: negationDeltas.length,
|
||||||
negatedDeltas: negatedDeltaIds.size,
|
negatedDeltas: negatedDeltaIds.size,
|
||||||
effectiveDeltas,
|
effectiveDeltas,
|
||||||
|
negationsByProperty,
|
||||||
negatedDeltaIds: Array.from(negatedDeltaIds),
|
negatedDeltaIds: Array.from(negatedDeltaIds),
|
||||||
negationMap
|
negationMap
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check if a delta with the given ID is a negation delta
|
||||||
|
*/
|
||||||
|
private static isNegationDeltaById(deltaId: DeltaID, deltas: Delta[]): boolean {
|
||||||
|
const delta = deltas.find(d => d.id === deltaId);
|
||||||
|
return delta ? this.isNegationDelta(delta) : false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply negations to a delta stream in chronological order
|
* Apply negations to a delta stream in chronological order
|
||||||
* Later negations can override earlier ones
|
* Later negations can override earlier ones
|
||||||
|
Loading…
x
Reference in New Issue
Block a user