@heripo/research-radar 5.0.6 → 5.1.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Previously reported service metrics were $0.2–1 per issue and 15% CTR. These a
23
23
 
24
24
  - Type-safe TypeScript with strict interfaces
25
25
  - Provider pattern for swapping components (Crawling/Analysis/Content/Email)
26
- - 59 active crawling targets across heritage agencies, museums, academic societies
26
+ - 73 active crawling targets across heritage agencies, museums, academic societies, filtered at runtime by robots.txt
27
27
  - Multi LLM providers: OpenAI GPT-5 (analysis) + selectable content generation (OpenAI / Anthropic / Google)
28
28
  - Built-in retries, chain options, preview emails
29
29
 
@@ -70,7 +70,7 @@ For academic publications:
70
70
  npm install @heripo/research-radar '@llm-newsletter-kit/core@~3.0.0'
71
71
  ```
72
72
 
73
- **Requirements**: Node.js >= 24 and an ESM application. The package exports `dist/index.js` and TypeScript declarations (`dist/index.d.ts`), with a JavaScript sourcemap. The core engine is a peer dependency; the current supported range is `~3.0.0`.
73
+ **Requirements**: Node.js >= 24 and an ESM application. The package exports `dist/index.js` and TypeScript declarations (`dist/index.d.ts`), with a JavaScript sourcemap. The core engine is a peer dependency; the current supported range is `~3.0.5`. 3.0.5 is the floor because the newsletter generation prompt this package ships relies on the self-verification retry cap added there.
74
74
 
75
75
  Article analysis requires an OpenAI API key. Content generation requires a key for the selected provider (OpenAI / Anthropic / Google); OpenAI can use the same key for both. Keys are passed explicitly to the library; load environment variables in your application.
76
76
 
@@ -136,13 +136,15 @@ Candidate selection belongs to your repository. Persist `usedArticles` associati
136
136
 
137
137
  ### Optional generation settings
138
138
 
139
- | Option | Behavior |
140
- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
141
- | `logger` | Core `AppLogger` implementation, such as `console` |
142
- | `publishDate` | A real calendar date in `YYYY-MM-DD` format; invalid values throw. Defaults to the current date in `Asia/Seoul` (KST), regardless of server timezone |
143
- | `customFetch` | A `typeof fetch` implementation for crawling and parser API requests, such as a proxy adapter; does not configure LLM requests |
144
- | `templateOptions` | Default or KRAS newsletter branding and Markdown sections (see below) |
145
- | `previewNewsletter` | Fetch a saved `Newsletter` and send it through a supplied core `EmailService` |
139
+ | Option | Behavior |
140
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `logger` | Core `AppLogger` implementation, such as `console` |
142
+ | `publishDate` | A real calendar date in `YYYY-MM-DD` format; invalid values throw. Defaults to the current date in `Asia/Seoul` (KST), regardless of server timezone |
143
+ | `customFetch` | A `typeof fetch` implementation for crawling and parser API requests, such as a proxy adapter; does not configure LLM requests |
144
+ | `templateOptions` | Default or KRAS newsletter branding and Markdown sections (see below) |
145
+ | `promptProvider` | Core `PromptProvider` overriding the package's own LLM prompts entirely; omit to use the package's prompts, which fall back to core's defaults |
146
+ | `excavationReportSource` | Supplies 국가유산청 발굴조사 보고서 entries from the application; that board is then never crawled. Omit to crawl it as before |
147
+ | `previewNewsletter` | Fetch a saved `Newsletter` and send it through a supplied core `EmailService` |
146
148
 
147
149
  `previewNewsletter` requires `fetchNewsletterForPreview: () => Promise<Newsletter>`, `emailService` (with `send(message)`), and `emailMessage` (core `EmailMessage` without `subject`, `html`, or `text`). The core fills those three fields and skips preview delivery when no newsletter was created. Ensure the callback fetches the issue saved by this run.
148
150
 
@@ -165,16 +167,16 @@ Defaults below describe the checked-in code, not provider recommendations. This
165
167
 
166
168
  | Stage | Provider | Default model |
167
169
  | ------------------ | --------- | ------------------------ |
168
- | Tag classification | OpenAI | `gpt-5-mini` |
169
- | Image analysis | OpenAI | `gpt-5.1` |
170
- | Importance scoring | OpenAI | `gpt-5.1` |
171
- | Content generation | OpenAI | `gpt-5.4` |
170
+ | Tag classification | OpenAI | `gpt-5.6-luna` |
171
+ | Image analysis | OpenAI | `gpt-5.6-terra` |
172
+ | Importance scoring | OpenAI | `gpt-5.6-terra` |
173
+ | Content generation | OpenAI | `gpt-5.6-sol` |
172
174
  | Content generation | Anthropic | `claude-sonnet-4-6` |
173
175
  | Content generation | Google | `gemini-3.1-pro-preview` |
174
176
 
175
177
  Select content generation with `contentGeneration: { provider, apiKey, model? }`. `model` overrides that provider's default. Analysis models are configured in [analysis.provider.ts](./src/providers/analysis.provider.ts).
176
178
 
177
- [src/config/index.ts](./src/config/index.ts) defines Korean output (`outputLanguage: '한국어'`), the cultural heritage domain (`expertField: ['문화유산']`), brand name, `subscribePageUrl`, LLM `maxRetries: 5`, chain `stopAfterAttempt: 3`, and generation `temperature: 0.3`. Publication settings are `minimumArticleCountForIssue: 5` and `priorityArticleScoreThreshold: 8`; the core engine evaluates them. In the locked core 3.0.4 implementation, the count check skips **5 or fewer** candidates unless at least one has importance score >= 8. An empty candidate list is always skipped.
179
+ [src/config/index.ts](./src/config/index.ts) defines Korean output (`outputLanguage: '한국어'`), the cultural heritage domain (`expertField: ['문화유산']`), brand name, `subscribePageUrl`, LLM `maxRetries: 5`, chain `stopAfterAttempt: 3`, and generation `temperature: 0.3`. Publication settings are `minimumArticleCountForIssue: 5` and `priorityArticleScoreThreshold: 8`; the core engine evaluates them. In the locked core 3.0.5 implementation, the count check skips **5 or fewer** candidates unless at least one has importance score >= 8. An empty candidate list is always skipped.
178
180
 
179
181
  ## Crawling targets and parsers
180
182
 
@@ -182,16 +184,20 @@ Select content generation with `contentGeneration: { provider, apiKey, model? }`
182
184
 
183
185
  | Group | Active | Commented out |
184
186
  | ---------- | -----: | ------------: |
185
- | News | 48 | 10 |
187
+ | News | 57 | 1 |
186
188
  | Business | 4 | 0 |
187
- | Employment | 7 | 3 |
188
- | **Total** | **59** | **13** |
189
+ | Employment | 12 | 0 |
190
+ | **Total** | **73** | **1** |
189
191
 
190
- The 13 commented targets are excluded from runtime configuration: one excavation status board is marked as low-value fragmented data, and 12 museum boards are marked as restricted by robots.txt. Their parser code remains in the repository.
192
+ Only the excavation status board stays commented out, as low-value fragmented data. Boards that a site's robots.txt restricts are configured normally and refused at runtime by the robots.txt check, so the configuration does not have to track each site's policy by hand. With current policies 14 of the 73 targets are refused.
193
+
194
+ Two Employment targets are read from data.go.kr open APIs instead of scraped: 나라일터 (`PblJobService`) and 알리오 (`recruitment`). Each is a single request, and `publicDataApiKey` supplies the service key — omit it and both answer with an empty list without making a request. `src/crawling/heritage-job-filter.ts` narrows them before analysis, since the boards carry every public-sector vacancy in the country; roughly 2% survive, about 2.6 postings a day.
195
+
196
+ Crawling fetches pass through that check (`src/crawling/robots.ts`) before reaching the network: a disallowed request is refused rather than sent, and a missing or unreachable robots.txt allows it. `robotsExemptOrigins` in `src/config/index.ts` lists origins exempted from the check, each with its reason.
191
197
 
192
198
  Sources include the Korea Heritage Service, National Research Institute of Cultural Heritage, National Research Institute of Maritime Heritage, Korea Heritage Agency, Korea Association of Archaeological Heritage, archaeological societies, and national museums.
193
199
 
194
- [src/parsers/](./src/parsers/) contains 20 organization-specific parser modules plus shared date and URL utilities. List parsers return `ParsedTargetListItem[]` (title, date, detail URL, date type, and optional source ID); detail parsers return Markdown `detailContent` and attachment/image flags. Parsers can be synchronous or asynchronous. KRAS, Yeongnam Archaeological Society, and maritime heritage sources use additional API requests for client-rendered content.
200
+ [src/parsers/](./src/parsers/) contains 22 organization-specific parser modules plus shared date and URL utilities. List parsers return `ParsedTargetListItem[]` (title, date, detail URL, date type, and optional source ID); detail parsers return Markdown `detailContent` and attachment/image flags. Parsers can be synchronous or asynchronous. KRAS, Yeongnam Archaeological Society, and maritime heritage sources use additional API requests for client-rendered content.
195
201
 
196
202
  `CrawlingProvider` uses a maximum concurrency of 5 and wraps the supplied fetch to route KRAS public detail URLs to its detail API, while retaining public URLs in article metadata. When constructing your own pipeline, use the provider's fetch together with its target groups.
197
203
 
@@ -203,7 +209,7 @@ import { getSourceList } from '@heripo/research-radar';
203
209
  const groups = getSourceList(); // [{ id, name, sources: [{ id, name, url }] }]
204
210
  ```
