@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
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # mrtdown-data
2
+
3
+ A comprehensive data repository and API system that tracks Singapore's MRT (Mass Rapid Transit) service disruptions, maintenance, and infrastructure issues. Functions as a status monitoring system for Singapore's public transportation network.
4
+
5
+ ## Tech Stack
6
+
7
+ - **Backend**: Node.js + TypeScript, Hono API framework
8
+ - **Database**: DuckDB with normalized relational schema
9
+ - **Validation**: Zod schemas with OpenAPI integration
10
+ - **Testing**: Vitest
11
+ - **Linting**: Biome
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Install dependencies
17
+ npm install
18
+
19
+ # Build and generate database
20
+ npm run build
21
+
22
+ # Start development API server
23
+ npm run api:dev # Runs on port 4000
24
+
25
+ # Run tests
26
+ npm test
27
+
28
+ # Lint and format
29
+ npx biome check
30
+ ```
31
+
32
+ ## Development Commands
33
+
34
+ ### Database Operations
35
+ ```bash
36
+ npm run build # Compile TypeScript (auto-runs db:generate)
37
+ npm run db:generate # Generate DuckDB database from source data
38
+ ```
39
+
40
+ ### API Development
41
+ ```bash
42
+ npm run api:dev # Start dev server on port 4000
43
+ ```
44
+
45
+ ### Data Processing
46
+ ```bash
47
+ npm run ingest:webhook # Process incoming webhook data
48
+ ```
49
+
50
+ ### Testing & Quality
51
+ ```bash
52
+ npm test # Run Vitest tests
53
+ npx biome check # Lint and format code
54
+ ```
55
+
56
+ ### Database Queries
57
+ ```bash
58
+ # Query the database (use -readonly when API server is running)
59
+ duckdb -readonly -c "SELECT * FROM issues LIMIT 10" mrtdown.duckdb
60
+ ```
61
+
62
+ ## Architecture Overview
63
+
64
+ ### Core Data Models
65
+ - **Lines**: MRT/LRT lines (NSL, EWL, CCL, etc.) with service schedules
66
+ - **Issues**: Disruptions, maintenance, infrastructure problems with time intervals
67
+ - **Stations**: Station information with multi-language support
68
+ - **Time-aware**: All operations handle Singapore timezone (`Asia/Singapore`)
69
+
70
+ ### API Structure
71
+ Located in `/src/api/routes/`:
72
+ - **Overview**: System-wide status and line summaries
73
+ - **Lines**: Individual line profiles with detailed uptime metrics
74
+ - **Issues**: Issue details and historical data
75
+ - **Stations**: Station-specific information
76
+ - **Analytics**: Statistical analysis endpoints
77
+
78
+ All endpoints require Bearer token authentication except `/docs`.
79
+
80
+ ### Data Flow
81
+ 1. **Source data** (JSON files in `/data/source/`)
82
+ 2. **Database generation** (DuckDB with complex analytics)
83
+ 3. **API endpoints** (Real-time queries with multi-language support)
84
+
85
+ ## Key Features
86
+
87
+ - **Real-time Status Monitoring**: Track MRT line disruptions and maintenance
88
+ - **Historical Analytics**: Complex uptime calculations and service metrics
89
+ - **Multi-language Support**: Content available in 4 languages
90
+ - **Time-zone Aware**: All operations in Singapore timezone
91
+ - **Service Hours Logic**: Different schedules for weekdays/weekends/holidays
92
+ - **Webhook Integration**: Real-time data ingestion capabilities
93
+
94
+ ## Issue Data Structure
95
+
96
+ - **File naming**: `YYYY-MM-DD-descriptive-slug.json`
97
+ - **Types**: `disruption`, `maintenance`, `infra`
98
+ - **Time intervals**: Start/end timestamps with timezone awareness
99
+ - **Multi-language**: All titles have 4-language translations
100
+
101
+ ## Development Notes
102
+
103
+ - Database regeneration required when source data changes
104
+ - API responses include related entities for client efficiency
105
+ - Performance optimized for read-heavy analytical workloads
106
+ - Extensive use of CTEs for complex uptime calculations
107
+ - Proper handling of ongoing issues (end_at = NULL)
@@ -0,0 +1,10 @@
1
+ export declare const DIR_ISSUE = "issue";
2
+ export declare const DIR_LINE = "line";
3
+ export declare const DIR_LANDMARK = "landmark";
4
+ export declare const DIR_OPERATOR = "operator";
5
+ export declare const DIR_SERVICE = "service";
6
+ export declare const DIR_STATION = "station";
7
+ export declare const DIR_TOWN = "town";
8
+ export declare const FILE_ISSUE = "issue.json";
9
+ export declare const FILE_ISSUE_EVIDENCE = "evidence.ndjson";
10
+ export declare const FILE_ISSUE_IMPACT = "impact.ndjson";
@@ -0,0 +1,11 @@
1
+ export const DIR_ISSUE = 'issue';
2
+ export const DIR_LINE = 'line';
3
+ export const DIR_LANDMARK = 'landmark';
4
+ export const DIR_OPERATOR = 'operator';
5
+ export const DIR_SERVICE = 'service';
6
+ export const DIR_STATION = 'station';
7
+ export const DIR_TOWN = 'town';
8
+ export const FILE_ISSUE = 'issue.json';
9
+ export const FILE_ISSUE_EVIDENCE = 'evidence.ndjson';
10
+ export const FILE_ISSUE_IMPACT = 'impact.ndjson';
11
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"/","sources":["constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC;AACjC,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAC/B,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AACvC,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AACrC,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AACrC,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE/B,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACrD,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC","sourcesContent":["export const DIR_ISSUE = 'issue';\nexport const DIR_LINE = 'line';\nexport const DIR_LANDMARK = 'landmark';\nexport const DIR_OPERATOR = 'operator';\nexport const DIR_SERVICE = 'service';\nexport const DIR_STATION = 'station';\nexport const DIR_TOWN = 'town';\n\nexport const FILE_ISSUE = 'issue.json';\nexport const FILE_ISSUE_EVIDENCE = 'evidence.ndjson';\nexport const FILE_ISSUE_IMPACT = 'impact.ndjson';\n"]}
@@ -0,0 +1,2 @@
1
+ import { type DateTime, Duration } from 'luxon';
2
+ export declare function calculateDurationWithinServiceHours(start: DateTime, end: DateTime): Duration;
@@ -0,0 +1,13 @@
1
+ import { Duration, Interval } from 'luxon';
2
+ import { assert } from '../util/assert.js';
3
+ import { splitIntervalByServiceHours } from './splitIntervalByServiceHours.js';
4
+ export function calculateDurationWithinServiceHours(start, end) {
5
+ const interval = Interval.fromDateTimes(start, end);
6
+ assert(interval.isValid);
7
+ let result = Duration.fromObject({ seconds: 0 });
8
+ for (const segment of splitIntervalByServiceHours(interval)) {
9
+ result = result.plus(segment.toDuration());
10
+ }
11
+ return result;
12
+ }
13
+ //# sourceMappingURL=calculateDurationWithinServiceHours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculateDurationWithinServiceHours.js","sourceRoot":"/","sources":["helpers/calculateDurationWithinServiceHours.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,QAAQ,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAE/E,MAAM,UAAU,mCAAmC,CACjD,KAAe,EACf,GAAa;IAEb,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpD,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAEzB,IAAI,MAAM,GAAG,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;IAEjD,KAAK,MAAM,OAAO,IAAI,2BAA2B,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { type DateTime, Duration, Interval } from 'luxon';\nimport { assert } from '../util/assert.js';\nimport { splitIntervalByServiceHours } from './splitIntervalByServiceHours.js';\n\nexport function calculateDurationWithinServiceHours(\n start: DateTime,\n end: DateTime,\n): Duration {\n const interval = Interval.fromDateTimes(start, end);\n assert(interval.isValid);\n\n let result = Duration.fromObject({ seconds: 0 });\n\n for (const segment of splitIntervalByServiceHours(interval)) {\n result = result.plus(segment.toDuration());\n }\n\n return result;\n}\n"]}
@@ -0,0 +1,83 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { calculateDurationWithinServiceHours } from './calculateDurationWithinServiceHours.js';
3
+ import { DateTime } from 'luxon';
4
+ describe('calculateDurationWithinServiceHours', () => {
5
+ test('single day, within service hours', () => {
6
+ expect(calculateDurationWithinServiceHours(DateTime.fromObject({
7
+ day: 1,
8
+ month: 4,
9
+ year: 2025,
10
+ hour: 8,
11
+ minute: 0,
12
+ }), DateTime.fromObject({
13
+ day: 1,
14
+ month: 4,
15
+ year: 2025,
16
+ hour: 12,
17
+ minute: 30,
18
+ })).as('hours')).toEqual(4.5);
19
+ });
20
+ describe('transcends service hour boundaries', () => {
21
+ test('single day, starts before service hours', () => {
22
+ expect(calculateDurationWithinServiceHours(DateTime.fromObject({
23
+ day: 1,
24
+ month: 4,
25
+ year: 2025,
26
+ hour: 5,
27
+ minute: 0,
28
+ }), DateTime.fromObject({
29
+ day: 1,
30
+ month: 4,
31
+ year: 2025,
32
+ hour: 6,
33
+ minute: 0,
34
+ })).as('hours')).toEqual(0.5);
35
+ });
36
+ test('single day, ends after service hours', () => {
37
+ expect(calculateDurationWithinServiceHours(DateTime.fromObject({
38
+ day: 1,
39
+ month: 4,
40
+ year: 2025,
41
+ hour: 18,
42
+ minute: 0,
43
+ }), DateTime.fromObject({
44
+ day: 2,
45
+ month: 4,
46
+ year: 2025,
47
+ hour: 1,
48
+ minute: 0,
49
+ })).as('hours')).toEqual(6);
50
+ });
51
+ test('multiple days, transcends multiple service hours/non services hour ranges', () => {
52
+ expect(calculateDurationWithinServiceHours(DateTime.fromObject({
53
+ day: 1,
54
+ month: 4,
55
+ year: 2025,
56
+ hour: 18,
57
+ minute: 0,
58
+ }), DateTime.fromObject({
59
+ day: 2,
60
+ month: 4,
61
+ year: 2025,
62
+ hour: 18,
63
+ minute: 0,
64
+ })).as('hours')).toEqual(18.5);
65
+ });
66
+ });
67
+ test('outside service hours', () => {
68
+ expect(calculateDurationWithinServiceHours(DateTime.fromObject({
69
+ day: 1,
70
+ month: 4,
71
+ year: 2025,
72
+ hour: 3,
73
+ minute: 0,
74
+ }), DateTime.fromObject({
75
+ day: 1,
76
+ month: 4,
77
+ year: 2025,
78
+ hour: 4,
79
+ minute: 30,
80
+ })).as('hours')).toEqual(0);
81
+ });
82
+ });
83
+ //# sourceMappingURL=calculateDurationWithinServiceHours.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculateDurationWithinServiceHours.test.js","sourceRoot":"/","sources":["helpers/calculateDurationWithinServiceHours.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from 'vitest';\nimport { calculateDurationWithinServiceHours } from './calculateDurationWithinServiceHours.js';\nimport { DateTime } from 'luxon';\n\ndescribe('calculateDurationWithinServiceHours', () => {\n test('single day, within service hours', () => {\n expect(\n calculateDurationWithinServiceHours(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 8,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 12,\n minute: 30,\n }),\n ).as('hours'),\n ).toEqual(4.5);\n });\n describe('transcends service hour boundaries', () => {\n test('single day, starts before service hours', () => {\n expect(\n calculateDurationWithinServiceHours(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 5,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 6,\n minute: 0,\n }),\n ).as('hours'),\n ).toEqual(0.5);\n });\n test('single day, ends after service hours', () => {\n expect(\n calculateDurationWithinServiceHours(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 1,\n minute: 0,\n }),\n ).as('hours'),\n ).toEqual(6);\n });\n test('multiple days, transcends multiple service hours/non services hour ranges', () => {\n expect(\n calculateDurationWithinServiceHours(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 0,\n }),\n ).as('hours'),\n ).toEqual(18.5);\n });\n });\n test('outside service hours', () => {\n expect(\n calculateDurationWithinServiceHours(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 3,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 4,\n minute: 30,\n }),\n ).as('hours'),\n ).toEqual(0);\n });\n});\n"]}
@@ -0,0 +1,21 @@
1
+ import { type IssueBundleState } from '../repo/issue/helpers/deriveCurrentState.js';
2
+ import type { IssueBundle } from '../schema/issue/bundle.js';
3
+ import type { Claim } from '../schema/issue/claim.js';
4
+ import type { ImpactEvent } from '../schema/issue/impactEvent.js';
5
+ type Params = {
6
+ issueBundle: IssueBundle;
7
+ evidenceId: string;
8
+ evidenceTs: string;
9
+ claims: Claim[];
10
+ };
11
+ type Result = {
12
+ newState: IssueBundleState;
13
+ newImpactEvents: ImpactEvent[];
14
+ };
15
+ /**
16
+ * Compute the impact from the evidence claims.
17
+ * @param params - The parameters.
18
+ * @returns The result.
19
+ */
20
+ export declare function computeImpactFromEvidenceClaims(params: Params): Result;
21
+ export {};
@@ -0,0 +1,293 @@
1
+ import { deepStrictEqual } from 'node:assert';
2
+ import { DateTime } from 'luxon';
3
+ import { deriveCurrentState, } from '../repo/issue/helpers/deriveCurrentState.js';
4
+ import { assert } from '../util/assert.js';
5
+ import { IdGenerator } from '../write/id/IdGenerator.js';
6
+ import { keyForAffectedEntity } from './keyForAffectedEntity.js';
7
+ /**
8
+ * Compute the impact from the evidence claims.
9
+ * @param params - The parameters.
10
+ * @returns The result.
11
+ */
12
+ export function computeImpactFromEvidenceClaims(params) {
13
+ const result = {
14
+ newState: {
15
+ services: {},
16
+ servicesProvenance: {},
17
+ facilities: {},
18
+ facilitiesProvenance: {},
19
+ impactEventIds: [],
20
+ },
21
+ newImpactEvents: [],
22
+ };
23
+ const eventTs = params.evidenceTs;
24
+ const eventDateTime = DateTime.fromISO(eventTs);
25
+ assert(eventDateTime.isValid, `Invalid date: ${eventTs}`);
26
+ const currentState = deriveCurrentState(params.issueBundle);
27
+ const claimsKeyedByAffectedEntity = Object.fromEntries(params.claims.map((claim) => [keyForAffectedEntity(claim.entity), claim]));
28
+ for (const [key, claim] of Object.entries(claimsKeyedByAffectedEntity)) {
29
+ switch (claim.entity.type) {
30
+ case 'service': {
31
+ const currentServiceState = currentState.services[key] ?? {
32
+ effect: null,
33
+ scopes: [],
34
+ periods: [],
35
+ causes: [],
36
+ };
37
+ const currentServiceProvenance = currentState.servicesProvenance[key] ?? {};
38
+ if (claim.effect?.service != null &&
39
+ !isEqual(currentServiceState.effect, claim.effect.service)) {
40
+ currentServiceState.effect = claim.effect.service;
41
+ currentServiceProvenance.effect = {
42
+ evidenceId: params.evidenceId,
43
+ };
44
+ result.newImpactEvents.push({
45
+ id: IdGenerator.impactEventId(eventDateTime),
46
+ type: 'service_effects.set',
47
+ ts: eventTs,
48
+ basis: { evidenceId: params.evidenceId },
49
+ entity: claim.entity,
50
+ effect: claim.effect.service,
51
+ });
52
+ }
53
+ switch (params.issueBundle.issue.type) {
54
+ case 'disruption': {
55
+ const isCleared = currentServiceState.periods.every((period) => period.kind === 'fixed' && period.endAt != null);
56
+ let currentStatus = 'open';
57
+ if (isCleared) {
58
+ currentStatus = 'cleared';
59
+ }
60
+ if (currentStatus !== claim.statusSignal) {
61
+ switch (claim.statusSignal) {
62
+ case 'open': {
63
+ break;
64
+ }
65
+ case 'cleared': {
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ if (claim.timeHints != null) {
74
+ const { newPeriods, hasChanged } = reconcilePeriodsWithTimeHints(currentServiceState.periods, claim.timeHints);
75
+ if (hasChanged) {
76
+ currentServiceState.periods = newPeriods;
77
+ currentServiceProvenance.periods = {
78
+ evidenceId: params.evidenceId,
79
+ };
80
+ result.newImpactEvents.push({
81
+ id: IdGenerator.impactEventId(eventDateTime),
82
+ type: 'periods.set',
83
+ ts: eventTs,
84
+ basis: { evidenceId: params.evidenceId },
85
+ entity: claim.entity,
86
+ periods: currentServiceState.periods,
87
+ });
88
+ }
89
+ }
90
+ if (claim.scopes?.service != null &&
91
+ !isEqual(currentServiceState.scopes, claim.scopes.service)) {
92
+ currentServiceState.scopes = claim.scopes.service;
93
+ currentServiceProvenance.scopes = {
94
+ evidenceId: params.evidenceId,
95
+ };
96
+ result.newImpactEvents.push({
97
+ id: IdGenerator.impactEventId(eventDateTime),
98
+ type: 'service_scopes.set',
99
+ ts: eventTs,
100
+ basis: { evidenceId: params.evidenceId },
101
+ entity: claim.entity,
102
+ serviceScopes: claim.scopes.service,
103
+ });
104
+ }
105
+ if (claim.causes != null &&
106
+ !isEqual(currentServiceState.causes, claim.causes)) {
107
+ currentServiceState.causes = claim.causes;
108
+ currentServiceProvenance.causes = {
109
+ evidenceId: params.evidenceId,
110
+ };
111
+ result.newImpactEvents.push({
112
+ id: IdGenerator.impactEventId(eventDateTime),
113
+ type: 'causes.set',
114
+ ts: eventTs,
115
+ basis: { evidenceId: params.evidenceId },
116
+ entity: claim.entity,
117
+ causes: claim.causes,
118
+ });
119
+ }
120
+ result.newState.services[key] = currentServiceState;
121
+ result.newState.servicesProvenance[key] = currentServiceProvenance;
122
+ break;
123
+ }
124
+ case 'facility': {
125
+ const currentFacilityState = currentState.facilities[key] ?? {
126
+ effect: null,
127
+ periods: [],
128
+ };
129
+ const currentFacilityProvenance = currentState.facilitiesProvenance[key] ?? {};
130
+ if (claim.effect?.facility != null &&
131
+ !isEqual(currentFacilityState.effect, claim.effect.facility)) {
132
+ currentFacilityState.effect = claim.effect.facility;
133
+ currentFacilityProvenance.effect = {
134
+ evidenceId: params.evidenceId,
135
+ };
136
+ result.newImpactEvents.push({
137
+ id: IdGenerator.impactEventId(eventDateTime),
138
+ type: 'facility_effects.set',
139
+ ts: eventTs,
140
+ basis: { evidenceId: params.evidenceId },
141
+ entity: claim.entity,
142
+ effect: claim.effect.facility,
143
+ });
144
+ }
145
+ if (claim.timeHints != null) {
146
+ const { newPeriods, hasChanged } = reconcilePeriodsWithTimeHints(currentFacilityState.periods, claim.timeHints);
147
+ if (hasChanged) {
148
+ currentFacilityState.periods = newPeriods;
149
+ currentFacilityProvenance.periods = {
150
+ evidenceId: params.evidenceId,
151
+ };
152
+ result.newImpactEvents.push({
153
+ id: IdGenerator.impactEventId(eventDateTime),
154
+ type: 'periods.set',
155
+ ts: eventTs,
156
+ basis: { evidenceId: params.evidenceId },
157
+ entity: claim.entity,
158
+ periods: currentFacilityState.periods,
159
+ });
160
+ }
161
+ }
162
+ if (claim.causes != null &&
163
+ !isEqual(currentFacilityState.causes, claim.causes)) {
164
+ currentFacilityState.causes = claim.causes;
165
+ currentFacilityProvenance.causes = {
166
+ evidenceId: params.evidenceId,
167
+ };
168
+ result.newImpactEvents.push({
169
+ id: IdGenerator.impactEventId(eventDateTime),
170
+ type: 'causes.set',
171
+ ts: eventTs,
172
+ basis: { evidenceId: params.evidenceId },
173
+ entity: claim.entity,
174
+ causes: claim.causes,
175
+ });
176
+ }
177
+ result.newState.facilities[key] = currentFacilityState;
178
+ result.newState.facilitiesProvenance[key] = currentFacilityProvenance;
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+ /**
186
+ * Reconcile the periods with the time hints.
187
+ * @param currentPeriods - The current periods.
188
+ * @param timeHints - The time hints.
189
+ * @returns The new periods and whether the periods have changed.
190
+ */
191
+ function reconcilePeriodsWithTimeHints(currentPeriods, timeHints) {
192
+ switch (timeHints.kind) {
193
+ case 'fixed': {
194
+ return {
195
+ newPeriods: [timeHints],
196
+ hasChanged: true,
197
+ };
198
+ }
199
+ case 'recurring': {
200
+ return {
201
+ newPeriods: [timeHints],
202
+ hasChanged: true,
203
+ };
204
+ }
205
+ case 'start-only': {
206
+ let hasChanged = false;
207
+ const newPeriods = [];
208
+ if (currentPeriods.length === 0) {
209
+ newPeriods.push({
210
+ kind: 'fixed',
211
+ startAt: timeHints.startAt,
212
+ endAt: null,
213
+ });
214
+ hasChanged = true;
215
+ return { newPeriods, hasChanged };
216
+ }
217
+ for (const period of currentPeriods) {
218
+ switch (period.kind) {
219
+ case 'fixed': {
220
+ newPeriods.push({
221
+ ...period,
222
+ startAt: timeHints.startAt,
223
+ });
224
+ hasChanged = true;
225
+ break;
226
+ }
227
+ case 'recurring': {
228
+ const newPeriod = {
229
+ ...period,
230
+ startAt: timeHints.startAt,
231
+ };
232
+ if (newPeriod.timeWindow != null) {
233
+ const startAt = DateTime.fromISO(timeHints.startAt);
234
+ assert(startAt.isValid);
235
+ newPeriod.timeWindow.startAt = startAt.toFormat('HH:mm:ss');
236
+ }
237
+ newPeriods.push(newPeriod);
238
+ hasChanged = true;
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ return {
244
+ newPeriods,
245
+ hasChanged,
246
+ };
247
+ }
248
+ case 'end-only': {
249
+ let hasChanged = false;
250
+ const newPeriods = [];
251
+ for (const period of currentPeriods) {
252
+ switch (period.kind) {
253
+ case 'fixed': {
254
+ newPeriods.push({
255
+ ...period,
256
+ endAt: timeHints.endAt,
257
+ });
258
+ hasChanged = true;
259
+ break;
260
+ }
261
+ case 'recurring': {
262
+ const newPeriod = {
263
+ ...period,
264
+ endAt: timeHints.endAt,
265
+ };
266
+ if (newPeriod.timeWindow != null) {
267
+ const endAt = DateTime.fromISO(timeHints.endAt);
268
+ assert(endAt.isValid);
269
+ newPeriod.timeWindow.endAt = endAt.toFormat('HH:mm:ss');
270
+ }
271
+ newPeriods.push(newPeriod);
272
+ hasChanged = true;
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ return {
278
+ newPeriods,
279
+ hasChanged,
280
+ };
281
+ }
282
+ }
283
+ }
284
+ function isEqual(a, b) {
285
+ try {
286
+ deepStrictEqual(a, b);
287
+ return true;
288
+ }
289
+ catch (error) {
290
+ return false;
291
+ }
292
+ }
293
+ //# sourceMappingURL=computeImpactFromEvidenceClaims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeImpactFromEvidenceClaims.js","sourceRoot":"/","sources":["helpers/computeImpactFromEvidenceClaims.ts"],"names":[],"mappings":"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","sourcesContent":["import { deepStrictEqual } from 'node:assert';\nimport { DateTime } from 'luxon';\nimport {\n deriveCurrentState,\n type IssueBundleState,\n} from '../repo/issue/helpers/deriveCurrentState.js';\nimport type { IssueBundle } from '../schema/issue/bundle.js';\nimport type {\n Claim,\n ClaimStatusSignal,\n ClaimTimeHints,\n} from '../schema/issue/claim.js';\nimport type { ImpactEvent } from '../schema/issue/impactEvent.js';\nimport type { Period } from '../schema/issue/period.js';\nimport { assert } from '../util/assert.js';\nimport { IdGenerator } from '../write/id/IdGenerator.js';\nimport { keyForAffectedEntity } from './keyForAffectedEntity.js';\n\ntype Params = {\n issueBundle: IssueBundle;\n evidenceId: string;\n evidenceTs: string;\n claims: Claim[];\n};\n\ntype Result = {\n newState: IssueBundleState;\n newImpactEvents: ImpactEvent[];\n};\n\n/**\n * Compute the impact from the evidence claims.\n * @param params - The parameters.\n * @returns The result.\n */\nexport function computeImpactFromEvidenceClaims(params: Params): Result {\n const result: Result = {\n newState: {\n services: {},\n servicesProvenance: {},\n facilities: {},\n facilitiesProvenance: {},\n impactEventIds: [],\n },\n newImpactEvents: [],\n };\n\n const eventTs = params.evidenceTs;\n const eventDateTime = DateTime.fromISO(eventTs);\n assert(eventDateTime.isValid, `Invalid date: ${eventTs}`);\n\n const currentState = deriveCurrentState(params.issueBundle);\n\n const claimsKeyedByAffectedEntity: Record<string, Claim> = Object.fromEntries(\n params.claims.map((claim) => [keyForAffectedEntity(claim.entity), claim]),\n );\n\n for (const [key, claim] of Object.entries(claimsKeyedByAffectedEntity)) {\n switch (claim.entity.type) {\n case 'service': {\n const currentServiceState = currentState.services[key] ?? {\n effect: null,\n scopes: [],\n periods: [],\n causes: [],\n };\n const currentServiceProvenance =\n currentState.servicesProvenance[key] ?? {};\n\n if (\n claim.effect?.service != null &&\n !isEqual(currentServiceState.effect, claim.effect.service)\n ) {\n currentServiceState.effect = claim.effect.service;\n currentServiceProvenance.effect = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'service_effects.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n effect: claim.effect.service,\n });\n }\n\n switch (params.issueBundle.issue.type) {\n case 'disruption': {\n const isCleared = currentServiceState.periods.every(\n (period) => period.kind === 'fixed' && period.endAt != null,\n );\n\n let currentStatus: ClaimStatusSignal = 'open';\n\n if (isCleared) {\n currentStatus = 'cleared';\n }\n\n if (currentStatus !== claim.statusSignal) {\n switch (claim.statusSignal) {\n case 'open': {\n break;\n }\n case 'cleared': {\n break;\n }\n }\n }\n break;\n }\n }\n\n if (claim.timeHints != null) {\n const { newPeriods, hasChanged } = reconcilePeriodsWithTimeHints(\n currentServiceState.periods,\n claim.timeHints,\n );\n if (hasChanged) {\n currentServiceState.periods = newPeriods;\n currentServiceProvenance.periods = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'periods.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n periods: currentServiceState.periods,\n });\n }\n }\n\n if (\n claim.scopes?.service != null &&\n !isEqual(currentServiceState.scopes, claim.scopes.service)\n ) {\n currentServiceState.scopes = claim.scopes.service;\n currentServiceProvenance.scopes = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'service_scopes.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n serviceScopes: claim.scopes.service,\n });\n }\n\n if (\n claim.causes != null &&\n !isEqual(currentServiceState.causes, claim.causes)\n ) {\n currentServiceState.causes = claim.causes;\n currentServiceProvenance.causes = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'causes.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n causes: claim.causes,\n });\n }\n\n result.newState.services[key] = currentServiceState;\n result.newState.servicesProvenance[key] = currentServiceProvenance;\n\n break;\n }\n case 'facility': {\n const currentFacilityState = currentState.facilities[key] ?? {\n effect: null,\n periods: [],\n };\n const currentFacilityProvenance =\n currentState.facilitiesProvenance[key] ?? {};\n\n if (\n claim.effect?.facility != null &&\n !isEqual(currentFacilityState.effect, claim.effect.facility)\n ) {\n currentFacilityState.effect = claim.effect.facility;\n currentFacilityProvenance.effect = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'facility_effects.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n effect: claim.effect.facility,\n });\n }\n\n if (claim.timeHints != null) {\n const { newPeriods, hasChanged } = reconcilePeriodsWithTimeHints(\n currentFacilityState.periods,\n claim.timeHints,\n );\n if (hasChanged) {\n currentFacilityState.periods = newPeriods;\n currentFacilityProvenance.periods = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'periods.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n periods: currentFacilityState.periods,\n });\n }\n }\n\n if (\n claim.causes != null &&\n !isEqual(currentFacilityState.causes, claim.causes)\n ) {\n currentFacilityState.causes = claim.causes;\n currentFacilityProvenance.causes = {\n evidenceId: params.evidenceId,\n };\n result.newImpactEvents.push({\n id: IdGenerator.impactEventId(eventDateTime),\n type: 'causes.set',\n ts: eventTs,\n basis: { evidenceId: params.evidenceId },\n entity: claim.entity,\n causes: claim.causes,\n });\n }\n\n result.newState.facilities[key] = currentFacilityState;\n result.newState.facilitiesProvenance[key] = currentFacilityProvenance;\n\n break;\n }\n }\n }\n\n return result;\n}\n\ntype ReconcilePeriodsWithTimeHintsResults = {\n newPeriods: Period[];\n hasChanged: boolean;\n};\n\n/**\n * Reconcile the periods with the time hints.\n * @param currentPeriods - The current periods.\n * @param timeHints - The time hints.\n * @returns The new periods and whether the periods have changed.\n */\nfunction reconcilePeriodsWithTimeHints(\n currentPeriods: Period[],\n timeHints: ClaimTimeHints,\n): ReconcilePeriodsWithTimeHintsResults {\n switch (timeHints.kind) {\n case 'fixed': {\n return {\n newPeriods: [timeHints],\n hasChanged: true,\n };\n }\n case 'recurring': {\n return {\n newPeriods: [timeHints],\n hasChanged: true,\n };\n }\n case 'start-only': {\n let hasChanged = false;\n const newPeriods: Period[] = [];\n if (currentPeriods.length === 0) {\n newPeriods.push({\n kind: 'fixed',\n startAt: timeHints.startAt,\n endAt: null,\n });\n hasChanged = true;\n return { newPeriods, hasChanged };\n }\n for (const period of currentPeriods) {\n switch (period.kind) {\n case 'fixed': {\n newPeriods.push({\n ...period,\n startAt: timeHints.startAt,\n });\n hasChanged = true;\n break;\n }\n case 'recurring': {\n const newPeriod: Period = {\n ...period,\n startAt: timeHints.startAt,\n };\n if (newPeriod.timeWindow != null) {\n const startAt = DateTime.fromISO(timeHints.startAt);\n assert(startAt.isValid);\n newPeriod.timeWindow.startAt = startAt.toFormat('HH:mm:ss');\n }\n newPeriods.push(newPeriod);\n hasChanged = true;\n break;\n }\n }\n }\n return {\n newPeriods,\n hasChanged,\n };\n }\n case 'end-only': {\n let hasChanged = false;\n const newPeriods: Period[] = [];\n for (const period of currentPeriods) {\n switch (period.kind) {\n case 'fixed': {\n newPeriods.push({\n ...period,\n endAt: timeHints.endAt,\n });\n hasChanged = true;\n break;\n }\n case 'recurring': {\n const newPeriod: Period = {\n ...period,\n endAt: timeHints.endAt,\n };\n if (newPeriod.timeWindow != null) {\n const endAt = DateTime.fromISO(timeHints.endAt);\n assert(endAt.isValid);\n newPeriod.timeWindow.endAt = endAt.toFormat('HH:mm:ss');\n }\n newPeriods.push(newPeriod);\n hasChanged = true;\n break;\n }\n }\n }\n return {\n newPeriods,\n hasChanged,\n };\n }\n }\n}\n\nfunction isEqual(a: unknown, b: unknown): boolean {\n try {\n deepStrictEqual(a, b);\n return true;\n } catch (error) {\n return false;\n }\n}\n"]}