@lariocpt/crust-mongo 0.3.5 → 0.3.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lariocpt/crust-mongo",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "crust with the MongoDB driver bundled — the mongo variant of the crust binary launcher",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,11 +1,23 @@
1
1
  ---
2
2
  name: crust-api-testing
3
- description: Test HTTP APIs with crust — .pipes suites (one fixture pipeline per line, request chaining via capture, inline SQL verification), .crust.ts fixtures with setup/matchers, and gen-fixtures which derives negative-case matrices and CRUD flow suites from an OpenAPI spec. Use for API integration tests, contract tests from a spec, auth/authz negative cases, or DB-verified endpoint tests.
3
+ description: Test HTTP APIs with crust — .pipes suites (one fixture pipeline per line, request chaining via capture, inline SQL verification), .crust.ts fixtures with setup/matchers, gen-fixtures which derives negative-case matrices and CRUD flow suites from an OpenAPI spec, and the evidence-tier taxonomy (snapshot vs DB-verified vs demo). Use for API integration tests, contract tests from a spec, auth/authz negative cases, or DB-verified endpoint tests.
4
4
  ---
5
5
 
6
6
  # crust API testing
7
7
 
8
- Three layers, one binary. Pick the lightest that fits:
8
+ ## The shared principle: the lightest CORRECT layer
9
+
10
+ Choose the lightest native layer that preserves correctness, observability,
11
+ and safety — not merely the shortest syntax.
12
+
13
+ - Use `.pipes` for straightforward requests, assertions, and supported request chaining.
14
+ - Use `.crust.ts` when setup, custom matchers, or required safety controls cannot be expressed in pipes.
15
+ - Use `gen-fixtures` for supported contract-derived cases, followed by review.
16
+ - Use native load pipelines for paced traffic, measurements, and explicit success gates.
17
+ - Do not force a case into pipes when doing so removes a required guarantee.
18
+ - Consult repository-specific execution and credential policies before running examples.
19
+
20
+ Three layers, one binary:
9
21
 
10
22
  | Layer | File | Best for |
11
23
  |---|---|---|
@@ -55,6 +67,9 @@ writes the same text report stdout shows; the file never changes the exit code.
55
67
  exits 0 — gate with `expect 200` or `stats | assert (s =>
56
68
  s.redirectRefused === 0)`; exit 0 alone is never request success in load
57
69
  mode.
70
+ - Result shapes per execution context (`Response` vs timed `{status, ms, url}`
71
+ records, body draining, `.json()` availability): canonical table at
72
+ https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
58
73
  - Exit codes: 0 all pass · 1 any fail · 2 no files/bad args.
59
74
 
60
75
  ## .crust.ts fixtures (test-fixture)
@@ -104,6 +119,36 @@ module-scope promise-cached factory. Fixture files may import ONLY relative
104
119
  modules and Bun builtins (the binary can't resolve npm at runtime) —
105
120
  `Bun.SQL`, `Bun.file`, `Bun.jwt`.
106
121
 
122
+ ## What evidence does your test produce?
123
+
124
+ **Tier 1 — snapshot / contract verification.** Response vs a declared case +
125
+ supplied snapshot; setup prepares REQUEST CONTEXT (url, headers, expected),
126
+ not state. Never call it "seeded"/"integration" when nothing is seeded or
127
+ verified at the data layer — it proves contract shape + declared oracle.
128
+
129
+ **Tier 2 — DB-verified flow.** seed → verify the seed → call the API → query
130
+ the DB → compare against the LIVE query result. Native shape:
131
+
132
+ ```crust
133
+ {"email": "u-$RUN_ID@t.dev"} | POST $BASE/api/users -H "authorization: Bearer $TOKEN" | assert (r => r.status === 201) | (r => r.json()) | capture UID (u => u.id)
134
+ sql "SELECT email FROM users WHERE id = '$UID'" | assert (r => r.email === "u-$RUN_ID@t.dev")
135
+ ```
136
+
137
+ Carries: run-owned data (`$RUN_ID` unique per run, set by setup — never
138
+ shared mutable state); explicit identity matching (query the id you
139
+ captured, never an unrelated record); failure-path cleanup defined BEFORE
140
+ writes — a later line is happy-path only; guaranteed teardown is `crust.onExit`.
141
+
142
+ **Tier 3 — demo / quickstart.** Disposable local resource, ordinary CRUD —
143
+ the built-in mock server is the native fit — isolates "how little glue does
144
+ crust need" from app safeguards (graph resolution, provisioning denial,
145
+ identity-sensitive stores) — separate approved fixture, never folded in.
146
+
147
+ Cross-cutting: label the tier so a green run cannot imply DB-level proof it
148
+ never produced; approval/redirect/transport safeguards stay regardless of
149
+ tier; setup attestations (flags asserting IAM/graph/provisioning policy) are
150
+ DECLARATIONS, not backend verification.
151
+
107
152
  ## gen-fixtures — spec-driven cases
