refactor: replace NegationHelper.createNegation with DeltaBuilder.negate

- Remove NegationHelper.createNegation in favor of using DeltaBuilder's fluent API
- Update all test cases to use createDelta().negate().buildV1() pattern
- Update documentation to reflect the preferred way to create negation deltas
- Remove unused isNegationDeltaById helper method
This commit is contained in:
Lentil Hoffman 2025-06-21 22:45:27 -05:00
parent 9f27912c4a
commit f4ea2eca39
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
4 changed files with 209 additions and 77 deletions

View File

@ -21,11 +21,9 @@ describe('Negation System', () => {
.setProperty('entity1', 'name', 'Alice')
.buildV1();
const negationDelta = NegationHelper.createNegation(
originalDelta.id,
'moderator',
'host1'
);
const negationDelta = createDelta('moderator', 'host1')
.negate(originalDelta.id)
.buildV1();
expect(negationDelta.creator).toBe('moderator');
expect(negationDelta.pointers).toHaveLength(1);
@ -42,11 +40,9 @@ describe('Negation System', () => {
.setProperty('entity1', 'name', 'Entity 1')
.buildV1();
const negationDelta = NegationHelper.createNegation(
'delta-to-negate',
'moderator',
'host1'
);
const negationDelta = createDelta('moderator', 'host1')
.negate('delta-to-negate')
.buildV1();
expect(NegationHelper.isNegationDelta(regularDelta)).toBe(false);
expect(NegationHelper.isNegationDelta(negationDelta)).toBe(true);
@ -54,11 +50,9 @@ describe('Negation System', () => {
it('should extract negated delta ID', () => {
const targetDeltaId = 'target-delta-123';
const negationDelta = NegationHelper.createNegation(
targetDeltaId,
'moderator',
'host1'
);
const negationDelta = createDelta('moderator', 'host1')
.negate(targetDeltaId)
.buildV1();
const extractedId = NegationHelper.getNegatedDeltaId(negationDelta);
expect(extractedId).toBe(targetDeltaId);
@ -79,9 +73,9 @@ describe('Negation System', () => {
.setProperty('entity1', 'age', 25)
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(delta1.id, 'mod2', 'host1');
const negation3 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
const negation2 = createDelta('mod2', 'host1').negate(delta1.id).buildV1();
const negation3 = createDelta('mod1', 'host1').negate(delta2.id).buildV1();
const allDeltas = [delta1, delta2, negation1, negation2, negation3];
@ -104,7 +98,7 @@ describe('Negation System', () => {
.setProperty('entity1', 'age', 25)
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
const allDeltas = [delta1, delta2, negation1];
expect(NegationHelper.isDeltaNegated(delta1.id, allDeltas)).toBe(true);
@ -124,8 +118,8 @@ describe('Negation System', () => {
.setProperty('entity1', 'email', 'entity1@example.com')
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(delta2.id, 'mod2', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
const negation2 = createDelta('mod2', 'host1').negate(delta2.id).buildV1();
const allDeltas = [delta1, delta2, delta3, negation1, negation2];
const filtered = NegationHelper.filterNegatedDeltas(allDeltas);
@ -144,7 +138,7 @@ describe('Negation System', () => {
.setProperty('entity1', 'age', 25)
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
const allDeltas = [delta1, delta2, negation1];
const stats = NegationHelper.getNegationStats(allDeltas);
@ -166,7 +160,7 @@ describe('Negation System', () => {
.setProperty('entity1', 'status', 'active')
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
negation1.timeCreated = baseTime + 1000; // 1 second later
const delta2 = createDelta('user1', 'host1')
@ -174,7 +168,7 @@ describe('Negation System', () => {
.setProperty('entity1', 'status', 'inactive')
.buildV1();
const negation2 = NegationHelper.createNegation(delta2.id, 'mod1', 'host1');
const negation2 = createDelta('mod1', 'host1').negate(delta2.id).buildV1();
negation2.timeCreated = baseTime + 3000; // 3 seconds later
const allDeltas = [delta1, negation1, delta2, negation2];
@ -193,11 +187,9 @@ describe('Negation System', () => {
.buildV1();
// Create negation delta
const negationDelta = NegationHelper.createNegation(
originalDelta.id,
'moderator',
'host1'
);
const negationDelta = createDelta('moderator', 'host1')
.negate(originalDelta.id)
.buildV1();
// Create another non-negated delta
@ -225,8 +217,8 @@ describe('Negation System', () => {
.setProperty('post1', 'content', 'Original content')
.buildV1();
const negation1 = NegationHelper.createNegation(originalDelta.id, 'mod1', 'host1');
const negation2 = NegationHelper.createNegation(originalDelta.id, 'mod2', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(originalDelta.id).buildV1();
const negation2 = createDelta('mod2', 'host1').negate(originalDelta.id).buildV1();
lossless.ingestDelta(originalDelta);
lossless.ingestDelta(negation1);
@ -247,7 +239,7 @@ describe('Negation System', () => {
.setProperty('article1', 'content', 'Article content')
.buildV1();
const negation1 = NegationHelper.createNegation(delta1.id, 'mod1', 'host1');
const negation1 = createDelta('mod1', 'host1').negate(delta1.id).buildV1();
lossless.ingestDelta(delta1);
lossless.ingestDelta(delta2);
@ -268,11 +260,7 @@ describe('Negation System', () => {
.setProperty('task1', 'status', 'pending')
.buildV1();
const negationDelta = NegationHelper.createNegation(
originalDelta.id,
'admin',
'host1'
);
const negationDelta = createDelta('admin', 'host1').negate(originalDelta.id).buildV1();
lossless.ingestDelta(originalDelta);
lossless.ingestDelta(negationDelta);
@ -299,7 +287,7 @@ describe('Negation System', () => {
.buildV1();
// Create negation delta in same transaction
const negationDelta = NegationHelper.createNegation(originalDelta.id, 'moderator', 'host1');
const negationDelta = createDelta('moderator', 'host1').negate(originalDelta.id).buildV1();
negationDelta.pointers.unshift({
localContext: '_transaction',
target: transactionId,
@ -324,7 +312,7 @@ describe('Negation System', () => {
.buildV1();
// Moderator negates it
const negationDelta = NegationHelper.createNegation(postDelta.id, 'moderator', 'host1');
const negationDelta = createDelta('moderator', 'host1').negate(postDelta.id).buildV1();
negationDelta.timeCreated = baseTime + 1000;
// User edits content (after negation)
@ -351,11 +339,7 @@ describe('Negation System', () => {
describe('Edge Cases', () => {
it('should handle negation of non-existent deltas', () => {
const negationDelta = NegationHelper.createNegation(
'non-existent-delta-id',
'moderator',
'host1'
);
const negationDelta = createDelta('moderator', 'host1').negate('non-existent-delta-id').buildV1();
lossless.ingestDelta(negationDelta);
@ -371,7 +355,7 @@ describe('Negation System', () => {
.setProperty('node1', 'child', 'node1') // Self-reference
.buildV1();
const negationDelta = NegationHelper.createNegation(selfRefDelta.id, 'admin', 'host1');
const negationDelta = createDelta('admin', 'host1').negate(selfRefDelta.id).buildV1();
lossless.ingestDelta(selfRefDelta);
lossless.ingestDelta(negationDelta);
@ -390,8 +374,8 @@ describe('Negation System', () => {
.buildV1();
// Create two negations of the same delta
const negation1 = NegationHelper.createNegation(originalDelta.id, 'user2', 'host1');
const negation2 = NegationHelper.createNegation(originalDelta.id, 'user3', 'host1');
const negation1 = createDelta('user2', 'host1').negate(originalDelta.id).buildV1();
const negation2 = createDelta('user3', 'host1').negate(originalDelta.id).buildV1();
// Process all deltas
testLossless.ingestDelta(originalDelta);
@ -421,9 +405,9 @@ describe('Negation System', () => {
.buildV1();
// 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');
const deltaB = createDelta('user2', 'host1').negate(deltaA.id).buildV1();
const deltaC = createDelta('user3', 'host1').negate(deltaB.id).buildV1();
const deltaD = createDelta('user4', 'host1').negate(deltaC.id).buildV1();
debug('Delta A (original): %s', deltaA.id);
debug('Delta B (negates A): %s', deltaB.id);
@ -498,8 +482,8 @@ describe('Negation System', () => {
.buildV1();
// Create negations for both deltas
const negation1 = NegationHelper.createNegation(delta1.id, 'user3', 'host1');
const negation2 = NegationHelper.createNegation(delta2.id, 'user4', 'host1');
const negation1 = createDelta('user3', 'host1').negate(delta1.id).buildV1();
const negation2 = createDelta('user4', 'host1').negate(delta2.id).buildV1();
// Process all deltas
testLossless.ingestDelta(delta1);

83
delta-patterns/summary.md Normal file
View File

@ -0,0 +1,83 @@
# Delta Patterns in Rhizome-Node
This document outlines the distinct delta patterns identified in the Rhizome-Node test suite.
## 1. Basic Entity Creation
```typescript
createDelta('creator', 'host')
.setProperty('entity1', 'name', 'Alice', 'user')
.buildV1();
```
## 2. Relationship Creation
```typescript
createDelta('creator', 'host')
.addPointer('users', 'alice', 'friends')
.addPointer('friend', 'bob')
.addPointer('type', 'friendship')
.buildV1();
```
## 3. Transaction-Enabled Deltas
```typescript
createDelta('user1', 'host1')
.inTransaction('tx123')
.setProperty('doc1', 'status', 'draft')
.buildV1();
```
## 4. Negation Deltas
```typescript
// Creating a negation delta
const delta = createDelta('user1', 'host1').buildV1();
const negation = createDelta('moderator', 'host1').negate(delta.id).buildV1();
```
## 5. Temporal Deltas
```typescript
createDelta('user1', 'host1')
.withTimestamp(1624233600000)
.setProperty('entity1', 'score', 100, 'game')
.buildV1();
```
## 6. Multi-Property Deltas
```typescript
createDelta('user1', 'host1')
.setProperty('entity1', 'title', 'Hello World', 'post')
.setProperty('entity1', 'content', 'This is a test', 'post')
.setProperty('entity1', 'published', true, 'post')
.buildV1();
```
## 7. Reference-Only Deltas
```typescript
createDelta('system', 'host1')
.addPointer('posts', 'post1', 'recent')
.buildV1();
```
## 8. Bulk Operation Deltas
```typescript
// Multiple entities in a single delta
createDelta('batch', 'host1')
.setProperty('user1', 'status', 'active', 'user')
.setProperty('user2', 'status', 'inactive', 'user')
.buildV1();
```
## 9. Versioned Deltas
```typescript
// V1 format
createDelta('a', 'h').buildV1();
// V2 format
createDelta('a', 'h').buildV2();
```
## Key Observations
- Most deltas follow a fluent builder pattern
- Deltas can be composed of multiple operations (setProperty, addPointer, etc.)
- Support for both V1 and V2 delta formats
- Strong typing and schema validation is commonly used
- Transaction support is built into the delta creation process
- Temporal aspects can be explicitly controlled

90
docs/schema-validation.md Normal file
View File

@ -0,0 +1,90 @@
# Schema Validation in Rhizome-Node
This document explains how schema validation works with deltas in Rhizome-Node.
## Overview
Schema validation in Rhizome-Node is enforced at the `TypedCollection` level when using the `put` method, which validates data before creating deltas. This means:
1. **Local Changes**: When you use `collection.put()`, the data is validated against the schema before any deltas are created and ingested.
2. **Peer Changes**: Deltas received from other peers are ingested without validation by default, which means invalid data can enter the system.
3. **Validation Tracking**: The system tracks which entities are valid/invalid after ingestion.
## Example Usage
```typescript
// 1. Define a schema for users
const userSchema = SchemaBuilder
.create('user')
.name('User')
.property('name', PrimitiveSchemas.requiredString())
.property('email', PrimitiveSchemas.email())
.property('age', PrimitiveSchemas.integer({ minimum: 0 }))
.required('name')
.build();
// 2. Create a typed collection with strict validation
const collection = new TypedCollectionImpl<{
name: string;
email?: string;
age?: number;
}>('users', userSchema, schemaRegistry, {
strictValidation: true // Enable strict validation
});
// Connect to the node
collection.rhizomeConnect(node);
// 3. Local changes - validated on put()
// Valid usage - will pass schema validation
await collection.put('user1', {
name: 'Alice',
email: 'alice@example.com',
age: 30
});
// Invalid usage - will throw SchemaValidationError
await expect(collection.put('user2', {
email: 'invalid-email', // Invalid email format
age: -5 // Negative age
})).rejects.toThrow(SchemaValidationError);
// 4. Peer data - ingested without validation by default
const unsafeDelta = createDelta('peer1', 'peer1')
.setProperty('user3', 'name', 'Bob', 'users')
.setProperty('user3', 'age', 'not-a-number', 'users')
.buildV1();
// This will be ingested without validation
node.lossless.ingestDelta(unsafeDelta);
// 5. Check validation status after the fact
const stats = collection.getValidationStats();
console.log(`Valid: ${stats.validEntities}, Invalid: ${stats.invalidEntities}`);
// Get details about invalid entities
const invalidUsers = collection.getInvalidEntities();
invalidUsers.forEach(user => {
console.log(`User ${user.entityId} is invalid:`, user.errors);
});
```
## Key Points
### Validation Timing
- Schema validation happens in `TypedCollection.put()` before deltas are created
- Deltas from peers are ingested without validation by default
### Validation Modes
- `strictValidation: true`: Throws errors on invalid data (recommended for local changes)
- `strictValidation: false`: Allows invalid data but tracks it (default)
### Monitoring
- Use `getValidationStats()` to get counts of valid/invalid entities
- Use `getInvalidEntities()` to get detailed error information
### Best Practices
- Always validate data before creating deltas when accepting external input
- Use `strictValidation: true` for collections where data integrity is critical
- Monitor validation statistics in production to detect data quality issues
- Consider implementing a validation layer for peer data if needed

View File

@ -14,23 +14,6 @@ export interface NegationPointer {
// Helper functions for creating and identifying negation deltas
export class NegationHelper {
/**
* Create a negation delta that negates another delta
*/
static createNegation(
deltaToNegate: DeltaID,
creator: CreatorID,
host: HostID
): Delta {
const negationDelta = createDelta(creator, host)
.negate(deltaToNegate)
.buildV1();
debug(`Created negation delta ${negationDelta.id} negating ${deltaToNegate}`);
return negationDelta;
}
/**
* Check if a delta is a negation delta
*/
@ -361,14 +344,6 @@ export class NegationHelper {
};
}
/**
* 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
* Later negations can override earlier ones