html-reader-llm 0.0.1__tar.gz → 0.0.2__tar.gz

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 (44) hide show
  1. html_reader_llm-0.0.2/.env.example +12 -0
  2. html_reader_llm-0.0.2/.gitignore +230 -0
  3. html_reader_llm-0.0.2/.python-version +1 -0
  4. html_reader_llm-0.0.2/AGENTS.md +102 -0
  5. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/PKG-INFO +1 -1
  6. html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/.openspec.yaml +2 -0
  7. html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/design.md +60 -0
  8. html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/proposal.md +25 -0
  9. html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/specs/chrome-path-detection/spec.md +93 -0
  10. html_reader_llm-0.0.2/openspec/changes/chrome-path-detection/tasks.md +25 -0
  11. html_reader_llm-0.0.2/openspec/changes/dotenv-config/.openspec.yaml +2 -0
  12. html_reader_llm-0.0.2/openspec/changes/llm-analysis/.openspec.yaml +2 -0
  13. html_reader_llm-0.0.2/openspec/changes/llm-analysis/design.md +108 -0
  14. html_reader_llm-0.0.2/openspec/changes/llm-analysis/proposal.md +30 -0
  15. html_reader_llm-0.0.2/openspec/changes/llm-analysis/specs/llm-analysis/spec.md +119 -0
  16. html_reader_llm-0.0.2/openspec/changes/llm-analysis/tasks.md +41 -0
  17. html_reader_llm-0.0.2/openspec/changes/render-detection/.openspec.yaml +2 -0
  18. html_reader_llm-0.0.2/openspec/changes/render-detection/design.md +99 -0
  19. html_reader_llm-0.0.2/openspec/changes/render-detection/proposal.md +31 -0
  20. html_reader_llm-0.0.2/openspec/changes/render-detection/specs/render-detection/spec.md +106 -0
  21. html_reader_llm-0.0.2/openspec/changes/render-detection/tasks.md +34 -0
  22. html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/.openspec.yaml +2 -0
  23. html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/design.md +88 -0
  24. html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/proposal.md +31 -0
  25. html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/specs/html-simplification/spec.md +191 -0
  26. html_reader_llm-0.0.2/openspec/changes/simplify-pipeline/tasks.md +50 -0
  27. html_reader_llm-0.0.2/openspec/config.yaml +32 -0
  28. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/pyproject.toml +1 -1
  29. html_reader_llm-0.0.2/tests/test_browser.py +205 -0
  30. html_reader_llm-0.0.2/tests/test_cli.py +137 -0
  31. html_reader_llm-0.0.2/tests/test_llm.py +231 -0
  32. html_reader_llm-0.0.2/tests/test_render_detect.py +304 -0
  33. html_reader_llm-0.0.2/tests/test_simplify.py +751 -0
  34. html_reader_llm-0.0.2/uv.lock +581 -0
  35. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/LICENSE +0 -0
  36. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/README.md +0 -0
  37. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/__init__.py +0 -0
  38. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/browser.py +0 -0
  39. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/cli.py +0 -0
  40. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/llm.py +0 -0
  41. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/render_detect.py +0 -0
  42. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/selector_validator.py +0 -0
  43. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/settings.py +0 -0
  44. {html_reader_llm-0.0.1 → html_reader_llm-0.0.2}/src/html_reader_llm/simplify.py +0 -0
