@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.
Files changed (216) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1535 -0
  4. package/dist/bin/index.js +5121 -0
  5. package/dist/cli/commands/clean.d.ts +17 -0
  6. package/dist/cli/commands/clean.d.ts.map +1 -0
  7. package/dist/cli/commands/clean.js +142 -0
  8. package/dist/cli/commands/clean.js.map +1 -0
  9. package/dist/cli/commands/doctor.d.ts +17 -0
  10. package/dist/cli/commands/doctor.d.ts.map +1 -0
  11. package/dist/cli/commands/doctor.js +202 -0
  12. package/dist/cli/commands/doctor.js.map +1 -0
  13. package/dist/cli/commands/enrich-ai.d.ts +17 -0
  14. package/dist/cli/commands/enrich-ai.d.ts.map +1 -0
  15. package/dist/cli/commands/enrich-ai.js +371 -0
  16. package/dist/cli/commands/enrich-ai.js.map +1 -0
  17. package/dist/cli/commands/index.d.ts +16 -0
  18. package/dist/cli/commands/index.d.ts.map +1 -0
  19. package/dist/cli/commands/index.js +16 -0
  20. package/dist/cli/commands/index.js.map +1 -0
  21. package/dist/cli/commands/ingest-csv.d.ts +17 -0
  22. package/dist/cli/commands/ingest-csv.d.ts.map +1 -0
  23. package/dist/cli/commands/ingest-csv.js +138 -0
  24. package/dist/cli/commands/ingest-csv.js.map +1 -0
  25. package/dist/cli/commands/ingest-db.d.ts +17 -0
  26. package/dist/cli/commands/ingest-db.d.ts.map +1 -0
  27. package/dist/cli/commands/ingest-db.js +159 -0
  28. package/dist/cli/commands/ingest-db.js.map +1 -0
  29. package/dist/cli/commands/init.d.ts +17 -0
  30. package/dist/cli/commands/init.d.ts.map +1 -0
  31. package/dist/cli/commands/init.js +110 -0
  32. package/dist/cli/commands/init.js.map +1 -0
  33. package/dist/cli/commands/normalize-link.d.ts +16 -0
  34. package/dist/cli/commands/normalize-link.d.ts.map +1 -0
  35. package/dist/cli/commands/normalize-link.js +144 -0
  36. package/dist/cli/commands/normalize-link.js.map +1 -0
  37. package/dist/cli/commands/render-markdown.d.ts +17 -0
  38. package/dist/cli/commands/render-markdown.d.ts.map +1 -0
  39. package/dist/cli/commands/render-markdown.js +218 -0
  40. package/dist/cli/commands/render-markdown.js.map +1 -0
  41. package/dist/cli/commands/stats.d.ts +17 -0
  42. package/dist/cli/commands/stats.d.ts.map +1 -0
  43. package/dist/cli/commands/stats.js +175 -0
  44. package/dist/cli/commands/stats.js.map +1 -0
  45. package/dist/cli/commands/validate.d.ts +17 -0
  46. package/dist/cli/commands/validate.d.ts.map +1 -0
  47. package/dist/cli/commands/validate.js +152 -0
  48. package/dist/cli/commands/validate.js.map +1 -0
  49. package/dist/cli/index.d.ts +13 -0
  50. package/dist/cli/index.d.ts.map +1 -0
  51. package/dist/cli/index.js +121 -0
  52. package/dist/cli/index.js.map +1 -0
  53. package/dist/cli/types.d.ts +93 -0
  54. package/dist/cli/types.d.ts.map +1 -0
  55. package/dist/cli/types.js +7 -0
  56. package/dist/cli/types.js.map +1 -0
  57. package/dist/cli/utils.d.ts +29 -0
  58. package/dist/cli/utils.d.ts.map +1 -0
  59. package/dist/cli/utils.js +53 -0
  60. package/dist/cli/utils.js.map +1 -0
  61. package/dist/cli.d.ts +9 -0
  62. package/dist/cli.d.ts.map +1 -0
  63. package/dist/cli.js +1805 -0
  64. package/dist/config/generator.d.ts +90 -0
  65. package/dist/config/generator.d.ts.map +1 -0
  66. package/dist/config/generator.js +320 -0
  67. package/dist/config/generator.js.map +1 -0
  68. package/dist/config/loader.d.ts +107 -0
  69. package/dist/config/loader.d.ts.map +1 -0
  70. package/dist/config/loader.js +251 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/schema.d.ts +107 -0
  73. package/dist/config/schema.d.ts.map +1 -0
  74. package/dist/config/schema.js +169 -0
  75. package/dist/config/schema.js.map +1 -0
  76. package/dist/enrich/audio-transcription.d.ts +77 -0
  77. package/dist/enrich/audio-transcription.d.ts.map +1 -0
  78. package/dist/enrich/audio-transcription.js +370 -0
  79. package/dist/enrich/audio-transcription.js.map +1 -0
  80. package/dist/enrich/checkpoint.d.ts +137 -0
  81. package/dist/enrich/checkpoint.d.ts.map +1 -0
  82. package/dist/enrich/checkpoint.js +205 -0
  83. package/dist/enrich/checkpoint.js.map +1 -0
  84. package/dist/enrich/idempotency.d.ts +90 -0
  85. package/dist/enrich/idempotency.d.ts.map +1 -0
  86. package/dist/enrich/idempotency.js +188 -0
  87. package/dist/enrich/idempotency.js.map +1 -0
  88. package/dist/enrich/image-analysis.d.ts +62 -0
  89. package/dist/enrich/image-analysis.d.ts.map +1 -0
  90. package/dist/enrich/image-analysis.js +264 -0
  91. package/dist/enrich/image-analysis.js.map +1 -0
  92. package/dist/enrich/index.d.ts +60 -0
  93. package/dist/enrich/index.d.ts.map +1 -0
  94. package/dist/enrich/index.js +74 -0
  95. package/dist/enrich/index.js.map +1 -0
  96. package/dist/enrich/link-enrichment.d.ts +37 -0
  97. package/dist/enrich/link-enrichment.d.ts.map +1 -0
  98. package/dist/enrich/link-enrichment.js +202 -0
  99. package/dist/enrich/link-enrichment.js.map +1 -0
  100. package/dist/enrich/pdf-video-handling.d.ts +49 -0
  101. package/dist/enrich/pdf-video-handling.d.ts.map +1 -0
  102. package/dist/enrich/pdf-video-handling.js +325 -0
  103. package/dist/enrich/pdf-video-handling.js.map +1 -0
  104. package/dist/enrich/progress-tracker.d.ts +120 -0
  105. package/dist/enrich/progress-tracker.d.ts.map +1 -0
  106. package/dist/enrich/progress-tracker.js +220 -0
  107. package/dist/enrich/progress-tracker.js.map +1 -0
  108. package/dist/enrich/providers/firecrawl.d.ts +18 -0
  109. package/dist/enrich/providers/firecrawl.d.ts.map +1 -0
  110. package/dist/enrich/providers/firecrawl.js +48 -0
  111. package/dist/enrich/providers/firecrawl.js.map +1 -0
  112. package/dist/enrich/providers/generic.d.ts +16 -0
  113. package/dist/enrich/providers/generic.d.ts.map +1 -0
  114. package/dist/enrich/providers/generic.js +36 -0
  115. package/dist/enrich/providers/generic.js.map +1 -0
  116. package/dist/enrich/providers/index.d.ts +14 -0
  117. package/dist/enrich/providers/index.d.ts.map +1 -0
  118. package/dist/enrich/providers/index.js +13 -0
  119. package/dist/enrich/providers/index.js.map +1 -0
  120. package/dist/enrich/providers/instagram.d.ts +16 -0
  121. package/dist/enrich/providers/instagram.d.ts.map +1 -0
  122. package/dist/enrich/providers/instagram.js +43 -0
  123. package/dist/enrich/providers/instagram.js.map +1 -0
  124. package/dist/enrich/providers/spotify.d.ts +16 -0
  125. package/dist/enrich/providers/spotify.d.ts.map +1 -0
  126. package/dist/enrich/providers/spotify.js +45 -0
  127. package/dist/enrich/providers/spotify.js.map +1 -0
  128. package/dist/enrich/providers/twitter.d.ts +16 -0
  129. package/dist/enrich/providers/twitter.d.ts.map +1 -0
  130. package/dist/enrich/providers/twitter.js +43 -0
  131. package/dist/enrich/providers/twitter.js.map +1 -0
  132. package/dist/enrich/providers/types.d.ts +47 -0
  133. package/dist/enrich/providers/types.d.ts.map +1 -0
  134. package/dist/enrich/providers/types.js +15 -0
  135. package/dist/enrich/providers/types.js.map +1 -0
  136. package/dist/enrich/providers/youtube.d.ts +16 -0
  137. package/dist/enrich/providers/youtube.d.ts.map +1 -0
  138. package/dist/enrich/providers/youtube.js +43 -0
  139. package/dist/enrich/providers/youtube.js.map +1 -0
  140. package/dist/enrich/rate-limiting.d.ts +118 -0
  141. package/dist/enrich/rate-limiting.d.ts.map +1 -0
  142. package/dist/enrich/rate-limiting.js +258 -0
  143. package/dist/enrich/rate-limiting.js.map +1 -0
  144. package/dist/index.d.ts +688 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +1729 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/ingest/dedup-merge.d.ts +82 -0
  149. package/dist/ingest/dedup-merge.d.ts.map +1 -0
  150. package/dist/ingest/dedup-merge.js +262 -0
  151. package/dist/ingest/dedup-merge.js.map +1 -0
  152. package/dist/ingest/ingest-csv.d.ts +62 -0
  153. package/dist/ingest/ingest-csv.d.ts.map +1 -0
  154. package/dist/ingest/ingest-csv.js +300 -0
  155. package/dist/ingest/ingest-csv.js.map +1 -0
  156. package/dist/ingest/ingest-db.d.ts +64 -0
  157. package/dist/ingest/ingest-db.d.ts.map +1 -0
  158. package/dist/ingest/ingest-db.js +172 -0
  159. package/dist/ingest/ingest-db.js.map +1 -0
  160. package/dist/ingest/link-replies-and-tapbacks.d.ts +53 -0
  161. package/dist/ingest/link-replies-and-tapbacks.d.ts.map +1 -0
  162. package/dist/ingest/link-replies-and-tapbacks.js +381 -0
  163. package/dist/ingest/link-replies-and-tapbacks.js.map +1 -0
  164. package/dist/normalize/date-converters.d.ts +45 -0
  165. package/dist/normalize/date-converters.d.ts.map +1 -0
  166. package/dist/normalize/date-converters.js +166 -0
  167. package/dist/normalize/date-converters.js.map +1 -0
  168. package/dist/normalize/path-validator.d.ts +65 -0
  169. package/dist/normalize/path-validator.d.ts.map +1 -0
  170. package/dist/normalize/path-validator.js +221 -0
  171. package/dist/normalize/path-validator.js.map +1 -0
  172. package/dist/normalize/validate-normalized.d.ts +45 -0
  173. package/dist/normalize/validate-normalized.d.ts.map +1 -0
  174. package/dist/normalize/validate-normalized.js +144 -0
  175. package/dist/normalize/validate-normalized.js.map +1 -0
  176. package/dist/render/embeds-blockquotes.d.ts +84 -0
  177. package/dist/render/embeds-blockquotes.d.ts.map +1 -0
  178. package/dist/render/embeds-blockquotes.js +204 -0
  179. package/dist/render/embeds-blockquotes.js.map +1 -0
  180. package/dist/render/grouping.d.ts +78 -0
  181. package/dist/render/grouping.d.ts.map +1 -0
  182. package/dist/render/grouping.js +134 -0
  183. package/dist/render/grouping.js.map +1 -0
  184. package/dist/render/index.d.ts +47 -0
  185. package/dist/render/index.d.ts.map +1 -0
  186. package/dist/render/index.js +245 -0
  187. package/dist/render/index.js.map +1 -0
  188. package/dist/render/reply-rendering.d.ts +88 -0
  189. package/dist/render/reply-rendering.d.ts.map +1 -0
  190. package/dist/render/reply-rendering.js +196 -0
  191. package/dist/render/reply-rendering.js.map +1 -0
  192. package/dist/schema/message.d.ts +125 -0
  193. package/dist/schema/message.d.ts.map +1 -0
  194. package/dist/schema/message.js +331 -0
  195. package/dist/schema/message.js.map +1 -0
  196. package/dist/utils/delta-detection.d.ts +107 -0
  197. package/dist/utils/delta-detection.d.ts.map +1 -0
  198. package/dist/utils/delta-detection.js +199 -0
  199. package/dist/utils/delta-detection.js.map +1 -0
  200. package/dist/utils/enrichment-merge.d.ts +135 -0
  201. package/dist/utils/enrichment-merge.d.ts.map +1 -0
  202. package/dist/utils/enrichment-merge.js +280 -0
  203. package/dist/utils/enrichment-merge.js.map +1 -0
  204. package/dist/utils/human.d.ts +15 -0
  205. package/dist/utils/human.d.ts.map +1 -0
  206. package/dist/utils/human.js +27 -0
  207. package/dist/utils/human.js.map +1 -0
  208. package/dist/utils/incremental-state.d.ts +133 -0
  209. package/dist/utils/incremental-state.d.ts.map +1 -0
  210. package/dist/utils/incremental-state.js +237 -0
  211. package/dist/utils/incremental-state.js.map +1 -0
  212. package/dist/utils/logger.d.ts +40 -0
  213. package/dist/utils/logger.d.ts.map +1 -0
  214. package/dist/utils/logger.js +176 -0
  215. package/dist/utils/logger.js.map +1 -0
  216. 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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"}