205
211
 
206
- `createCrawlingTargetGroups(customFetch?)`, `getSourceList()`, `contentOptions`, `newsletterConfig`, and `llmConfig` are public exports. The package also exports the three provider classes, `DateService`, `TaskService`, and their public configuration/dependency types through [src/index.ts](./src/index.ts).
212
+ `createCrawlingTargetGroups(customFetch?)`, `getSourceList()`, `contentOptions`, `newsletterConfig`, `llmConfig`, and `researchRadarPromptProvider` are public exports. `ExcavationReport` and `ExcavationReportSource` are exported as types. The package also exports the three provider classes, `DateService`, `TaskService`, and their public configuration/dependency types through [src/index.ts](./src/index.ts).
207
213
 
208
214
  ## Email templates
209
215
 
@@ -332,7 +338,7 @@ Use repeatable `--skip-target=<id-or-name>` or `--skip-target <id-or-name>` to e
332
338
 
333
339
  **Output**: Console table summary + compact text summary for CI integrations.
334
340
 
335
- **CI**: [.github/workflows/parser-health-check.yml](./.github/workflows/parser-health-check.yml) runs daily at 08:00 UTC (17:00 KST), or manually, on an `org-linux` runner with a 30-minute job timeout. It skips the two KHS excavation report/site-open targets (57 targets checked with the current configuration). Slack notifications require the `SLACK_BOT_TOKEN` secret and `SLACK_ALERT_DEV_CHANNEL` repository variable. Forks need a matching runner and notification configuration to use this workflow unchanged. The CLI also writes GitHub Actions outputs and a job summary when their environment variables are present.
341
+ **CI**: [.github/workflows/parser-health-check.yml](./.github/workflows/parser-health-check.yml) runs daily at 08:00 UTC (17:00 KST), or manually, on an `org-linux` runner with a 30-minute job timeout. It skips the two KHS excavation report/site-open targets. The health-check composes the same fetch as production — robots.txt gate, then the KRAS detail adapter — so disallowed boards are reported as skipped rather than failed: 16 skipped and 55 checked with the current configuration. The detail check tries up to three list items, so one unreadable post at the top of a board does not fail the target. Slack notifications require the `SLACK_BOT_TOKEN` secret and `SLACK_ALERT_DEV_CHANNEL` repository variable. Forks need a matching runner and notification configuration to use this workflow unchanged. The CLI also writes GitHub Actions outputs and a job summary when their environment variables are present.
336
342
 
