@heripo/research-radar 5.2.2 → 5.2.4

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
@@ -300,6 +300,36 @@ const createHeritageBidTriage = (options) => {
300
300
  };
301
301
  };
302
302
 
303
+ /**
304
+ * Reads what data.go.kr said when it refused a request.
305
+ *
306
+ * Its gateway answers a refusal with HTTP 400 and an XML envelope, whatever
307
+ * format the service itself speaks. The envelope names the cause — a spent
308
+ * daily quota, an unregistered key, a withdrawn service and a denied caller
309
+ * are all different `returnReasonCode`s — but an adapter that reports only the
310
+ * status throws that away.
311
+ *
312
+ * It cost a week to learn that. A nightly run failed at the same minute for
313
+ * days with nothing in the log but `Request failed (status=502)`, and the
314
+ * reason the gateway had supplied each time was discarded before anyone could
315
+ * read it.
316
+ */
317
+ function describeOpenDataError(body) {
318
+ const read = (tag) => new RegExp(`<${tag}>\\s*([^<]*)</${tag}>`).exec(body)?.[1]?.trim();
319
+ const parts = [
320
+ read('errMsg'),
321
+ read('returnReasonCode') && `code ${read('returnReasonCode')}`,
322
+ read('returnAuthMsg'),
323
+ ].filter((part) => Boolean(part));
324
+ if (parts.length > 0) {
325
+ return parts.join(' / ');
326
+ }
327
+ // Not the documented envelope. A short body is still worth quoting; a long
328
+ // one is a page rather than an error and only adds noise to the log.
329
+ const trimmed = body.trim().replace(/\s+/g, ' ');
330
+ return trimmed.length > 0 && trimmed.length <= 200 ? trimmed : null;
331
+ }
332
+
303
333
  const API_BASE$2 = 'https://apis.data.go.kr/1051000/recruitment';
304
334
  const SITE_BASE$2 = 'https://job.alio.go.kr';
305
335
  /**
@@ -317,6 +347,17 @@ function toIsoDate$2(compact) {
317
347
  ? `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`
318
348
  : '';
319
349
  }
350
+ /** `resultCode` this service returns on success. */
351
+ const SUCCESS_RESULT_CODE$2 = 200;
352
+ function parseResultCode(body) {
353
+ try {
354
+ const parsed = JSON.parse(body);
355
+ return typeof parsed.resultCode === 'number' ? parsed.resultCode : null;
356
+ }
357
+ catch {
358
+ return null;
359
+ }
360
+ }
320
361
  /**
321
362
  * Serves the 알리오 target from the 재정경제부 open API.
322
363
  *
@@ -328,7 +369,7 @@ function toIsoDate$2(compact) {
328
369
  * Only postings still open (`ongoingYn=Y`) are requested.
329
370
  */
