@mrtdown/core 2.0.0-alpha.3 → 2.0.0-alpha.5

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 (140) hide show
  1. package/dist/cli/commands/create.d.ts +30 -0
  2. package/dist/cli/commands/create.js +189 -0
  3. package/dist/cli/commands/create.js.map +1 -0
  4. package/dist/cli/commands/list.d.ts +6 -0
  5. package/dist/cli/commands/list.js +106 -0
  6. package/dist/cli/commands/list.js.map +1 -0
  7. package/dist/cli/commands/show.d.ts +6 -0
  8. package/dist/cli/commands/show.js +156 -0
  9. package/dist/cli/commands/show.js.map +1 -0
  10. package/dist/cli/commands/validate.d.ts +6 -0
  11. package/dist/cli/commands/validate.js +19 -0
  12. package/dist/cli/commands/validate.js.map +1 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +162 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/helpers/keyForAffectedEntity.d.ts +1 -1
  17. package/dist/helpers/keyForAffectedEntity.js.map +1 -1
  18. package/dist/helpers/normalizeRecurringPeriod.d.ts +1 -1
  19. package/dist/helpers/normalizeRecurringPeriod.js +1 -1
  20. package/dist/helpers/normalizeRecurringPeriod.js.map +1 -1
  21. package/dist/helpers/normalizeRecurringPeriod.test.js.map +1 -1
  22. package/dist/helpers/resolvePeriods.d.ts +1 -1
  23. package/dist/helpers/resolvePeriods.js +1 -1
  24. package/dist/helpers/resolvePeriods.js.map +1 -1
  25. package/dist/helpers/resolvePeriods.test.js.map +1 -1
  26. package/dist/index.d.ts +25 -25
  27. package/dist/index.js +25 -25
  28. package/dist/index.js.map +1 -1
  29. package/dist/llm/client.d.ts +2 -0
  30. package/dist/llm/client.js +5 -0
  31. package/dist/llm/client.js.map +1 -0
  32. package/dist/llm/common/MemoryStore.d.ts +21 -0
  33. package/dist/llm/common/MemoryStore.js +100 -0
  34. package/dist/llm/common/MemoryStore.js.map +1 -0
  35. package/dist/llm/common/MemoryStore.test.d.ts +1 -0
  36. package/dist/llm/common/MemoryStore.test.js +225 -0
  37. package/dist/llm/common/MemoryStore.test.js.map +1 -0
  38. package/dist/llm/common/formatCurrentState.d.ts +10 -0
  39. package/dist/llm/common/formatCurrentState.js +342 -0
  40. package/dist/llm/common/formatCurrentState.js.map +1 -0
  41. package/dist/llm/common/tool.d.ts +32 -0
  42. package/dist/llm/common/tool.js +6 -0
  43. package/dist/llm/common/tool.js.map +1 -0
  44. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.d.ts +1 -0
  45. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js +433 -0
  46. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js.map +1 -0
  47. package/dist/llm/functions/extractClaimsFromNewEvidence/index.d.ts +18 -0
  48. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js +153 -0
  49. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js.map +1 -0
  50. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.d.ts +1 -0
  51. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js +168 -0
  52. package/dist/llm/functions/extractClaimsFromNewEvidence/prompt.js.map +1 -0
  53. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.d.ts +19 -0
  54. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js +65 -0
  55. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js.map +1 -0
  56. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.d.ts +21 -0
  57. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js +115 -0
  58. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js.map +1 -0
  59. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.d.ts +24 -0
  60. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js +110 -0
  61. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js.map +1 -0
  62. package/dist/llm/functions/generateIssueTitleAndSlug/index.d.ts +14 -0
  63. package/dist/llm/functions/generateIssueTitleAndSlug/index.js +38 -0
  64. package/dist/llm/functions/generateIssueTitleAndSlug/index.js.map +1 -0
  65. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.d.ts +1 -0
  66. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js +23 -0
  67. package/dist/llm/functions/generateIssueTitleAndSlug/prompt.js.map +1 -0
  68. package/dist/llm/functions/translate/index.d.ts +1 -0
  69. package/dist/llm/functions/translate/index.js +59 -0
  70. package/dist/llm/functions/translate/index.js.map +1 -0
  71. package/dist/llm/functions/triageNewEvidence/eval.test.d.ts +1 -0
  72. package/dist/llm/functions/triageNewEvidence/eval.test.js +139 -0
  73. package/dist/llm/functions/triageNewEvidence/eval.test.js.map +1 -0
  74. package/dist/llm/functions/triageNewEvidence/index.d.ts +37 -0
  75. package/dist/llm/functions/triageNewEvidence/index.js +121 -0
  76. package/dist/llm/functions/triageNewEvidence/index.js.map +1 -0
  77. package/dist/llm/functions/triageNewEvidence/prompt.d.ts +1 -0
  78. package/dist/llm/functions/triageNewEvidence/prompt.js +60 -0
  79. package/dist/llm/functions/triageNewEvidence/prompt.js.map +1 -0
  80. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.d.ts +19 -0
  81. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js +65 -0
  82. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js.map +1 -0
  83. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.d.ts +19 -0
  84. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js +37 -0
  85. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js.map +1 -0
  86. package/dist/repo/issue/IssueRepository.d.ts +1 -1
  87. package/dist/repo/issue/IssueRepository.js +3 -3
  88. package/dist/repo/issue/IssueRepository.js.map +1 -1
  89. package/dist/repo/issue/helpers/deriveCurrentState.d.ts +6 -6
  90. package/dist/repo/issue/helpers/deriveCurrentState.js +1 -1
  91. package/dist/repo/issue/helpers/deriveCurrentState.js.map +1 -1
  92. package/dist/repo/issue/helpers/deriveCurrentState.test.js.map +1 -1
  93. package/dist/schema/issue/evidence.js +1 -1
  94. package/dist/schema/issue/evidence.js.map +1 -1
  95. package/dist/schema/issue/issue.js +1 -1
  96. package/dist/schema/issue/issue.js.map +1 -1
  97. package/dist/scripts/ingestViaWebhook.d.ts +1 -0
  98. package/dist/scripts/ingestViaWebhook.js +9 -0
  99. package/dist/scripts/ingestViaWebhook.js.map +1 -0
  100. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.d.ts +1 -1
  101. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js +1 -1
  102. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js.map +1 -1
  103. package/dist/util/ingestContent/index.js +9 -9
  104. package/dist/util/ingestContent/index.js.map +1 -1
  105. package/dist/validators/buildContext.d.ts +7 -0
  106. package/dist/validators/buildContext.js +164 -0
  107. package/dist/validators/buildContext.js.map +1 -0
  108. package/dist/validators/index.d.ts +17 -0
  109. package/dist/validators/index.js +58 -0
  110. package/dist/validators/index.js.map +1 -0
  111. package/dist/validators/issue.d.ts +13 -0
  112. package/dist/validators/issue.js +220 -0
  113. package/dist/validators/issue.js.map +1 -0
  114. package/dist/validators/landmark.d.ts +7 -0
  115. package/dist/validators/landmark.js +43 -0
  116. package/dist/validators/landmark.js.map +1 -0
  117. package/dist/validators/line.d.ts +8 -0
  118. package/dist/validators/line.js +87 -0
  119. package/dist/validators/line.js.map +1 -0
  120. package/dist/validators/operator.d.ts +7 -0
  121. package/dist/validators/operator.js +43 -0
  122. package/dist/validators/operator.js.map +1 -0
  123. package/dist/validators/service.d.ts +8 -0
  124. package/dist/validators/service.js +87 -0
  125. package/dist/validators/service.js.map +1 -0
  126. package/dist/validators/station.d.ts +8 -0
  127. package/dist/validators/station.js +93 -0
  128. package/dist/validators/station.js.map +1 -0
  129. package/dist/validators/town.d.ts +7 -0
  130. package/dist/validators/town.js +43 -0
  131. package/dist/validators/town.js.map +1 -0
  132. package/dist/validators/types.d.ts +19 -0
  133. package/dist/validators/types.js +2 -0
  134. package/dist/validators/types.js.map +1 -0
  135. package/dist/validators/utils.d.ts +2 -0
  136. package/dist/validators/utils.js +9 -0
  137. package/dist/validators/utils.js.map +1 -0
  138. package/dist/write/issue/IssueWriter.d.ts +3 -3
  139. package/dist/write/issue/IssueWriter.js.map +1 -1
  140. package/package.json +2 -7
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'node:path';
3
+ import { Command } from 'commander';
4
+ import { runCreateIssue, runCreateLandmark, runCreateLine, runCreateOperator, runCreateService, runCreateStation, runCreateTown, } from './commands/create.js';
5
+ import { runList } from './commands/list.js';
6
+ import { runShowIssue } from './commands/show.js';
7
+ import { runValidate } from './commands/validate.js';
8
+ const program = new Command();
9
+ program
10
+ .name('mrtdown-cli')
11
+ .description('CLI for mrtdown-data: create entities and validate data')
12
+ .option('-d, --data-dir <path>', 'Data directory', join(process.cwd(), 'data'));
13
+ program
14
+ .command('validate')
15
+ .description('Validate all data files against schemas')
16
+ .option('--scope <scope>', 'Only validate these entity types (repeatable): town, landmark, operator, station, line, service, issue', (val, prev) => (prev ?? []).concat(val))
17
+ .action((opts) => {
18
+ const dataDir = program.opts().dataDir;
19
+ const scope = opts.scope;
20
+ const code = runValidate({
21
+ dataDir,
22
+ scope: scope?.length
23
+ ? scope
24
+ : undefined,
25
+ });
26
+ process.exit(code);
27
+ });
28
+ program
29
+ .command('show')
30
+ .description('Display the current state of an issue')
31
+ .argument('<issue-id>', 'Issue ID (e.g. 2011-09-20-faulty-cable-led-to-circle-line-disruption)')
32
+ .option('--json', 'Output as JSON')
33
+ .action((issueId, opts) => {
34
+ const dataDir = program.opts().dataDir;
35
+ const code = runShowIssue({
36
+ dataDir,
37
+ issueId,
38
+ json: opts.json,
39
+ });
40
+ process.exit(code);
41
+ });
42
+ const list = program.command('list').description('List entities');
43
+ const listEntities = [
44
+ 'issue',
45
+ 'town',
46
+ 'landmark',
47
+ 'operator',
48
+ 'station',
49
+ 'line',
50
+ 'service',
51
+ ];
52
+ for (const entity of listEntities) {
53
+ list
54
+ .command(entity)
55
+ .description(`List ${entity}s`)
56
+ .option('--json', 'Output as JSON')
57
+ .action((opts) => {
58
+ const dataDir = program.opts().dataDir;
59
+ const code = runList({
60
+ dataDir,
61
+ entity,
62
+ json: opts.json,
63
+ });
64
+ process.exit(code);
65
+ });
66
+ }
67
+ const create = program.command('create').description('Create a new entity');
68
+ create
69
+ .command('issue')
70
+ .description('Create a new issue')
71
+ .requiredOption('--date <YYYY-MM-DD>', 'Issue date')
72
+ .requiredOption('--slug <slug>', 'URL-safe slug for the issue')
73
+ .requiredOption('--title <title>', 'English title')
74
+ .option('--type <type>', 'Issue type: disruption, maintenance, infra', 'disruption')
75
+ .option('--source <source>', 'Title source', 'cli')
76
+ .option('--dry-run', 'Print what would be created without writing')
77
+ .action(async (opts) => {
78
+ const dataDir = program.opts().dataDir;
79
+ const code = await runCreateIssue({ dataDir, dryRun: opts.dryRun }, {
80
+ date: opts.date,
81
+ slug: opts.slug,
82
+ title: opts.title,
83
+ type: opts.type,
84
+ source: opts.source,
85
+ });
86
+ process.exit(code);
87
+ });
88
+ create
89
+ .command('town')
90
+ .description('Create a new town')
91
+ .requiredOption('--id <id>', 'Town ID (e.g. yishun)')
92
+ .requiredOption('--name <name>', 'English name')
93
+ .option('--dry-run', 'Print what would be created without writing')
94
+ .action(async (opts) => {
95
+ const dataDir = program.opts().dataDir;
96
+ const code = await runCreateTown({ dataDir, dryRun: opts.dryRun }, { id: opts.id, name: opts.name });
97
+ process.exit(code);
98
+ });
99
+ create
100
+ .command('landmark')
101
+ .description('Create a new landmark')
102
+ .requiredOption('--id <id>', 'Landmark ID (e.g. northpoint-city)')
103
+ .requiredOption('--name <name>', 'English name')
104
+ .option('--dry-run', 'Print what would be created without writing')
105
+ .action(async (opts) => {
106
+ const dataDir = program.opts().dataDir;
107
+ const code = await runCreateLandmark({ dataDir, dryRun: opts.dryRun }, { id: opts.id, name: opts.name });
108
+ process.exit(code);
109
+ });
110
+ create
111
+ .command('operator')
112
+ .description('Create a new operator')
113
+ .requiredOption('--id <id>', 'Operator ID (e.g. SMRT_TRAINS)')
114
+ .requiredOption('--name <name>', 'English name')
115
+ .requiredOption('--founded-at <date>', 'Founded date (YYYY-MM-DD)')
116
+ .option('--url <url>', 'Operator website URL')
117
+ .option('--dry-run', 'Print what would be created without writing')
118
+ .action(async (opts) => {
119
+ const dataDir = program.opts().dataDir;
120
+ const code = await runCreateOperator({ dataDir, dryRun: opts.dryRun }, {
121
+ id: opts.id,
122
+ name: opts.name,
123
+ foundedAt: opts.foundedAt,
124
+ url: opts.url,
125
+ });
126
+ process.exit(code);
127
+ });
128
+ create
129
+ .command('station')
130
+ .description('Create a station from JSON (--stdin or --file)')
131
+ .option('--stdin', 'Read JSON from stdin')
132
+ .option('--file <path>', 'Read JSON from file')
133
+ .option('--dry-run', 'Print what would be created without writing')
134
+ .action(async (opts) => {
135
+ const dataDir = program.opts().dataDir;
136
+ const code = await runCreateStation({ dataDir, dryRun: opts.dryRun, stdin: opts.stdin }, { file: opts.file });
137
+ process.exit(code);
138
+ });
139
+ create
140
+ .command('line')
141
+ .description('Create a line from JSON (--stdin or --file)')
142
+ .option('--stdin', 'Read JSON from stdin')
143
+ .option('--file <path>', 'Read JSON from file')
144
+ .option('--dry-run', 'Print what would be created without writing')
145
+ .action(async (opts) => {
146
+ const dataDir = program.opts().dataDir;
147
+ const code = await runCreateLine({ dataDir, dryRun: opts.dryRun, stdin: opts.stdin }, { file: opts.file });
148
+ process.exit(code);
149
+ });
150
+ create
151
+ .command('service')
152
+ .description('Create a service from JSON (--stdin or --file)')
153
+ .option('--stdin', 'Read JSON from stdin')
154
+ .option('--file <path>', 'Read JSON from file')
155
+ .option('--dry-run', 'Print what would be created without writing')
156
+ .action(async (opts) => {
157
+ const dataDir = program.opts().dataDir;
158
+ const code = await runCreateService({ dataDir, dryRun: opts.dryRun, stdin: opts.stdin }, { file: opts.file });
159
+ process.exit(code);
160
+ });
161
+ program.parse();
162
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["cli/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { join } from 'node:path';\nimport { Command } from 'commander';\nimport {\n runCreateIssue,\n runCreateLandmark,\n runCreateLine,\n runCreateOperator,\n runCreateService,\n runCreateStation,\n runCreateTown,\n} from './commands/create.js';\nimport { runList } from './commands/list.js';\nimport { runShowIssue } from './commands/show.js';\nimport { runValidate } from './commands/validate.js';\n\nconst program = new Command();\n\nprogram\n .name('mrtdown-cli')\n .description('CLI for mrtdown-data: create entities and validate data')\n .option(\n '-d, --data-dir <path>',\n 'Data directory',\n join(process.cwd(), 'data'),\n );\n\nprogram\n .command('validate')\n .description('Validate all data files against schemas')\n .option(\n '--scope <scope>',\n 'Only validate these entity types (repeatable): town, landmark, operator, station, line, service, issue',\n (val: string, prev: string[] | undefined) => (prev ?? []).concat(val),\n )\n .action((opts) => {\n const dataDir = program.opts().dataDir;\n const scope = opts.scope as string[] | undefined;\n const code = runValidate({\n dataDir,\n scope: scope?.length\n ? (scope as import('../../src/validators/index.js').ValidationScope[])\n : undefined,\n });\n process.exit(code);\n });\n\nprogram\n .command('show')\n .description('Display the current state of an issue')\n .argument('<issue-id>', 'Issue ID (e.g. 2011-09-20-faulty-cable-led-to-circle-line-disruption)')\n .option('--json', 'Output as JSON')\n .action((issueId, opts) => {\n const dataDir = program.opts().dataDir;\n const code = runShowIssue({\n dataDir,\n issueId,\n json: opts.json,\n });\n process.exit(code);\n });\n\nconst list = program.command('list').description('List entities');\n\nconst listEntities = [\n 'issue',\n 'town',\n 'landmark',\n 'operator',\n 'station',\n 'line',\n 'service',\n] as const;\n\nfor (const entity of listEntities) {\n list\n .command(entity)\n .description(`List ${entity}s`)\n .option('--json', 'Output as JSON')\n .action((opts) => {\n const dataDir = program.opts().dataDir;\n const code = runList({\n dataDir,\n entity,\n json: opts.json,\n });\n process.exit(code);\n });\n}\n\nconst create = program.command('create').description('Create a new entity');\n\ncreate\n .command('issue')\n .description('Create a new issue')\n .requiredOption('--date <YYYY-MM-DD>', 'Issue date')\n .requiredOption('--slug <slug>', 'URL-safe slug for the issue')\n .requiredOption('--title <title>', 'English title')\n .option(\n '--type <type>',\n 'Issue type: disruption, maintenance, infra',\n 'disruption',\n )\n .option('--source <source>', 'Title source', 'cli')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateIssue(\n { dataDir, dryRun: opts.dryRun },\n {\n date: opts.date,\n slug: opts.slug,\n title: opts.title,\n type: opts.type,\n source: opts.source,\n },\n );\n process.exit(code);\n });\n\ncreate\n .command('town')\n .description('Create a new town')\n .requiredOption('--id <id>', 'Town ID (e.g. yishun)')\n .requiredOption('--name <name>', 'English name')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateTown(\n { dataDir, dryRun: opts.dryRun },\n { id: opts.id, name: opts.name },\n );\n process.exit(code);\n });\n\ncreate\n .command('landmark')\n .description('Create a new landmark')\n .requiredOption('--id <id>', 'Landmark ID (e.g. northpoint-city)')\n .requiredOption('--name <name>', 'English name')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateLandmark(\n { dataDir, dryRun: opts.dryRun },\n { id: opts.id, name: opts.name },\n );\n process.exit(code);\n });\n\ncreate\n .command('operator')\n .description('Create a new operator')\n .requiredOption('--id <id>', 'Operator ID (e.g. SMRT_TRAINS)')\n .requiredOption('--name <name>', 'English name')\n .requiredOption('--founded-at <date>', 'Founded date (YYYY-MM-DD)')\n .option('--url <url>', 'Operator website URL')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateOperator(\n { dataDir, dryRun: opts.dryRun },\n {\n id: opts.id,\n name: opts.name,\n foundedAt: opts.foundedAt,\n url: opts.url,\n },\n );\n process.exit(code);\n });\n\ncreate\n .command('station')\n .description('Create a station from JSON (--stdin or --file)')\n .option('--stdin', 'Read JSON from stdin')\n .option('--file <path>', 'Read JSON from file')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateStation(\n { dataDir, dryRun: opts.dryRun, stdin: opts.stdin },\n { file: opts.file },\n );\n process.exit(code);\n });\n\ncreate\n .command('line')\n .description('Create a line from JSON (--stdin or --file)')\n .option('--stdin', 'Read JSON from stdin')\n .option('--file <path>', 'Read JSON from file')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateLine(\n { dataDir, dryRun: opts.dryRun, stdin: opts.stdin },\n { file: opts.file },\n );\n process.exit(code);\n });\n\ncreate\n .command('service')\n .description('Create a service from JSON (--stdin or --file)')\n .option('--stdin', 'Read JSON from stdin')\n .option('--file <path>', 'Read JSON from file')\n .option('--dry-run', 'Print what would be created without writing')\n .action(async (opts) => {\n const dataDir = program.opts().dataDir;\n const code = await runCreateService(\n { dataDir, dryRun: opts.dryRun, stdin: opts.stdin },\n { file: opts.file },\n );\n process.exit(code);\n });\n\nprogram.parse();\n"]}
@@ -1,4 +1,4 @@
1
- import type { AffectedEntity } from '#schema/issue/entity.js';
1
+ import type { AffectedEntity } from '../schema/issue/entity.js';
2
2
  /**
3
3
  * Generate a stable key for an affected entity.
4
4
  * @param target - The affected entity.
@@ -1 +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"]}
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"]}
@@ -1,4 +1,4 @@
1
- import type { PeriodFixed, PeriodRecurring } from '#schema/issue/period.js';
1
+ import type { PeriodFixed, PeriodRecurring } from '../schema/issue/period.js';
2
2
  /**
3
3
  * Normalize a recurring period into a list of fixed periods.
4
4
  * @param period - The recurring period to normalize.
@@ -1,6 +1,6 @@
1
1
  import { DateTime } from 'luxon';
2
2
  import { DateTime as DateTimeRust, Frequency, RRule, RRuleSet, Weekday, } from 'rrule-rust';
3
- import { assert } from '#util/assert.js';
3
+ import { assert } from '../util/assert.js';
4
4
  function toFrequency(frequency) {
5
5
  switch (frequency) {
6
6
  case 'daily':
@@ -1 +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"]}
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"]}
@@ -1 +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"]}
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"]}
@@ -1,4 +1,4 @@
1
- import type { Period } from '#schema/issue/period.js';
1
+ import type { Period } from '../schema/issue/period.js';
2
2
  /**
3
3
  * Optional inference tuning for operational mode.
4
4
  */
