@sayue_ltr/fleq 1.50.0 → 1.51.0

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 (92) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +47 -5
  3. package/dist/config.js +35 -2
  4. package/dist/dmdata/rest-client.js +58 -3
  5. package/dist/dmdata/telegram-parser.js +37 -59
  6. package/dist/dmdata/ws-client.js +49 -18
  7. package/dist/engine/cli/cli-run.js +71 -1
  8. package/dist/engine/cli/cli.js +12 -0
  9. package/dist/engine/filter/compile-filter.js +21 -0
  10. package/dist/engine/filter/compiler.js +188 -0
  11. package/dist/engine/filter/errors.js +41 -0
  12. package/dist/engine/filter/field-registry.js +78 -0
  13. package/dist/engine/filter/index.js +15 -0
  14. package/dist/engine/filter/parser.js +137 -0
  15. package/dist/engine/filter/rank-maps.js +34 -0
  16. package/dist/engine/filter/tokenizer.js +121 -0
  17. package/dist/engine/filter/type-checker.js +104 -0
  18. package/dist/engine/filter/types.js +2 -0
  19. package/dist/engine/filter-template/pipeline-controller.js +73 -0
  20. package/dist/engine/filter-template/pipeline.js +16 -0
  21. package/dist/engine/messages/display-callbacks.js +7 -0
  22. package/dist/engine/messages/message-router.js +114 -182
  23. package/dist/engine/messages/summary-tracker.js +106 -0
  24. package/dist/engine/messages/telegram-stats.js +103 -0
  25. package/dist/engine/messages/volcano-route-handler.js +122 -0
  26. package/dist/engine/monitor/monitor.js +51 -3
  27. package/dist/engine/monitor/shutdown.js +1 -0
  28. package/dist/engine/notification/notifier.js +16 -1
  29. package/dist/engine/notification/sound-player.js +193 -28
  30. package/dist/engine/presentation/diff-store.js +158 -0
  31. package/dist/engine/presentation/diff-types.js +2 -0
  32. package/dist/engine/presentation/events/from-earthquake.js +53 -0
  33. package/dist/engine/presentation/events/from-eew.js +72 -0
  34. package/dist/engine/presentation/events/from-lg-observation.js +58 -0
  35. package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
  36. package/dist/engine/presentation/events/from-raw.js +35 -0
  37. package/dist/engine/presentation/events/from-seismic-text.js +37 -0
  38. package/dist/engine/presentation/events/from-tsunami.js +51 -0
  39. package/dist/engine/presentation/events/from-volcano.js +88 -0
  40. package/dist/engine/presentation/events/to-presentation-event.js +32 -0
  41. package/dist/engine/presentation/level-helpers.js +118 -0
  42. package/dist/engine/presentation/processors/process-earthquake.js +36 -0
  43. package/dist/engine/presentation/processors/process-eew.js +90 -0
  44. package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
  45. package/dist/engine/presentation/processors/process-message.js +53 -0
  46. package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
  47. package/dist/engine/presentation/processors/process-raw.js +22 -0
  48. package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
  49. package/dist/engine/presentation/processors/process-tsunami.js +42 -0
  50. package/dist/engine/presentation/processors/process-volcano.js +41 -0
  51. package/dist/engine/presentation/types.js +2 -0
  52. package/dist/engine/startup/config-resolver.js +2 -0
  53. package/dist/engine/template/compile-template.js +18 -0
  54. package/dist/engine/template/compiler.js +102 -0
  55. package/dist/engine/template/field-accessor.js +25 -0
  56. package/dist/engine/template/filters.js +94 -0
  57. package/dist/engine/template/index.js +5 -0
  58. package/dist/engine/template/parser.js +190 -0
  59. package/dist/engine/template/tokenizer.js +96 -0
  60. package/dist/engine/template/types.js +2 -0
  61. package/dist/types.js +2 -1
  62. package/dist/ui/display-adapter.js +60 -0
  63. package/dist/ui/earthquake-formatter.js +17 -5
  64. package/dist/ui/eew-formatter.js +25 -10
  65. package/dist/ui/formatter.js +67 -32
  66. package/dist/ui/minimap/grid-layout.js +91 -0
  67. package/dist/ui/minimap/index.js +16 -0
  68. package/dist/ui/minimap/minimap-renderer.js +277 -0
  69. package/dist/ui/minimap/pref-mapping.js +82 -0
  70. package/dist/ui/minimap/types.js +2 -0
  71. package/dist/ui/night-overlay.js +56 -0
  72. package/dist/ui/repl-handlers/command-definitions.js +320 -0
  73. package/dist/ui/repl-handlers/index.js +11 -0
  74. package/dist/ui/repl-handlers/info-handlers.js +577 -0
  75. package/dist/ui/repl-handlers/operation-handlers.js +233 -0
  76. package/dist/ui/repl-handlers/settings-handlers.js +923 -0
  77. package/dist/ui/repl-handlers/types.js +10 -0
  78. package/dist/ui/repl.js +81 -1752
  79. package/dist/ui/statistics-formatter.js +208 -0
  80. package/dist/ui/status-line.js +69 -0
  81. package/dist/ui/summary/index.js +5 -0
  82. package/dist/ui/summary/summary-line.js +18 -0
  83. package/dist/ui/summary/summary-model.js +31 -0
  84. package/dist/ui/summary/token-builders.js +317 -0
  85. package/dist/ui/summary/types.js +2 -0
  86. package/dist/ui/summary/width-fit.js +41 -0
  87. package/dist/ui/summary-interval-formatter.js +72 -0
  88. package/dist/ui/theme.js +34 -5
  89. package/dist/ui/tip-shuffler.js +81 -0
  90. package/dist/ui/volcano-formatter.js +15 -13
  91. package/dist/ui/waiting-tips.js +289 -249
  92. package/package.json +1 -1
