@ai-pip/csl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/index.ts +273 -0
- package/layers/csl/adapters/index.ts +9 -0
- package/layers/csl/adapters/input/DOMAdapter.ts +236 -0
- package/layers/csl/adapters/input/UIAdapter.ts +0 -0
- package/layers/csl/adapters/output/ConsoleLogger.ts +34 -0
- package/layers/csl/adapters/output/CryptoHashGenerator.ts +29 -0
- package/layers/csl/adapters/output/FilePolicyRepository.ts +0 -0
- package/layers/csl/adapters/output/InMemoryPolicyRepository.ts +135 -0
- package/layers/csl/adapters/output/SystemTimestampProvider.ts +9 -0
- package/layers/csl/domain/entities/CSLResult.ts +309 -0
- package/layers/csl/domain/entities/Segment.ts +338 -0
- package/layers/csl/domain/entities/index.ts +2 -0
- package/layers/csl/domain/exceptions/ClassificationError.ts +26 -0
- package/layers/csl/domain/exceptions/SegmentationError.ts +30 -0
- package/layers/csl/domain/exceptions/index.ts +2 -0
- package/layers/csl/domain/index.ts +4 -0
- package/layers/csl/domain/services/AnomalyService.ts +255 -0
- package/layers/csl/domain/services/LineageService.ts +224 -0
- package/layers/csl/domain/services/NormalizationService.ts +392 -0
- package/layers/csl/domain/services/OriginClassificationService.ts +69 -0
- package/layers/csl/domain/services/PiDetectionService.ts +475 -0
- package/layers/csl/domain/services/PolicyService.ts +296 -0
- package/layers/csl/domain/services/SegmentClassificationService.ts +105 -0
- package/layers/csl/domain/services/SerializationService.ts +229 -0
- package/layers/csl/domain/services/index.ts +7 -0
- package/layers/csl/domain/value-objects/AnomalyScore.ts +23 -0
- package/layers/csl/domain/value-objects/ContentHash.ts +54 -0
- package/layers/csl/domain/value-objects/LineageEntry.ts +42 -0
- package/layers/csl/domain/value-objects/Origin-map.ts +67 -0
- package/layers/csl/domain/value-objects/Origin.ts +99 -0
- package/layers/csl/domain/value-objects/Pattern.ts +221 -0
- package/layers/csl/domain/value-objects/PiDetection.ts +140 -0
- package/layers/csl/domain/value-objects/PiDetectionResult.ts +275 -0
- package/layers/csl/domain/value-objects/PolicyRule.ts +151 -0
- package/layers/csl/domain/value-objects/TrustLevel.ts +34 -0
- package/layers/csl/domain/value-objects/index.ts +10 -0
- package/layers/csl/index.ts +3 -0
- package/layers/csl/ports/index.ts +10 -0
- package/layers/csl/ports/input/ClassificationPort.ts +76 -0
- package/layers/csl/ports/input/SegmentationPort.ts +81 -0
- package/layers/csl/ports/output/DOMAdapter.ts +14 -0
- package/layers/csl/ports/output/HashGenerator.ts +18 -0
- package/layers/csl/ports/output/Logger.ts +17 -0
- package/layers/csl/ports/output/PolicyRepository.ts +29 -0
- package/layers/csl/ports/output/SegmentClassified.ts +8 -0
- package/layers/csl/ports/output/TimeStampProvider.ts +5 -0
- package/layers/csl/services/CSLService.ts +393 -0
- package/layers/csl/services/index.ts +1 -0
- package/layers/csl/types/entities-types.ts +37 -0
- package/layers/csl/types/index.ts +4 -0
- package/layers/csl/types/pi-types.ts +111 -0
- package/layers/csl/types/port-output-types.ts +17 -0
- package/layers/csl/types/value-objects-types.ts +213 -0
- package/layers/csl/utils/colors.ts +25 -0
- package/layers/csl/utils/pattern-helpers.ts +174 -0
- package/package.json +50 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { Segment } from '../entities'
|
|
2
|
+
import { AnomalyScore } from '../value-objects'
|
|
3
|
+
import type { AnomalyAction, RiskScore } from '../../types'
|
|
4
|
+
import { TrustLevelType } from '../../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for AnomalyService
|
|
8
|
+
*
|
|
9
|
+
* @property highRiskThreshold - Score threshold for BLOCK action (default: 0.7)
|
|
10
|
+
* @property mediumRiskThreshold - Score threshold for WARN action (default: 0.3)
|
|
11
|
+
* @property trustLevelWeight - Weight multiplier for trust level in score calculation (default: 0.3)
|
|
12
|
+
* @property piDetectionWeight - Weight multiplier for PI detection in score calculation (default: 0.5)
|
|
13
|
+
* @property contentLengthWeight - Weight multiplier for content length anomaly (default: 0.1)
|
|
14
|
+
* @property originWeight - Weight multiplier for origin risk in score calculation (default: 0.1)
|
|
15
|
+
*/
|
|
16
|
+
export interface AnomalyServiceConfig {
|
|
17
|
+
readonly highRiskThreshold?: RiskScore
|
|
18
|
+
readonly mediumRiskThreshold?: RiskScore
|
|
19
|
+
readonly trustLevelWeight?: number
|
|
20
|
+
readonly piDetectionWeight?: number
|
|
21
|
+
readonly contentLengthWeight?: number
|
|
22
|
+
readonly originWeight?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* AnomalyService calculates anomaly scores for content segments based on multiple risk factors.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* This service performs complex risk analysis by combining multiple factors:
|
|
30
|
+
* - Trust level of the segment
|
|
31
|
+
* - Prompt injection detection results
|
|
32
|
+
* - Content length anomalies
|
|
33
|
+
* - Origin risk assessment
|
|
34
|
+
*
|
|
35
|
+
* **Key Features:**
|
|
36
|
+
* - Configurable thresholds and weights
|
|
37
|
+
* - Multi-factor risk analysis
|
|
38
|
+
* - Deterministic scoring algorithm
|
|
39
|
+
* - Returns AnomalyScore with recommended action
|
|
40
|
+
*
|
|
41
|
+
* **Scoring Algorithm:**
|
|
42
|
+
* The final score is a weighted combination of:
|
|
43
|
+
* 1. Trust Level Risk (0-1): Lower trust = higher risk
|
|
44
|
+
* 2. PI Detection Risk (0-1): Based on detection score
|
|
45
|
+
* 3. Content Length Anomaly (0-1): Very short or very long content
|
|
46
|
+
* 4. Origin Risk (0-1): Based on origin type
|
|
47
|
+
*
|
|
48
|
+
* Final score = (trustLevelRisk * trustLevelWeight) +
|
|
49
|
+
* (piDetectionRisk * piDetectionWeight) +
|
|
50
|
+
* (contentLengthRisk * contentLengthWeight) +
|
|
51
|
+
* (originRisk * originWeight)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // Default configuration
|
|
56
|
+
* const anomalyService = new AnomalyService()
|
|
57
|
+
*
|
|
58
|
+
* // Custom configuration
|
|
59
|
+
* const customService = new AnomalyService({
|
|
60
|
+
* highRiskThreshold: 0.8,
|
|
61
|
+
* mediumRiskThreshold: 0.4,
|
|
62
|
+
* piDetectionWeight: 0.7
|
|
63
|
+
* })
|
|
64
|
+
*
|
|
65
|
+
* // Calculate anomaly score
|
|
66
|
+
* const anomalyScore = anomalyService.calculateScore(segment)
|
|
67
|
+
*
|
|
68
|
+
* if (anomalyScore.isHighRisk()) {
|
|
69
|
+
* console.log('High risk detected!')
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export class AnomalyService {
|
|
74
|
+
private readonly config: Required<AnomalyServiceConfig>
|
|
75
|
+
|
|
76
|
+
constructor(config?: AnomalyServiceConfig) {
|
|
77
|
+
this.config = {
|
|
78
|
+
highRiskThreshold: config?.highRiskThreshold ?? 0.7,
|
|
79
|
+
mediumRiskThreshold: config?.mediumRiskThreshold ?? 0.3,
|
|
80
|
+
trustLevelWeight: config?.trustLevelWeight ?? 0.3,
|
|
81
|
+
piDetectionWeight: config?.piDetectionWeight ?? 0.5,
|
|
82
|
+
contentLengthWeight: config?.contentLengthWeight ?? 0.1,
|
|
83
|
+
originWeight: config?.originWeight ?? 0.1,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate thresholds
|
|
87
|
+
if (this.config.highRiskThreshold < this.config.mediumRiskThreshold) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`AnomalyService: highRiskThreshold (${this.config.highRiskThreshold}) ` +
|
|
90
|
+
`must be >= mediumRiskThreshold (${this.config.mediumRiskThreshold})`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate weights sum to approximately 1.0 (allow small floating point differences)
|
|
95
|
+
const totalWeight = this.config.trustLevelWeight +
|
|
96
|
+
this.config.piDetectionWeight +
|
|
97
|
+
this.config.contentLengthWeight +
|
|
98
|
+
this.config.originWeight
|
|
99
|
+
|
|
100
|
+
if (Math.abs(totalWeight - 1) > 0.01) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`AnomalyService: weights must sum to 1.0, got ${totalWeight}`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Calculates the anomaly score for a segment based on multiple risk factors
|
|
109
|
+
*
|
|
110
|
+
* @param segment - The segment to analyze
|
|
111
|
+
* @returns AnomalyScore with calculated score and recommended action
|
|
112
|
+
*
|
|
113
|
+
* @throws {TypeError} If segment is not a Segment instance
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const anomalyScore = anomalyService.calculateScore(segment)
|
|
118
|
+
* console.log(`Anomaly score: ${anomalyScore.score}, Action: ${anomalyScore.action}`)
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
calculateScore(segment: Segment): AnomalyScore {
|
|
122
|
+
if (!segment || typeof segment !== 'object' || !('id' in segment)) {
|
|
123
|
+
throw new TypeError('AnomalyService.calculateScore: segment must be a Segment instance')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Calculate individual risk components
|
|
127
|
+
const trustLevelRisk = this.calculateTrustLevelRisk(segment)
|
|
128
|
+
const piDetectionRisk = this.calculatePiDetectionRisk(segment)
|
|
129
|
+
const contentLengthRisk = this.calculateContentLengthRisk(segment)
|
|
130
|
+
const originRisk = this.calculateOriginRisk(segment)
|
|
131
|
+
|
|
132
|
+
// Weighted combination
|
|
133
|
+
const finalScore = Math.min(1,
|
|
134
|
+
(trustLevelRisk * this.config.trustLevelWeight) +
|
|
135
|
+
(piDetectionRisk * this.config.piDetectionWeight) +
|
|
136
|
+
(contentLengthRisk * this.config.contentLengthWeight) +
|
|
137
|
+
(originRisk * this.config.originWeight)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Determine action based on thresholds
|
|
141
|
+
const action = this.determineAction(finalScore)
|
|
142
|
+
|
|
143
|
+
return new AnomalyScore(finalScore, action)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculates risk based on trust level
|
|
148
|
+
* TC = 0.0 (no risk), STC = 0.5 (medium risk), UC = 1.0 (high risk)
|
|
149
|
+
*/
|
|
150
|
+
private calculateTrustLevelRisk(segment: Segment): RiskScore {
|
|
151
|
+
const trustLevel = segment.trustLevel
|
|
152
|
+
if (!trustLevel) {
|
|
153
|
+
return 1 as RiskScore // Unknown trust level = maximum risk
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
switch (trustLevel.value) {
|
|
157
|
+
case TrustLevelType.TC:
|
|
158
|
+
return 0 as RiskScore
|
|
159
|
+
case TrustLevelType.STC:
|
|
160
|
+
return 0.5 as RiskScore
|
|
161
|
+
case TrustLevelType.UC:
|
|
162
|
+
return 1 as RiskScore
|
|
163
|
+
default:
|
|
164
|
+
return 1 as RiskScore
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Calculates risk based on prompt injection detection results
|
|
170
|
+
*/
|
|
171
|
+
private calculatePiDetectionRisk(segment: Segment): RiskScore {
|
|
172
|
+
const piDetection = segment.piDetection
|
|
173
|
+
if (!piDetection) {
|
|
174
|
+
return 0 as RiskScore // No detection = no risk from this factor
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Use the detection score directly
|
|
178
|
+
return piDetection.score
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculates risk based on content length anomalies
|
|
183
|
+
* Very short (< 10 chars) or very long (> 10000 chars) content is suspicious
|
|
184
|
+
*/
|
|
185
|
+
private calculateContentLengthRisk(segment: Segment): RiskScore {
|
|
186
|
+
const contentLength = segment.content.length
|
|
187
|
+
|
|
188
|
+
// Very short content (potential injection fragments)
|
|
189
|
+
if (contentLength < 10) {
|
|
190
|
+
return 0.8 as RiskScore
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Very long content (potential obfuscation)
|
|
194
|
+
if (contentLength > 10000) {
|
|
195
|
+
return 0.6 as RiskScore
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Normal length range
|
|
199
|
+
if (contentLength >= 10 && contentLength <= 1000) {
|
|
200
|
+
return 0 as RiskScore
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Medium length (1000-10000) - slight risk
|
|
204
|
+
return 0.2 as RiskScore
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Calculates risk based on origin type
|
|
209
|
+
*/
|
|
210
|
+
private calculateOriginRisk(segment: Segment): RiskScore {
|
|
211
|
+
const origin = segment.origin
|
|
212
|
+
|
|
213
|
+
// User input is always high risk
|
|
214
|
+
if (origin.isUser()) {
|
|
215
|
+
return 1 as RiskScore
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// External content (network, injected) is high risk
|
|
219
|
+
if (origin.isExternal()) {
|
|
220
|
+
return 0.9 as RiskScore
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// System content is low risk
|
|
224
|
+
if (origin.isSystem()) {
|
|
225
|
+
return 0 as RiskScore
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// DOM content risk depends on visibility
|
|
229
|
+
if (origin.isDom()) {
|
|
230
|
+
// Hidden DOM content is more risky
|
|
231
|
+
return origin.type === 'DOM_HIDDEN' ? 0.7 as RiskScore : 0.3 as RiskScore
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Unknown origin is high risk
|
|
235
|
+
if (origin.isUnknown()) {
|
|
236
|
+
return 1 as RiskScore
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Default: medium risk
|
|
240
|
+
return 0.5 as RiskScore
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Determines the action based on the calculated score
|
|
245
|
+
*/
|
|
246
|
+
private determineAction(score: RiskScore): AnomalyAction {
|
|
247
|
+
if (score >= this.config.highRiskThreshold) {
|
|
248
|
+
return 'BLOCK'
|
|
249
|
+
} else if (score >= this.config.mediumRiskThreshold) {
|
|
250
|
+
return 'WARN'
|
|
251
|
+
} else {
|
|
252
|
+
return 'ALLOW'
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { LineageEntry } from '../value-objects'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LineageService manages the lineage (audit trail) of content segments.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* This service maintains a stateful map of segment IDs to their lineage entries,
|
|
8
|
+
* allowing complete traceability of how each segment was processed through the pipeline.
|
|
9
|
+
*
|
|
10
|
+
* **Key Features:**
|
|
11
|
+
* - Maintains internal state (Map) for lineage tracking
|
|
12
|
+
* - Supports multiple entries per segment (chronological order)
|
|
13
|
+
* - Provides query methods for lineage retrieval
|
|
14
|
+
* - Thread-safe operations (immutable entries)
|
|
15
|
+
*
|
|
16
|
+
* **Usage:**
|
|
17
|
+
* LineageService is instantiated once and reused across the processing pipeline.
|
|
18
|
+
* Each processing step should add an entry to track what happened to the segment.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const lineageService = new LineageService()
|
|
23
|
+
*
|
|
24
|
+
* // Add entries during processing
|
|
25
|
+
* lineageService.addEntry('seg-123', new LineageEntry('normalization', Date.now(), 'Unicode normalized'))
|
|
26
|
+
* lineageService.addEntry('seg-123', new LineageEntry('classification', Date.now(), 'Classified as TC'))
|
|
27
|
+
*
|
|
28
|
+
* // Retrieve lineage
|
|
29
|
+
* const lineage = lineageService.getLineage('seg-123')
|
|
30
|
+
* console.log(`Segment has ${lineage.length} processing steps`)
|
|
31
|
+
*
|
|
32
|
+
* // Clear lineage when done
|
|
33
|
+
* lineageService.clearLineage('seg-123')
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class LineageService {
|
|
37
|
+
private readonly lineageMap: Map<string, LineageEntry[]>
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.lineageMap = new Map()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds a lineage entry for a specific segment
|
|
45
|
+
*
|
|
46
|
+
* @param segmentId - The unique identifier of the segment
|
|
47
|
+
* @param entry - The lineage entry to add
|
|
48
|
+
*
|
|
49
|
+
* @throws {TypeError} If segmentId is not a valid string
|
|
50
|
+
* @throws {TypeError} If entry is not a LineageEntry instance
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* lineageService.addEntry('seg-123', new LineageEntry('normalization', Date.now(), 'Applied'))
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
addEntry(segmentId: string, entry: LineageEntry): void {
|
|
58
|
+
if (!segmentId || typeof segmentId !== 'string' || segmentId.trim().length === 0) {
|
|
59
|
+
throw new TypeError('LineageService.addEntry: segmentId must be a non-empty string')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!(entry instanceof LineageEntry)) {
|
|
63
|
+
throw new TypeError('LineageService.addEntry: entry must be a LineageEntry instance')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const currentLineage = this.lineageMap.get(segmentId) ?? []
|
|
67
|
+
this.lineageMap.set(segmentId, [...currentLineage, entry])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves all lineage entries for a specific segment
|
|
72
|
+
*
|
|
73
|
+
* @param segmentId - The unique identifier of the segment
|
|
74
|
+
* @returns Array of LineageEntry instances in chronological order (empty array if no entries)
|
|
75
|
+
*
|
|
76
|
+
* @throws {TypeError} If segmentId is not a valid string
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const lineage = lineageService.getLineage('seg-123')
|
|
81
|
+
* lineage.forEach(entry => {
|
|
82
|
+
* console.log(`${entry.step} at ${new Date(entry.timestamp).toISOString()}`)
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
getLineage(segmentId: string): readonly LineageEntry[] {
|
|
87
|
+
if (!segmentId || typeof segmentId !== 'string' || segmentId.trim().length === 0) {
|
|
88
|
+
throw new TypeError('LineageService.getLineage: segmentId must be a non-empty string')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lineage = this.lineageMap.get(segmentId)
|
|
92
|
+
return lineage ? Object.freeze([...lineage]) : Object.freeze([])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if a segment has any lineage entries
|
|
97
|
+
*
|
|
98
|
+
* @param segmentId - The unique identifier of the segment
|
|
99
|
+
* @returns true if the segment has lineage entries, false otherwise
|
|
100
|
+
*
|
|
101
|
+
* @throws {TypeError} If segmentId is not a valid string
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* if (lineageService.hasLineage('seg-123')) {
|
|
106
|
+
* const entries = lineageService.getLineage('seg-123')
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
hasLineage(segmentId: string): boolean {
|
|
111
|
+
if (!segmentId || typeof segmentId !== 'string' || segmentId.trim().length === 0) {
|
|
112
|
+
throw new TypeError('LineageService.hasLineage: segmentId must be a non-empty string')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lineage = this.lineageMap.get(segmentId)
|
|
116
|
+
return lineage !== undefined && lineage.length > 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clears all lineage entries for a specific segment
|
|
121
|
+
*
|
|
122
|
+
* @param segmentId - The unique identifier of the segment
|
|
123
|
+
* @returns true if lineage was cleared, false if segment had no lineage
|
|
124
|
+
*
|
|
125
|
+
* @throws {TypeError} If segmentId is not a valid string
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const hadLineage = lineageService.clearLineage('seg-123')
|
|
130
|
+
* if (hadLineage) {
|
|
131
|
+
* console.log('Lineage cleared')
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
clearLineage(segmentId: string): boolean {
|
|
136
|
+
if (!segmentId || typeof segmentId !== 'string' || segmentId.trim().length === 0) {
|
|
137
|
+
throw new TypeError('LineageService.clearLineage: segmentId must be a non-empty string')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hadLineage = this.lineageMap.has(segmentId)
|
|
141
|
+
this.lineageMap.delete(segmentId)
|
|
142
|
+
return hadLineage
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Retrieves all segment IDs that have lineage entries
|
|
147
|
+
*
|
|
148
|
+
* @returns Array of segment IDs that have lineage entries
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const segmentIds = lineageService.getAllSegmentIds()
|
|
153
|
+
* console.log(`Tracking ${segmentIds.length} segments`)
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
getAllSegmentIds(): readonly string[] {
|
|
157
|
+
return Object.freeze([...this.lineageMap.keys()])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Retrieves the total count of lineage entries across all segments
|
|
162
|
+
*
|
|
163
|
+
* @returns Total number of lineage entries
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const totalEntries = lineageService.getTotalEntryCount()
|
|
168
|
+
* console.log(`Total lineage entries: ${totalEntries}`)
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
getTotalEntryCount(): number {
|
|
172
|
+
let total = 0
|
|
173
|
+
for (const lineage of this.lineageMap.values()) {
|
|
174
|
+
total += lineage.length
|
|
175
|
+
}
|
|
176
|
+
return total
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clears all lineage entries for all segments
|
|
181
|
+
*
|
|
182
|
+
* @returns Number of segments that had lineage entries
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* const clearedCount = lineageService.clearAllLineage()
|
|
187
|
+
* console.log(`Cleared lineage for ${clearedCount} segments`)
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
clearAllLineage(): number {
|
|
191
|
+
const count = this.lineageMap.size
|
|
192
|
+
this.lineageMap.clear()
|
|
193
|
+
return count
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets lineage entries for a specific step type
|
|
198
|
+
*
|
|
199
|
+
* @param segmentId - The unique identifier of the segment
|
|
200
|
+
* @param stepType - The step type to filter by (e.g., 'normalization', 'classification')
|
|
201
|
+
* @returns Array of LineageEntry instances matching the step type
|
|
202
|
+
*
|
|
203
|
+
* @throws {TypeError} If segmentId or stepType are not valid strings
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const normalizationEntries = lineageService.getLineageByStep('seg-123', 'normalization')
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
getLineageByStep(segmentId: string, stepType: string): readonly LineageEntry[] {
|
|
211
|
+
if (!segmentId || typeof segmentId !== 'string' || segmentId.trim().length === 0) {
|
|
212
|
+
throw new TypeError('LineageService.getLineageByStep: segmentId must be a non-empty string')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!stepType || typeof stepType !== 'string' || stepType.trim().length === 0) {
|
|
216
|
+
throw new TypeError('LineageService.getLineageByStep: stepType must be a non-empty string')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lineage = this.getLineage(segmentId)
|
|
220
|
+
return Object.freeze(
|
|
221
|
+
lineage.filter(entry => entry.step === stepType.trim())
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|