nous 0.3.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.agents/skills/verify-nous/SKILL.md +6 -0
- data/CHANGELOG.md +75 -1
- data/CODING_STANDARDS.md +203 -0
- data/README.md +175 -17
- data/Rakefile +2 -0
- data/docs/setup/defuddle.md +38 -0
- data/docs/setup/verification.md +21 -0
- data/docs/verification/README.md +77 -0
- data/docs/verification/defuddle.md +45 -0
- data/docs/verification/failure-propagation.md +37 -0
- data/docs/verification/retained-html.md +37 -0
- data/javascript/build.mjs +24 -0
- data/javascript/defuddle.mjs +28 -0
- data/lib/nous/cli.rb +3 -3
- data/lib/nous/crawler/async_page_fetcher.rb +43 -11
- data/lib/nous/crawler/recursive_page_fetcher.rb +34 -26
- data/lib/nous/crawler/single_page_fetcher.rb +49 -9
- data/lib/nous/decode_html.rb +80 -0
- data/lib/nous/extract_html.rb +109 -0
- data/lib/nous/extractor/default/client.rb +30 -12
- data/lib/nous/extractor/default.rb +2 -4
- data/lib/nous/extractor/defuddle/client.rb +57 -0
- data/lib/nous/extractor/defuddle/run_process.rb +104 -0
- data/lib/nous/extractor/defuddle.rb +23 -0
- data/lib/nous/extractor/extraction_error.rb +7 -0
- data/lib/nous/fetcher/extraction_runner.rb +26 -11
- data/lib/nous/fetcher/page_extractor.rb +27 -12
- data/lib/nous/fetcher.rb +46 -10
- data/lib/nous/primitives/configuration.rb +1 -0
- data/lib/nous/primitives/fetch_record.rb +26 -0
- data/lib/nous/primitives/fetch_result.rb +21 -0
- data/lib/nous/primitives/page.rb +1 -1
- data/lib/nous/serializer.rb +9 -1
- data/lib/nous/version.rb +1 -1
- data/lib/nous.rb +2 -2
- data/package-lock.json +858 -0
- data/package.json +15 -0
- data/tasks/verification.rake +23 -0
- data/vendor/THIRD_PARTY_LICENSES.txt +518 -0
- data/vendor/defuddle.mjs +403 -0
- data/vendor/defuddle.mjs.LEGAL.txt +10 -0
- metadata +25 -4
- data/lib/nous/extractor/jina/client.rb +0 -59
- data/lib/nous/extractor/jina.rb +0 -26
- data/lib/nous/primitives/raw_page.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4371689843d5f5275e56afaaab2630ba2a703e57e29cfb79d6d47d19040d934
|
|
4
|
+
data.tar.gz: 1402e3ab54931a99bbad484002345514142a9ade10453938c8f2feae77ef7742
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14a0487839b29fa46c277e341e4c33b631bb22380cbb2ee91954fcff61f264856baf981f2bc8a6e20b6da3e8a81c7c26c7178db5f96d96f5be0e2a53676d72f6
|
|
7
|
+
data.tar.gz: 8e2eeb9b3e790ea85f79d877c19176569918e3030fff096950c6de7771e3ebe9556cdeb82b16a67cb8d9c6ea166c6634c3fec6c40ea8f72ffa9168b0c4fa9370
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-nous
|
|
3
|
+
description: Verify Nous retained-HTML extraction and detailed fetch outcomes through offline Ruby recipes; use when changing extraction or assessing Substrate and Autodidact readiness.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read [the verification guide](../../../docs/verification/README.md) from the repository root. Select the affected recipe, execute launch, doctor, drive, evidence capture and cleanup, and report every exit status. Follow the guide's source-identification and blocker rules. A draft or failed recipe does not certify readiness. Keep provisioning in the linked setup guide; do not add live network calls to these checks.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,72 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.5.0] - 2026-10-25
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `Nous::ExtractHtml.call` extracts retained HTML without network acquisition, with
|
|
6
|
+
Markdown or plain text, source metadata, explicit decoding errors and caller byte limits.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Defuddle is the default for URL and retained-HTML extraction. The gem bundles
|
|
11
|
+
its JavaScript dependencies and requires Node.js 22+ on macOS/Linux.
|
|
12
|
+
- Readability remains available through `Extractor::Default` and `--readability`.
|
|
13
|
+
Extraction failures do not silently switch backends.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- Jina Reader integration (`Extractor::Jina`, `--jina`, `JINA_API_KEY`). Neither
|
|
18
|
+
remaining backend renders JavaScript pages.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Detailed fetch results retain extraction failures and all attempted failures from
|
|
23
|
+
concurrent crawl batches, with accurate request counts and unchanged page limits.
|
|
24
|
+
|
|
25
|
+
## [0.4.0] - 2026-04-11
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **New `details: true` option for `Nous.fetch`** - Returns a `FetchResult` object containing both successful pages and failed fetch/extraction attempts. This enables explicit failure handling without exceptions.
|
|
30
|
+
```ruby
|
|
31
|
+
result = Nous.fetch("https://example.com", details: true)
|
|
32
|
+
result.pages # Array<Page> - successfully extracted
|
|
33
|
+
result.failures # [{requested_url:, error:}, ...]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **Page metadata** - Every extracted page now includes provenance information:
|
|
37
|
+
- `extractor`: Which extractor backend was used (e.g., "Nous::Extractor::Default")
|
|
38
|
+
- `requested_url`: The original URL before any redirects
|
|
39
|
+
- `content_type`: HTTP Content-Type header from the response
|
|
40
|
+
- `redirected`: Boolean indicating if redirects occurred
|
|
41
|
+
|
|
42
|
+
- **FetchRecord internal primitive** - Unified fetch result representation that captures both success and failure cases with full provenance tracking. Replaces the previous `RawPage` which only handled successful fetches.
|
|
43
|
+
|
|
44
|
+
- **Configuration#single_page? helper** - Convenience predicate method for checking if the current configuration is in single-page (non-recursive) mode.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Improved title extraction** - Title extraction now uses a fallback chain: readability extracted title → HTML `<title>` tag → first `<h1>` element. This significantly improves title reliability on pages where readability fails to identify the title.
|
|
49
|
+
|
|
50
|
+
- **Reduced aggressive DOM stripping** - The default extractor now preserves more content before readability processing. Previously removed elements (`header`, `img`, `video`, `svg`, `link`) are now retained, providing better context for readability scoring and preserving useful content like captions and bylines.
|
|
51
|
+
|
|
52
|
+
- **Unified fetch contract** - Both single-page and recursive crawling now use the same internal `FetchRecord` structure, ensuring consistent provenance tracking and failure handling across all fetch modes.
|
|
53
|
+
|
|
54
|
+
- **Serializer schema updated** - Both text and JSON output formats now include:
|
|
55
|
+
- `pathname`: URL path component
|
|
56
|
+
- `extractor`: Which extractor processed the page
|
|
57
|
+
- Full metadata object (JSON only)
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **JSON serialization** - The JSON output now correctly includes the `pathname` field that was documented but missing in previous versions.
|
|
62
|
+
|
|
63
|
+
- **Extraction failure visibility** - Previously, extraction failures were only visible with debug logging enabled. The new `FetchResult` structure makes failures programmatically accessible.
|
|
64
|
+
|
|
65
|
+
### Internal Changes
|
|
66
|
+
|
|
67
|
+
- **Duck-typed extractor interface** - Extractors now receive the full `FetchRecord` object and can access the fields they need (`Default` uses `record.html`, `Jina` uses `record.final_url`).
|
|
68
|
+
|
|
69
|
+
- **Removed `RawPage` primitive** - Superseded by the richer `FetchRecord` which handles both success and failure uniformly.
|
|
2
70
|
|
|
3
71
|
## [0.3.0] - 2026-02-23
|
|
4
72
|
|
|
@@ -29,3 +97,9 @@
|
|
|
29
97
|
## [0.1.0] - 2026-02-21
|
|
30
98
|
|
|
31
99
|
- Initial release
|
|
100
|
+
|
|
101
|
+
[Unreleased]: https://github.com/danfrenette/nous/compare/v0.4.0...HEAD
|
|
102
|
+
[0.4.0]: https://github.com/danfrenette/nous/compare/v0.3.0...v0.4.0
|
|
103
|
+
[0.3.0]: https://github.com/danfrenette/nous/compare/v0.2.0...v0.3.0
|
|
104
|
+
[0.2.0]: https://github.com/danfrenette/nous/compare/v0.1.0...v0.2.0
|
|
105
|
+
[0.1.0]: https://github.com/danfrenette/nous/releases/tag/v0.1.0
|
data/CODING_STANDARDS.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Coding standards
|
|
2
|
+
|
|
3
|
+
Read these standards before implementation and include them in Anaphor's Standards
|
|
4
|
+
review. They apply to this Ruby gem and its development tools. Existing code is not
|
|
5
|
+
a blanket exception: when changing an affected boundary, address deviations or
|
|
6
|
+
record a concrete compatibility reason in the PR. This document does not itself
|
|
7
|
+
change existing public APIs.
|
|
8
|
+
|
|
9
|
+
## Responsibilities and service objects
|
|
10
|
+
|
|
11
|
+
Keep crawling, readable-content extraction, conversion, and serialization distinct.
|
|
12
|
+
Commands perform operations; fetch records describe acquired originals; pages describe
|
|
13
|
+
extracted content and provenance. Keep HTTP details in crawler and remote extractor
|
|
14
|
+
adapters. Follow the existing Zeitwerk layout; `lib/nous/primitives/` is collapsed
|
|
15
|
+
into the `Nous` namespace.
|
|
16
|
+
|
|
17
|
+
A command exposes one public instance operation, `call`, invoked through inherited
|
|
18
|
+
`.call`. Its initializer accepts explicit inputs and dependencies and stores state;
|
|
19
|
+
construction must not launch processes, perform network requests, or do business
|
|
20
|
+
work. Resolve environment-dependent defaults when the operation runs.
|
|
21
|
+
|
|
22
|
+
Application-service instance `call` methods that describe business operations must
|
|
23
|
+
contain only bareword calls to private methods named for business functions. Read
|
|
24
|
+
them as prose: one named step per line, without arguments, explicit receivers,
|
|
25
|
+
assignments, conditionals, loops, blocks, or explicit returns. A `rescue` clause
|
|
26
|
+
is the only exception; it may map an expected exception to a failure result.
|
|
27
|
+
Put decisions, resource scopes, collaborator calls, and result construction inside
|
|
28
|
+
the private steps. The final step returns the service result. For example:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
def call
|
|
32
|
+
prepare_retained_content
|
|
33
|
+
extract_readable_content
|
|
34
|
+
build_extraction_result
|
|
35
|
+
rescue ArgumentError => error
|
|
36
|
+
failure(error)
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A failed prerequisite must stop the sequence: raise a meaningful exception handled
|
|
41
|
+
by the service or the existing command boundary. Returning a failure from a private
|
|
42
|
+
step alone does not stop later steps. Keep resource acquisition, use and cleanup
|
|
43
|
+
in one private scope rather than spreading temporary paths across orchestration
|
|
44
|
+
steps. Do not add instance state merely to split an indivisible operation.
|
|
45
|
+
|
|
46
|
+
This rule governs business-operation services, including extraction workflows. The shared class-level dispatcher and low-level value/protocol helpers
|
|
47
|
+
are not business orchestration. Apply the rule when changing an existing service;
|
|
48
|
+
this change does not claim that all historical commands have been migrated.
|
|
49
|
+
|
|
50
|
+
Before extracting an object, name the policy, resource, or collaborator contract
|
|
51
|
+
it will own. If no separate owner exists, keep the steps private in the current
|
|
52
|
+
service. Do not extract forwarding layers to satisfy a line count: they add places
|
|
53
|
+
to navigate without reducing the caller's obligations. Name an extraction for its
|
|
54
|
+
operation, not Manager, Processor, or Utils. Keep duplication when sharing would
|
|
55
|
+
require unrelated behavior flags; those flags couple operations that change apart.
|
|
56
|
+
|
|
57
|
+
Use Sandi Metz's five-line methods, 100-line classes, and four-parameter guideline
|
|
58
|
+
as review prompts, not mechanical limits. Count independent keyword options as
|
|
59
|
+
parameters. A growing constructor suggests mixed responsibilities; hiding those
|
|
60
|
+
options in a hash does not fix that. Introduce an input object only when its data
|
|
61
|
+
has cohesive behavior or invariants. Explain justified exceptions locally in the PR.
|
|
62
|
+
|
|
63
|
+
Apply tell, don't ask: put an operation with the object that owns its behavior.
|
|
64
|
+
Use a known collaborator's interface instead of branching on `is_a?`, `kind_of?`,
|
|
65
|
+
or `respond_to?`. At an untrusted provider boundary, validate the actual protocol
|
|
66
|
+
and required data before producing normalized content. Do not spread speculative
|
|
67
|
+
type checks through in-process callers.
|
|
68
|
+
|
|
69
|
+
## Building and configuring objects
|
|
70
|
+
|
|
71
|
+
Prefer `tap` when configuring and returning the same object, and
|
|
72
|
+
`each_with_object` when accumulating into a mutable collection, instead of a
|
|
73
|
+
temporary local used only to initialize, mutate, and return that object. This
|
|
74
|
+
keeps construction and its result in one expression. Use `map` for one-to-one
|
|
75
|
+
transformations; keep named locals when they explain an intermediate result or
|
|
76
|
+
are reused. With `each_with_object`, mutate the accumulator rather than reassign
|
|
77
|
+
the block variable. These blocks belong inside private service steps, preserving
|
|
78
|
+
the bareword-only business `call` convention.
|
|
79
|
+
|
|
80
|
+
## Arguments and defaults
|
|
81
|
+
|
|
82
|
+
For each proposed argument, inspect its callers and choose in this order:
|
|
83
|
+
|
|
84
|
+
1. If no caller or accepted use case needs the choice, do not add the argument.
|
|
85
|
+
A test convenience alone does not justify expanding the public API.
|
|
86
|
+
2. If every caller must supply a value, require it.
|
|
87
|
+
3. If omission selects a normal value, give it a concrete default or default
|
|
88
|
+
expression. Do not use `nil` merely to replace it with that value later.
|
|
89
|
+
4. If absence changes the operation, document it and permit `nil`. For example,
|
|
90
|
+
`selector: nil` leaves extraction unscoped.
|
|
91
|
+
5. If automatic selection differs from an explicit override, represent that mode
|
|
92
|
+
explicitly and test both paths. Discover environment dependencies during `call`,
|
|
93
|
+
not construction, so creating the service does not launch work.
|
|
94
|
+
|
|
95
|
+
For an existing argument, record the behavior of omission, explicit `nil`, and
|
|
96
|
+
explicit values before editing. Preserve supported cases and test the changed
|
|
97
|
+
boundary. Do not silently make a strict executable override fall back to another
|
|
98
|
+
installation. Add a sentinel or null object only when those behaviors must differ;
|
|
99
|
+
otherwise it adds another state every caller must understand.
|
|
100
|
+
|
|
101
|
+
## Results, failures, and resources
|
|
102
|
+
|
|
103
|
+
Use the existing `Nous::Command` and `Nous::Command::Result` contract in
|
|
104
|
+
[lib/nous/command.rb](lib/nous/command.rb): commands return `payload`, `error`, and
|
|
105
|
+
`metadata`; callers inspect `success?` or `failure?`. Extractors expose `extract(record)`
|
|
106
|
+
and return the same result contract; do not rename that protocol as a style change. Do not copy Rails `ApplicationService`, named
|
|
107
|
+
result fields, HTTP error codes, or reporting infrastructure into this gem.
|
|
108
|
+
|
|
109
|
+
The inherited class `.call` currently converts `StandardError` into a failure
|
|
110
|
+
result, wrapping post-construction errors as `CommandError`. Preserve that behavior
|
|
111
|
+
unless an explicit API change is accepted. Direct instance `call` and extractor
|
|
112
|
+
`extract` do not receive that dispatcher protection. A failed child command does not
|
|
113
|
+
short-circuit its parent automatically: inspect its result and stop before any
|
|
114
|
+
step that depends on success. Keep stage-specific errors useful without including
|
|
115
|
+
credentials or sensitive source content. Do not turn a failure into empty success.
|
|
116
|
+
|
|
117
|
+
One owner acquires, uses, and releases each temporary resource. Keep cleanup in a
|
|
118
|
+
block or `ensure` that covers success and failure, including parsing. Do not return
|
|
119
|
+
a temporary path after its owner has released it. Never use the original input as
|
|
120
|
+
an output destination. Invoke subprocesses with argument arrays, not interpolated
|
|
121
|
+
shell strings. Make partial external effects explicit; cleanup cannot undo them all.
|
|
122
|
+
|
|
123
|
+
## Development and verification commands
|
|
124
|
+
|
|
125
|
+
Choose the command's owner before adding a file:
|
|
126
|
+
|
|
127
|
+
1. For a repeatable project check, add a named Rake task. Put live checks under
|
|
128
|
+
`verify:` and document inputs in the owning recipe so agents can discover them
|
|
129
|
+
with `bundle exec rake -T`. Create these directories when a check needs them.
|
|
130
|
+
2. Put verification assertions, fixture preparation, and cleanup in a focused
|
|
131
|
+
harness under `spec/verification/`. Keep `tasks/*.rake` limited to task inputs
|
|
132
|
+
and dispatch, so command plumbing does not obscure what the check proves.
|
|
133
|
+
3. Use `bin/` for setup and console; the existing public command lives in `exe/nous`. Do not
|
|
134
|
+
add a generic scripts directory for verification commands; it creates a second
|
|
135
|
+
entry-point convention to discover and maintain.
|
|
136
|
+
|
|
137
|
+
Keep production behavior in the library and exercise its public API from the
|
|
138
|
+
harness. Never add downloads or live provider calls to default `bundle exec rake`;
|
|
139
|
+
ordinary verification must run without external services or model provisioning.
|
|
140
|
+
|
|
141
|
+
Document required binaries, models, credentials, supported runtime versions, and
|
|
142
|
+
readiness checks in `docs/setup/`. Keep executable verification steps and evidence
|
|
143
|
+
limits in `docs/verification/`. Link both from onboarding. Install dependencies through explicit setup
|
|
144
|
+
steps, never silently during a check; readiness and behavior verification are separate results. Retain evidence with
|
|
145
|
+
the tested source revision; never claim that simulated processes prove a local
|
|
146
|
+
runtime works.
|
|
147
|
+
|
|
148
|
+
## Tests and review
|
|
149
|
+
|
|
150
|
+
Test observable outcomes through command `.call` or the owning source interface.
|
|
151
|
+
Do not assert private helper order. When a workflow has prerequisites, cover failure before dependent steps. When it
|
|
152
|
+
owns files or processes, cover cleanup and input preservation on success and
|
|
153
|
+
failure. When calls can overlap, prove their resources stay isolated. Use
|
|
154
|
+
controlled provider/process fixtures for ordinary tests and separate opt-in live
|
|
155
|
+
checks for actual compatibility. Keep fixtures' provenance and limitations visible.
|
|
156
|
+
|
|
157
|
+
For Ruby changes, run affected specs, the full RSpec suite, and Standard using
|
|
158
|
+
the Development commands in [README.md](README.md). Include an affected verification
|
|
159
|
+
recipe once one exists. Documentation-only changes need
|
|
160
|
+
link, command, and contract checks; they do not require pretending runtime behavior
|
|
161
|
+
changed. Anaphor reviews must distinguish documented rules, design heuristics, and
|
|
162
|
+
accepted requirements. A green suite does not establish compliance with every
|
|
163
|
+
principle or prove live-provider behavior.
|
|
164
|
+
|
|
165
|
+
## Retained HTML convention
|
|
166
|
+
|
|
167
|
+
`ExtractHtml` keeps six explicit inputs: original bytes, source URL, content type,
|
|
168
|
+
output representation and two independent caller budgets. This is a deliberate
|
|
169
|
+
exception to the parameter heuristic: a hash or options object without cohesive
|
|
170
|
+
behavior would only hide the same choices. The command orchestrates decoding, readable
|
|
171
|
+
selection, rendering and bounded output. `DecodeHtml` owns the byte interpretation
|
|
172
|
+
policy shared with acquired Defuddle HTML. Within `ExtractHtml`, invoke `DecodeHtml`
|
|
173
|
+
and `Defuddle::Client` with `new(...).call`: this deliberate exception to inherited
|
|
174
|
+
`.call` preserves the original unexpected exception as `UnexpectedFailure#cause`.
|
|
175
|
+
The shared dispatcher wraps those exceptions and loses their identity. Keep this
|
|
176
|
+
exception local to the retained command's diagnostic contract and cover failures
|
|
177
|
+
during dependency execution. Trust the client's normalized title and Markdown;
|
|
178
|
+
the retained command owns final rendering, emptiness and combined-byte budgets.
|
|
179
|
+
Legacy shared extractors must
|
|
180
|
+
not acquire per-record mutable state: the fetch runner calls them concurrently.
|
|
181
|
+
|
|
182
|
+
## Sources and adaptation
|
|
183
|
+
|
|
184
|
+
Adapted from Substrate's `CODING_STANDARDS.md` on October 25, 2026, which was
|
|
185
|
+
adapted from Autodidact's standards. Nous-specific responsibilities, result names,
|
|
186
|
+
extractor protocol, executable locations, and verification commands replace their
|
|
187
|
+
Substrate equivalents. Local audio conventions do not apply to Nous.
|
|
188
|
+
Retained service orchestration, explicit inputs, failure sequencing, ownership,
|
|
189
|
+
and public-interface testing. Rails controllers, Active Record, transactions,
|
|
190
|
+
Jbuilder, Sentry, Next.js, and that application's result shape are outside this
|
|
191
|
+
project's conventions. The source document provides context, not inherited
|
|
192
|
+
instructions to provision those systems.
|
|
193
|
+
|
|
194
|
+
- [Thoughtbot: Tell, Don't Ask](https://thoughtbot.com/blog/tell-dont-ask)
|
|
195
|
+
informs behavior ownership and collaborator interfaces.
|
|
196
|
+
- [Thoughtbot: Sandi Metz' Rules for Developers](https://thoughtbot.com/blog/sandi-metz-rules-for-developers)
|
|
197
|
+
supplies the size and parameter heuristics; their use as review prompts here is
|
|
198
|
+
a local convention.
|
|
199
|
+
- [Sandi Metz: The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction)
|
|
200
|
+
informs the preference for cohesive objects over premature generalization.
|
|
201
|
+
|
|
202
|
+
Nullable-default policy and Rake placement are project decisions, not rules
|
|
203
|
+
attributed to these authors.
|
data/README.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Crawl websites and extract readable Markdown, optimized for LLM consumption. Inspired by [sitefetch](https://github.com/egoist/sitefetch).
|
|
4
4
|
|
|
5
|
-
Nous fetches same-host pages starting from a seed URL, extracts readable content, and outputs clean Markdown as XML-tagged text or JSON. It supports concurrent crawling, glob-based URL filtering, and
|
|
5
|
+
Nous fetches same-host pages starting from a seed URL, extracts readable content, and outputs clean Markdown as XML-tagged text or JSON. It supports concurrent crawling, glob-based URL filtering, and local extraction with Defuddle by default and an explicit ruby-readability backup. Both parse acquired HTML without rendering JavaScript. Defuddle decodes acquired bytes using the same header/BOM/meta/UTF-8 precedence as retained HTML; invalid bytes produce extraction failures.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
+
Default extraction requires Node.js 22+ on PATH (macOS/Linux). The gem includes
|
|
10
|
+
the bundled Defuddle adapter; npm is only needed to rebuild it during development.
|
|
11
|
+
See [Defuddle setup](docs/setup/defuddle.md).
|
|
12
|
+
|
|
9
13
|
Add to your Gemfile:
|
|
10
14
|
|
|
11
15
|
```ruby
|
|
@@ -39,8 +43,8 @@ nous https://example.com -m "/blog/*"
|
|
|
39
43
|
# Scope extraction to a CSS selector
|
|
40
44
|
nous https://example.com -s "article.post"
|
|
41
45
|
|
|
42
|
-
# Use
|
|
43
|
-
nous https://example.com --
|
|
46
|
+
# Use the Ruby-only Readability backup
|
|
47
|
+
nous https://example.com --readability
|
|
44
48
|
|
|
45
49
|
# Debug logging
|
|
46
50
|
nous https://example.com -d
|
|
@@ -57,20 +61,22 @@ nous https://example.com -d
|
|
|
57
61
|
| `-s`, `--selector SELECTOR` | CSS selector to scope extraction | none |
|
|
58
62
|
| `-l`, `--limit N` | Maximum pages to fetch | `100` |
|
|
59
63
|
| `--timeout N` | Per-request timeout in seconds | `15` |
|
|
60
|
-
| `--
|
|
64
|
+
| `--readability` | Use the Ruby-only Readability backup | off |
|
|
61
65
|
| `-v`, `--version` | Print version and exit | off |
|
|
62
66
|
| `-h`, `--help` | Print usage and exit | off |
|
|
63
67
|
| `-d`, `--debug` | Debug logging to stderr | off |
|
|
64
68
|
|
|
65
69
|
## Ruby API
|
|
66
70
|
|
|
71
|
+
### Basic Usage
|
|
72
|
+
|
|
67
73
|
```ruby
|
|
68
74
|
require "nous"
|
|
69
75
|
|
|
70
76
|
# Fetch pages with the default extractor
|
|
71
77
|
pages = Nous.fetch("https://example.com", limit: 10, concurrency: 3)
|
|
72
78
|
|
|
73
|
-
# Each page is a Nous::Page with title, url, pathname, content
|
|
79
|
+
# Each page is a Nous::Page with title, url, pathname, content, metadata
|
|
74
80
|
pages.each do |page|
|
|
75
81
|
puts "#{page.title} (#{page.url})"
|
|
76
82
|
puts page.content
|
|
@@ -82,22 +88,161 @@ text = Nous.serialize(pages, format: :text)
|
|
|
82
88
|
# Serialize to JSON
|
|
83
89
|
json = Nous.serialize(pages, format: :json)
|
|
84
90
|
|
|
85
|
-
#
|
|
86
|
-
pages = Nous.fetch("https://
|
|
87
|
-
extractor: Nous::Extractor::
|
|
91
|
+
# Select the Readability backup explicitly
|
|
92
|
+
pages = Nous.fetch("https://example.com",
|
|
93
|
+
extractor: Nous::Extractor::Default.new,
|
|
88
94
|
limit: 5
|
|
89
95
|
)
|
|
90
96
|
```
|
|
91
97
|
|
|
98
|
+
### Detailed Results
|
|
99
|
+
|
|
100
|
+
Use the `details: true` option to receive full fetch results including failures:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
result = Nous.fetch("https://example.com", details: true)
|
|
104
|
+
|
|
105
|
+
result.pages # Array<Nous::Page> - successfully extracted pages
|
|
106
|
+
result.failures # Array<{requested_url:, error:}> - failed acquisition or extraction
|
|
107
|
+
result.total_requested # Integer - total URLs attempted
|
|
108
|
+
result.all_succeeded? # Boolean - true if no failures
|
|
109
|
+
result.any_succeeded? # Boolean - true if at least one page extracted
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Acquisition failures retain their existing messages; extraction failures retain their
|
|
113
|
+
exception objects. Detailed results include failed recursive requests as well as
|
|
114
|
+
failed extraction. Pages-only calls still return successful pages in source order.
|
|
115
|
+
A concurrent batch may already have attempted more successful pages than the limit
|
|
116
|
+
allows returning. `total_requested` counts every attempted request in that batch;
|
|
117
|
+
failures remain visible, while excess successful pages are omitted from `pages`.
|
|
118
|
+
The limit still governs retained successful pages, not failed attempts.
|
|
119
|
+
|
|
120
|
+
This is useful when you need to handle failures explicitly:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
result = Nous.fetch("https://example.com/api-docs", details: true)
|
|
124
|
+
|
|
125
|
+
if result.failures.any?
|
|
126
|
+
puts "Failed to fetch:"
|
|
127
|
+
result.failures.each do |failure|
|
|
128
|
+
puts " #{failure[:requested_url]}: #{failure[:error]}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
result.pages.each do |page|
|
|
133
|
+
puts "Successfully extracted: #{page.title}"
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Retained HTML extraction
|
|
138
|
+
|
|
139
|
+
Use saved bytes directly, without crawling or reacquiring a URL:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
result = Nous::ExtractHtml.call(
|
|
143
|
+
html: File.binread("saved.html"),
|
|
144
|
+
source_url: "https://example.com/article",
|
|
145
|
+
content_type: "text/html; charset=utf-8",
|
|
146
|
+
format: :text
|
|
147
|
+
)
|
|
148
|
+
raise result.error if result.failure?
|
|
149
|
+
puts result.payload.title
|
|
150
|
+
puts result.payload.content
|
|
151
|
+
puts result.metadata
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`html:` and `source_url:` are required. The URL is provenance only; this command
|
|
155
|
+
never requests it. `content_type:` defaults to `"text/html"`; HTML and XHTML are
|
|
156
|
+
supported. `format:` defaults to `:markdown`; `:text` returns readable text with
|
|
157
|
+
collapsed whitespace and separated blocks. Defuddle is used for both formats. Legacy `Extractor::Default#extract`
|
|
158
|
+
continues producing Markdown with its existing selector behavior.
|
|
159
|
+
|
|
160
|
+
Character decoding uses a declared content-type charset first, then a UTF BOM,
|
|
161
|
+
then an HTML meta charset in the first 1024 bytes, then UTF-8. Input is treated as
|
|
162
|
+
bytes regardless of its Ruby encoding label and is never modified. Invalid byte
|
|
163
|
+
sequences and decoded NULs fail; unsupported charset names fail explicitly. A
|
|
164
|
+
conflicting meta declaration does not override a declared header charset.
|
|
165
|
+
|
|
166
|
+
The result payload remains `Nous::ExtractedContent` (`title`, `content`). Metadata
|
|
167
|
+
contains `source_url`, original `content_type`, selected `encoding`, `format`, and
|
|
168
|
+
limitations `static_html_only` and `authentication_not_detected`. Static readable
|
|
169
|
+
content does not prove that an authenticated or JavaScript-rendered page is complete.
|
|
170
|
+
|
|
171
|
+
`max_input_bytes:` defaults to 25 MiB; `max_output_bytes:` defaults to 32 MiB.
|
|
172
|
+
Both require positive integers. Input is checked before copying/parsing. Output
|
|
173
|
+
counts the combined UTF-8 bytes of title and content, excluding caller-supplied
|
|
174
|
+
provenance metadata. These are input/result guards, not peak-memory, CPU or wall-time
|
|
175
|
+
limits; callers needing those protections must retain process supervision.
|
|
176
|
+
|
|
177
|
+
Expected errors are nested under `Nous::ExtractHtml`:
|
|
178
|
+
|
|
179
|
+
| Error | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `InvalidContent` | Invalid encoded bytes, NULs, or invalid decoded output |
|
|
182
|
+
| `UnsupportedContent` | Unsupported media type, output format or charset |
|
|
183
|
+
| `NoContent` | No readable content or empty converted output |
|
|
184
|
+
| `InputLimitExceeded` / `OutputLimitExceeded` | Caller byte budget exceeded |
|
|
185
|
+
| `InvalidLimits` (`ArgumentError`) | Nonpositive or noninteger byte limit |
|
|
186
|
+
| `UnexpectedFailure` | Safe outward message; original exception in `cause` |
|
|
187
|
+
|
|
188
|
+
Treat diagnostic causes as private debugging data; their messages can contain
|
|
189
|
+
source content. Rails acquisition, storage, lifecycle and process supervision are
|
|
190
|
+
outside this command. See the [verification map](docs/verification/README.md) for
|
|
191
|
+
executed surfaces and remaining consumer acceptance.
|
|
192
|
+
|
|
193
|
+
### Page Structure
|
|
194
|
+
|
|
195
|
+
Each extracted page contains:
|
|
196
|
+
|
|
197
|
+
| Field | Type | Description |
|
|
198
|
+
|-------|------|-------------|
|
|
199
|
+
| `title` | String | Page title selected by the extraction backend |
|
|
200
|
+
| `url` | String | Final URL after redirects |
|
|
201
|
+
| `pathname` | String | URL path component |
|
|
202
|
+
| `content` | String | Extracted content as Markdown |
|
|
203
|
+
| `metadata` | Hash | Provenance information (see below) |
|
|
204
|
+
|
|
205
|
+
### Page Metadata
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
page.metadata # => {
|
|
209
|
+
# extractor: "Nous::Extractor::Defuddle", # Which extractor was used
|
|
210
|
+
# requested_url: "https://example.com/blog", # Original URL before redirects
|
|
211
|
+
# content_type: "text/html; charset=utf-8", # HTTP Content-Type header
|
|
212
|
+
# redirected: true # Whether redirects occurred
|
|
213
|
+
# }
|
|
214
|
+
```
|
|
215
|
+
|
|
92
216
|
## Extraction Backends
|
|
93
217
|
|
|
94
|
-
###
|
|
218
|
+
### Defuddle (default)
|
|
219
|
+
|
|
220
|
+
Runs the bundled Defuddle parser in Node.js against the acquired HTML and final
|
|
221
|
+
URL. Returns Markdown directly, retaining structure such as code blocks, tables,
|
|
222
|
+
and links. Supports `--selector`. Each extraction owns its process, with a
|
|
223
|
+
15-second deadline, a 64 MiB JSON response limit and a 64 KiB diagnostic limit.
|
|
224
|
+
These are separate from HTTP timeouts and retained-command byte budgets.
|
|
225
|
+
Page scripts do not execute and third-party fallback fetching is disabled.
|
|
226
|
+
Failures remain visible through `details: true`; there is no automatic switch
|
|
227
|
+
to Readability. `ExtractHtml` uses this same engine while retaining its decoding,
|
|
228
|
+
plain-text rendering and byte-budget contract.
|
|
95
229
|
|
|
96
|
-
|
|
230
|
+
### Readability (explicit backup)
|
|
97
231
|
|
|
98
|
-
|
|
232
|
+
Parses static HTML using [ruby-readability](https://github.com/cantino/ruby-readability), strips noisy elements (script, style, nav, footer), and converts to Markdown via [reverse_markdown](https://github.com/xijo/reverse_markdown). Fast and requires no external services, but cannot extract content from JS-rendered pages.
|
|
99
233
|
|
|
100
|
-
|
|
234
|
+
Title extraction uses a fallback chain:
|
|
235
|
+
1. Readability's extracted title
|
|
236
|
+
2. Original `<title>` tag from HTML
|
|
237
|
+
3. First `<h1>` from extracted content
|
|
238
|
+
|
|
239
|
+
### Migrating from Jina
|
|
240
|
+
|
|
241
|
+
`Extractor::Jina` and `--jina` have been removed. Remove the extractor argument or
|
|
242
|
+
flag to use Defuddle; `JINA_API_KEY` is no longer used. This retires remote browser
|
|
243
|
+
rendering: pages requiring JavaScript may yield incomplete or no readable content.
|
|
244
|
+
Use `Extractor::Default.new` or `--readability` for the Ruby-only backup.
|
|
245
|
+
`ExtractHtml` always uses Defuddle; its result and error shapes remain unchanged.
|
|
101
246
|
|
|
102
247
|
## Output Formats
|
|
103
248
|
|
|
@@ -107,13 +252,15 @@ XML-tagged output designed for LLM context windows:
|
|
|
107
252
|
|
|
108
253
|
```xml
|
|
109
254
|
<page>
|
|
110
|
-
<title>Page Title</title>
|
|
111
|
-
<url>https://example.com/page</url>
|
|
112
|
-
<
|
|
255
|
+
<title>Page Title</title>
|
|
256
|
+
<url>https://example.com/page</url>
|
|
257
|
+
<pathname>/page</pathname>
|
|
258
|
+
<extractor>Nous::Extractor::Defuddle</extractor>
|
|
259
|
+
<content>
|
|
113
260
|
# Heading
|
|
114
261
|
|
|
115
262
|
Extracted markdown content...
|
|
116
|
-
</content>
|
|
263
|
+
</content>
|
|
117
264
|
</page>
|
|
118
265
|
```
|
|
119
266
|
|
|
@@ -125,13 +272,24 @@ Extracted markdown content...
|
|
|
125
272
|
"title": "Page Title",
|
|
126
273
|
"url": "https://example.com/page",
|
|
127
274
|
"pathname": "/page",
|
|
128
|
-
"content": "# Heading\n\nExtracted markdown content..."
|
|
275
|
+
"content": "# Heading\n\nExtracted markdown content...",
|
|
276
|
+
"metadata": {
|
|
277
|
+
"extractor": "Nous::Extractor::Defuddle",
|
|
278
|
+
"requested_url": "https://example.com/page",
|
|
279
|
+
"content_type": "text/html; charset=utf-8",
|
|
280
|
+
"redirected": false
|
|
281
|
+
}
|
|
129
282
|
}
|
|
130
283
|
]
|
|
131
284
|
```
|
|
132
285
|
|
|
133
286
|
## Development
|
|
134
287
|
|
|
288
|
+
Read [CODING_STANDARDS.md](CODING_STANDARDS.md) before implementation and include it
|
|
289
|
+
in Anaphor Standards reviews. Use the [verification map](docs/verification/README.md)
|
|
290
|
+
and project-local [verify-nous skill](.agents/skills/verify-nous/SKILL.md) for the
|
|
291
|
+
retained-HTML and failure-propagation readiness checks.
|
|
292
|
+
|
|
135
293
|
```bash
|
|
136
294
|
bin/setup # Install dependencies
|
|
137
295
|
bundle exec rspec # Run tests
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Defuddle runtime
|
|
2
|
+
|
|
3
|
+
Default URL and retained-HTML extraction requires Node.js 22 or newer on PATH.
|
|
4
|
+
The process adapter currently targets macOS/Linux (POSIX process groups). Install
|
|
5
|
+
Node explicitly using your normal runtime manager. No API key or service is used.
|
|
6
|
+
Check readiness from the repository root:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
node --version
|
|
10
|
+
node -e 'if (Number(process.versions.node.split(".")[0]) < 22) process.exit(1)'
|
|
11
|
+
printf '%s' '{"html":"<html><head><title>Probe</title></head><body><p>Readable probe.</p></body></html>","url":"https://example.com"}' | node vendor/defuddle.mjs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Require exit zero and JSON containing title `Probe` and Markdown `Readable probe.`.
|
|
15
|
+
The published gem includes `vendor/defuddle.mjs` and third-party license notices;
|
|
16
|
+
users do not run npm. A missing Node binary produces an extraction failure.
|
|
17
|
+
`Nous.fetch(..., extractor: Nous::Extractor::Default.new)` and `--readability`
|
|
18
|
+
select the Ruby-only backup explicitly. Retained `ExtractHtml` requires Node.
|
|
19
|
+
|
|
20
|
+
## Rebuilding the bundled adapter
|
|
21
|
+
|
|
22
|
+
Maintainers use the pinned dependencies in `package-lock.json`. From the repository
|
|
23
|
+
root, provision dependencies explicitly and rebuild:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm ci --ignore-scripts
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Commit the source, package manifest/lockfile, generated bundle and license notices
|
|
31
|
+
together. `javascript/build.mjs` regenerates the bundle and gathers dependency
|
|
32
|
+
license texts. Runtime extraction and verification never install dependencies.
|
|
33
|
+
Run the [Defuddle recipe](../verification/defuddle.md) after rebuilding.
|
|
34
|
+
|
|
35
|
+
The bridge uses a non-executing DOM parser, disables async extractors and rejects
|
|
36
|
+
fetch calls. It is not a browser renderer. The adapter uses a 15-second process
|
|
37
|
+
deadline and bounded output, but does not enforce a process memory limit. Callers
|
|
38
|
+
with stronger isolation requirements still own external process supervision.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Verification setup
|
|
2
|
+
|
|
3
|
+
Work from the Nous repository root. Use the Ruby declared in `.ruby-version`
|
|
4
|
+
(currently 4.0.1), the Bundler version in `Gemfile.lock` (currently 4.0.6), and
|
|
5
|
+
that lockfile's gems. With rbenv, `rbenv exec ruby -v` must identify the intended
|
|
6
|
+
Ruby; shell/system Ruby 2.6 is unsuitable. Put rbenv shims on PATH or prefix the
|
|
7
|
+
commands in the verification guide with `rbenv exec`.
|
|
8
|
+
|
|
9
|
+
Run `BUNDLE_FROZEN=true bundle check`. If dependencies are absent, run
|
|
10
|
+
`BUNDLE_FROZEN=true bundle install` once, then repeat the check. Installation may
|
|
11
|
+
need network and permission to write the gem installation. If Ruby/Bundler is
|
|
12
|
+
missing, provision those declared versions using your runtime manager first.
|
|
13
|
+
Do not update or regenerate the lockfile to make verification work. Frozen mode
|
|
14
|
+
also prevents Bundler from opportunistically rewriting platform metadata.
|
|
15
|
+
|
|
16
|
+
The recipes use RSpec, WebMock, Node.js 22+ and the bundled Defuddle adapter.
|
|
17
|
+
Follow [Defuddle setup and readiness](defuddle.md) before running them. No server,
|
|
18
|
+
browser, remote account or model is needed. HTTP in the failure recipe is a
|
|
19
|
+
controlled response fixture. WebMock blocks Ruby HTTP; the Node adapter disables
|
|
20
|
+
Defuddle async fetching and rejects fetch calls.
|
|
21
|
+
No credentials or retained private content belong in evidence.
|