@bpinternal/site-scout 0.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 ADDED
@@ -0,0 +1,92 @@
1
+ # @bpinternal/site-scout
2
+
3
+ Website URL discovery + LLM page selection — scouts a site for the pages worth
4
+ reading. Given a website, it finds candidate URLs, scores them into a
5
+ prioritized tree, then makes **one** injected-LLM call to pick the pages best
6
+ suited to seed a support bot's knowledge base.
7
+
8
+ ## What it does
9
+
10
+ Two independent units, composed:
11
+
12
+ ```
13
+ discover(website, opts, deps) -> MasterMap (prioritized tree)
14
+ select(map, opts, deps) -> Selection (best ≤limit URLs)
15
+ ```
16
+
17
+ `findKnowledgeUrls()` runs guard → discover → select for the common case.
18
+
19
+ - **Discovery** pulls candidate URLs from **5 sources** — `llms.txt` /
20
+ `llms-full.txt`, `sitemap.xml` (incl. children), `robots.txt`, the host's
21
+ `discoverUrls` crawl, and a `site:` web search — normalizes/dedupes them, and
22
+ builds a **score tree**: one node per path segment, with aggregate scores and
23
+ leaf counts so selection can reason about cardinality without walking every
24
+ leaf.
25
+ - **Selection** renders the tree as a compact numbered list and makes **one
26
+ LLM call** (`extract`) that returns the picked page _numbers_ (best-first),
27
+ which map back to URLs. The LLM pick is the decision — no deterministic
28
+ fallback list.
29
+
30
+ ## The injection contract (`SiteScoutDeps`)
31
+
32
+ site-scout does no I/O and never imports a runtime itself. Every consumer
33
+ provides the I/O + LLM as `deps`:
34
+
35
+ ```ts
36
+ type SiteScoutDeps = {
37
+ // discovery I/O
38
+ fetchText(url, baseHost, timeoutMs?): Promise<string | null>
39
+ fetchJson(url, baseHost, timeoutMs?): Promise<T | null>
40
+ discoverUrls(args): Promise<{ urls: string[]; stopReason: string }>
41
+ webSearch(args): Promise<{ results: SearchResult[] }>
42
+ // the one LLM touchpoint (zai-shaped extract)
43
+ extract(input, schema, { instructions }): Promise<z.infer<schema>>
44
+ // optional record/replay cache around the LLM call (defaults to passthrough)
45
+ cache?(kind, keyParts, fn): Promise<T>
46
+ }
47
+ ```
48
+
49
+ `@bpinternal/zui` is a **peer dependency** — consumers bring their own
50
+ (the schema passed to `extract` uses it).
51
+
52
+ ## Usage
53
+
54
+ ```ts
55
+ import { findKnowledgeUrls } from '@bpinternal/site-scout'
56
+
57
+ const result = await findKnowledgeUrls(
58
+ {
59
+ website: 'https://example.com',
60
+ limit: 50,
61
+ topics: ['pricing', 'docs'],
62
+ prompt: 'Prioritize developer docs, API reference, pricing.',
63
+ context: { company: 'Example', website: 'https://example.com', overview: '…' },
64
+ },
65
+ deps // your SiteScoutDeps implementation
66
+ )
67
+ // result.urls — prioritized URLs, best first
68
+ // result.discovered — total distinct URLs found before selection
69
+ // result.stopReason — 'ok' | 'unsupported_site' | 'no_sources' | 'time_limit_reached'
70
+ ```
71
+
72
+ `discover()` and `select()` are also exported for callers that want to show the
73
+ tree, let a user refine, then select. `fetchText`/`fetchJson`/`hostOf`/
74
+ `siteOrigin` (an SSRF-guarded fetcher + URL helpers) are re-exported so a
75
+ consumer can reuse them when wiring its own deps.
76
+
77
+ ## Record / replay fixtures
78
+
79
+ The tests are offline and deterministic:
80
+
81
+ - **Network** — discovery I/O is wrapped by `cachedDeps(group, realDeps)`, which
82
+ records/replays each call against `src/__fixtures__/<group>.jsonl` (one file
83
+ per site). In replay mode a miss throws (a test can't silently hit the
84
+ network); in production `cachedDeps` is a passthrough. Mode is env-driven
85
+ (`URL_FIXTURE_RECORD` to record; `NODE_ENV=test` / `VITEST` / `BUN_TEST` /
86
+ `URL_FIXTURE_REPLAY` to replay).
87
+ - **LLM** — the `select` call is keyed and replayed from
88
+ `src/__fixtures__/llm-cache.jsonl`. The package e2e (`index.e2e.test.ts`)
89
+ loads that file and serves the recorded decision, so no live model is called.
90
+
91
+ Fixtures are **recorded via the app harness** (viber-compiler-bot), which owns
92
+ the real runtime/LLM wiring; this package only replays them.