330
371
  const createAlioFetch = (baseFetch = fetch, options) => {
331
- const { apiKey, rowsPerPage = 3000 } = options;
372
+ const { apiKey, rowsPerPage = 3000, onError } = options;
332
373
  return async (input, init) => {
333
374
  const requestUrl = typeof input === 'string'
334
375
  ? input
@@ -350,8 +391,33 @@ const createAlioFetch = (baseFetch = fetch, options) => {
350
391
  return Response.json({ result: [] });
351
392
  }
352
393
  if (url.pathname === '/recruit.do') {
353
- return baseFetch(`${API_BASE$2}/list?serviceKey=${apiKey}&resultType=json` +
394
+ const response = await baseFetch(`${API_BASE$2}/list?serviceKey=${apiKey}&resultType=json` +
354
395
  `&numOfRows=${rowsPerPage}&pageNo=1&ongoingYn=Y`, init);
396
+ if (!response.ok) {
397
+ // See the 나라일터 adapter: keep the upstream status, report the reason.
398
+ const body = await response.text();
399
+ const detail = describeOpenDataError(body);
400
+ onError?.(`알리오 list failed with HTTP ${response.status}` +
401
+ (detail ? ` — ${detail}` : ''));
402
+ return new Response(body, {
403
+ status: response.status,
404
+ statusText: response.statusText,
405
+ });
406
+ }
407
+ // Same reasoning as the 나라일터 adapter: data.go.kr answers its own
408
+ // errors with HTTP 200 and a result code, and passing one through turns a
409
+ // rejected key or an outage into a board that simply has no postings.
410
+ const body = await response.text();
411
+ const resultCode = parseResultCode(body);
412
+ if (resultCode !== null && resultCode !== SUCCESS_RESULT_CODE$2) {
413
+ const reason = `알리오 list returned resultCode ${resultCode}`;
414
+ onError?.(reason);
415
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
416
+ }
417
+ return new Response(body, {
418
+ status: 200,
419
+ headers: { 'content-type': 'application/json' },
420
+ });
355
421
  }
356
422
  if (url.pathname === '/recruitview.do') {
357
423
  const serial = url.searchParams.get('idx');
@@ -798,7 +864,7 @@ const buildG2bDetailUrl = (notice, order) => `${SITE_BASE$1}/link/PNPE027_01/sin
798
864
  */
799
865
  const OPERATIONS = ['Servc', 'Cnstwk'];
800
866
  /** `resultCode` the service returns on success. */
801
- const SUCCESS_RESULT_CODE = '00';
867
+ const SUCCESS_RESULT_CODE$1 = '00';
802
868
  /** Offset the service's wall clock runs on. */
803
869
  const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
804
870
  /**
@@ -835,7 +901,11 @@ function jsonResponse(payload) {
835
901
  * daily quota of 1,000 calls is barely touched.
836
902
  */
837
903
  const createG2bFetch = (baseFetch = fetch, options) => {
838
- const { apiKey, windowHours = 48, rowsPerPage = 999, maxPages = 4, triage, } = options;
904
+ const { apiKey, windowHours = 48, rowsPerPage = 999, maxPages = 4, triage, onError, } = options;
905
+ const fail = (reason) => {
906
+ onError?.(reason);
907
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
908
+ };
839
909
  /** Notices from the most recent list call, keyed by `<bidNtceNo>:<bidNtceOrd>`. */
840
910
  const noticeCache = new Map();
841
911
  return async (input, init) => {
@@ -880,12 +950,14 @@ const createG2bFetch = (baseFetch = fetch, options) => {
880
950
  `&numOfRows=${rowsPerPage}&pageNo=${page}&type=json&inqryDiv=1` +
881
951
  `&inqryBgnDt=${toApiDateTime(begin)}&inqryEndDt=${toApiDateTime(end)}`, init);
882
952
  if (!response.ok) {
883
- return `나라장터 ${operation} list failed with HTTP ${response.status}`;
953
+ const detail = describeOpenDataError(await response.text());
954
+ return (`나라장터 ${operation} list failed with HTTP ${response.status}` +
955
+ (detail ? ` — ${detail}` : ''));
884
956
  }
885
957
  const body = (await response.json());
886
958
  const resultCode = body.response?.header?.resultCode;
887
959
  // data.go.kr answers its own errors with HTTP 200 and a result code.
888
- if (resultCode !== SUCCESS_RESULT_CODE) {
960
+ if (resultCode !== SUCCESS_RESULT_CODE$1) {
889
961
  return `나라장터 ${operation} list returned resultCode ${resultCode ?? 'none'}`;
890
962
  }
891
963
  const batch = body.response?.body?.items ?? [];
@@ -915,10 +987,7 @@ const createG2bFetch = (baseFetch = fetch, options) => {
915
987
  }));
916
988
  const failure = pages.find((result) => typeof result === 'string');
917
989
  if (typeof failure === 'string') {
918
- return new Response(failure, {
919
- status: 502,
920
- statusText: 'Bad Gateway',
921
- });
990
+ return fail(failure);
922
991
  }
923
992
  const collected = pages.flat();
924
993
  // Deduplicate before judging: the same notice arrives on more than one
@@ -1152,6 +1221,8 @@ function toIsoDate(compact) {
1152
1221
  ? `${compact.slice(0, 4)}-${compact.slice(4, 6)}-${compact.slice(6, 8)}`
1153
1222
  : '';
1154
1223
  }
1224
+ /** `resultCode` the service returns on success. */
1225
+ const SUCCESS_RESULT_CODE = '00';
1155
1226
  /**
1156
1227
  * Serves the 나라일터 target from the 인사혁신처 open API.
1157
1228
  *
@@ -1164,7 +1235,7 @@ function toIsoDate(compact) {
1164
1235
  * returns the announcement body directly — no HTML page is fetched.
1165
1236
  */
1166
1237
  const createGojobsFetch = (baseFetch = fetch, options) => {
1167
- const { apiKey, windowDays = 7, rowsPerPage = 3000 } = options;
1238
+ const { apiKey, windowDays = 7, rowsPerPage = 3000, onError } = options;
1168
1239
  return async (input, init) => {
1169
1240
  const requestUrl = typeof input === 'string'
1170
1241
  ? input
@@ -1192,8 +1263,39 @@ const createGojobsFetch = (baseFetch = fetch, options) => {
1192
1263
  if (url.pathname === '/apmList.do') {
1193
1264
  const end = new Date();
1194
1265
  const begin = new Date(end.getTime() - windowDays * 24 * 60 * 60 * 1000);
1195
- return baseFetch(`${API_BASE}/getList?serviceKey=${apiKey}&numOfRows=${rowsPerPage}` +
1266
+ const response = await baseFetch(`${API_BASE}/getList?serviceKey=${apiKey}&numOfRows=${rowsPerPage}` +
1196
1267
  `&pageNo=1&Begin_de=${toApiDate(begin)}&End_de=${toApiDate(end)}`, init);
1268
+ if (!response.ok) {
1269
+ // Core logs the status; the gateway's own explanation is in the body and
1270
+ // would otherwise be dropped. Keep the status so the log still shows what
1271
+ // the service answered, and report the reason alongside it.
1272
+ const body = await response.text();
1273
+ const detail = describeOpenDataError(body);
1274
+ onError?.(`나라일터 list failed with HTTP ${response.status}` +
1275
+ (detail ? ` — ${detail}` : ''));
1276
+ return new Response(body, {
1277
+ status: response.status,
1278
+ statusText: response.statusText,
1279
+ });
1280
+ }
1281
+ // data.go.kr answers its own errors with HTTP 200 and a result code, so a
1282
+ // rejected key or a service outage arrives looking like a successful
1283
+ // response with no postings in it. Passing that through made the board
1284
+ // report zero vacancies and the run look healthy, which is how two days
1285
+ // of an empty 나라일터 went unnoticed. Fail instead, the same way the
1286
+ // 나라장터 adapter does.
1287
+ const xml = await response.text();
1288
+ const resultCode = /<resultCode>\s*([^<]*)<\/resultCode>/.exec(xml)?.[1];
1289
+ if (resultCode !== undefined &&
1290
+ resultCode.trim() !== SUCCESS_RESULT_CODE) {
1291
+ const reason = `나라일터 list returned resultCode ${resultCode.trim()}`;
1292
+ onError?.(reason);
1293
+ return new Response(reason, { status: 502, statusText: 'Bad Gateway' });
1294
+ }
1295
+ return new Response(xml, {
1296
+ status: 200,
1297
+ headers: { 'content-type': 'application/xml' },
1298
+ });
1197
1299
  }
1198
1300
  if (url.pathname === '/apmView.do') {
1199
1301
  const idx = url.searchParams.get('empmnsn');
@@ -3084,6 +3186,15 @@ const maximumImportanceScoreByDomain = {
3084
3186
  */
3085
3187
  const robotsExemptOrigins = [
3086
3188
  'http://www.yngogo.or.kr',
3189
+ // data.go.kr open APIs. robots.txt governs crawlers reading a site's
3190
+ // documents; these are authorised API calls made with a registered service
3191
+ // key, and the terms that bind them are the service's own. The host does not
3192
+ // publish a robots.txt at all: its gateway answers any unknown path — that
3193
+ // one included — with HTTP 400 and `NO_OPENAPI_SERVICE_ERROR`, so every run
3194
+ // was spending a request to be told the file does not exist. The adapters
3195
+ // rewrite 나라일터/알리오/나라장터 board URLs to this origin and the gate sits
3196
+ // inside them, which is how an API call ended up being asked about at all.
3197
+ 'https://apis.data.go.kr',
3087
3198
  ];
3088
3199
  /**
3089
3200
  * LLM configuration
@@ -3203,22 +3314,44 @@ const SUBJECT_OVER_INSTITUTION = `## 발주·공고 기관이 아니라 사업
3203
3314
 
3204
3315
  유산 기관이 낸 공고라도 **내용이 유산 업무가 아니면 낮게 준다.** 기관명은 단서일 뿐 근거가 아니다.
3205
3316
 
3206
- - 유산 업무: 발굴·시굴조사, 매장유산, 문화재 수리·보수정비, 보존처리, 기록화, 유적 정비,
3207
- 학술연구, 유물 조사·분석, 전시 기획
3208
- - 유산 업무가 아닌 것: 소방·전기·냉난방·창호·정보통신 같은 건물 설비와 그 유지보수,
3209
- 청사 환경개선, 홈페이지·전산, 차량 임차, 홍보물·영상 제작,
3210
- 축제·행사의 운영과 그 부대 용역(무대, 홍보, 교통·안전관리 포함)
3211
- → 국가유산청·국립박물관이 발주했더라도 **1점**. 발주처가 유산 기관이라는 이유로
3212
- 2-3점으로 올리지 않는다. 유산 업무가 아니면 1점이다.
3213
-
3214
- 예: "창덕궁 미분무 소화설비 성능개선 공사"는 궁궐 소재지만 소방 설비 공사다.
3215
- "대전선사박물관 창호 긴급교체 공사"도 박물관 건물의 창호 교체다.
3216
- "제72회 백제문화제 혼잡·교통유도 및 차량동선 안전관리 용역", "2026 경북종가음식문화대전
3217
- 사업 운영 용역", "청춘양구 펀치볼 사과시래기축제 홍보물 제작"은 모두 행사 운영·지원이다.
3218
- "국립항공박물관 정보통신설비 유지보수" 역시 건물 설비 유지보수다.
3219
- 반대로 "팔만대장경 P-XRF 분석"은 발주처가 사찰이어도 명백한 유산 조사이고,
3220
- "안양암 아미타괘불도 보존처리", "중요동산문화유산 기록화 3D 스캐닝",
3221
- "소장유물 복제", "소장품도록 발간"은 유물을 직접 다루므로 정상 채점한다.`;
3317
+ **유산 업무 — 정상 채점**
3318
+
3319
+ - 발굴·시굴조사, 매장유산, 문화재 수리·보수정비, 보존처리, 기록화, 유적 정비
3320
+ - 학술연구, 유물 조사·분석
3321
+ - 전시·해설의 기획과 **콘텐츠 개발** — 무엇을 보여주고 어떻게 설명할지 정하는 일
3322
+ (예: "달천철장 홍보관 무장애 관광 콘텐츠 개발 용역")
3323
+ - **유산 정보를 다루는 데이터베이스·아카이브·디지털화의 구축과 유지보수**
3324
+ (예: "B-헤리티지 웹 아카이빙 플랫폼 구축", "국외문화유산DB관리시스템 유지관리",
3325
+ "AI 학습데이터 구축 기반 아카이브 자료 디지털화")
3326
+ - **유산 교육 프로그램의 기획·운영** (예: "평창올림픽 유산교육 프로그램 기획·운영 대행용역")
3327
+
3328
+ **유산 업무가 아닌 것 → 발주처가 국가유산청·국립박물관이어도 1점**
3329
+
3330
+ - 소방·전기·냉난방·창호·정보통신 같은 건물 설비와 그 유지보수, 청사 환경개선
3331
+ - **구조안전진단**, 건물 철거와 폐기물 처리
3332
+ - 기관 홈페이지, 사무용 전산 시스템
3333
+ - 차량 임차, **운송·운반** (예: "국내 주요 화석산지 표본 헬기 운반")
3334
+ - 홍보물·영상 제작
3335
+ - **전시를 물리적으로 만들고 옮기고 설치하는 일** — 전시설계, 전시물·실감영상 제작·설치,
3336
+ 전시품 포장·운송 (예: "상설전시실3 실감영상 제작·설치", "특별전 전시품 포장, 운송 및 설치")
3337
+ - 축제·행사의 운영과 그 부대 용역(무대, 홍보, 교통·안전관리)
3338
+
3339
+ 발주처가 유산 기관이라는 이유로 2-3점으로 올리지 않는다. 유산 업무가 아니면 1점이다.
3340
+
3341
+ **갈리는 지점 세 가지**
3342
+
3343
+ 1. **전산**: 다루는 대상이 무엇인지로 가른다. 유산 자료를 담고 보여주고 보존하는
3344
+ 시스템이면 유산 업무이고, 기관을 운영하기 위한 전산이면 아니다.
3345
+ 2. **전시**: 무엇을 보여줄지 정하는 일은 유산 업무이고, 그것을 만들고 옮기고 설치하는
3346
+ 일은 아니다.
3347
+ 3. **프로그램**: 유산을 가르치고 해설하는 프로그램은 유산 업무이고, 축제·기념행사를
3348
+ 진행하는 일은 아니다.
3349
+
3350
+ 그 밖의 예: "창덕궁 미분무 소화설비 성능개선 공사"는 궁궐 소재지만 소방 설비 공사다.
3351
+ "국립항공박물관 정보통신설비 유지보수"도 건물 설비다. 반대로 "팔만대장경 P-XRF 분석"은
3352
+ 발주처가 사찰이어도 명백한 유산 조사이고, "안양암 아미타괘불도 보존처리",
3353
+ "중요동산문화유산 기록화 3D 스캐닝", "소장유물 복제", "소장품도록 발간"은
3354
+ 유물을 직접 다루므로 정상 채점한다.`;
3222
3355
  const EMPLOYMENT_FILTER = `## 채용 공고 판별
3223
3356
 
3224
3357
  채용 공고는 **직무가 아래 지원 직무 목록에 해당하는지**를 먼저 본다.
@@ -4729,7 +4862,32 @@ class CrawlingProvider {
4729
4862
  // The two public job boards are read from data.go.kr open APIs. Without a
4730
4863
  // key they answer with an empty list and make no request, so the targets
4731
4864
  // stay configured and simply collect nothing.
4732
- const withPublicJobs = createG2bFetch(createAlioFetch(createGojobsFetch(withKras, { apiKey: publicDataApiKey ?? '' }), { apiKey: publicDataApiKey ?? '' }), { apiKey: publicDataApiKey ?? '', triage: bidTriage });
4865
+ const withPublicJobs = createG2bFetch(createAlioFetch(createGojobsFetch(withKras, {
4866
+ apiKey: publicDataApiKey ?? '',
4867
+ onError: (reason) => {
4868
+ logger?.error({
4869
+ event: 'crawl.gojobs.list.failed',
4870
+ data: { reason },
4871
+ });
4872
+ },
4873
+ }), {
4874
+ apiKey: publicDataApiKey ?? '',
4875
+ onError: (reason) => {
4876
+ logger?.error({
4877
+ event: 'crawl.alio.list.failed',
4878
+ data: { reason },
4879
+ });
4880
+ },
4881
+ }), {
4882
+ apiKey: publicDataApiKey ?? '',
4883
+ triage: bidTriage,
4884
+ onError: (reason) => {
4885
+ logger?.error({
4886
+ event: 'crawl.g2b.list.failed',
4887
+ data: { reason },
4888
+ });
4889
+ },
4890
+ });
4733
4891
  // When the application supplies excavation reports, that board is served
4734
4892
  // from the injected source and never requested over the network. Every
4735
4893
  // 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.2",
5
+ "version": "5.2.4",
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",