@@ -1,5 +1,5 @@
1
1
  import { DateTime } from 'luxon';
2
- import { assert } from '#util/assert.js';
2
+ import { assert } from '../util/assert.js';
3
3
  import { normalizeRecurringPeriod } from './normalizeRecurringPeriod.js';
4
4
  const DEFAULTS = {
5
5
  evidenceStaleAfterMinutes: 120,
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePeriods.js","sourceRoot":"/","sources":["helpers/resolvePeriods.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport type { Period, PeriodFixed } from '#schema/issue/period.js';\nimport { assert } from '#util/assert.js';\nimport { normalizeRecurringPeriod } from './normalizeRecurringPeriod.js';\n\nconst DEFAULTS: ResolvePeriodsOperationalModeConfig = {\n evidenceStaleAfterMinutes: 120,\n crowdExitGraceMinutes: 30,\n maxInferredDurationMinutes: 18 * 60,\n};\n\n/**\n * Optional inference tuning for operational mode.\n */\ntype ResolvePeriodsOperationalModeConfig = {\n /**\n * Minutes after `lastEvidenceAt` before an open period is considered stale.\n *\n * @default 120\n */\n evidenceStaleAfterMinutes?: number;\n /**\n * Grace minutes after crowd activity decays before inferring resolution.\n *\n * @default 30\n */\n crowdExitGraceMinutes?: number;\n /**\n * Hard cap for inferred period length from `startAt`.\n *\n * @default 1080\n */\n maxInferredDurationMinutes?: number;\n};\n\n/**\n * Crowd-derived signal used as a positive indicator of ongoing disruption.\n */\ntype ResolvePeriodsCrowdSignal = {\n /**\n * Whether crowd reports currently indicate active disruption.\n */\n activeNow: boolean;\n /**\n * Most recent timestamp when crowd activity was observed.\n */\n lastActiveAt?: string | null;\n /**\n * Explicit timestamp when crowd reports indicate resolution.\n */\n exitedAt?: string | null;\n /**\n * Optional model confidence for `activeNow` in the [0, 1] range.\n */\n confidenceNow?: number | null;\n};\n\nexport type ResolvePeriodsMode =\n | { kind: 'canonical' }\n | {\n kind: 'operational';\n /**\n * Timestamp of the most recent evidence supporting an ongoing state\n * for this entity.\n *\n * If provided and endAt is null:\n * - May be used to infer an end time after a configured staleness window.\n *\n * If null or undefined:\n * - No evidence-timeout inference will occur.\n */\n lastEvidenceAt?: string | null;\n /**\n * Optional crowd signal state for this entity.\n *\n * Crowd data is treated as a positive signal:\n * - activeNow = true -> disruption likely ongoing.\n * - exitedAt or lastActiveAt may be used to infer resolution.\n */\n crowd?: ResolvePeriodsCrowdSignal | null;\n /**\n * Optional configuration overrides for inference behavior.\n *\n * If omitted, sensible defaults are used.\n *\n * @default { evidenceStaleAfterMinutes: 120, crowdExitGraceMinutes: 30, maxInferredDurationMinutes: 1080 }\n */\n config?: ResolvePeriodsOperationalModeConfig;\n };\n\nexport type ResolvePeriodsEndAtSource = 'fact' | 'inferred' | 'none';\nexport type ResolvePeriodsEndAtReason = 'crowd_decay' | 'evidence_timeout';\n\n/**\n * Parameters for resolvePeriods().\n *\n * These inputs provide:\n * - The canonical periods to resolve\n * - The evaluation timestamp (`asOf`)\n * - The normalization strategy (`mode`)\n * - Optional contextual signals used for inference (evidence + crowd)\n *\n * None of these inputs modify canonical storage. They are used only to\n * derive a view suitable for UI or analytics.\n */\nexport type ResolvePeriodsParams = {\n /**\n * Canonical periods for a single entity (service or facility).\n *\n * Requirements:\n * - startAt and endAt must be ISO 8601 strings with timezone offsets.\n * - endAt may be null when resolution was not explicitly recorded.\n *\n * These are treated as factual inputs. resolvePeriods() does not\n * mutate or rewrite them.\n */\n periods: Period[];\n\n /**\n * The timestamp at which normalization is evaluated.\n *\n * Must be an ISO 8601 string with timezone offset (e.g. +08:00).\n *\n * Examples:\n * - Determines whether a period is currently active.\n * - Prevents inferred end times from extending into the future.\n */\n asOf: string;\n\n /**\n * Controls how open-ended periods are interpreted.\n */\n mode: ResolvePeriodsMode;\n};\n\n/**\n * Normalized periods returned by `resolvePeriods()`.\n *\n * Each item preserves canonical `startAt`/`endAt` values and adds mode-aware\n * resolution metadata for consumers that need either factual timelines or\n * operational \"active now\" behavior.\n */\ntype ResolvePeriodsResult = {\n /**\n * Start timestamp from canonical period data.\n */\n startAt: string;\n /**\n * Canonical end timestamp as stored in source data.\n *\n * This remains null for open-ended canonical periods.\n */\n endAt: string | null;\n /**\n * Effective end timestamp for the selected mode.\n *\n * - \"canonical\": equals `endAt`\n * - \"operational\": may be inferred (end of day when inferred)\n */\n endAtResolved: string | null;\n /**\n * Origin of `endAtResolved`.\n */\n endAtSource: ResolvePeriodsEndAtSource;\n /**\n * Heuristic used when `endAtSource` is \"inferred\".\n */\n endAtReason?: ResolvePeriodsEndAtReason;\n}[];\n\nfunction resolveByMode(args: {\n period: PeriodFixed;\n mode: ResolvePeriodsMode['kind'];\n asOf: DateTime;\n lastEvidenceAt?: string | null;\n crowd?: ResolvePeriodsCrowdSignal | null;\n config: ResolvePeriodsOperationalModeConfig;\n}): ResolvePeriodsResult[number] {\n const { period, mode, asOf, lastEvidenceAt, crowd, config } = args;\n const startAtDt = DateTime.fromISO(period.startAt, { setZone: true });\n assert(startAtDt.isValid, `Invalid ISO datetime: ${period.startAt}`);\n\n if (mode === 'canonical') {\n return {\n ...period,\n endAtResolved: period.endAt,\n endAtSource: period.endAt ? 'fact' : 'none',\n };\n }\n\n if (period.endAt) {\n return {\n ...period,\n endAtResolved: period.endAt,\n endAtSource: 'fact',\n };\n }\n\n const evidenceTimeoutEnd = lastEvidenceAt\n ? (() => {\n const parsedLastEvidenceAt = DateTime.fromISO(lastEvidenceAt, {\n setZone: true,\n });\n assert(\n parsedLastEvidenceAt.isValid,\n `Invalid ISO datetime: ${lastEvidenceAt}`,\n );\n return parsedLastEvidenceAt.plus({\n minutes: config.evidenceStaleAfterMinutes,\n });\n })()\n : null;\n\n let crowdDecayEnd: DateTime | null = null;\n if (crowd) {\n if (crowd.exitedAt) {\n crowdDecayEnd = DateTime.fromISO(crowd.exitedAt, { setZone: true });\n assert(crowdDecayEnd.isValid, `Invalid ISO datetime: ${crowd.exitedAt}`);\n } else if (!crowd.activeNow && crowd.lastActiveAt) {\n const parsedLastActiveAt = DateTime.fromISO(crowd.lastActiveAt, {\n setZone: true,\n });\n assert(\n parsedLastActiveAt.isValid,\n `Invalid ISO datetime: ${crowd.lastActiveAt}`,\n );\n crowdDecayEnd = parsedLastActiveAt.plus({\n minutes: config.crowdExitGraceMinutes,\n });\n }\n }\n\n const inferredCandidate = crowdDecayEnd ?? evidenceTimeoutEnd;\n const inferredReason: ResolvePeriodsResult[number]['endAtReason'] =\n crowdDecayEnd\n ? 'crowd_decay'\n : evidenceTimeoutEnd\n ? 'evidence_timeout'\n : undefined;\n\n if (!inferredCandidate || !inferredReason) {\n return {\n ...period,\n endAtResolved: null,\n endAtSource: 'none',\n };\n }\n\n const maxInferredEnd = startAtDt.plus({\n minutes: config.maxInferredDurationMinutes,\n });\n\n // Inferred end = end of day (00:00 next day, exclusive) in Singapore timezone.\n let inferredEnd = inferredCandidate\n .setZone('Asia/Singapore')\n .startOf('day')\n .plus({ days: 1 });\n\n // Never infer an end before the period starts.\n if (inferredEnd < startAtDt) {\n inferredEnd = startAtDt;\n }\n // Never infer beyond the configured operational maximum window.\n if (inferredEnd > maxInferredEnd) {\n inferredEnd = maxInferredEnd;\n }\n\n // If inferred close time is in the future relative to asOf, keep it open.\n if (inferredEnd > asOf) {\n return {\n ...period,\n endAtResolved: null,\n endAtSource: 'none',\n };\n }\n\n return {\n ...period,\n endAtResolved: inferredEnd.toISO(),\n endAtSource: 'inferred',\n endAtReason: inferredReason,\n };\n}\n\n/**\n * Resolves canonical Period[] into a view suitable for UI or statistics.\n *\n * This function does NOT mutate canonical period data. It derives a view over\n * stored periods depending on the selected normalization mode.\n *\n * The core problem this solves:\n * - In real operations, disruption \"end\" is often not explicitly reported.\n * - Crowd reports are positive-only (people report problems more than resolution).\n * - Canonical logs should not fabricate timestamps, but the product still needs:\n * - a usable \"active now\" experience, and\n * - honest uptime/statistics.\n *\n * ---------------------------------------------------------------------\n * MODES\n * ---------------------------------------------------------------------\n *\n * 1) \"canonical\" (truth / audit)\n *\n * Intended for:\n * - Issue detail timelines and audit views (\"what do we actually know?\")\n * - Debugging and deterministic replay\n * - Data exports and downstream processing\n *\n * Behavior:\n * - Returns periods exactly as stored.\n * - endAtresolved === endAt.\n * - Open-ended periods (endAt = null) remain open.\n * - No inferred end times are introduced.\n *\n * Use this when you want maximum factual integrity and reproducibility.\n *\n *\n * 2) \"operational\" (live UX)\n *\n * Intended for:\n * - Live disruption UI (homepage banners, \"active now\", notifications)\n * - User-facing duration display (\"likely ended around ...\")\n * - Operational dashboards where preventing \"zombie incidents\" is important\n *\n * Behavior:\n * - If a period has a factual endAt, use it.\n * - If endAt is null, attempt to infer an end time using heuristics such as:\n * - crowd signal decay (preferred when available)\n * - evidence staleness timeout (fallback)\n * - Inferred ends are annotated:\n * endAtSource = \"inferred\"\n * endAtReason = \"crowd_decay\" | \"evidence_timeout\"\n * - If inference would produce an end time later than `asOf`,\n * the period remains open (still active).\n *\n * IMPORTANT:\n * - Inferred ends are derived and reversible.\n * - They must NOT be written back into canonical storage.\n *\n * Inferred ends are set to end of day (00:00 next day, exclusive) in Singapore\n * timezone, not duration-based. This avoids artificially shortening disruption.\n *\n * Use this when you want a stable, user-friendly view of \"what's happening now\"\n * even when reporting is incomplete.\n *\n *\n * ---------------------------------------------------------------------\n * DESIGN PRINCIPLE\n * ---------------------------------------------------------------------\n *\n * Canonical data must remain factually correct and append-only.\n * Heuristics (timeouts, crowd decay, assumptions) belong in derived views,\n * not in canonical period storage.\n */\nexport function resolvePeriods(\n params: ResolvePeriodsParams,\n): ResolvePeriodsResult {\n const { periods, asOf, mode } = params;\n const lastEvidenceAt =\n mode.kind === 'operational' ? mode.lastEvidenceAt : undefined;\n const crowd = mode.kind === 'operational' ? (mode.crowd ?? null) : null;\n const config = mode.kind === 'operational' ? mode.config : undefined;\n\n const normalizedPeriods = periods.flatMap((period) => {\n switch (period.kind) {\n case 'fixed':\n return [period];\n case 'recurring':\n return normalizeRecurringPeriod(period);\n default:\n // @ts-expect-error - we only support fixed and recurring periods for now\n throw new Error(`Invalid period kind: ${period.kind}`);\n }\n });\n\n const effectiveConfig = {\n evidenceStaleAfterMinutes:\n config?.evidenceStaleAfterMinutes ?? DEFAULTS.evidenceStaleAfterMinutes,\n crowdExitGraceMinutes:\n config?.crowdExitGraceMinutes ?? DEFAULTS.crowdExitGraceMinutes,\n maxInferredDurationMinutes:\n config?.maxInferredDurationMinutes ?? DEFAULTS.maxInferredDurationMinutes,\n };\n const asOfDt = DateTime.fromISO(asOf, { setZone: true });\n assert(asOfDt.isValid, `Invalid ISO datetime: ${asOf}`);\n const sorted = [...normalizedPeriods].sort((a, b) => {\n const aStart = DateTime.fromISO(a.startAt, { setZone: true });\n const bStart = DateTime.fromISO(b.startAt, { setZone: true });\n assert(aStart.isValid, `Invalid ISO datetime: ${a.startAt}`);\n assert(bStart.isValid, `Invalid ISO datetime: ${b.startAt}`);\n return aStart.toMillis() - bStart.toMillis();\n });\n\n return sorted.map((period) =>\n resolveByMode({\n period: { ...period },\n mode: mode.kind,\n asOf: asOfDt,\n lastEvidenceAt,\n crowd,\n config: effectiveConfig,\n }),\n );\n}\n"]}
1
+ {"version":3,"file":"resolvePeriods.js","sourceRoot":"/","sources":["helpers/resolvePeriods.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport type { Period, PeriodFixed } from '../schema/issue/period.js';\nimport { assert } from '../util/assert.js';\nimport { normalizeRecurringPeriod } from './normalizeRecurringPeriod.js';\n\nconst DEFAULTS: ResolvePeriodsOperationalModeConfig = {\n evidenceStaleAfterMinutes: 120,\n crowdExitGraceMinutes: 30,\n maxInferredDurationMinutes: 18 * 60,\n};\n\n/**\n * Optional inference tuning for operational mode.\n */\ntype ResolvePeriodsOperationalModeConfig = {\n /**\n * Minutes after `lastEvidenceAt` before an open period is considered stale.\n *\n * @default 120\n */\n evidenceStaleAfterMinutes?: number;\n /**\n * Grace minutes after crowd activity decays before inferring resolution.\n *\n * @default 30\n */\n crowdExitGraceMinutes?: number;\n /**\n * Hard cap for inferred period length from `startAt`.\n *\n * @default 1080\n */\n maxInferredDurationMinutes?: number;\n};\n\n/**\n * Crowd-derived signal used as a positive indicator of ongoing disruption.\n */\ntype ResolvePeriodsCrowdSignal = {\n /**\n * Whether crowd reports currently indicate active disruption.\n */\n activeNow: boolean;\n /**\n * Most recent timestamp when crowd activity was observed.\n */\n lastActiveAt?: string | null;\n /**\n * Explicit timestamp when crowd reports indicate resolution.\n */\n exitedAt?: string | null;\n /**\n * Optional model confidence for `activeNow` in the [0, 1] range.\n */\n confidenceNow?: number | null;\n};\n\nexport type ResolvePeriodsMode =\n | { kind: 'canonical' }\n | {\n kind: 'operational';\n /**\n * Timestamp of the most recent evidence supporting an ongoing state\n * for this entity.\n *\n * If provided and endAt is null:\n * - May be used to infer an end time after a configured staleness window.\n *\n * If null or undefined:\n * - No evidence-timeout inference will occur.\n */\n lastEvidenceAt?: string | null;\n /**\n * Optional crowd signal state for this entity.\n *\n * Crowd data is treated as a positive signal:\n * - activeNow = true -> disruption likely ongoing.\n * - exitedAt or lastActiveAt may be used to infer resolution.\n */\n crowd?: ResolvePeriodsCrowdSignal | null;\n /**\n * Optional configuration overrides for inference behavior.\n *\n * If omitted, sensible defaults are used.\n *\n * @default { evidenceStaleAfterMinutes: 120, crowdExitGraceMinutes: 30, maxInferredDurationMinutes: 1080 }\n */\n config?: ResolvePeriodsOperationalModeConfig;\n };\n\nexport type ResolvePeriodsEndAtSource = 'fact' | 'inferred' | 'none';\nexport type ResolvePeriodsEndAtReason = 'crowd_decay' | 'evidence_timeout';\n\n/**\n * Parameters for resolvePeriods().\n *\n * These inputs provide:\n * - The canonical periods to resolve\n * - The evaluation timestamp (`asOf`)\n * - The normalization strategy (`mode`)\n * - Optional contextual signals used for inference (evidence + crowd)\n *\n * None of these inputs modify canonical storage. They are used only to\n * derive a view suitable for UI or analytics.\n */\nexport type ResolvePeriodsParams = {\n /**\n * Canonical periods for a single entity (service or facility).\n *\n * Requirements:\n * - startAt and endAt must be ISO 8601 strings with timezone offsets.\n * - endAt may be null when resolution was not explicitly recorded.\n *\n * These are treated as factual inputs. resolvePeriods() does not\n * mutate or rewrite them.\n */\n periods: Period[];\n\n /**\n * The timestamp at which normalization is evaluated.\n *\n * Must be an ISO 8601 string with timezone offset (e.g. +08:00).\n *\n * Examples:\n * - Determines whether a period is currently active.\n * - Prevents inferred end times from extending into the future.\n */\n asOf: string;\n\n /**\n * Controls how open-ended periods are interpreted.\n */\n mode: ResolvePeriodsMode;\n};\n\n/**\n * Normalized periods returned by `resolvePeriods()`.\n *\n * Each item preserves canonical `startAt`/`endAt` values and adds mode-aware\n * resolution metadata for consumers that need either factual timelines or\n * operational \"active now\" behavior.\n */\ntype ResolvePeriodsResult = {\n /**\n * Start timestamp from canonical period data.\n */\n startAt: string;\n /**\n * Canonical end timestamp as stored in source data.\n *\n * This remains null for open-ended canonical periods.\n */\n endAt: string | null;\n /**\n * Effective end timestamp for the selected mode.\n *\n * - \"canonical\": equals `endAt`\n * - \"operational\": may be inferred (end of day when inferred)\n */\n endAtResolved: string | null;\n /**\n * Origin of `endAtResolved`.\n */\n endAtSource: ResolvePeriodsEndAtSource;\n /**\n * Heuristic used when `endAtSource` is \"inferred\".\n */\n endAtReason?: ResolvePeriodsEndAtReason;\n}[];\n\nfunction resolveByMode(args: {\n period: PeriodFixed;\n mode: ResolvePeriodsMode['kind'];\n asOf: DateTime;\n lastEvidenceAt?: string | null;\n crowd?: ResolvePeriodsCrowdSignal | null;\n config: ResolvePeriodsOperationalModeConfig;\n}): ResolvePeriodsResult[number] {\n const { period, mode, asOf, lastEvidenceAt, crowd, config } = args;\n const startAtDt = DateTime.fromISO(period.startAt, { setZone: true });\n assert(startAtDt.isValid, `Invalid ISO datetime: ${period.startAt}`);\n\n if (mode === 'canonical') {\n return {\n ...period,\n endAtResolved: period.endAt,\n endAtSource: period.endAt ? 'fact' : 'none',\n };\n }\n\n if (period.endAt) {\n return {\n ...period,\n endAtResolved: period.endAt,\n endAtSource: 'fact',\n };\n }\n\n const evidenceTimeoutEnd = lastEvidenceAt\n ? (() => {\n const parsedLastEvidenceAt = DateTime.fromISO(lastEvidenceAt, {\n setZone: true,\n });\n assert(\n parsedLastEvidenceAt.isValid,\n `Invalid ISO datetime: ${lastEvidenceAt}`,\n );\n return parsedLastEvidenceAt.plus({\n minutes: config.evidenceStaleAfterMinutes,\n });\n })()\n : null;\n\n let crowdDecayEnd: DateTime | null = null;\n if (crowd) {\n if (crowd.exitedAt) {\n crowdDecayEnd = DateTime.fromISO(crowd.exitedAt, { setZone: true });\n assert(crowdDecayEnd.isValid, `Invalid ISO datetime: ${crowd.exitedAt}`);\n } else if (!crowd.activeNow && crowd.lastActiveAt) {\n const parsedLastActiveAt = DateTime.fromISO(crowd.lastActiveAt, {\n setZone: true,\n });\n assert(\n parsedLastActiveAt.isValid,\n `Invalid ISO datetime: ${crowd.lastActiveAt}`,\n );\n crowdDecayEnd = parsedLastActiveAt.plus({\n minutes: config.crowdExitGraceMinutes,\n });\n }\n }\n\n const inferredCandidate = crowdDecayEnd ?? evidenceTimeoutEnd;\n const inferredReason: ResolvePeriodsResult[number]['endAtReason'] =\n crowdDecayEnd\n ? 'crowd_decay'\n : evidenceTimeoutEnd\n ? 'evidence_timeout'\n : undefined;\n\n if (!inferredCandidate || !inferredReason) {\n return {\n ...period,\n endAtResolved: null,\n endAtSource: 'none',\n };\n }\n\n const maxInferredEnd = startAtDt.plus({\n minutes: config.maxInferredDurationMinutes,\n });\n\n // Inferred end = end of day (00:00 next day, exclusive) in Singapore timezone.\n let inferredEnd = inferredCandidate\n .setZone('Asia/Singapore')\n .startOf('day')\n .plus({ days: 1 });\n\n // Never infer an end before the period starts.\n if (inferredEnd < startAtDt) {\n inferredEnd = startAtDt;\n }\n // Never infer beyond the configured operational maximum window.\n if (inferredEnd > maxInferredEnd) {\n inferredEnd = maxInferredEnd;\n }\n\n // If inferred close time is in the future relative to asOf, keep it open.\n if (inferredEnd > asOf) {\n return {\n ...period,\n endAtResolved: null,\n endAtSource: 'none',\n };\n }\n\n return {\n ...period,\n endAtResolved: inferredEnd.toISO(),\n endAtSource: 'inferred',\n endAtReason: inferredReason,\n };\n}\n\n/**\n * Resolves canonical Period[] into a view suitable for UI or statistics.\n *\n * This function does NOT mutate canonical period data. It derives a view over\n * stored periods depending on the selected normalization mode.\n *\n * The core problem this solves:\n * - In real operations, disruption \"end\" is often not explicitly reported.\n * - Crowd reports are positive-only (people report problems more than resolution).\n * - Canonical logs should not fabricate timestamps, but the product still needs:\n * - a usable \"active now\" experience, and\n * - honest uptime/statistics.\n *\n * ---------------------------------------------------------------------\n * MODES\n * ---------------------------------------------------------------------\n *\n * 1) \"canonical\" (truth / audit)\n *\n * Intended for:\n * - Issue detail timelines and audit views (\"what do we actually know?\")\n * - Debugging and deterministic replay\n * - Data exports and downstream processing\n *\n * Behavior:\n * - Returns periods exactly as stored.\n * - endAtresolved === endAt.\n * - Open-ended periods (endAt = null) remain open.\n * - No inferred end times are introduced.\n *\n * Use this when you want maximum factual integrity and reproducibility.\n *\n *\n * 2) \"operational\" (live UX)\n *\n * Intended for:\n * - Live disruption UI (homepage banners, \"active now\", notifications)\n * - User-facing duration display (\"likely ended around ...\")\n * - Operational dashboards where preventing \"zombie incidents\" is important\n *\n * Behavior:\n * - If a period has a factual endAt, use it.\n * - If endAt is null, attempt to infer an end time using heuristics such as:\n * - crowd signal decay (preferred when available)\n * - evidence staleness timeout (fallback)\n * - Inferred ends are annotated:\n * endAtSource = \"inferred\"\n * endAtReason = \"crowd_decay\" | \"evidence_timeout\"\n * - If inference would produce an end time later than `asOf`,\n * the period remains open (still active).\n *\n * IMPORTANT:\n * - Inferred ends are derived and reversible.\n * - They must NOT be written back into canonical storage.\n *\n * Inferred ends are set to end of day (00:00 next day, exclusive) in Singapore\n * timezone, not duration-based. This avoids artificially shortening disruption.\n *\n * Use this when you want a stable, user-friendly view of \"what's happening now\"\n * even when reporting is incomplete.\n *\n *\n * ---------------------------------------------------------------------\n * DESIGN PRINCIPLE\n * ---------------------------------------------------------------------\n *\n * Canonical data must remain factually correct and append-only.\n * Heuristics (timeouts, crowd decay, assumptions) belong in derived views,\n * not in canonical period storage.\n */\nexport function resolvePeriods(\n params: ResolvePeriodsParams,\n): ResolvePeriodsResult {\n const { periods, asOf, mode } = params;\n const lastEvidenceAt =\n mode.kind === 'operational' ? mode.lastEvidenceAt : undefined;\n const crowd = mode.kind === 'operational' ? (mode.crowd ?? null) : null;\n const config = mode.kind === 'operational' ? mode.config : undefined;\n\n const normalizedPeriods = periods.flatMap((period) => {\n switch (period.kind) {\n case 'fixed':\n return [period];\n case 'recurring':\n return normalizeRecurringPeriod(period);\n default:\n // @ts-expect-error - we only support fixed and recurring periods for now\n throw new Error(`Invalid period kind: ${period.kind}`);\n }\n });\n\n const effectiveConfig = {\n evidenceStaleAfterMinutes:\n config?.evidenceStaleAfterMinutes ?? DEFAULTS.evidenceStaleAfterMinutes,\n crowdExitGraceMinutes:\n config?.crowdExitGraceMinutes ?? DEFAULTS.crowdExitGraceMinutes,\n maxInferredDurationMinutes:\n config?.maxInferredDurationMinutes ?? DEFAULTS.maxInferredDurationMinutes,\n };\n const asOfDt = DateTime.fromISO(asOf, { setZone: true });\n assert(asOfDt.isValid, `Invalid ISO datetime: ${asOf}`);\n const sorted = [...normalizedPeriods].sort((a, b) => {\n const aStart = DateTime.fromISO(a.startAt, { setZone: true });\n const bStart = DateTime.fromISO(b.startAt, { setZone: true });\n assert(aStart.isValid, `Invalid ISO datetime: ${a.startAt}`);\n assert(bStart.isValid, `Invalid ISO datetime: ${b.startAt}`);\n return aStart.toMillis() - bStart.toMillis();\n });\n\n return sorted.map((period) =>\n resolveByMode({\n period: { ...period },\n mode: mode.kind,\n asOf: asOfDt,\n lastEvidenceAt,\n crowd,\n config: effectiveConfig,\n }),\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePeriods.test.js","sourceRoot":"/","sources":["helpers/resolvePeriods.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from 'vitest';\nimport type { Period } from '#schema/issue/period.js';\nimport { resolvePeriods } from './resolvePeriods.js';\n\ndescribe('resolvePeriods', () => {\n const asOf = '2025-01-01T12:00:00+08:00';\n const factEndedPeriod: Period[] = [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: '2025-01-01T09:00:00+08:00',\n },\n ];\n\n test('keeps fact-ended period in all modes', () => {\n const canonical = resolvePeriods({\n periods: factEndedPeriod,\n asOf,\n mode: { kind: 'canonical' },\n });\n const operational = resolvePeriods({\n periods: factEndedPeriod,\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(canonical[0]).toMatchObject({\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: '2025-01-01T09:00:00+08:00',\n endAtResolved: '2025-01-01T09:00:00+08:00',\n endAtSource: 'fact',\n });\n expect(operational[0].endAtSource).toBe('fact');\n });\n\n test('open period with no evidence and no crowd remains open', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(result[0]).toMatchObject({\n endAt: null,\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('open period infers end from evidence timeout only', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T01:00:00+08:00', // Past inferred end-of-day (2025-01-02T00:00)\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:00:00+08:00',\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-02T00:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'evidence_timeout',\n });\n });\n\n test('crowd exited inference wins over evidence timeout', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T01:00:00+08:00', // Past inferred end-of-day (2025-01-02T00:00)\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:00:00+08:00',\n crowd: {\n activeNow: false,\n exitedAt: '2025-01-01T10:15:00+08:00',\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-02T00:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'crowd_decay',\n });\n });\n\n test('crowd.activeNow=true prevents crowd-based inference fallback', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf,\n mode: {\n kind: 'operational',\n crowd: {\n activeNow: true,\n lastActiveAt: '2025-01-01T09:30:00+08:00',\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('clamps inferred end to maxInferredDurationMinutes', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T13:00:00+08:00',\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-02T10:00:00+08:00',\n config: {\n maxInferredDurationMinutes: 60,\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-01T09:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'evidence_timeout',\n });\n });\n\n test('does not close period when inferred end is later than asOf', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-01T10:00:00+08:00',\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:30:00+08:00',\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('expands recurring periods into fixed periods in canonical mode', () => {\n const result = resolvePeriods({\n periods: [\n {\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 },\n ],\n asOf,\n mode: { kind: 'canonical' },\n });\n\n expect(result).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 endAtResolved: '2025-01-01T10:00:00.000+08:00',\n endAtSource: 'fact',\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 endAtResolved: '2025-01-02T10:00:00.000+08:00',\n endAtSource: 'fact',\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 endAtResolved: '2025-01-03T10:00:00.000+08:00',\n endAtSource: 'fact',\n },\n ]);\n });\n\n test('sorts normalized recurring periods together with fixed periods', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T07:00:00+08:00',\n endAt: '2025-01-01T07:30:00+08:00',\n },\n {\n kind: 'recurring',\n frequency: 'daily',\n startAt: '2025-01-01T00:00:00+08:00',\n endAt: '2025-01-02T23:59:59+08:00',\n daysOfWeek: null,\n timeWindow: {\n startAt: '08:00:00',\n endAt: '09:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: null,\n },\n ],\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(result.map((period) => period.startAt)).toEqual([\n '2025-01-01T07:00:00+08:00',\n '2025-01-01T08:00:00.000+08:00',\n '2025-01-02T08:00:00.000+08:00',\n ]);\n expect(result.every((period) => period.endAtSource === 'fact')).toBe(true);\n });\n});\n"]}
1
+ {"version":3,"file":"resolvePeriods.test.js","sourceRoot":"/","sources":["helpers/resolvePeriods.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, test } from 'vitest';\nimport type { Period } from '../schema/issue/period.js';\nimport { resolvePeriods } from './resolvePeriods.js';\n\ndescribe('resolvePeriods', () => {\n const asOf = '2025-01-01T12:00:00+08:00';\n const factEndedPeriod: Period[] = [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: '2025-01-01T09:00:00+08:00',\n },\n ];\n\n test('keeps fact-ended period in all modes', () => {\n const canonical = resolvePeriods({\n periods: factEndedPeriod,\n asOf,\n mode: { kind: 'canonical' },\n });\n const operational = resolvePeriods({\n periods: factEndedPeriod,\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(canonical[0]).toMatchObject({\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: '2025-01-01T09:00:00+08:00',\n endAtResolved: '2025-01-01T09:00:00+08:00',\n endAtSource: 'fact',\n });\n expect(operational[0].endAtSource).toBe('fact');\n });\n\n test('open period with no evidence and no crowd remains open', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(result[0]).toMatchObject({\n endAt: null,\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('open period infers end from evidence timeout only', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T01:00:00+08:00', // Past inferred end-of-day (2025-01-02T00:00)\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:00:00+08:00',\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-02T00:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'evidence_timeout',\n });\n });\n\n test('crowd exited inference wins over evidence timeout', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T01:00:00+08:00', // Past inferred end-of-day (2025-01-02T00:00)\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:00:00+08:00',\n crowd: {\n activeNow: false,\n exitedAt: '2025-01-01T10:15:00+08:00',\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-02T00:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'crowd_decay',\n });\n });\n\n test('crowd.activeNow=true prevents crowd-based inference fallback', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf,\n mode: {\n kind: 'operational',\n crowd: {\n activeNow: true,\n lastActiveAt: '2025-01-01T09:30:00+08:00',\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('clamps inferred end to maxInferredDurationMinutes', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-02T13:00:00+08:00',\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-02T10:00:00+08:00',\n config: {\n maxInferredDurationMinutes: 60,\n },\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: '2025-01-01T09:00:00.000+08:00',\n endAtSource: 'inferred',\n endAtReason: 'evidence_timeout',\n });\n });\n\n test('does not close period when inferred end is later than asOf', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T08:00:00+08:00',\n endAt: null,\n },\n ],\n asOf: '2025-01-01T10:00:00+08:00',\n mode: {\n kind: 'operational',\n lastEvidenceAt: '2025-01-01T09:30:00+08:00',\n },\n });\n\n expect(result[0]).toMatchObject({\n endAtResolved: null,\n endAtSource: 'none',\n });\n });\n\n test('expands recurring periods into fixed periods in canonical mode', () => {\n const result = resolvePeriods({\n periods: [\n {\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 },\n ],\n asOf,\n mode: { kind: 'canonical' },\n });\n\n expect(result).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 endAtResolved: '2025-01-01T10:00:00.000+08:00',\n endAtSource: 'fact',\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 endAtResolved: '2025-01-02T10:00:00.000+08:00',\n endAtSource: 'fact',\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 endAtResolved: '2025-01-03T10:00:00.000+08:00',\n endAtSource: 'fact',\n },\n ]);\n });\n\n test('sorts normalized recurring periods together with fixed periods', () => {\n const result = resolvePeriods({\n periods: [\n {\n kind: 'fixed',\n startAt: '2025-01-01T07:00:00+08:00',\n endAt: '2025-01-01T07:30:00+08:00',\n },\n {\n kind: 'recurring',\n frequency: 'daily',\n startAt: '2025-01-01T00:00:00+08:00',\n endAt: '2025-01-02T23:59:59+08:00',\n daysOfWeek: null,\n timeWindow: {\n startAt: '08:00:00',\n endAt: '09:00:00',\n },\n timeZone: 'Asia/Singapore',\n excludedDates: null,\n },\n ],\n asOf,\n mode: { kind: 'operational' },\n });\n\n expect(result.map((period) => period.startAt)).toEqual([\n '2025-01-01T07:00:00+08:00',\n '2025-01-01T08:00:00.000+08:00',\n '2025-01-02T08:00:00.000+08:00',\n ]);\n expect(result.every((period) => period.endAtSource === 'fact')).toBe(true);\n });\n});\n"]}
package/dist/index.d.ts CHANGED
@@ -1,27 +1,27 @@
1
- export { FileStore } from '#repo/common/FileStore.js';
2
- export { MRTDownRepository } from '#repo/MRTDownRepository.js';
3
- export * from '#schema/common.js';
4
- export * from '#schema/issue/bundle.js';
5
- export * from '#schema/issue/cause.js';
6
- export * from '#schema/issue/claim.js';
7
- export * from '#schema/issue/entity.js';
8
- export * from '#schema/issue/evidence.js';
9
- export * from '#schema/issue/facilityEffect.js';
10
- export * from '#schema/issue/id.js';
11
- export * from '#schema/issue/impactEvent.js';
12
- export * from '#schema/issue/issue.js';
13
- export * from '#schema/issue/issueType.js';
14
- export * from '#schema/issue/period.js';
15
- export * from '#schema/issue/serviceEffect.js';
16
- export * from '#schema/issue/serviceScope.js';
17
- export * from '#schema/Landmark.js';
18
- export * from '#schema/Line.js';
19
- export * from '#schema/Operator.js';
20
- export * from '#schema/Service.js';
21
- export * from '#schema/Station.js';
22
- export * from '#schema/Town.js';
23
- export { FileWriteStore } from '#write/common/FileWriteStore.js';
24
- export { IdGenerator } from '#write/id/IdGenerator.js';
25
- export { MRTDownWriter } from '#write/MRTDownWriter.js';
1
+ export { FileStore } from './repo/common/FileStore.js';
2
+ export { MRTDownRepository } from './repo/MRTDownRepository.js';
3
+ export * from './schema/common.js';
4
+ export * from './schema/issue/bundle.js';
5
+ export * from './schema/issue/cause.js';
6
+ export * from './schema/issue/claim.js';
7
+ export * from './schema/issue/entity.js';
8
+ export * from './schema/issue/evidence.js';
9
+ export * from './schema/issue/facilityEffect.js';
10
+ export * from './schema/issue/id.js';
11
+ export * from './schema/issue/impactEvent.js';
12
+ export * from './schema/issue/issue.js';
13
+ export * from './schema/issue/issueType.js';
14
+ export * from './schema/issue/period.js';
15
+ export * from './schema/issue/serviceEffect.js';
16
+ export * from './schema/issue/serviceScope.js';
17
+ export * from './schema/Landmark.js';
18
+ export * from './schema/Line.js';
19
+ export * from './schema/Operator.js';
20
+ export * from './schema/Service.js';
21
+ export * from './schema/Station.js';
22
+ export * from './schema/Town.js';
23
+ export { FileWriteStore } from './write/common/FileWriteStore.js';
24
+ export { IdGenerator } from './write/id/IdGenerator.js';
25
+ export { MRTDownWriter } from './write/MRTDownWriter.js';
26
26
  export { normalizeRecurringPeriod } from './helpers/normalizeRecurringPeriod.js';
27
27
  export { resolvePeriods } from './helpers/resolvePeriods.js';
package/dist/index.js CHANGED
@@ -1,28 +1,28 @@
1
- export { FileStore } from '#repo/common/FileStore.js';
2
- export { MRTDownRepository } from '#repo/MRTDownRepository.js';
3
- export * from '#schema/common.js';
4
- export * from '#schema/issue/bundle.js';
5
- export * from '#schema/issue/cause.js';
6
- export * from '#schema/issue/claim.js';
7
- export * from '#schema/issue/entity.js';
8
- export * from '#schema/issue/evidence.js';
9
- export * from '#schema/issue/facilityEffect.js';
10
- export * from '#schema/issue/id.js';
11
- export * from '#schema/issue/impactEvent.js';
12
- export * from '#schema/issue/issue.js';
13
- export * from '#schema/issue/issueType.js';
14
- export * from '#schema/issue/period.js';
15
- export * from '#schema/issue/serviceEffect.js';
16
- export * from '#schema/issue/serviceScope.js';
17
- export * from '#schema/Landmark.js';
18
- export * from '#schema/Line.js';
19
- export * from '#schema/Operator.js';
20
- export * from '#schema/Service.js';
21
- export * from '#schema/Station.js';
22
- export * from '#schema/Town.js';
23
- export { FileWriteStore } from '#write/common/FileWriteStore.js';
24
- export { IdGenerator } from '#write/id/IdGenerator.js';
25
- export { MRTDownWriter } from '#write/MRTDownWriter.js';
1
+ export { FileStore } from './repo/common/FileStore.js';
2
+ export { MRTDownRepository } from './repo/MRTDownRepository.js';
3
+ export * from './schema/common.js';
4
+ export * from './schema/issue/bundle.js';
5
+ export * from './schema/issue/cause.js';
6
+ export * from './schema/issue/claim.js';
7
+ export * from './schema/issue/entity.js';
8
+ export * from './schema/issue/evidence.js';
9
+ export * from './schema/issue/facilityEffect.js';
10
+ export * from './schema/issue/id.js';
11
+ export * from './schema/issue/impactEvent.js';
12
+ export * from './schema/issue/issue.js';
13
+ export * from './schema/issue/issueType.js';
14
+ export * from './schema/issue/period.js';
15
+ export * from './schema/issue/serviceEffect.js';
16
+ export * from './schema/issue/serviceScope.js';
17
+ export * from './schema/Landmark.js';
18
+ export * from './schema/Line.js';
19
+ export * from './schema/Operator.js';
20
+ export * from './schema/Service.js';
21
+ export * from './schema/Station.js';
22
+ export * from './schema/Town.js';
23
+ export { FileWriteStore } from './write/common/FileWriteStore.js';
24
+ export { IdGenerator } from './write/id/IdGenerator.js';
25
+ export { MRTDownWriter } from './write/MRTDownWriter.js';
26
26
  export { normalizeRecurringPeriod } from './helpers/normalizeRecurringPeriod.js';
27
27
  export { resolvePeriods } from './helpers/resolvePeriods.js';
28
28
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAEhC,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["export { FileStore } from '#repo/common/FileStore.js';\nexport { MRTDownRepository } from '#repo/MRTDownRepository.js';\n\nexport * from '#schema/common.js';\nexport * from '#schema/issue/bundle.js';\nexport * from '#schema/issue/cause.js';\nexport * from '#schema/issue/claim.js';\nexport * from '#schema/issue/entity.js';\nexport * from '#schema/issue/evidence.js';\nexport * from '#schema/issue/facilityEffect.js';\nexport * from '#schema/issue/id.js';\nexport * from '#schema/issue/impactEvent.js';\nexport * from '#schema/issue/issue.js';\nexport * from '#schema/issue/issueType.js';\nexport * from '#schema/issue/period.js';\nexport * from '#schema/issue/serviceEffect.js';\nexport * from '#schema/issue/serviceScope.js';\nexport * from '#schema/Landmark.js';\nexport * from '#schema/Line.js';\nexport * from '#schema/Operator.js';\nexport * from '#schema/Service.js';\nexport * from '#schema/Station.js';\nexport * from '#schema/Town.js';\n\nexport { FileWriteStore } from '#write/common/FileWriteStore.js';\nexport { IdGenerator } from '#write/id/IdGenerator.js';\nexport { MRTDownWriter } from '#write/MRTDownWriter.js';\nexport { normalizeRecurringPeriod } from './helpers/normalizeRecurringPeriod.js';\nexport { resolvePeriods } from './helpers/resolvePeriods.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,sBAAsB,CAAC;AACrC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["export { FileStore } from './repo/common/FileStore.js';\nexport { MRTDownRepository } from './repo/MRTDownRepository.js';\n\nexport * from './schema/common.js';\nexport * from './schema/issue/bundle.js';\nexport * from './schema/issue/cause.js';\nexport * from './schema/issue/claim.js';\nexport * from './schema/issue/entity.js';\nexport * from './schema/issue/evidence.js';\nexport * from './schema/issue/facilityEffect.js';\nexport * from './schema/issue/id.js';\nexport * from './schema/issue/impactEvent.js';\nexport * from './schema/issue/issue.js';\nexport * from './schema/issue/issueType.js';\nexport * from './schema/issue/period.js';\nexport * from './schema/issue/serviceEffect.js';\nexport * from './schema/issue/serviceScope.js';\nexport * from './schema/Landmark.js';\nexport * from './schema/Line.js';\nexport * from './schema/Operator.js';\nexport * from './schema/Service.js';\nexport * from './schema/Station.js';\nexport * from './schema/Town.js';\n\nexport { FileWriteStore } from './write/common/FileWriteStore.js';\nexport { IdGenerator } from './write/id/IdGenerator.js';\nexport { MRTDownWriter } from './write/MRTDownWriter.js';\nexport { normalizeRecurringPeriod } from './helpers/normalizeRecurringPeriod.js';\nexport { resolvePeriods } from './helpers/resolvePeriods.js';\n"]}
@@ -0,0 +1,2 @@
1
+ import OpenAI from 'openai';
2
+ export declare const openAiClient: OpenAI;
@@ -0,0 +1,5 @@
1
+ import OpenAI from 'openai';
2
+ export const openAiClient = new OpenAI({
3
+ apiKey: process.env.OPENAI_AI_KEY,
4
+ });
5
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"/","sources":["llm/client.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC;IACrC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;CAClC,CAAC,CAAC","sourcesContent":["import OpenAI from 'openai';\n\nexport const openAiClient = new OpenAI({\n apiKey: process.env.OPENAI_AI_KEY,\n});\n"]}