nous 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c73c21d427c9bb99cc148e089ed5899e7aa9e3ca86a4825540380d41771354d2
4
- data.tar.gz: 4b361a7aed3c0dfb28a6a650b0813d371622c82b910b8880502281047152a739
3
+ metadata.gz: e4371689843d5f5275e56afaaab2630ba2a703e57e29cfb79d6d47d19040d934
4
+ data.tar.gz: 1402e3ab54931a99bbad484002345514142a9ade10453938c8f2feae77ef7742
5
5
  SHA512:
6
- metadata.gz: aacbc4777dc1e5bd66513ddc3bc5a1f667276ac2e89ff781f7b43762133ae640d68bc1191c11b61ee00ac7911a128fcf6ad80653f86d369d284223e830f09120
7
- data.tar.gz: 90fc8f0cf3c30c6e06bf6aeebd2790539ff80d0d63469a367c4a09b008c9ceed3dedc0d18528e5f6fab3c4a02103b4d2c7c9b3788b1708c6a1a46790d9f5cbab
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,26 @@
1
- ## [Unreleased]
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.
2
24
 
3
25
  ## [0.4.0] - 2026-04-11
4
26
 
@@ -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 two extraction backends: a local parser (ruby-readability) and the Jina Reader API for JS-rendered sites.
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 Jina Reader API for JS-rendered sites (Next.js, SPAs)
43
- nous https://example.com --jina
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,7 +61,7 @@ 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
- | `--jina` | Use Jina Reader API for extraction | off |
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 |
@@ -84,9 +88,9 @@ text = Nous.serialize(pages, format: :text)
84
88
  # Serialize to JSON
85
89
  json = Nous.serialize(pages, format: :json)
86
90
 
