@mrtdown/core 2.0.0-alpha.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 (182) hide show
  1. package/README.md +107 -0
  2. package/dist/constants.d.ts +10 -0
  3. package/dist/constants.js +11 -0
  4. package/dist/constants.js.map +1 -0
  5. package/dist/helpers/calculateDurationWithinServiceHours.d.ts +2 -0
  6. package/dist/helpers/calculateDurationWithinServiceHours.js +13 -0
  7. package/dist/helpers/calculateDurationWithinServiceHours.js.map +1 -0
  8. package/dist/helpers/calculateDurationWithinServiceHours.test.d.ts +1 -0
  9. package/dist/helpers/calculateDurationWithinServiceHours.test.js +83 -0
  10. package/dist/helpers/calculateDurationWithinServiceHours.test.js.map +1 -0
  11. package/dist/helpers/computeImpactFromEvidenceClaims.d.ts +21 -0
  12. package/dist/helpers/computeImpactFromEvidenceClaims.js +293 -0
  13. package/dist/helpers/computeImpactFromEvidenceClaims.js.map +1 -0
  14. package/dist/helpers/computeImpactFromEvidenceClaims.test.d.ts +1 -0
  15. package/dist/helpers/computeImpactFromEvidenceClaims.test.js +544 -0
  16. package/dist/helpers/computeImpactFromEvidenceClaims.test.js.map +1 -0
  17. package/dist/helpers/computeStartOfDaysWithinInterval.d.ts +2 -0
  18. package/dist/helpers/computeStartOfDaysWithinInterval.js +15 -0
  19. package/dist/helpers/computeStartOfDaysWithinInterval.js.map +1 -0
  20. package/dist/helpers/computeStartOfDaysWithinInterval.test.d.ts +1 -0
  21. package/dist/helpers/computeStartOfDaysWithinInterval.test.js +126 -0
  22. package/dist/helpers/computeStartOfDaysWithinInterval.test.js.map +1 -0
  23. package/dist/helpers/estimateOpenAICost.d.ts +40 -0
  24. package/dist/helpers/estimateOpenAICost.js +55 -0
  25. package/dist/helpers/estimateOpenAICost.js.map +1 -0
  26. package/dist/helpers/keyForAffectedEntity.d.ts +7 -0
  27. package/dist/helpers/keyForAffectedEntity.js +14 -0
  28. package/dist/helpers/keyForAffectedEntity.js.map +1 -0
  29. package/dist/helpers/normalizeRecurringPeriod.d.ts +7 -0
  30. package/dist/helpers/normalizeRecurringPeriod.js +118 -0
  31. package/dist/helpers/normalizeRecurringPeriod.js.map +1 -0
  32. package/dist/helpers/normalizeRecurringPeriod.test.d.ts +1 -0
  33. package/dist/helpers/normalizeRecurringPeriod.test.js +93 -0
  34. package/dist/helpers/normalizeRecurringPeriod.test.js.map +1 -0
  35. package/dist/helpers/resolvePeriods.d.ts +224 -0
  36. package/dist/helpers/resolvePeriods.js +207 -0
  37. package/dist/helpers/resolvePeriods.js.map +1 -0
  38. package/dist/helpers/resolvePeriods.test.d.ts +1 -0
  39. package/dist/helpers/resolvePeriods.test.js +239 -0
  40. package/dist/helpers/resolvePeriods.test.js.map +1 -0
  41. package/dist/helpers/splitIntervalByServiceHours.d.ts +2 -0
  42. package/dist/helpers/splitIntervalByServiceHours.js +30 -0
  43. package/dist/helpers/splitIntervalByServiceHours.js.map +1 -0
  44. package/dist/helpers/splitIntervalByServiceHours.test.d.ts +1 -0
  45. package/dist/helpers/splitIntervalByServiceHours.test.js +152 -0
  46. package/dist/helpers/splitIntervalByServiceHours.test.js.map +1 -0
  47. package/dist/helpers/sumIntervalDuration.d.ts +2 -0
  48. package/dist/helpers/sumIntervalDuration.js +9 -0
  49. package/dist/helpers/sumIntervalDuration.js.map +1 -0
  50. package/dist/index.d.ts +18 -0
  51. package/dist/index.js +19 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/repo/MRTDownRepository.d.ts +23 -0
  54. package/dist/repo/MRTDownRepository.js +28 -0
  55. package/dist/repo/MRTDownRepository.js.map +1 -0
  56. package/dist/repo/common/FileStore.d.ts +12 -0
  57. package/dist/repo/common/FileStore.js +27 -0
  58. package/dist/repo/common/FileStore.js.map +1 -0
  59. package/dist/repo/common/StandardRepository.d.ts +32 -0
  60. package/dist/repo/common/StandardRepository.js +58 -0
  61. package/dist/repo/common/StandardRepository.js.map +1 -0
  62. package/dist/repo/common/store.d.ts +29 -0
  63. package/dist/repo/common/store.js +2 -0
  64. package/dist/repo/common/store.js.map +1 -0
  65. package/dist/repo/issue/IssueRepository.d.ts +36 -0
  66. package/dist/repo/issue/IssueRepository.js +177 -0
  67. package/dist/repo/issue/IssueRepository.js.map +1 -0
  68. package/dist/repo/issue/helpers/deriveCurrentState.d.ts +51 -0
  69. package/dist/repo/issue/helpers/deriveCurrentState.js +113 -0
  70. package/dist/repo/issue/helpers/deriveCurrentState.js.map +1 -0
  71. package/dist/repo/issue/helpers/deriveCurrentState.test.d.ts +1 -0
  72. package/dist/repo/issue/helpers/deriveCurrentState.test.js +477 -0
  73. package/dist/repo/issue/helpers/deriveCurrentState.test.js.map +1 -0
  74. package/dist/repo/landmark/LandmarkRepository.d.ts +7 -0
  75. package/dist/repo/landmark/LandmarkRepository.js +12 -0
  76. package/dist/repo/landmark/LandmarkRepository.js.map +1 -0
  77. package/dist/repo/line/LineRepository.d.ts +13 -0
  78. package/dist/repo/line/LineRepository.js +32 -0
  79. package/dist/repo/line/LineRepository.js.map +1 -0
  80. package/dist/repo/operator/OperatorRepository.d.ts +7 -0
  81. package/dist/repo/operator/OperatorRepository.js +12 -0
  82. package/dist/repo/operator/OperatorRepository.js.map +1 -0
  83. package/dist/repo/service/ServiceRepository.d.ts +19 -0
  84. package/dist/repo/service/ServiceRepository.js +39 -0
  85. package/dist/repo/service/ServiceRepository.js.map +1 -0
  86. package/dist/repo/station/StationRepository.d.ts +13 -0
  87. package/dist/repo/station/StationRepository.js +30 -0
  88. package/dist/repo/station/StationRepository.js.map +1 -0
  89. package/dist/repo/town/TownRepository.d.ts +7 -0
  90. package/dist/repo/town/TownRepository.js +12 -0
  91. package/dist/repo/town/TownRepository.js.map +1 -0
  92. package/dist/schema/Landmark.d.ts +11 -0
  93. package/dist/schema/Landmark.js +7 -0
  94. package/dist/schema/Landmark.js.map +1 -0
  95. package/dist/schema/Line.d.ts +58 -0
  96. package/dist/schema/Line.js +35 -0
  97. package/dist/schema/Line.js.map +1 -0
  98. package/dist/schema/Operator.d.ts +16 -0
  99. package/dist/schema/Operator.js +12 -0
  100. package/dist/schema/Operator.js.map +1 -0
  101. package/dist/schema/Service.d.ts +55 -0
  102. package/dist/schema/Service.js +24 -0
  103. package/dist/schema/Service.js.map +1 -0
  104. package/dist/schema/Station.d.ts +29 -0
  105. package/dist/schema/Station.js +25 -0
  106. package/dist/schema/Station.js.map +1 -0
  107. package/dist/schema/Town.d.ts +11 -0
  108. package/dist/schema/Town.js +7 -0
  109. package/dist/schema/Town.js.map +1 -0
  110. package/dist/schema/common.d.ts +24 -0
  111. package/dist/schema/common.js +23 -0
  112. package/dist/schema/common.js.map +1 -0
  113. package/dist/schema/issue/bundle.d.ts +239 -0
  114. package/dist/schema/issue/bundle.js +11 -0
  115. package/dist/schema/issue/bundle.js.map +1 -0
  116. package/dist/schema/issue/cause.d.ts +51 -0
  117. package/dist/schema/issue/cause.js +30 -0
  118. package/dist/schema/issue/cause.js.map +1 -0
  119. package/dist/schema/issue/claim.d.ts +149 -0
  120. package/dist/schema/issue/claim.js +36 -0
  121. package/dist/schema/issue/claim.js.map +1 -0
  122. package/dist/schema/issue/entity.d.ts +176 -0
  123. package/dist/schema/issue/entity.js +35 -0
  124. package/dist/schema/issue/entity.js.map +1 -0
  125. package/dist/schema/issue/evidence.d.ts +124 -0
  126. package/dist/schema/issue/evidence.js +30 -0
  127. package/dist/schema/issue/evidence.js.map +1 -0
  128. package/dist/schema/issue/facilityEffect.d.ts +15 -0
  129. package/dist/schema/issue/facilityEffect.js +12 -0
  130. package/dist/schema/issue/facilityEffect.js.map +1 -0
  131. package/dist/schema/issue/id.d.ts +3 -0
  132. package/dist/schema/issue/id.js +6 -0
  133. package/dist/schema/issue/id.js.map +1 -0
  134. package/dist/schema/issue/impactEvent.d.ts +373 -0
  135. package/dist/schema/issue/impactEvent.js +43 -0
  136. package/dist/schema/issue/impactEvent.js.map +1 -0
  137. package/dist/schema/issue/issue.d.ts +19 -0
  138. package/dist/schema/issue/issue.js +13 -0
  139. package/dist/schema/issue/issue.js.map +1 -0
  140. package/dist/schema/issue/issueType.d.ts +7 -0
  141. package/dist/schema/issue/issueType.js +3 -0
  142. package/dist/schema/issue/issueType.js.map +1 -0
  143. package/dist/schema/issue/period.d.ts +72 -0
  144. package/dist/schema/issue/period.js +32 -0
  145. package/dist/schema/issue/period.js.map +1 -0
  146. package/dist/schema/issue/serviceEffect.d.ts +29 -0
  147. package/dist/schema/issue/serviceEffect.js +21 -0
  148. package/dist/schema/issue/serviceEffect.js.map +1 -0
  149. package/dist/schema/issue/serviceScope.d.ts +38 -0
  150. package/dist/schema/issue/serviceScope.js +30 -0
  151. package/dist/schema/issue/serviceScope.js.map +1 -0
  152. package/dist/util/assert.d.ts +1 -0
  153. package/dist/util/assert.js +6 -0
  154. package/dist/util/assert.js.map +1 -0
  155. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.d.ts +7 -0
  156. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js +24 -0
  157. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js.map +1 -0
  158. package/dist/util/ingestContent/index.d.ts +12 -0
  159. package/dist/util/ingestContent/index.js +171 -0
  160. package/dist/util/ingestContent/index.js.map +1 -0
  161. package/dist/util/ingestContent/types.d.ts +32 -0
  162. package/dist/util/ingestContent/types.js +2 -0
  163. package/dist/util/ingestContent/types.js.map +1 -0
  164. package/dist/write/MRTDownWriter.d.ts +27 -0
  165. package/dist/write/MRTDownWriter.js +27 -0
  166. package/dist/write/MRTDownWriter.js.map +1 -0
  167. package/dist/write/common/FileWriteStore.d.ts +13 -0
  168. package/dist/write/common/FileWriteStore.js +31 -0
  169. package/dist/write/common/FileWriteStore.js.map +1 -0
  170. package/dist/write/common/StandardWriter.d.ts +14 -0
  171. package/dist/write/common/StandardWriter.js +17 -0
  172. package/dist/write/common/StandardWriter.js.map +1 -0
  173. package/dist/write/common/store.d.ts +32 -0
  174. package/dist/write/common/store.js +2 -0
  175. package/dist/write/common/store.js.map +1 -0
  176. package/dist/write/id/IdGenerator.d.ts +18 -0
  177. package/dist/write/id/IdGenerator.js +23 -0
  178. package/dist/write/id/IdGenerator.js.map +1 -0
  179. package/dist/write/issue/IssueWriter.d.ts +12 -0
  180. package/dist/write/issue/IssueWriter.js +33 -0
  181. package/dist/write/issue/IssueWriter.js.map +1 -0
  182. package/package.json +80 -0
