@nathanvale/chatline 0.0.1
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/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +1535 -0
- package/dist/bin/index.js +5121 -0
- package/dist/cli/commands/clean.d.ts +17 -0
- package/dist/cli/commands/clean.d.ts.map +1 -0
- package/dist/cli/commands/clean.js +142 -0
- package/dist/cli/commands/clean.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +17 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +202 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/enrich-ai.d.ts +17 -0
- package/dist/cli/commands/enrich-ai.d.ts.map +1 -0
- package/dist/cli/commands/enrich-ai.js +371 -0
- package/dist/cli/commands/enrich-ai.js.map +1 -0
- package/dist/cli/commands/index.d.ts +16 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +16 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/ingest-csv.d.ts +17 -0
- package/dist/cli/commands/ingest-csv.d.ts.map +1 -0
- package/dist/cli/commands/ingest-csv.js +138 -0
- package/dist/cli/commands/ingest-csv.js.map +1 -0
- package/dist/cli/commands/ingest-db.d.ts +17 -0
- package/dist/cli/commands/ingest-db.d.ts.map +1 -0
- package/dist/cli/commands/ingest-db.js +159 -0
- package/dist/cli/commands/ingest-db.js.map +1 -0
- package/dist/cli/commands/init.d.ts +17 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +110 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/normalize-link.d.ts +16 -0
- package/dist/cli/commands/normalize-link.d.ts.map +1 -0
- package/dist/cli/commands/normalize-link.js +144 -0
- package/dist/cli/commands/normalize-link.js.map +1 -0
- package/dist/cli/commands/render-markdown.d.ts +17 -0
- package/dist/cli/commands/render-markdown.d.ts.map +1 -0
- package/dist/cli/commands/render-markdown.js +218 -0
- package/dist/cli/commands/render-markdown.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +17 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +175 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +17 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +152 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +121 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/types.d.ts +93 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +7 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/utils.d.ts +29 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +53 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1805 -0
- package/dist/config/generator.d.ts +90 -0
- package/dist/config/generator.d.ts.map +1 -0
- package/dist/config/generator.js +320 -0
- package/dist/config/generator.js.map +1 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +251 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +107 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +169 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/enrich/audio-transcription.d.ts +77 -0
- package/dist/enrich/audio-transcription.d.ts.map +1 -0
- package/dist/enrich/audio-transcription.js +370 -0
- package/dist/enrich/audio-transcription.js.map +1 -0
- package/dist/enrich/checkpoint.d.ts +137 -0
- package/dist/enrich/checkpoint.d.ts.map +1 -0
- package/dist/enrich/checkpoint.js +205 -0
- package/dist/enrich/checkpoint.js.map +1 -0
- package/dist/enrich/idempotency.d.ts +90 -0
- package/dist/enrich/idempotency.d.ts.map +1 -0
- package/dist/enrich/idempotency.js +188 -0
- package/dist/enrich/idempotency.js.map +1 -0
- package/dist/enrich/image-analysis.d.ts +62 -0
- package/dist/enrich/image-analysis.d.ts.map +1 -0
- package/dist/enrich/image-analysis.js +264 -0
- package/dist/enrich/image-analysis.js.map +1 -0
- package/dist/enrich/index.d.ts +60 -0
- package/dist/enrich/index.d.ts.map +1 -0
- package/dist/enrich/index.js +74 -0
- package/dist/enrich/index.js.map +1 -0
- package/dist/enrich/link-enrichment.d.ts +37 -0
- package/dist/enrich/link-enrichment.d.ts.map +1 -0
- package/dist/enrich/link-enrichment.js +202 -0
- package/dist/enrich/link-enrichment.js.map +1 -0
- package/dist/enrich/pdf-video-handling.d.ts +49 -0
- package/dist/enrich/pdf-video-handling.d.ts.map +1 -0
- package/dist/enrich/pdf-video-handling.js +325 -0
- package/dist/enrich/pdf-video-handling.js.map +1 -0
- package/dist/enrich/progress-tracker.d.ts +120 -0
- package/dist/enrich/progress-tracker.d.ts.map +1 -0
- package/dist/enrich/progress-tracker.js +220 -0
- package/dist/enrich/progress-tracker.js.map +1 -0
- package/dist/enrich/providers/firecrawl.d.ts +18 -0
- package/dist/enrich/providers/firecrawl.d.ts.map +1 -0
- package/dist/enrich/providers/firecrawl.js +48 -0
- package/dist/enrich/providers/firecrawl.js.map +1 -0
- package/dist/enrich/providers/generic.d.ts +16 -0
- package/dist/enrich/providers/generic.d.ts.map +1 -0
- package/dist/enrich/providers/generic.js +36 -0
- package/dist/enrich/providers/generic.js.map +1 -0
- package/dist/enrich/providers/index.d.ts +14 -0
- package/dist/enrich/providers/index.d.ts.map +1 -0
- package/dist/enrich/providers/index.js +13 -0
- package/dist/enrich/providers/index.js.map +1 -0
- package/dist/enrich/providers/instagram.d.ts +16 -0
- package/dist/enrich/providers/instagram.d.ts.map +1 -0
- package/dist/enrich/providers/instagram.js +43 -0
- package/dist/enrich/providers/instagram.js.map +1 -0
- package/dist/enrich/providers/spotify.d.ts +16 -0
- package/dist/enrich/providers/spotify.d.ts.map +1 -0
- package/dist/enrich/providers/spotify.js +45 -0
- package/dist/enrich/providers/spotify.js.map +1 -0
- package/dist/enrich/providers/twitter.d.ts +16 -0
- package/dist/enrich/providers/twitter.d.ts.map +1 -0
- package/dist/enrich/providers/twitter.js +43 -0
- package/dist/enrich/providers/twitter.js.map +1 -0
- package/dist/enrich/providers/types.d.ts +47 -0
- package/dist/enrich/providers/types.d.ts.map +1 -0
- package/dist/enrich/providers/types.js +15 -0
- package/dist/enrich/providers/types.js.map +1 -0
- package/dist/enrich/providers/youtube.d.ts +16 -0
- package/dist/enrich/providers/youtube.d.ts.map +1 -0
- package/dist/enrich/providers/youtube.js +43 -0
- package/dist/enrich/providers/youtube.js.map +1 -0
- package/dist/enrich/rate-limiting.d.ts +118 -0
- package/dist/enrich/rate-limiting.d.ts.map +1 -0
- package/dist/enrich/rate-limiting.js +258 -0
- package/dist/enrich/rate-limiting.js.map +1 -0
- package/dist/index.d.ts +688 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1729 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/dedup-merge.d.ts +82 -0
- package/dist/ingest/dedup-merge.d.ts.map +1 -0
- package/dist/ingest/dedup-merge.js +262 -0
- package/dist/ingest/dedup-merge.js.map +1 -0
- package/dist/ingest/ingest-csv.d.ts +62 -0
- package/dist/ingest/ingest-csv.d.ts.map +1 -0
- package/dist/ingest/ingest-csv.js +300 -0
- package/dist/ingest/ingest-csv.js.map +1 -0
- package/dist/ingest/ingest-db.d.ts +64 -0
- package/dist/ingest/ingest-db.d.ts.map +1 -0
- package/dist/ingest/ingest-db.js +172 -0
- package/dist/ingest/ingest-db.js.map +1 -0
- package/dist/ingest/link-replies-and-tapbacks.d.ts +53 -0
- package/dist/ingest/link-replies-and-tapbacks.d.ts.map +1 -0
- package/dist/ingest/link-replies-and-tapbacks.js +381 -0
- package/dist/ingest/link-replies-and-tapbacks.js.map +1 -0
- package/dist/normalize/date-converters.d.ts +45 -0
- package/dist/normalize/date-converters.d.ts.map +1 -0
- package/dist/normalize/date-converters.js +166 -0
- package/dist/normalize/date-converters.js.map +1 -0
- package/dist/normalize/path-validator.d.ts +65 -0
- package/dist/normalize/path-validator.d.ts.map +1 -0
- package/dist/normalize/path-validator.js +221 -0
- package/dist/normalize/path-validator.js.map +1 -0
- package/dist/normalize/validate-normalized.d.ts +45 -0
- package/dist/normalize/validate-normalized.d.ts.map +1 -0
- package/dist/normalize/validate-normalized.js +144 -0
- package/dist/normalize/validate-normalized.js.map +1 -0
- package/dist/render/embeds-blockquotes.d.ts +84 -0
- package/dist/render/embeds-blockquotes.d.ts.map +1 -0
- package/dist/render/embeds-blockquotes.js +204 -0
- package/dist/render/embeds-blockquotes.js.map +1 -0
- package/dist/render/grouping.d.ts +78 -0
- package/dist/render/grouping.d.ts.map +1 -0
- package/dist/render/grouping.js +134 -0
- package/dist/render/grouping.js.map +1 -0
- package/dist/render/index.d.ts +47 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +245 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/reply-rendering.d.ts +88 -0
- package/dist/render/reply-rendering.d.ts.map +1 -0
- package/dist/render/reply-rendering.js +196 -0
- package/dist/render/reply-rendering.js.map +1 -0
- package/dist/schema/message.d.ts +125 -0
- package/dist/schema/message.d.ts.map +1 -0
- package/dist/schema/message.js +331 -0
- package/dist/schema/message.js.map +1 -0
- package/dist/utils/delta-detection.d.ts +107 -0
- package/dist/utils/delta-detection.d.ts.map +1 -0
- package/dist/utils/delta-detection.js +199 -0
- package/dist/utils/delta-detection.js.map +1 -0
- package/dist/utils/enrichment-merge.d.ts +135 -0
- package/dist/utils/enrichment-merge.d.ts.map +1 -0
- package/dist/utils/enrichment-merge.js +280 -0
- package/dist/utils/enrichment-merge.js.map +1 -0
- package/dist/utils/human.d.ts +15 -0
- package/dist/utils/human.d.ts.map +1 -0
- package/dist/utils/human.js +27 -0
- package/dist/utils/human.js.map +1 -0
- package/dist/utils/incremental-state.d.ts +133 -0
- package/dist/utils/incremental-state.d.ts.map +1 -0
- package/dist/utils/incremental-state.js +237 -0
- package/dist/utils/incremental-state.js.map +1 -0
- package/dist/utils/logger.d.ts +40 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +176 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +165 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply and tapback linking for NORMALIZE--T03
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* AC01: Link replies using DB association_guid as primary method
|
|
6
|
+
* AC02: Apply heuristics for unlinked replies (timestamp proximity <30s, content patterns)
|
|
7
|
+
* AC03: Link tapbacks to parent message GUIDs (including part GUIDs)
|
|
8
|
+
* AC04: Handle ambiguous links with structured logging and tie counters
|
|
9
|
+
* AC05: Maintain parity with CSV linking rules from original analyzer
|
|
10
|
+
*/
|
|
11
|
+
const REPLY_WINDOW_SECONDS = 30; // AC02: <30s proximity threshold
|
|
12
|
+
const REPLY_SEARCH_WINDOW_MINUTES = 5; // Expand to ±5 minutes if needed
|
|
13
|
+
const TAPBACK_WINDOW_SECONDS = 30; // Tapbacks within 30s of parent
|
|
14
|
+
/**
|
|
15
|
+
* AC01 + AC02: Link replies to their parent messages
|
|
16
|
+
*
|
|
17
|
+
* Primary: DB association_guid when present
|
|
18
|
+
* Fallback: Heuristics using timestamp and content matching
|
|
19
|
+
*/
|
|
20
|
+
export function linkRepliesToParents(messages, options = {}) {
|
|
21
|
+
const { trackAmbiguous = false, minConfidenceThreshold: _minConfidenceThreshold = 0.7, } = options;
|
|
22
|
+
// Build indices for fast lookup
|
|
23
|
+
const byGuid = new Map();
|
|
24
|
+
const byTimestamp = new Map();
|
|
25
|
+
messages.forEach((msg) => {
|
|
26
|
+
byGuid.set(msg.guid, msg);
|
|
27
|
+
// Use minute-based buckets for O(1) lookup in time window searches
|
|
28
|
+
const minuteBucket = new Date(msg.date).toISOString().slice(0, 16); // YYYY-MM-DDTHH:mm
|
|
29
|
+
if (!byTimestamp.has(minuteBucket)) {
|
|
30
|
+
byTimestamp.set(minuteBucket, []);
|
|
31
|
+
}
|
|
32
|
+
byTimestamp.get(minuteBucket).push(msg);
|
|
33
|
+
});
|
|
34
|
+
const ambiguousLinks = [];
|
|
35
|
+
const result = messages.map((msg) => {
|
|
36
|
+
// Only process text and media replies (not already linked)
|
|
37
|
+
if (msg.messageKind !== 'text' && msg.messageKind !== 'media') {
|
|
38
|
+
return msg;
|
|
39
|
+
}
|
|
40
|
+
// Skip if already has DB association
|
|
41
|
+
if (msg.replyingTo?.targetMessageGuid) {
|
|
42
|
+
return msg;
|
|
43
|
+
}
|
|
44
|
+
// Skip empty replies
|
|
45
|
+
if (!msg.text || msg.text.trim().length === 0) {
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
// Try to link using heuristics
|
|
49
|
+
const candidates = findReplyParentCandidates(msg, messages, byGuid, byTimestamp);
|
|
50
|
+
if (candidates.length === 0) {
|
|
51
|
+
return msg;
|
|
52
|
+
}
|
|
53
|
+
// Sort by score (descending)
|
|
54
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
55
|
+
const topCandidate = candidates[0];
|
|
56
|
+
if (!topCandidate) {
|
|
57
|
+
return msg; // Should never happen since we checked length > 0
|
|
58
|
+
}
|
|
59
|
+
const topScore = topCandidate.score;
|
|
60
|
+
// Check for ties
|
|
61
|
+
const tiedCandidates = candidates.filter((c) => c.score === topScore);
|
|
62
|
+
const isTie = tiedCandidates.length > 1;
|
|
63
|
+
if (isTie && trackAmbiguous) {
|
|
64
|
+
const firstTied = tiedCandidates[0];
|
|
65
|
+
if (firstTied) {
|
|
66
|
+
ambiguousLinks.push({
|
|
67
|
+
messageGuid: msg.guid,
|
|
68
|
+
selectedTarget: firstTied.message.guid,
|
|
69
|
+
candidates: tiedCandidates,
|
|
70
|
+
tieCount: tiedCandidates.length,
|
|
71
|
+
confidenceScore: topScore,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Link to best candidate
|
|
76
|
+
return {
|
|
77
|
+
...msg,
|
|
78
|
+
replyingTo: {
|
|
79
|
+
...msg.replyingTo,
|
|
80
|
+
targetMessageGuid: topCandidate.message.guid,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return trackAmbiguous ? { messages: result, ambiguousLinks } : result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* AC03: Link tapbacks to their parent messages
|
|
88
|
+
*
|
|
89
|
+
* Primary: DB association_guid when present
|
|
90
|
+
* Fallback: Heuristics preferring media messages
|
|
91
|
+
*/
|
|
92
|
+
export function linkTapbacksToParents(messages, options = {}) {
|
|
93
|
+
const { trackAmbiguous = false } = options;
|
|
94
|
+
// Build indices
|
|
95
|
+
const byGuid = new Map();
|
|
96
|
+
const byTimestamp = new Map();
|
|
97
|
+
messages.forEach((msg) => {
|
|
98
|
+
byGuid.set(msg.guid, msg);
|
|
99
|
+
// Use minute-based buckets for O(1) lookup in time window searches
|
|
100
|
+
const minuteBucket = new Date(msg.date).toISOString().slice(0, 16); // YYYY-MM-DDTHH:mm
|
|
101
|
+
if (!byTimestamp.has(minuteBucket)) {
|
|
102
|
+
byTimestamp.set(minuteBucket, []);
|
|
103
|
+
}
|
|
104
|
+
byTimestamp.get(minuteBucket).push(msg);
|
|
105
|
+
});
|
|
106
|
+
const ambiguousLinks = [];
|
|
107
|
+
const result = messages.map((msg) => {
|
|
108
|
+
// Only process tapback messages
|
|
109
|
+
if (msg.messageKind !== 'tapback') {
|
|
110
|
+
return msg;
|
|
111
|
+
}
|
|
112
|
+
// Skip if already has DB association
|
|
113
|
+
if (msg.tapback?.targetMessageGuid) {
|
|
114
|
+
return msg;
|
|
115
|
+
}
|
|
116
|
+
// Find parent for this tapback
|
|
117
|
+
const candidates = findTapbackParentCandidates(msg, messages, byGuid, byTimestamp);
|
|
118
|
+
if (candidates.length === 0) {
|
|
119
|
+
return msg;
|
|
120
|
+
}
|
|
121
|
+
// Sort by score
|
|
122
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
123
|
+
const topCandidate = candidates[0];
|
|
124
|
+
if (!topCandidate) {
|
|
125
|
+
return msg; // Should never happen since we checked length > 0
|
|
126
|
+
}
|
|
127
|
+
const topScore = topCandidate.score;
|
|
128
|
+
// Check for ties
|
|
129
|
+
const tiedCandidates = candidates.filter((c) => c.score === topScore);
|
|
130
|
+
if (tiedCandidates.length > 1 && trackAmbiguous) {
|
|
131
|
+
const firstTied = tiedCandidates[0];
|
|
132
|
+
if (firstTied) {
|
|
133
|
+
ambiguousLinks.push({
|
|
134
|
+
messageGuid: msg.guid,
|
|
135
|
+
selectedTarget: firstTied.message.guid,
|
|
136
|
+
candidates: tiedCandidates,
|
|
137
|
+
tieCount: tiedCandidates.length,
|
|
138
|
+
confidenceScore: topScore,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Link to best candidate
|
|
143
|
+
if (!msg.tapback) {
|
|
144
|
+
return msg; // Shouldn't happen for tapback messages
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...msg,
|
|
148
|
+
tapback: {
|
|
149
|
+
...msg.tapback,
|
|
150
|
+
targetMessageGuid: topCandidate.message.guid,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
return trackAmbiguous ? { messages: result, ambiguousLinks } : result;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* AC04: Detect and report ambiguous links with confidence scores
|
|
158
|
+
*/
|
|
159
|
+
export function detectAmbiguousLinks(messages) {
|
|
160
|
+
const ambiguous = linkRepliesToParents(messages, {
|
|
161
|
+
trackAmbiguous: true,
|
|
162
|
+
});
|
|
163
|
+
const tapbackAmbiguous = linkTapbacksToParents(messages, {
|
|
164
|
+
trackAmbiguous: true,
|
|
165
|
+
});
|
|
166
|
+
const allAmbiguous = [
|
|
167
|
+
...(ambiguous.ambiguousLinks || []),
|
|
168
|
+
...(tapbackAmbiguous.ambiguousLinks || []),
|
|
169
|
+
];
|
|
170
|
+
return {
|
|
171
|
+
tieCount: allAmbiguous.length,
|
|
172
|
+
ambiguousMessages: allAmbiguous.map((link) => ({
|
|
173
|
+
messageGuid: link.messageGuid,
|
|
174
|
+
selectedTarget: link.selectedTarget,
|
|
175
|
+
tieCount: link.tieCount,
|
|
176
|
+
topCandidates: link.candidates.map((c) => ({
|
|
177
|
+
guid: c.message.guid,
|
|
178
|
+
score: c.score,
|
|
179
|
+
reasons: c.reasons,
|
|
180
|
+
})),
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Helper Functions
|
|
186
|
+
// ============================================================================
|
|
187
|
+
/**
|
|
188
|
+
* Get time bucket keys for a date within a window (for O(1) lookups)
|
|
189
|
+
*/
|
|
190
|
+
function getTimeBucketKeys(date, windowMinutes) {
|
|
191
|
+
const keys = [];
|
|
192
|
+
const baseTime = date.getTime();
|
|
193
|
+
// Generate bucket keys for the window before the date
|
|
194
|
+
for (let i = 0; i <= windowMinutes; i++) {
|
|
195
|
+
const bucketDate = new Date(baseTime - i * 60 * 1000);
|
|
196
|
+
keys.push(bucketDate.toISOString().slice(0, 16)); // YYYY-MM-DDTHH:mm (minute bucket)
|
|
197
|
+
}
|
|
198
|
+
return keys;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Find candidate parent messages for a reply
|
|
202
|
+
* Returns scored candidates
|
|
203
|
+
* Uses byTimestamp Map for O(1) bucket lookups instead of O(n) scan
|
|
204
|
+
*/
|
|
205
|
+
function findReplyParentCandidates(reply, _allMessages, _byGuid, byTimestamp) {
|
|
206
|
+
const replyDate = new Date(reply.date);
|
|
207
|
+
const replyTime = replyDate.getTime();
|
|
208
|
+
const candidates = [];
|
|
209
|
+
// Use replyTime for arithmetic operations (replyDate is Date object)
|
|
210
|
+
// Extract snippet from reply if present (CSV pattern: "➜ Replying to: \"<snippet>\"")
|
|
211
|
+
const snippetMatch = reply.text?.match(/(?:➜\s*Replying to:?\s+[«"]([^»"]+)[»"]|Replying to:?\s+[«"]([^»"]+)[»"])/);
|
|
212
|
+
const snippet = snippetMatch?.[1] || snippetMatch?.[2];
|
|
213
|
+
// Use time-bucketed lookup for O(1) average case per bucket
|
|
214
|
+
const bucketKeys = getTimeBucketKeys(replyDate, REPLY_SEARCH_WINDOW_MINUTES);
|
|
215
|
+
const seenGuids = new Set();
|
|
216
|
+
const potentialParents = [];
|
|
217
|
+
for (const key of bucketKeys) {
|
|
218
|
+
const bucketMessages = byTimestamp.get(key);
|
|
219
|
+
if (bucketMessages) {
|
|
220
|
+
for (const msg of bucketMessages) {
|
|
221
|
+
if (!seenGuids.has(msg.guid) &&
|
|
222
|
+
msg.messageKind !== 'tapback' &&
|
|
223
|
+
msg.messageKind !== 'notification' &&
|
|
224
|
+
msg.guid !== reply.guid) {
|
|
225
|
+
seenGuids.add(msg.guid);
|
|
226
|
+
potentialParents.push(msg);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Score each candidate
|
|
232
|
+
for (const candidate of potentialParents) {
|
|
233
|
+
if (!candidate.text && candidate.messageKind !== 'media') {
|
|
234
|
+
continue; // Skip messages without text or media
|
|
235
|
+
}
|
|
236
|
+
const candidateTime = new Date(candidate.date).getTime();
|
|
237
|
+
const timeDeltaMs = replyTime - candidateTime;
|
|
238
|
+
const timeDeltaSeconds = timeDeltaMs / 1000;
|
|
239
|
+
// Skip if too old (not within search window)
|
|
240
|
+
if (timeDeltaSeconds < 0 ||
|
|
241
|
+
timeDeltaSeconds > REPLY_SEARCH_WINDOW_MINUTES * 60) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let score = 0;
|
|
245
|
+
const reasons = [];
|
|
246
|
+
// AC02: Timestamp proximity scoring
|
|
247
|
+
if (timeDeltaSeconds <= REPLY_WINDOW_SECONDS) {
|
|
248
|
+
score += 20;
|
|
249
|
+
reasons.push(`exact_second_match (Δ${timeDeltaSeconds.toFixed(1)}s)`);
|
|
250
|
+
}
|
|
251
|
+
// Snippet matching (AC05: CSV parity)
|
|
252
|
+
let hasContentMatch = false;
|
|
253
|
+
if (snippet && candidate.text) {
|
|
254
|
+
const normalizedText = candidate.text.toLowerCase();
|
|
255
|
+
const normalizedSnippet = snippet.toLowerCase();
|
|
256
|
+
if (normalizedText.startsWith(normalizedSnippet)) {
|
|
257
|
+
score += 100;
|
|
258
|
+
reasons.push('snippet_startswith');
|
|
259
|
+
hasContentMatch = true;
|
|
260
|
+
}
|
|
261
|
+
else if (normalizedText.includes(normalizedSnippet)) {
|
|
262
|
+
score += 50;
|
|
263
|
+
reasons.push('snippet_includes');
|
|
264
|
+
hasContentMatch = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Media-implied replies (AC05: CSV parity)
|
|
268
|
+
if (candidate.messageKind === 'media') {
|
|
269
|
+
if (!snippet ||
|
|
270
|
+
reply.text?.toLowerCase().includes('photo') ||
|
|
271
|
+
reply.text?.toLowerCase().includes('image')) {
|
|
272
|
+
score += 80;
|
|
273
|
+
reasons.push('media_candidate');
|
|
274
|
+
hasContentMatch = true;
|
|
275
|
+
// Prefer lower timestamp_index (earlier part)
|
|
276
|
+
const indexMatch = candidate.guid.match(/p:(\d+)\//);
|
|
277
|
+
if (indexMatch?.[1]) {
|
|
278
|
+
score += 10 - Number.parseInt(indexMatch[1], 10);
|
|
279
|
+
reasons.push(`index_preference(${indexMatch[1]})`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Only extend beyond 30s window if there's strong content evidence
|
|
284
|
+
if (timeDeltaSeconds > REPLY_WINDOW_SECONDS && hasContentMatch) {
|
|
285
|
+
score -= timeDeltaSeconds / 100; // Mild penalty for distance
|
|
286
|
+
reasons.push(`extended_window (Δ${timeDeltaSeconds.toFixed(1)}s)`);
|
|
287
|
+
}
|
|
288
|
+
// Same sender preference
|
|
289
|
+
if (reply.handle && candidate.handle === reply.handle) {
|
|
290
|
+
score += 15;
|
|
291
|
+
reasons.push('same_sender');
|
|
292
|
+
}
|
|
293
|
+
// Same group/moment preference
|
|
294
|
+
if (reply.groupGuid && candidate.groupGuid === reply.groupGuid) {
|
|
295
|
+
score += 10;
|
|
296
|
+
reasons.push('same_group');
|
|
297
|
+
}
|
|
298
|
+
if (score > 0) {
|
|
299
|
+
candidates.push({ message: candidate, score, reasons });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Sort all candidates: first by score (desc), then by time proximity (asc) for tiebreaking
|
|
303
|
+
candidates.sort((a, b) => {
|
|
304
|
+
// Primary: score (higher is better)
|
|
305
|
+
if (a.score !== b.score) {
|
|
306
|
+
return b.score - a.score;
|
|
307
|
+
}
|
|
308
|
+
// Tiebreaker: nearest prior message (lowest time delta)
|
|
309
|
+
const aDelta = replyTime - new Date(a.message.date).getTime();
|
|
310
|
+
const bDelta = replyTime - new Date(b.message.date).getTime();
|
|
311
|
+
return aDelta - bDelta;
|
|
312
|
+
});
|
|
313
|
+
return candidates;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Find candidate parent messages for a tapback
|
|
317
|
+
* Prefers media messages
|
|
318
|
+
* Uses byTimestamp Map for O(1) bucket lookups instead of O(n) scan
|
|
319
|
+
*/
|
|
320
|
+
function findTapbackParentCandidates(tapback, _allMessages, _byGuid, byTimestamp) {
|
|
321
|
+
const tapbackDate = new Date(tapback.date);
|
|
322
|
+
const tapbackTime = tapbackDate.getTime();
|
|
323
|
+
const candidates = [];
|
|
324
|
+
// Use time-bucketed lookup for O(1) average case per bucket
|
|
325
|
+
const bucketKeys = getTimeBucketKeys(tapbackDate, REPLY_SEARCH_WINDOW_MINUTES);
|
|
326
|
+
const seenGuids = new Set();
|
|
327
|
+
const potentialParents = [];
|
|
328
|
+
for (const key of bucketKeys) {
|
|
329
|
+
const bucketMessages = byTimestamp.get(key);
|
|
330
|
+
if (bucketMessages) {
|
|
331
|
+
for (const msg of bucketMessages) {
|
|
332
|
+
if (!seenGuids.has(msg.guid) &&
|
|
333
|
+
msg.messageKind !== 'tapback' &&
|
|
334
|
+
msg.messageKind !== 'notification' &&
|
|
335
|
+
msg.guid !== tapback.guid) {
|
|
336
|
+
seenGuids.add(msg.guid);
|
|
337
|
+
potentialParents.push(msg);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Score each candidate
|
|
343
|
+
for (const candidate of potentialParents) {
|
|
344
|
+
const candidateTime = new Date(candidate.date).getTime();
|
|
345
|
+
const timeDeltaSeconds = (tapbackTime - candidateTime) / 1000;
|
|
346
|
+
// Skip if too old or in future
|
|
347
|
+
if (timeDeltaSeconds < 0 ||
|
|
348
|
+
timeDeltaSeconds > REPLY_SEARCH_WINDOW_MINUTES * 60) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
let score = 0;
|
|
352
|
+
const reasons = [];
|
|
353
|
+
// Timestamp proximity
|
|
354
|
+
if (timeDeltaSeconds <= TAPBACK_WINDOW_SECONDS) {
|
|
355
|
+
score += 20;
|
|
356
|
+
reasons.push(`near_tap (Δ${timeDeltaSeconds.toFixed(1)}s)`);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
score -= timeDeltaSeconds;
|
|
360
|
+
}
|
|
361
|
+
// Media messages score higher (AC03: preferred targets)
|
|
362
|
+
if (candidate.messageKind === 'media') {
|
|
363
|
+
score += 80;
|
|
364
|
+
reasons.push('is_media');
|
|
365
|
+
}
|
|
366
|
+
else if (candidate.messageKind === 'text') {
|
|
367
|
+
score += 20;
|
|
368
|
+
reasons.push('is_text');
|
|
369
|
+
}
|
|
370
|
+
// Same group preference
|
|
371
|
+
if (tapback.groupGuid && candidate.groupGuid === tapback.groupGuid) {
|
|
372
|
+
score += 10;
|
|
373
|
+
reasons.push('same_group');
|
|
374
|
+
}
|
|
375
|
+
if (score > 0) {
|
|
376
|
+
candidates.push({ message: candidate, score, reasons });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return candidates;
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=link-replies-and-tapbacks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-replies-and-tapbacks.js","sourceRoot":"","sources":["../../src/ingest/link-replies-and-tapbacks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AC02: Convert Apple epoch (seconds since 2001-01-01 UTC) to ISO 8601 UTC string
|
|
3
|
+
* @param appleEpochSeconds - Seconds since 2001-01-01 00:00:00 UTC (may have fractional part)
|
|
4
|
+
* @returns ISO 8601 UTC string with Z suffix (e.g., "2001-01-01T00:00:00.000Z")
|
|
5
|
+
*/
|
|
6
|
+
export declare function convertAppleEpochToUTC(appleEpochSeconds: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* AC01 & AC03: Normalize CSV UTC timestamp to ISO 8601 with Z suffix
|
|
9
|
+
* Preserves timezone information and ensures consistent format
|
|
10
|
+
* @param csvDate - CSV timestamp (typically already UTC with Z suffix)
|
|
11
|
+
* @returns Normalized ISO 8601 UTC string with Z suffix
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeCSVDate(csvDate: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* AC01: Validate date format is ISO 8601 UTC with Z suffix
|
|
16
|
+
* Strict validation rejects malformed dates and non-UTC timezones
|
|
17
|
+
* @param dateString - Date string to validate
|
|
18
|
+
* @returns Object with valid flag and optional error message
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateDateFormat(dateString: string): {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* AC04: Detect timezone drift between two timestamps
|
|
26
|
+
* Returns true if timestamps differ by more than a reasonable margin
|
|
27
|
+
* @param original - Original timestamp
|
|
28
|
+
* @param converted - Converted/processed timestamp
|
|
29
|
+
* @returns true if drift detected (timestamps differ significantly)
|
|
30
|
+
*/
|
|
31
|
+
export declare function detectTimezoneDrift(original: string, converted: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* AC04: End-to-end round-trip validation
|
|
34
|
+
* Validates that a date can be parsed, normalized, and validated without drift
|
|
35
|
+
* @param dateString - Input date string
|
|
36
|
+
* @returns Validation result with drift detection
|
|
37
|
+
*/
|
|
38
|
+
export declare function roundTripDateValidation(dateString: string): {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
driftDetected: boolean;
|
|
41
|
+
input: string;
|
|
42
|
+
normalized: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=date-converters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-converters.d.ts","sourceRoot":"","sources":["../../src/normalize/date-converters.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAOxE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWxD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG;IACvD,KAAK,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACd,CAoEA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GACf,OAAO,CAST;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,aAAa,EAAE,OAAO,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd,CAwDA"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/normalize/date-converters.ts
|
|
2
|
+
// Apple epoch and CSV UTC converters with end-to-end validation
|
|
3
|
+
// Spec §13: Dates & Timezones, Risks & Mitigations
|
|
4
|
+
/**
|
|
5
|
+
* Apple epoch constant: seconds since 2001-01-01 00:00:00 UTC
|
|
6
|
+
* Used by macOS for timestamps
|
|
7
|
+
*/
|
|
8
|
+
const APPLE_EPOCH_SECONDS = 978_307_200;
|
|
9
|
+
/**
|
|
10
|
+
* AC02: Convert Apple epoch (seconds since 2001-01-01 UTC) to ISO 8601 UTC string
|
|
11
|
+
* @param appleEpochSeconds - Seconds since 2001-01-01 00:00:00 UTC (may have fractional part)
|
|
12
|
+
* @returns ISO 8601 UTC string with Z suffix (e.g., "2001-01-01T00:00:00.000Z")
|
|
13
|
+
*/
|
|
14
|
+
export function convertAppleEpochToUTC(appleEpochSeconds) {
|
|
15
|
+
// Convert Apple epoch seconds to Unix epoch milliseconds
|
|
16
|
+
const unixMs = (appleEpochSeconds + APPLE_EPOCH_SECONDS) * 1000;
|
|
17
|
+
// Create date and convert to ISO 8601 UTC
|
|
18
|
+
const date = new Date(unixMs);
|
|
19
|
+
return date.toISOString();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* AC01 & AC03: Normalize CSV UTC timestamp to ISO 8601 with Z suffix
|
|
23
|
+
* Preserves timezone information and ensures consistent format
|
|
24
|
+
* @param csvDate - CSV timestamp (typically already UTC with Z suffix)
|
|
25
|
+
* @returns Normalized ISO 8601 UTC string with Z suffix
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeCSVDate(csvDate) {
|
|
28
|
+
// Parse the CSV date string
|
|
29
|
+
const date = new Date(csvDate);
|
|
30
|
+
// Check if parse was successful
|
|
31
|
+
if (Number.isNaN(date.getTime())) {
|
|
32
|
+
throw new Error(`Invalid CSV date format: ${csvDate}`);
|
|
33
|
+
}
|
|
34
|
+
// Return ISO 8601 UTC string with Z suffix
|
|
35
|
+
return date.toISOString();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* AC01: Validate date format is ISO 8601 UTC with Z suffix
|
|
39
|
+
* Strict validation rejects malformed dates and non-UTC timezones
|
|
40
|
+
* @param dateString - Date string to validate
|
|
41
|
+
* @returns Object with valid flag and optional error message
|
|
42
|
+
*/
|
|
43
|
+
export function validateDateFormat(dateString) {
|
|
44
|
+
// Check if empty or whitespace
|
|
45
|
+
if (!dateString || typeof dateString !== 'string' || !dateString.trim()) {
|
|
46
|
+
return { valid: false, error: 'Date string is empty or whitespace' };
|
|
47
|
+
}
|
|
48
|
+
// Check for Z suffix
|
|
49
|
+
if (!dateString.endsWith('Z')) {
|
|
50
|
+
return { valid: false, error: 'Date must end with Z suffix (UTC)' };
|
|
51
|
+
}
|
|
52
|
+
// Check for timezone offset (should not be present)
|
|
53
|
+
if (dateString.includes('+') || dateString.match(/-\d{2}:\d{2}$/)) {
|
|
54
|
+
return { valid: false, error: 'Date must be UTC only (no timezone offset)' };
|
|
55
|
+
}
|
|
56
|
+
// ISO 8601 requires T separator, not space
|
|
57
|
+
if (dateString.includes(' ')) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
error: 'Date must use T separator (ISO 8601), not space',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Strict ISO 8601 format check: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
64
|
+
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
65
|
+
if (!iso8601Regex.test(dateString)) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: 'Date must match ISO 8601 format (YYYY-MM-DDTHH:mm:ss[.sss]Z)',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Attempt to parse
|
|
72
|
+
const date = new Date(dateString);
|
|
73
|
+
if (Number.isNaN(date.getTime())) {
|
|
74
|
+
return { valid: false, error: 'Invalid ISO 8601 date format' };
|
|
75
|
+
}
|
|
76
|
+
// Validate the date is actually valid (e.g., Feb 29 in non-leap year)
|
|
77
|
+
// Re-stringify and compare to ensure components match
|
|
78
|
+
const isoString = date.toISOString();
|
|
79
|
+
// Extract components from original and reformed
|
|
80
|
+
const originalParts = dateString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
|
|
81
|
+
const reformedParts = isoString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
|
|
82
|
+
if (!originalParts || !reformedParts) {
|
|
83
|
+
return { valid: false, error: 'Failed to parse date components' };
|
|
84
|
+
}
|
|
85
|
+
// Check year, month, day, hour, minute, second match
|
|
86
|
+
// If they don't match, it means the date was invalid (like Feb 30)
|
|
87
|
+
for (let i = 1; i <= 6; i++) {
|
|
88
|
+
if (originalParts[i] !== reformedParts[i]) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Invalid date values: component mismatch at index ${i}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { valid: true };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* AC04: Detect timezone drift between two timestamps
|
|
99
|
+
* Returns true if timestamps differ by more than a reasonable margin
|
|
100
|
+
* @param original - Original timestamp
|
|
101
|
+
* @param converted - Converted/processed timestamp
|
|
102
|
+
* @returns true if drift detected (timestamps differ significantly)
|
|
103
|
+
*/
|
|
104
|
+
export function detectTimezoneDrift(original, converted) {
|
|
105
|
+
const origTime = new Date(original).getTime();
|
|
106
|
+
const convTime = new Date(converted).getTime();
|
|
107
|
+
// Allow 1 second tolerance for rounding/processing differences
|
|
108
|
+
const toleranceMs = 1000;
|
|
109
|
+
const diffMs = Math.abs(origTime - convTime);
|
|
110
|
+
return diffMs > toleranceMs;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* AC04: End-to-end round-trip validation
|
|
114
|
+
* Validates that a date can be parsed, normalized, and validated without drift
|
|
115
|
+
* @param dateString - Input date string
|
|
116
|
+
* @returns Validation result with drift detection
|
|
117
|
+
*/
|
|
118
|
+
export function roundTripDateValidation(dateString) {
|
|
119
|
+
try {
|
|
120
|
+
// Step 1: Validate original format
|
|
121
|
+
const formatValidation = validateDateFormat(dateString);
|
|
122
|
+
if (!formatValidation.valid) {
|
|
123
|
+
const result = {
|
|
124
|
+
valid: false,
|
|
125
|
+
driftDetected: false,
|
|
126
|
+
input: dateString,
|
|
127
|
+
normalized: '',
|
|
128
|
+
};
|
|
129
|
+
if (formatValidation.error) {
|
|
130
|
+
result.error = formatValidation.error;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
// Step 2: Normalize (re-parse to ensure consistent formatting)
|
|
135
|
+
const normalized = normalizeCSVDate(dateString);
|
|
136
|
+
// Step 3: Validate normalized format
|
|
137
|
+
const normalizedValidation = validateDateFormat(normalized);
|
|
138
|
+
if (!normalizedValidation.valid) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
driftDetected: false,
|
|
142
|
+
input: dateString,
|
|
143
|
+
normalized,
|
|
144
|
+
error: `Normalized date failed validation: ${normalizedValidation.error}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Step 4: Detect drift
|
|
148
|
+
const driftDetected = detectTimezoneDrift(dateString, normalized);
|
|
149
|
+
return {
|
|
150
|
+
valid: true,
|
|
151
|
+
driftDetected,
|
|
152
|
+
input: dateString,
|
|
153
|
+
normalized,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return {
|
|
158
|
+
valid: false,
|
|
159
|
+
driftDetected: false,
|
|
160
|
+
input: dateString,
|
|
161
|
+
normalized: '',
|
|
162
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=date-converters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-converters.js","sourceRoot":"","sources":["../../src/normalize/date-converters.ts"],"names":[],"mappings":"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"}
|