diff --git a/__tests__/unit/core/delta/delta-builder.test.ts b/__tests__/unit/core/delta/delta-builder.test.ts index 00d3a87..674e5f1 100644 --- a/__tests__/unit/core/delta/delta-builder.test.ts +++ b/__tests__/unit/core/delta/delta-builder.test.ts @@ -46,8 +46,8 @@ describe('DeltaBuilder', () => { // Verify that the entity property resolves correctly const lossless = new Lossless(node); - lossless.ingestDelta(delta); const lossy = new TimestampResolver(lossless); + lossless.ingestDelta(delta); const result = lossy.resolve(); expect(result).toBeDefined(); expect(result!['entity-1'].properties.name).toBe('Test Entity'); @@ -71,8 +71,8 @@ describe('DeltaBuilder', () => { // Verify that the entity property resolves correctly const lossless = new Lossless(node); - lossless.ingestDelta(delta); const lossy = new TimestampResolver(lossless); + lossless.ingestDelta(delta); const result = lossy.resolve(); expect(result).toBeDefined(); expect(result!['entity-1'].properties.name).toBe('Test Entity'); @@ -171,8 +171,8 @@ describe('DeltaBuilder', () => { expect(delta.pointers).toHaveProperty('type', 'follows'); const lossless = new Lossless(node); - lossless.ingestDelta(delta); const lossy = new TimestampResolver(lossless); + lossless.ingestDelta(delta); const result = lossy.resolve([relId]); expect(result).toBeDefined(); expect(result![relId]).toMatchObject({ @@ -201,8 +201,8 @@ describe('DeltaBuilder', () => { expect(delta.pointers).toHaveProperty('version', 1); const lossless = new Lossless(node); - lossless.ingestDelta(delta); const lossy = new TimestampResolver(lossless); + lossless.ingestDelta(delta); const result = lossy.resolve([relId]); expect(result).toBeDefined(); expect(result![relId]).toMatchObject({ diff --git a/__tests__/unit/core/negation.test.ts b/__tests__/unit/core/negation.test.ts index 508ad70..564492d 100644 --- a/__tests__/unit/core/negation.test.ts +++ b/__tests__/unit/core/negation.test.ts @@ -351,8 +351,8 @@ describe('Negation System', () => { test('should handle self-referential entities in negations', () => { // Create a delta that references itself const selfRefDelta = createDelta('user1', 'host1') - .setProperty('node1', 'parent', 'node1') - .setProperty('node1', 'child', 'node1') // Self-reference + .setProperty('node1', 'parent', 'node1', 'node-as-parent') + .setProperty('node1', 'child', 'node1', 'node-as-child') // Self-reference .buildV1(); const negationDelta = createDelta('admin', 'host1').negate(selfRefDelta.id).buildV1(); diff --git a/__tests__/unit/views/lossy.test.ts b/__tests__/unit/views/lossy.test.ts index 834e20b..c815f6c 100644 --- a/__tests__/unit/views/lossy.test.ts +++ b/__tests__/unit/views/lossy.test.ts @@ -4,7 +4,7 @@ import { Lossless, LosslessViewOne } from "../../../src/views/lossless"; import { Lossy } from "../../../src/views/lossy"; import { RhizomeNode } from "../../../src/node"; import { valueFromCollapsedDelta } from "../../../src/views/resolvers/aggregation-resolvers"; -import { lastValueFromDeltas } from "../../../src/views/resolvers/timestamp-resolvers"; +import { latestFromCollapsedDeltas } from "../../../src/views/resolvers/timestamp-resolvers"; import { createDelta } from "../../../src/core/delta-builder"; const debug = Debug('rz:test:lossy'); @@ -18,8 +18,16 @@ type Summary = { roles: Role[]; }; -class Summarizer extends Lossy { +class Summarizer extends Lossy { + private readonly debug: debug.Debugger; + + constructor(lossless: Lossless) { + super(lossless); + this.debug = Debug('rz:test:lossy:summarizer'); + } + initializer(): Summary { + this.debug('Initializing new summary'); return { roles: [] }; @@ -30,23 +38,53 @@ class Summarizer extends Lossy { // TODO: Prove with failing test reducer(acc: Summary, cur: LosslessViewOne): Summary { + this.debug(`Processing view for entity ${cur.id} (referenced as: ${cur.referencedAs.join(', ')})`); + this.debug(`lossless view:`, JSON.stringify(cur)); + if (cur.referencedAs.includes("role")) { - const {delta, value: actor} = lastValueFromDeltas("actor", cur.propertyDeltas["actor"]) ?? {}; - if (!delta) throw new Error('expected to find delta'); - if (!actor) throw new Error('expected to find actor'); + this.debug(`Found role entity: ${cur.id}`); + + const actorDeltas = cur.propertyDeltas["actor"]; + this.debug(`Found ${actorDeltas?.length ?? 0} actor deltas`); + + const {delta, value: actor} = latestFromCollapsedDeltas("actor", actorDeltas) ?? {}; + + if (!delta) { + this.debug('No delta found for actor property'); + throw new Error('expected to find delta'); + } + + if (!actor) { + this.debug('No actor value found in deltas'); + throw new Error('expected to find actor'); + } + + this.debug(`Found actor: ${actor}`); const film = valueFromCollapsedDelta("film", delta); - if (!film) throw new Error('expected to find film'); - acc.roles.push({ + + if (!film) { + this.debug('No film property found in delta'); + throw new Error('expected to find film'); + } + + this.debug(`Found film: ${film}`); + const role = { role: cur.id, actor, film - }); + }; + + acc.roles.push(role); + this.debug(`Added role: ${JSON.stringify(role)}`); } + this.debug(`Updated accumulator: ${JSON.stringify(acc, null, 2)}`); + return acc; } resolver(acc: Summary): Summary { + this.debug(`Resolving summary with ${acc.roles.length} roles`); return acc; } } diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index dd4ea43..9e288d6 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -2,6 +2,9 @@ import { DeltaV1, DeltaV2 } from './delta'; import { randomUUID } from 'crypto'; import { PropertyTypes } from './types'; import { PointersV2 } from './delta'; +import Debug from 'debug'; + +const debug = Debug('rz:delta-builder'); /** * A fluent builder for creating Delta objects with proper validation and type safety. @@ -77,9 +80,11 @@ export class DeltaBuilder { addPointer(localContext: string, target: string | number | boolean | null, targetContext?: string): this { const pointerTarget = (targetContext && typeof target === 'string') ? { [target]: targetContext } : target; - if (this.pointers[localContext] && + // Prevent duplicate primitive properties with the same key + if (this.pointers[localContext] && JSON.stringify(this.pointers[localContext]) !== JSON.stringify(pointerTarget) ) { + debug(`Pointer for '${localContext}' already exists with different value: ${JSON.stringify(this.pointers[localContext])} !== ${JSON.stringify(pointerTarget)}`); throw new Error(`Pointer for ${localContext} already exists with different value`); } this.pointers[localContext] = pointerTarget; diff --git a/src/orchestration/docker-orchestrator/managers/image-manager.ts b/src/orchestration/docker-orchestrator/managers/image-manager.ts index e1e63ce..d013c28 100644 --- a/src/orchestration/docker-orchestrator/managers/image-manager.ts +++ b/src/orchestration/docker-orchestrator/managers/image-manager.ts @@ -1,4 +1,4 @@ -import Docker, { DockerOptions } from 'dockerode'; +import Docker from 'dockerode'; import * as path from 'path'; import { promises as fs } from 'fs'; import * as tar from 'tar-fs'; @@ -63,9 +63,9 @@ export class ImageManager implements IImageManager { debug('Created build context tar stream'); testImageBuildPromise = new Promise((resolve, reject) => { - const log = (...args: any[]) => { + const log = (...args: unknown[]) => { const message = args.map(arg => - typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ).join(' '); debug(message); }; @@ -111,7 +111,7 @@ export class ImageManager implements IImageManager { // Log any other non-empty JSON objects log(`[Docker Build] ${JSON.stringify(json)}`); } - } catch (e) { + } catch (_e) { // If not JSON, log as plain text if not empty if (line.trim()) { log(`[Docker Build] ${line}`); diff --git a/src/orchestration/docker-orchestrator/managers/status-manager.ts b/src/orchestration/docker-orchestrator/managers/status-manager.ts index fafe851..ee6164d 100644 --- a/src/orchestration/docker-orchestrator/managers/status-manager.ts +++ b/src/orchestration/docker-orchestrator/managers/status-manager.ts @@ -1,4 +1,4 @@ -import Docker, { Container } from 'dockerode'; +import { Container } from 'dockerode'; import { IStatusManager } from './interfaces'; import { NodeHandle, NodeStatus } from '../../types'; import Debug from 'debug'; diff --git a/src/query/query-engine.ts b/src/query/query-engine.ts index 36a7d58..76dcaec 100644 --- a/src/query/query-engine.ts +++ b/src/query/query-engine.ts @@ -269,7 +269,7 @@ export class QueryEngine { case 'primitive': { // Use last-write-wins for primitives const deltasSorted = deltas.sort((a, b) => b.timeCreated - a.timeCreated); - for (let delta of deltasSorted) { + for (const delta of deltasSorted) { const primitiveValue = this.extractPrimitiveValue(delta, propertyId); if (primitiveValue !== null) { obj[propertyId] = primitiveValue; diff --git a/src/views/lossless.ts b/src/views/lossless.ts index 574b876..993e624 100644 --- a/src/views/lossless.ts +++ b/src/views/lossless.ts @@ -72,7 +72,7 @@ class LosslessEntity { export class Lossless { domainEntities = new LosslessEntityMap(); transactions: Transactions; - referencedAs = new Map>(); + referencedAs = new Map>(); eventStream = new EventEmitter(); // Track all deltas by ID for negation processing @@ -155,19 +155,6 @@ export class Lossless { } } - for (const {target, localContext} of delta.pointers) { - if (typeof target === "string" && this.domainEntities.has(target)) { - if (this.domainEntities.has(target)) { - let referencedAs = this.referencedAs.get(localContext); - if (!referencedAs) { - referencedAs = new Set(); - this.referencedAs.set(localContext, referencedAs); - } - referencedAs.add(target); - } - } - } - const transactionId = this.transactions.ingestDelta(delta, targets); if (!transactionId) { @@ -232,8 +219,8 @@ export class Lossless { const ent = this.domainEntities.get(id); if (!ent) continue; - const referencedAs = new Set(); + const propertyDeltas: { [key: PropertyID]: CollapsedDelta[] } = {}; @@ -272,15 +259,11 @@ export class Lossless { const pointers: CollapsedPointer[] = []; - for (const {localContext, target, targetContext} of delta.pointers) { - if (targetContext) { - // Only store primitive pointers in the collapsed delta - continue; - } - pointers.push({[localContext]: target}); + for (const {localContext, target} of delta.pointers) { if (target === ent.id) { referencedAs.add(localContext); } + pointers.push({[localContext]: target}); } visibleDeltas.push({ @@ -295,6 +278,14 @@ export class Lossless { } } + if (this.referencedAs.has(ent.id)) { + for (const ref of referencedAs) { + this.referencedAs.get(ent.id)!.add(ref); + } + } else { + this.referencedAs.set(ent.id, referencedAs); + } + // Only include entity in view if it has visible deltas if (hasVisibleDeltas) { view[ent.id] = { diff --git a/src/views/lossy.ts b/src/views/lossy.ts index da2ac4b..a8101d6 100644 --- a/src/views/lossy.ts +++ b/src/views/lossy.ts @@ -47,6 +47,7 @@ export abstract class Lossy { return this.deltaFilter(delta); }; const losslessPartial = this.lossless.compose([entityId], combinedFilter); + debug(`Lossless partial for entity ${entityId}:`, JSON.stringify(losslessPartial)); if (!losslessPartial) { // This should not happen; this should only be called after the lossless view has been updated diff --git a/src/views/resolvers/custom-resolvers/resolver.ts b/src/views/resolvers/custom-resolvers/resolver.ts index 5b84624..a320a6b 100644 --- a/src/views/resolvers/custom-resolvers/resolver.ts +++ b/src/views/resolvers/custom-resolvers/resolver.ts @@ -199,7 +199,7 @@ export class CustomResolver extends Lossy { */ reducer(acc: Accumulator, {id: entityId, propertyDeltas}: LosslessViewOne): Accumulator { debug(`Processing deltas for entity: ${entityId}`); - debug('Property deltas:', JSON.stringify(propertyDeltas, null, 2)); + debug('Property deltas:', JSON.stringify(propertyDeltas)); if (!acc[entityId]) { acc[entityId] = {}; @@ -246,7 +246,7 @@ export class CustomResolver extends Lossy { // Update the plugin state with the new delta entityState[pluginKey] = plugin.update(pluginState, propertyValue, updateDelta, dependencies); - debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey], null, 2)); + debugState(`Updated entity state for ${entityId}:`, JSON.stringify(entityState[pluginKey])); } return acc; @@ -254,7 +254,7 @@ export class CustomResolver extends Lossy { resolver(acc: Accumulator, entityIds: DomainEntityID[]) { const result: Result = {}; - debug('Initial accumulator state:', JSON.stringify(acc, null, 2)); + debug('Initial accumulator state:', JSON.stringify(acc)); for (const entityId in acc) { if (!entityIds.includes(entityId)) continue; @@ -270,9 +270,9 @@ export class CustomResolver extends Lossy { debug(`Processing property: ${propertyId} (key: ${pluginKey})`); const dependencies = this.getDependencyStates(acc[entityId], plugin); - debug(`Dependencies for ${propertyId}:`, JSON.stringify(dependencies, null, 2)); + debug(`Dependencies for ${propertyId}:`, JSON.stringify(dependencies)); const state = acc[entityId][pluginKey] || plugin.initialize(dependencies); - debug(`State for ${propertyId}:`, JSON.stringify(state, null, 2)); + debug(`State for ${propertyId}:`, JSON.stringify(state)); const resolvedValue = plugin.resolve(state, dependencies); if (resolvedValue === undefined) throw new Error(`Resolved value for property ${propertyId} is undefined`) diff --git a/src/views/resolvers/timestamp-resolvers.ts b/src/views/resolvers/timestamp-resolvers.ts index 9b66ffc..ac4c139 100644 --- a/src/views/resolvers/timestamp-resolvers.ts +++ b/src/views/resolvers/timestamp-resolvers.ts @@ -158,7 +158,7 @@ export class LexicographicTimestampResolver extends TimestampResolver { } // Resolve a value for an entity by last write wins -export function lastValueFromDeltas( +export function latestFromCollapsedDeltas( key: string, deltas?: CollapsedDelta[] ): {