108
153
 
109
154
  ```
@@ -130,6 +175,10 @@ Setup-module contract (imported lazily): `shared()` promise-cached context,
130
175
  `flowOverrides: { "<template>": { body?: {...}, skip?: true } }` for creates
131
176
  whose values a spec can't express (business date rules → `body` merge;
132
177
  required foreign keys → `skip`).
178
+ When the contract disagrees with the 400 convention (422 backends, 404/403
179
+ concealment), the optional `expectations` block configures correct statuses —
180
+ a documented status always wins over a configured one;
181
+ `docs/USAGE.md §gen-fixtures` has the schema and precedence.
133
182
 
134
183
  The `--out` dir is DELETED and recreated every run — never hand-edit
135
184
  generated files.
@@ -145,10 +194,106 @@ whose create body cannot be filled — are dropped and NAMED on stdout
145
194
  (`gen-fixtures: 2 case(s) skipped for POST /things …`, `skipping flow for
146
195
  /things …`). A silently smaller suite would look like coverage.
147
196
 
197
+ Every generation's LAST write is `<out>/gen-manifest.json`: each case with
198
+ its expectation's provenance (`contract:<spec pointer>` when the spec
199
+ documents the status), each skip, each unsupported op — review it BEFORE
200
+ running. Deterministic (same spec + version → byte-identical); it records
201
+ generation, never execution.
202
+ https://lariocpt.github.io/crust-website/docs/recipes/gen-fixtures/
203
+
204
+ ## Generated cases are candidates, not certified coverage
205
+
206
+ A generator can derive request mutations from a schema. It cannot universally
207
+ infer backend-specific status codes, error envelopes, authorization
208
+ concealment policies, business preconditions, or cleanup behavior.
209
+
210
+ For negative tests:
211
+
212
+ - Begin with an otherwise valid request.
213
+ - Mutate one intended condition.
214
+ - Ensure authentication and resource preconditions allow the request to
215
+ reach the intended validation layer (a request that dies at auth never
216
+ proves the validation code).
217
+ - Assert the documented status.
218
+ - Where the contract provides structured error evidence, verify the relevant
219
+ field, location, or error code.
220
+ - Avoid exact human-readable message matching unless wording is contractual.
221
+
222
+ A generic error status is insufficient evidence when the request could have
223
+ failed for an unrelated reason. And the generated **400 validation matrix is
224
+ a generator assumption, not a universal API convention** — some contracts
225
+ specify `422` (or another status) for validation failures; when the contract
226
+ disagrees, say so in review and adjust expectations to the CONTRACT, never
227
+ the other way around. Document unsupported assumptions and skipped cases
228
+ rather than silently reducing coverage or normalizing the contract to fit
229
+ generation.
230
+
231
+ ## Contract review before generation
232
+
233
+ Review the contract BEFORE generating and record the review beside the
234
+ generated output. Record fields (one per line):
235
+
236
+ ```
237
+ API/service:
238
+ Contract path:
239
+ Contract revision:
240
+ Backend revision, if available:
241
+ Crust version:
242
+ Reviewer:
243
+ Review date:
244
+ Generated output directory:
245
+ Intended execution scope:
246
+ Unresolved discrepancies:
247
+ ```
248
+
249
+ Highest-risk checks (full checklist:
250
+ https://lariocpt.github.io/crust-website/docs/contract-review/):
251
+
252
+ - Expected statuses come from the reviewed contract — validation cases do
253
+ NOT default to 400 (a 422 contract must generate 422 expectations).
254
+ - Authorization cases account for intentional 404 concealment; unknown-id
255
+ cases use syntactically valid identifiers.
256
+ - One intended condition per negative case; unrelated fields stay valid so
257
+ the request reaches the intended validation layer.
258
+ - Dedicated `--out` dir; regeneration cannot touch manual fixtures; never
259
+ hand-edit generated files.
260
+ - Classify every failure (backend · contract · generator · environment ·
261
+ incorrect test) BEFORE changing any expectation.
262
+ - Execution is approved separately — target, identity, resource scope,
263
+ writes, cleanup. This review grants no permission beyond the recorded
264
+ execution scope.
265
+
148
266
  ## Workflow for a new API
149
267
 
150
- 1. `gen-fixtures` against the spec → run matrix + flows → fix what fails.
151
- 2. Hand-write `.pipes` for business flows the spec can't express (SQL
152
- checks, multi-resource invariants).
153
- 3. Reach for `.crust.ts` only when a case needs real code (crypto, files,
154
- custom matchers).
268
+ 1. Identify the authoritative contract and record its version or revision.
269
+ 2. Review generation assumptions against that contract: validation status
270
+ codes; authentication and authorization behavior; error envelopes;
271
+ resource identifiers; response schemas; CRUD lifecycle support.
272
+ 3. Generate into a dedicated output directory (`--out`, default `tests/gen`).
273
+ Generation DELETES and recreates that directory; never target manually
274
+ authored sources.
275
+ 4. Review generated cases, expected responses, setup requirements, and
276
+ skipped cases (they are NAMED on stdout) before execution.
277
+ 5. Obtain the required target, identity, resource, write, cleanup, and load
278
+ approvals from repository policy — the skill never grants any of them.
279
+ 6. Execute only the approved scope.
280
+ 7. Classify failures before changing expectations or implementation:
281
+ backend defect · contract defect · unsupported generator assumption ·
282
+ environment/setup failure · incorrect test.
283
+ 8. Add manually authored native fixtures or pipelines for unsupported cases.
284
+ Do not hand-edit generated files (the next regeneration erases the edit).
285
+ 9. Record remaining coverage gaps explicitly.
286
+
287
+ > Never change an expected result solely because the backend returned
288
+ > something different. A passing test must reflect a reviewed contract.
289
+
290
+ ## Version and evidence
291
+
292
+ Behavior described here was verified against **crust v0.3.5** (source-read
293
+ and run through the compiled binary where it matters: `--redirect` record
294
+ shapes, `output.schema` $ref refusal, gen-fixtures skip reporting). New in
295
+ **v0.3.6**: the `gen-manifest.json` review artifact, the setup-module
296
+ `expectations` block, and the evidence-tier taxonomy. Newer
297
+ crust versions may extend behavior; re-verify before documenting it. Keep
298
+ approved targets, credentials, transport exceptions, resource ownership, and
299
+ execution restrictions in repository policy, not in this skill.
@@ -27,12 +27,25 @@ load 10s 50/s, 30s 200/s | parallel 100 | GET :3000/health | stats --every 5
27
27
  load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users | expect 2xx | stats
28
28
  ```
