@heripo/research-radar 5.2.1 → 5.2.3

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.
package/dist/index.js CHANGED
@@ -317,6 +317,17 @@ function toIsoDate$2(compact) {
317
317
  ? `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`
318
318
  : '';
319
319
  }
320
+ /** `resultCode` this service returns on success. */
321
+ const SUCCESS_RESULT_CODE$2 = 200;
322
+ function parseResultCode(body) {
323
+ try {
324
+ const parsed = JSON.parse(body);
325
+ return typeof parsed.resultCode === 'number' ? parsed.resultCode : null;
326
+ }
327
+ catch {
328
+ return null;
329
+ }
330
+ }
320
331
  /**
321
332
  * Serves the 알리오 target from the 재정경제부 open API.
322
333
  *
@@ -328,7 +339,7 @@ function toIsoDate$2(compact) {
328
339
  * Only postings still open (`ongoingYn=Y`) are requested.
329
340
  */
330
341
  const createAlioFetch = (baseFetch = fetch, options) => {
331
- const { apiKey, rowsPerPage = 3000 } = options;
342
+ const { apiKey, rowsPerPage = 3000, onError } = options;
332
343
  return async (input, init) => {
333
344
  const requestUrl = typeof input === 'string'
334
345
  ? input
@@ -350,8 +361,25 @@ const createAlioFetch = (baseFetch = fetch, options) => {
350
361
  return Response.json({ result: [] });
351
362
  }
352
363
  if (url.pathname === '/recruit.do') {
353
- return baseFetch(`${API_BASE$2}/list?serviceKey=${apiKey}&resultType=json` +
364
+ const response = await baseFetch(`${API_BASE$2}/list?serviceKey=${apiKey}&resultType=json` +
354
365
  `&numOfRows=${rowsPerPage}&pageNo=1&ongoingYn=Y`, init);
366
+ if (!response.ok) {
367
+ return response;
368
+ }
369
+ // Same reasoning as the 나라일터 adapter: data.go.kr answers its own
370
+ // errors with HTTP 200 and a result code, and passing one through turns a
371
+ // rejected key or an outage into a board that simply has no postings.
372
+ const body = await response.text();
373
+ const resultCode = parseResultCode(body);
374
+ if (resultCode !== null && resultCode !== SUCCESS_RESULT_CODE$2) {
375
+ const reason = `알리오 list returned resultCode ${resultCode}`;
376
+ onError?.(reason);
377
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
378
+ }
379
+ return new Response(body, {
380
+ status: 200,
381
+ headers: { 'content-type': 'application/json' },
382
+ });
355
383
  }
356
384
  if (url.pathname === '/recruitview.do') {
357
385
  const serial = url.searchParams.get('idx');
@@ -798,7 +826,7 @@ const buildG2bDetailUrl = (notice, order) => `${SITE_BASE$1}/link/PNPE027_01/sin
798
826
  */
799
827
  const OPERATIONS = ['Servc', 'Cnstwk'];
800
828
  /** `resultCode` the service returns on success. */
801
- const SUCCESS_RESULT_CODE = '00';
829
+ const SUCCESS_RESULT_CODE$1 = '00';
802
830
  /** Offset the service's wall clock runs on. */
803
831
  const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
804
832
  /**
@@ -835,7 +863,11 @@ function jsonResponse(payload) {
835
863
  * daily quota of 1,000 calls is barely touched.
836
864
  */
837
865
  const createG2bFetch = (baseFetch = fetch, options) => {
838
- const { apiKey, windowHours = 48, rowsPerPage = 999, maxPages = 4, triage, } = options;
866
+ const { apiKey, windowHours = 48, rowsPerPage = 999, maxPages = 4, triage, onError, } = options;
867
+ const fail = (reason) => {
868
+ onError?.(reason);
869
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
870
+ };
839
871
  /** Notices from the most recent list call, keyed by `<bidNtceNo>:<bidNtceOrd>`. */
840
872
  const noticeCache = new Map();
841
873
  return async (input, init) => {
@@ -885,7 +917,7 @@ const createG2bFetch = (baseFetch = fetch, options) => {
885
917
  const body = (await response.json());
886
918
  const resultCode = body.response?.header?.resultCode;
887
919
  // data.go.kr answers its own errors with HTTP 200 and a result code.
888
- if (resultCode !== SUCCESS_RESULT_CODE) {
920
+ if (resultCode !== SUCCESS_RESULT_CODE$1) {
889
921
  return `나라장터 ${operation} list returned resultCode ${resultCode ?? 'none'}`;
890
922
  }
891
923
  const batch = body.response?.body?.items ?? [];
@@ -915,10 +947,7 @@ const createG2bFetch = (baseFetch = fetch, options) => {
915
947
  }));
916
948
  const failure = pages.find((result) => typeof result === 'string');
917
949
  if (typeof failure === 'string') {
918
- return new Response(failure, {
919
- status: 502,
920
- statusText: 'Bad Gateway',
921
- });
950
+ return fail(failure);
922
951
  }
923
952
  const collected = pages.flat();
924
953
  // Deduplicate before judging: the same notice arrives on more than one
@@ -1152,6 +1181,8 @@ function toIsoDate(compact) {
1152
1181
  ? `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`
1153
1182
  : '';
1154
1183
  }
1184
+ /** `resultCode` the service returns on success. */
1185
+ const SUCCESS_RESULT_CODE = '00';
1155
1186
  /**
1156
1187
  * Serves the 나라일터 target from the 인사혁신처 open API.
1157
1188
  *
@@ -1164,7 +1195,7 @@ function toIsoDate(compact) {
1164
1195
  * returns the announcement body directly — no HTML page is fetched.
1165
1196
  */
1166
1197
  const createGojobsFetch = (baseFetch = fetch, options) => {
1167
- const { apiKey, windowDays = 7, rowsPerPage = 3000 } = options;
1198
+ const { apiKey, windowDays = 7, rowsPerPage = 3000, onError } = options;
1168
1199
  return async (input, init) => {
1169
1200
  const requestUrl = typeof input === 'string'
1170
1201
  ? input
@@ -1192,8 +1223,29 @@ const createGojobsFetch = (baseFetch = fetch, options) => {
1192
1223
  if (url.pathname === '/apmList.do') {
1193
1224
  const end = new Date();
1194
1225
  const begin = new Date(end.getTime() - windowDays * 24 * 60 * 60 * 1000);
1195
- return baseFetch(`${API_BASE}/getList?serviceKey=${apiKey}&numOfRows=${rowsPerPage}` +
1226
+ const response = await baseFetch(`${API_BASE}/getList?serviceKey=${apiKey}&numOfRows=${rowsPerPage}` +
1196
1227
  `&pageNo=1&Begin_de=${toApiDate(begin)}&End_de=${toApiDate(end)}`, init);
1228
+ if (!response.ok) {
1229
+ return response;
1230
+ }
1231
+ // data.go.kr answers its own errors with HTTP 200 and a result code, so a
1232
+ // rejected key or a service outage arrives looking like a successful
1233
+ // response with no postings in it. Passing that through made the board
1234
+ // report zero vacancies and the run look healthy, which is how two days
1235
+ // of an empty 나라일터 went unnoticed. Fail instead, the same way the
1236
+ // 나라장터 adapter does.
1237
+ const xml = await response.text();
1238
+ const resultCode = /<resultCode>\s*([^<]*)<\/resultCode>/.exec(xml)?.[1];
1239
+ if (resultCode !== undefined &&
1240
+ resultCode.trim() !== SUCCESS_RESULT_CODE) {
1241
+ const reason = `나라일터 list returned resultCode ${resultCode.trim()}`;
1242
+ onError?.(reason);
1243
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
1244
+ }
1245
+ return new Response(xml, {
1246
+ status: 200,
1247
+ headers: { 'content-type': 'application/xml' },
1248
+ });
1197
1249
  }
1198
1250
  if (url.pathname === '/apmView.do') {
1199
1251
  const idx = url.searchParams.get('empmnsn');
@@ -2250,6 +2302,23 @@ const extractNttId = (html) => {
2250
2302
  }
2251
2303
  return '';
2252
2304
  };
2305
+ /**
2306
+ * Reads a JSON body without trusting that it is JSON.
2307
+ *
2308
+ * These parsers fetch their own API through the crawling fetch, which does not
2309
+ * always carry the API's answer: a request the origin's robots.txt disallows is
2310
+ * answered locally with an empty document, and `response.json()` throws on it.
2311
+ * A refusal should leave the target with nothing to report, not raise a parse
2312
+ * error that reads like a broken parser.
2313
+ */
2314
+ async function readJson(response) {
2315
+ try {
2316
+ return (await response.json());
2317
+ }
2318
+ catch {
2319
+ return null;
2320
+ }
2321
+ }
2253
2322
  /**
2254
2323
  * Parses the list page from seamuse.go.kr (국립해양유산연구소).
2255
2324
  * The site uses client-side rendering — list data is fetched from a JSON API.
@@ -2267,9 +2336,9 @@ const parseSeamuseList = async (_html, listDataPath, infoPathPrefix, customFetch
2267
2336
  },
2268
2337
  body,
2269
2338
  });
2270
- const data = await response.json();
2339
+ const data = await readJson(response);
2271
2340
  const posts = [];
2272
- for (const item of data.resultList) {
2341
+ for (const item of data?.resultList ?? []) {
2273
2342
  posts.push({
2274
2343
  uniqId: String(item.nttId),
2275
2344
  title: item.nttSj?.trim() ?? '',
@@ -2302,11 +2371,11 @@ const parseSeamuseDetail = async (html, infoDataPath, customFetch) => {
2302
2371
  'Content-Type': 'application/json',
2303
2372
  },
2304
2373
  });
2305
- const data = await response.json();
2306
- const content = data.result.nttCn ?? '';
2374
+ const data = await readJson(response);
2375
+ const content = data?.result?.nttCn ?? '';
2307
2376
  return {
2308
2377
  detailContent: new TurndownService().turndown(content),
2309
- hasAttachedFile: (data.files?.length ?? 0) > 0,
2378
+ hasAttachedFile: (data?.files?.length ?? 0) > 0,
2310
2379
  hasAttachedImage: content.includes('<img'),
2311
2380
  };
2312
2381
  };
@@ -3067,6 +3136,15 @@ const maximumImportanceScoreByDomain = {
3067
3136
  */
3068
3137
  const robotsExemptOrigins = [
3069
3138
  'http://www.yngogo.or.kr',
3139
+ // data.go.kr open APIs. robots.txt governs crawlers reading a site's
3140
+ // documents; these are authorised API calls made with a registered service
3141
+ // key, and the terms that bind them are the service's own. The host does not
3142
+ // publish a robots.txt at all: its gateway answers any unknown path — that
3143
+ // one included — with HTTP 400 and `NO_OPENAPI_SERVICE_ERROR`, so every run
3144
+ // was spending a request to be told the file does not exist. The adapters
3145
+ // rewrite 나라일터/알리오/나라장터 board URLs to this origin and the gate sits
3146
+ // inside them, which is how an API call ended up being asked about at all.
3147
+ 'https://apis.data.go.kr',
3070
3148
  ];
3071
3149
  /**
3072
3150
  * LLM configuration
@@ -4440,9 +4518,10 @@ class ContentGenerateProvider {
4440
4518
  *
4441
4519
  * Applied at the fetch layer, the same seam the KRAS and excavation-report
4442
4520
  * adapters use, so core's crawling pipeline is untouched: a disallowed request
4443
- * is answered with 403 instead of being sent. Core treats a 4xx list response as
4444
- * a failed fetch, logs `crawl.list.fetch.failed`, and continues with an empty
4445
- * page, so a blocked target yields no articles rather than breaking the run.
4521
+ * is answered locally with an empty document instead of being sent, and the
4522
+ * target simply yields no articles. The refusal is reported through
4523
+ * `onBlocked`, not as a fetch failure — see {@link createRobotsGate} for why
4524
+ * that distinction matters.
4446
4525
  */
4447
4526
  /**
4448
4527
  * Parses robots.txt into user-agent groups.
@@ -4648,13 +4727,31 @@ function createRobotsGate(baseFetch = fetch, options = {}) {
4648
4727
  return baseFetch(input, init);
4649
4728
  }
4650
4729
  onBlocked?.({ url: requestUrl, userAgent, rule: verdict.rule });
4651
- return new Response(`Blocked by robots.txt (${verdict.rule}) - ${new URL(requestUrl).origin}/robots.txt`, { status: 403, statusText: 'Blocked by robots.txt' });
4730
+ // An empty document rather than a 4xx. A site's crawling policy is not a
4731
+ // fault on our side, but core cannot tell the difference: it turns any
4732
+ // non-2xx list response into a thrown error and logs
4733
+ // `crawl.list.fetch.failed` at error level, which applications forward to
4734
+ // their alerting. Fourteen targets are disallowed at all times, so every
4735
+ // run raised fourteen alerts that no one could act on, and real fetch
4736
+ // failures were buried among them.
4737
+ //
4738
+ // Answering 200 with no content makes the target yield nothing, which is
4739
+ // what being disallowed means. The refusal is not hidden: it is still
4740
+ // reported through `onBlocked`, which the provider logs as
4741
+ // `crawl.robots.blocked`, and the health-check decides what to skip from
4742
+ // `isAllowed` rather than from this status. Every parser in this package
4743
+ // returns an empty list for this body.
4744
+ return new Response(`<!-- Blocked by robots.txt (${verdict.rule}) - ${new URL(requestUrl).origin}/robots.txt -->`, {
4745
+ status: 200,
4746
+ statusText: 'Blocked by robots.txt',
4747
+ headers: { 'content-type': 'text/html; charset=utf-8' },
4748
+ });
4652
4749
  };
4653
4750
  return { isAllowed, fetch: gatedFetch };
4654
4751
  }
4655
4752
  /**
4656
4753
  * Wraps a fetch so that requests disallowed by the origin's robots.txt are
4657
- * refused with 403 instead of sent. See {@link createRobotsGate}.
4754
+ * answered with an empty document instead of sent. See {@link createRobotsGate}.
4658
4755
  */
4659
4756
  function createRobotsAwareFetch(baseFetch = fetch, options = {}) {
4660
4757
  return createRobotsGate(baseFetch, options).fetch;
@@ -4693,7 +4790,32 @@ class CrawlingProvider {
4693
4790
  // The two public job boards are read from data.go.kr open APIs. Without a
4694
4791
  // key they answer with an empty list and make no request, so the targets
4695
4792
  // stay configured and simply collect nothing.
4696
- const withPublicJobs = createG2bFetch(createAlioFetch(createGojobsFetch(withKras, { apiKey: publicDataApiKey ?? '' }), { apiKey: publicDataApiKey ?? '' }), { apiKey: publicDataApiKey ?? '', triage: bidTriage });
4793
+ const withPublicJobs = createG2bFetch(createAlioFetch(createGojobsFetch(withKras, {
4794
+ apiKey: publicDataApiKey ?? '',
4795
+ onError: (reason) => {
4796
+ logger?.error({
4797
+ event: 'crawl.gojobs.list.failed',
4798
+ data: { reason },
4799
+ });
4800
+ },
4801
+ }), {
4802
+ apiKey: publicDataApiKey ?? '',
4803
+ onError: (reason) => {
4804
+ logger?.error({
4805
+ event: 'crawl.alio.list.failed',
4806
+ data: { reason },
4807
+ });
4808
+ },
4809
+ }), {
4810
+ apiKey: publicDataApiKey ?? '',
4811
+ triage: bidTriage,
4812
+ onError: (reason) => {
4813
+ logger?.error({
4814
+ event: 'crawl.g2b.list.failed',
4815
+ data: { reason },
4816
+ });
4817
+ },
4818
+ });
4697
4819
  // When the application supplies excavation reports, that board is served
4698
4820
  // from the injected source and never requested over the network. Every
4699
4821
  // other target keeps going through the same fetch as before.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@heripo/research-radar",
3
3
  "private": false,
4
4
  "type": "module",
5
- "version": "5.2.1",
5
+ "version": "5.2.3",
6
6
  "description": "AI-driven intelligence for Korean cultural heritage. This package serves as both a ready-to-use newsletter service and a practical implementation example for the LLM-Newsletter-Kit.",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",