337
343
  ## 🤝 Contributing
338
344
 
@@ -390,7 +396,7 @@ const contentGeneration: ContentGenerationConfig = {
390
396
  };
391
397
  ```
392
398
 
393
- Default models: openai=`gpt-5.4`, anthropic=`claude-sonnet-4-6`, google=`gemini-3.1-pro-preview`
399
+ Default models: openai=`gpt-5.6-sol`, anthropic=`claude-sonnet-4-6`, google=`gemini-3.1-pro-preview`
394
400
 
395
401
  To change analysis providers, update both `src/providers/analysis.provider.ts` (provider type and models) and `src/newsletter-generator.ts` (provider construction), using a compatible AI SDK provider. Also adapt domain-specific minimum-score rules in the analysis provider, output language and expert fields in config, package metadata, and the GitHub Actions runner/Slack settings for your fork.
396
402
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { UrlString, ParsedTarget, CrawlingTargetGroup, CrawlingTarget, UnscoredArticle, ArticleForUpdateByAnalysis, ArticleForGenerateContent, Newsletter, AppLogger, EmailService, EmailMessage, DateService as DateService$1, IsoDateString, TaskService as TaskService$1, AnalysisProvider as AnalysisProvider$1, ContentGenerateProvider as ContentGenerateProvider$1, HtmlTemplate, CrawlingProvider as CrawlingProvider$1, GenerateNewsletterConfig } from '@llm-newsletter-kit/core';
1
+ import { UrlString, ParsedTarget, CrawlingTargetGroup, CrawlingTarget, UnscoredArticle, ArticleForUpdateByAnalysis, ArticleForGenerateContent, Newsletter, IsoDateString, AppLogger, EmailService, EmailMessage, PromptProvider, DateService as DateService$1, TaskService as TaskService$1, AnalysisProvider as AnalysisProvider$1, ContentGenerateProvider as ContentGenerateProvider$1, HtmlTemplate, CrawlingProvider as CrawlingProvider$1, GenerateNewsletterConfig } from '@llm-newsletter-kit/core';
2
2
  import { OpenAIProvider } from '@ai-sdk/openai';
3
3
  import { LanguageModel } from 'ai';
4
4
 
@@ -54,6 +54,53 @@ interface ArticleRepository {
54
54
  */
55
55
  findCandidatesForNewsletter(): Promise<ArticleForGenerateContent[]>;
56
56
  }
57
+ /**
58
+ * A single excavation report supplied by the application instead of crawled.
59
+ *
60
+ * Field names mirror what the 국가유산청 report board exposes, so an application
61
+ * that already stores these rows can hand them over without reshaping them.
62
+ */
63
+ interface ExcavationReport {
64
+ /**
65
+ * The board's own identifier (`ecexmRcno`). Used both as the article's unique
66
+ * id and to build its public detail URL, so it must match the value the board
67
+ * uses — otherwise previously crawled reports are re-saved as duplicates.
68
+ * @example "202609157717"
69
+ */
70
+ externalId: string;
71
+ /**
72
+ * Report title (보고서명).
73
+ * @example "태안 태안읍성 -남문지 및 연지-"
74
+ */
75
+ title: string;
76
+ /**
77
+ * Submission date (제출일) in ISO format (YYYY-MM-DD).
78
+ * @example "2026-09-15"
79
+ */
80
+ submittedDate: IsoDateString;
81
+ /**
82
+ * Report detail fields as label/value pairs, rendered into the article body in
83
+ * insertion order. Supply whatever the source holds — 허가번호, 유적명,
84
+ * 발간기관, 조사시도시군구, 조사기간, 유적성격/시대구분 and so on.
85
+ * Empty values are skipped.
86
+ */
87
+ fields: Record<string, string | null | undefined>;
88
+ /**
89
+ * Whether the report has a downloadable file. Defaults to true, matching the
90
+ * crawled parser.
91
+ */
92
+ hasAttachedFile?: boolean;
93
+ }
94
+ /**
95
+ * Supplies excavation reports from the application instead of crawling them.
96
+ *
97
+ * When provided, the 국가유산청 발굴조사 보고서 board is served from this
98
+ * function and never requested over the network; the rest of the crawl is
99
+ * unaffected. When omitted, the board is crawled as before.
100
+ *
101
+ * Called at most once per generation run.
102
+ */
103
+ type ExcavationReportSource = () => Promise<ExcavationReport[]>;
57
104
  /**
58
105
  * Repository interface for tag management
59
106
  */
@@ -178,7 +225,7 @@ interface PreviewNewsletterOptions {
178
225
  * Each provider uses a sensible default model that can be overridden.
179
226
  *
180
227
  * Default models:
181
- * - openai: `gpt-5.4`
228
+ * - openai: `gpt-5.6-sol`
182
229
  * - anthropic: `claude-sonnet-4-6`
183
230
  * - google: `gemini-3.1-pro-preview`
184
231
  */
@@ -226,6 +273,30 @@ interface NewsletterGeneratorDependencies {
226
273
  templateOptions?: NewsletterTemplateOptions;
227
274
  /** Custom fetch function for crawling (e.g., proxy-based fetch). Optional. */
228
275
  customFetch?: typeof fetch;
276
+ /**
277
+ * Supplies 국가유산청 발굴조사 보고서 entries from the application (optional).
278
+ *
279
+ * When provided, that board is read from this function and never crawled,
280
+ * which lets an application that already stores the reports reuse them. Omit
281
+ * it to keep crawling the board as before. No other target is affected.
282
+ */
283
+ excavationReportSource?: ExcavationReportSource;
284
+ /**
285
+ * data.go.kr service key for the 나라일터 and 알리오 job boards (optional).
286
+ *
287
+ * Pass the encoded key exactly as the portal supplies it. Both boards are read
288
+ * through open APIs rather than scraped; omit the key and they collect
289
+ * nothing while every other target is unaffected.
290
+ */
291
+ publicDataApiKey?: string;
292
+ /**
293
+ * LLM prompt overrides (optional).
294
+ *
295
+ * When provided, this replaces Research Radar's own prompt provider entirely
296
+ * rather than merging with it. Omit it to use the package's tuned prompts,
297
+ * which in turn fall back to core's defaults for any stage they do not define.
298
+ */
299
+ promptProvider?: PromptProvider;
229
300
  }
230
301
  /**
231
302
  * Newsletter generation execution function
@@ -433,7 +504,7 @@ declare class CrawlingProvider implements CrawlingProvider$1 {
433
504
  customFetch?: typeof fetch;
434
505
  /** Crawling target groups configuration */
435
506
  crawlingTargetGroups: CrawlingTargetGroup[];
436
- constructor(articleRepository: ArticleRepository, customFetch?: typeof fetch);
507
+ constructor(articleRepository: ArticleRepository, customFetch?: typeof fetch, excavationReportSource?: ExcavationReportSource, logger?: AppLogger, publicDataApiKey?: string);
437
508
  /**
438
509
  * Fetch existing articles by URLs to avoid duplicate crawling
439
510
  * @param articleUrls - URLs to check
@@ -489,6 +560,38 @@ declare const contentOptions: ContentOptions;
489
560
  * Newsletter brand configuration
490
561
  */
491
562
  declare const newsletterConfig: NewsletterConfig;
563
+ /**
564
+ * Maximum importance score per heritage domain (`tag1`).
565
+ *
566
+ * The newsletter is archaeology-first: archaeology and cultural heritage keep
567
+ * the full 1-10 range, while natural and intangible heritage are capped so they
568
+ * cannot crowd out archaeological coverage. `기타` — material that is not
569
+ * heritage at all — is capped lowest, because the scoring prompt's academic-value
570
+ * floor is domain-blind and would otherwise lift things like a general journal's
571
+ * call for papers into the top half of the scale.
572
+ *
573
+ * The cap is applied deterministically after scoring, in
574
+ * `AnalysisProvider.update()`, rather than asked for in the prompt, so the
575
+ * ceiling always holds. It never reaches 1: a score of 1 means "exclude from the
576
+ * newsletter" in the consuming application's candidate query, so capping to 1
577
+ * would delete these articles instead of demoting them.
578
+ *
579
+ * Domains absent from this map are not capped.
580
+ */
581
+ declare const maximumImportanceScoreByDomain: Record<string, number>;
582
+ /**
583
+ * Origins exempted from the robots.txt check.
584
+ *
585
+ * Each entry deliberately overrides what the site publishes, so it needs a
586
+ * reason and should be revisited when that site's robots.txt changes.
587
+ *
588
+ * - `http://www.yngogo.or.kr` (영남고고학회): its board is rendered from
589
+ * `/module/ntt/unity/selectNttListAjax.ink`, and robots.txt carries a blanket
590
+ * `Disallow: /module`. The rule reads as protecting an internal path rather
591
+ * than the public board it happens to serve, and there is no other route to
592
+ * the listing, so the society's boards are collected under this exemption.
593
+ */
594
+ declare const robotsExemptOrigins: readonly string[];
492
595
  /**
493
596
  * LLM configuration
494
597
  */
