@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,126 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { computeStartOfDaysWithinInterval } from './computeStartOfDaysWithinInterval.js';
3
+ import { DateTime } from 'luxon';
4
+ describe('computeStartOfDaysWithinInterval', () => {
5
+ test('interval that does not transcend days', () => {
6
+ expect(computeStartOfDaysWithinInterval(DateTime.fromObject({
7
+ day: 1,
8
+ month: 4,
9
+ year: 2025,
10
+ hour: 12,
11
+ minute: 30,
12
+ }), DateTime.fromObject({
13
+ day: 1,
14
+ month: 4,
15
+ year: 2025,
16
+ hour: 15,
17
+ minute: 30,
18
+ }))).toEqual([]);
19
+ });
20
+ test('interval that starts from midnight', () => {
21
+ expect(computeStartOfDaysWithinInterval(DateTime.fromObject({
22
+ day: 1,
23
+ month: 4,
24
+ year: 2025,
25
+ hour: 0,
26
+ minute: 0,
27
+ }), DateTime.fromObject({
28
+ day: 1,
29
+ month: 4,
30
+ year: 2025,
31
+ hour: 15,
32
+ minute: 30,
33
+ }))).toEqual([
34
+ DateTime.fromObject({
35
+ day: 1,
36
+ month: 4,
37
+ year: 2025,
38
+ hour: 0,
39
+ minute: 0,
40
+ }),
41
+ ]);
42
+ });
43
+ test('interval that ends at midnight', () => {
44
+ expect(computeStartOfDaysWithinInterval(DateTime.fromObject({
45
+ day: 1,
46
+ month: 4,
47
+ year: 2025,
48
+ hour: 15,
49
+ minute: 30,
50
+ }), DateTime.fromObject({
51
+ day: 2,
52
+ month: 4,
53
+ year: 2025,
54
+ hour: 0,
55
+ minute: 0,
56
+ }))).toEqual([]);
57
+ });
58
+ test('interval that transcends 2 days', () => {
59
+ expect(computeStartOfDaysWithinInterval(DateTime.fromObject({
60
+ day: 1,
61
+ month: 4,
62
+ year: 2025,
63
+ hour: 18,
64
+ minute: 30,
65
+ }), DateTime.fromObject({
66
+ day: 2,
67
+ month: 4,
68
+ year: 2025,
69
+ hour: 18,
70
+ minute: 30,
71
+ }))).toEqual([
72
+ DateTime.fromObject({
73
+ day: 2,
74
+ month: 4,
75
+ year: 2025,
76
+ hour: 0,
77
+ minute: 0,
78
+ }),
79
+ ]);
80
+ });
81
+ test('interval that transcends more than 2 days', () => {
82
+ expect(computeStartOfDaysWithinInterval(DateTime.fromObject({
83
+ day: 1,
84
+ month: 4,
85
+ year: 2025,
86
+ hour: 18,
87
+ minute: 30,
88
+ }), DateTime.fromObject({
89
+ day: 5,
90
+ month: 4,
91
+ year: 2025,
92
+ hour: 4,
93
+ minute: 30,
94
+ }))).toEqual([
95
+ DateTime.fromObject({
96
+ day: 2,
97
+ month: 4,
98
+ year: 2025,
99
+ hour: 0,
100
+ minute: 0,
101
+ }),
102
+ DateTime.fromObject({
103
+ day: 3,
104
+ month: 4,
105
+ year: 2025,
106
+ hour: 0,
107
+ minute: 0,
108
+ }),
109
+ DateTime.fromObject({
110
+ day: 4,
111
+ month: 4,
112
+ year: 2025,
113
+ hour: 0,
114
+ minute: 0,
115
+ }),
116
+ DateTime.fromObject({
117
+ day: 5,
118
+ month: 4,
119
+ year: 2025,
120
+ hour: 0,
121
+ minute: 0,
122
+ }),
123
+ ]);
124
+ });
125
+ });
126
+ //# sourceMappingURL=computeStartOfDaysWithinInterval.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeStartOfDaysWithinInterval.test.js","sourceRoot":"/","sources":["helpers/computeStartOfDaysWithinInterval.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from 'vitest';\n\nimport { computeStartOfDaysWithinInterval } from './computeStartOfDaysWithinInterval.js';\nimport { DateTime } from 'luxon';\n\ndescribe('computeStartOfDaysWithinInterval', () => {\n test('interval that does not transcend days', () => {\n expect(\n computeStartOfDaysWithinInterval(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 12,\n minute: 30,\n }),\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 15,\n minute: 30,\n }),\n ),\n ).toEqual([] satisfies DateTime[]);\n });\n\n test('interval that starts from midnight', () => {\n expect(\n computeStartOfDaysWithinInterval(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 15,\n minute: 30,\n }),\n ),\n ).toEqual([\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n ] satisfies DateTime[]);\n });\n\n test('interval that ends at midnight', () => {\n expect(\n computeStartOfDaysWithinInterval(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 15,\n minute: 30,\n }),\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n ),\n ).toEqual([] satisfies DateTime[]);\n });\n\n test('interval that transcends 2 days', () => {\n expect(\n computeStartOfDaysWithinInterval(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 30,\n }),\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 30,\n }),\n ),\n ).toEqual([\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n ] satisfies DateTime[]);\n });\n\n test('interval that transcends more than 2 days', () => {\n expect(\n computeStartOfDaysWithinInterval(\n DateTime.fromObject({\n day: 1,\n month: 4,\n year: 2025,\n hour: 18,\n minute: 30,\n }),\n DateTime.fromObject({\n day: 5,\n month: 4,\n year: 2025,\n hour: 4,\n minute: 30,\n }),\n ),\n ).toEqual([\n DateTime.fromObject({\n day: 2,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 3,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 4,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n DateTime.fromObject({\n day: 5,\n month: 4,\n year: 2025,\n hour: 0,\n minute: 0,\n }),\n ] satisfies DateTime[]);\n });\n});\n"]}
@@ -0,0 +1,40 @@
1
+ type ResponsesUsageLike = {
2
+ input_tokens: number;
3
+ output_tokens: number;
4
+ total_tokens: number;
5
+ input_tokens_details?: {
6
+ cached_tokens?: number;
7
+ } | null;
8
+ } | null | undefined;
9
+ type ChatCompletionUsageLike = {
10
+ prompt_tokens: number;
11
+ completion_tokens: number;
12
+ total_tokens: number;
13
+ prompt_tokens_details?: {
14
+ cached_tokens?: number;
15
+ } | null;
16
+ } | null | undefined;
17
+ export type OpenAITokenUsage = {
18
+ inputTokens: number;
19
+ cachedInputTokens: number;
20
+ outputTokens: number;
21
+ totalTokens: number;
22
+ };
23
+ export type OpenAIModelPricing = {
24
+ inputUsdPer1MTokens: number;
25
+ cachedInputUsdPer1MTokens: number;
26
+ outputUsdPer1MTokens: number;
27
+ };
28
+ export declare const OPENAI_MODEL_PRICING: Record<string, OpenAIModelPricing>;
29
+ export declare function normalizeOpenAIResponsesUsage(usage: ResponsesUsageLike): OpenAITokenUsage | null;
30
+ export declare function normalizeOpenAIChatCompletionUsage(usage: ChatCompletionUsageLike): OpenAITokenUsage | null;
31
+ export declare function estimateOpenAICostFromUsage({ model, usage, pricingByModel, }: {
32
+ model: string;
33
+ usage: OpenAITokenUsage | null;
34
+ pricingByModel?: Record<string, OpenAIModelPricing>;
35
+ }): {
36
+ estimatedCostUsd: number;
37
+ usage: OpenAITokenUsage;
38
+ pricing: OpenAIModelPricing;
39
+ } | null;
40
+ export {};
@@ -0,0 +1,55 @@
1
+ export const OPENAI_MODEL_PRICING = {
2
+ 'gpt-5-mini': {
3
+ inputUsdPer1MTokens: 0.25,
4
+ cachedInputUsdPer1MTokens: 0.025,
5
+ outputUsdPer1MTokens: 2,
6
+ },
7
+ 'gpt-5-nano': {
8
+ inputUsdPer1MTokens: 0.05,
9
+ cachedInputUsdPer1MTokens: 0.005,
10
+ outputUsdPer1MTokens: 0.4,
11
+ },
12
+ };
13
+ export function normalizeOpenAIResponsesUsage(usage) {
14
+ if (usage == null) {
15
+ return null;
16
+ }
17
+ return {
18
+ inputTokens: usage.input_tokens,
19
+ cachedInputTokens: usage.input_tokens_details?.cached_tokens ?? 0,
20
+ outputTokens: usage.output_tokens,
21
+ totalTokens: usage.total_tokens,
22
+ };
23
+ }
24
+ export function normalizeOpenAIChatCompletionUsage(usage) {
25
+ if (usage == null) {
26
+ return null;
27
+ }
28
+ return {
29
+ inputTokens: usage.prompt_tokens,
30
+ cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
31
+ outputTokens: usage.completion_tokens,
32
+ totalTokens: usage.total_tokens,
33
+ };
34
+ }
35
+ export function estimateOpenAICostFromUsage({ model, usage, pricingByModel = OPENAI_MODEL_PRICING, }) {
36
+ if (usage == null) {
37
+ return null;
38
+ }
39
+ const pricing = pricingByModel[model];
40
+ if (pricing == null) {
41
+ return null;
42
+ }
43
+ const cachedInputTokens = Math.max(usage.cachedInputTokens, 0);
44
+ const uncachedInputTokens = Math.max(usage.inputTokens - cachedInputTokens, 0);
45
+ const outputTokens = Math.max(usage.outputTokens, 0);
46
+ const estimatedCostUsd = (uncachedInputTokens / 1_000_000) * pricing.inputUsdPer1MTokens +
47
+ (cachedInputTokens / 1_000_000) * pricing.cachedInputUsdPer1MTokens +
48
+ (outputTokens / 1_000_000) * pricing.outputUsdPer1MTokens;
49
+ return {
50
+ estimatedCostUsd,
51
+ usage,
52
+ pricing,
53
+ };
54
+ }
55
+ //# sourceMappingURL=estimateOpenAICost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimateOpenAICost.js","sourceRoot":"/","sources":["helpers/estimateOpenAICost.ts"],"names":[],"mappings":"AA+BA,MAAM,CAAC,MAAM,oBAAoB,GAAuC;IACtE,YAAY,EAAE;QACZ,mBAAmB,EAAE,IAAI;QACzB,yBAAyB,EAAE,KAAK;QAChC,oBAAoB,EAAE,CAAC;KACxB;IACD,YAAY,EAAE;QACZ,mBAAmB,EAAE,IAAI;QACzB,yBAAyB,EAAE,KAAK;QAChC,oBAAoB,EAAE,GAAG;KAC1B;CACF,CAAC;AAEF,MAAM,UAAU,6BAA6B,CAC3C,KAAyB;IAEzB,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,WAAW,EAAE,KAAK,CAAC,YAAY;QAC/B,iBAAiB,EAAE,KAAK,CAAC,oBAAoB,EAAE,aAAa,IAAI,CAAC;QACjE,YAAY,EAAE,KAAK,CAAC,aAAa;QACjC,WAAW,EAAE,KAAK,CAAC,YAAY;KAChC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,KAA8B;IAE9B,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,WAAW,EAAE,KAAK,CAAC,aAAa;QAChC,iBAAiB,EAAE,KAAK,CAAC,qBAAqB,EAAE,aAAa,IAAI,CAAC;QAClE,YAAY,EAAE,KAAK,CAAC,iBAAiB;QACrC,WAAW,EAAE,KAAK,CAAC,YAAY;KAChC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,EAC1C,KAAK,EACL,KAAK,EACL,cAAc,GAAG,oBAAoB,GAKtC;IACC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC;IAC/D,MAAM,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,GAAG,iBAAiB,EAAE,CAAC,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAErD,MAAM,gBAAgB,GACpB,CAAC,mBAAmB,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,mBAAmB;QAC/D,CAAC,iBAAiB,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,yBAAyB;QACnE,CAAC,YAAY,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAE5D,OAAO;QACL,gBAAgB;QAChB,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC","sourcesContent":["type ResponsesUsageLike = {\n input_tokens: number;\n output_tokens: number;\n total_tokens: number;\n input_tokens_details?: {\n cached_tokens?: number;\n } | null;\n} | null | undefined;\n\ntype ChatCompletionUsageLike = {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n prompt_tokens_details?: {\n cached_tokens?: number;\n } | null;\n} | null | undefined;\n\nexport type OpenAITokenUsage = {\n inputTokens: number;\n cachedInputTokens: number;\n outputTokens: number;\n totalTokens: number;\n};\n\nexport type OpenAIModelPricing = {\n inputUsdPer1MTokens: number;\n cachedInputUsdPer1MTokens: number;\n outputUsdPer1MTokens: number;\n};\n\nexport const OPENAI_MODEL_PRICING: Record<string, OpenAIModelPricing> = {\n 'gpt-5-mini': {\n inputUsdPer1MTokens: 0.25,\n cachedInputUsdPer1MTokens: 0.025,\n outputUsdPer1MTokens: 2,\n },\n 'gpt-5-nano': {\n inputUsdPer1MTokens: 0.05,\n cachedInputUsdPer1MTokens: 0.005,\n outputUsdPer1MTokens: 0.4,\n },\n};\n\nexport function normalizeOpenAIResponsesUsage(\n usage: ResponsesUsageLike,\n): OpenAITokenUsage | null {\n if (usage == null) {\n return null;\n }\n\n return {\n inputTokens: usage.input_tokens,\n cachedInputTokens: usage.input_tokens_details?.cached_tokens ?? 0,\n outputTokens: usage.output_tokens,\n totalTokens: usage.total_tokens,\n };\n}\n\nexport function normalizeOpenAIChatCompletionUsage(\n usage: ChatCompletionUsageLike,\n): OpenAITokenUsage | null {\n if (usage == null) {\n return null;\n }\n\n return {\n inputTokens: usage.prompt_tokens,\n cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,\n outputTokens: usage.completion_tokens,\n totalTokens: usage.total_tokens,\n };\n}\n\nexport function estimateOpenAICostFromUsage({\n model,\n usage,\n pricingByModel = OPENAI_MODEL_PRICING,\n}: {\n model: string;\n usage: OpenAITokenUsage | null;\n pricingByModel?: Record<string, OpenAIModelPricing>;\n}) {\n if (usage == null) {\n return null;\n }\n\n const pricing = pricingByModel[model];\n if (pricing == null) {\n return null;\n }\n\n const cachedInputTokens = Math.max(usage.cachedInputTokens, 0);\n const uncachedInputTokens = Math.max(usage.inputTokens - cachedInputTokens, 0);\n const outputTokens = Math.max(usage.outputTokens, 0);\n\n const estimatedCostUsd =\n (uncachedInputTokens / 1_000_000) * pricing.inputUsdPer1MTokens +\n (cachedInputTokens / 1_000_000) * pricing.cachedInputUsdPer1MTokens +\n (outputTokens / 1_000_000) * pricing.outputUsdPer1MTokens;\n\n return {\n estimatedCostUsd,\n usage,\n pricing,\n };\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { AffectedEntity } from '#schema/issue/entity.js';
2
+ /**
3
+ * Generate a stable key for an affected entity.
4
+ * @param target - The affected entity.
5
+ * @returns The key.
6
+ */
7
+ export declare function keyForAffectedEntity(affectedEntity: AffectedEntity): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generate a stable key for an affected entity.
3
+ * @param target - The affected entity.
4
+ * @returns The key.
5
+ */
6
+ export function keyForAffectedEntity(affectedEntity) {
7
+ switch (affectedEntity.type) {
8
+ case 'service':
9
+ return `service:${affectedEntity.serviceId}`;
10
+ case 'facility':
11
+ return `facility:${affectedEntity.stationId}:${affectedEntity.kind}`;
12
+ }
13
+ }
14
+ //# sourceMappingURL=keyForAffectedEntity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyForAffectedEntity.js","sourceRoot":"/","sources":["helpers/keyForAffectedEntity.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,cAA8B;IACjE,QAAQ,cAAc,CAAC,IAAI,EAAE,CAAC;QAC5B,KAAK,SAAS;YACZ,OAAO,WAAW,cAAc,CAAC,SAAS,EAAE,CAAC;QAC/C,KAAK,UAAU;YACb,OAAO,YAAY,cAAc,CAAC,SAAS,IAAI,cAAc,CAAC,IAAI,EAAE,CAAC;IACzE,CAAC;AACH,CAAC","sourcesContent":["import type { AffectedEntity } from '#schema/issue/entity.js';\n\n/**\n * Generate a stable key for an affected entity.\n * @param target - The affected entity.\n * @returns The key.\n */\nexport function keyForAffectedEntity(affectedEntity: AffectedEntity): string {\n switch (affectedEntity.type) {\n case 'service':\n return `service:${affectedEntity.serviceId}`;\n case 'facility':\n return `facility:${affectedEntity.stationId}:${affectedEntity.kind}`;\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { PeriodFixed, PeriodRecurring } from '#schema/issue/period.js';
2
+ /**
3
+ * Normalize a recurring period into a list of fixed periods.
4
+ * @param period - The recurring period to normalize.
5
+ * @returns The list of fixed periods.
6
+ */
7
+ export declare function normalizeRecurringPeriod(period: PeriodRecurring): PeriodFixed[];
@@ -0,0 +1,118 @@
1
+ import { DateTime } from 'luxon';
2
+ import { DateTime as DateTimeRust, Frequency, RRule, RRuleSet, Weekday, } from 'rrule-rust';
3
+ import { assert } from '#util/assert.js';
4
+ function toFrequency(frequency) {
5
+ switch (frequency) {
6
+ case 'daily':
7
+ return Frequency.Daily;
8
+ case 'weekly':
9
+ return Frequency.Weekly;
10
+ case 'monthly':
11
+ return Frequency.Monthly;
12
+ case 'yearly':
13
+ return Frequency.Yearly;
14
+ default:
15
+ throw new Error(`Invalid frequency: ${frequency}`);
16
+ }
17
+ }
18
+ function toDaysOfWeek(daysOfWeek) {
19
+ const result = [];
20
+ for (const day of daysOfWeek ?? []) {
21
+ switch (day) {
22
+ case 'MO':
23
+ result.push(Weekday.Monday);
24
+ break;
25
+ case 'TU':
26
+ result.push(Weekday.Tuesday);
27
+ break;
28
+ case 'WE':
29
+ result.push(Weekday.Wednesday);
30
+ break;
31
+ case 'TH':
32
+ result.push(Weekday.Thursday);
33
+ break;
34
+ case 'FR':
35
+ result.push(Weekday.Friday);
36
+ break;
37
+ case 'SA':
38
+ result.push(Weekday.Saturday);
39
+ break;
40
+ case 'SU':
41
+ result.push(Weekday.Sunday);
42
+ break;
43
+ default:
44
+ throw new Error(`Invalid day of week: ${day}`);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ function toByTimes(timeWindow) {
50
+ const startAtTime = DateTime.fromISO(timeWindow.startAt);
51
+ assert(startAtTime.isValid, `Invalid ISO datetime: ${timeWindow.startAt}`);
52
+ return {
53
+ byHour: [startAtTime.hour],
54
+ byMinute: [startAtTime.minute],
55
+ bySecond: [startAtTime.second],
56
+ };
57
+ }
58
+ /**
59
+ * Normalize a recurring period into a list of fixed periods.
60
+ * @param period - The recurring period to normalize.
61
+ * @returns The list of fixed periods.
62
+ */
63
+ export function normalizeRecurringPeriod(period) {
64
+ const fixedPeriods = [];
65
+ const startAt = DateTime.fromISO(period.startAt).setZone(period.timeZone, {
66
+ keepLocalTime: true,
67
+ });
68
+ assert(startAt.isValid, `Invalid ISO datetime: ${period.startAt}`);
69
+ const endAt = DateTime.fromISO(period.endAt).setZone(period.timeZone, {
70
+ keepLocalTime: true,
71
+ });
72
+ assert(endAt.isValid, `Invalid ISO datetime: ${period.endAt}`);
73
+ const byTimes = toByTimes(period.timeWindow);
74
+ const rruleSet = new RRuleSet({
75
+ dtstart: DateTimeRust.fromObject(startAt.toObject()),
76
+ tzid: period.timeZone,
77
+ rrules: [
78
+ new RRule({
79
+ until: DateTimeRust.fromObject(endAt.toObject()),
80
+ frequency: toFrequency(period.frequency),
81
+ interval: 1,
82
+ byWeekday: toDaysOfWeek(period.daysOfWeek),
83
+ byHour: byTimes.byHour,
84
+ byMinute: byTimes.byMinute,
85
+ bySecond: byTimes.bySecond,
86
+ }),
87
+ ],
88
+ exdates: period.excludedDates?.map((date) => {
89
+ const dateTime = DateTime.fromISO(date).setZone(period.timeZone, {
90
+ keepLocalTime: true,
91
+ });
92
+ assert(dateTime.isValid, `Invalid ISO datetime: ${date}`);
93
+ return DateTimeRust.fromObject(dateTime.toObject());
94
+ }),
95
+ });
96
+ const timeWindowEndAt = DateTime.fromISO(period.timeWindow.endAt);
97
+ assert(timeWindowEndAt.isValid, `Invalid ISO datetime: ${period.timeWindow.endAt}`);
98
+ for (const dt of rruleSet.all()) {
99
+ const dtStart = DateTime.fromObject(dt.toObject()).setZone(rruleSet.tzid, {
100
+ keepLocalTime: true,
101
+ });
102
+ assert(dtStart.isValid);
103
+ const dtEnd = DateTime.fromObject({
104
+ ...dt.toObject(),
105
+ hour: timeWindowEndAt.toObject().hour,
106
+ minute: timeWindowEndAt.toObject().minute,
107
+ second: timeWindowEndAt.toObject().second,
108
+ });
109
+ assert(dtEnd.isValid);
110
+ fixedPeriods.push({
111
+ kind: 'fixed',
112
+ startAt: dtStart.toISO(),
113
+ endAt: dtEnd.toISO(),
114
+ });
115
+ }
116
+ return fixedPeriods;
117
+ }
118
+ //# sourceMappingURL=normalizeRecurringPeriod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeRecurringPeriod.js","sourceRoot":"/","sources":["helpers/normalizeRecurringPeriod.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport {\n DateTime as DateTimeRust,\n Frequency,\n type NWeekday,\n RRule,\n RRuleSet,\n Weekday,\n} from 'rrule-rust';\nimport type {\n PeriodFixed,\n PeriodFrequency,\n PeriodRecurring,\n} from '#schema/issue/period.js';\nimport { assert } from '#util/assert.js';\n\nfunction toFrequency(frequency: PeriodFrequency): Frequency {\n switch (frequency) {\n case 'daily':\n return Frequency.Daily;\n case 'weekly':\n return Frequency.Weekly;\n case 'monthly':\n return Frequency.Monthly;\n case 'yearly':\n return Frequency.Yearly;\n default:\n throw new Error(`Invalid frequency: ${frequency}`);\n }\n}\n\nfunction toDaysOfWeek(\n daysOfWeek: PeriodRecurring['daysOfWeek'],\n): readonly (NWeekday | Weekday)[] {\n const result: (NWeekday | Weekday)[] = [];\n\n for (const day of daysOfWeek ?? []) {\n switch (day) {\n case 'MO':\n result.push(Weekday.Monday);\n break;\n case 'TU':\n result.push(Weekday.Tuesday);\n break;\n case 'WE':\n result.push(Weekday.Wednesday);\n break;\n case 'TH':\n result.push(Weekday.Thursday);\n break;\n case 'FR':\n result.push(Weekday.Friday);\n break;\n case 'SA':\n result.push(Weekday.Saturday);\n break;\n case 'SU':\n result.push(Weekday.Sunday);\n break;\n default:\n throw new Error(`Invalid day of week: ${day}`);\n }\n }\n\n return result;\n}\n\nfunction toByTimes(timeWindow: PeriodRecurring['timeWindow']): {\n byHour: number[];\n byMinute: number[];\n bySecond: number[];\n} {\n const startAtTime = DateTime.fromISO(timeWindow.startAt);\n assert(startAtTime.isValid, `Invalid ISO datetime: ${timeWindow.startAt}`);\n\n return {\n byHour: [startAtTime.hour],\n byMinute: [startAtTime.minute],\n bySecond: [startAtTime.second],\n };\n}\n\n/**\n * Normalize a recurring period into a list of fixed periods.\n * @param period - The recurring period to normalize.\n * @returns The list of fixed periods.\n */\nexport function normalizeRecurringPeriod(\n period: PeriodRecurring,\n): PeriodFixed[] {\n const fixedPeriods: PeriodFixed[] = [];\n\n const startAt = DateTime.fromISO(period.startAt).setZone(period.timeZone, {\n keepLocalTime: true,\n });\n assert(startAt.isValid, `Invalid ISO datetime: ${period.startAt}`);\n const endAt = DateTime.fromISO(period.endAt).setZone(period.timeZone, {\n keepLocalTime: true,\n });\n assert(endAt.isValid, `Invalid ISO datetime: ${period.endAt}`);\n\n const byTimes = toByTimes(period.timeWindow);\n\n const rruleSet = new RRuleSet({\n dtstart: DateTimeRust.fromObject(startAt.toObject()),\n tzid: period.timeZone,\n rrules: [\n new RRule({\n until: DateTimeRust.fromObject(endAt.toObject()),\n frequency: toFrequency(period.frequency),\n interval: 1,\n byWeekday: toDaysOfWeek(period.daysOfWeek),\n byHour: byTimes.byHour,\n byMinute: byTimes.byMinute,\n bySecond: byTimes.bySecond,\n }),\n ],\n exdates: period.excludedDates?.map((date) => {\n const dateTime = DateTime.fromISO(date).setZone(period.timeZone, {\n keepLocalTime: true,\n });\n assert(dateTime.isValid, `Invalid ISO datetime: ${date}`);\n return DateTimeRust.fromObject(dateTime.toObject());\n }),\n });\n\n const timeWindowEndAt = DateTime.fromISO(period.timeWindow.endAt);\n assert(\n timeWindowEndAt.isValid,\n `Invalid ISO datetime: ${period.timeWindow.endAt}`,\n );\n\n for (const dt of rruleSet.all()) {\n const dtStart = DateTime.fromObject(dt.toObject()).setZone(rruleSet.tzid, {\n keepLocalTime: true,\n });\n assert(dtStart.isValid);\n const dtEnd = DateTime.fromObject({\n ...dt.toObject(),\n hour: timeWindowEndAt.toObject().hour,\n minute: timeWindowEndAt.toObject().minute,\n second: timeWindowEndAt.toObject().second,\n });\n assert(dtEnd.isValid);\n fixedPeriods.push({\n kind: 'fixed',\n startAt: dtStart.toISO(),\n endAt: dtEnd.toISO(),\n });\n }\n\n return fixedPeriods;\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { normalizeRecurringPeriod } from './normalizeRecurringPeriod.js';
3
+ function makeRecurringPeriod(overrides = {}) {
4
+ return {
5
+ kind: 'recurring',
6
+ frequency: 'daily',
7
+ startAt: '2025-01-01T00:00:00+08:00',
8
+ endAt: '2025-01-03T23:59:59+08:00',
9
+ daysOfWeek: null,
10
+ timeWindow: {
11
+ startAt: '08:00:00',
12
+ endAt: '10:00:00',
13
+ },
14
+ timeZone: 'Asia/Singapore',
15
+ excludedDates: null,
16
+ ...overrides,
17
+ };
18
+ }
19
+ describe('normalizeRecurringPeriod', () => {
20
+ test('normalizes a bounded daily recurring period into fixed periods', () => {
21
+ const period = makeRecurringPeriod();
22
+ const fixed = normalizeRecurringPeriod(period);
23
+ expect(fixed).toHaveLength(3);
24
+ expect(fixed).toEqual([
25
+ {
26
+ kind: 'fixed',
27
+ startAt: '2025-01-01T08:00:00.000+08:00',
28
+ endAt: '2025-01-01T10:00:00.000+08:00',
29
+ },
30
+ {
31
+ kind: 'fixed',
32
+ startAt: '2025-01-02T08:00:00.000+08:00',
33
+ endAt: '2025-01-02T10:00:00.000+08:00',
34
+ },
35
+ {
36
+ kind: 'fixed',
37
+ startAt: '2025-01-03T08:00:00.000+08:00',
38
+ endAt: '2025-01-03T10:00:00.000+08:00',
39
+ },
40
+ ]);
41
+ });
42
+ test('filters recurrence by configured daysOfWeek', () => {
43
+ const period = makeRecurringPeriod({
44
+ startAt: '2025-01-01T00:00:00+08:00', // Wednesday
45
+ endAt: '2025-01-10T23:59:59+08:00',
46
+ frequency: 'weekly',
47
+ daysOfWeek: ['MO', 'WE', 'FR'],
48
+ });
49
+ const fixed = normalizeRecurringPeriod(period);
50
+ expect(fixed.map((item) => item.startAt)).toEqual([
51
+ '2025-01-01T08:00:00.000+08:00',
52
+ '2025-01-03T08:00:00.000+08:00',
53
+ '2025-01-06T08:00:00.000+08:00',
54
+ '2025-01-08T08:00:00.000+08:00',
55
+ '2025-01-10T08:00:00.000+08:00',
56
+ ]);
57
+ });
58
+ test('excludes specific dates from recurrence', () => {
59
+ const period = makeRecurringPeriod({
60
+ timeWindow: {
61
+ startAt: '00:00:00',
62
+ endAt: '01:00:00',
63
+ },
64
+ excludedDates: ['2025-01-02'],
65
+ });
66
+ const fixed = normalizeRecurringPeriod(period);
67
+ expect(fixed).toEqual([
68
+ {
69
+ kind: 'fixed',
70
+ startAt: '2025-01-01T00:00:00.000+08:00',
71
+ endAt: '2025-01-01T01:00:00.000+08:00',
72
+ },
73
+ {
74
+ kind: 'fixed',
75
+ startAt: '2025-01-03T00:00:00.000+08:00',
76
+ endAt: '2025-01-03T01:00:00.000+08:00',
77
+ },
78
+ ]);
79
+ });
80
+ test('returns start and end timestamps in Singapore timezone', () => {
81
+ const period = makeRecurringPeriod({
82
+ startAt: '2025-01-01T00:00:00Z',
83
+ endAt: '2025-01-02T23:59:59Z',
84
+ });
85
+ const fixed = normalizeRecurringPeriod(period);
86
+ expect(fixed).toHaveLength(2);
87
+ for (const item of fixed) {
88
+ expect(item.startAt.endsWith('+08:00')).toBe(true);
89
+ expect(item.endAt?.endsWith('+08:00')).toBe(true);
90
+ }
91
+ });
92
+ });
93
+ //# sourceMappingURL=normalizeRecurringPeriod.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizeRecurringPeriod.test.js","sourceRoot":"/","sources":["helpers/normalizeRecurringPeriod.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from 'vitest';\nimport type { PeriodRecurring } from '#schema/issue/period.js';\nimport { normalizeRecurringPeriod } from './normalizeRecurringPeriod.js';\n\nfunction makeRecurringPeriod(\n overrides: Partial<PeriodRecurring> = {},\n): PeriodRecurring {\n return {\n kind: 'recurring',\n frequency: 'daily',\n startAt: '2025-01-01T00:00:00+08:00',\n endAt: '2025-01-03T23:59:59+08:00',\n daysOfWeek: null,\n timeWindow: {\n startAt: '08:00:00',\n endAt: '10:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: null,\n ...overrides,\n };\n}\n\ndescribe('normalizeRecurringPeriod', () => {\n test('normalizes a bounded daily recurring period into fixed periods', () => {\n const period = makeRecurringPeriod();\n\n const fixed = normalizeRecurringPeriod(period);\n\n expect(fixed).toHaveLength(3);\n expect(fixed).toEqual([\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00.000+08:00',\n endAt: '2025-01-01T10:00:00.000+08:00',\n },\n {\n kind: 'fixed',\n startAt: '2025-01-02T08:00:00.000+08:00',\n endAt: '2025-01-02T10:00:00.000+08:00',\n },\n {\n kind: 'fixed',\n startAt: '2025-01-03T08:00:00.000+08:00',\n endAt: '2025-01-03T10:00:00.000+08:00',\n },\n ]);\n });\n\n test('filters recurrence by configured daysOfWeek', () => {\n const period = makeRecurringPeriod({\n startAt: '2025-01-01T00:00:00+08:00', // Wednesday\n endAt: '2025-01-10T23:59:59+08:00',\n frequency: 'weekly',\n daysOfWeek: ['MO', 'WE', 'FR'],\n });\n\n const fixed = normalizeRecurringPeriod(period);\n\n expect(fixed.map((item) => item.startAt)).toEqual([\n '2025-01-01T08:00:00.000+08:00',\n '2025-01-03T08:00:00.000+08:00',\n '2025-01-06T08:00:00.000+08:00',\n '2025-01-08T08:00:00.000+08:00',\n '2025-01-10T08:00:00.000+08:00',\n ]);\n });\n\n test('excludes specific dates from recurrence', () => {\n const period = makeRecurringPeriod({\n timeWindow: {\n startAt: '00:00:00',\n endAt: '01:00:00',\n },\n excludedDates: ['2025-01-02'],\n });\n\n const fixed = normalizeRecurringPeriod(period);\n\n expect(fixed).toEqual([\n {\n kind: 'fixed',\n startAt: '2025-01-01T00:00:00.000+08:00',\n endAt: '2025-01-01T01:00:00.000+08:00',\n },\n {\n kind: 'fixed',\n startAt: '2025-01-03T00:00:00.000+08:00',\n endAt: '2025-01-03T01:00:00.000+08:00',\n },\n ]);\n });\n\n test('returns start and end timestamps in Singapore timezone', () => {\n const period = makeRecurringPeriod({\n startAt: '2025-01-01T00:00:00Z',\n endAt: '2025-01-02T23:59:59Z',\n });\n\n const fixed = normalizeRecurringPeriod(period);\n\n expect(fixed).toHaveLength(2);\n for (const item of fixed) {\n expect(item.startAt.endsWith('+08:00')).toBe(true);\n expect(item.endAt?.endsWith('+08:00')).toBe(true);\n }\n });\n});\n"]}