29
29
 
30
+ > **Report availability:** `expect` throws at drain, so an `expect` placed
31
+ > BEFORE `stats` that fails prevents the summary from ever being emitted
32
+ > (the first two shapes above are illustrative of syntax, not of CI
33
+ > reporting). When the summary/report must survive a status failure, put
34
+ > `stats` (and `stats --out report.json`) before the gates and gate status
35
+ > on the summary instead — see CI gates below.
36
+
30
37
  - `parallel` puts ANY http verb in load mode: output is `{status, ms, url}`
31
38
  timing records, bodies drained, network errors become `status: 0` records
32
39
  (they show in the histogram instead of killing the run). So a run whose
33
40
  every request was refused prints `{"status":{"0":40}}` and still exits 0
34
41
  unless a gate is present — `expect 200` over a dead port exits 1. `stats`
35
42
  reports, it does not gate.
43
+ - **Completion ≠ success — three separate outcomes:** the run *completed*
44
+ (exit 0, no fatal stage error), *request correctness passed* (explicit
45
+ gates accepted the outcomes), *performance passed* (latency/throughput
46
+ thresholds held). A gate that only checks latency can be satisfied by fast
47
+ error responses — a 500 in 2ms is under any p95 budget. Latency-only
48
+ examples are NOT the definition of a passing load test; gate status too.
36
49
  - The summary carries failure tallies `timedOut` and `redirectRefused`
37
50
  (always present, 0 on a clean run): a `--timeout` budget hit or a
