diff --git a/__tests__/negation.ts b/__tests__/negation.ts index 53e4fa7..6038e1c 100644 --- a/__tests__/negation.ts +++ b/__tests__/negation.ts @@ -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); diff --git a/delta-patterns/summary.md b/delta-patterns/summary.md new file mode 100644 index 0000000..fc1fa24 --- /dev/null +++ b/delta-patterns/summary.md @@ -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 diff --git a/docs/schema-validation.md b/docs/schema-validation.md new file mode 100644 index 0000000..f160cd0 --- /dev/null +++ b/docs/schema-validation.md @@ -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 diff --git a/src/features/negation.ts b/src/features/negation.ts index 4f4c30f..939ac33 100644 --- a/src/features/negation.ts +++ b/src/features/negation.ts @@ -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