@opencraw/mcp 0.1.45 → 0.1.47

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.
Files changed (60) hide show
  1. package/README.md +27 -1
  2. package/dist/diff-tool/diff-tool.handler.d.ts +42 -0
  3. package/dist/diff-tool/index.d.ts +3 -0
  4. package/dist/docs-tool/docs-folder.repository.d.ts +24 -0
  5. package/dist/docs-tool/docs-root.client.d.ts +8 -0
  6. package/dist/docs-tool/docs-tool.handler.d.ts +21 -0
  7. package/dist/docs-tool/docs-topics.algorithm.d.ts +39 -0
  8. package/dist/docs-tool/index.d.ts +3 -0
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.esm.js +430 -1
  11. package/dist/list-tool/index.d.ts +3 -0
  12. package/dist/list-tool/list-recipes-tool.handler.d.ts +32 -0
  13. package/dist/main.d.ts +7 -0
  14. package/dist/probe-tool/index.d.ts +2 -0
  15. package/dist/probe-tool/probe-tool.handler.d.ts +27 -0
  16. package/dist/recipe-source/index.d.ts +3 -0
  17. package/dist/recipe-source/recipe-source.mapper.d.ts +21 -0
  18. package/dist/run-tool/index.d.ts +3 -0
  19. package/dist/run-tool/run-tool.handler.d.ts +51 -0
  20. package/dist/server/create-server.use-case.d.ts +11 -0
  21. package/dist/server/index.d.ts +2 -0
  22. package/dist/src/docs-tool/docs-folder.repository.d.ts +24 -0
  23. package/dist/src/docs-tool/docs-root.client.d.ts +8 -0
  24. package/dist/src/docs-tool/docs-tool.handler.d.ts +21 -0
  25. package/dist/src/docs-tool/docs-topics.algorithm.d.ts +39 -0
  26. package/dist/src/docs-tool/index.d.ts +3 -0
  27. package/dist/src/server/create-server.use-case.d.ts +1 -1
  28. package/dist/src/studio-tool/index.d.ts +2 -0
  29. package/dist/src/studio-tool/studio-command.client.d.ts +11 -0
  30. package/dist/src/studio-tool/studio-instance.repository.d.ts +22 -0
  31. package/dist/src/studio-tool/studio-tools.handler.d.ts +47 -0
  32. package/dist/studio-tool/index.d.ts +2 -0
  33. package/dist/studio-tool/studio-command.client.d.ts +11 -0
  34. package/dist/studio-tool/studio-instance.repository.d.ts +22 -0
  35. package/dist/studio-tool/studio-tools.handler.d.ts +47 -0
  36. package/dist/validate-tool/index.d.ts +3 -0
  37. package/dist/validate-tool/validate-tool.handler.d.ts +31 -0
  38. package/docs/how-it-works/01-recipes.md +698 -0
  39. package/docs/how-it-works/02-page-steps.md +672 -0
  40. package/docs/how-it-works/03-data-steps.md +813 -0
  41. package/docs/how-it-works/04-flow-steps.md +738 -0
  42. package/docs/how-it-works/05-templates-and-scope.md +428 -0
  43. package/docs/how-it-works/06-documents.md +1413 -0
  44. package/docs/how-it-works/07-mapping-and-transforms.md +699 -0
  45. package/docs/how-it-works/08-policies.md +405 -0
  46. package/docs/how-it-works/09-running.md +804 -0
  47. package/docs/how-it-works/README.md +225 -0
  48. package/docs/recipes/README.md +22 -0
  49. package/docs/recipes/access.md +440 -0
  50. package/docs/recipes/authoring.md +1503 -0
  51. package/docs/recipes/azure-durable.md +180 -0
  52. package/docs/recipes/captcha.md +204 -0
  53. package/docs/recipes/movies-example.md +12 -0
  54. package/docs/recipes/programmatic-authoring.md +101 -0
  55. package/docs/recipes/quick-guide.md +157 -0
  56. package/docs/recipes/vehicles-example.md +9 -0
  57. package/docs/recipes/worker-mode.md +221 -0
  58. package/docs/studio/manual.md +312 -0
  59. package/docs/studio/pdf-agreements.md +150 -0
  60. package/package.json +29 -4