@@ -90,6 +90,8 @@ class WebSocketManager {
90
90
  /** 接続を開始する */
91
91
  async connect() {
92
92
  this.shouldRun = true;
93
+ // 既存の再接続タイマーと CONNECTING 中のソケットを中止してから新規接続する
94
+ this.cancelInflight();
93
95
  const seq = ++this.connectSeq;
94
96
  await this.doConnect(seq);
95
97
  }
@@ -123,13 +125,46 @@ class WebSocketManager {
123
125
  this.heartbeatTimer = null;
124
126
  }
125
127
  }
128
+ /** 再接続タイマーと CONNECTING 中のソケットを中止する */
129
+ cancelInflight() {
130
+ if (this.reconnectTimer) {
131
+ clearTimeout(this.reconnectTimer);
132
+ this.reconnectTimer = null;
133
+ }
134
+ // CONNECTING 中のソケットがあれば閉じて孤立を防止
135
+ if (this.ws && this.ws.readyState !== ws_1.default.OPEN) {
136
+ try {
137
+ this.ws.close();
138
+ }
139
+ catch {
140
+ // close() 自体の失敗は無視
141
+ }
142
+ this.ws = null;
143
+ }
144
+ }
145
+ /** close/error 共通の切断後処理 */
146
+ onDisconnect(reason) {
147
+ this.clearTimers();
148
+ this.ws = null;
149
+ this.previousSocketId = this.socketId;
150
+ this.socketId = null;
151
+ this.heartbeatDeadlineAt = null;
152
+ this.endpointSelector.recordDisconnected();
153
+ this.events.onDisconnected(reason);
154
+ this.scheduleReconnect();
155
+ }
126
156
  async doConnect(seq) {
127
157
  try {
158
+ // REST API 呼び出し前に世代チェック — 古い再接続タイマーやシャットダウン後の呼び出しを弾く
159
+ if (!this.shouldRun || seq !== this.connectSeq) {
160
+ log.debug("接続中断(pre-API): shouldRun=false または世代不一致");
161
+ return;
162
+ }
128
163
  log.info("Socket Start を実行中...");
129
164
  const startRes = await (0, rest_client_1.prepareAndStartSocket)(this.config, this.previousSocketId ?? undefined);
130
- // close() が呼ばれていたら接続を中断
165
+ // REST API 完了後に再度世代チェック — API 呼び出し中に close() や新しい connect() が来た場合を検出
131
166
  if (!this.shouldRun || seq !== this.connectSeq) {
132
- log.debug("接続中断: close() が呼ばれたため新しいソケットを作成しません");
167
+ log.debug("接続中断(post-API): close() または新しい connect() が呼ばれたため新しいソケットを作成しません");
133
168
  return;
134
169
  }
135
170
  if (!startRes.websocket) {
@@ -137,6 +172,11 @@ class WebSocketManager {
137
172
  }
138
173
  const wsUrl = this.endpointSelector.resolveUrl(startRes.websocket.url);
139
174
  log.info(`WebSocket に接続中: ${wsUrl.replace(/ticket=.*/, "ticket=***")}`);
175
+ // WebSocket 作成前に最終チェック
176
+ if (!this.shouldRun || seq !== this.connectSeq) {
177
+ log.debug("接続中断(pre-WS): close() または新しい connect() が呼ばれたため WebSocket を作成しません");
178
+ return;
179
+ }
140
180
  const socket = new ws_1.default(wsUrl, ["dmdata.v2"]);
141
181
  this.ws = socket;
142
182
  socket.on("open", () => {
@@ -161,14 +201,7 @@ class WebSocketManager {
161
201
  return;
162
202
  const reasonStr = reason.toString() || `code=${code}`;
163
203
  log.warn(`WebSocket 切断: ${reasonStr}`);
164
- this.clearTimers();
165
- this.ws = null;
166
- this.previousSocketId = this.socketId;
167
- this.socketId = null;
168
- this.heartbeatDeadlineAt = null;
169
- this.endpointSelector.recordDisconnected();
170
- this.events.onDisconnected(reasonStr);
171
- this.scheduleReconnect();
204
+ this.onDisconnect(reasonStr);
172
205
  });
173
206
  socket.on("error", (err) => {
174
207
  log.error(`WebSocket エラー: ${err.message}`);
@@ -181,14 +214,7 @@ class WebSocketManager {
181
214
  catch {
182
215
  // close() 自体の失敗は無視
183
216
  }
184
- this.clearTimers();
185
- this.ws = null;
186
- this.previousSocketId = this.socketId;
187
- this.socketId = null;
188
- this.heartbeatDeadlineAt = null;
189
- this.endpointSelector.recordDisconnected();
190
- this.events.onDisconnected(`error: ${err.message}`);
191
- this.scheduleReconnect();
217
+ this.onDisconnect(`error: ${err.message}`);
192
218
  });
193
219
  }
194
220
  catch (err) {
@@ -329,6 +355,11 @@ class WebSocketManager {
329
355
  const currentSeq = this.connectSeq;
330
356
  this.reconnectTimer = setTimeout(async () => {
331
357
  this.reconnectTimer = null;
358
+ // タイマー発火時に世代が変わっていたら(手動 retry や close() があった)何もしない
359
+ if (!this.shouldRun || currentSeq !== this.connectSeq) {
360
+ log.debug("再接続タイマー発火をスキップ: shouldRun=false または世代不一致");
361
+ return;
362
+ }
332
363
  await this.doConnect(currentSeq);
333
364
  }, delay);
334
365
  }
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.runMonitor = runMonitor;
40
40
  exports.resetTerminalTitle = resetTerminalTitle;
41
41
  const chalk_1 = __importDefault(require("chalk"));
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
42
44
  const rest_client_1 = require("../../dmdata/rest-client");
43
45
  const monitor_1 = require("../monitor/monitor");
44
46
  const formatter_1 = require("../../ui/formatter");
@@ -46,6 +48,7 @@ const theme_1 = require("../../ui/theme");
46
48
  const config_resolver_1 = require("../startup/config-resolver");
47
49
  const updateChecker = __importStar(require("../startup/update-checker"));
48
50
  const log = __importStar(require("../../logger"));
51
+ const pipeline_controller_1 = require("../filter-template/pipeline-controller");
49
52
  // eslint-disable-next-line @typescript-eslint/no-var-requires
50
53
  const { version: VERSION } = require("../../../package.json");
51
54
  async function runMonitor(opts) {
@@ -84,6 +87,11 @@ async function runMonitor(opts) {
84
87
  for (const w of themeWarnings) {
85
88
  log.warn(w);
86
89
  }
90
+ // ナイトモード (resolveConfig で解決済み: CLI --night > Config > デフォルト)
91
+ if (config.nightMode) {
92
+ (0, theme_1.setNightMode)(true);
93
+ log.info("ナイトモード: ON");
94
+ }
87
95
  // formatter キャッシュ初期化
88
96
  if (config.tableWidth != null) {
89
97
  (0, formatter_1.setFrameWidth)(config.tableWidth);
@@ -92,9 +100,71 @@ async function runMonitor(opts) {
92
100
  (0, formatter_1.setDisplayMode)(config.displayMode);
93
101
  (0, formatter_1.setMaxObservations)(config.maxObservations);
94
102
  (0, formatter_1.setTruncation)(config.truncation);
103
+ // Filter / Template コンパイル
104
+ const pipelineController = new pipeline_controller_1.PipelineController();
105
+ if (opts.filter && opts.filter.length > 0) {
106
+ try {
107
+ // 複数フィルタは括弧付きで AND 結合
108
+ const combined = opts.filter.map((e) => `(${e})`).join(" and ");
109
+ pipelineController.setFilter(combined);
110
+ log.info(`フィルタ: ${opts.filter.join(" AND ")}`);
111
+ }
112
+ catch (err) {
113
+ if (err instanceof Error) {
114
+ log.error(`フィルタのコンパイルに失敗しました:\n${err.message}`);
115
+ }
116
+ process.exit(1);
117
+ }
118
+ }
119
+ if (opts.template) {
120
+ try {
121
+ let tplSource = opts.template;
122
+ if (tplSource.startsWith("@")) {
123
+ const filePath = tplSource.slice(1).replace(/^~/, os.homedir());
124
+ const MAX_TEMPLATE_SIZE = 1024 * 1024; // 1MB
125
+ const stat = fs.statSync(filePath);
126
+ if (stat.size > MAX_TEMPLATE_SIZE) {
127
+ log.error(`テンプレートファイルが大きすぎます (${stat.size} bytes, 上限 ${MAX_TEMPLATE_SIZE} bytes): ${filePath}`);
128
+ process.exit(1);
129
+ }
130
+ tplSource = fs.readFileSync(filePath, "utf-8").trim();
131
+ }
132
+ pipelineController.setTemplate(tplSource);
133
+ log.info("テンプレート: カスタム");
134
+ }
135
+ catch (err) {
136
+ if (err instanceof Error) {
137
+ log.warn(`テンプレートのコンパイルに失敗しました:\n${err.message}`);
138
+ }
139
+ // template エラーは警告のみ — 通常表示にフォールバック
140
+ }
141
+ }
142
+ if (opts.focus) {
143
+ try {
144
+ pipelineController.setFocus(opts.focus);
145
+ log.info(`フォーカス: ${opts.focus}`);
146
+ }
147
+ catch (err) {
148
+ if (err instanceof Error) {
149
+ log.error(`フォーカスのコンパイルに失敗しました:\n${err.message}`);
150
+ }
151
+ process.exit(1);
152
+ }
153
+ }
154
+ // summaryInterval (CLI > Config > デフォルト, 0 = 無効化)
155
+ if (opts.summaryInterval != null) {
156
+ if (opts.summaryInterval === 0) {
157
+ config.summaryInterval = null;
158
+ log.info("定期要約: 無効");
159
+ }
160
+ else {
161
+ config.summaryInterval = opts.summaryInterval;
162
+ log.info(`定期要約: ${opts.summaryInterval}分間隔`);
163
+ }
164
+ }
95
165
  printBanner(config);
96
166
  updateChecker.checkForUpdates("fleq", VERSION);
97
- await (0, monitor_1.startMonitor)(config);
167
+ await (0, monitor_1.startMonitor)(config, pipelineController);
98
168
  }
99
169
  /** ターミナルタイトルを設定する (ANSI OSC sequence) */
100
170
  function setTerminalTitle(title) {
@@ -51,6 +51,18 @@ function buildProgram() {
51
51
  .option("--keep-existing", "既存のWebSocket接続を維持します(互換オプション。現在はこちらがデフォルトです)")
52
52
  .option("--close-others", "同一APIキーの既存 open socket を閉じてから接続します")
53
53
  .option("--mode <mode>", '表示モードを指定します: "normal" | "compact"')
54
+ .option("--filter <expr>", "条件式で電文を絞り込みます (複数指定で AND 結合)", (val, prev) => [...prev, val], [])
55
+ .option("--template <template>", "電文の1行要約テンプレートを指定します (@ でファイル読込)")
56
+ .option("--focus <expr>", "条件に一致しない電文を dim 表示に落とします")
57
+ .option("--summary-interval [minutes]", "N分ごとに受信要約を表示 (デフォルト10分, 0で無効化)", (val) => {
58
+ if (val === undefined || val === "true")
59
+ return 10;
60
+ const n = parseInt(val, 10);
61
+ if (n === 0)
62
+ return 0;
63
+ return Number.isFinite(n) && n > 0 ? n : 10;
64
+ })
65
+ .option("--night", "ナイトモードを有効にします")
54
66
  .option("--debug", "デバッグログを表示します", false)
55
67
  .action(async (opts) => {
56
68
  const { runMonitor } = await Promise.resolve().then(() => __importStar(require("./cli-run")));
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileFilter = compileFilter;
4
+ const tokenizer_1 = require("./tokenizer");
5
+ const parser_1 = require("./parser");
6
+ const type_checker_1 = require("./type-checker");
7
+ const compiler_1 = require("./compiler");
8
+ /**
9
+ * フィルタ式文字列を受け取り、tokenize → parse → typeCheck → compile の
10
+ * パイプラインを通して FilterPredicate を返す公開 API。
11
+ *
12
+ * @throws FilterSyntaxError — 構文エラー
13
+ * @throws FilterFieldError — 未知フィールド
14
+ * @throws FilterTypeError — 型不整合
15
+ */
16
+ function compileFilter(expr) {
17
+ const tokens = (0, tokenizer_1.tokenize)(expr);
18
+ const ast = (0, parser_1.parse)(tokens, expr);
19
+ (0, type_checker_1.typeCheck)(ast, expr);
20
+ return (0, compiler_1.compile)(ast);
21
+ }
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compile = compile;
4
+ const field_registry_1 = require("./field-registry");
5
+ const rank_maps_1 = require("./rank-maps");
6
+ function compile(ast) {
7
+ switch (ast.kind) {
8
+ case "or":
9
+ return compileOr(ast.children.map(compile));
10
+ case "and":
11
+ return compileAnd(ast.children.map(compile));
12
+ case "not":
13
+ return compileNot(compile(ast.operand));
14
+ case "truthy":
15
+ return compileTruthy(ast.value);
16
+ case "comparison":
17
+ return compileComparison(ast.left, ast.op, ast.right);
18
+ }
19
+ }
20
+ function compileOr(predicates) {
21
+ return (event) => predicates.some((p) => p(event));
22
+ }
23
+ function compileAnd(predicates) {
24
+ return (event) => predicates.every((p) => p(event));
25
+ }
26
+ function compileNot(predicate) {
27
+ return (event) => !predicate(event);
28
+ }
29
+ function compileTruthy(node) {
30
+ const getter = makeGetter(node);
31
+ return (event) => {
32
+ const val = getter(event);
33
+ return val != null && val !== false && val !== "" && val !== 0;
34
+ };
35
+ }
36
+ function compileComparison(left, op, right) {
37
+ const getLeft = makeGetter(left);
38
+ const getRight = makeGetter(right);
39
+ // フィールドの型情報を取得 (enum ランク変換用)
40
+ const leftField = left.kind === "path" ? (0, field_registry_1.resolveField)(left.segments.join(".")) : null;
41
+ const rankFn = leftField ? getRankFn(leftField.kind) : null;
42
+ switch (op) {
43
+ case "=":
44
+ return (event) => {
45
+ const l = getLeft(event);
46
+ const r = getRight(event);
47
+ if (l == null || r == null)
48
+ return false;
49
+ return l === r;
50
+ };
51
+ case "!=":
52
+ return (event) => {
53
+ const l = getLeft(event);
54
+ const r = getRight(event);
55
+ if (l == null || r == null)
56
+ return false;
57
+ return l !== r;
58
+ };
59
+ case "<":
60
+ case "<=":
61
+ case ">":
62
+ case ">=":
63
+ return (event) => {
64
+ let l = getLeft(event);
65
+ let r = getRight(event);
66
+ if (l == null || r == null)
67
+ return false;
68
+ if (rankFn != null) {
69
+ l = rankFn(String(l));
70
+ r = rankFn(String(r));
71
+ if (l == null || r == null)
72
+ return false;
73
+ }
74
+ return compareOrdered(l, op, r);
75
+ };
76
+ case "~": {
77
+ // 右辺が文字列リテラルならコンパイル時に RegExp をキャッシュ
78
+ const cachedRegex = right.kind === "string" ? new RegExp(right.value) : null;
79
+ return (event) => {
80
+ const l = getLeft(event);
81
+ if (l == null)
82
+ return false;
83
+ const re = cachedRegex ?? (() => {
84
+ const r = getRight(event);
85
+ if (r == null)
86
+ return null;
87
+ try {
88
+ return new RegExp(String(r));
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ })();
94
+ if (re == null)
95
+ return false;
96
+ return re.test(String(l));
97
+ };
98
+ }
99
+ case "!~": {
100
+ const cachedRegexNeg = right.kind === "string" ? new RegExp(right.value) : null;
101
+ return (event) => {
102
+ const l = getLeft(event);
103
+ if (l == null)
104
+ return false;
105
+ const re = cachedRegexNeg ?? (() => {
106
+ const r = getRight(event);
107
+ if (r == null)
108
+ return null;
109
+ try {
110
+ return new RegExp(String(r));
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ })();
116
+ if (re == null)
117
+ return false;
118
+ return !re.test(String(l));
119
+ };
120
+ }
121
+ case "in":
122
+ return (event) => {
123
+ const l = getLeft(event);
124
+ const r = getRight(event);
125
+ if (l == null || r == null)
126
+ return false;
127
+ if (Array.isArray(r))
128
+ return r.includes(l);
129
+ return false;
130
+ };
131
+ case "contains":
132
+ return (event) => {
133
+ const l = getLeft(event);
134
+ const r = getRight(event);
135
+ if (l == null || r == null)
136
+ return false;
137
+ if (Array.isArray(l))
138
+ return l.includes(r);
139
+ if (typeof l === "string" && typeof r === "string")
140
+ return l.includes(r);
141
+ return false;
142
+ };
143
+ }
144
+ }
145
+ function makeGetter(node) {
146
+ switch (node.kind) {
147
+ case "path": {
148
+ const field = (0, field_registry_1.resolveField)(node.segments.join("."));
149
+ if (field == null)
150
+ return () => null;
151
+ return (event) => field.get(event);
152
+ }
153
+ case "string":
154
+ return () => node.value;
155
+ case "number":
156
+ return () => node.value;
157
+ case "boolean":
158
+ return () => node.value;
159
+ case "null":
160
+ return () => null;
161
+ case "list":
162
+ return () => node.items.map((item) => {
163
+ switch (item.kind) {
164
+ case "string": return item.value;
165
+ case "number": return item.value;
166
+ case "boolean": return item.value;
167
+ default: return null;
168
+ }
169
+ });
170
+ }
171
+ }
172
+ function compareOrdered(l, op, r) {
173
+ switch (op) {
174
+ case "<": return l < r;
175
+ case "<=": return l <= r;
176
+ case ">": return l > r;
177
+ case ">=": return l >= r;
178
+ default: return false;
179
+ }
180
+ }
181
+ function getRankFn(kind) {
182
+ switch (kind) {
183
+ case "enum:frameLevel": return rank_maps_1.toFrameLevelRank;
184
+ case "enum:intensity": return rank_maps_1.toIntensityRank;
185
+ case "enum:lgInt": return rank_maps_1.toLgIntRank;
186
+ default: return null;
187
+ }
188
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilterFieldError = exports.FilterTypeError = exports.FilterSyntaxError = void 0;
4
+ class FilterSyntaxError extends Error {
5
+ source;
6
+ position;
7
+ constructor(source, position, message) {
8
+ super(message);
9
+ this.source = source;
10
+ this.position = position;
11
+ this.name = "FilterSyntaxError";
12
+ }
13
+ /** 位置付きフォーマット済みエラー表示 */
14
+ format() {
15
+ const pointer = " ".repeat(this.position) + "^";
16
+ return `フィルタ構文エラー: ${this.position + 1}文字目${this.message}\n${this.source}\n${pointer}`;
17
+ }
18
+ }
19
+ exports.FilterSyntaxError = FilterSyntaxError;
20
+ class FilterTypeError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = "FilterTypeError";
24
+ }
25
+ }
26
+ exports.FilterTypeError = FilterTypeError;
27
+ class FilterFieldError extends Error {
28
+ fieldName;
29
+ availableFields;
30
+ constructor(fieldName, availableFields) {
31
+ super(`未知のフィールド: ${fieldName}`);
32
+ this.fieldName = fieldName;
33
+ this.availableFields = availableFields;
34
+ this.name = "FilterFieldError";
35
+ }
36
+ format() {
37
+ const examples = this.availableFields.slice(0, 6).join(", ");
38
+ return `未知のフィールド: ${this.fieldName}\n使える例: ${examples}`;
39
+ }
40
+ }
41
+ exports.FilterFieldError = FilterFieldError;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILTER_FIELDS = void 0;
4
+ exports.resolveField = resolveField;
5
+ exports.fieldNames = fieldNames;
6
+ function field(kind, aliases, get, supportsOrder) {
7
+ return { kind, aliases, get, supportsOrder };
8
+ }
9
+ /** depth 文字列 "10km" → 数値 10 */
10
+ function parseDepth(d) {
11
+ if (d == null)
12
+ return null;
13
+ const m = d.match(/^(\d+)/);
14
+ return m ? Number(m[1]) : null;
15
+ }
16
+ /** magnitude 文字列 → 数値 */
17
+ function parseMagnitude(m) {
18
+ if (m == null)
19
+ return null;
20
+ const n = Number(m);
21
+ return Number.isFinite(n) ? n : null;
22
+ }
23
+ exports.FILTER_FIELDS = {
24
+ // 識別
25
+ domain: field("string", [], (e) => e.domain),
26
+ type: field("string", ["headType"], (e) => e.type),
27
+ subType: field("string", [], (e) => e.subType),
28
+ classification: field("string", [], (e) => e.classification),
29
+ id: field("string", [], (e) => e.id),
30
+ infoType: field("string", [], (e) => e.infoType),
31
+ // レベル
32
+ frameLevel: field("enum:frameLevel", ["level"], (e) => e.frameLevel, true),
33
+ // 状態フラグ
34
+ isCancellation: field("boolean", ["isCancelled"], (e) => e.isCancellation),
35
+ isWarning: field("boolean", [], (e) => e.isWarning),
36
+ isFinal: field("boolean", [], (e) => e.isFinal),
37
+ isTest: field("boolean", [], (e) => e.isTest),
38
+ isRenotification: field("boolean", [], (e) => e.isRenotification),
39
+ // イベント追跡
40
+ eventId: field("string", [], (e) => e.eventId),
41
+ serial: field("string", [], (e) => e.serial),
42
+ volcanoCode: field("string", [], (e) => e.volcanoCode),
43
+ volcanoName: field("string", [], (e) => e.volcanoName),
44
+ // 震源情報
45
+ hypocenterName: field("string", ["hypocenter"], (e) => e.hypocenterName),
46
+ depth: field("number", [], (e) => parseDepth(e.depth), true),
47
+ magnitude: field("number", ["mag"], (e) => parseMagnitude(e.magnitude), true),
48
+ // 強度
49
+ maxInt: field("enum:intensity", [], (e) => e.maxInt, true),
50
+ maxLgInt: field("enum:lgInt", [], (e) => e.maxLgInt, true),
51
+ forecastMaxInt: field("enum:intensity", [], (e) => e.forecastMaxInt, true),
52
+ alertLevel: field("number", [], (e) => e.alertLevel, true),
53
+ // テキスト
54
+ title: field("string", [], (e) => e.title),
55
+ headline: field("string", [], (e) => e.headline),
56
+ // 地域集約
57
+ areaNames: field("string[]", [], (e) => e.areaNames),
58
+ forecastAreaNames: field("string[]", [], (e) => e.forecastAreaNames),
59
+ municipalityNames: field("string[]", [], (e) => e.municipalityNames),
60
+ observationNames: field("string[]", [], (e) => e.observationNames),
61
+ areaCount: field("number", [], (e) => e.areaCount),
62
+ // 津波
63
+ tsunamiKinds: field("string[]", [], (e) => e.tsunamiKinds),
64
+ };
65
+ /** フィールド名 or エイリアスから FilterField を解決する */
66
+ function resolveField(name) {
67
+ if (name in exports.FILTER_FIELDS)
68
+ return exports.FILTER_FIELDS[name];
69
+ for (const [, f] of Object.entries(exports.FILTER_FIELDS)) {
70
+ if (f.aliases.includes(name))
71
+ return f;
72
+ }
73
+ return null;
74
+ }
75
+ /** 公開フィールド名一覧 (エラーメッセージ用) */
76
+ function fieldNames() {
77
+ return Object.keys(exports.FILTER_FIELDS);
78
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fieldNames = exports.resolveField = exports.FilterFieldError = exports.FilterTypeError = exports.FilterSyntaxError = exports.compileFilter = void 0;
4
+ // 公開 API
5
+ var compile_filter_1 = require("./compile-filter");
6
+ Object.defineProperty(exports, "compileFilter", { enumerable: true, get: function () { return compile_filter_1.compileFilter; } });
7
+ // エラー
8
+ var errors_1 = require("./errors");
9
+ Object.defineProperty(exports, "FilterSyntaxError", { enumerable: true, get: function () { return errors_1.FilterSyntaxError; } });
10
+ Object.defineProperty(exports, "FilterTypeError", { enumerable: true, get: function () { return errors_1.FilterTypeError; } });
11
+ Object.defineProperty(exports, "FilterFieldError", { enumerable: true, get: function () { return errors_1.FilterFieldError; } });
12
+ // フィールドユーティリティ
13
+ var field_registry_1 = require("./field-registry");
14
+ Object.defineProperty(exports, "resolveField", { enumerable: true, get: function () { return field_registry_1.resolveField; } });
15
+ Object.defineProperty(exports, "fieldNames", { enumerable: true, get: function () { return field_registry_1.fieldNames; } });