outcrawl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ # Build and cache artefacts for this package.
2
+ #
3
+ # This file exists for a second, non-obvious reason: hatchling FORCE-INCLUDES
4
+ # the VCS exclusion file it used into the sdist, so that a rebuild from an
5
+ # unpacked sdist applies the same exclusions. Without a `.gitignore` here it
6
+ # walked up and shipped the MONOREPO's root one — `node_modules/`, the 126 GB
7
+ # Chromium checkout path, our credential rules — at the root of a distribution
8
+ # a customer unpacks. `exclude` in pyproject.toml cannot remove it, because
9
+ # `force_include` runs after exclusions by design. A local file is the fix.
10
+ dist/
11
+ __pycache__/
12
+ *.py[cod]
13
+ .pytest_cache/
14
+ *.egg-info/
15
+ .venv/
16
+
17
+ # `uv run` writes this. Deliberately NOT tracked: this is a LIBRARY, so the
18
+ # resolution a customer gets is decided by their own resolver against
19
+ # `dependencies`, and a committed lock would pin only OUR dev environment while
20
+ # generating churn on every `uv run`. An application in this repo would commit
21
+ # one; a library published to PyPI should not.
22
+ uv.lock
outcrawl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aeonmind LLC
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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: outcrawl
3
+ Version: 0.1.0
4
+ Summary: The Python client for Outcrawl: scrape, crawl, search and run agent browser tasks on one credit balance.
5
+ Project-URL: Homepage, https://outcrawl.ai
6
+ Project-URL: Repository, https://github.com/aeonmindai/outcrawl-browser
7
+ Project-URL: Issues, https://github.com/aeonmindai/outcrawl-browser/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,browser-automation,crawler,outcrawl,scraping,stealth,web-search
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx<1,>=0.27
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # outcrawl
32
+
33
+ The Python client for [Outcrawl](https://api.outcrawl.ai): scrape, crawl, search and run agent browser
34
+ tasks, on one credit balance. Pure Python — one `py3-none-any` wheel for every operating system,
35
+ and no compiler on any of them.
36
+
37
+ ```sh
38
+ pip install outcrawl
39
+ ```
40
+
41
+ Requires Python 3.10 or newer. One runtime dependency, [`httpx`](https://www.python-httpx.org/),
42
+ which is pure Python, as is everything it pulls.
43
+
44
+ ## Usage
45
+
46
+ ```python
47
+ import asyncio
48
+ from outcrawl import Outcrawl
49
+
50
+ async def main() -> None:
51
+ async with Outcrawl() as oc: # reads OUTCRAWL_API_KEY
52
+ doc = await oc.scrape("https://example.com", formats=["markdown"])
53
+ print(doc.markdown) # or doc["markdown"] — it is the wire object
54
+ print(doc.usage) # every call returns what it cost
55
+
56
+ job = oc.agent(
57
+ task="Report the title of the top story on Hacker News.",
58
+ caps={"budget": "2.00"}, # the one ceiling a submit must carry
59
+ )
60
+ async for record in job: # the event cursor, while it runs
61
+ print(record["kind"], record.get("text"))
62
+ run = await job # and the settled run
63
+ print(run["status"], run["data"])
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ `await` and `async for` on **one** job is one run and not two: they are separate requests against
69
+ one durable row, so watching a run and having its answer is not a choice.
70
+
71
+ ## Configuration
72
+
73
+ | Variable | Meaning | Default |
74
+ | --- | --- | --- |
75
+ | `OUTCRAWL_API_KEY` | your key | required |
76
+ | `OUTCRAWL_API_URL` | base url | `https://api.outcrawl.ai` |
77
+
78
+ The same two names `@outcrawl/sdk`, the `outcrawl` CLI and the Outcrawl MCP server read. Pass
79
+ `Outcrawl(api_key=..., base_url=...)` to override them per client.
80
+
81
+ ## Surface
82
+
83
+ Every one of the 34 capabilities in the registry is reachable, and `tests/test_registry.py` fails
84
+ when one is not.
85
+
86
+ - `oc.scrape(url, **options)` / `oc.crawl(url, **options)` / `oc.search(query, **options)` —
87
+ `crawl` is async-iterable and single-pass.
88
+ - `oc.crawl.job(id)` — the same crawl by id, with `status()`, `results()`, `cancel()`, and
89
+ `oc.crawl.get(id)` / `.results(id)` / `.cancel(id)` for an id on its own. A streaming crawl
90
+ belongs to its connection, so hanging up cancels it: the row settles `cancelled` with every
91
+ page it had already delivered, and these routes are how you read the pages you paid for. The
92
+ id is on `handle.job["id"]` from the first progress frame — store it before you need it.
93
+ - `oc.agent(task=..., caps=...)` — `caps` is required at runtime and `caps.budget` is required
94
+ inside it; `caps.steps` and `caps.duration` are optional and unset unless you name them, and
95
+ all three are hard stops when present. The job is awaitable, async-iterable, and carries
96
+ everything a handle does: `status()`, `results()`, `events()`, `cancel()`, `control()`,
97
+ `answer()`, `add_file()`. `oc.agent.get(id)` and friends reach a run by id — an id from a
98
+ webhook needs no submit. `schema` accepts a JSON Schema object and constrains `run["data"]` to
99
+ it, the same typed submit `@outcrawl/sdk` documents.
100
+ - `oc.profiles` / `oc.secrets` / `oc.rules` / `oc.integrations` / `oc.sessions` / `oc.monitors`.
101
+ `oc.secrets` has deliberately **no `get`**: there is no route to widen. A run reaches a value
102
+ only by naming its HANDLE in the submit, and the substitution happens below the model.
103
+ - `oc.usage(**query)` and `oc.credits()` — every credit figure is a decimal **string**; use
104
+ `decimal.Decimal`, never `float()`.
105
+ - `CAPABILITY_AVAILABILITY` — whether anything is SERVED behind a declared route. A capability
106
+ nothing serves is refused in your own process, with what is missing, rather than as a 503.
107
+
108
+ ## Errors
109
+
110
+ Identical to the TypeScript SDK's: same classes, same fields, same sentences. Branch on
111
+ `error.code`, never on `str(error)`.
112
+
113
+ ```python
114
+ from outcrawl import ProfileInUseError, QuotaExceededError
115
+
116
+ try:
117
+ ...
118
+ except ProfileInUseError as leased:
119
+ retry_at = leased.held_until # a retry that knows when to retry
120
+ except QuotaExceededError as over:
121
+ await asyncio.sleep(over.retry_after_seconds)
122
+ ```
123
+
124
+ `tests/error_parity.json` is emitted from the TypeScript SDK's own error reconstruction and read by
125
+ both test suites, so neither surface can drift from the other without turning its own suite red.
126
+
127
+ ## What is not here
128
+
129
+ `oc.browser()`. The live browser needs a CDP connection driven in the caller's own process, which
130
+ is why it is deliberately not a registry capability and why the TypeScript SDK is the one place it
131
+ exists. The same page capabilities are reachable through `agent` and `scrape`, which run the loop
132
+ on our side. That is a difference in kind, not an omission.
133
+
134
+ The same reason takes `Session.take_control` with it: the TypeScript SDK's `takeControl()` opens a
135
+ CDP connection to a running session's own page so a person can drive it directly, and that
136
+ connection has to live in the caller's process for the same reason `oc.browser()` does. Everything
137
+ else on a session — `get`, `list`, `export`, `live()` — is a row read or a held stream and is here.
138
+
139
+ ## Development
140
+
141
+ ```sh
142
+ pip install -e '.[dev]' # pytest + pytest-asyncio, never runtime dependencies
143
+ python3 scripts/gen_registry.py # regenerate registry.py from packages/core/src/registry.ts
144
+ python3 scripts/gen_registry.py --check # or just fail if it is stale
145
+ bun ../sdk-python/scripts/gen_error_parity.ts # regenerate the shared error contract
146
+ python3 -m pytest tests -q # the drift, parity and ergonomics tests
147
+ ```
148
+
149
+ From the repo root, `npm run test:python` runs the staleness check and the suite together on
150
+ the SUPPORTED FLOOR — `uv run --extra dev --python 3.10 pytest` — so the gate exercises 3.10
151
+ rather than whatever `python3` happens to be. Without `uv`: `pip install -e '.[dev]'` and
152
+ `python3 -m pytest tests -q`.
153
+
154
+ The two registry-versus-TypeScript tests skip when run from an unpacked sdist, which has no
155
+ TypeScript to compare against, and run whenever the monorepo is present. The reachability test —
156
+ the one that fails when a registry row has no method — has no such dependency and always runs.
@@ -0,0 +1,126 @@
1
+ # outcrawl
2
+
3
+ The Python client for [Outcrawl](https://api.outcrawl.ai): scrape, crawl, search and run agent browser
4
+ tasks, on one credit balance. Pure Python — one `py3-none-any` wheel for every operating system,
5
+ and no compiler on any of them.
6
+
7
+ ```sh
8
+ pip install outcrawl
9
+ ```
10
+
11
+ Requires Python 3.10 or newer. One runtime dependency, [`httpx`](https://www.python-httpx.org/),
12
+ which is pure Python, as is everything it pulls.
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ import asyncio
18
+ from outcrawl import Outcrawl
19
+
20
+ async def main() -> None:
21
+ async with Outcrawl() as oc: # reads OUTCRAWL_API_KEY
22
+ doc = await oc.scrape("https://example.com", formats=["markdown"])
23
+ print(doc.markdown) # or doc["markdown"] — it is the wire object
24
+ print(doc.usage) # every call returns what it cost
25
+
26
+ job = oc.agent(
27
+ task="Report the title of the top story on Hacker News.",
28
+ caps={"budget": "2.00"}, # the one ceiling a submit must carry
29
+ )
30
+ async for record in job: # the event cursor, while it runs
31
+ print(record["kind"], record.get("text"))
32
+ run = await job # and the settled run
33
+ print(run["status"], run["data"])
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ `await` and `async for` on **one** job is one run and not two: they are separate requests against
39
+ one durable row, so watching a run and having its answer is not a choice.
40
+
41
+ ## Configuration
42
+
43
+ | Variable | Meaning | Default |
44
+ | --- | --- | --- |
45
+ | `OUTCRAWL_API_KEY` | your key | required |
46
+ | `OUTCRAWL_API_URL` | base url | `https://api.outcrawl.ai` |
47
+
48
+ The same two names `@outcrawl/sdk`, the `outcrawl` CLI and the Outcrawl MCP server read. Pass
49
+ `Outcrawl(api_key=..., base_url=...)` to override them per client.
50
+
51
+ ## Surface
52
+
53
+ Every one of the 34 capabilities in the registry is reachable, and `tests/test_registry.py` fails
54
+ when one is not.
55
+
56
+ - `oc.scrape(url, **options)` / `oc.crawl(url, **options)` / `oc.search(query, **options)` —
57
+ `crawl` is async-iterable and single-pass.
58
+ - `oc.crawl.job(id)` — the same crawl by id, with `status()`, `results()`, `cancel()`, and
59
+ `oc.crawl.get(id)` / `.results(id)` / `.cancel(id)` for an id on its own. A streaming crawl
60
+ belongs to its connection, so hanging up cancels it: the row settles `cancelled` with every
61
+ page it had already delivered, and these routes are how you read the pages you paid for. The
62
+ id is on `handle.job["id"]` from the first progress frame — store it before you need it.
63
+ - `oc.agent(task=..., caps=...)` — `caps` is required at runtime and `caps.budget` is required
64
+ inside it; `caps.steps` and `caps.duration` are optional and unset unless you name them, and
65
+ all three are hard stops when present. The job is awaitable, async-iterable, and carries
66
+ everything a handle does: `status()`, `results()`, `events()`, `cancel()`, `control()`,
67
+ `answer()`, `add_file()`. `oc.agent.get(id)` and friends reach a run by id — an id from a
68
+ webhook needs no submit. `schema` accepts a JSON Schema object and constrains `run["data"]` to
69
+ it, the same typed submit `@outcrawl/sdk` documents.
70
+ - `oc.profiles` / `oc.secrets` / `oc.rules` / `oc.integrations` / `oc.sessions` / `oc.monitors`.
71
+ `oc.secrets` has deliberately **no `get`**: there is no route to widen. A run reaches a value
72
+ only by naming its HANDLE in the submit, and the substitution happens below the model.
73
+ - `oc.usage(**query)` and `oc.credits()` — every credit figure is a decimal **string**; use
74
+ `decimal.Decimal`, never `float()`.
75
+ - `CAPABILITY_AVAILABILITY` — whether anything is SERVED behind a declared route. A capability
76
+ nothing serves is refused in your own process, with what is missing, rather than as a 503.
77
+
78
+ ## Errors
79
+
80
+ Identical to the TypeScript SDK's: same classes, same fields, same sentences. Branch on
81
+ `error.code`, never on `str(error)`.
82
+
83
+ ```python
84
+ from outcrawl import ProfileInUseError, QuotaExceededError
85
+
86
+ try:
87
+ ...
88
+ except ProfileInUseError as leased:
89
+ retry_at = leased.held_until # a retry that knows when to retry
90
+ except QuotaExceededError as over:
91
+ await asyncio.sleep(over.retry_after_seconds)
92
+ ```
93
+
94
+ `tests/error_parity.json` is emitted from the TypeScript SDK's own error reconstruction and read by
95
+ both test suites, so neither surface can drift from the other without turning its own suite red.
96
+
97
+ ## What is not here
98
+
99
+ `oc.browser()`. The live browser needs a CDP connection driven in the caller's own process, which
100
+ is why it is deliberately not a registry capability and why the TypeScript SDK is the one place it
101
+ exists. The same page capabilities are reachable through `agent` and `scrape`, which run the loop
102
+ on our side. That is a difference in kind, not an omission.
103
+
104
+ The same reason takes `Session.take_control` with it: the TypeScript SDK's `takeControl()` opens a
105
+ CDP connection to a running session's own page so a person can drive it directly, and that
106
+ connection has to live in the caller's process for the same reason `oc.browser()` does. Everything
107
+ else on a session — `get`, `list`, `export`, `live()` — is a row read or a held stream and is here.
108
+
109
+ ## Development
110
+
111
+ ```sh
112
+ pip install -e '.[dev]' # pytest + pytest-asyncio, never runtime dependencies
113
+ python3 scripts/gen_registry.py # regenerate registry.py from packages/core/src/registry.ts
114
+ python3 scripts/gen_registry.py --check # or just fail if it is stale
115
+ bun ../sdk-python/scripts/gen_error_parity.ts # regenerate the shared error contract
116
+ python3 -m pytest tests -q # the drift, parity and ergonomics tests
117
+ ```
118
+
119
+ From the repo root, `npm run test:python` runs the staleness check and the suite together on
120
+ the SUPPORTED FLOOR — `uv run --extra dev --python 3.10 pytest` — so the gate exercises 3.10
121
+ rather than whatever `python3` happens to be. Without `uv`: `pip install -e '.[dev]'` and
122
+ `python3 -m pytest tests -q`.
123
+
124
+ The two registry-versus-TypeScript tests skip when run from an unpacked sdist, which has no
125
+ TypeScript to compare against, and run whenever the monorepo is present. The reachability test —
126
+ the one that fails when a registry row has no method — has no such dependency and always runs.
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "outcrawl"
7
+ version = "0.1.0"
8
+ description = "The Python client for Outcrawl: scrape, crawl, search and run agent browser tasks on one credit balance."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ keywords = [
14
+ "outcrawl",
15
+ "scraping",
16
+ "crawler",
17
+ "web-search",
18
+ "browser-automation",
19
+ "agent",
20
+ "stealth",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Programming Language :: Python :: Implementation :: CPython",
34
+ "Topic :: Internet :: WWW/HTTP",
35
+ "Typing :: Typed",
36
+ ]
37
+
38
+ # The one runtime dependency, and it is pure Python — as is everything it pulls
39
+ # (httpcore, h11, certifi, idna, anyio, sniffio). That is what keeps this wheel
40
+ # `py3-none-any` and makes "supports every OS" a fact about the artefact rather
41
+ # than a hope about the user's compiler.
42
+ dependencies = ["httpx>=0.27,<1"]
43
+
44
+ [project.urls]
45
+ Homepage = "https://outcrawl.ai"
46
+ Repository = "https://github.com/aeonmindai/outcrawl-browser"
47
+ Issues = "https://github.com/aeonmindai/outcrawl-browser/issues"
48
+
49
+ [project.optional-dependencies]
50
+ # Test-only. Never a runtime dependency: a customer installing this package must
51
+ # not be handed a test runner.
52
+ dev = ["pytest>=8", "pytest-asyncio>=0.24"]
53
+
54
+ [tool.hatch.build.targets.wheel]
55
+ packages = ["src/outcrawl"]
56
+
57
+ [tool.hatch.build.targets.sdist]
58
+ # The sdist carries the tests and the generator, so `pytest` works from an
59
+ # unpacked source distribution — including the registry drift test, which needs
60
+ # the TypeScript registry and therefore reports honestly that it cannot run
61
+ # rather than passing silently.
62
+ include = ["src", "tests", "scripts", "README.md", "pyproject.toml", "LICENSE"]
63
+ # Hatchling FORCE-INCLUDES the VCS exclusion file it used, so a rebuild from an
64
+ # unpacked sdist applies the same exclusions — and `exclude` cannot remove a
65
+ # force-included path. With no `.gitignore` in this directory it walked up and
66
+ # shipped the MONOREPO's root one, whose contents are `node_modules/`, the
67
+ # Chromium checkout path and our credential rules. `packages/sdk-python/.gitignore`
68
+ # is what makes the shipped one this package's own; see the note in it.
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+ asyncio_mode = "auto"
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Emit the cross-language error contract both SDKs are held to.
3
+ *
4
+ * THE PROBLEM. "Auth, base URL and error shapes identical across both" is a
5
+ * claim about two codebases in two languages, and the only way it stays true is
6
+ * if one artefact pins it and BOTH suites read that artefact. A Python mirror
7
+ * checked only by Python tests is a mirror that drifts the first time a
8
+ * TypeScript message is reworded, and the customer who notices is the one
9
+ * quoting one sentence to support while their logs contain the other.
10
+ *
11
+ * So this writes `tests/error_parity.json`: for every code in
12
+ * `OUTCRAWL_ERROR_CODES`, the wire body the platform sends and exactly what the
13
+ * TypeScript SDK's `errorFromWire` reconstructs from it — class, code, status,
14
+ * message, details.
15
+ *
16
+ * - `packages/sdk-python/tests/test_errors.py` asserts Python rebuilds the
17
+ * same thing from the same body.
18
+ * - `packages/sdk/test/error-parity.test.ts` asserts TypeScript still does,
19
+ * so a reworded message turns the TS suite red rather than silently
20
+ * invalidating the fixture the Python suite trusts.
21
+ *
22
+ * The fixture lives in the Python package because it must ship inside the
23
+ * sdist: `pytest` from an unpacked source distribution has no TypeScript to
24
+ * read, and a parity test that skips when it cannot find the other language is
25
+ * a parity test that passes while the two disagree.
26
+ *
27
+ * bun packages/sdk-python/scripts/gen_error_parity.ts
28
+ */
29
+
30
+ import { writeFileSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+
33
+ import { OUTCRAWL_ERROR_CODES, isOutcrawlError } from '../../core/src/errors';
34
+ import { ERROR_SAMPLES } from '../../api/src/errors';
35
+ import { errorFromWire } from '../../sdk/src/transport';
36
+
37
+ interface Rebuilt {
38
+ readonly class: string;
39
+ readonly code: string | null;
40
+ readonly status: number | null;
41
+ readonly message: string;
42
+ readonly details: Record<string, unknown> | null;
43
+ }
44
+
45
+ interface ParityRow {
46
+ readonly capabilityCode: string;
47
+ readonly status: number;
48
+ readonly wire: { code: string; message: string; details: Record<string, unknown> };
49
+ readonly rebuilt: Rebuilt;
50
+ }
51
+
52
+ function describe(error: Error): Rebuilt {
53
+ if (!isOutcrawlError(error)) {
54
+ // Every declared code must reconstruct into a typed error; an
55
+ // `OutcrawlApiError` here is the drop this file exists to catch, and it is
56
+ // recorded rather than thrown on so the fixture shows the truth.
57
+ return {
58
+ class: error.constructor.name,
59
+ code: null,
60
+ status: null,
61
+ message: error.message,
62
+ details: null,
63
+ };
64
+ }
65
+ return {
66
+ class: error.constructor.name,
67
+ code: error.code,
68
+ status: error.status,
69
+ message: error.message,
70
+ details: { ...error.details },
71
+ };
72
+ }
73
+
74
+ const rows: ParityRow[] = OUTCRAWL_ERROR_CODES.map((code) => {
75
+ const sample = ERROR_SAMPLES[code];
76
+ const wire = sample.toJSON();
77
+ return {
78
+ capabilityCode: code,
79
+ status: sample.status,
80
+ wire,
81
+ rebuilt: describe(errorFromWire(sample.status, wire)),
82
+ };
83
+ });
84
+
85
+ const dropped = rows.filter((row) => row.rebuilt.code === null);
86
+ if (dropped.length > 0) {
87
+ throw new Error(
88
+ `errorFromWire drops ${dropped.length} declared code(s) to an untyped error: ` +
89
+ `${dropped.map((row) => row.capabilityCode).join(', ')}. Map them before regenerating.`,
90
+ );
91
+ }
92
+
93
+ const target = join(import.meta.dir, '..', 'tests', 'error_parity.json');
94
+ writeFileSync(target, `${JSON.stringify({ rows }, null, 2)}\n`, 'utf8');
95
+ console.log(`wrote ${target} (${rows.length} codes)`);