@@ -0,0 +1,12 @@
1
+ # htmlreader environment variables
2
+
3
+ # LLM API configuration
4
+ HTMLREADER_LLM_BASE_URL=
5
+ HTMLREADER_LLM_API_KEY=
6
+ HTMLREADER_LLM_MODEL=
7
+
8
+ # Optional overrides
9
+ # HTMLREADER_LLM_MAX_TOKENS=512000
10
+ # HTMLREADER_HTTP_TIMEOUT=15
11
+ # HTMLREADER_CHROME_TIMEOUT=30
12
+ # HTMLREADER_CHROME_PATH=
@@ -0,0 +1,230 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
217
+
218
+ # claude
219
+ .claude/specs/
220
+ .claude/
221
+
222
+ # zcode
223
+ .zcode/
224
+
225
+ # env files
226
+ .env
227
+
228
+ # temporary files
229
+ .cache/
230
+ .temp/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,102 @@
1
+ # 项目上下文 (AI Agent 指令)
2
+
3
+ ## 基本信息
4
+
5
+ - **项目**:htmlreader — 智能 HTML 解析与提取库
6
+ - **阶段**:Phase 1(核心功能开发)
7
+ - **技术栈**:Python 3.12+ / uv / httpx2 / selectolax
8
+ - **语言**:中文优先
9
+
10
+ ## 常用命令
11
+
12
+ ```bash
13
+ uv venv && uv sync # 安装依赖
14
+ uv run htmlreader # CLI
15
+ uv build # 构建
16
+ ```
17
+
18
+ ## 架构
19
+
20
+ ```
21
+ 输入 URL/HTML
22
+ │
23
+ ├─ 自定义简化流程(后续参考草稿+开源代码)─→ minify-html ─→ LLM 分析结构
24
+ │
25
+ ├─ 普通请求 vs 浏览器渲染 ─→ 差异度计算 + 文本统计
26
+ │
27
+ ├─ httpx2 请求 ─→ LLM(页面分类 / CSS 选择器规则生成)
28
+ │ └─ tranfilatura 辅助元数据提取
29
+ │
30
+ └─ 自定义 CSS 提取语法 ─→ 低代码提取工作流
31
+ ```
32
+
33
+ ## 设计决策(探索阶段)
34
+
35
+ ### HTML 简化
36
+ - minify-html 是喂给 LLM 之前的最后一步压缩
37
+ - 中间有自定义简化步骤,后续提供草稿代码 + 大量开源代码学习后确定
38
+
39
+ ### 浏览器渲染判断
40
+ - 不用框架,用 Chrome 原生 `--dump-dom` 管道获取渲染后 HTML
41
+ - 第一版让用户填入 Chrome 路径(或 auto 自动查找)
42
+ - 关键参数:
43
+ - `--virtual-time-budget=` 预估等待 JS 异步加载
44
+ - `--disable-gpu`
45
+ - `--run-all-compositor-stages-before-draw`
46
+ - `--user-agent="指定的 UA"`
47
+ - `--headless`
48
+ - `--timeout=毫秒数`
49
+ - `--blink-settings=imagesEnabled=false` 禁用图片减少流量
50
+ - `--user-data-dir=` 缓存用户数据,减少重复加载
51
+ - 纯管道模式,不走 CDP 协议(后续可扩展)
52
+ - 渲染后 HTML 与 httpx2 结果对比,统计文本差异度
53
+
54
+ ### CSS 提取语法(草稿)
55
+ - 三个核心字段:
56
+ - `selector`:CSS 选择器
57
+ - `value_mode`:取值方式
58
+ - `$html`:取元素内部完整 HTML
59
+ - `$text`:取纯文本(去标签)
60
+ - `@attribute`:取指定属性值(如 `@href`、`@src`)
61
+ - 交互形式(YAML / 链式 API / LLM 生成)后续再定
62
+
63
+ ## 核心需求
64
+
65
+ ### 1. HTML 简化(供 LLM 分析)
66
+ - 将原始 HTML 转换为适合 LLM 理解的简化格式
67
+ - 保留结构语义,去除噪声(样式、脚本、无用属性)
68
+
69
+ ### 2. 渲染方式判断
70
+ - 对同一 URL,分别用普通 HTTP 请求和浏览器渲染获取结果
71
+ - 计算差异度(DOM 结构、文本量)
72
+ - 统计文本长度,判断是否需要浏览器渲染
73
+ - 输出决策:`use_browser: bool`
74
+
75
+ ### 3. LLM 驱动的智能分析
76
+ - 使用 httpx2 发送 HTTP 请求与 LLM 通信
77
+ - **页面分类**:识别 `list`(列表页)/ `detail`(详情页)
78
+ - **CSS 选择器规则生成**:
79
+ - list 页:提取列表规则
80
+ - detail 页:提取正文 + 元数据规则
81
+ - **tranfilatura 协作**:提取元数据作为 LLM 上下文,提高选择器准确度
82
+
83
+ ### 4. CSS 提取语法工作流
84
+ - 自定义一套文本化的 CSS 提取规则语法
85
+ - 低代码方式描述网页内容提取逻辑
86
+ - 支持组合、嵌套、条件选择
87
+ - 规则可序列化、可存储、可复用
88
+
89
+ ## 依赖说明
90
+
91
+ | 依赖 | 用途 |
92
+ |---|---|
93
+ | `httpx2` | 异步 HTTP 客户端,用于下载页面及 LLM API 通信 |
94
+ | `selectolax` | 高性能 HTML 解析(基于 Modest/Lexbor) |
95
+ | `minify-html` | HTML 极致压缩,缩短 LLM 提示词长度 |
96
+
97
+ ## 关键概念
98
+
99
+ - **页面分类**:`list`(列表/索引页)vs `detail`(内容/详情页)
100
+ - **差异度**:普通请求与浏览器渲染结果的 DOM/文本相似度
101
+ - **提取规则**:文本化 CSS 选择器组合,描述从 HTML 中提取目标数据的逻辑
102
+ - **简化格式**:去除样式噪声后的 HTML,保留结构语义供 LLM 理解
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: html-reader-llm
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: HTML simplification and intelligent extraction for LLM
5
5
  Author-email: Clericpy <clericpy@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-08-01
