@mrtdown/core 2.0.0-alpha.4 → 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 (85) hide show
  1. package/dist/cli/commands/create.js +3 -3
  2. package/dist/cli/commands/create.js.map +1 -1
  3. package/dist/cli/commands/list.js +2 -2
  4. package/dist/cli/commands/list.js.map +1 -1
  5. package/dist/cli/commands/show.js +3 -3
  6. package/dist/cli/commands/show.js.map +1 -1
  7. package/dist/cli/commands/validate.js +1 -1
  8. package/dist/cli/commands/validate.js.map +1 -1
  9. package/dist/helpers/keyForAffectedEntity.d.ts +1 -1
  10. package/dist/helpers/keyForAffectedEntity.js.map +1 -1
  11. package/dist/helpers/normalizeRecurringPeriod.d.ts +1 -1
  12. package/dist/helpers/normalizeRecurringPeriod.js +1 -1
  13. package/dist/helpers/normalizeRecurringPeriod.js.map +1 -1
  14. package/dist/helpers/normalizeRecurringPeriod.test.js.map +1 -1
  15. package/dist/helpers/resolvePeriods.d.ts +1 -1
  16. package/dist/helpers/resolvePeriods.js +1 -1
  17. package/dist/helpers/resolvePeriods.js.map +1 -1
  18. package/dist/helpers/resolvePeriods.test.js.map +1 -1
  19. package/dist/index.d.ts +25 -25
  20. package/dist/index.js +25 -25
  21. package/dist/index.js.map +1 -1
  22. package/dist/llm/common/MemoryStore.d.ts +2 -2
  23. package/dist/llm/common/MemoryStore.js.map +1 -1
  24. package/dist/llm/common/formatCurrentState.d.ts +2 -2
  25. package/dist/llm/common/formatCurrentState.js.map +1 -1
  26. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js +3 -3
  27. package/dist/llm/functions/extractClaimsFromNewEvidence/eval.test.js.map +1 -1
  28. package/dist/llm/functions/extractClaimsFromNewEvidence/index.d.ts +2 -2
  29. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js +4 -4
  30. package/dist/llm/functions/extractClaimsFromNewEvidence/index.js.map +1 -1
  31. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.d.ts +1 -1
  32. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindLinesTool.js.map +1 -1
  33. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.d.ts +1 -1
  34. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js +1 -1
  35. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindServicesTool.js.map +1 -1
  36. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.d.ts +2 -2
  37. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js +2 -2
  38. package/dist/llm/functions/extractClaimsFromNewEvidence/tools/FindStationsTool.js.map +1 -1
  39. package/dist/llm/functions/generateIssueTitleAndSlug/index.js +2 -2
  40. package/dist/llm/functions/generateIssueTitleAndSlug/index.js.map +1 -1
  41. package/dist/llm/functions/translate/index.js +4 -4
  42. package/dist/llm/functions/translate/index.js.map +1 -1
  43. package/dist/llm/functions/triageNewEvidence/eval.test.js +2 -2
  44. package/dist/llm/functions/triageNewEvidence/eval.test.js.map +1 -1
  45. package/dist/llm/functions/triageNewEvidence/index.d.ts +1 -1
  46. package/dist/llm/functions/triageNewEvidence/index.js +4 -4
  47. package/dist/llm/functions/triageNewEvidence/index.js.map +1 -1
  48. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.d.ts +2 -2
  49. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js +1 -1
  50. package/dist/llm/functions/triageNewEvidence/tools/FindIssuesTool.js.map +1 -1
  51. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.d.ts +1 -1
  52. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js +1 -1
  53. package/dist/llm/functions/triageNewEvidence/tools/GetIssueTool.js.map +1 -1
  54. package/dist/repo/issue/IssueRepository.d.ts +1 -1
  55. package/dist/repo/issue/IssueRepository.js +3 -3
  56. package/dist/repo/issue/IssueRepository.js.map +1 -1
  57. package/dist/repo/issue/helpers/deriveCurrentState.d.ts +6 -6
  58. package/dist/repo/issue/helpers/deriveCurrentState.js +1 -1
  59. package/dist/repo/issue/helpers/deriveCurrentState.js.map +1 -1
  60. package/dist/repo/issue/helpers/deriveCurrentState.test.js.map +1 -1
  61. package/dist/schema/issue/evidence.js +1 -1
  62. package/dist/schema/issue/evidence.js.map +1 -1
  63. package/dist/schema/issue/issue.js +1 -1
  64. package/dist/schema/issue/issue.js.map +1 -1
  65. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.d.ts +1 -1
  66. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js +1 -1
  67. package/dist/util/ingestContent/helpers/getSlugDateTimeFromClaims.js.map +1 -1
  68. package/dist/util/ingestContent/index.js +9 -9
  69. package/dist/util/ingestContent/index.js.map +1 -1
  70. package/dist/validators/buildContext.js +6 -6
  71. package/dist/validators/buildContext.js.map +1 -1
  72. package/dist/validators/issue.d.ts +1 -1
  73. package/dist/validators/issue.js +3 -3
  74. package/dist/validators/issue.js.map +1 -1
  75. package/dist/validators/line.d.ts +1 -1
  76. package/dist/validators/line.js +1 -1
  77. package/dist/validators/line.js.map +1 -1
  78. package/dist/validators/operator.js +1 -1
  79. package/dist/validators/operator.js.map +1 -1
  80. package/dist/validators/town.d.ts +1 -1
  81. package/dist/validators/town.js +1 -1
  82. package/dist/validators/town.js.map +1 -1
  83. package/dist/write/issue/IssueWriter.d.ts +3 -3
  84. package/dist/write/issue/IssueWriter.js.map +1 -1
  85. package/package.json +1 -10
