/**
* Generates appropriate representations based on tilt parameters
*/
export default class TiltProjector {
constructor(options = {}) {
this.config = {
embeddingDimension: options.embeddingDimension || 1536,
keywordLimit: options.keywordLimit || 20,
minKeywordScore: options.minKeywordScore || 0.1,
graphDepth: options.graphDepth || 3,
temporalGranularity: options.temporalGranularity || 'day',
includeMetadata: options.includeMetadata !== false,
...options
};
this.initializeProjectionStrategies();
this.initializeOutputFormats();
}
/**
* Initialize projection strategies for each tilt representation
*/
initializeProjectionStrategies() {
this.projectionStrategies = {
embedding: {
name: 'Vector Embedding Projection',
outputType: 'vector',
processor: this.projectToEmbedding.bind(this),
requirements: ['embeddingHandler'],
metadata: ['similarity', 'dimension', 'model']
},
keywords: {
name: 'Keyword Extraction Projection',
outputType: 'text',
processor: this.projectToKeywords.bind(this),
requirements: ['textAnalyzer'],
metadata: ['score', 'frequency', 'tfidf']
},
graph: {
name: 'Graph Structure Projection',
outputType: 'structured',
processor: this.projectToGraph.bind(this),
requirements: ['graphAnalyzer'],
metadata: ['centrality', 'connections', 'communities']
},
temporal: {
name: 'Temporal Sequence Projection',
outputType: 'sequence',
processor: this.projectToTemporal.bind(this),
requirements: ['temporalAnalyzer'],
metadata: ['timestamp', 'sequence', 'duration']
}
};
}
/**
* Initialize output format specifications
*/
initializeOutputFormats() {
this.outputFormats = {
vector: {
schema: {
embedding: 'number[]',
dimension: 'number',
similarity: 'number?',
model: 'string',
metadata: 'object?'
},
example: {
embedding: [0.1, -0.2, 0.3],
dimension: 1536,
similarity: 0.85,
model: 'nomic-embed-text'
}
},
text: {
schema: {
keywords: 'string[]',
scores: 'number[]',
summary: 'string?',
metadata: 'object?'
},
example: {
keywords: ['machine learning', 'neural networks', 'AI'],
scores: [0.9, 0.8, 0.7],
summary: 'Key concepts about machine learning and AI'
}
},
structured: {
schema: {
nodes: 'object[]',
edges: 'object[]',
properties: 'object?',
metadata: 'object?'
},
example: {
nodes: [{ id: 'entity1', type: 'Entity', label: 'Machine Learning' }],
edges: [{ source: 'entity1', target: 'entity2', type: 'relatedTo' }]
}
},
sequence: {
schema: {
events: 'object[]',
timeline: 'object[]',
duration: 'number?',
metadata: 'object?'
},
example: {
events: [{ timestamp: '2024-01-01', event: 'Creation', data: {} }],
timeline: [{ start: '2024-01-01', end: '2024-12-31' }]
}
}
};
}
/**
* Main projection method - transforms corpuscles based on tilt representation
* @param {Array} corpuscles - Selected corpuscles to transform
* @param {Object} tiltParams - Normalized tilt parameters
* @param {Object} context - Projection context and dependencies
* @returns {Promise<Object>} Projected representation
*/
async project(corpuscles, tiltParams, context = {}) {
const { representation } = tiltParams;
const strategy = this.projectionStrategies[representation];
if (!strategy) {
throw new Error(`Unsupported tilt representation: ${representation}`);
}
// Validate requirements
this.validateRequirements(strategy, context);
try {
// Execute projection
const projection = await strategy.processor(corpuscles, tiltParams, context);
// Enrich with metadata
const enrichedProjection = this.enrichProjection(projection, strategy, context);
// Format output
const formattedOutput = this.formatOutput(enrichedProjection, strategy.outputType);
return {
representation,
outputType: strategy.outputType,
data: formattedOutput,
metadata: {
corpuscleCount: corpuscles.length,
projectionStrategy: strategy.name,
timestamp: new Date().toISOString(),
config: this.config
}
};
} catch (error) {
throw new Error(`Projection failed for ${representation}: ${error.message}`);
}
}
/**
* Project corpuscles to embedding representation
*/
async projectToEmbedding(corpuscles, tiltParams, context) {
const { embeddingHandler } = context;
const embeddings = [];
const similarities = [];
// Process each corpuscle
for (const corpuscle of corpuscles) {
let embedding = null;
let similarity = corpuscle.similarity || 0;
// Use existing embedding if available
if (corpuscle.metadata.embedding) {
embedding = corpuscle.metadata.embedding;
} else {
// Generate embedding from content
const content = this.extractTextContent(corpuscle);
if (content) {
embedding = await embeddingHandler.generateEmbedding(content);
}
}
if (embedding) {
embeddings.push({
uri: corpuscle.uri,
embedding,
similarity,
dimension: embedding.length,
source: corpuscle.metadata.embedding ? 'cached' : 'generated'
});
similarities.push(similarity);
}
}
// Calculate aggregate statistics
const avgSimilarity = similarities.length > 0 ?
similarities.reduce((a, b) => a + b, 0) / similarities.length : 0;
return {
embeddings,
aggregateStats: {
count: embeddings.length,
avgSimilarity,
dimension: this.config.embeddingDimension,
model: embeddingHandler.model || 'unknown'
},
centroid: this.calculateCentroid(embeddings.map(e => e.embedding))
};
}
/**
* Project corpuscles to keyword representation
*/
async projectToKeywords(corpuscles, tiltParams, context) {
const allKeywords = new Map(); // keyword -> { score, frequency, sources }
const corpuscleKeywords = [];
// Extract keywords from each corpuscle
for (const corpuscle of corpuscles) {
const content = this.extractTextContent(corpuscle);
if (!content) continue;
const keywords = this.extractKeywords(content);
const scoredKeywords = this.scoreKeywords(keywords, content);
corpuscleKeywords.push({
uri: corpuscle.uri,
keywords: scoredKeywords,
content: content.substring(0, 200) + '...'
});
// Aggregate keywords globally
scoredKeywords.forEach(({ keyword, score }) => {
if (allKeywords.has(keyword)) {
const existing = allKeywords.get(keyword);
existing.score = Math.max(existing.score, score);
existing.frequency += 1;
existing.sources.push(corpuscle.uri);
} else {
allKeywords.set(keyword, {
score,
frequency: 1,
sources: [corpuscle.uri]
});
}
});
}
// Convert to sorted arrays
const globalKeywords = Array.from(allKeywords.entries())
.map(([keyword, data]) => ({ keyword, ...data }))
.sort((a, b) => b.score - a.score)
.slice(0, this.config.keywordLimit);
// Generate summary
const topKeywords = globalKeywords.slice(0, 10).map(k => k.keyword);
const summary = this.generateKeywordSummary(topKeywords);
return {
globalKeywords,
corpuscleKeywords,
summary,
stats: {
totalKeywords: allKeywords.size,
avgKeywordsPerCorpuscle: corpuscleKeywords.length > 0 ?
corpuscleKeywords.reduce((sum, c) => sum + c.keywords.length, 0) / corpuscleKeywords.length : 0,
coverageScore: this.calculateKeywordCoverage(globalKeywords, corpuscleKeywords)
}
};
}
/**
* Project corpuscles to graph representation
*/
async projectToGraph(corpuscles, tiltParams, context) {
const nodes = new Map(); // uri -> node
const edges = [];
const nodeMetrics = new Map(); // uri -> metrics
// Build nodes from corpuscles
for (const corpuscle of corpuscles) {
const nodeId = corpuscle.uri;
const node = {
id: nodeId,
type: corpuscle.type,
label: this.extractLabel(corpuscle),
properties: this.extractNodeProperties(corpuscle),
score: corpuscle.score || 0
};
nodes.set(nodeId, node);
}
// Extract relationships and build edges
for (const corpuscle of corpuscles) {
const relationships = this.extractRelationships(corpuscle);
relationships.forEach(rel => {
if (nodes.has(rel.target)) {
edges.push({
id: `${corpuscle.uri}-${rel.type}-${rel.target}`,
source: corpuscle.uri,
target: rel.target,
type: rel.type,
weight: rel.weight || 1,
properties: rel.properties || {}
});
}
});
}
// Calculate graph metrics
for (const [nodeId, node] of nodes) {
const metrics = this.calculateNodeMetrics(nodeId, edges);
nodeMetrics.set(nodeId, metrics);
node.metrics = metrics;
}
// Detect communities
const communities = this.detectCommunities(Array.from(nodes.values()), edges);
// Calculate graph statistics
const graphStats = this.calculateGraphStatistics(nodes, edges);
return {
nodes: Array.from(nodes.values()),
edges,
communities,
metrics: {
nodeCount: nodes.size,
edgeCount: edges.length,
density: edges.length / (nodes.size * (nodes.size - 1)),
avgDegree: graphStats.avgDegree,
clusteringCoefficient: graphStats.clusteringCoefficient
},
layout: this.generateLayoutHints(Array.from(nodes.values()), edges)
};
}
/**
* Project corpuscles to temporal representation
*/
async projectToTemporal(corpuscles, tiltParams, context) {
const events = [];
const timeline = [];
const temporalBuckets = new Map(); // time bucket -> corpuscles
// Extract temporal information from corpuscles
for (const corpuscle of corpuscles) {
const temporalData = this.extractTemporalData(corpuscle);
if (temporalData.timestamp) {
const event = {
id: corpuscle.uri,
timestamp: temporalData.timestamp,
type: corpuscle.type,
label: this.extractLabel(corpuscle),
data: temporalData.data,
score: corpuscle.score || 0
};
events.push(event);
// Group into temporal buckets
const bucket = this.getTemporalBucket(temporalData.timestamp);
if (!temporalBuckets.has(bucket)) {
temporalBuckets.set(bucket, []);
}
temporalBuckets.get(bucket).push(corpuscle);
}
}
// Sort events by timestamp
events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
// Create timeline from buckets
for (const [bucket, corpuscles] of temporalBuckets) {
const [start, end] = this.getBucketRange(bucket);
timeline.push({
period: bucket,
start,
end,
count: corpuscles.length,
avgScore: corpuscles.reduce((sum, c) => sum + (c.score || 0), 0) / corpuscles.length,
types: [...new Set(corpuscles.map(c => c.type))]
});
}
// Sort timeline
timeline.sort((a, b) => new Date(a.start) - new Date(b.start));
// Calculate temporal statistics
const duration = this.calculateTemporalDuration(events);
const frequency = this.calculateEventFrequency(events);
return {
events,
timeline,
sequences: this.detectTemporalSequences(events),
stats: {
eventCount: events.length,
timelineSpan: timeline.length,
duration,
frequency,
granularity: this.config.temporalGranularity
},
patterns: this.detectTemporalPatterns(events, timeline)
};
}
/**
* Helper methods for content extraction
*/
extractTextContent(corpuscle) {
const content = corpuscle.content || {};
return [
content.text,
content.content,
content.label,
content.prefLabel,
content.description
].filter(Boolean).join(' ');
}
extractLabel(corpuscle) {
const content = corpuscle.content || {};
return content.prefLabel || content.label || content.text?.substring(0, 50) || corpuscle.uri;
}
extractNodeProperties(corpuscle) {
return {
type: corpuscle.type,
score: corpuscle.score,
created: corpuscle.metadata.created,
source: corpuscle.metadata.source
};
}
extractRelationships(corpuscle) {
// Extract relationships from corpuscle binding data
const relationships = [];
const binding = corpuscle.binding || {};
if (binding.entity?.value) {
relationships.push({
target: binding.entity.value,
type: 'relatedTo',
weight: 1
});
}
if (binding.unit?.value) {
relationships.push({
target: binding.unit.value,
type: 'partOf',
weight: 0.8
});
}
return relationships;
}
extractTemporalData(corpuscle) {
const metadata = corpuscle.metadata || {};
const binding = corpuscle.binding || {};
return {
timestamp: metadata.created || binding.created?.value,
modified: metadata.modified || binding.modified?.value,
data: {
type: corpuscle.type,
score: corpuscle.score
}
};
}
/**
* Analysis and processing methods
*/
extractKeywords(text) {
// Simple keyword extraction - could be enhanced with NLP
const words = text.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3);
// Remove common stop words
const stopWords = new Set(['this', 'that', 'with', 'have', 'will', 'been', 'from', 'they', 'them', 'were', 'said', 'each', 'which', 'their', 'time', 'more', 'very', 'when', 'come', 'here', 'just', 'like', 'long', 'make', 'many', 'over', 'such', 'take', 'than', 'them', 'well', 'were']);
return words.filter(word => !stopWords.has(word));
}
scoreKeywords(keywords, text) {
const wordFreq = new Map();
const totalWords = keywords.length;
// Calculate frequency
keywords.forEach(word => {
wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
});
// Score keywords by frequency and position
return Array.from(wordFreq.entries())
.map(([keyword, freq]) => ({
keyword,
score: (freq / totalWords) * (1 + (text.toLowerCase().indexOf(keyword) === -1 ? 0 : 0.1)),
frequency: freq
}))
.filter(k => k.score >= this.config.minKeywordScore)
.sort((a, b) => b.score - a.score);
}
generateKeywordSummary(topKeywords) {
if (topKeywords.length === 0) return 'No keywords extracted';
const summary = `Key topics include: ${topKeywords.slice(0, 5).join(', ')}`;
return summary + (topKeywords.length > 5 ? ' and others.' : '.');
}
calculateKeywordCoverage(globalKeywords, corpuscleKeywords) {
const topGlobalKeywords = new Set(globalKeywords.slice(0, 10).map(k => k.keyword));
let totalCoverage = 0;
corpuscleKeywords.forEach(corpuscle => {
const corpuscleKeywordSet = new Set(corpuscle.keywords.map(k => k.keyword));
const intersection = new Set([...topGlobalKeywords].filter(k => corpuscleKeywordSet.has(k)));
totalCoverage += intersection.size / topGlobalKeywords.size;
});
return corpuscleKeywords.length > 0 ? totalCoverage / corpuscleKeywords.length : 0;
}
calculateNodeMetrics(nodeId, edges) {
const inEdges = edges.filter(e => e.target === nodeId);
const outEdges = edges.filter(e => e.source === nodeId);
const totalEdges = inEdges.length + outEdges.length;
return {
inDegree: inEdges.length,
outDegree: outEdges.length,
degree: totalEdges,
centrality: totalEdges / Math.max(1, edges.length), // Simplified centrality
clustering: this.calculateClusteringCoefficient(nodeId, edges)
};
}
calculateClusteringCoefficient(nodeId, edges) {
// Simplified clustering coefficient calculation
const neighbors = new Set();
edges.forEach(edge => {
if (edge.source === nodeId) neighbors.add(edge.target);
if (edge.target === nodeId) neighbors.add(edge.source);
});
if (neighbors.size < 2) return 0;
let neighborConnections = 0;
const neighborArray = Array.from(neighbors);
for (let i = 0; i < neighborArray.length; i++) {
for (let j = i + 1; j < neighborArray.length; j++) {
if (edges.some(e =>
(e.source === neighborArray[i] && e.target === neighborArray[j]) ||
(e.source === neighborArray[j] && e.target === neighborArray[i])
)) {
neighborConnections++;
}
}
}
const maxPossibleConnections = neighbors.size * (neighbors.size - 1) / 2;
return maxPossibleConnections > 0 ? neighborConnections / maxPossibleConnections : 0;
}
detectCommunities(nodes, edges) {
// Simple community detection based on connected components
const communities = [];
const visited = new Set();
nodes.forEach(node => {
if (!visited.has(node.id)) {
const community = this.findConnectedComponent(node.id, edges, visited);
if (community.length > 1) {
communities.push({
id: `community_${communities.length}`,
members: community,
size: community.length
});
}
}
});
return communities;
}
findConnectedComponent(startNode, edges, visited) {
const component = [];
const queue = [startNode];
while (queue.length > 0) {
const node = queue.shift();
if (visited.has(node)) continue;
visited.add(node);
component.push(node);
// Find neighbors
edges.forEach(edge => {
if (edge.source === node && !visited.has(edge.target)) {
queue.push(edge.target);
}
if (edge.target === node && !visited.has(edge.source)) {
queue.push(edge.source);
}
});
}
return component;
}
calculateGraphStatistics(nodes, edges) {
const nodeCount = nodes.size;
const edgeCount = edges.length;
let totalDegree = 0;
let totalClustering = 0;
for (const [nodeId, node] of nodes) {
if (node.metrics) {
totalDegree += node.metrics.degree;
totalClustering += node.metrics.clustering;
}
}
return {
avgDegree: nodeCount > 0 ? totalDegree / nodeCount : 0,
clusteringCoefficient: nodeCount > 0 ? totalClustering / nodeCount : 0
};
}
generateLayoutHints(nodes, edges) {
// Generate layout hints for graph visualization
return {
algorithm: 'force-directed',
parameters: {
attraction: 0.1,
repulsion: 100,
iterations: 100
},
clusters: nodes.length > 20 ? 'detect' : 'none',
nodeSize: 'score',
edgeWidth: 'weight'
};
}
getTemporalBucket(timestamp) {
const date = new Date(timestamp);
const granularity = this.config.temporalGranularity;
switch (granularity) {
case 'year':
return date.getFullYear().toString();
case 'month':
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
case 'day':
return date.toISOString().split('T')[0];
case 'hour':
return `${date.toISOString().split('T')[0]}T${date.getHours().toString().padStart(2, '0')}`;
default:
return date.toISOString().split('T')[0];
}
}
getBucketRange(bucket) {
const granularity = this.config.temporalGranularity;
switch (granularity) {
case 'year':
return [`${bucket}-01-01T00:00:00Z`, `${bucket}-12-31T23:59:59Z`];
case 'month':
const [year, month] = bucket.split('-');
const lastDay = new Date(parseInt(year), parseInt(month), 0).getDate();
return [`${bucket}-01T00:00:00Z`, `${bucket}-${lastDay}T23:59:59Z`];
case 'day':
return [`${bucket}T00:00:00Z`, `${bucket}T23:59:59Z`];
case 'hour':
return [`${bucket}:00:00Z`, `${bucket}:59:59Z`];
default:
return [`${bucket}T00:00:00Z`, `${bucket}T23:59:59Z`];
}
}
detectTemporalSequences(events) {
// Detect sequences of related events
const sequences = [];
const sortedEvents = events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
let currentSequence = [];
let lastTimestamp = null;
const maxGap = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
for (const event of sortedEvents) {
const currentTimestamp = new Date(event.timestamp).getTime();
if (lastTimestamp && currentTimestamp - lastTimestamp > maxGap) {
if (currentSequence.length > 1) {
sequences.push({
id: `sequence_${sequences.length}`,
events: [...currentSequence],
duration: currentSequence[currentSequence.length - 1].timestamp - currentSequence[0].timestamp
});
}
currentSequence = [];
}
currentSequence.push(event);
lastTimestamp = currentTimestamp;
}
if (currentSequence.length > 1) {
sequences.push({
id: `sequence_${sequences.length}`,
events: currentSequence,
duration: new Date(currentSequence[currentSequence.length - 1].timestamp) -
new Date(currentSequence[0].timestamp)
});
}
return sequences;
}
calculateTemporalDuration(events) {
if (events.length < 2) return 0;
const timestamps = events.map(e => new Date(e.timestamp).getTime()).sort((a, b) => a - b);
return timestamps[timestamps.length - 1] - timestamps[0];
}
calculateEventFrequency(events) {
if (events.length < 2) return 0;
const duration = this.calculateTemporalDuration(events);
return duration > 0 ? events.length / (duration / (24 * 60 * 60 * 1000)) : 0; // events per day
}
detectTemporalPatterns(events, timeline) {
// Detect patterns in temporal data
const patterns = [];
// Detect periodic patterns
const periods = this.detectPeriodicPatterns(timeline);
if (periods.length > 0) {
patterns.push({
type: 'periodic',
description: 'Regular temporal patterns detected',
periods
});
}
// Detect bursts
const bursts = this.detectBurstPatterns(events);
if (bursts.length > 0) {
patterns.push({
type: 'burst',
description: 'Event burst patterns detected',
bursts
});
}
return patterns;
}
detectPeriodicPatterns(timeline) {
// Simple periodic pattern detection
if (timeline.length < 3) return [];
const intervals = [];
for (let i = 1; i < timeline.length; i++) {
const interval = new Date(timeline[i].start) - new Date(timeline[i-1].start);
intervals.push(interval);
}
// Check for regular intervals
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const variance = intervals.reduce((sum, interval) => sum + Math.pow(interval - avgInterval, 2), 0) / intervals.length;
const stdDev = Math.sqrt(variance);
if (stdDev / avgInterval < 0.2) { // Low relative standard deviation indicates regularity
return [{
interval: avgInterval,
confidence: 1 - (stdDev / avgInterval),
description: `Regular interval of ${Math.round(avgInterval / (24 * 60 * 60 * 1000))} days`
}];
}
return [];
}
detectBurstPatterns(events) {
// Detect event bursts (periods of high activity)
const bursts = [];
const sortedEvents = events.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
let currentBurst = [];
let lastTimestamp = null;
const burstThreshold = 3; // Minimum events for a burst
const burstWindow = 60 * 60 * 1000; // 1 hour window
for (const event of sortedEvents) {
const currentTimestamp = new Date(event.timestamp).getTime();
if (lastTimestamp && currentTimestamp - lastTimestamp <= burstWindow) {
currentBurst.push(event);
} else {
if (currentBurst.length >= burstThreshold) {
bursts.push({
start: currentBurst[0].timestamp,
end: currentBurst[currentBurst.length - 1].timestamp,
eventCount: currentBurst.length,
intensity: currentBurst.length / burstWindow * (60 * 60 * 1000) // events per hour
});
}
currentBurst = [event];
}
lastTimestamp = currentTimestamp;
}
if (currentBurst.length >= burstThreshold) {
bursts.push({
start: currentBurst[0].timestamp,
end: currentBurst[currentBurst.length - 1].timestamp,
eventCount: currentBurst.length,
intensity: currentBurst.length / burstWindow * (60 * 60 * 1000)
});
}
return bursts;
}
/**
* Utility methods
*/
calculateCentroid(embeddings) {
if (embeddings.length === 0) return null;
const dimension = embeddings[0].length;
const centroid = new Array(dimension).fill(0);
embeddings.forEach(embedding => {
embedding.forEach((value, index) => {
centroid[index] += value;
});
});
return centroid.map(value => value / embeddings.length);
}
validateRequirements(strategy, context) {
const missing = strategy.requirements.filter(req => !context[req]);
if (missing.length > 0) {
throw new Error(`Missing required dependencies for ${strategy.name}: ${missing.join(', ')}`);
}
}
enrichProjection(projection, strategy, context) {
if (!this.config.includeMetadata) return projection;
return {
...projection,
enrichment: {
strategy: strategy.name,
outputType: strategy.outputType,
requiredMetadata: strategy.metadata,
processingTime: Date.now(),
config: this.config
}
};
}
formatOutput(projection, outputType) {
const format = this.outputFormats[outputType];
if (!format) return projection;
// Validate against schema (simplified validation)
return this.validateAndFormat(projection, format);
}
validateAndFormat(data, format) {
// Simplified validation and formatting
// In a full implementation, would use JSON Schema validation
return data;
}
/**
* Get projection documentation
*/
getProjectionDocumentation() {
return {
strategies: Object.entries(this.projectionStrategies).map(([key, strategy]) => ({
name: key,
description: strategy.name,
outputType: strategy.outputType,
requirements: strategy.requirements,
metadata: strategy.metadata
})),
outputFormats: this.outputFormats,
config: this.config
};
}
}