Skip to main content
Glama
quality-metrics.aggregate.ts12.4 kB
/** * @fileoverview QualityMetrics aggregate root * * This module defines the QualityMetrics aggregate which represents quality metrics * for a project with their thresholds, measurements, and history. */ import { AggregateRoot } from '../../shared/aggregate-root.js'; import { ProjectKey, GraphQLNodeId } from '../../../types/branded.js'; import { ThresholdValue } from '../../value-objects/threshold-value.js'; import { MetricValue } from '../../value-objects/metric-value.js'; import { QualityMetricsId, MetricConfiguration, MetricHistoryEntry, MetricTrend, ThresholdStatus, CreateQualityMetricsParams, UpdateMetricConfigParams, RecordMeasurementParams, } from './quality-metrics.types.js'; /** * QualityMetrics aggregate root * * Represents quality metrics for a project with configuration, thresholds, and history. * Tracks metric values over time and evaluates compliance with thresholds. * * @example * ```typescript * const metrics = QualityMetrics.create({ * projectKey: asProjectKey('my-project'), * repositoryId: asGraphQLNodeId('repo123'), * configuration: { * name: 'Line Coverage', * shortcode: MetricShortcode.LCV, * metricKey: MetricKey.AGGREGATE, * unit: '%', * minAllowed: 0, * maxAllowed: 100, * positiveDirection: 'UPWARD', * isReported: true, * isThresholdEnforced: true, * threshold: ThresholdValue.createPercentage(80) * } * }); * * metrics.recordMeasurement({ value: 85.5, commitOid: 'abc123' }); * console.log(metrics.isCompliant); // true * ``` */ export class QualityMetrics extends AggregateRoot<string> { private _projectKey: ProjectKey; private _repositoryId: GraphQLNodeId; private _configuration: MetricConfiguration; private _currentValue: MetricValue | null; private _history: MetricHistoryEntry[]; private _lastUpdated: Date; private static readonly MAX_HISTORY_ENTRIES = 100; private constructor( id: string, projectKey: ProjectKey, repositoryId: GraphQLNodeId, configuration: MetricConfiguration, currentValue: MetricValue | null, history: MetricHistoryEntry[], lastUpdated: Date ) { super(id); this._projectKey = projectKey; this._repositoryId = repositoryId; this._configuration = configuration; this._currentValue = currentValue; this._history = history; this._lastUpdated = lastUpdated; } /** * Creates a new QualityMetrics aggregate * * @param params - Creation parameters * @returns A new QualityMetrics instance */ static create(params: CreateQualityMetricsParams): QualityMetrics { const { projectKey, repositoryId, configuration, currentValue, history } = params; // Validate configuration if (!configuration.name || configuration.name.trim().length === 0) { throw new Error('Metric name cannot be empty'); } if (configuration.minAllowed >= configuration.maxAllowed) { throw new Error('Min allowed value must be less than max allowed value'); } // Create composite ID const id = QualityMetrics.createId({ projectKey, metricKey: configuration.metricKey, shortcode: configuration.shortcode, }); const now = new Date(); const metrics = new QualityMetrics( id, projectKey, repositoryId, configuration, currentValue || null, history || [], now ); metrics.addDomainEvent({ aggregateId: id, eventType: 'QualityMetricsCreated', occurredAt: now, payload: { projectKey, metricKey: configuration.metricKey, shortcode: configuration.shortcode, threshold: configuration.threshold?.value, }, }); return metrics; } /** * Creates a composite ID for the aggregate */ private static createId(id: QualityMetricsId): string { return `${id.projectKey}:${id.metricKey}:${id.shortcode}`; } /** * Gets the project key */ get projectKey(): ProjectKey { return this._projectKey; } /** * Gets the repository ID */ get repositoryId(): GraphQLNodeId { return this._repositoryId; } /** * Gets the metric configuration */ get configuration(): Readonly<MetricConfiguration> { return { ...this._configuration }; } /** * Gets the current metric value */ get currentValue(): MetricValue | null { return this._currentValue; } /** * Gets the metric history */ get history(): ReadonlyArray<MetricHistoryEntry> { return [...this._history]; } /** * Gets the last update timestamp */ get lastUpdated(): Date { return this._lastUpdated; } /** * Checks if the metric is currently compliant with its threshold */ get isCompliant(): boolean { if (!this._configuration.threshold || !this._currentValue) { return true; // No threshold or no value means compliant } return this._configuration.threshold.isMet( this._currentValue.value, this._configuration.positiveDirection === 'UPWARD' ? 'upward' : 'downward' ); } /** * Gets the current threshold status */ get thresholdStatus(): ThresholdStatus { if (!this._configuration.threshold || !this._currentValue) { return 'UNKNOWN'; } return this.isCompliant ? 'PASSING' : 'FAILING'; } /** * Updates the metric threshold * * @param threshold - New threshold value or null to remove */ updateThreshold(threshold: ThresholdValue | null): void { const oldThreshold = this._configuration.threshold; if (threshold && threshold.unit !== this._configuration.unit) { throw new Error( `Threshold unit '${threshold.unit}' does not match metric unit '${this._configuration.unit}'` ); } if ( threshold && (threshold.value < this._configuration.minAllowed || threshold.value > this._configuration.maxAllowed) ) { throw new Error('Threshold value is outside allowed range for this metric'); } this._configuration.threshold = threshold; this._lastUpdated = new Date(); this.addDomainEvent({ aggregateId: this._id, eventType: 'MetricThresholdUpdated', occurredAt: this._lastUpdated, payload: { oldThreshold: oldThreshold?.value, newThreshold: threshold?.value, unit: this._configuration.unit, }, }); this.markAsModified(); } /** * Updates the metric configuration * * @param params - Configuration update parameters */ updateConfiguration(params: UpdateMetricConfigParams): void { let hasChanges = false; if (params.name !== undefined) { const trimmedName = params.name.trim(); if (trimmedName.length === 0) { throw new Error('Metric name cannot be empty'); } if (trimmedName !== this._configuration.name) { this._configuration.name = trimmedName; hasChanges = true; } } if ( params.description !== undefined && params.description !== this._configuration.description ) { this._configuration.description = params.description; hasChanges = true; } if (params.isReported !== undefined && params.isReported !== this._configuration.isReported) { this._configuration.isReported = params.isReported; hasChanges = true; } if ( params.isThresholdEnforced !== undefined && params.isThresholdEnforced !== this._configuration.isThresholdEnforced ) { this._configuration.isThresholdEnforced = params.isThresholdEnforced; hasChanges = true; } if (hasChanges) { this._lastUpdated = new Date(); this.addDomainEvent({ aggregateId: this._id, eventType: 'MetricConfigurationUpdated', occurredAt: this._lastUpdated, payload: params as Record<string, unknown>, }); this.markAsModified(); } } /** * Records a new measurement * * @param params - Measurement parameters */ recordMeasurement(params: RecordMeasurementParams): void { const { value, commitOid, measuredAt } = params; if (value < this._configuration.minAllowed || value > this._configuration.maxAllowed) { throw new Error( `Value ${value} is outside allowed range [${this._configuration.minAllowed}, ${this._configuration.maxAllowed}]` ); } const timestamp = measuredAt || new Date(); const metricValue = MetricValue.create(value, this._configuration.unit, undefined, timestamp); // Update current value this._currentValue = metricValue; // Add to history const historyEntry: MetricHistoryEntry = { value: metricValue, threshold: this._configuration.threshold, thresholdStatus: this.thresholdStatus, commitOid, recordedAt: timestamp, }; this._history.push(historyEntry); // Trim history if needed if (this._history.length > QualityMetrics.MAX_HISTORY_ENTRIES) { this._history = this._history.slice(-QualityMetrics.MAX_HISTORY_ENTRIES); } this._lastUpdated = timestamp; this.addDomainEvent({ aggregateId: this._id, eventType: 'MeasurementRecorded', occurredAt: timestamp, payload: { value, unit: this._configuration.unit, commitOid, thresholdStatus: this.thresholdStatus, }, }); this.markAsModified(); } /** * Evaluates compliance at a specific point in time * * @param value - The value to evaluate * @returns Whether the value meets the threshold */ evaluateCompliance(value: number): boolean { if (!this._configuration.threshold) { return true; } return this._configuration.threshold.isMet( value, this._configuration.positiveDirection === 'UPWARD' ? 'upward' : 'downward' ); } /** * Gets the trend over a specified period * * @param periodDays - Number of days to analyze * @returns Trend information or null if insufficient data */ getTrend(periodDays = 30): MetricTrend | null { if (this._history.length < 2) { return null; } const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - periodDays); const recentHistory = this._history.filter((entry) => entry.recordedAt >= cutoffDate); if (recentHistory.length < 2) { return null; } const firstValue = recentHistory[0]?.value.value; const lastValue = recentHistory[recentHistory.length - 1]?.value.value; if (firstValue === undefined || lastValue === undefined) { return null; } const changePercentage = ((lastValue - firstValue) / firstValue) * 100; let direction: 'IMPROVING' | 'DEGRADING' | 'STABLE'; if (Math.abs(changePercentage) < 1) { direction = 'STABLE'; } else if ( (this._configuration.positiveDirection === 'UPWARD' && changePercentage > 0) || (this._configuration.positiveDirection === 'DOWNWARD' && changePercentage < 0) ) { direction = 'IMPROVING'; } else { direction = 'DEGRADING'; } return { direction, changePercentage, periodDays, }; } /** * Reconstructs QualityMetrics from persisted data * * @param data - Persisted metrics data * @returns A reconstructed QualityMetrics instance */ static fromPersistence(data: { id: string; projectKey: ProjectKey; repositoryId: GraphQLNodeId; configuration: MetricConfiguration; currentValue: MetricValue | null; history: MetricHistoryEntry[]; lastUpdated: Date; }): QualityMetrics { return new QualityMetrics( data.id, data.projectKey, data.repositoryId, data.configuration, data.currentValue, data.history, data.lastUpdated ); } /** * Converts the metrics to a persistence-friendly format */ toPersistence(): { id: string; projectKey: ProjectKey; repositoryId: GraphQLNodeId; configuration: MetricConfiguration; currentValue: MetricValue | null; history: MetricHistoryEntry[]; lastUpdated: Date; } { return { id: this._id, projectKey: this._projectKey, repositoryId: this._repositoryId, configuration: { ...this._configuration }, currentValue: this._currentValue, history: [...this._history], lastUpdated: this._lastUpdated, }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server