@@ -0,0 +1,60 @@
1
+ ## Context
2
+
3
+ The browser rendering feature needs a Chrome/Edge path. Users may not know where their browser is installed, so automatic detection is essential. The provided draft code in the conversation demonstrates the core approach: platform-specific path lists + verification.
4
+
5
+ ## Goals / Non-Goals
6
+
7
+ **Goals:**
8
+ - Standalone `browser.py` module, zero external dependencies (stdlib only)
9
+ - Three usage modes: list all paths, auto-select first, validate user path
10
+ - ENV override via `HTMLREADER_CHROME_PATH`
11
+ - Cross-platform: Windows, Linux, macOS
12
+
13
+ **Non-Goals:**
14
+ - Browser version comparison or feature detection
15
+ - CDP protocol connection (deferred to browser rendering stage)
16
+ - Browser installation/download
17
+ - Firefox or other browsers (Chrome/Edge only)
18
+
19
+ ## Decisions
20
+
21
+ ### 1. Detection strategy: file existence + --version verification
22
+
23
+ **Decision:** On Windows, check known paths via `os.path.isfile()`. On Linux/macOS, check PATH-based commands via `[cmd, "--version"]` subprocess call (timeout 2s). Match output prefix to confirm it's actually Chrome/Edge (not another binary with the same name).
24
+
25
+ **Rationale:** File existence is fast and reliable for fixed Windows paths. `--version` verification prevents false positives on Linux where a script might be named `google-chrome` but not actually be Chrome.
26
+
27
+ **Alternative considered:** Only check file existence on all platforms. Rejected — Linux/macOS binaries on PATH need verification to avoid false matches.
28
+
29
+ ### 2. Path list deduplication + mtime sorting
30
+
31
+ **Decision:** Collect all found paths into a set (dedup), resolve to absolute, sort by `os.path.getmtime()` descending (newest first).
32
+
33
+ **Rationale:** Users with multiple Chrome versions (stable/beta/dev) want the newest. Set dedup handles cases where the same binary is found via multiple detection methods.
34
+
35
+ ### 3. Module structure
36
+
37
+ **Decision:** Single file `src/htmlreader/browser.py` with these public functions:
38
+ - `detect_browser_paths() -> list[str]` — list all found paths
39
+ - `get_browser_path() -> str | None` — auto-select first, or None
40
+ - `validate_browser_path(path: str) -> str` — validate user path, raise if invalid
41
+
42
+ **Rationale:** Three clear functions for three usage modes. The `get_browser_path()` function checks ENV first, then falls back to auto-detect.
43
+
44
+ ### 4. Error handling
45
+
46
+ **Decision:** Custom `BrowserNotFoundError` exception (not `ChromeNotFoundError` — we also detect Edge).
47
+
48
+ **Rationale:** The name should reflect what we're actually looking for (browsers, not just Chrome).
49
+
50
+ ### 5. ENV integration
51
+
52
+ **Decision:** `get_browser_path()` checks `HTMLREADER_CHROME_PATH` first. If set, validate it and return. If not set, fall back to auto-detect.
53
+
54
+ **Rationale:** ENV override lets users pin a specific browser without code changes. Consistent with the `HTMLREADER_*` convention from settings.py.
55
+
56
+ ## Risks / Trade-offs
57
+
58
+ - **[False negatives on unusual install paths]** → Mitigated by supporting both fixed paths and PATH-based detection
59
+ - **[Subprocess timeout on slow systems]** → 2s timeout is generous; if it times out, that binary is skipped
60
+ - **[mtime doesn't always correlate with "best" browser]** → Acceptable heuristic for first version; users can override via ENV or explicit path
@@ -0,0 +1,25 @@
1
+ ## Why
2
+
3
+ The browser rendering feature requires a Chrome/Edge browser path. Users can provide an explicit path, but most will prefer automatic detection. The system needs to discover installed Chrome/Edge executables on the current platform, validate user-provided paths, and expose the detection as a standalone module that other features can import.
4
+
5
+ ## What Changes
6
+
7
+ - **New module**: `src/htmlreader/browser.py` — Chrome/Edge path detection
8
+ - **Auto-detect**: Platform-aware search (Windows/Linux/macOS) for Chrome and Edge executables
9
+ - **Three modes**: list all found paths, auto-select first match, validate user-provided path
10
+ - **ENV support**: `HTMLREADER_CHROME_PATH` overrides auto-detection
11
+ - **Test file**: `tests/test_browser.py` — detection + validation tests
12
+
13
+ ## Capabilities
14
+
15
+ ### New Capabilities
16
+ - `chrome-path-detection`: Platform-aware Chrome/Edge path discovery, validation, and selection
17
+
18
+ ### Modified Capabilities
19
+ <!-- None -->
20
+
21
+ ## Impact
22
+
23
+ - New files: `src/htmlreader/browser.py`, `tests/test_browser.py`
24
+ - Modified: `src/htmlreader/__init__.py` (public API export)
25
+ - No new dependencies (stdlib only: `os`, `platform`, `subprocess`, `shutil`)
@@ -0,0 +1,93 @@
1
+ ## Purpose
2
+
3
+ Provides platform-aware Chrome/Edge browser path discovery, validation, and selection for the browser rendering feature.
4
+
5
+ ## ADDED Requirements
6
+
7
+ ### Requirement: Platform-aware browser detection
8
+ The system SHALL detect installed Chrome and Edge executables on the current platform (Windows, Linux, macOS). On Windows, it SHALL check known installation paths and use `os.path.isfile()` to verify existence. On Linux, it SHALL check PATH-based commands via `--version` output. On macOS, it SHALL check known application bundle paths.
9
+
10
+ #### Scenario: Windows Chrome found
11
+ - **WHEN** Chrome is installed at a known Windows path
12
+ - **THEN** that path appears in the detected paths list
13
+
14
+ #### Scenario: Linux Chrome found
15
+ - **WHEN** `google-chrome-stable` is available on PATH
16
+ - **THEN** its absolute path appears in the detected paths list
17
+
18
+ #### Scenario: macOS Chrome found
19
+ - **WHEN** Chrome is installed at `/Applications/Google Chrome.app/...`
20
+ - **THEN** that path appears in the detected paths list
21
+
22
+ #### Scenario: No browser found
23
+ - **WHEN** no Chrome or Edge installation is detected
24
+ - **THEN** an empty list is returned
25
+
26
+ ---
27
+
28
+ ### Requirement: Path list with ordering
29
+ The system SHALL return all detected browser paths sorted by modification time (newest first). Each path SHALL be an absolute, resolved path.
30
+
31
+ #### Scenario: Multiple browsers found
32
+ - **WHEN** both Chrome and Edge are installed
33
+ - **THEN** both paths are returned, sorted by mtime descending
34
+
35
+ #### Scenario: Paths are absolute
36
+ - **WHEN** a browser is found via a relative PATH command on Linux
37
+ - **THEN** the returned path is resolved to absolute
38
+
39
+ ---
40
+
41
+ ### Requirement: Auto-select first path
42
+ The system SHALL provide a function that returns the first detected browser path (newest by mtime), or `None` if no browser is found.
43
+
44
+ #### Scenario: Auto-select with browsers present
45
+ - **WHEN** multiple browsers are installed
46
+ - **THEN** the function returns the path with the most recent modification time
47
+
48
+ #### Scenario: Auto-select with no browsers
49
+ - **WHEN** no browser is found
50
+ - **THEN** the function returns `None`
51
+
52
+ ---
53
+
54
+ ### Requirement: User path validation
55
+ The system SHALL validate a user-provided browser path. It SHALL check that the file exists and is a valid executable. If invalid, it SHALL raise a clear error.
56
+
57
+ #### Scenario: Valid user path
58
+ - **WHEN** user provides a path that exists on disk
59
+ - **THEN** the path is returned as-is (resolved to absolute)
60
+
61
+ #### Scenario: Invalid user path
62
+ - **WHEN** user provides a path that does not exist
63
+ - **THEN** a `BrowserNotFoundError` is raised with a descriptive message
64
+
65
+ ---
66
+
67
+ ### Requirement: ENV override
68
+ The system SHALL respect `HTMLREADER_CHROME_PATH` environment variable. When set, it overrides auto-detection and is used directly (after validation).
69
+
70
+ #### Scenario: ENV path set and valid
71
+ - **WHEN** `HTMLREADER_CHROME_PATH=/usr/bin/google-chrome` is set and the file exists
72
+ - **THEN** that path is returned without scanning
73
+
74
+ #### Scenario: ENV path set but invalid
75
+ - **WHEN** `HTMLREADER_CHROME_PATH=/nonexistent` is set
76
+ - **THEN** a `BrowserNotFoundError` is raised
77
+
78
+ ---
79
+
80
+ ### Requirement: Edge browser support
81
+ The system SHALL detect Microsoft Edge in addition to Google Chrome, on all supported platforms.
82
+
83
+ #### Scenario: Edge on Windows
84
+ - **WHEN** Edge is installed at `C:/Program Files/Microsoft/Edge/Application/msedge.exe`
85
+ - **THEN** that path is detected
86
+
87
+ #### Scenario: Edge on Linux
88
+ - **WHEN** `microsoft-edge-stable` is available on PATH
89
+ - **THEN** its path is detected
90
+
91
+ #### Scenario: Edge on macOS
92
+ - **WHEN** Edge is installed at `/Applications/Microsoft Edge.app/...`
93
+ - **THEN** that path is detected
@@ -0,0 +1,25 @@
1
+ ## 1. Module setup
2
+
3
+ - [ ] 1.1 Create `src/htmlreader/browser.py` with platform path constants (WIN32_PATHS, LINUX_PATHS, DARWIN_PATHS)
4
+ - [ ] 1.2 Define `BrowserNotFoundError` exception
5
+
6
+ ## 2. Detection functions
7
+
8
+ - [ ] 2.1 Implement `_iter_browser_paths()` — platform-aware iterator yielding found paths
9
+ - [ ] 2.2 Implement `detect_browser_paths() -> list[str]` — dedup, resolve to absolute, sort by mtime descending
10
+ - [ ] 2.3 Implement `get_browser_path() -> str | None` — check ENV first, then auto-select first detected
11
+ - [ ] 2.4 Implement `validate_browser_path(path: str) -> str` — validate user path, raise BrowserNotFoundError if invalid
12
+
13
+ ## 3. Public API
14
+
15
+ - [ ] 3.1 Export `BrowserNotFoundError`, `detect_browser_paths`, `get_browser_path`, `validate_browser_path` from `src/htmlreader/__init__.py`
16
+
17
+ ## 4. Tests
18
+
19
+ - [ ] 4.1 Test detect_browser_paths returns list on current platform
20
+ - [ ] 4.2 Test get_browser_path returns path or None (no crash)
21
+ - [ ] 4.3 Test validate_browser_path with valid path
22
+ - [ ] 4.4 Test validate_browser_path with invalid path raises BrowserNotFoundError
23
+ - [ ] 4.5 Test ENV override: HTMLREADER_CHROME_PATH set to valid path
24
+ - [ ] 4.6 Test ENV override: HTMLREADER_CHROME_PATH set to invalid path raises error
25
+ - [ ] 4.7 Test platform constants are non-empty for each OS
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-08-02
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-08-02
@@ -0,0 +1,108 @@
1
+ ## Context
2
+
3
+ The project has HTML simplification (`simplify.py`), render detection (`render_detect.py`), and browser path detection (`browser.py`). The next layer is LLM intelligence: send simplified HTML to an LLM, get back page classification and CSS selector rules.
4
+
5
+ ## Goals / Non-Goals
6
+
7
+ **Goals:**
8
+ - OpenAI-compatible API via httpx2
9
+ - trafilatura metadata as LLM prompt context
10
+ - Page classification (list/detail)
11
+ - CSS selector rule generation for both page types
12
+ - Prompt budget management (<50% of context window)
13
+ - CLI `analyze` command
14
+
15
+ **Non-Goals:**
16
+ - Streaming responses
17
+ - Multi-turn conversation
18
+ - Rule execution (just generation)
19
+ - Rule storage/persistence
20
+ - Multiple LLM provider fallback
21
+
22
+ ## Decisions
23
+
24
+ ### 1. HTTP client for LLM: httpx2
25
+
26
+ **Decision:** Use httpx2 for LLM API communication.
27
+
28
+ **Rationale:** Already a project dependency. Supports async (future), proper timeout, and JSON handling.
29
+
30
+ ### 2. API format: OpenAI chat completions
31
+
32
+ **Decision:** Use `/v1/chat/completions` with `response_format: { type: "json_object" }`.
33
+
34
+ **Rationale:** Widely compatible with most LLM providers. JSON mode ensures structured output.
35
+
36
+ ### 3. Prompt structure
37
+
38
+ **Decision:** Three-part prompt:
39
+ 1. System prompt: role definition + output format instructions
40
+ 2. trafilatura metadata: extracted fields as context
41
+ 3. Simplified HTML: the page content
42
+
43
+ ```python
44
+ system = """You are an HTML analysis expert. Analyze the given HTML and return JSON with:
45
+ - page_type: "list" or "detail"
46
+ - metadata: {title, author, date, ...} from trafilatura
47
+ - rules: CSS selector extraction rules
48
+ - list: [{name, is_main, selectors: {title, link, pubtime}}]
49
+ - detail: {title, author, date, description, image, categories, tags, content}
50
+ """
51
+
52
+ user = f"""Metadata (from trafilatura):
53
+ {metadata_json}
54
+
55
+ HTML:
56
+ {simplified_html}
57
+ """
58
+ ```
59
+
60
+ ### 4. Prompt budget: 50% of max_tokens
61
+
62
+ **Decision:** Estimate token count as `len(text) / 3` (rough CJK/English average). If HTML exceeds 50% of max_tokens, truncate from the middle, keeping head and tail.
63
+
64
+ **Rationale:** Middle truncation preserves page structure (header/nav at top, footer at bottom) while cutting the bulk content.
65
+
66
+ ### 5. trafilatura integration: extract before simplify
67
+
68
+ **Decision:** Run trafilatura on the **raw** HTML (before simplification), because trafilatura's parsers expect full HTML with metadata tags. The simplified HTML goes to LLM.
69
+
70
+ **Order:** raw HTML → trafilatura (metadata) + simplify (HTML) → LLM prompt
71
+
72
+ ### 6. Result structure: TypedDict
73
+
74
+ ```python
75
+ class ListRule(TypedDict):
76
+ name: str # "main", "sidebar", etc.
77
+ is_main: bool
78
+ selectors: dict[str, str] # title, link, pubtime CSS selectors
79
+
80
+ class DetailRules(TypedDict):
81
+ # Each field is Optional CSS selector string
82
+ title: str | None
83
+ author: str | None
84
+ date: str | None
85
+ description: str | None
86
+ image: str | None
87
+ categories: str | None
88
+ tags: str | None
89
+ content: str | None
90
+
91
+ class AnalysisResult(TypedDict):
92
+ page_type: str # "list" or "detail"
93
+ metadata: dict # trafilatura metadata
94
+ list_rules: list[ListRule] | None
95
+ detail_rules: DetailRules | None
96
+ raw_llm_response: str
97
+ ```
98
+
99
+ ### 7. Error handling
100
+
101
+ **Decision:** If LLM returns invalid JSON, log warning and return result with `raw_llm_response` filled but rules as None. If LLM request fails, raise with clear error message.
102
+
103
+ ## Risks / Trade-offs
104
+
105
+ - **[Token estimation inaccuracy]** → `len/3` is rough; may over-truncate or under-truncate. Mitigated by logging actual token usage from API response.
106
+ - **[trafilatura metadata quality]** → Some sites have poor metadata. LLM can still work without it, just less accurate.
107
+ - **[LLM hallucination in selectors]** → Generated selectors may not exist in the HTML. Downstream validation needed (not in this change).
108
+ - **[Cost]** → Each analysis is one LLM call. Budget management prevents runaway costs.