@@ -500,5 +603,35 @@ declare const llmConfig: {
500
603
  };
501
604
  };
502
605
 
503
- export { AnalysisProvider, ContentGenerateProvider, CrawlingProvider, DateService, TaskService, contentOptions, createCrawlingTargetGroups, generateNewsletter, generateWelcomeHTML, getSourceList, llmConfig, newsletterConfig };
504
- export type { ArticleRepository, ContentGenerationConfig, ContentOptions, NewsletterConfig, NewsletterGeneratorDependencies, NewsletterRepository, NewsletterTemplateOptions, PreviewNewsletterOptions, SourceGroup, SourceItem, TagRepository, TaskRepository, WelcomeTemplateOptions };
606
+ /**
607
+ * Research Radar's LLM prompt overrides.
608
+ *
609
+ * Each builder here **replaces** core's built-in prompt for that stage rather
610
+ * than extending it — a `PromptBuilder` returns the whole prompt string. That is
611
+ * deliberate: core's defaults instruct the model to use emoticons, render
612
+ * importance as star ratings, and add share-of-coverage statistics, all of which
613
+ * this newsletter removes. Appending contradicting rules to those defaults
614
+ * degrades the output, so a replacement is written from scratch instead.
615
+ *
616
+ * **Contract a replacement must still satisfy.** Core's output schema is fixed,
617
+ * and `generateNewsletter` regenerates the whole newsletter whenever the model
618
+ * reports a failure. A replacement prompt has to steer the model toward:
619
+ *
620
+ * - `title`: 20–70 characters
621
+ * - `isWrittenInOutputLanguage`, `copyrightVerified`, `factAccuracy`: true
622
+ * - `titleContext` (KRAS mode): the phrase must appear in the title
623
+ *
624
+ * Core caps that loop at 5 attempts, so a prompt that ignores the contract costs
625
+ * up to 5 full generations on the most expensive model in the pipeline.
626
+ *
627
+ * Tune these against real articles with core's playground
628
+ * (`npm run playground:generate-newsletter` in ../llm-newsletter-kit-core) and
629
+ * diff them against the defaults for free with `playground:verify-prompts`.
630
+ * The playground loads this module from `dist`, so run `npm run build` here
631
+ * after every edit — its source cannot be imported directly across repos
632
+ * because of the `~/*` path alias.
633
+ */
634
+ declare const researchRadarPromptProvider: PromptProvider;
635
+
636
+ export { AnalysisProvider, ContentGenerateProvider, CrawlingProvider, DateService, TaskService, contentOptions, createCrawlingTargetGroups, generateNewsletter, generateWelcomeHTML, getSourceList, llmConfig, maximumImportanceScoreByDomain, newsletterConfig, researchRadarPromptProvider, robotsExemptOrigins };
637
+ export type { ArticleRepository, ContentGenerationConfig, ContentOptions, ExcavationReport, ExcavationReportSource, NewsletterConfig, NewsletterGeneratorDependencies, NewsletterRepository, NewsletterTemplateOptions, PreviewNewsletterOptions, SourceGroup, SourceItem, TagRepository, TaskRepository, WelcomeTemplateOptions };