@@ -0,0 +1,405 @@
1
+ <p align="center">
2
+ <img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
3
+ </p>
4
+
5
+ # Policies
6
+
7
+ [← How OpenCraw works](README.md) · Previous: [8. Mapping and transforms](07-mapping-and-transforms.md) · Next: [10. Running](09-running.md)
8
+
9
+ After its transforms, each field's value is coerced to the field's type and validated. Four things can go
10
+ wrong on the way, and each one ends in one of two ways:
11
+
12
+ | What went wrong | Example | Who decides |
13
+ |---|---|---|
14
+ | The value is **missing** | a season with no overtime losses recorded | the missing-value policy, in full precedence |
15
+ | The value **can't be coerced** | a relative link into a `url` field | the same policy, in full precedence |
16
+ | The value **fails validation** | `3` in a field with `min: 5` | the same policy, but only `skip-record` helps |
17
+ | A **transform throws** | the `integer` transform on an empty cell, a hook that throws | the mapping rule's own `onMissing` only |
18
+
19
+ The two endings: the record is **rejected** (`RecordRejectedError`: it is dropped, counted, and the walk goes
20
+ on), or the mapping **fails** (`MappingFailedError`: the step that emitted fails, and by default the recipe
21
+ stops). This page shows each case on real data. The reference is
22
+ [authoring.md §7](../recipes/authoring.md#7-policies-what-happens-when-something-is-missing-or-fails).
23
+
24
+ ## What counts as missing
25
+
26
+ A value is missing when it is `undefined` (the path isn't there, `regex` found no match, `first` of an empty
27
+ list), `null`, or `""`. An empty list is a value. An `object` field that ends up with no members is missing too.
28
+
29
+ Blank text, `""` or only spaces, is missing in a field of every type except `string` and `json`. An empty table
30
+ cell mapped into an `integer`, `number`, `currency`, `date`, `boolean`, `url` or `enum` field is missing, so the
31
+ field's `default`, `null` or its policy applies: it is never a coercion error, and a `boolean` field doesn't turn
32
+ it into `false`. A `string` or `json` field keeps text as it is, so there `""` is missing and `" "` is a value.
33
+ The [precedence](#the-precedence) scene below maps an empty cell into `integer` fields with no transform.
34
+
35
+ An explicit `number` or `integer` **transform** is another matter: it is asked to read a number, and blank text
36
+ has none, so it throws (see [transform failures](#transform-failures-look-only-at-the-rule)).
37
+
38
+ ## The precedence
39
+
40
+ When a field's value is missing, the policy comes from the first of these that is set:
41
+
42
+ 1. the mapping rule's `onMissing` (in the input recipe);
43
+ 2. the field's `onMissing` (in the output recipe);
44
+ 3. `default`, when the field has a `default` value;
45
+ 4. the output recipe's `onMissing`;
46
+ 5. built in: `fail` when the field is `required`, `null` otherwise.
47
+
48
+ The rule comes first because it belongs to one site: an input recipe can relax or tighten a field for its own
49
+ source without changing the shared output recipe.
50
+
51
+ The hockey table on [scrapethissite.com](https://www.scrapethissite.com/pages/forms/?q=Boston) leaves the "OT
52
+ Losses" column empty before 1999, when the league started counting overtime losses. The scene reads Boston's
53
+ seasons from 1997 to 2001 ([recipes](recipes/policy-precedence)) into an output recipe whose `onMissing` is
54
+ `skip-record`, with four `integer` fields fed from the same cell, with no transform. Before 1999 the cell is
55
+ empty, and blank text in an `integer` field is missing:
56
+
57
+ ```json
58
+ "onMissing": "skip-record",
59
+ "fields": {
60
+ "otLosses": { "type": "integer", "default": 0 },
61
+ "otLossesField": { "type": "integer", "default": 0, "onMissing": "null" },
62
+ "otLossesRule": { "type": "integer", "default": 0, "onMissing": "default" },
63
+ "otLossesNote": { "type": "integer", "default": "-1" }
64
+ }
65
+ ```
66
+
67
+ and in the input recipe, `"otLossesRule": { "from": "ot", "onMissing": "null" }`.
68
+
69
+ <!-- capture:policy-precedence records n=3 -->
70
+ ```json
71
+ {"team":"Boston Bruins","year":1997,"otLosses":0,"otLossesField":null,"otLossesRule":null,"otLossesNote":-1}
72
+ {"team":"Boston Bruins","year":1998,"otLosses":0,"otLossesField":null,"otLossesRule":null,"otLossesNote":-1}
73
+ {"team":"Boston Bruins","year":1999,"otLosses":6,"otLossesField":6,"otLossesRule":6,"otLossesNote":6}
74
+ ```
75
+ <!-- /capture -->
76
+
77
+ <!-- capture:policy-precedence mapping fields=otLosses,otLossesRule record=0 -->
78
+ | Field | Step | Value |
79
+ |---|---|---|
80
+ | `otLosses` | read | `""` |
81
+ | | **field** | `0` |
82
+ | `otLossesRule` | read | `""` |
83
+ | | **field** | `null` |
84
+ <!-- /capture -->
85
+
86
+ For 1997 and 1998, with the cell empty:
87
+
88
+ - `otLosses`: no rule or field policy, and the field has a `default`, so step 3 applies: `0`. The output
89
+ recipe's `skip-record` (step 4) is never reached, which is why no season is dropped.
90
+ - `otLossesField`: the field's own `onMissing: "null"` (step 2) comes before its `default`: `null`.
91
+ - `otLossesRule`: the field says `default`, the rule says `null`, and the rule wins (step 1): `null`.
92
+ - `otLossesNote`: step 3 again, with the `default` written as the text `"-1"`. A `default` is coerced to its
93
+ field's type like a mapped value, so the field holds the integer `-1`.
94
+
95
+ From 1999 the cell has a number, and no policy is consulted.
96
+
97
+ The loader coerces and validates every `default` before anything runs. With `"default": "not recorded"` on
98
+ `otLossesNote`, `loadRecipeSet` rejects the set with a `RecipeBindingError`, and no page is requested:
99
+
100
+ ```text
101
+ recipes do not bind
102
+ ot-season fields.otLossesNote.default: "not recorded" is not a valid default: integer field: no number in "not recorded"
103
+ ```
104
+
105
+ A `default` that breaks the field's `min`, `max` or `pattern` fails the same way, and so does
106
+ `onMissing: "default"` on a field (or a rule) whose field has no `default`.
107
+
108
+ ## The four policies
109
+
110
+ | Policy | Effect on a missing value |
111
+ |---|---|
112
+ | `skip-record` | `RecordRejectedError`: the record is dropped, `rejected` is counted, a `record:reject` event (`✖` in the trace) names the field, and the walk goes on. |
113
+ | `fail` | `MappingFailedError`: the step that emitted fails, and goes through its `onError`. |
114
+ | `null` | The field is `null`, when it is `nullable` or not `required`. A required field that isn't `nullable` fails instead. |
115
+ | `default` | The field's `default`, coerced to the field's type (`"0"` in a `number` field is `0`). The loader checks it: a `default` the field can't take, or a `default` policy on a field without one, is a `RecipeBindingError`. |
116
+
117
+ The output recipe's own `onMissing` takes only `fail`, `skip-record` or `null`; `default` belongs to a field.
118
+
119
+ ## One missing tag, three outcomes
120
+
121
+ Page 3 of the quotes API on [quotes.toscrape.com](https://quotes.toscrape.com/api/quotes?page=3) has a quote
122
+ with no tags: its eighth, by J.K. Rowling. Three input recipes read pages 3 and 4 into the same output recipe,
123
+ whose `mainTag` is `required` and mapped with `first` on the tags, so it is missing for that quote
124
+ ([recipes](recipes/policy-quotes)):
125
+
126
+ - `quotes-reject` puts `"onMissing": "skip-record"` on the `mainTag` rule;
127
+ - `quotes-skip-step` has no policy, and `"onError": { "policy": "skip" }` on the emitting `forEach`;
128
+ - `quotes-stop` has no policy anywhere, so the built-in `fail` applies.
129
+
130
+ <!-- capture:policy-quotes summary -->
131
+ ```text
132
+ quotes-reject: 19 emitted, 1 rejected, 0 duplicates, 2 pages
133
+ quotes-skip-step: 17 emitted, 0 rejected, 0 duplicates, 2 pages
134
+ quotes-stop: 7 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: mainTag: missing
135
+ ```
136
+ <!-- /capture -->
137
+
138
+ <!-- capture:policy-quotes trace grep=✖|↷|■ -->
139
+ ```text
140
+ ✖ record rejected: mainTag: missing
141
+ ■ quotes-reject: 19 emitted, 1 rejected, 0 duplicates, 2 pages, … ms
142
+ ↷ steps.2 forEach skipped: mapping failed: mainTag: missing
143
+ ■ quotes-skip-step: 17 emitted, 0 rejected, 0 duplicates, 1 steps skipped, 2 pages, … ms
144
+ ✖ step steps.2 (forEach) failed: mapping failed: mainTag: missing
145
+ ■ quotes-stop: 7 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
146
+ ✖ stopped: step steps.2 (forEach) failed: mapping failed: mainTag: missing
147
+ ```
148
+ <!-- /capture -->
149
+
150
+ **`quotes-reject`: the record is rejected.** The quote is dropped and counted, and the walk goes on to the
151
+ next quote and to page 4: 19 records and 1 rejected. The captured records show the rejected one in its place,
152
+ with the field and the reason:
153
+
154
+ <!-- capture:policy-quotes records n=9 -->
155
+ ```json
156
+ {"text":"“I love you without knowing how, or when, or from where. I love you simply, without problems or pride: I love you in this way because I do not know any other…","author":"Pablo Neruda","mainTag":"love"}
157
+ {"text":"“For every minute you are angry you lose sixty seconds of happiness.”","author":"Ralph Waldo Emerson","mainTag":"happiness"}
158
+ {"text":"“If you judge people, you have no time to love them.”","author":"Mother Teresa","mainTag":"attributed-no-source"}
159
+ {"text":"“Anyone who thinks sitting in church can make you a Christian must also think that sitting in a garage can make you a car.”","author":"Garrison Keillor","mainTag":"humor"}
160
+ {"text":"“Beauty is in the eye of the beholder and it may be necessary from time to time to give a stupid or misinformed beholder a black eye.”","author":"Jim Henson","mainTag":"humor"}
161
+ {"text":"“Today you are You, that is truer than true. There is no one alive who is Youer than You.”","author":"Dr. Seuss","mainTag":"comedy"}
162
+ {"text":"“If you want your children to be intelligent, read them fairy tales. If you want them to be more intelligent, read them more fairy tales.”","author":"Albert Einstein","mainTag":"children"}
163
+ {"rejected":{"field":"mainTag","reason":"missing"}}
164
+ {"text":"“Logic will get you from A to Z; imagination will get you everywhere.”","author":"Albert Einstein","mainTag":"imagination"}
165
+ ```
166
+ <!-- /capture -->
167
+
168
+ <!-- capture:policy-quotes trace lines=14 -->
169
+ ```text
170
+ ▶ quotes-reject (api)
171
+ ⇄ access direct (direct)
172
+ ⇢ page 1 https://quotes.toscrape.com/api/quotes?page=3
173
+ · steps.0 request … ms
174
+ · steps.1 extract quotes … ms
175
+ ✚ record (no key)
176
+ ✚ record (no key)
177
+ ✚ record (no key)
178
+ ✚ record (no key)
179
+ ✚ record (no key)
180
+ ✚ record (no key)
181
+ ✚ record (no key)
182
+ ✖ record rejected: mainTag: missing
183
+ ✚ record (no key)
184
+ …
185
+ ```
186
+ <!-- /capture -->
187
+
188
+ **`quotes-stop`: the mapping fails, and the recipe stops.** A `MappingFailedError` isn't handled where the
189
+ record is made. It rejects inside the step that emitted, here the `forEach`, which fails as a whole, not only
190
+ the one iteration. The `forEach` has no `onError`, and neither has the recipe, so the policy is `fail`: the
191
+ recipe stops with the error in its report (`errorKind: "mapping"`) and a `✖ stopped` line. The seven records
192
+ written before stay in the sink. Page 4 is never read: it is a second start point of the same recipe, and the
193
+ recipe has stopped. Another input recipe in the same run still runs, unless the crawler was created with
194
+ `onRecipeError: "stop"`.
195
+
196
+ **`quotes-skip-step`: the step is skipped.** With `onError: skip` on the `forEach`, the failure becomes a
197
+ `step:skip` (`↷`). The walk goes on after the `forEach`, and page 4 is read. But the whole `forEach` was
198
+ skipped: the two quotes after Rowling's on page 3 were never mapped, so the recipe has 17 records, not 19. Use
199
+ `skip-record` to drop one record; `onError` acts on steps. (The `summary` above doesn't count skipped steps;
200
+ the `■` line in the trace does.)
201
+
202
+ ## Coercion failures follow the full precedence
203
+
204
+ A value that can't be converted to the field's type goes through the same precedence as a missing one (fixed
205
+ in [#65](https://github.com/Open-Craw/OpenCraw/issues/65): before, the output recipe's `onMissing` was
206
+ ignored). If it resolves to `skip-record`, the record is rejected. **Anything else fails the mapping**: a
207
+ `null` or `default` policy doesn't replace a value that is there but wrong.
208
+
209
+ The scene maps the first three links of books.toscrape.com's Mystery category, as the page writes them
210
+ (`../../../sharp-objects_997/index.html`), into a `url` field, which needs an absolute URL
211
+ ([recipes](recipes/policy-coercion)). The output recipe's `onMissing` is `skip-record`. `link-as-is` maps the
212
+ link as it is; `link-null` adds `"onMissing": "null"` to the rule:
213
+
214
+ <!-- capture:policy-coercion summary -->
215
+ ```text
216
+ link-as-is: 0 emitted, 3 rejected, 0 duplicates, 1 pages
217
+ link-null: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
218
+ ```
219
+ <!-- /capture -->
220
+
221
+ <!-- capture:policy-coercion records n=3 -->
222
+ ```json
223
+ {"rejected":{"field":"url","reason":"\"../../../sharp-objects_997/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
224
+ {"rejected":{"field":"url","reason":"\"../../../in-a-dark-dark-wood_963/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
225
+ {"rejected":{"field":"url","reason":"\"../../../the-past-never-ends_942/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
226
+ ```
227
+ <!-- /capture -->
228
+
229
+ <!-- capture:policy-coercion trace grep=▶|✖ -->
230
+ ```text
231
+ ▶ link-as-is (api)
232
+ ✖ record rejected: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
233
+ ✖ record rejected: url: "../../../in-a-dark-dark-wood_963/index.html" is not an absolute URL (use the absoluteUrl transform)
234
+ ✖ record rejected: url: "../../../the-past-never-ends_942/index.html" is not an absolute URL (use the absoluteUrl transform)
235
+ ▶ link-null (api)
236
+ ✖ step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
237
+ ■ link-null: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
238
+ ✖ stopped: step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
239
+ ```
240
+ <!-- /capture -->
241
+
242
+ For `link-as-is`, nothing is set on the rule or the field and there is no `default`, so the output recipe's
243
+ `skip-record` applies and each book is rejected. For `link-null`, the rule says `null`, which isn't
244
+ `skip-record`, so the first book fails the mapping and the recipe stops. The fix is the `absoluteUrl`
245
+ transform, which the error message names. The rejection holds the field and the reason apart; the trace line
246
+ joins them, with the field's path once.
247
+
248
+ ## Transform failures look only at the rule
249
+
250
+ When a transform throws, the only policy consulted is the mapping rule's own `onMissing`, and only
251
+ `skip-record` counts: it reads as "without this field the record is worthless". The field's policy, its
252
+ `default` and the output recipe's `onMissing` are not consulted, and the mapping fails.
253
+
254
+ The same hockey seasons, 1997 to 2001, into an output recipe whose `onMissing` is `skip-record` and whose
255
+ `otLosses` is an `integer` field. Three input recipes map the empty cell three ways
256
+ ([recipes](recipes/policy-transform)):
257
+
258
+ - `ot-integer-skip`: `integer` transform, `"onMissing": "skip-record"` on the rule;
259
+ - `ot-integer`: `integer` transform, nothing else;
260
+ - `ot-plain`: no transform.
261
+
262
+ <!-- capture:policy-transform summary -->
263
+ ```text
264
+ ot-integer-skip: 3 emitted, 2 rejected, 0 duplicates, 1 pages
265
+ ot-integer: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
266
+ ot-plain: 3 emitted, 2 rejected, 0 duplicates, 1 pages
267
+ ```
268
+ <!-- /capture -->
269
+
270
+ <!-- capture:policy-transform trace grep=▶|✖ -->
271
+ ```text
272
+ ▶ ot-integer-skip (api)
273
+ ✖ record rejected: otLosses: transform "integer": no number in ""
274
+ ✖ record rejected: otLosses: transform "integer": no number in ""
275
+ ▶ ot-integer (api)
276
+ ✖ step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
277
+ ■ ot-integer: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
278
+ ✖ stopped: step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
279
+ ▶ ot-plain (api)
280
+ ✖ record rejected: otLosses: missing
281
+ ✖ record rejected: otLosses: missing
282
+ ```
283
+ <!-- /capture -->
284
+
285
+ <!-- capture:policy-transform records n=5 -->
286
+ ```json
287
+ {"rejected":{"field":"otLosses","reason":"transform \"integer\": no number in \"\""}}
288
+ {"rejected":{"field":"otLosses","reason":"transform \"integer\": no number in \"\""}}
289
+ {"team":"Boston Bruins","year":1999,"otLosses":6}
290
+ {"team":"Boston Bruins","year":2000,"otLosses":8}
291
+ {"team":"Boston Bruins","year":2001,"otLosses":9}
292
+ ```
293
+ <!-- /capture -->
294
+
295
+ - `ot-integer-skip`: the `integer` transform throws on `""`: it was asked for a number and there is none. The
296
+ rule says `skip-record`, so 1997 and 1998 are rejected, and 1999 to 2001 kept.
297
+ - `ot-integer`: the same error, and the rule says nothing. The output recipe's `skip-record` isn't consulted
298
+ for a transform failure, so the first season stops the recipe.
299
+ - `ot-plain`: no transform, so no transform failure. `""` reaches the `integer` field, where blank text is
300
+ missing ([What counts as missing](#what-counts-as-missing)). Nothing on the rule or the field, and no
301
+ `default`, so the output recipe's `skip-record` applies: the two seasons are rejected as `missing`.
302
+
303
+ An `integer` field reads the number itself, so a transform that only does the same adds a way to fail. Keep the
304
+ `number` transform for what the field can't do, such as reading with a `locale`, or before a step that needs a
305
+ number (`sum`), and give its rule `skip-record` when the cell can be empty.
306
+
307
+ ## Validation failures
308
+
309
+ After coercion, a present value is checked against the field's rules: `min` and `max` for numbers,
310
+ `minLength`, `maxLength` and `pattern` for text, `minLength` and `maxLength` for arrays
311
+ ([authoring.md §1.1](../recipes/authoring.md#11-field-spec)). A value that breaks one is resolved like a
312
+ coercion failure: the policy in full precedence, `skip-record` rejects the record, anything else fails the
313
+ mapping (`mapping failed: n: 3 is below the minimum 5`). None of this guide's sites gave a value out of range,
314
+ so there is no captured example.
315
+
316
+ ## Rejected or failed
317
+
318
+ | | `RecordRejectedError` | `MappingFailedError` |
319
+ |---|---|---|
320
+ | Raised by | `skip-record` for a missing, unconvertible or invalid value; a transform failure with the rule's `skip-record` | every other policy for those; a transform failure without the rule's `skip-record`; `each` over something that isn't a list |
321
+ | Message | `record rejected: <field>: <reason>` | `mapping failed: <field>: <reason>` |
322
+ | Event | `record:reject` with `field`, `reason`, and the snapshot under `debug` | with `fail`, `error` and then `recipe:finish` with `error`; with `skip`, `step:skip` |
323
+ | Report | `rejected` + 1 | with `fail`, `error` and `errorKind: "mapping"`; with `skip`, `stepsSkipped` + 1 |
324
+ | The walk | goes on; `maxRecords` counts only emitted records | the emitting step fails through its `onError`: `fail` (the default) stops the recipe, `skip` skips the whole step |
325
+ | Records written before | kept | kept |
326
+
327
+ ## The items of `each`
328
+
329
+ An `each` rule builds a list of objects, and each member of each item goes through the same steps as a field:
330
+ coercion, validation, then the missing-value policy with its `default`. The precedence is the same too, with the
331
+ rule inside `each.fields` first. A problem is reported at the item's index (`variants[1].size`).
332
+
333
+ PokeAPI lists the abilities a Pokémon had in older generations. For Bulbasaur the one entry has
334
+ `"ability": null`, because its hidden ability didn't exist before generation V:
335
+
336
+ <!-- capture:policy-each-gap scope ids=past -->
337
+ ```json
338
+ {
339
+ "past": [
340
+ {
341
+ "generation": {
342
+ "name": "generation-iv",
343
+ "url": "https://pokeapi.co/api/v2/generation/4/"
344
+ },
345
+ "abilities": [
346
+ {
347
+ "is_hidden": true,
348
+ "slot": 3,
349
+ "ability": null
350
+ }
351
+ ]
352
+ }
353
+ ]
354
+ }
355
+ ```
356
+ <!-- /capture -->
357
+
358
+ The output recipe makes `name` required in each item ([recipes](recipes/policy-each-gap)). Two input recipes
359
+ map it: `pokeapi-past` sets no policy, and `pokeapi-past-skip` puts `"onMissing": "skip-record"` on the `name`
360
+ rule inside `each.fields`:
361
+
362
+ <!-- capture:policy-each-gap summary -->
363
+ ```text
364
+ pokeapi-past-skip: 0 emitted, 1 rejected, 0 duplicates, 1 pages
365
+ pokeapi-past: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
366
+ ```
367
+ <!-- /capture -->
368
+
369
+ <!-- capture:policy-each-gap trace grep=✖ -->
370
+ ```text
371
+ ✖ record rejected: pastAbilities[0].name: missing
372
+ ✖ step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
373
+ ■ pokeapi-past: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
374
+ ✖ stopped: step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
375
+ ```
376
+ <!-- /capture -->
377
+
378
+ With no policy anywhere, the required member's built-in `fail` applies and the recipe stops. With the rule's
379
+ `skip-record`, the record is rejected and the rejection names the member at its index:
380
+
381
+ <!-- capture:policy-each-gap record -->
382
+ ```json
383
+ {
384
+ "rejected": {
385
+ "field": "pastAbilities[0].name",
386
+ "reason": "missing"
387
+ }
388
+ }
389
+ ```
390
+ <!-- /capture -->
391
+
392
+ The loader checks `each` rules as well. A name in `each.fields` that the items don't have, or a required member
393
+ that no rule maps (and that has no `default`), is a `RecipeBindingError`. Renaming `name` to `title` in the rule
394
+ gives both:
395
+
396
+ ```text
397
+ recipes do not bind
398
+ pokeapi-past mapping.pastAbilities.fields.title: the items of "pastAbilities" have no field "title"
399
+ pokeapi-past mapping.pastAbilities.fields: required field of the items of "pastAbilities" "name" is not mapped
400
+ ```
401
+
402
+ ## What's next
403
+
404
+ [Running](09-running.md): sessions, access, blocks, retries and the other things that decide whether a page
405
+ arrives at all.