@@ -0,0 +1,544 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { IdGenerator } from '../write/id/IdGenerator.js';
3
+ import { computeImpactFromEvidenceClaims } from './computeImpactFromEvidenceClaims.js';
4
+ import { keyForAffectedEntity } from './keyForAffectedEntity.js';
5
+ vi.mock('../write/id/IdGenerator.js', () => ({
6
+ IdGenerator: {
7
+ impactEventId: vi.fn(),
8
+ evidenceId: vi.fn(),
9
+ },
10
+ }));
11
+ function createMockBundle(impactEvents) {
12
+ return {
13
+ issue: {
14
+ id: '2025-01-01-test-issue',
15
+ type: 'disruption',
16
+ title: {
17
+ 'en-SG': 'Test Issue',
18
+ 'zh-Hans': null,
19
+ ms: null,
20
+ ta: null,
21
+ },
22
+ titleMeta: {
23
+ source: 'test',
24
+ },
25
+ },
26
+ evidence: [],
27
+ impactEvents,
28
+ path: 'test/path',
29
+ };
30
+ }
31
+ function createServiceClaim(overrides = {}) {
32
+ return {
33
+ entity: { type: 'service', serviceId: 'NSL' },
34
+ effect: null,
35
+ scopes: { service: null },
36
+ statusSignal: null,
37
+ timeHints: null,
38
+ causes: null,
39
+ ...overrides,
40
+ };
41
+ }
42
+ function createFacilityClaim(overrides = {}) {
43
+ return {
44
+ entity: { type: 'facility', stationId: 'NS1', kind: 'lift' },
45
+ effect: null,
46
+ scopes: { service: null },
47
+ statusSignal: null,
48
+ timeHints: null,
49
+ causes: null,
50
+ ...overrides,
51
+ };
52
+ }
53
+ describe('computeImpactFromEvidenceClaims', () => {
54
+ beforeEach(() => {
55
+ let idCounter = 0;
56
+ vi.mocked(IdGenerator.impactEventId).mockImplementation(() => {
57
+ idCounter += 1;
58
+ return `ie_test_${String(idCounter).padStart(3, '0')}`;
59
+ });
60
+ });
61
+ test('returns empty result when no claims are provided', () => {
62
+ const result = computeImpactFromEvidenceClaims({
63
+ issueBundle: createMockBundle([]),
64
+ evidenceId: '2025-01-01T10:00:00+08:00',
65
+ evidenceTs: '2025-01-01T10:00:00+08:00',
66
+ claims: [],
67
+ });
68
+ expect(result).toEqual({
69
+ newState: {
70
+ services: {},
71
+ servicesProvenance: {},
72
+ facilities: {},
73
+ facilitiesProvenance: {},
74
+ impactEventIds: [],
75
+ },
76
+ newImpactEvents: [],
77
+ });
78
+ });
79
+ test('creates service effect and periods events for fixed time hints', () => {
80
+ const claim = createServiceClaim({
81
+ effect: {
82
+ service: { kind: 'delay', duration: null },
83
+ facility: null,
84
+ },
85
+ statusSignal: 'open',
86
+ timeHints: {
87
+ kind: 'fixed',
88
+ startAt: '2025-01-01T10:00:00+08:00',
89
+ endAt: '2025-01-01T12:00:00+08:00',
90
+ },
91
+ });
92
+ const evidenceId = '2025-01-01T10:05:00+08:00';
93
+ const result = computeImpactFromEvidenceClaims({
94
+ issueBundle: createMockBundle([]),
95
+ evidenceId,
96
+ evidenceTs: evidenceId,
97
+ claims: [claim],
98
+ });
99
+ const serviceKey = keyForAffectedEntity(claim.entity);
100
+ expect(result.newState.services[serviceKey]).toEqual({
101
+ effect: { kind: 'delay', duration: null },
102
+ scopes: [],
103
+ periods: [
104
+ {
105
+ kind: 'fixed',
106
+ startAt: '2025-01-01T10:00:00+08:00',
107
+ endAt: '2025-01-01T12:00:00+08:00',
108
+ },
109
+ ],
110
+ causes: [],
111
+ });
112
+ expect(result.newState.servicesProvenance[serviceKey]).toEqual({
113
+ effect: { evidenceId },
114
+ periods: { evidenceId },
115
+ });
116
+ expect(result.newImpactEvents).toEqual([
117
+ {
118
+ id: 'ie_test_001',
119
+ type: 'service_effects.set',
120
+ ts: evidenceId,
121
+ basis: { evidenceId },
122
+ entity: claim.entity,
123
+ effect: { kind: 'delay', duration: null },
124
+ },
125
+ {
126
+ id: 'ie_test_002',
127
+ type: 'periods.set',
128
+ ts: evidenceId,
129
+ basis: { evidenceId },
130
+ entity: claim.entity,
131
+ periods: [
132
+ {
133
+ kind: 'fixed',
134
+ startAt: '2025-01-01T10:00:00+08:00',
135
+ endAt: '2025-01-01T12:00:00+08:00',
136
+ },
137
+ ],
138
+ },
139
+ ]);
140
+ });
141
+ test('deduplicates claims by affected entity and keeps the last claim', () => {
142
+ const firstClaim = createServiceClaim({
143
+ effect: {
144
+ service: { kind: 'delay', duration: null },
145
+ facility: null,
146
+ },
147
+ });
148
+ const secondClaim = createServiceClaim({
149
+ effect: {
150
+ service: { kind: 'no-service' },
151
+ facility: null,
152
+ },
153
+ });
154
+ const result = computeImpactFromEvidenceClaims({
155
+ issueBundle: createMockBundle([]),
156
+ evidenceId: '2025-01-01T11:00:00+08:00',
157
+ evidenceTs: '2025-01-01T11:00:00+08:00',
158
+ claims: [firstClaim, secondClaim],
159
+ });
160
+ expect(result.newImpactEvents).toHaveLength(1);
161
+ expect(result.newImpactEvents[0]).toMatchObject({
162
+ id: 'ie_test_001',
163
+ type: 'service_effects.set',
164
+ effect: { kind: 'no-service' },
165
+ });
166
+ });
167
+ test('creates service scopes state, provenance, and event', () => {
168
+ const claim = createServiceClaim({
169
+ scopes: {
170
+ service: [
171
+ { type: 'service.whole' },
172
+ {
173
+ type: 'service.segment',
174
+ fromStationId: 'NS1',
175
+ toStationId: 'NS3',
176
+ },
177
+ ],
178
+ },
179
+ });
180
+ const evidenceId = '2025-01-02T09:00:00+08:00';
181
+ const result = computeImpactFromEvidenceClaims({
182
+ issueBundle: createMockBundle([]),
183
+ evidenceId,
184
+ evidenceTs: evidenceId,
185
+ claims: [claim],
186
+ });
187
+ const serviceKey = keyForAffectedEntity(claim.entity);
188
+ expect(result.newState.services[serviceKey]).toEqual({
189
+ effect: null,
190
+ scopes: [
191
+ { type: 'service.whole' },
192
+ {
193
+ type: 'service.segment',
194
+ fromStationId: 'NS1',
195
+ toStationId: 'NS3',
196
+ },
197
+ ],
198
+ periods: [],
199
+ causes: [],
200
+ });
201
+ expect(result.newState.servicesProvenance[serviceKey]).toEqual({
202
+ scopes: { evidenceId },
203
+ });
204
+ expect(result.newImpactEvents).toEqual([
205
+ {
206
+ id: 'ie_test_001',
207
+ type: 'service_scopes.set',
208
+ ts: evidenceId,
209
+ basis: { evidenceId },
210
+ entity: claim.entity,
211
+ serviceScopes: [
212
+ { type: 'service.whole' },
213
+ {
214
+ type: 'service.segment',
215
+ fromStationId: 'NS1',
216
+ toStationId: 'NS3',
217
+ },
218
+ ],
219
+ },
220
+ ]);
221
+ });
222
+ test('emits effects, periods, then scopes when all are present', () => {
223
+ const claim = createServiceClaim({
224
+ effect: {
225
+ service: { kind: 'reduced-service' },
226
+ facility: null,
227
+ },
228
+ timeHints: {
229
+ kind: 'fixed',
230
+ startAt: '2025-01-10T20:00:00+08:00',
231
+ endAt: '2025-01-10T22:00:00+08:00',
232
+ },
233
+ scopes: {
234
+ service: [{ type: 'service.point', stationId: 'NS5' }],
235
+ },
236
+ });
237
+ const evidenceId = '2025-01-10T20:05:00+08:00';
238
+ const result = computeImpactFromEvidenceClaims({
239
+ issueBundle: createMockBundle([]),
240
+ evidenceId,
241
+ evidenceTs: evidenceId,
242
+ claims: [claim],
243
+ });
244
+ expect(result.newImpactEvents.map((event) => event.type)).toEqual([
245
+ 'service_effects.set',
246
+ 'periods.set',
247
+ 'service_scopes.set',
248
+ ]);
249
+ });
250
+ test('emits causes events for service and facility claims', () => {
251
+ const serviceClaim = createServiceClaim({
252
+ causes: ['signal.fault', 'delay'],
253
+ });
254
+ const facilityClaim = createFacilityClaim({
255
+ causes: ['elevator.outage'],
256
+ });
257
+ const evidenceId = '2025-01-10T23:00:00+08:00';
258
+ const result = computeImpactFromEvidenceClaims({
259
+ issueBundle: createMockBundle([]),
260
+ evidenceId,
261
+ evidenceTs: evidenceId,
262
+ claims: [serviceClaim, facilityClaim],
263
+ });
264
+ const serviceKey = keyForAffectedEntity(serviceClaim.entity);
265
+ const facilityKey = keyForAffectedEntity(facilityClaim.entity);
266
+ expect(result.newState.services[serviceKey].causes).toEqual([
267
+ 'signal.fault',
268
+ 'delay',
269
+ ]);
270
+ expect(result.newState.servicesProvenance[serviceKey]).toEqual({
271
+ causes: { evidenceId },
272
+ });
273
+ expect(result.newState.facilities[facilityKey].causes).toEqual([
274
+ 'elevator.outage',
275
+ ]);
276
+ expect(result.newState.facilitiesProvenance[facilityKey]).toEqual({
277
+ causes: { evidenceId },
278
+ });
279
+ expect(result.newImpactEvents.map((event) => event.type)).toEqual([
280
+ 'causes.set',
281
+ 'causes.set',
282
+ ]);
283
+ });
284
+ test('does not emit events when effect, scopes, and causes are unchanged', () => {
285
+ const serviceEntity = { type: 'service', serviceId: 'NSL' };
286
+ const issueBundle = createMockBundle([
287
+ {
288
+ id: 'ie_seed_effect',
289
+ type: 'service_effects.set',
290
+ entity: serviceEntity,
291
+ ts: '2025-01-01T09:00:00+08:00',
292
+ basis: { evidenceId: 'seed_effect' },
293
+ effect: { kind: 'delay', duration: null },
294
+ },
295
+ {
296
+ id: 'ie_seed_scopes',
297
+ type: 'service_scopes.set',
298
+ entity: serviceEntity,
299
+ ts: '2025-01-01T09:01:00+08:00',
300
+ basis: { evidenceId: 'seed_scopes' },
301
+ serviceScopes: [{ type: 'service.point', stationId: 'NS4' }],
302
+ },
303
+ {
304
+ id: 'ie_seed_causes',
305
+ type: 'causes.set',
306
+ entity: serviceEntity,
307
+ ts: '2025-01-01T09:02:00+08:00',
308
+ basis: { evidenceId: 'seed_causes' },
309
+ causes: ['signal.fault'],
310
+ },
311
+ ]);
312
+ const claim = createServiceClaim({
313
+ entity: serviceEntity,
314
+ effect: {
315
+ service: { kind: 'delay', duration: null },
316
+ facility: null,
317
+ },
318
+ scopes: {
319
+ service: [{ type: 'service.point', stationId: 'NS4' }],
320
+ },
321
+ causes: ['signal.fault'],
322
+ });
323
+ const evidenceId = '2025-01-01T10:00:00+08:00';
324
+ const result = computeImpactFromEvidenceClaims({
325
+ issueBundle,
326
+ evidenceId,
327
+ evidenceTs: evidenceId,
328
+ claims: [claim],
329
+ });
330
+ const serviceKey = keyForAffectedEntity(serviceEntity);
331
+ expect(result.newState.servicesProvenance[serviceKey]).toEqual({
332
+ effect: { evidenceId: 'seed_effect' },
333
+ scopes: { evidenceId: 'seed_scopes' },
334
+ causes: { evidenceId: 'seed_causes' },
335
+ });
336
+ expect(result.newImpactEvents).toEqual([]);
337
+ });
338
+ test('deduplicates scope claims and keeps scopes from last claim', () => {
339
+ const firstClaim = createServiceClaim({
340
+ scopes: {
341
+ service: [{ type: 'service.whole' }],
342
+ },
343
+ });
344
+ const secondClaim = createServiceClaim({
345
+ scopes: {
346
+ service: [{ type: 'service.point', stationId: 'NS9' }],
347
+ },
348
+ });
349
+ const evidenceId = '2025-01-11T08:00:00+08:00';
350
+ const result = computeImpactFromEvidenceClaims({
351
+ issueBundle: createMockBundle([]),
352
+ evidenceId,
353
+ evidenceTs: evidenceId,
354
+ claims: [firstClaim, secondClaim],
355
+ });
356
+ const serviceKey = keyForAffectedEntity(secondClaim.entity);
357
+ expect(result.newState.services[serviceKey].scopes).toEqual([
358
+ { type: 'service.point', stationId: 'NS9' },
359
+ ]);
360
+ expect(result.newState.servicesProvenance[serviceKey]).toEqual({
361
+ scopes: { evidenceId },
362
+ });
363
+ expect(result.newImpactEvents).toEqual([
364
+ {
365
+ id: 'ie_test_001',
366
+ type: 'service_scopes.set',
367
+ ts: evidenceId,
368
+ basis: { evidenceId },
369
+ entity: secondClaim.entity,
370
+ serviceScopes: [{ type: 'service.point', stationId: 'NS9' }],
371
+ },
372
+ ]);
373
+ });
374
+ test('applies start-only hints to recurring periods and updates window start', () => {
375
+ const serviceEntity = { type: 'service', serviceId: 'NSL' };
376
+ const issueBundle = createMockBundle([
377
+ {
378
+ id: 'ie_seed_01',
379
+ type: 'periods.set',
380
+ entity: serviceEntity,
381
+ ts: '2025-01-01T08:00:00+08:00',
382
+ basis: { evidenceId: 'seed' },
383
+ periods: [
384
+ {
385
+ kind: 'recurring',
386
+ frequency: 'daily',
387
+ startAt: '2025-01-01T06:00:00+08:00',
388
+ endAt: '2025-01-07T06:00:00+08:00',
389
+ daysOfWeek: null,
390
+ timeWindow: {
391
+ startAt: '06:00:00',
392
+ endAt: '08:00:00',
393
+ },
394
+ timeZone: 'Asia/Singapore',
395
+ excludedDates: null,
396
+ },
397
+ ],
398
+ },
399
+ ]);
400
+ const claim = createServiceClaim({
401
+ entity: serviceEntity,
402
+ timeHints: {
403
+ kind: 'start-only',
404
+ startAt: '2025-01-02T11:30:00+08:00',
405
+ },
406
+ });
407
+ const evidenceId = '2025-01-02T11:35:00+08:00';
408
+ const result = computeImpactFromEvidenceClaims({
409
+ issueBundle,
410
+ evidenceId,
411
+ evidenceTs: evidenceId,
412
+ claims: [claim],
413
+ });
414
+ const serviceKey = keyForAffectedEntity(serviceEntity);
415
+ expect(result.newState.services[serviceKey].periods).toEqual([
416
+ {
417
+ kind: 'recurring',
418
+ frequency: 'daily',
419
+ startAt: '2025-01-02T11:30:00+08:00',
420
+ endAt: '2025-01-07T06:00:00+08:00',
421
+ daysOfWeek: null,
422
+ timeWindow: {
423
+ startAt: '11:30:00',
424
+ endAt: '08:00:00',
425
+ },
426
+ timeZone: 'Asia/Singapore',
427
+ excludedDates: null,
428
+ },
429
+ ]);
430
+ expect(result.newImpactEvents).toEqual([
431
+ {
432
+ id: 'ie_test_001',
433
+ type: 'periods.set',
434
+ ts: evidenceId,
435
+ basis: { evidenceId },
436
+ entity: serviceEntity,
437
+ periods: result.newState.services[serviceKey].periods,
438
+ },
439
+ ]);
440
+ });
441
+ test('applies end-only hints to facility recurring periods and updates window end', () => {
442
+ const facilityEntity = {
443
+ type: 'facility',
444
+ stationId: 'NS1',
445
+ kind: 'lift',
446
+ };
447
+ const issueBundle = createMockBundle([
448
+ {
449
+ id: 'ie_seed_02',
450
+ type: 'periods.set',
451
+ entity: facilityEntity,
452
+ ts: '2025-01-01T08:00:00+08:00',
453
+ basis: { evidenceId: 'seed' },
454
+ periods: [
455
+ {
456
+ kind: 'recurring',
457
+ frequency: 'weekly',
458
+ startAt: '2025-01-01T06:00:00+08:00',
459
+ endAt: '2025-02-01T06:00:00+08:00',
460
+ daysOfWeek: ['MO', 'WE'],
461
+ timeWindow: {
462
+ startAt: '06:00:00',
463
+ endAt: '09:00:00',
464
+ },
465
+ timeZone: 'Asia/Singapore',
466
+ excludedDates: ['2025-01-15'],
467
+ },
468
+ ],
469
+ },
470
+ ]);
471
+ const claim = createFacilityClaim({
472
+ entity: facilityEntity,
473
+ timeHints: {
474
+ kind: 'end-only',
475
+ endAt: '2025-01-25T23:15:00+08:00',
476
+ },
477
+ });
478
+ const evidenceId = '2025-01-03T12:00:00+08:00';
479
+ const result = computeImpactFromEvidenceClaims({
480
+ issueBundle,
481
+ evidenceId,
482
+ evidenceTs: evidenceId,
483
+ claims: [claim],
484
+ });
485
+ const facilityKey = keyForAffectedEntity(facilityEntity);
486
+ expect(result.newState.facilities[facilityKey].periods).toEqual([
487
+ {
488
+ kind: 'recurring',
489
+ frequency: 'weekly',
490
+ startAt: '2025-01-01T06:00:00+08:00',
491
+ endAt: '2025-01-25T23:15:00+08:00',
492
+ daysOfWeek: ['MO', 'WE'],
493
+ timeWindow: {
494
+ startAt: '06:00:00',
495
+ endAt: '23:15:00',
496
+ },
497
+ timeZone: 'Asia/Singapore',
498
+ excludedDates: ['2025-01-15'],
499
+ },
500
+ ]);
501
+ expect(result.newImpactEvents).toEqual([
502
+ {
503
+ id: 'ie_test_001',
504
+ type: 'periods.set',
505
+ ts: evidenceId,
506
+ basis: { evidenceId },
507
+ entity: facilityEntity,
508
+ periods: result.newState.facilities[facilityKey].periods,
509
+ },
510
+ ]);
511
+ });
512
+ test('start-only with no current periods creates an open fixed period', () => {
513
+ const claim = createServiceClaim({
514
+ timeHints: {
515
+ kind: 'start-only',
516
+ startAt: '2025-01-05T01:00:00+08:00',
517
+ },
518
+ });
519
+ const evidenceId = '2025-01-05T01:05:00+08:00';
520
+ const result = computeImpactFromEvidenceClaims({
521
+ issueBundle: createMockBundle([]),
522
+ evidenceId,
523
+ evidenceTs: evidenceId,
524
+ claims: [claim],
525
+ });
526
+ expect(result.newImpactEvents).toEqual([
527
+ {
528
+ id: 'ie_test_001',
529
+ type: 'periods.set',
530
+ ts: evidenceId,
531
+ basis: { evidenceId },
532
+ entity: claim.entity,
533
+ periods: [
534
+ {
535
+ kind: 'fixed',
536
+ startAt: '2025-01-05T01:00:00+08:00',
537
+ endAt: null,
538
+ },
539
+ ],
540
+ },
541
+ ]);
542
+ });
543
+ });
544
+ //# sourceMappingURL=computeImpactFromEvidenceClaims.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeImpactFromEvidenceClaims.test.js","sourceRoot":"/","sources":["helpers/computeImpactFromEvidenceClaims.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { beforeEach, describe, expect, test, vi } from 'vitest';\nimport type { IssueBundle } from '../schema/issue/bundle.js';\nimport type { Claim } from '../schema/issue/claim.js';\nimport type { ImpactEvent } from '../schema/issue/impactEvent.js';\nimport { IdGenerator } from '../write/id/IdGenerator.js';\nimport { computeImpactFromEvidenceClaims } from './computeImpactFromEvidenceClaims.js';\nimport { keyForAffectedEntity } from './keyForAffectedEntity.js';\n\nvi.mock('../write/id/IdGenerator.js', () => ({\n IdGenerator: {\n impactEventId: vi.fn(),\n evidenceId: vi.fn(),\n },\n}));\n\nfunction createMockBundle(impactEvents: ImpactEvent[]): IssueBundle {\n return {\n issue: {\n id: '2025-01-01-test-issue',\n type: 'disruption',\n title: {\n 'en-SG': 'Test Issue',\n 'zh-Hans': null,\n ms: null,\n ta: null,\n },\n titleMeta: {\n source: 'test',\n },\n },\n evidence: [],\n impactEvents,\n path: 'test/path',\n };\n}\n\nfunction createServiceClaim(overrides: Partial<Claim> = {}): Claim {\n return {\n entity: { type: 'service', serviceId: 'NSL' },\n effect: null,\n scopes: { service: null },\n statusSignal: null,\n timeHints: null,\n causes: null,\n ...overrides,\n };\n}\n\nfunction createFacilityClaim(overrides: Partial<Claim> = {}): Claim {\n return {\n entity: { type: 'facility', stationId: 'NS1', kind: 'lift' },\n effect: null,\n scopes: { service: null },\n statusSignal: null,\n timeHints: null,\n causes: null,\n ...overrides,\n };\n}\n\ndescribe('computeImpactFromEvidenceClaims', () => {\n beforeEach(() => {\n let idCounter = 0;\n vi.mocked(IdGenerator.impactEventId).mockImplementation(() => {\n idCounter += 1;\n return `ie_test_${String(idCounter).padStart(3, '0')}`;\n });\n });\n\n test('returns empty result when no claims are provided', () => {\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId: '2025-01-01T10:00:00+08:00',\n evidenceTs: '2025-01-01T10:00:00+08:00',\n claims: [],\n });\n\n expect(result).toEqual({\n newState: {\n services: {},\n servicesProvenance: {},\n facilities: {},\n facilitiesProvenance: {},\n impactEventIds: [],\n },\n newImpactEvents: [],\n });\n });\n\n test('creates service effect and periods events for fixed time hints', () => {\n const claim = createServiceClaim({\n effect: {\n service: { kind: 'delay', duration: null },\n facility: null,\n },\n statusSignal: 'open',\n timeHints: {\n kind: 'fixed',\n startAt: '2025-01-01T10:00:00+08:00',\n endAt: '2025-01-01T12:00:00+08:00',\n },\n });\n\n const evidenceId = '2025-01-01T10:05:00+08:00';\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n const serviceKey = keyForAffectedEntity(claim.entity);\n\n expect(result.newState.services[serviceKey]).toEqual({\n effect: { kind: 'delay', duration: null },\n scopes: [],\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T10:00:00+08:00',\n endAt: '2025-01-01T12:00:00+08:00',\n },\n ],\n causes: [],\n });\n expect(result.newState.servicesProvenance[serviceKey]).toEqual({\n effect: { evidenceId },\n periods: { evidenceId },\n });\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'service_effects.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: claim.entity,\n effect: { kind: 'delay', duration: null },\n },\n {\n id: 'ie_test_002',\n type: 'periods.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: claim.entity,\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T10:00:00+08:00',\n endAt: '2025-01-01T12:00:00+08:00',\n },\n ],\n },\n ]);\n });\n\n test('deduplicates claims by affected entity and keeps the last claim', () => {\n const firstClaim = createServiceClaim({\n effect: {\n service: { kind: 'delay', duration: null },\n facility: null,\n },\n });\n const secondClaim = createServiceClaim({\n effect: {\n service: { kind: 'no-service' },\n facility: null,\n },\n });\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId: '2025-01-01T11:00:00+08:00',\n evidenceTs: '2025-01-01T11:00:00+08:00',\n claims: [firstClaim, secondClaim],\n });\n\n expect(result.newImpactEvents).toHaveLength(1);\n expect(result.newImpactEvents[0]).toMatchObject({\n id: 'ie_test_001',\n type: 'service_effects.set',\n effect: { kind: 'no-service' },\n });\n });\n\n test('creates service scopes state, provenance, and event', () => {\n const claim = createServiceClaim({\n scopes: {\n service: [\n { type: 'service.whole' },\n {\n type: 'service.segment',\n fromStationId: 'NS1',\n toStationId: 'NS3',\n },\n ],\n },\n });\n const evidenceId = '2025-01-02T09:00:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n const serviceKey = keyForAffectedEntity(claim.entity);\n expect(result.newState.services[serviceKey]).toEqual({\n effect: null,\n scopes: [\n { type: 'service.whole' },\n {\n type: 'service.segment',\n fromStationId: 'NS1',\n toStationId: 'NS3',\n },\n ],\n periods: [],\n causes: [],\n });\n expect(result.newState.servicesProvenance[serviceKey]).toEqual({\n scopes: { evidenceId },\n });\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'service_scopes.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: claim.entity,\n serviceScopes: [\n { type: 'service.whole' },\n {\n type: 'service.segment',\n fromStationId: 'NS1',\n toStationId: 'NS3',\n },\n ],\n },\n ]);\n });\n\n test('emits effects, periods, then scopes when all are present', () => {\n const claim = createServiceClaim({\n effect: {\n service: { kind: 'reduced-service' },\n facility: null,\n },\n timeHints: {\n kind: 'fixed',\n startAt: '2025-01-10T20:00:00+08:00',\n endAt: '2025-01-10T22:00:00+08:00',\n },\n scopes: {\n service: [{ type: 'service.point', stationId: 'NS5' }],\n },\n });\n const evidenceId = '2025-01-10T20:05:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n expect(result.newImpactEvents.map((event) => event.type)).toEqual([\n 'service_effects.set',\n 'periods.set',\n 'service_scopes.set',\n ]);\n });\n\n test('emits causes events for service and facility claims', () => {\n const serviceClaim = createServiceClaim({\n causes: ['signal.fault', 'delay'],\n });\n const facilityClaim = createFacilityClaim({\n causes: ['elevator.outage'],\n });\n const evidenceId = '2025-01-10T23:00:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [serviceClaim, facilityClaim],\n });\n\n const serviceKey = keyForAffectedEntity(serviceClaim.entity);\n const facilityKey = keyForAffectedEntity(facilityClaim.entity);\n\n expect(result.newState.services[serviceKey].causes).toEqual([\n 'signal.fault',\n 'delay',\n ]);\n expect(result.newState.servicesProvenance[serviceKey]).toEqual({\n causes: { evidenceId },\n });\n expect(result.newState.facilities[facilityKey].causes).toEqual([\n 'elevator.outage',\n ]);\n expect(result.newState.facilitiesProvenance[facilityKey]).toEqual({\n causes: { evidenceId },\n });\n expect(result.newImpactEvents.map((event) => event.type)).toEqual([\n 'causes.set',\n 'causes.set',\n ]);\n });\n\n test('does not emit events when effect, scopes, and causes are unchanged', () => {\n const serviceEntity = { type: 'service' as const, serviceId: 'NSL' };\n const issueBundle = createMockBundle([\n {\n id: 'ie_seed_effect',\n type: 'service_effects.set',\n entity: serviceEntity,\n ts: '2025-01-01T09:00:00+08:00',\n basis: { evidenceId: 'seed_effect' },\n effect: { kind: 'delay', duration: null },\n },\n {\n id: 'ie_seed_scopes',\n type: 'service_scopes.set',\n entity: serviceEntity,\n ts: '2025-01-01T09:01:00+08:00',\n basis: { evidenceId: 'seed_scopes' },\n serviceScopes: [{ type: 'service.point', stationId: 'NS4' }],\n },\n {\n id: 'ie_seed_causes',\n type: 'causes.set',\n entity: serviceEntity,\n ts: '2025-01-01T09:02:00+08:00',\n basis: { evidenceId: 'seed_causes' },\n causes: ['signal.fault'],\n },\n ]);\n const claim = createServiceClaim({\n entity: serviceEntity,\n effect: {\n service: { kind: 'delay', duration: null },\n facility: null,\n },\n scopes: {\n service: [{ type: 'service.point', stationId: 'NS4' }],\n },\n causes: ['signal.fault'],\n });\n const evidenceId = '2025-01-01T10:00:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle,\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n const serviceKey = keyForAffectedEntity(serviceEntity);\n expect(result.newState.servicesProvenance[serviceKey]).toEqual({\n effect: { evidenceId: 'seed_effect' },\n scopes: { evidenceId: 'seed_scopes' },\n causes: { evidenceId: 'seed_causes' },\n });\n expect(result.newImpactEvents).toEqual([]);\n });\n\n test('deduplicates scope claims and keeps scopes from last claim', () => {\n const firstClaim = createServiceClaim({\n scopes: {\n service: [{ type: 'service.whole' }],\n },\n });\n const secondClaim = createServiceClaim({\n scopes: {\n service: [{ type: 'service.point', stationId: 'NS9' }],\n },\n });\n const evidenceId = '2025-01-11T08:00:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [firstClaim, secondClaim],\n });\n\n const serviceKey = keyForAffectedEntity(secondClaim.entity);\n expect(result.newState.services[serviceKey].scopes).toEqual([\n { type: 'service.point', stationId: 'NS9' },\n ]);\n expect(result.newState.servicesProvenance[serviceKey]).toEqual({\n scopes: { evidenceId },\n });\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'service_scopes.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: secondClaim.entity,\n serviceScopes: [{ type: 'service.point', stationId: 'NS9' }],\n },\n ]);\n });\n\n test('applies start-only hints to recurring periods and updates window start', () => {\n const serviceEntity = { type: 'service' as const, serviceId: 'NSL' };\n const issueBundle = createMockBundle([\n {\n id: 'ie_seed_01',\n type: 'periods.set',\n entity: serviceEntity,\n ts: '2025-01-01T08:00:00+08:00',\n basis: { evidenceId: 'seed' },\n periods: [\n {\n kind: 'recurring',\n frequency: 'daily',\n startAt: '2025-01-01T06:00:00+08:00',\n endAt: '2025-01-07T06:00:00+08:00',\n daysOfWeek: null,\n timeWindow: {\n startAt: '06:00:00',\n endAt: '08:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: null,\n },\n ],\n },\n ]);\n\n const claim = createServiceClaim({\n entity: serviceEntity,\n timeHints: {\n kind: 'start-only',\n startAt: '2025-01-02T11:30:00+08:00',\n },\n });\n const evidenceId = '2025-01-02T11:35:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle,\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n const serviceKey = keyForAffectedEntity(serviceEntity);\n expect(result.newState.services[serviceKey].periods).toEqual([\n {\n kind: 'recurring',\n frequency: 'daily',\n startAt: '2025-01-02T11:30:00+08:00',\n endAt: '2025-01-07T06:00:00+08:00',\n daysOfWeek: null,\n timeWindow: {\n startAt: '11:30:00',\n endAt: '08:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: null,\n },\n ]);\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'periods.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: serviceEntity,\n periods: result.newState.services[serviceKey].periods,\n },\n ]);\n });\n\n test('applies end-only hints to facility recurring periods and updates window end', () => {\n const facilityEntity = {\n type: 'facility' as const,\n stationId: 'NS1',\n kind: 'lift' as const,\n };\n const issueBundle = createMockBundle([\n {\n id: 'ie_seed_02',\n type: 'periods.set',\n entity: facilityEntity,\n ts: '2025-01-01T08:00:00+08:00',\n basis: { evidenceId: 'seed' },\n periods: [\n {\n kind: 'recurring',\n frequency: 'weekly',\n startAt: '2025-01-01T06:00:00+08:00',\n endAt: '2025-02-01T06:00:00+08:00',\n daysOfWeek: ['MO', 'WE'],\n timeWindow: {\n startAt: '06:00:00',\n endAt: '09:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: ['2025-01-15'],\n },\n ],\n },\n ]);\n\n const claim = createFacilityClaim({\n entity: facilityEntity,\n timeHints: {\n kind: 'end-only',\n endAt: '2025-01-25T23:15:00+08:00',\n },\n });\n const evidenceId = '2025-01-03T12:00:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle,\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n const facilityKey = keyForAffectedEntity(facilityEntity);\n expect(result.newState.facilities[facilityKey].periods).toEqual([\n {\n kind: 'recurring',\n frequency: 'weekly',\n startAt: '2025-01-01T06:00:00+08:00',\n endAt: '2025-01-25T23:15:00+08:00',\n daysOfWeek: ['MO', 'WE'],\n timeWindow: {\n startAt: '06:00:00',\n endAt: '23:15:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: ['2025-01-15'],\n },\n ]);\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'periods.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: facilityEntity,\n periods: result.newState.facilities[facilityKey].periods,\n },\n ]);\n });\n\n test('start-only with no current periods creates an open fixed period', () => {\n const claim = createServiceClaim({\n timeHints: {\n kind: 'start-only',\n startAt: '2025-01-05T01:00:00+08:00',\n },\n });\n const evidenceId = '2025-01-05T01:05:00+08:00';\n\n const result = computeImpactFromEvidenceClaims({\n issueBundle: createMockBundle([]),\n evidenceId,\n evidenceTs: evidenceId,\n claims: [claim],\n });\n\n expect(result.newImpactEvents).toEqual([\n {\n id: 'ie_test_001',\n type: 'periods.set',\n ts: evidenceId,\n basis: { evidenceId },\n entity: claim.entity,\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-05T01:00:00+08:00',\n endAt: null,\n },\n ],\n },\n ]);\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ import { type DateTime } from 'luxon';
2
+ export declare function computeStartOfDaysWithinInterval(start: DateTime, end: DateTime): DateTime[];
@@ -0,0 +1,15 @@
1
+ import { Interval } from 'luxon';
2
+ import { assert } from '../util/assert.js';
3
+ export function computeStartOfDaysWithinInterval(start, end) {
4
+ const interval = Interval.fromDateTimes(start.startOf('day'), end);
5
+ const segments = interval.splitBy({ days: 1 });
6
+ if (start.toMillis() !== start.startOf('day').toMillis()) {
7
+ segments.splice(0, 1);
8
+ }
9
+ const result = segments.map((segment) => {
10
+ assert(segment.start != null);
11
+ return segment.start;
12
+ });
13
+ return result;
14
+ }
15
+ //# sourceMappingURL=computeStartOfDaysWithinInterval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeStartOfDaysWithinInterval.js","sourceRoot":"/","sources":["helpers/computeStartOfDaysWithinInterval.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,QAAQ,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,UAAU,gCAAgC,CAC9C,KAAe,EACf,GAAa;IAEb,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAE/C,IAAI,KAAK,CAAC,QAAQ,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;QACzD,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxB,CAAC;IAED,MAAM,MAAM,GAAe,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAClD,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;QAC9B,OAAO,OAAO,CAAC,KAAK,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { type DateTime, Interval } from 'luxon';\nimport { assert } from '../util/assert.js';\n\nexport function computeStartOfDaysWithinInterval(\n start: DateTime,\n end: DateTime,\n): DateTime[] {\n const interval = Interval.fromDateTimes(start.startOf('day'), end);\n const segments = interval.splitBy({ days: 1 });\n\n if (start.toMillis() !== start.startOf('day').toMillis()) {\n segments.splice(0, 1);\n }\n\n const result: DateTime[] = segments.map((segment) => {\n assert(segment.start != null);\n return segment.start;\n });\n\n return result;\n}\n"]}