@@ -1,9 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { createInterface } from 'node:readline';
4
- import { IssueTypeSchema } from '#schema/issue/issueType.js';
5
- import { FileWriteStore } from '#write/common/FileWriteStore.js';
6
- import { MRTDownWriter } from '#write/MRTDownWriter.js';
4
+ import { IssueTypeSchema } from '../../schema/issue/issueType.js';
5
+ import { FileWriteStore } from '../../write/common/FileWriteStore.js';
6
+ import { MRTDownWriter } from '../../write/MRTDownWriter.js';
7
7
  function translationsFromEn(en) {
8
8
  return {
9
9
  'en-SG': en,
@@ -1 +1 @@
1
- {"version":3,"file":"create.js","sourceRoot":"/","sources":["cli/commands/create.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport type { IssueType } from '#schema/issue/issueType.js';\nimport { IssueTypeSchema } from '#schema/issue/issueType.js';\nimport { FileWriteStore } from '#write/common/FileWriteStore.js';\nimport { MRTDownWriter } from '#write/MRTDownWriter.js';\n\ntype CreateOptions = {\n dataDir: string;\n dryRun?: boolean;\n stdin?: boolean;\n};\n\nfunction translationsFromEn(en: string) {\n return {\n 'en-SG': en,\n 'zh-Hans': null,\n ms: null,\n ta: null,\n };\n}\n\nexport async function runCreateIssue(\n opts: CreateOptions,\n args: {\n date?: string;\n slug?: string;\n title?: string;\n type?: string;\n source?: string;\n },\n): Promise<number> {\n const { date, slug, title, type, source } = args;\n if (!date || !slug || !title) {\n console.error(\n 'Usage: create issue --date YYYY-MM-DD --slug <slug> --title <title> [--type disruption|maintenance|infra] [--source <source>]',\n );\n return 1;\n }\n\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date);\n if (!dateMatch) {\n console.error('Invalid date format. Use YYYY-MM-DD.');\n return 1;\n }\n\n const issueId = `${date}-${slug}`;\n const issueType = (\n type && IssueTypeSchema.safeParse(type).success ? type : 'disruption'\n ) as IssueType;\n\n const issue = {\n id: issueId,\n type: issueType,\n title: translationsFromEn(title),\n titleMeta: { source: source ?? 'cli' },\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(issue, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.issues.create(issue);\n console.log(\n `Created issue: ${join(opts.dataDir, 'issue', dateMatch[1], dateMatch[2], issueId)}`,\n );\n return 0;\n}\n\nexport async function runCreateTown(\n opts: CreateOptions,\n args: { id?: string; name?: string },\n): Promise<number> {\n if (!args.id || !args.name) {\n console.error('Usage: create town --id <id> --name <name>');\n return 1;\n }\n\n const town = {\n id: args.id,\n name: translationsFromEn(args.name),\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(town, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.towns.create(town);\n console.log(`Created town: ${join(opts.dataDir, 'town', `${town.id}.json`)}`);\n return 0;\n}\n\nexport async function runCreateLandmark(\n opts: CreateOptions,\n args: { id?: string; name?: string },\n): Promise<number> {\n if (!args.id || !args.name) {\n console.error('Usage: create landmark --id <id> --name <name>');\n return 1;\n }\n\n const landmark = {\n id: args.id,\n name: translationsFromEn(args.name),\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(landmark, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.landmarks.create(landmark);\n console.log(\n `Created landmark: ${join(opts.dataDir, 'landmark', `${landmark.id}.json`)}`,\n );\n return 0;\n}\n\nexport async function runCreateOperator(\n opts: CreateOptions,\n args: { id?: string; name?: string; foundedAt?: string; url?: string },\n): Promise<number> {\n if (!args.id || !args.name || !args.foundedAt) {\n console.error(\n 'Usage: create operator --id <id> --name <name> --founded-at YYYY-MM-DD [--url <url>]',\n );\n return 1;\n }\n\n const operator = {\n id: args.id,\n name: translationsFromEn(args.name),\n foundedAt: args.foundedAt,\n url: args.url ?? null,\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(operator, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.operators.create(operator);\n console.log(\n `Created operator: ${join(opts.dataDir, 'operator', `${operator.id}.json`)}`,\n );\n return 0;\n}\n\nasync function readStdin(): Promise<string> {\n const rl = createInterface({ input: process.stdin });\n const chunks: string[] = [];\n for await (const line of rl) {\n chunks.push(line);\n }\n return chunks.join('\\n');\n}\n\nexport async function runCreateStation(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create station --stdin < JSON | create station --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create station:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.stations.create(json as Parameters<typeof writer.stations.create>[0]);\n return 0;\n}\n\nexport async function runCreateLine(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create line --stdin < JSON | create line --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create line:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.lines.create(json as Parameters<typeof writer.lines.create>[0]);\n return 0;\n}\n\nexport async function runCreateService(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create service --stdin < JSON | create service --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create service:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.services.create(json as Parameters<typeof writer.services.create>[0]);\n return 0;\n}\n"]}
1
+ {"version":3,"file":"create.js","sourceRoot":"/","sources":["cli/commands/create.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport type { IssueType } from '../../schema/issue/issueType.js';\nimport { IssueTypeSchema } from '../../schema/issue/issueType.js';\nimport { FileWriteStore } from '../../write/common/FileWriteStore.js';\nimport { MRTDownWriter } from '../../write/MRTDownWriter.js';\n\ntype CreateOptions = {\n dataDir: string;\n dryRun?: boolean;\n stdin?: boolean;\n};\n\nfunction translationsFromEn(en: string) {\n return {\n 'en-SG': en,\n 'zh-Hans': null,\n ms: null,\n ta: null,\n };\n}\n\nexport async function runCreateIssue(\n opts: CreateOptions,\n args: {\n date?: string;\n slug?: string;\n title?: string;\n type?: string;\n source?: string;\n },\n): Promise<number> {\n const { date, slug, title, type, source } = args;\n if (!date || !slug || !title) {\n console.error(\n 'Usage: create issue --date YYYY-MM-DD --slug <slug> --title <title> [--type disruption|maintenance|infra] [--source <source>]',\n );\n return 1;\n }\n\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(date);\n if (!dateMatch) {\n console.error('Invalid date format. Use YYYY-MM-DD.');\n return 1;\n }\n\n const issueId = `${date}-${slug}`;\n const issueType = (\n type && IssueTypeSchema.safeParse(type).success ? type : 'disruption'\n ) as IssueType;\n\n const issue = {\n id: issueId,\n type: issueType,\n title: translationsFromEn(title),\n titleMeta: { source: source ?? 'cli' },\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(issue, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.issues.create(issue);\n console.log(\n `Created issue: ${join(opts.dataDir, 'issue', dateMatch[1], dateMatch[2], issueId)}`,\n );\n return 0;\n}\n\nexport async function runCreateTown(\n opts: CreateOptions,\n args: { id?: string; name?: string },\n): Promise<number> {\n if (!args.id || !args.name) {\n console.error('Usage: create town --id <id> --name <name>');\n return 1;\n }\n\n const town = {\n id: args.id,\n name: translationsFromEn(args.name),\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(town, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.towns.create(town);\n console.log(`Created town: ${join(opts.dataDir, 'town', `${town.id}.json`)}`);\n return 0;\n}\n\nexport async function runCreateLandmark(\n opts: CreateOptions,\n args: { id?: string; name?: string },\n): Promise<number> {\n if (!args.id || !args.name) {\n console.error('Usage: create landmark --id <id> --name <name>');\n return 1;\n }\n\n const landmark = {\n id: args.id,\n name: translationsFromEn(args.name),\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(landmark, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.landmarks.create(landmark);\n console.log(\n `Created landmark: ${join(opts.dataDir, 'landmark', `${landmark.id}.json`)}`,\n );\n return 0;\n}\n\nexport async function runCreateOperator(\n opts: CreateOptions,\n args: { id?: string; name?: string; foundedAt?: string; url?: string },\n): Promise<number> {\n if (!args.id || !args.name || !args.foundedAt) {\n console.error(\n 'Usage: create operator --id <id> --name <name> --founded-at YYYY-MM-DD [--url <url>]',\n );\n return 1;\n }\n\n const operator = {\n id: args.id,\n name: translationsFromEn(args.name),\n foundedAt: args.foundedAt,\n url: args.url ?? null,\n };\n\n if (opts.dryRun) {\n console.log('Would create:', JSON.stringify(operator, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.operators.create(operator);\n console.log(\n `Created operator: ${join(opts.dataDir, 'operator', `${operator.id}.json`)}`,\n );\n return 0;\n}\n\nasync function readStdin(): Promise<string> {\n const rl = createInterface({ input: process.stdin });\n const chunks: string[] = [];\n for await (const line of rl) {\n chunks.push(line);\n }\n return chunks.join('\\n');\n}\n\nexport async function runCreateStation(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create station --stdin < JSON | create station --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create station:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.stations.create(json as Parameters<typeof writer.stations.create>[0]);\n return 0;\n}\n\nexport async function runCreateLine(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create line --stdin < JSON | create line --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create line:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.lines.create(json as Parameters<typeof writer.lines.create>[0]);\n return 0;\n}\n\nexport async function runCreateService(\n opts: CreateOptions,\n args: Record<string, string | undefined>,\n): Promise<number> {\n let json: unknown;\n if (opts.stdin) {\n const raw = await readStdin();\n json = JSON.parse(raw);\n } else {\n const file = args.stdinFile ?? args.file;\n if (file) {\n const raw = await readFile(file, 'utf-8');\n json = JSON.parse(raw);\n } else {\n console.error(\n 'Usage: create service --stdin < JSON | create service --file <path>',\n );\n return 1;\n }\n }\n\n if (opts.dryRun) {\n console.log('Would create service:', JSON.stringify(json, null, 2));\n return 0;\n }\n\n const store = new FileWriteStore(opts.dataDir);\n const writer = new MRTDownWriter({ store });\n writer.services.create(json as Parameters<typeof writer.services.create>[0]);\n return 0;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { FileStore } from '#repo/common/FileStore.js';
2
- import { MRTDownRepository } from '#repo/MRTDownRepository.js';
1
+ import { FileStore } from '../../repo/common/FileStore.js';
2
+ import { MRTDownRepository } from '../../repo/MRTDownRepository.js';
3
3
  function nameEn(item) {
4
4
  return item.name?.['en-SG'] ?? '—';
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"list.js","sourceRoot":"/","sources":["cli/commands/list.ts"],"names":[],"mappings":"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","sourcesContent":["import { FileStore } from '#repo/common/FileStore.js';\nimport { MRTDownRepository } from '#repo/MRTDownRepository.js';\n\nexport type ListOptions = {\n dataDir: string;\n entity:\n | 'issue'\n | 'town'\n | 'landmark'\n | 'operator'\n | 'station'\n | 'line'\n | 'service';\n json?: boolean;\n};\n\nfunction nameEn(item: { name?: { 'en-SG'?: string } }): string {\n return item.name?.['en-SG'] ?? '—';\n}\n\nexport function runList(opts: ListOptions): number {\n const store = new FileStore(opts.dataDir);\n const repo = new MRTDownRepository({ store });\n\n switch (opts.entity) {\n case 'issue': {\n const bundles = repo.issues.list();\n if (opts.json) {\n console.log(\n JSON.stringify(\n bundles.map((b) => ({\n id: b.issue.id,\n type: b.issue.type,\n title: b.issue.title['en-SG'],\n })),\n null,\n 2,\n ),\n );\n } else {\n for (const b of bundles) {\n console.log(`${b.issue.id} ${b.issue.title['en-SG'] ?? b.issue.id}`);\n }\n }\n break;\n }\n case 'town': {\n const items = repo.towns.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'landmark': {\n const items = repo.landmarks.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'operator': {\n const items = repo.operators.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'station': {\n const items = repo.stations.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'line': {\n const items = repo.lines.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)} ${item.type}`);\n }\n }\n break;\n }\n case 'service': {\n const items = repo.services.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)} ${item.lineId}`);\n }\n }\n break;\n }\n default: {\n const _: never = opts.entity;\n console.error(`Unknown entity: ${opts.entity}`);\n return 1;\n }\n }\n\n return 0;\n}\n"]}
1
+ {"version":3,"file":"list.js","sourceRoot":"/","sources":["cli/commands/list.ts"],"names":[],"mappings":"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","sourcesContent":["import { FileStore } from '../../repo/common/FileStore.js';\nimport { MRTDownRepository } from '../../repo/MRTDownRepository.js';\n\nexport type ListOptions = {\n dataDir: string;\n entity:\n | 'issue'\n | 'town'\n | 'landmark'\n | 'operator'\n | 'station'\n | 'line'\n | 'service';\n json?: boolean;\n};\n\nfunction nameEn(item: { name?: { 'en-SG'?: string } }): string {\n return item.name?.['en-SG'] ?? '—';\n}\n\nexport function runList(opts: ListOptions): number {\n const store = new FileStore(opts.dataDir);\n const repo = new MRTDownRepository({ store });\n\n switch (opts.entity) {\n case 'issue': {\n const bundles = repo.issues.list();\n if (opts.json) {\n console.log(\n JSON.stringify(\n bundles.map((b) => ({\n id: b.issue.id,\n type: b.issue.type,\n title: b.issue.title['en-SG'],\n })),\n null,\n 2,\n ),\n );\n } else {\n for (const b of bundles) {\n console.log(`${b.issue.id} ${b.issue.title['en-SG'] ?? b.issue.id}`);\n }\n }\n break;\n }\n case 'town': {\n const items = repo.towns.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'landmark': {\n const items = repo.landmarks.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'operator': {\n const items = repo.operators.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'station': {\n const items = repo.stations.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)}`);\n }\n }\n break;\n }\n case 'line': {\n const items = repo.lines.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)} ${item.type}`);\n }\n }\n break;\n }\n case 'service': {\n const items = repo.services.list();\n if (opts.json) {\n console.log(JSON.stringify(items, null, 2));\n } else {\n for (const item of items) {\n console.log(`${item.id} ${nameEn(item)} ${item.lineId}`);\n }\n }\n break;\n }\n default: {\n const _: never = opts.entity;\n console.error(`Unknown entity: ${opts.entity}`);\n return 1;\n }\n }\n\n return 0;\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { DateTime } from 'luxon';
2
- import { FileStore } from '#repo/common/FileStore.js';
3
- import { deriveCurrentState } from '#repo/issue/helpers/deriveCurrentState.js';
4
- import { IssueRepository } from '#repo/issue/IssueRepository.js';
2
+ import { FileStore } from '../../repo/common/FileStore.js';
3
+ import { deriveCurrentState } from '../../repo/issue/helpers/deriveCurrentState.js';
4
+ import { IssueRepository } from '../../repo/issue/IssueRepository.js';
5
5
  import { resolvePeriods, } from '../../helpers/resolvePeriods.js';
6
6
  function formatScope(scope) {
7
7
  switch (scope.type) {
@@ -1 +1 @@
1
- {"version":3,"file":"show.js","sourceRoot":"/","sources":["cli/commands/show.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport { FileStore } from '#repo/common/FileStore.js';\nimport { deriveCurrentState } from '#repo/issue/helpers/deriveCurrentState.js';\nimport { IssueRepository } from '#repo/issue/IssueRepository.js';\nimport type { Period } from '#schema/issue/period.js';\nimport {\n type ResolvePeriodsMode,\n resolvePeriods,\n} from '../../helpers/resolvePeriods.js';\n\nexport type ShowIssueOptions = {\n dataDir: string;\n issueId: string;\n json?: boolean;\n};\n\nfunction formatScope(scope: {\n type: string;\n fromStationId?: string;\n toStationId?: string;\n stationId?: string;\n}): string {\n switch (scope.type) {\n case 'service.whole':\n return 'whole line';\n case 'service.segment':\n return `${scope.fromStationId} → ${scope.toStationId}`;\n case 'service.point':\n return scope.stationId ?? '?';\n default:\n return JSON.stringify(scope);\n }\n}\n\nfunction formatEffect(\n effect: { kind: string; duration?: string | null } | null,\n): string {\n if (!effect) return '—';\n if (effect.kind === 'delay') {\n return effect.duration ? `delay (${effect.duration})` : 'delay';\n }\n return effect.kind;\n}\n\ntype ResolvedPeriod = {\n startAt: string;\n endAt: string | null;\n endAtResolved: string | null;\n endAtSource: 'fact' | 'inferred' | 'none';\n endAtReason?: 'crowd_decay' | 'evidence_timeout';\n};\n\nfunction formatPeriod(period: ResolvedPeriod): string {\n const start = period.startAt;\n const end = period.endAtResolved ?? period.endAt ?? 'ongoing';\n let suffix = '';\n if (period.endAtSource === 'inferred' && period.endAtReason) {\n suffix = ` (inferred: ${period.endAtReason})`;\n }\n return `${start} → ${end}${suffix}`;\n}\n\nconst MODES = ['canonical', 'operational'] as const;\n\nfunction buildResolveMode(\n mode: (typeof MODES)[number],\n evidence: { ts: string }[],\n): ResolvePeriodsMode {\n switch (mode) {\n case 'canonical':\n return { kind: 'canonical' };\n case 'operational': {\n const lastEvidenceAt =\n evidence.length > 0\n ? evidence.reduce<string>(\n (latest, e) => (e.ts > latest ? e.ts : latest),\n evidence[0].ts,\n )\n : null;\n return { kind: 'operational', lastEvidenceAt };\n }\n }\n}\n\nfunction resolvePeriodsByMode(\n periods: Period[],\n asOf: string,\n evidence: { ts: string }[],\n): Record<(typeof MODES)[number], ResolvedPeriod[]> {\n const result = {} as Record<(typeof MODES)[number], ResolvedPeriod[]>;\n for (const mode of MODES) {\n result[mode] =\n periods.length > 0\n ? resolvePeriods({\n periods,\n asOf,\n mode: buildResolveMode(mode, evidence),\n })\n : [];\n }\n return result;\n}\n\nexport function runShowIssue(opts: ShowIssueOptions): number {\n const store = new FileStore(opts.dataDir);\n const repo = new IssueRepository(store);\n\n const bundle = repo.get(opts.issueId);\n if (!bundle) {\n console.error(`Issue not found: ${opts.issueId}`);\n return 1;\n }\n\n const state = deriveCurrentState(bundle);\n const asOf = DateTime.now().toISO() ?? '';\n\n const resolvedServices = Object.fromEntries(\n Object.entries(state.services).map(([key, svc]) => [\n key,\n {\n ...svc,\n periodsByMode: resolvePeriodsByMode(svc.periods, asOf, bundle.evidence),\n },\n ]),\n );\n\n const resolvedFacilities = Object.fromEntries(\n Object.entries(state.facilities).map(([key, fac]) => [\n key,\n {\n ...fac,\n periodsByMode: resolvePeriodsByMode(fac.periods, asOf, bundle.evidence),\n },\n ]),\n );\n\n if (opts.json) {\n const output = {\n issue: bundle.issue,\n evidenceCount: bundle.evidence.length,\n impactEventCount: bundle.impactEvents.length,\n currentState: {\n ...state,\n services: resolvedServices,\n facilities: resolvedFacilities,\n },\n };\n console.log(JSON.stringify(output, null, 2));\n return 0;\n }\n\n // Human-readable output\n const { issue, evidence, impactEvents } = bundle;\n console.log(`\\n${issue.title['en-SG'] ?? issue.id}`);\n console.log('─'.repeat(60));\n console.log(`ID: ${issue.id}`);\n console.log(`Type: ${issue.type}`);\n console.log(`Path: ${bundle.path}`);\n console.log(`Evidence: ${evidence.length} item(s)`);\n console.log(`Impact: ${impactEvents.length} event(s)`);\n console.log('');\n\n const hasServices = Object.keys(resolvedServices).length > 0;\n const hasFacilities = Object.keys(resolvedFacilities).length > 0;\n\n if (hasServices) {\n console.log('Current state — Services');\n console.log('─'.repeat(40));\n for (const [, svc] of Object.entries(resolvedServices)) {\n console.log(` ${svc.serviceId}`);\n console.log(` effect: ${formatEffect(svc.effect)}`);\n if (svc.scopes.length > 0) {\n console.log(` scopes: ${svc.scopes.map(formatScope).join('; ')}`);\n }\n for (const mode of MODES) {\n const periods = svc.periodsByMode[mode];\n if (periods.length > 0) {\n console.log(\n ` periods (${mode}): ${periods.map(formatPeriod).join('; ')}`,\n );\n }\n }\n if (svc.causes.length > 0) {\n console.log(` causes: ${svc.causes.join(', ')}`);\n }\n console.log('');\n }\n }\n\n if (hasFacilities) {\n console.log('Current state — Facilities');\n console.log('─'.repeat(40));\n for (const [, fac] of Object.entries(resolvedFacilities)) {\n console.log(` ${fac.stationId} (${fac.kind})`);\n console.log(` effect: ${formatEffect(fac.effect)}`);\n for (const mode of MODES) {\n const periods = fac.periodsByMode[mode];\n if (periods.length > 0) {\n console.log(\n ` periods (${mode}): ${periods.map(formatPeriod).join('; ')}`,\n );\n }\n }\n if (fac.causes.length > 0) {\n console.log(` causes: ${fac.causes.join(', ')}`);\n }\n console.log('');\n }\n }\n\n if (!hasServices && !hasFacilities) {\n console.log('Current state: (no services or facilities affected)');\n }\n\n return 0;\n}\n"]}
1
+ {"version":3,"file":"show.js","sourceRoot":"/","sources":["cli/commands/show.ts"],"names":[],"mappings":"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","sourcesContent":["import { DateTime } from 'luxon';\nimport { FileStore } from '../../repo/common/FileStore.js';\nimport { deriveCurrentState } from '../../repo/issue/helpers/deriveCurrentState.js';\nimport { IssueRepository } from '../../repo/issue/IssueRepository.js';\nimport type { Period } from '../../schema/issue/period.js';\nimport {\n type ResolvePeriodsMode,\n resolvePeriods,\n} from '../../helpers/resolvePeriods.js';\n\nexport type ShowIssueOptions = {\n dataDir: string;\n issueId: string;\n json?: boolean;\n};\n\nfunction formatScope(scope: {\n type: string;\n fromStationId?: string;\n toStationId?: string;\n stationId?: string;\n}): string {\n switch (scope.type) {\n case 'service.whole':\n return 'whole line';\n case 'service.segment':\n return `${scope.fromStationId} → ${scope.toStationId}`;\n case 'service.point':\n return scope.stationId ?? '?';\n default:\n return JSON.stringify(scope);\n }\n}\n\nfunction formatEffect(\n effect: { kind: string; duration?: string | null } | null,\n): string {\n if (!effect) return '—';\n if (effect.kind === 'delay') {\n return effect.duration ? `delay (${effect.duration})` : 'delay';\n }\n return effect.kind;\n}\n\ntype ResolvedPeriod = {\n startAt: string;\n endAt: string | null;\n endAtResolved: string | null;\n endAtSource: 'fact' | 'inferred' | 'none';\n endAtReason?: 'crowd_decay' | 'evidence_timeout';\n};\n\nfunction formatPeriod(period: ResolvedPeriod): string {\n const start = period.startAt;\n const end = period.endAtResolved ?? period.endAt ?? 'ongoing';\n let suffix = '';\n if (period.endAtSource === 'inferred' && period.endAtReason) {\n suffix = ` (inferred: ${period.endAtReason})`;\n }\n return `${start} → ${end}${suffix}`;\n}\n\nconst MODES = ['canonical', 'operational'] as const;\n\nfunction buildResolveMode(\n mode: (typeof MODES)[number],\n evidence: { ts: string }[],\n): ResolvePeriodsMode {\n switch (mode) {\n case 'canonical':\n return { kind: 'canonical' };\n case 'operational': {\n const lastEvidenceAt =\n evidence.length > 0\n ? evidence.reduce<string>(\n (latest, e) => (e.ts > latest ? e.ts : latest),\n evidence[0].ts,\n )\n : null;\n return { kind: 'operational', lastEvidenceAt };\n }\n }\n}\n\nfunction resolvePeriodsByMode(\n periods: Period[],\n asOf: string,\n evidence: { ts: string }[],\n): Record<(typeof MODES)[number], ResolvedPeriod[]> {\n const result = {} as Record<(typeof MODES)[number], ResolvedPeriod[]>;\n for (const mode of MODES) {\n result[mode] =\n periods.length > 0\n ? resolvePeriods({\n periods,\n asOf,\n mode: buildResolveMode(mode, evidence),\n })\n : [];\n }\n return result;\n}\n\nexport function runShowIssue(opts: ShowIssueOptions): number {\n const store = new FileStore(opts.dataDir);\n const repo = new IssueRepository(store);\n\n const bundle = repo.get(opts.issueId);\n if (!bundle) {\n console.error(`Issue not found: ${opts.issueId}`);\n return 1;\n }\n\n const state = deriveCurrentState(bundle);\n const asOf = DateTime.now().toISO() ?? '';\n\n const resolvedServices = Object.fromEntries(\n Object.entries(state.services).map(([key, svc]) => [\n key,\n {\n ...svc,\n periodsByMode: resolvePeriodsByMode(svc.periods, asOf, bundle.evidence),\n },\n ]),\n );\n\n const resolvedFacilities = Object.fromEntries(\n Object.entries(state.facilities).map(([key, fac]) => [\n key,\n {\n ...fac,\n periodsByMode: resolvePeriodsByMode(fac.periods, asOf, bundle.evidence),\n },\n ]),\n );\n\n if (opts.json) {\n const output = {\n issue: bundle.issue,\n evidenceCount: bundle.evidence.length,\n impactEventCount: bundle.impactEvents.length,\n currentState: {\n ...state,\n services: resolvedServices,\n facilities: resolvedFacilities,\n },\n };\n console.log(JSON.stringify(output, null, 2));\n return 0;\n }\n\n // Human-readable output\n const { issue, evidence, impactEvents } = bundle;\n console.log(`\\n${issue.title['en-SG'] ?? issue.id}`);\n console.log('─'.repeat(60));\n console.log(`ID: ${issue.id}`);\n console.log(`Type: ${issue.type}`);\n console.log(`Path: ${bundle.path}`);\n console.log(`Evidence: ${evidence.length} item(s)`);\n console.log(`Impact: ${impactEvents.length} event(s)`);\n console.log('');\n\n const hasServices = Object.keys(resolvedServices).length > 0;\n const hasFacilities = Object.keys(resolvedFacilities).length > 0;\n\n if (hasServices) {\n console.log('Current state — Services');\n console.log('─'.repeat(40));\n for (const [, svc] of Object.entries(resolvedServices)) {\n console.log(` ${svc.serviceId}`);\n console.log(` effect: ${formatEffect(svc.effect)}`);\n if (svc.scopes.length > 0) {\n console.log(` scopes: ${svc.scopes.map(formatScope).join('; ')}`);\n }\n for (const mode of MODES) {\n const periods = svc.periodsByMode[mode];\n if (periods.length > 0) {\n console.log(\n ` periods (${mode}): ${periods.map(formatPeriod).join('; ')}`,\n );\n }\n }\n if (svc.causes.length > 0) {\n console.log(` causes: ${svc.causes.join(', ')}`);\n }\n console.log('');\n }\n }\n\n if (hasFacilities) {\n console.log('Current state — Facilities');\n console.log('─'.repeat(40));\n for (const [, fac] of Object.entries(resolvedFacilities)) {\n console.log(` ${fac.stationId} (${fac.kind})`);\n console.log(` effect: ${formatEffect(fac.effect)}`);\n for (const mode of MODES) {\n const periods = fac.periodsByMode[mode];\n if (periods.length > 0) {\n console.log(\n ` periods (${mode}): ${periods.map(formatPeriod).join('; ')}`,\n );\n }\n }\n if (fac.causes.length > 0) {\n console.log(` causes: ${fac.causes.join(', ')}`);\n }\n console.log('');\n }\n }\n\n if (!hasServices && !hasFacilities) {\n console.log('Current state: (no services or facilities affected)');\n }\n\n return 0;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { FileStore } from '#repo/common/FileStore.js';
1
+ import { FileStore } from '../../repo/common/FileStore.js';
2
2
  import { validateAll } from '../../validators/index.js';
3
3
  export function runValidate(opts) {
4
4
  const store = new FileStore(opts.dataDir);
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"/","sources":["cli/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAwB,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAO9E,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAEzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,yBAAyB,CAAC,CAAC;IACjE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["import { FileStore } from '#repo/common/FileStore.js';\nimport { type ValidationScope, validateAll } from '../../validators/index.js';\n\nexport interface ValidateCliOptions {\n dataDir: string;\n scope?: ValidationScope[];\n}\n\nexport function runValidate(opts: ValidateCliOptions): number {\n const store = new FileStore(opts.dataDir);\n const errors = validateAll(store, { scope: opts.scope });\n\n if (errors.length === 0) {\n console.log('All data files are valid.');\n return 0;\n }\n\n console.error(`\\nFound ${errors.length} validation error(s):\\n`);\n for (const err of errors) {\n const loc = err.line ? `${err.file}:${err.line}` : err.file;\n console.error(` ${loc}`);\n console.error(` ${err.message}`);\n console.error('');\n }\n return 1;\n}\n"]}
1
+ {"version":3,"file":"validate.js","sourceRoot":"/","sources":["cli/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAwB,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAO9E,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAEzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,MAAM,yBAAyB,CAAC,CAAC;IACjE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;QAC5D,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["import { FileStore } from '../../repo/common/FileStore.js';\nimport { type ValidationScope, validateAll } from '../../validators/index.js';\n\nexport interface ValidateCliOptions {\n dataDir: string;\n scope?: ValidationScope[];\n}\n\nexport function runValidate(opts: ValidateCliOptions): number {\n const store = new FileStore(opts.dataDir);\n const errors = validateAll(store, { scope: opts.scope });\n\n if (errors.length === 0) {\n console.log('All data files are valid.');\n return 0;\n }\n\n console.error(`\\nFound ${errors.length} validation error(s):\\n`);\n for (const err of errors) {\n const loc = err.line ? `${err.file}:${err.line}` : err.file;\n console.error(` ${loc}`);\n console.error(` ${err.message}`);\n console.error('');\n }\n return 1;\n}\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":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,MAAM,QAAQ,GAAwC;IACpD,yBAAyB,EAAE,GAAG;IAC9B,qBAAqB,EAAE,EAAE;IACzB,0BAA0B,EAAE,EAAE,GAAG,EAAE;CACpC,CAAC;AAiKF,SAAS,aAAa,CAAC,IAOtB;IACC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnE,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACtE,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,yBAAyB,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAErE,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACzB,OAAO;YACL,GAAG,MAAM;YACT,aAAa,EAAE,MAAM,CAAC,KAAK;YAC3B,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;SAC5C,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO;YACL,GAAG,MAAM;YACT,aAAa,EAAE,MAAM,CAAC,KAAK;YAC3B,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;IAED,MAAM,kBAAkB,GAAG,cAAc;QACvC,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,oBAAoB,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE;gBAC5D,OAAO,EAAE,IAAI;aACd,CAAC,CAAC;YACH,MAAM,CACJ,oBAAoB,CAAC,OAAO,EAC5B,yBAAyB,cAAc,EAAE,CAC1C,CAAC;YACF,OAAO,oBAAoB,CAAC,IAAI,CAAC;gBAC/B,OAAO,EAAE,MAAM,CAAC,yBAAyB;aAC1C,CAAC,CAAC;QACL,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,IAAI,CAAC;IAET,IAAI,aAAa,GAAoB,IAAI,CAAC;IAC1C,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACpE,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,yBAAyB,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC3E,CAAC;aAAM,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YAClD,MAAM,kBAAkB,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE;gBAC9D,OAAO,EAAE,IAAI;aACd,CAAC,CAAC;YACH,MAAM,CACJ,kBAAkB,CAAC,OAAO,EAC1B,yBAAyB,KAAK,CAAC,YAAY,EAAE,CAC9C,CAAC;YACF,aAAa,GAAG,kBAAkB,CAAC,IAAI,CAAC;gBACtC,OAAO,EAAE,MAAM,CAAC,qBAAqB;aACtC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,iBAAiB,GAAG,aAAa,IAAI,kBAAkB,CAAC;IAC9D,MAAM,cAAc,GAClB,aAAa;QACX,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,kBAAkB;YAClB,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,SAAS,CAAC;IAElB,IAAI,CAAC,iBAAiB,IAAI,CAAC,cAAc,EAAE,CAAC;QAC1C,OAAO;YACL,GAAG,MAAM;YACT,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC;QACpC,OAAO,EAAE,MAAM,CAAC,0BAA0B;KAC3C,CAAC,CAAC;IAEH,+EAA+E;IAC/E,IAAI,WAAW,GAAG,iBAAiB;SAChC,OAAO,CAAC,gBAAgB,CAAC;SACzB,OAAO,CAAC,KAAK,CAAC;SACd,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAErB,+CAA+C;IAC/C,IAAI,WAAW,GAAG,SAAS,EAAE,CAAC;QAC5B,WAAW,GAAG,SAAS,CAAC;IAC1B,CAAC;IACD,gEAAgE;IAChE,IAAI,WAAW,GAAG,cAAc,EAAE,CAAC;QACjC,WAAW,GAAG,cAAc,CAAC;IAC/B,CAAC;IAED,0EAA0E;IAC1E,IAAI,WAAW,GAAG,IAAI,EAAE,CAAC;QACvB,OAAO;YACL,GAAG,MAAM;YACT,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,MAAM;SACpB,CAAC;IACJ,CAAC;IAED,OAAO;QACL,GAAG,MAAM;QACT,aAAa,EAAE,WAAW,CAAC,KAAK,EAAE;QAClC,WAAW,EAAE,UAAU;QACvB,WAAW,EAAE,cAAc;KAC5B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,MAAM,UAAU,cAAc,CAC5B,MAA4B;IAE5B,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IACvC,MAAM,cAAc,GAClB,IAAI,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IAErE,MAAM,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;QACnD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,OAAO;gBACV,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,KAAK,WAAW;gBACd,OAAO,wBAAwB,CAAC,MAAM,CAAC,CAAC;YAC1C;gBACE,yEAAyE;gBACzE,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG;QACtB,yBAAyB,EACvB,MAAM,EAAE,yBAAyB,IAAI,QAAQ,CAAC,yBAAyB;QACzE,qBAAqB,EACnB,MAAM,EAAE,qBAAqB,IAAI,QAAQ,CAAC,qBAAqB;QACjE,0BAA0B,EACxB,MAAM,EAAE,0BAA0B,IAAI,QAAQ,CAAC,0BAA0B;KAC5E,CAAC;IACF,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,yBAAyB,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,CAAC,GAAG,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,yBAAyB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,yBAAyB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,OAAO,MAAM,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC3B,aAAa,CAAC;QACZ,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE;QACrB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,MAAM;QACZ,cAAc;QACd,KAAK;QACL,MAAM,EAAE,eAAe;KACxB,CAAC,CACH,CAAC;AACJ,CAAC","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';