38
51
  `--redirect error` refusal is a `{status: 0, timedOut/redirectRefused:
@@ -43,6 +56,8 @@ load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users |
43
56
  Little's law sizes the pool: `N ≥ rate × p99-in-seconds`.
44
57
  - Durations `ms|s|m`; rates `N/s` or `N/m`, decimals allowed.
45
58
 
59
+ Canonical, version-pinned result-shape table: https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
60
+
46
61
  ## Honesty guarantees (read before quoting numbers)
47
62
 
48
63
  - A gate that measured NOTHING fails: `stats` over an empty stream is tagged
@@ -67,12 +82,24 @@ load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users |
67
82
 
68
83
  ## CI gates — thresholds are assert composition
69
84
 
85
+ The primary shape gates **status first, then performance**, with `stats`
86
+ (and the report) upstream of every gate so the summary survives a failing
87
+ gate. When failure artifacts must survive request-status failures, collect
88
+ statistics and write the report BEFORE applying aggregate gates:
89
+
70
90
  ```crust
71
- load 30s 100/s | parallel 50 | GET :3000/health | stats | assert (s => s.p95 < 200) | assert (s => s.rps > 80)
91
+ load 30s 100/s | parallel 50 | GET :3000/health | stats --out load/last.json | assert (s => Object.keys(s.status).every(k => k.startsWith("2"))) | assert (s => s.timedOut === 0) | assert (s => s.redirectRefused === 0) | assert (s => s.p95 < 200) | assert (s => s.rps > 80)
72
92
  ```
73
93
 
74
94
  One assert per threshold: the failure names the predicate and prints the
75
- actual summary, and the line exits 1 (`crust -c` stops there).
95
+ actual summary, and the line exits 1 (`crust -c` stops there). The gates
96
+ above cover all four things a load gate must mean: every response was 2xx
97
+ (a fast 500 is not a pass), no timeout or redirect-refusal records, the
98
+ latency budget held, and the achieved rate held. An empty run is refused by
99
+ `assert` itself (`empty: true`), so "measured nothing" never passes.
100
+ `rps` is the ACHIEVED rate — if the target was not sustained, stderr already
101
+ said so (`emitted … dropped … achieved …`); a drop report means the number
102
+ below the rate gate is the honest one.
76
103
 
77
104
  Baseline gate — `--out` writes a versioned artifact
78
105
  (`{crustStats: 1, startedAt, urls, summary, windows?}`), an async assert
@@ -121,3 +148,14 @@ const out = await load([{ durMs: 10_000, rps: 100 }])
121
148
 
122
149
  `timedHttpItem(method, url, opts?)` is the any-verb per-item timer when you
123
150
  need POST bodies from upstream items.
151
+
152
+ ## Version and evidence
153
+
154
+ Behavior here was verified against **crust v0.3.5** — summary schema
155
+ (`{count, wallMs, rps, status, timedOut, redirectRefused, p50, p95, p99,
156
+ meanMs}`, `empty: true` on a zero-request run) and the `--out`-before-gates
157
+ write order are source-verified; throughput claims (5000+ ticks/s) are
158
+ single-process measurements on the author's hardware — environment-dependent,
159
+ not a promise. Pick the lightest correct layer for the job (see the
160
+ crust-api-testing skill's shared principle), and keep approved targets and
161
+ execution restrictions in repository policy, not in this skill.
@@ -66,6 +66,31 @@ printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion
66
66
  range(1,5) | (n => n * n) | stats
67
67
  ```
68
68
 
69
+ ## Tool classes and teardown tiers
70
+
71
+ The `initialize` handshake carries this taxonomy in `instructions` (0.3.6):
72
+
73
+ - **Static check** — `crust_check_line`: parses, runs nothing.
74
+ - **Execution** — `crust_run_line` and the test/gen tools: arbitrary stages,
75
+ real side effects. Target approval, credentials, writes and cleanup stay
76
+ the caller's responsibility.
77
+ - **Network/load generation** — `crust_load_run`, `crust_verify_web_links`:
78
+ bounded generation, approved non-production targets only.
79
+ - **Long-lived resources** — mock server, procs, log sessions: held by the
80
+ server process, die when it exits.
81
+
82
+ Teardown has three tiers and no tool crosses them: **local processes** are
83
+ killed whole (process group, grandchildren included); **mock sessions** close
84
+ (port released — `--state` files/DBs are application data and stay on disk);
85
+ **application data** is never touched — a container, row or queue a proc
86
+ created remotely survives its local teardown, releasing it is your job.
87
+
88
+ Transport contract: stdio JSON-RPC, one frame per line; one-shot tools spawn
89
+ a fresh crust child per call (`cwd`/`env` per call, nothing persists). A
90
+ refusal whose error string never appears in crust's own output (an
91
+ "unavailable tool" style host error) never reached the server — debug the
92
+ host's spawn command, not crust.
93
+
69
94
  ## Honest failure — never a false pass over MCP
