liferay-context-builder 0.6.2__py3-none-any.whl

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.
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.4
2
+ Name: liferay-context-builder
3
+ Version: 0.6.2
4
+ Summary: Build a local, cited Liferay DXP context library for the liferay-expert Claude Code skill.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: <3.14,>=3.10
8
+ Requires-Dist: beautifulsoup4<5,>=4.12
9
+ Requires-Dist: crawl4ai<1.0,>=0.9.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # liferay-context-builder
13
+
14
+ Give Claude Code a local, source-backed Liferay DXP knowledge base it can
15
+ actually read before answering.
16
+
17
+ `liferay-context-builder` turns the public Liferay docs into a local context
18
+ library and pairs it with the `liferay-expert` Claude Code skill. The result is
19
+ simple: when someone on the team asks a Liferay question, the assistant can
20
+ look up the relevant docs, cite the original URL, and avoid guessing from model
21
+ memory.
22
+
23
+ It is built for team use:
24
+
25
+ - Answers stay tied to official `learn.liferay.com` sources.
26
+ - Every project can share the same local docs folder.
27
+ - There is no bundled Liferay content, vector database, or embedding service to
28
+ manage.
29
+ - A doctor command checks whether the docs and skill are ready.
30
+
31
+ ![Demo of liferay-context-builder in Codex](docs/assets/liferay-doc-demo.gif)
32
+
33
+ [Download the MP4 demo](docs/assets/liferay-doc-demo.mp4)
34
+
35
+ [Project page](https://mordonez.github.io/liferay-context-builder/) ·
36
+ [PyPI package](https://pypi.org/project/liferay-context-builder/) · Python
37
+ 3.10-3.13 · [MIT license](LICENSE)
38
+
39
+ ## Quickstart
40
+
41
+ From zero to source-backed Liferay answers in Claude Code:
42
+
43
+ ```bash
44
+ # 1. One-time browser setup for crawl4ai/Playwright
45
+ uvx --from crawl4ai crawl4ai-setup
46
+
47
+ # 2. Build the local Liferay DXP context library in ~/.liferay-docs
48
+ uvx liferay-context-builder
49
+
50
+ # 3. Install the Claude Code skill in your current project
51
+ npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
52
+
53
+ # 4. Verify docs freshness and skill installation
54
+ uvx --from liferay-context-builder liferay-context-builder-doctor
55
+ ```
56
+
57
+ Then ask Claude Code something like:
58
+
59
+ > How do I configure synonym sets in Liferay Search?
60
+
61
+ The skill searches the local context library, reads the best matching pages,
62
+ and cites the original `learn.liferay.com` URL.
63
+
64
+ Keep `-a claude-code` in the install command. It avoids interactive installer
65
+ edge cases where the skill can appear installed but not land in
66
+ `.claude/skills/`.
67
+
68
+ ## Requirements
69
+
70
+ - Python 3.10-3.13
71
+ - [`uv`](https://docs.astral.sh/uv/)
72
+ - Node/npm for `npx skills add`
73
+
74
+ `crawl4ai` uses Playwright. Run the browser setup once per machine before the
75
+ first scrape:
76
+
77
+ ```bash
78
+ uvx --from crawl4ai crawl4ai-setup
79
+ ```
80
+
81
+ ## How It Works
82
+
83
+ ```mermaid
84
+ flowchart LR
85
+ A[learn.liferay.com] --> B[crawl4ai BFS crawl]
86
+ B --> C[local Markdown in ~/.liferay-docs]
87
+ C --> D[search_index.jsonl and anomalies.jsonl]
88
+ C --> E[liferay-expert Claude Code skill]
89
+ D --> E
90
+ E --> F[cited Liferay answers]
91
+ ```
92
+
93
+ The official context builder starts at
94
+ `https://learn.liferay.com/w/dxp/index` and uses crawl4ai's BFS deep crawler to
95
+ follow internal `/w/dxp/*` links. For each page, it extracts the article body,
96
+ classifies the URL into a Liferay capability, and writes Markdown locally.
97
+
98
+ The builder is intentionally boring:
99
+
100
+ - It fetches from the live Liferay docs when you run it; this package does not
101
+ redistribute Liferay documentation text.
102
+ - It writes to one shared docs directory, so every project can use the same
103
+ corpus.
104
+ - It retries through crawl4ai, writes files atomically, and exits non-zero when
105
+ the crawl or page fetches fail.
106
+ - It never starts a long scrape from inside the skill. If docs are missing, the
107
+ skill tells you which command to run.
108
+
109
+ ## Where Files Go
110
+
111
+ By default, everything is written under:
112
+
113
+ ```text
114
+ ~/.liferay-docs
115
+ ```
116
+
117
+ Use `LIFERAY_DOCS_DIR` when you want a repo-local or custom corpus:
118
+
119
+ ```bash
120
+ export LIFERAY_DOCS_DIR="$PWD/.liferay-docs"
121
+ uvx liferay-context-builder
122
+ uvx --from liferay-context-builder liferay-context-builder-doctor
123
+ ```
124
+
125
+ Layout:
126
+
127
+ ```text
128
+ ~/.liferay-docs/
129
+ raw/{capability}/*.md
130
+ raw/_navigation/{capability}/*.md
131
+ raw/_removed/{capability}/*.md
132
+ raw/community-howto/{capability}/*.md
133
+ raw/community-troubleshooting/{capability}/*.md
134
+ reports/filtered/
135
+ search_index.jsonl
136
+ anomalies.jsonl
137
+ summary.json
138
+ *_urls.txt
139
+ ```
140
+
141
+ `raw/{capability}/*.md` is the main official-docs corpus the skill reads first.
142
+ `raw/_navigation/` keeps table-of-contents/navigation pages out of normal
143
+ answers while preserving them. `raw/_removed/` holds pages only after the
144
+ scraper directly confirms their original URL is gone.
145
+
146
+ ## Refreshing Official Docs
147
+
148
+ Run the scraper again whenever you want fresh docs:
149
+
150
+ ```bash
151
+ uvx liferay-context-builder
152
+ ```
153
+
154
+ A normal full run usually takes tens of minutes. For a smoke test:
155
+
156
+ ```bash
157
+ uvx liferay-context-builder --max-pages 200
158
+ ```
159
+
160
+ Useful options:
161
+
162
+ ```bash
163
+ uvx liferay-context-builder --max-depth 12
164
+ uvx liferay-context-builder --max-pages 3000
165
+ ```
166
+
167
+ Each full run starts from the current site state. If a previously known page is
168
+ not rediscovered by BFS, the scraper checks that page directly before moving it
169
+ to `raw/_removed/`. If the page is still alive, it refreshes it directly and
170
+ records the BFS coverage gap in the reports.
171
+
172
+ ## Community Articles
173
+
174
+ Community articles are optional, larger, and lower-authority than the official
175
+ DXP docs:
176
+
177
+ ```bash
178
+ uvx --from liferay-context-builder liferay-context-builder-community
179
+ ```
180
+
181
+ This fetches Liferay community How-To and Troubleshooting articles from
182
+ `learn.liferay.com/kb-article/*`. They are stored separately:
183
+
184
+ ```text
185
+ raw/community-howto/{capability}/*.md
186
+ raw/community-troubleshooting/{capability}/*.md
187
+ ```
188
+
189
+ Many community articles have no usable capability tag, so they go to
190
+ `_uncategorized/`. The skill treats community content as secondary evidence and
191
+ says so when citing it.
192
+
193
+ Useful commands:
194
+
195
+ ```bash
196
+ # Only How-To articles
197
+ uvx --from liferay-context-builder liferay-context-builder-community --resource-type howto
198
+
199
+ # Smaller test run per resource type
200
+ uvx --from liferay-context-builder liferay-context-builder-community --limit 100
201
+ ```
202
+
203
+ Community scraping can take much longer than the official-docs scrape because
204
+ it fetches thousands of additional articles.
205
+
206
+ ## Installing The Skill
207
+
208
+ Install `liferay-expert` into each Claude Code project where you want Liferay
209
+ help:
210
+
211
+ ```bash
212
+ npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
213
+ ```
214
+
215
+ Manual install also works: place the skill file at:
216
+
217
+ ```text
218
+ .claude/skills/liferay-expert/SKILL.md
219
+ ```
220
+
221
+ The skill resolves docs the same way the scraper does:
222
+
223
+ 1. `$LIFERAY_DOCS_DIR`, if set.
224
+ 2. `~/.liferay-docs`, otherwise.
225
+
226
+ When answering, it searches `reports/filtered/search_index.jsonl` when present,
227
+ falls back to normal file search under `raw/`, reads Markdown files directly,
228
+ and cites the `url:` frontmatter. Official docs are preferred over community
229
+ articles when both cover the same topic.
230
+
231
+ ## Doctor
232
+
233
+ Use the doctor when something feels off:
234
+
235
+ ```bash
236
+ uvx --from liferay-context-builder liferay-context-builder-doctor
237
+ ```
238
+
239
+ It checks:
240
+
241
+ - Which docs directory is active.
242
+ - Whether official Markdown exists.
243
+ - How many community Markdown files exist.
244
+ - The official-docs freshness window.
245
+ - Search index and anomaly report entry counts.
246
+ - Whether `.claude/skills/liferay-expert/SKILL.md` exists in the current
247
+ project.
248
+
249
+ To inspect a different project directory:
250
+
251
+ ```bash
252
+ uvx --from liferay-context-builder liferay-context-builder-doctor --project-dir /path/to/project
253
+ ```
254
+
255
+ The doctor does not scrape docs and does not install the skill. It only reports
256
+ status and prints the next command to run.
257
+
258
+ ## Reports
259
+
260
+ The scraper writes agent-facing reports under `reports/filtered/`.
261
+
262
+ `search_index.jsonl` is a local retrieval index. Each JSON line includes title,
263
+ source URL, source type, capability, file path, headings, and `fetched_at`. The
264
+ skill uses it first because it is faster and cleaner than searching every
265
+ Markdown file.
266
+
267
+ `anomalies.jsonl` is an informational scrape-quality report. It flags signals
268
+ like very short bodies, missing titles, known error markers, unusually large
269
+ pages, and large body-size swings versus the previous local copy. It does not
270
+ mean a page is unusable; it means the page may deserve a quick check before you
271
+ trust or cite it heavily.
272
+
273
+ `summary.json` records the latest run counts, crawl failures, direct refreshes,
274
+ coverage gaps, and search index size.
275
+
276
+ ## Troubleshooting
277
+
278
+ **`crawl4ai` or browser errors on the first run**
279
+
280
+ Run the Playwright setup again:
281
+
282
+ ```bash
283
+ uvx --from crawl4ai crawl4ai-setup
284
+ ```
285
+
286
+ **Claude Code says the skill is missing**
287
+
288
+ Run the install command from the project where you are using Claude Code:
289
+
290
+ ```bash
291
+ npx skills add mordonez/liferay-context-builder --skill liferay-expert -a claude-code
292
+ ```
293
+
294
+ Then verify:
295
+
296
+ ```bash
297
+ uvx --from liferay-context-builder liferay-context-builder-doctor
298
+ ```
299
+
300
+ **Claude Code says docs are missing**
301
+
302
+ Check whether you are using a custom docs directory:
303
+
304
+ ```bash
305
+ echo "$LIFERAY_DOCS_DIR"
306
+ ```
307
+
308
+ If it is empty, the skill expects `~/.liferay-docs`. If it points somewhere
309
+ else, run the scraper with that same environment variable.
310
+
311
+ **Docs are stale**
312
+
313
+ Refresh official docs:
314
+
315
+ ```bash
316
+ uvx liferay-context-builder
317
+ ```
318
+
319
+ The doctor warns when official docs are older than about seven days.
320
+
321
+ **A scrape stops partway through**
322
+
323
+ Rerun the same command. Already written Markdown remains usable, but a failed
324
+ run exits non-zero and avoids treating untouched pages as removed.
325
+
326
+ **Community answers feel weaker than official docs**
327
+
328
+ That is expected. Community How-To and Troubleshooting articles are useful for
329
+ practical cases and errors, but the skill should label them as community
330
+ content and prefer official docs when official docs answer the question.
331
+
332
+ ## Development
333
+
334
+ ```bash
335
+ uv sync --group dev
336
+ uv run ruff check .
337
+ uv run --with pytest python -m pytest
338
+ uv build
339
+ ```
340
+
341
+ Run `uv sync --group dev` once before local development so the project and dev
342
+ tools are installed into uv's project environment. The pytest command uses
343
+ `python -m pytest` with `--with pytest` because older or unsynced uv
344
+ environments can fail to find the `pytest` console script even when Python can
345
+ run the module.
346
+
347
+ CI runs lint, tests, and package build on Python 3.10, 3.11, 3.12, and 3.13.
348
+ It does not run a real scrape. Release publishing is documented in
349
+ [`docs/release.md`](docs/release.md).
350
+
351
+ ## License
352
+
353
+ [MIT](LICENSE) applies to this tool and skill only. Liferay documentation
354
+ content remains Liferay's content and is fetched locally by each user.
@@ -0,0 +1,12 @@
1
+ liferay_docs_scraper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ liferay_docs_scraper/classify_pages.py,sha256=yXo6eKLlsFpoM1CKzcZUNEWBN8RcQ1Nc_2qNgqnkLLs,1448
3
+ liferay_docs_scraper/community.py,sha256=I0p6iZz-H66uwo4CPagnqiYbBi0LeexClNMoFdN1fo4,18860
4
+ liferay_docs_scraper/doctor.py,sha256=A3f8jttJ-bw3a_JCouXpJyTp2pxLqEN17XLrcmpTNXY,6859
5
+ liferay_docs_scraper/filter_urls.py,sha256=3qZCcGghJrjnfLNflj5OwwHzsKyKqmPBypVC0KrxaRQ,7403
6
+ liferay_docs_scraper/index.py,sha256=xpdAewS-Edxe90_S0XJGGZT983LzAXVZNWnmxMTcjdQ,6442
7
+ liferay_docs_scraper/pipeline.py,sha256=-JQZBKCmiQl6L62G9U3uPguZZAFiCdAqSTW_HGB2Lkk,24486
8
+ liferay_context_builder-0.6.2.dist-info/METADATA,sha256=LmhbeWEHg6EH0lI83aEaxoDI_Eu-kQQXYXCyErg6OSk,10507
9
+ liferay_context_builder-0.6.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
10
+ liferay_context_builder-0.6.2.dist-info/entry_points.txt,sha256=X5Cc5NdbdtYY4gdrunOYDupeQMZd-OAZYjrIa_bAC48,217
11
+ liferay_context_builder-0.6.2.dist-info/licenses/LICENSE,sha256=li7C8a1CCl-Qhn8tmQ55RDdRnOziMgqa08pfVjJuSyw,1072
12
+ liferay_context_builder-0.6.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ liferay-context-builder = liferay_docs_scraper.pipeline:main
3
+ liferay-context-builder-community = liferay_docs_scraper.community:main
4
+ liferay-context-builder-doctor = liferay_docs_scraper.doctor:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miguel Ordoñez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
File without changes
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """Navigation-vs-content heuristic, shared with crawl4ai_pipeline.py.
3
+
4
+ An "index"/navigation page is one whose body (frontmatter stripped) is
5
+ short and consists mostly of links to subpages -- no substantial technical
6
+ content of its own. Everything else is "content".
7
+
8
+ Heuristic (no API calls, pure text analysis):
9
+ - Strip Markdown link syntax down to visible text (`[text](url)` -> `text`)
10
+ and strip heading/emphasis markup (`#`, `*`, `_`, backticks).
11
+ - total_words: word count of that visible text.
12
+ - link_ratio: fraction of those words that come from inside a Markdown
13
+ link's link-text span.
14
+ - "index" iff total_words < INDEX_MAX_WORDS and link_ratio >= INDEX_MIN_LINK_RATIO.
15
+ """
16
+
17
+ import re
18
+
19
+ INDEX_MAX_WORDS = 150
20
+ INDEX_MIN_LINK_RATIO = 0.5
21
+
22
+ LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]*)\)")
23
+ MARKUP_RE = re.compile(r"[#*_`\\]")
24
+
25
+
26
+ def analyze_body(body: str) -> tuple[int, float]:
27
+ links = LINK_RE.findall(body)
28
+ link_word_count = sum(len(text.split()) for text, _url in links)
29
+
30
+ visible = LINK_RE.sub(lambda m: m.group(1), body)
31
+ visible = MARKUP_RE.sub("", visible)
32
+ total_words = len(visible.split())
33
+
34
+ link_ratio = (link_word_count / total_words) if total_words else 0.0
35
+ return total_words, link_ratio
36
+
37
+
38
+ def classify(total_words: int, link_ratio: float) -> str:
39
+ if total_words < INDEX_MAX_WORDS and link_ratio >= INDEX_MIN_LINK_RATIO:
40
+ return "index"
41
+ return "content"