87
- # Use the Jina extractor for JS-heavy sites
88
- pages = Nous.fetch("https://spa-site.com",
89
- extractor: Nous::Extractor::Jina.new,
91
+ # Select the Readability backup explicitly
92
+ pages = Nous.fetch("https://example.com",
93
+ extractor: Nous::Extractor::Default.new,
90
94
  limit: 5
91
95
  )
92
96
  ```
@@ -99,12 +103,20 @@ Use the `details: true` option to receive full fetch results including failures:
99
103
  result = Nous.fetch("https://example.com", details: true)
100
104
 
101
105
  result.pages # Array<Nous::Page> - successfully extracted pages
102
- result.failures # Array<{requested_url:, error:}> - failed fetches
106
+ result.failures # Array<{requested_url:, error:}> - failed acquisition or extraction
103
107
  result.total_requested # Integer - total URLs attempted
104
108
  result.all_succeeded? # Boolean - true if no failures
105
109
  result.any_succeeded? # Boolean - true if at least one page extracted
106
110
  ```
107
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
+
108
120
  This is useful when you need to handle failures explicitly:
109
121
 
110
122
  ```ruby
@@ -122,13 +134,69 @@ result.pages.each do |page|
122
134
  end
123
135
  ```
124
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
+
125
193
  ### Page Structure
126
194
 
127
195
  Each extracted page contains:
128
196
 
129
197
  | Field | Type | Description |
130
198
  |-------|------|-------------|
131
- | `title` | String | Page title (fallback chain: readability → `<title>` tag → `<h1>`) |
199
+ | `title` | String | Page title selected by the extraction backend |
132
200
  | `url` | String | Final URL after redirects |
133
201
  | `pathname` | String | URL path component |
134
202
  | `content` | String | Extracted content as Markdown |
@@ -138,7 +206,7 @@ Each extracted page contains:
138
206
 
139
207
  ```ruby
140
208
  page.metadata # => {
141
- # extractor: "Nous::Extractor::Default", # Which extractor was used
209
+ # extractor: "Nous::Extractor::Defuddle", # Which extractor was used
142
210
  # requested_url: "https://example.com/blog", # Original URL before redirects
143
211
  # content_type: "text/html; charset=utf-8", # HTTP Content-Type header
144
212
  # redirected: true # Whether redirects occurred
@@ -147,7 +215,19 @@ page.metadata # => {
147
215
 
148
216
  ## Extraction Backends
149
217
 
150
- ### Default (ruby-readability)
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.
229
+
230
+ ### Readability (explicit backup)
151
231
 
152
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.
153
233
 
@@ -156,9 +236,13 @@ Title extraction uses a fallback chain:
156
236
  2. Original `<title>` tag from HTML
157
237
  3. First `<h1>` from extracted content
158
238
 
159
- ### Jina Reader API
239
+ ### Migrating from Jina
160
240
 
161
- Uses the [Jina Reader API](https://jina.ai/reader/) which renders pages with headless Chrome. Handles Next.js App Router, React Server Components, SPAs, and other JS-heavy sites. Free tier allows 20 requests/minute without a key, or 500 RPM with a `JINA_API_KEY` environment variable.
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.
162
246
 
163
247
  ## Output Formats
164
248
 
@@ -171,7 +255,7 @@ XML-tagged output designed for LLM context windows:
171
255
  <title>Page Title</title>
172
256
  <url>https://example.com/page</url>
173
257
  <pathname>/page</pathname>
174
- <extractor>Nous::Extractor::Default</extractor>
258
+ <extractor>Nous::Extractor::Defuddle</extractor>
175
259
  <content>
176
260
  # Heading
177
261
 
@@ -190,7 +274,7 @@ Extracted markdown content...
190
274
  "pathname": "/page",
191
275
  "content": "# Heading\n\nExtracted markdown content...",
192
276
  "metadata": {
193
- "extractor": "Nous::Extractor::Default",
277
+ "extractor": "Nous::Extractor::Defuddle",
194
278
  "requested_url": "https://example.com/page",
195
279
  "content_type": "text/html; charset=utf-8",
196
280
  "redirected": false
@@ -201,6 +285,11 @@ Extracted markdown content...
201
285
 
202
286
  ## Development
203
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
+
204
293
  ```bash
205
294
  bin/setup # Install dependencies
206
295
  bundle exec rspec # Run tests
data/Rakefile CHANGED
@@ -8,3 +8,5 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  require "standard/rake"
9
9
 
10
10
  task default: %i[spec standard]
11
+
12
+ Dir.glob("tasks/*.rake").sort.each { |path| load path }
@@ -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.
@@ -0,0 +1,77 @@
1
+ # Nous verification
2
+
3
+ The project-local [verify-nous skill](../../.agents/skills/verify-nous/SKILL.md)
4
+ uses this guide. Initial scope is the two selected architecture candidates:
5
+ retained-HTML extraction and visible extraction failures. This map does not
6
+ certify all crawling, rendering, serialization or consumer behavior.
7
+
8
+ | Recipe | State | Coverage |
9
+ | --- | --- | --- |
10
+ | [Defuddle](defuddle.md) | Implemented; run required | Bundled parser, selectors, process failures, concurrency and packaging |
11
+ | [Retained HTML](retained-html.md) | Implemented; run required | Offline content/provenance, formats, decoding, byte budgets, errors and legacy compatibility |
12
+ | [Failure propagation](failure-propagation.md) | Implemented; run required | Public detailed result must retain extraction failure |
13
+
14
+ ## Launch
15
+
16
+ Follow [setup](../setup/verification.md). These are one-shot Ruby commands; no
17
+ persistent process is launched. Start an evidence directory for this run:
18
+
19
+ ```bash
20
+ mkdir -p tmp/verification
21
+ receipt_dir="$(mktemp -d "$PWD/tmp/verification/run-XXXXXX")"
22
+ export receipt_dir
23
+ ```
24
+
25
+ Keep the same shell variable for the commands below. Do not pipe through `tee`
26
+ without preserving the original exit status.
27
+
28
+ ## Doctor
29
+
30
+ ```bash
31
+ ruby -v > "$receipt_dir/ruby.log" 2>&1
32
+ printf '%s\n' "$?" > "$receipt_dir/ruby.exit"
33
+ BUNDLE_FROZEN=true bundle check > "$receipt_dir/bundle.log" 2>&1
34
+ printf '%s\n' "$?" > "$receipt_dir/bundle.exit"
35
+ git rev-parse HEAD > "$receipt_dir/head.txt"
36
+ git status --short > "$receipt_dir/status.txt"
37
+ git diff --binary HEAD > "$receipt_dir/working-tree.patch"
38
+ BUNDLE_FROZEN=true bundle exec ruby -Ilib -r nous -e 'puts Nous::VERSION; puts $LOADED_FEATURES.grep(%r{/lib/nous.rb$})' > "$receipt_dir/target.log" 2>&1
39
+ printf '%s\n' "$?" > "$receipt_dir/target.exit"
40
+ git ls-files --others --exclude-standard -z | BUNDLE_FROZEN=true bundle exec ruby -rdigest -e 'STDIN.read.split("\0").each { |path| puts "#{Digest::SHA256.file(path).hexdigest} #{path}" if File.file?(path) }' > "$receipt_dir/untracked.sha256"
41
+ ```
42
+
43
+ Inspect each exit file and the target path before driving: Ruby, bundle and target
44
+ must exit zero and target must resolve this checkout. Untracked hashes identify
45
+ new recipes/harnesses; retain their contents with the change. For committed work,
46
+ HEAD identifies the source; for dirty work retain the patch and hashes too.
47
+
48
+ ## Drive and capture
49
+
50
+ Discover opt-in checks with `bundle exec rake -T`.
51
+ Run each selected recipe's exact command, redirect its output to the named log,
52
+ and immediately record `$?` in its exit file. A nonzero product assertion stays
53
+ failed; do not weaken it to current behavior. For a prerequisite failure, attempt
54
+ one documented setup recovery, retry once, then mark blocked if unavailable.
55
+
56
+ Record a receipt table with criterion, repository/revision, command, expected,
57
+ observed, evidence path and pass/fail/blocked/not-run status. Keep failed logs.
58
+ Do not treat successful neighbors as readiness for a failed or unimplemented goal.
59
+
60
+ For Ruby or harness changes also run `BUNDLE_FROZEN=true bundle exec rspec` and
61
+ `BUNDLE_FROZEN=true bundle exec standardrb`, capturing output and exit status.
62
+ The draft readiness harness is opt-in and is not part of ordinary specs.
63
+
64
+ ## Cleanup
65
+
66
+ These checks leave no server, child process, storage record or fixture changes.
67
+ RSpec may update the ignored `.rspec_status`; it is not evidence. Keep
68
+ `$receipt_dir` and all logs after cleanup; do not delete the evidence directory.
69
+ Never use private captures for the synthetic fixture checks.
70
+
71
+ ## Coverage limits
72
+
73
+ Real-world readability quality, process CPU/memory/wall-time supervision,
74
+ Substrate integration, locked Autodidact compatibility and hosted acceptance remain
75
+ outside this map. Neither local backend renders JavaScript.
76
+ The caller owns immutable storage and any application lifecycle. Passing library
77
+ recipes alone does not certify downstream adoption.
@@ -0,0 +1,45 @@
1
+ # Defuddle extraction
2
+
3
+ Status: implemented; execute on the source under review.
4
+
5
+ Outcomes: Defuddle is the default for `Nous.fetch`, `Fetcher` and `ExtractHtml`.
6
+ The CLI selects Defuddle unless `--readability` is supplied. Jina is removed.
7
+ The extractor consumes already acquired HTML and the final URL, returns title
8
+ and Markdown through the existing result contract, honors selectors, resolves
9
+ relative links, and keeps concurrent calls isolated. Empty content and runtime
10
+ failures remain failures; no automatic Readability fallback occurs.
11
+
12
+ Follow [launch and doctor](README.md) and [Node readiness](../setup/defuddle.md).
13
+ Then run:
14
+
15
+ ```bash
16
+ BUNDLE_FROZEN=true bundle exec rake verify:defuddle > "$receipt_dir/defuddle.log" 2>&1
17
+ printf '%s\n' "$?" > "$receipt_dir/defuddle.exit"
18
+ ```
19
+
20
+ Require exit zero. The extractor specs exercise the actual bundled parser with
21
+ synthetic HTML. Client specs cover JSON/field validation, empty content and propagation of a
22
+ failed process result. `RunProcess` specs independently exercise raw process
23
+ output, nonzero exit, output limits, reaping and timeout cleanup with controlled
24
+ Ruby subprocesses. Missing-runtime and safe-diagnostic errors are covered through `RunProcess`. CLI specs check backend selection and observable selector behavior. Acquired-byte
25
+ specs cover BOM/meta decoding and meaningful invalid-byte/charset failures.
26
+ Run the retained-HTML and failure-propagation recipes, full suite and Standard too.
27
+
28
+ For installed-runtime and packaging compatibility, run the separate opt-in check:
29
+
30
+ ```bash
31
+ BUNDLE_FROZEN=true bundle exec rake verify:defuddle_package > "$receipt_dir/defuddle-package.log" 2>&1
32
+ printf '%s\n' "$?" > "$receipt_dir/defuddle-package.exit"
33
+ ```
34
+
35
+ Require exit zero. The harness in `spec/verification/defuddle_package.rb` builds
36
+ and unpacks the gem in an isolated temporary directory, verifies the loaded Nous
37
+ path, checks Jina is absent, and asserts title/content through `ExtractHtml`.
38
+ It owns and cleans up the temporary package on success and failure. This verifies
39
+ that extraction does not resolve npm dependencies from the checkout.
40
+
41
+ Record source identity, commands, exit statuses and logs using the verification
42
+ guide. Runtime checks establish compatibility only with the Node/platform tested;
43
+ controlled subprocesses do not establish Node compatibility. Synthetic fixtures
44
+ do not establish extraction quality across live sites, JavaScript completeness,
45
+ authentication detection or downstream Substrate acceptance.