70
95
 
71
96
  A nonzero child exit, a killed timeout, a failed `assert`, a mock validation
@@ -97,8 +122,11 @@ filter (l => l.proc === "api") | (l => l.line) | grep -i timeout
97
122
  the returned `url`. `proxy: "<upstream>"` turns it into a conformance audit;
98
123
  read findings with `crust_mock_server_violations`.
99
124
  - `crust_load_run` composes the documented pipeline (`load … | parallel … |
100
- … | stats | assert`); an `assert` failure is `isError` — a run that
101
- measured nothing is refused (`empty`), never a green gate.
125
+ … | stats | assert`); gates go in `assert` — one predicate, or an array of
126
+ them (0.3.6), one assert stage per gate. An `expect` inside `target` runs BEFORE
127
+ stats and suppresses the summary when it fails; a post-stats `assert`
128
+ failure is `isError` with the stats still included. A run that measured
129
+ nothing is refused (`empty`), never a green gate.
102
130
 
103
131
  ## Limits, stated
104
132
 
@@ -9,6 +9,10 @@ crust is a Bun-powered pipeline runner where every command is a typed stream
9
9
  (`Pipeline<T>`). Stages compose under `|`. Plain shell still works — a line
10
10
  whose stages are all ordinary commands is handed to `sh -c` untouched.
11
11
 
12
+ Choose the lightest native stage that preserves correctness, observability,
13
+ and safety — not merely the shortest syntax (full statement: the
14
+ crust-api-testing skill's shared principle).
15
+
12
16
  ## How a stage is classified (first token decides)
13
17
 
14
18
  | You type | You get |
@@ -39,6 +43,33 @@ whose stages are all ordinary commands is handed to `sh -c` untouched.
39
43
 
40
44
  `:3000/path` expands to `http://localhost:3000/path` everywhere.
41
45
 
46
+ ## HTTP result shapes — same syntax, different payloads
47
+
48
+ Similar HTTP syntax does NOT guarantee identical result shapes. Know which
49
+ shape a stage receives before reaching for `.json()` or gating results
50
+ (verified against crust v0.3.5):
51
+
52
+ | Context | What the stage emits | Authoring implication |
53
+ |---|---|---|
54
+ | First-stage ordinary verb (`GET :3000/x`) | a `Response` | body parsing is available: `(r => r.json())` |
55
+ | Mid-pipeline ordinary verb, non-GET (per-item, untimed) | a `Response` per item | same — bodies still parseable |
56
+ | Timed per-item path (mid-pipeline `GET` — with or without `parallel` — or `parallel N \| <any other verb>`) | `{status, ms, url}` records; **bodies drained**; network errors are `status: 0` records; `--timeout`/`--redirect error` outcomes are `{status: 0, timedOut/redirectRefused: true}` records | `.json()` does NOT exist on these; gate status and the failure tallies explicitly |
57
+
58
+ Canonical, version-pinned table: https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
59
+
60
+ ## Completion is not success — three separate outcomes
61
+
62
+ 1. **Execution completed** — the pipeline ran without a fatal stage error (exit 0).
63
+ 2. **Request correctness passed** — explicit gates (`expect`, `assert`) accepted the request outcomes.
64
+ 3. **Performance passed** — explicit latency/throughput thresholds passed.
65
+
66
+ `stats` reports measurements; it is not itself a success gate. In timed/load
67
+ paths, timeouts, network failures, and redirect refusals are per-item
68
+ RECORDS (`status: 0` + a tally field), not exits — exit 0 alone is never
69
+ evidence that requests succeeded. Use an explicit expected-status gate
70
+ (`expect 200`/`expect 2xx`), and where stats is in play also check
71
+ `s.timedOut === 0` / `s.redirectRefused === 0`.
72
+
42
73
  ## Env expansion — parse time, opt-in per position
43
74
 
44
75
  `$VAR`/`${VAR}` expand from `process.env` in: URLs, whole `-H` header
@@ -159,3 +190,20 @@ Any stage throw → `crust: <message>` on stderr, line exits 1. `crust -c`,
159
190
  the same fail-fast loop and STOP at the first non-zero — put warmup and
160
191
  cleanup on their own lines. Blank lines and `#` comments are skipped, so a
161
192
  `#!/usr/bin/env crust` shebang works in `.crust` files.
193
+
194
+ **Cleanup on a later line is happy-path cleanup, not guaranteed teardown.**
195
+ Under fail-fast, an earlier failure means that line never runs. When
196
+ failure-path cleanup is a requirement (containers, temp rows, mock servers),
197
+ use the verified lifecycle hook: `crust.onExit = async (code) => {…}` in
198
+ `init.ts` — it fires on every shutdown-path exit,
199
+ including the fail-fast failure path of `-c`, script, and stdin runs
200
+ (test-pinned by `tests/onexit.test.ts`), and receives the exit code. If a needed
201
+ guarantee is still missing (e.g. `kill -9`), document the limitation rather
202
+ than implying line placement solves it.
203
+
204
+ ## Version and evidence
205
+
206
+ Behavior here was verified against **crust v0.3.5** — result shapes and
207
+ failure-record semantics by source and through the compiled binary; keep
208
+ approved targets, credentials, and execution restrictions in repository
209
+ policy, not in this skill.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: crust-verify-web-links
3
- description: Crawl a site from its sitemap and verify every link, #anchor, redirect chain, OG image and meta tag against .crust.ts fixtures. `--base-url` auto-discovers the sitemap (robots.txt → /sitemap.xml → /sitemap-index.xml), `--site-map-url` takes a local sitemap so you can check build output, `--fixtures` diffs meta. Use when link or SEO health must gate a deploy, when a built site should be checked before publishing, or when OG/meta regressions need catching.
3
+ description: Crawl a site from its sitemap and verify every link, #anchor, redirect chain, OG image and meta tag against .crust.ts fixtures. `--base-url` auto-discovers the sitemap (robots.txt → /sitemap.xml → /sitemap-index.xml), `--site-map-url` takes a local sitemap so you can check build output, `--fixtures` diffs meta. Every failure names its source page and element (structured `sources`/`meta`), and every failing run ends with an aligned fix-list. Use when link or SEO health must gate a deploy, when a built site should be checked before publishing, or when OG/meta regressions need catching.
4
4
  ---
5
5
 
6
6
  # crust verify-web-links
@@ -21,6 +21,40 @@ having left discovered links unchecked. `2` also covers `--site-map-url`
21
21
  together with `--base-url`, neither of them given, `no fixture files matched
22
22
  <glob>`, `--concurrency must be >= 1`, and `unexpected argument`.
23
23
 
24
+ ## Every failure names its source (new in 0.3.6)
25
+
26
+ Failures are **one per failing target**, and each answers "which page did I
27
+ find this on, and which element":
28
+
29
+ - `sources[]` — every link that pointed at the failing target, as
30
+ `{fromUrl, kind, href}`. In text each prints as an indented
31
+ `from <page> (<kind>)` line under the failure; in `--json` it is the full
32
+ array. A broken URL linked from fifty pages is still **one** failure with
33
+ fifty sources — `totals.failures` and exit codes are unchanged.
34
+ `kind: "sitemap"` means the URL arrived as a sitemap `<loc>` and no page
35
+ linked it; the sitemap URL is named as `fromUrl` instead of the source being
36
+ omitted.
37
+ - `meta` — structured `{key, expected, actual}` for `meta-mismatch`
38
+ (`meta-fixture-no-page` carries `key: "fixture"`).
39
+ - **Fix-list**: every failing text run ends with a separate aligned section —
40
+ one line per (page, element, broken-thing) triple, nothing else (excerpt of
41
+ the real output; the full failing report is in USAGE and on the website):
42
+
43
+ ```text
44
+ fix list
45
+ page element broken link / diff
46
+ http://localhost:47831/page-a a http://localhost:47831/broken
47
+ http://localhost:47831/page-a a http://localhost:47831/page-x#ghost
48
+ http://localhost:47831/page-b img http://localhost:47831/broken
49
+ http://localhost:47831/page-x meta:og:title got "Page X — Real Title" (expected "Page X — Wrong Title")
50
+ ```
51
+
52
+ A clean run prints no fix-list section. `--json` carries the structured
53
+ `sources`/`meta` fields instead — the data *is* the fix-list there.
54
+
55
+ The `detail` prose is unchanged from v0.3.5: the structured fields are purely
56
+ additive.
57
+
24
58
  ## Getting it pointed at the right thing
25
59
 
26
60
  ```crust