@zvndev/darkmown 2.2.0 → 2.3.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +34 -2
  3. package/docs/spec-alignment.md +10 -0
  4. package/package.json +16 -11
  5. package/site/pages/docs/index.wd +14 -0
  6. package/site/pages/playground/index.js +47 -46
  7. package/src/catalog.js +16 -1
  8. package/src/compiler/actions.js +42 -22
  9. package/src/compiler/body.js +36 -14
  10. package/src/compiler/collections.js +16 -10
  11. package/src/compiler/context.js +39 -10
  12. package/src/compiler/fetch.js +39 -15
  13. package/src/compiler/format.js +5 -2
  14. package/src/compiler/forms.js +91 -26
  15. package/src/compiler/frontmatter.js +11 -2
  16. package/src/compiler/fs-reader.js +2 -1
  17. package/src/compiler/image-size.js +340 -0
  18. package/src/compiler/includes.js +8 -4
  19. package/src/compiler/interpolation.js +15 -7
  20. package/src/compiler/loops.js +63 -35
  21. package/src/compiler/markdown.js +4 -3
  22. package/src/compiler/media.js +17 -3
  23. package/src/compiler/page.js +8 -6
  24. package/src/compiler/predicates.js +49 -24
  25. package/src/compiler/reader.js +12 -2
  26. package/src/compiler/state.js +16 -2
  27. package/src/compiler/structure.js +24 -14
  28. package/src/deploy.js +8 -4
  29. package/src/errors.js +1156 -0
  30. package/src/router.js +7 -4
  31. package/src/scaffold.js +4 -2
  32. package/src/skin.js +17 -6
  33. package/types/api-runner.d.ts +94 -0
  34. package/types/builder.d.ts +141 -10
  35. package/types/catalog.d.ts +97 -0
  36. package/types/compiler/actions.d.ts +41 -0
  37. package/types/compiler/body.d.ts +11 -0
  38. package/types/compiler/collections.d.ts +76 -0
  39. package/types/compiler/context.d.ts +661 -0
  40. package/types/compiler/directives.d.ts +6 -0
  41. package/types/compiler/expr-ast.d.ts +20 -0
  42. package/types/compiler/fetch.d.ts +29 -0
  43. package/types/compiler/format.d.ts +49 -0
  44. package/types/compiler/forms.d.ts +94 -0
  45. package/types/compiler/frontmatter.d.ts +33 -0
  46. package/types/compiler/fs-reader.d.ts +7 -0
  47. package/types/compiler/image-size.d.ts +14 -0
  48. package/types/compiler/includes.d.ts +100 -0
  49. package/types/compiler/index.d.ts +5 -0
  50. package/types/compiler/interpolation.d.ts +115 -0
  51. package/types/compiler/loops.d.ts +36 -0
  52. package/types/compiler/markdown.d.ts +34 -0
  53. package/types/compiler/media.d.ts +32 -0
  54. package/types/compiler/page.d.ts +109 -0
  55. package/types/compiler/predicates.d.ts +50 -0
  56. package/types/compiler/reader.d.ts +56 -0
  57. package/types/compiler/state.d.ts +68 -0
  58. package/types/compiler/structure.d.ts +56 -0
  59. package/types/compiler.d.ts +35 -223
  60. package/types/dev.d.ts +47 -2
  61. package/types/errors.d.ts +100 -0
  62. package/types/feeds.d.ts +150 -0
  63. package/types/headers.d.ts +44 -0
  64. package/types/highlight.d.ts +24 -0
  65. package/types/router.d.ts +42 -3
  66. package/types/scaffold.d.ts +15 -3
  67. package/types/skin.d.ts +28 -1
  68. package/types/statics.d.ts +2 -1
package/src/errors.js ADDED
@@ -0,0 +1,1156 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The stable compile-error code registry.
3
+ //
4
+ // Every author-facing compile error carries a `WDxxx` code: `wdError()`
5
+ // (src/compiler/context.js) prefixes the message with `[WDxxx] ` and mirrors it
6
+ // onto `err.wd.code`, so a user can paste the code into a search box and tooling
7
+ // can match on an identifier instead of prose. This file is the single source of
8
+ // truth for what those codes MEAN; `docs/errors.md` is generated from it
9
+ // (scripts/gen-errors.mjs) and `directiveCatalog().errors` exposes it to the
10
+ // machine-readable surface AI tooling already consumes.
11
+ //
12
+ // NUMBERING (the hundreds digit is the subsystem; it never moves):
13
+ // WD0xx source, frontmatter, prose interpolation, block structure
14
+ // WD1xx loops (@loop) and typed content collections
15
+ // WD2xx state (:state/:store/:computed/:theme) and the expression whitelists
16
+ // WD3xx actions (:button/:effect/:every)
17
+ // WD4xx forms and form fields
18
+ // WD5xx data fetching (:fetch) and URL safety
19
+ // WD6xx includes and page structure (@include, :::, :if, :carousel, :try)
20
+ // WD7xx media and embeds
21
+ // WD8xx skins and styling
22
+ // WD9xx project, routing, deploy, CLI
23
+ //
24
+ // CODES ARE A PUBLIC CONTRACT. Once a code ships it is permanent:
25
+ // * NEVER renumber a code, and NEVER reuse a retired one. A code that is
26
+ // withdrawn goes in RETIRED_CODES below and stays there forever, so the
27
+ // next number is always fresh even when the error itself is gone.
28
+ // * A code's MEANING may be refined (better wording, a better fix) but must
29
+ // keep pointing at the same authoring mistake.
30
+ // * New errors take the next free number in their subsystem block.
31
+ //
32
+ // WHAT GETS A CODE. Author-facing compile errors (anything a `.wd`/`.md`/
33
+ // `.skin`/frontmatter/CLI input can trigger) all go through `wdError` and carry
34
+ // a code. Genuine INTERNAL invariants do not: `src/compiler/expr-ast.js` parses
35
+ // the compiler's own already-validated output and `src/compiler/reader.js`
36
+ // guards the host reader contract, so a throw there is a framework bug, not
37
+ // something an author can fix, and it stays a plain `Error`. That split is
38
+ // enforced by tests/error-codes.test.js.
39
+ //
40
+ // Examples are drawn from the SAME `*_EXAMPLE` constants the compiler's error
41
+ // hints use, exactly like src/catalog.js, so a documented example cannot drift
42
+ // from what actually compiles.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ import {
46
+ ACTION_EXAMPLE,
47
+ BUTTON_EXAMPLE,
48
+ EFFECT_EXAMPLE,
49
+ EVERY_EXAMPLE
50
+ } from "./compiler/actions.js";
51
+ import { FETCH_EXAMPLE } from "./compiler/fetch.js";
52
+ import {
53
+ BIND_EXAMPLE,
54
+ CHECKBOX_EXAMPLE,
55
+ FORM_EXAMPLE,
56
+ INPUT_EXAMPLE,
57
+ RADIO_EXAMPLE,
58
+ SELECT_EXAMPLE,
59
+ SLIDER_EXAMPLE,
60
+ SUBMIT_EXAMPLE,
61
+ TEXTAREA_EXAMPLE
62
+ } from "./compiler/forms.js";
63
+ import { LOOP_EXAMPLE } from "./compiler/loops.js";
64
+ import { EMBED_EXAMPLE, VIDEO_EXAMPLE } from "./compiler/media.js";
65
+ import { COMPUTED_EXAMPLE, STATE_EXAMPLE, STORE_EXAMPLE, THEME_EXAMPLE } from "./compiler/state.js";
66
+ import {
67
+ CAROUSEL_EXAMPLE,
68
+ CONTAINER_EXAMPLE,
69
+ IF_EXAMPLE,
70
+ INCLUDE_EXAMPLE
71
+ } from "./compiler/structure.js";
72
+
73
+ /**
74
+ * One subsystem block of the numbering scheme.
75
+ * @typedef {object} ErrorArea
76
+ * @property {number} block The hundreds digit (0-9).
77
+ * @property {string} range Human label for the block, e.g. `WD1xx`.
78
+ * @property {string} name Short machine name, e.g. `loops`.
79
+ * @property {string} title One-line description of what the block covers.
80
+ */
81
+
82
+ /**
83
+ * A registered error code.
84
+ * @typedef {object} ErrorEntry
85
+ * @property {string} code The stable `WDxxx` identifier.
86
+ * @property {string} area The subsystem name (derived from the hundreds digit).
87
+ * @property {string} title Short name for the mistake.
88
+ * @property {string} cause What the author wrote that triggers it.
89
+ * @property {string} fix The corrective action, in one line.
90
+ * @property {string} [example] A concrete, compilable line, sourced from the
91
+ * compiler's own `*_EXAMPLE` constants.
92
+ */
93
+
94
+ /** The subsystem blocks, in code order. */
95
+ /** @type {ErrorArea[]} */
96
+ export const ERROR_AREAS = [
97
+ { block: 0, range: "WD0xx", name: "source", title: "Source, frontmatter & block structure" },
98
+ { block: 1, range: "WD1xx", name: "loops", title: "Loops & collections" },
99
+ { block: 2, range: "WD2xx", name: "state", title: "State & expressions" },
100
+ { block: 3, range: "WD3xx", name: "actions", title: "Button, effect & timer actions" },
101
+ { block: 4, range: "WD4xx", name: "forms", title: "Forms & form fields" },
102
+ { block: 5, range: "WD5xx", name: "fetch", title: "Data fetching & URL safety" },
103
+ { block: 6, range: "WD6xx", name: "structure", title: "Includes & page structure" },
104
+ { block: 7, range: "WD7xx", name: "media", title: "Media & embeds" },
105
+ { block: 8, range: "WD8xx", name: "skin", title: "Skins & styling" },
106
+ { block: 9, range: "WD9xx", name: "project", title: "Project, routing & CLI" }
107
+ ];
108
+
109
+ /**
110
+ * Codes that were withdrawn. They are NEVER reused: a retired number stays
111
+ * listed here so the next free number in its block is always genuinely free,
112
+ * and so an old error message found in a log or an issue still resolves to a
113
+ * meaningful (if historical) entry. Empty today.
114
+ * @type {string[]}
115
+ */
116
+ export const RETIRED_CODES = [];
117
+
118
+ /** The registry, in code order. */
119
+ /** @type {Omit<ErrorEntry, "area">[]} */
120
+ const ENTRIES = [
121
+ // --- WD0xx: source, frontmatter, prose, block structure ------------------
122
+ {
123
+ code: "WD001",
124
+ title: "Unterminated frontmatter",
125
+ cause: "The file opens with `---` but never closes the block.",
126
+ fix: "Add a closing `---` on its own line before the page body."
127
+ },
128
+ {
129
+ code: "WD002",
130
+ title: "Disallowed path segment",
131
+ cause: "A dotted path contains `constructor`, `prototype`, or `__proto__`.",
132
+ fix: "Read a different path; those segments are rejected everywhere for safety."
133
+ },
134
+ {
135
+ code: "WD003",
136
+ title: "Cannot interpolate an object value",
137
+ cause: "`{ name }` resolved to an object, which has no sensible text form.",
138
+ fix: "Interpolate one of its fields (`{ name.title }`) or iterate it with `@loop`."
139
+ },
140
+ {
141
+ code: "WD004",
142
+ title: "Unbalanced JSON value",
143
+ cause: "A `:state`/`:store`/`:theme` value opens `[` or `{` and never closes it.",
144
+ fix: "Balance the literal (no blank lines inside it), or quote it as text.",
145
+ example: STATE_EXAMPLE
146
+ },
147
+ {
148
+ code: "WD005",
149
+ title: "Loop variable used outside a loop",
150
+ cause: "`{ $index }` (or another `$` row variable) appears outside a `@loop` body.",
151
+ fix: "Move the interpolation into a loop body, or rename your value."
152
+ },
153
+ {
154
+ code: "WD006",
155
+ title: "Unknown format pipe",
156
+ cause: "`{ value | name }` names a formatter that is not in the fixed whitelist.",
157
+ fix: "Use one of the catalogued pipes; the message lists every available name."
158
+ },
159
+ {
160
+ code: "WD007",
161
+ title: "Stray `:::` close",
162
+ cause: "A bare `:::` line appears with no container open.",
163
+ fix: "Open the container first, or delete the stray closer.",
164
+ example: CONTAINER_EXAMPLE
165
+ },
166
+ {
167
+ code: "WD008",
168
+ title: "Retired `@repeat` directive",
169
+ cause: "`@repeat` was replaced by the single `@loop` directive.",
170
+ fix: "Rewrite the block as `@loop … into … @endloop`.",
171
+ example: LOOP_EXAMPLE
172
+ },
173
+ {
174
+ code: "WD009",
175
+ title: "Retired `:for` directive",
176
+ cause: "`:for` was replaced by the single `@loop` directive.",
177
+ fix: "Rewrite the block as `@loop … into … @endloop`.",
178
+ example: LOOP_EXAMPLE
179
+ },
180
+ {
181
+ code: "WD010",
182
+ title: "Stray block closer",
183
+ cause: "An `@endloop`/`:endif`/`:endform`/`:else` line has no matching opener.",
184
+ fix: "Open the matching block, or delete the stray closer."
185
+ },
186
+ {
187
+ code: "WD011",
188
+ title: "Missing block closer",
189
+ cause: "A `@loop`/`:form`/`:carousel` block runs to the end of the file unclosed.",
190
+ fix: "Close the block with its own closing token."
191
+ },
192
+ {
193
+ code: "WD012",
194
+ title: "Missing closing `:::`",
195
+ cause: "A `::: container` block is never closed.",
196
+ fix: "Add a bare `:::` line to close the container.",
197
+ example: CONTAINER_EXAMPLE
198
+ },
199
+ {
200
+ code: "WD013",
201
+ title: "`:else if` after `:else`",
202
+ cause: "A conditional continues with `:else if` after a bare `:else` already closed it.",
203
+ fix: "Order every `:else if` before the bare `:else`."
204
+ },
205
+ {
206
+ code: "WD014",
207
+ title: "Duplicate `:else`",
208
+ cause: "A conditional has more than one bare `:else` branch.",
209
+ fix: "Keep one `:else`; turn the others into `:else if` branches."
210
+ },
211
+ {
212
+ code: "WD015",
213
+ title: "Missing `:endif`",
214
+ cause: "An `:if` region is never closed.",
215
+ fix: "Close the region with `:endif`.",
216
+ example: IF_EXAMPLE
217
+ },
218
+
219
+ // --- WD1xx: loops and collections ----------------------------------------
220
+ {
221
+ code: "WD101",
222
+ title: "Malformed `@loop` header",
223
+ cause: "The header does not read `@loop <source> into <item>`.",
224
+ fix: "Name a source and an item variable, then any clauses in the fixed order.",
225
+ example: LOOP_EXAMPLE
226
+ },
227
+ {
228
+ code: "WD102",
229
+ title: "Malformed `@loop` clause",
230
+ cause: "A clause is misspelled or written out of the fixed order.",
231
+ fix: "Order clauses `where`, `sort by`, `reverse`, `offset`, `limit`, `paginate`, `sortable`.",
232
+ example: LOOP_EXAMPLE
233
+ },
234
+ {
235
+ code: "WD103",
236
+ title: "Unknown `sort by { state }` key",
237
+ cause: "A reactive sort key references a `:state`/`:store` that is not declared.",
238
+ fix: "Declare the state first, or sort by a literal item field.",
239
+ example: LOOP_EXAMPLE
240
+ },
241
+ {
242
+ code: "WD104",
243
+ title: "Sort key is not a loop-item field",
244
+ cause: "`sort by` names something other than the loop item.",
245
+ fix: "Sort by `<item>.field`, using the item name from the `into` clause.",
246
+ example: LOOP_EXAMPLE
247
+ },
248
+ {
249
+ code: "WD105",
250
+ title: "Disallowed sort key path",
251
+ cause: "The sort key contains `constructor`, `prototype`, or `__proto__`.",
252
+ fix: "Sort by an ordinary data field."
253
+ },
254
+ {
255
+ code: "WD106",
256
+ title: "Unknown sort direction state",
257
+ cause: "A reactive `{ direction }` references a `:state`/`:store` that is not declared.",
258
+ fix: "Declare the state first, or use the literal `asc`/`desc`.",
259
+ example: LOOP_EXAMPLE
260
+ },
261
+ {
262
+ code: "WD107",
263
+ title: "Invalid `offset`/`limit` argument",
264
+ cause: "The argument is neither a non-negative integer nor a declared `:state`.",
265
+ fix: "Pass an integer literal or a declared state key.",
266
+ example: LOOP_EXAMPLE
267
+ },
268
+ {
269
+ code: "WD108",
270
+ title: "`sortable` combined with other clauses",
271
+ cause: "`sortable` reorders the underlying list, so a derived view would desynchronise.",
272
+ fix: "Drop `where`/`sort`/`reverse`/`offset`/`limit`/`paginate` from a sortable loop."
273
+ },
274
+ {
275
+ code: "WD109",
276
+ title: "`paginate` combined with `offset`/`limit`",
277
+ cause: "`paginate` already slices each page, so an explicit slice conflicts with it.",
278
+ fix: "Keep `paginate N` and remove `offset`/`limit`."
279
+ },
280
+ {
281
+ code: "WD110",
282
+ title: "`paginate` on a non-collection source",
283
+ cause: "Pagination multiplies static routes, which only makes sense for a collection.",
284
+ fix: "Paginate a collection (a `site/pages/<name>/` subdirectory) by its bare name."
285
+ },
286
+ {
287
+ code: "WD111",
288
+ title: "Loop data file is not a JSON array",
289
+ cause: "The `.json` source parses to an object or a scalar.",
290
+ fix: "Make the file a top-level JSON array of rows."
291
+ },
292
+ {
293
+ code: "WD112",
294
+ title: "In-scope loop source is not a list",
295
+ cause: "The name resolved to a scalar or object value in scope.",
296
+ fix: "Point the loop at a list, or omit the field to render the `@empty` branch."
297
+ },
298
+ {
299
+ code: "WD113",
300
+ title: "Disallowed `@loop` source path",
301
+ cause: "The source path contains `constructor`, `prototype`, or `__proto__`.",
302
+ fix: "Loop an ordinary collection, JSON file, scope value, or state list."
303
+ },
304
+ {
305
+ code: "WD114",
306
+ title: "`sortable` on a nested loop",
307
+ cause: "An item-relative loop has no top-level state list to reorder.",
308
+ fix: "Make the sortable loop read a top-level `:state`/`:store` list."
309
+ },
310
+ {
311
+ code: "WD115",
312
+ title: "Unresolved `@loop` source",
313
+ cause: "The source is not a collection, JSON file, in-scope value, or declared state.",
314
+ fix: "Check the name; the message lists the available collections.",
315
+ example: LOOP_EXAMPLE
316
+ },
317
+ {
318
+ code: "WD116",
319
+ title: "Reactive loop nested too deep",
320
+ cause: "A third reactive `@loop` level would paint empty at runtime.",
321
+ fix: "Unroll the outer data at build time, or move the innermost list into build data."
322
+ },
323
+ {
324
+ code: "WD117",
325
+ title: "`sortable` needs a state list",
326
+ cause: "`sortable` was used on a JSON file or in-scope value, which cannot be reordered.",
327
+ fix: "Reorder a `:state`/`:store` list instead."
328
+ },
329
+ {
330
+ code: "WD120",
331
+ title: "Malformed `_schema.wd` line",
332
+ cause: "A schema line does not read `field: type`.",
333
+ fix: "Write one `field: type` rule per line inside the `---` block."
334
+ },
335
+ {
336
+ code: "WD121",
337
+ title: "Unknown `_schema.wd` type",
338
+ cause: "The type is outside the closed vocabulary.",
339
+ fix: "Use `string`, `number`, `boolean`, `date`, or `string[]`, with `?` for optional."
340
+ },
341
+ {
342
+ code: "WD122",
343
+ title: "Undeclared frontmatter field",
344
+ cause: "A collection entry has a field the schema does not declare (often a typo).",
345
+ fix: "Add the field to `_schema.wd`, or remove it from the entry."
346
+ },
347
+ {
348
+ code: "WD123",
349
+ title: "Missing required frontmatter field",
350
+ cause: "A collection entry omits a field the schema requires.",
351
+ fix: "Add the field to the entry, or mark it optional with `?` in the schema."
352
+ },
353
+ {
354
+ code: "WD124",
355
+ title: "Frontmatter field has the wrong type",
356
+ cause: "An entry's value does not match its schema type.",
357
+ fix: "Fix the entry's value, or widen the type in `_schema.wd`."
358
+ },
359
+
360
+ // --- WD2xx: state and expressions ----------------------------------------
361
+ {
362
+ code: "WD201",
363
+ title: "Malformed `:state`",
364
+ cause: "The line does not read `:state name = value [persist]`.",
365
+ fix: "Give the state a name and an initial value.",
366
+ example: STATE_EXAMPLE
367
+ },
368
+ {
369
+ code: "WD202",
370
+ title: "`:state` inside a reactive loop",
371
+ cause: "State cannot be declared per row; a loop body has no place to hold it.",
372
+ fix: "Declare the state outside the loop."
373
+ },
374
+ {
375
+ code: "WD203",
376
+ title: "`:state` collides with a `:store`",
377
+ cause: "A page-global store already owns that name.",
378
+ fix: "Rename one of them, or use the store everywhere.",
379
+ example: STORE_EXAMPLE
380
+ },
381
+ {
382
+ code: "WD204",
383
+ title: "`:state` declared twice",
384
+ cause: "The same name is declared twice in one section scope.",
385
+ fix: "Remove the duplicate, or scope one declaration to its own `::: section`."
386
+ },
387
+ {
388
+ code: "WD205",
389
+ title: "Malformed `:store`",
390
+ cause: "The line does not read `:store name = value [ephemeral]`.",
391
+ fix: "Give the store a name and an initial value.",
392
+ example: STORE_EXAMPLE
393
+ },
394
+ {
395
+ code: "WD206",
396
+ title: "`:store` declared twice",
397
+ cause: "Stores are page-global, so a second declaration is ambiguous.",
398
+ fix: "Declare the store once and reference it everywhere else.",
399
+ example: STORE_EXAMPLE
400
+ },
401
+ {
402
+ code: "WD207",
403
+ title: "`:store` collides with a `:state`",
404
+ cause: "A `:state` of the same name is already declared on the page.",
405
+ fix: "Rename one of them, or use the store everywhere.",
406
+ example: STORE_EXAMPLE
407
+ },
408
+ {
409
+ code: "WD208",
410
+ title: "Malformed `:computed`",
411
+ cause: "The line does not read `:computed name = <expression>`.",
412
+ fix: "Give the computed value a name and a right-hand expression.",
413
+ example: COMPUTED_EXAMPLE
414
+ },
415
+ {
416
+ code: "WD209",
417
+ title: "Malformed `:computed` expression",
418
+ cause: "The right-hand side passes the character whitelist but is not a real expression.",
419
+ fix: "Write a complete expression over declared state, numbers, and operators.",
420
+ example: COMPUTED_EXAMPLE
421
+ },
422
+ {
423
+ code: "WD210",
424
+ title: "Malformed `:theme`",
425
+ cause: 'The line does not read `:theme [name] [= "auto"]`.',
426
+ fix: "Use a bare `:theme`, or name the store and seed it.",
427
+ example: THEME_EXAMPLE
428
+ },
429
+ {
430
+ code: "WD220",
431
+ title: "Malformed `where` condition",
432
+ cause: "A `where` condition is not `operand <op> operand`.",
433
+ fix: "Compare a loop-item field with a value using a whitelisted operator."
434
+ },
435
+ {
436
+ code: "WD221",
437
+ title: "Unsupported `where` operand",
438
+ cause: "An operand is not an item field, a state name, a number, or a quoted string.",
439
+ fix: "Use one of those four operand forms."
440
+ },
441
+ {
442
+ code: "WD222",
443
+ title: "Disallowed `where` path",
444
+ cause: "An operand path contains `constructor`, `prototype`, or `__proto__`.",
445
+ fix: "Compare an ordinary data field."
446
+ },
447
+ {
448
+ code: "WD223",
449
+ title: "Unknown name in `where`",
450
+ cause: "An operand names neither the loop item nor a declared `:state`.",
451
+ fix: "Use the loop item's field, or declare the state first."
452
+ },
453
+ {
454
+ code: "WD224",
455
+ title: "Unsupported `when`/`:if` operand",
456
+ cause: "An operand is not an item field, a state name, a number, or a quoted string.",
457
+ fix: "Use one of those four operand forms.",
458
+ example: IF_EXAMPLE
459
+ },
460
+ {
461
+ code: "WD225",
462
+ title: "Disallowed `when`/`:if` path",
463
+ cause: "An operand path contains `constructor`, `prototype`, or `__proto__`.",
464
+ fix: "Compare an ordinary data field."
465
+ },
466
+ {
467
+ code: "WD226",
468
+ title: "Unknown name in `when`/`:if`",
469
+ cause: "An operand names nothing in scope: no loop item, no scope value, no state.",
470
+ fix: "Declare the state first, or reference a value that is in scope.",
471
+ example: IF_EXAMPLE
472
+ },
473
+ {
474
+ code: "WD227",
475
+ title: "Disallowed aggregate path",
476
+ cause: "A `sum`/`avg`/`min`/`max`/`count` argument contains a prototype-poisoning segment.",
477
+ fix: "Aggregate an ordinary state list."
478
+ },
479
+ {
480
+ code: "WD228",
481
+ title: "Unknown state in a `:computed` aggregate",
482
+ cause: "The aggregated list is not a declared `:state`/`:store`/`:fetch` key.",
483
+ fix: "Declare the list first.",
484
+ example: COMPUTED_EXAMPLE
485
+ },
486
+ {
487
+ code: "WD229",
488
+ title: "Unsupported string syntax in `:computed`",
489
+ cause: "The expression contains a quote, backslash, or backtick outside a simple literal.",
490
+ fix: "Use plain double- or single-quoted string literals."
491
+ },
492
+ {
493
+ code: "WD230",
494
+ title: "Unsupported syntax in `:computed`",
495
+ cause: "The expression contains characters outside the closed whitelist.",
496
+ fix: "Use state names, numbers, strings, arithmetic, comparisons, and `&& || !`.",
497
+ example: COMPUTED_EXAMPLE
498
+ },
499
+ {
500
+ code: "WD231",
501
+ title: "Assignment in `:computed`",
502
+ cause: "A `:computed` expression derives a value; it may not assign one.",
503
+ fix: "Mutate state from a `:button`/`:effect` action instead."
504
+ },
505
+ {
506
+ code: "WD232",
507
+ title: "Function call in `:computed`",
508
+ cause: "Only the fixed `sum`/`avg`/`min`/`max`/`count` aggregates may look like calls.",
509
+ fix: "Remove the call, or use one of the aggregates."
510
+ },
511
+ {
512
+ code: "WD233",
513
+ title: "Disallowed `:computed` path segment",
514
+ cause: "A referenced path contains `constructor`, `prototype`, or `__proto__`.",
515
+ fix: "Read an ordinary state path."
516
+ },
517
+ {
518
+ code: "WD234",
519
+ title: "Unknown state in `:computed`",
520
+ cause: "The expression references a name that is not declared state.",
521
+ fix: "Declare it with `:state`, `:store`, or `:fetch` first.",
522
+ example: COMPUTED_EXAMPLE
523
+ },
524
+
525
+ // --- WD3xx: actions ------------------------------------------------------
526
+ {
527
+ code: "WD301",
528
+ title: "Malformed `:button`",
529
+ cause: 'The line does not read `:button "Label" -> action`.',
530
+ fix: "Quote the label and give at least one action after the arrow.",
531
+ example: BUTTON_EXAMPLE
532
+ },
533
+ {
534
+ code: "WD302",
535
+ title: "Malformed `:effect`",
536
+ cause: "The line does not read `:effect watchedState -> action`.",
537
+ fix: "Name one state to watch and the actions to run when it changes.",
538
+ example: EFFECT_EXAMPLE
539
+ },
540
+ {
541
+ code: "WD303",
542
+ title: "`:effect` watches unknown state",
543
+ cause: "The watched name is not a declared `:state`/`:store`.",
544
+ fix: "Declare the state first.",
545
+ example: EFFECT_EXAMPLE
546
+ },
547
+ {
548
+ code: "WD304",
549
+ title: "Malformed `:every`",
550
+ cause: "The line does not read `:every <duration> -> action`.",
551
+ fix: "Give a duration and the actions to run on each tick.",
552
+ example: EVERY_EXAMPLE
553
+ },
554
+ {
555
+ code: "WD305",
556
+ title: "Invalid `:every` duration",
557
+ cause: "The duration is not a positive `<int>[ms|s|m]` value.",
558
+ fix: "Use a duration like `500ms`, `5s`, or `2m`.",
559
+ example: EVERY_EXAMPLE
560
+ },
561
+ {
562
+ code: "WD306",
563
+ title: "Action targets unknown state",
564
+ cause: "The action mutates a name that is not declared state.",
565
+ fix: "Declare the state first.",
566
+ example: BUTTON_EXAMPLE
567
+ },
568
+ {
569
+ code: "WD307",
570
+ title: "Row append needs a list state",
571
+ cause: "`list += item` carries the current row into a target that is not a list.",
572
+ fix: "Declare the target as a list, e.g. `:state cart = []`."
573
+ },
574
+ {
575
+ code: "WD308",
576
+ title: "`+=` needs a number or a list",
577
+ cause: "`+=` with a non-number value requires a list target to append to.",
578
+ fix: "Add a number, or declare the target as a list."
579
+ },
580
+ {
581
+ code: "WD309",
582
+ title: "Row remove inside a nested loop",
583
+ cause: "An item-relative loop has no top-level list for the runtime to remove from.",
584
+ fix: "Carry the row into a top-level list first, then remove it there."
585
+ },
586
+ {
587
+ code: "WD310",
588
+ title: "Row remove targets the wrong list",
589
+ cause: "Per-row `remove` must target the same list the loop is iterating.",
590
+ fix: "Name the looped list, or remove a value instead of the current row."
591
+ },
592
+ {
593
+ code: "WD311",
594
+ title: "Unsupported action",
595
+ cause: "The expression is outside the fixed action vocabulary (no JavaScript is allowed).",
596
+ fix: "Use a catalogued op; the message lists the whole vocabulary.",
597
+ example: ACTION_EXAMPLE
598
+ },
599
+ {
600
+ code: "WD312",
601
+ title: "`merge` operand is unknown state",
602
+ cause: "The right-hand name of a `merge` is not declared state.",
603
+ fix: "Declare it first, or pass an inline object literal."
604
+ },
605
+ {
606
+ code: "WD313",
607
+ title: "Unsupported `merge` operand",
608
+ cause: "The operand is neither a state key nor an inline object literal.",
609
+ fix: "Merge a declared object state, or an inline `{…}` literal."
610
+ },
611
+ {
612
+ code: "WD314",
613
+ title: "Unsupported action literal",
614
+ cause: "The action value is not a quoted string, number, boolean, null, or valid JSON.",
615
+ fix: "Quote strings and use valid JSON for arrays and objects."
616
+ },
617
+
618
+ // --- WD4xx: forms and fields ---------------------------------------------
619
+ {
620
+ code: "WD401",
621
+ title: "Malformed `:form`",
622
+ cause: 'The opener has neither `into <name>` nor `action="…"`, or has leftover text.',
623
+ fix: "Capture into state, post to a URL, or do both for a fetch round-trip.",
624
+ example: FORM_EXAMPLE
625
+ },
626
+ {
627
+ code: "WD402",
628
+ title: "Malformed `:input`",
629
+ cause: "The line does not start with a field name.",
630
+ fix: "Name the field, then add optional attributes and flags.",
631
+ example: INPUT_EXAMPLE
632
+ },
633
+ {
634
+ code: "WD403",
635
+ title: "Unknown `:input` flag",
636
+ cause: "The bare flag is not one of `required`/`autofocus`/`disabled`/`readonly`.",
637
+ fix: "Use a supported flag, or write it as `name=value`.",
638
+ example: INPUT_EXAMPLE
639
+ },
640
+ {
641
+ code: "WD404",
642
+ title: "Unknown `:input` attribute",
643
+ cause: "The attribute is outside the supported set.",
644
+ fix: "Use a supported attribute; the compiler emits no arbitrary HTML attributes.",
645
+ example: INPUT_EXAMPLE
646
+ },
647
+ {
648
+ code: "WD405",
649
+ title: "Malformed `:bind`",
650
+ cause: "The line does not start with the bound state name.",
651
+ fix: "Name a declared `:state`, then add optional attributes.",
652
+ example: BIND_EXAMPLE
653
+ },
654
+ {
655
+ code: "WD406",
656
+ title: "`:bind` has no matching state",
657
+ cause: "The bound name is not a declared `:state`/`:store`.",
658
+ fix: "Declare the state before binding to it.",
659
+ example: BIND_EXAMPLE
660
+ },
661
+ {
662
+ code: "WD407",
663
+ title: "Unknown `:bind` flag",
664
+ cause: "The bare flag is not `required` or `autofocus`.",
665
+ fix: "Use a supported flag.",
666
+ example: BIND_EXAMPLE
667
+ },
668
+ {
669
+ code: "WD408",
670
+ title: "Unknown `:bind` attribute",
671
+ cause: "The attribute is outside the supported set.",
672
+ fix: "Use `placeholder`, `type`, `autocomplete`, or an `aria-*` attribute.",
673
+ example: BIND_EXAMPLE
674
+ },
675
+ {
676
+ code: "WD409",
677
+ title: "Malformed `:slider`",
678
+ cause: "The line does not start with the slider's state name.",
679
+ fix: "Name the state, then optionally seed it and set the range.",
680
+ example: SLIDER_EXAMPLE
681
+ },
682
+ {
683
+ code: "WD410",
684
+ title: "Malformed `:slider` initial value",
685
+ cause: "The `=` is present but no value follows it.",
686
+ fix: "Give the slider a numeric initial value.",
687
+ example: SLIDER_EXAMPLE
688
+ },
689
+ {
690
+ code: "WD411",
691
+ title: "Unknown `:slider` attribute",
692
+ cause: "The attribute is not `min`, `max`, `step`, or `aria-label`.",
693
+ fix: "Use a supported attribute.",
694
+ example: SLIDER_EXAMPLE
695
+ },
696
+ {
697
+ code: "WD412",
698
+ title: "Non-numeric `:slider` bound",
699
+ cause: "`min`, `max`, or `step` is not a number.",
700
+ fix: "Give every bound a numeric value.",
701
+ example: SLIDER_EXAMPLE
702
+ },
703
+ {
704
+ code: "WD413",
705
+ title: "Non-numeric `:slider` initial value",
706
+ cause: "A slider is bound to a number, so its seed must be numeric.",
707
+ fix: "Seed the slider with a number.",
708
+ example: SLIDER_EXAMPLE
709
+ },
710
+ {
711
+ code: "WD414",
712
+ title: "`:slider persist` without an inline value",
713
+ cause: "`persist` belongs to the declaration, and this slider binds existing state.",
714
+ fix: "Declare the value inline to persist it, or persist it on the `:state` line.",
715
+ example: SLIDER_EXAMPLE
716
+ },
717
+ {
718
+ code: "WD415",
719
+ title: "`:slider` has no matching state",
720
+ cause: "The slider binds a name that is not declared state.",
721
+ fix: "Declare the state, or seed it inline on the slider.",
722
+ example: SLIDER_EXAMPLE
723
+ },
724
+ {
725
+ code: "WD416",
726
+ title: "Malformed `:submit`",
727
+ cause: 'The line does not read `:submit "Label"`.',
728
+ fix: "Quote the button label.",
729
+ example: SUBMIT_EXAMPLE
730
+ },
731
+ {
732
+ code: "WD417",
733
+ title: "Malformed `:textarea`",
734
+ cause: "The line does not start with a field name.",
735
+ fix: "Name the field, then add optional attributes and flags.",
736
+ example: TEXTAREA_EXAMPLE
737
+ },
738
+ {
739
+ code: "WD418",
740
+ title: "Unknown `:textarea` flag",
741
+ cause: "The bare flag is not one of `required`/`autofocus`/`disabled`/`readonly`.",
742
+ fix: "Use a supported flag.",
743
+ example: TEXTAREA_EXAMPLE
744
+ },
745
+ {
746
+ code: "WD419",
747
+ title: "Unknown `:textarea` attribute",
748
+ cause: "The attribute is outside the supported set.",
749
+ fix: "Use a supported attribute such as `placeholder`, `rows`, or `maxlength`.",
750
+ example: TEXTAREA_EXAMPLE
751
+ },
752
+ {
753
+ code: "WD420",
754
+ title: "Malformed `:select`",
755
+ cause: "The line does not start with a field name.",
756
+ fix: "Name the field, then list the options as `- Label` lines.",
757
+ example: SELECT_EXAMPLE
758
+ },
759
+ {
760
+ code: "WD421",
761
+ title: "Unknown `:select` flag",
762
+ cause: "The bare flag is not `required`, `disabled`, or `autofocus`.",
763
+ fix: "Use a supported flag.",
764
+ example: SELECT_EXAMPLE
765
+ },
766
+ {
767
+ code: "WD422",
768
+ title: "Unknown `:select` attribute",
769
+ cause: "The attribute is not `autocomplete` or an `aria-*` attribute.",
770
+ fix: "Use a supported attribute.",
771
+ example: SELECT_EXAMPLE
772
+ },
773
+ {
774
+ code: "WD423",
775
+ title: "`:select` has no options",
776
+ cause: "No `- Label` lines follow the opener.",
777
+ fix: "Add one `- Label` line per option directly beneath it.",
778
+ example: SELECT_EXAMPLE
779
+ },
780
+ {
781
+ code: "WD424",
782
+ title: "Malformed `:checkbox`/`:radio`",
783
+ cause: "The line does not start with a group name.",
784
+ fix: "Name the group, then list the options as `- Label` lines.",
785
+ example: CHECKBOX_EXAMPLE
786
+ },
787
+ {
788
+ code: "WD425",
789
+ title: "Unknown `:checkbox`/`:radio` flag",
790
+ cause: "The bare flag is not `required`, `disabled`, or `autofocus`.",
791
+ fix: "Use a supported flag.",
792
+ example: CHECKBOX_EXAMPLE
793
+ },
794
+ {
795
+ code: "WD426",
796
+ title: "Unknown `:checkbox`/`:radio` attribute",
797
+ cause: "The attribute is not `aria-label` or `aria-describedby`.",
798
+ fix: "Use a supported attribute.",
799
+ example: CHECKBOX_EXAMPLE
800
+ },
801
+ {
802
+ code: "WD427",
803
+ title: "`:checkbox`/`:radio` has no options",
804
+ cause: "No `- Label` lines follow the opener.",
805
+ fix: "Add one `- Label` line per option directly beneath it.",
806
+ example: RADIO_EXAMPLE
807
+ },
808
+
809
+ // --- WD5xx: data fetching and URL safety ---------------------------------
810
+ {
811
+ code: "WD501",
812
+ title: "Malformed `:fetch`",
813
+ cause: 'The line does not read `:fetch name from "url" [options]`.',
814
+ fix: "Name the state key and quote the URL, then add keyword options.",
815
+ example: FETCH_EXAMPLE
816
+ },
817
+ {
818
+ code: "WD502",
819
+ title: "Unknown `:fetch` option",
820
+ cause: "An option is not a `name=value` pair from the supported set.",
821
+ fix: "Use `method`, `when`, `timeout`, `retry`, `headers`, `body`, or `refresh`.",
822
+ example: FETCH_EXAMPLE
823
+ },
824
+ {
825
+ code: "WD503",
826
+ title: "Unsupported `:fetch` method",
827
+ cause: "The method is outside `GET`/`POST`/`PUT`/`PATCH`/`DELETE`.",
828
+ fix: "Use one of the supported HTTP methods.",
829
+ example: FETCH_EXAMPLE
830
+ },
831
+ {
832
+ code: "WD504",
833
+ title: "Unsupported `:fetch when`",
834
+ cause: "`when` is neither `load` nor `visible`.",
835
+ fix: "Fetch on `load` (the default) or when the marker becomes `visible`.",
836
+ example: FETCH_EXAMPLE
837
+ },
838
+ {
839
+ code: "WD505",
840
+ title: "Non-integer `:fetch timeout`/`retry`",
841
+ cause: "The value is not a non-negative integer.",
842
+ fix: "Pass whole numbers (milliseconds for `timeout`, attempts for `retry`).",
843
+ example: FETCH_EXAMPLE
844
+ },
845
+ {
846
+ code: "WD506",
847
+ title: "`:fetch refresh=` without `headers=`",
848
+ cause: "A token refresh has nowhere to write the renewed token back to.",
849
+ fix: "Add `headers=<stateKey>` alongside `refresh=`.",
850
+ example: FETCH_EXAMPLE
851
+ },
852
+ {
853
+ code: "WD507",
854
+ title: "Unsafe URL",
855
+ cause: "The URL is empty, padded with whitespace, or contains control characters.",
856
+ fix: "Use a clean relative path, an http(s) URL, or a `{ state }` interpolation."
857
+ },
858
+ {
859
+ code: "WD508",
860
+ title: "Protocol-relative URL",
861
+ cause: "A `//host` URL inherits whatever scheme the page was served over.",
862
+ fix: "Write `http://` or `https://` explicitly."
863
+ },
864
+ {
865
+ code: "WD509",
866
+ title: "Disallowed URL scheme",
867
+ cause: "The scheme is not `http` or `https` (`javascript:`, `data:`, `file:`, and friends).",
868
+ fix: "Use http(s) or a relative path."
869
+ },
870
+
871
+ // --- WD6xx: includes and page structure ----------------------------------
872
+ {
873
+ code: "WD601",
874
+ title: "Include outside the source sandbox",
875
+ cause: "The include path escapes `site/pages` and `site/_`.",
876
+ fix: "Move the partial inside the sandbox and reference it from there.",
877
+ example: INCLUDE_EXAMPLE
878
+ },
879
+ {
880
+ code: "WD602",
881
+ title: "Include not found",
882
+ cause: "No file matched the spec in `site/pages` or `site/_`.",
883
+ fix: "Check the path, the extension, and the leading slash.",
884
+ example: INCLUDE_EXAMPLE
885
+ },
886
+ {
887
+ code: "WD603",
888
+ title: "Malformed `@include`",
889
+ cause: 'The line does not read `@include /partial.wd [with key="value"]`.',
890
+ fix: "Give one target path, then optional `with` arguments.",
891
+ example: INCLUDE_EXAMPLE
892
+ },
893
+ {
894
+ code: "WD604",
895
+ title: "Include argument is not in scope",
896
+ cause: "A `with key={ value }` argument references a name that does not resolve.",
897
+ fix: "Pass a literal, or reference a value that is in scope at the include site.",
898
+ example: INCLUDE_EXAMPLE
899
+ },
900
+ {
901
+ code: "WD605",
902
+ title: "Unexpected token in a container header",
903
+ cause: "The header holds something that is not a name, `.class`, or `#id`.",
904
+ fix: "Use a leading name, then `.class`/`#id` tokens.",
905
+ example: CONTAINER_EXAMPLE
906
+ },
907
+ {
908
+ code: "WD606",
909
+ title: "Malformed `:carousel`",
910
+ cause: "The opener carries something other than `autoplay=<ms>`.",
911
+ fix: "Open it bare, or with a numeric `autoplay`.",
912
+ example: CAROUSEL_EXAMPLE
913
+ },
914
+ {
915
+ code: "WD607",
916
+ title: "Loop variable in `:if` outside a loop",
917
+ cause: "`:if $first` (or another row variable) appears outside a `@loop` body.",
918
+ fix: "Move the conditional into the loop body."
919
+ },
920
+ {
921
+ code: "WD608",
922
+ title: "`:if` name is not declared",
923
+ cause: "The condition names neither a `:state` nor an in-scope value.",
924
+ fix: "Declare the state first, or reference a value that is in scope.",
925
+ example: IF_EXAMPLE
926
+ },
927
+ {
928
+ code: "WD609",
929
+ title: "Malformed `:if`",
930
+ cause: "The `:if` line carries no condition.",
931
+ fix: "Give a name, or a comparison joined with `and`/`or`/`not`.",
932
+ example: IF_EXAMPLE
933
+ },
934
+ {
935
+ code: "WD610",
936
+ title: "Unsafe `:try` href",
937
+ cause: "The href has control characters, or a scheme outside http/https/mailto.",
938
+ fix: "Use a relative URL, or an `http:`, `https:`, or `mailto:` URL."
939
+ },
940
+ {
941
+ code: "WD611",
942
+ title: "Protocol-relative `:try` href",
943
+ cause: "A `//host` href inherits whatever scheme the page was served over.",
944
+ fix: "Write `http:` or `https:` explicitly."
945
+ },
946
+ {
947
+ code: "WD612",
948
+ title: "Include cycle",
949
+ cause:
950
+ "An `@include` chain reaches a file that is already being compiled, so the compile would never terminate.",
951
+ fix: "Break the loop: remove the `@include` that points back, or move the shared content into a third file both sides include."
952
+ },
953
+
954
+ // --- WD7xx: media and embeds ---------------------------------------------
955
+ {
956
+ code: "WD701",
957
+ title: "Malformed `:video`/`:audio`",
958
+ cause: "The line carries no clip source.",
959
+ fix: "Give the clip path, then optional attributes and flags.",
960
+ example: VIDEO_EXAMPLE
961
+ },
962
+ {
963
+ code: "WD702",
964
+ title: "Unknown `:video`/`:audio` flag",
965
+ cause: "The bare flag is outside the supported set for that element.",
966
+ fix: "Use `controls`, `autoplay`, `loop`, `muted`, or (video only) `playsinline`.",
967
+ example: VIDEO_EXAMPLE
968
+ },
969
+ {
970
+ code: "WD703",
971
+ title: "Unknown `:video`/`:audio` attribute",
972
+ cause: "The attribute is outside the supported set for that element.",
973
+ fix: "Use `poster`, `width`, `height`, or `preload` (audio takes `preload` only).",
974
+ example: VIDEO_EXAMPLE
975
+ },
976
+ {
977
+ code: "WD704",
978
+ title: "Malformed `:embed`",
979
+ cause: "The line carries no URL.",
980
+ fix: "Give the share or watch URL, then an optional title.",
981
+ example: EMBED_EXAMPLE
982
+ },
983
+ {
984
+ code: "WD705",
985
+ title: "Unknown `:embed` attribute",
986
+ cause: "`title` is the only supported attribute.",
987
+ fix: "Remove the attribute, or set the accessible title.",
988
+ example: EMBED_EXAMPLE
989
+ },
990
+
991
+ // --- WD8xx: skins ---------------------------------------------------------
992
+ {
993
+ code: "WD801",
994
+ title: "`scoped` is not the first line",
995
+ cause: "The scoping opt-in appears somewhere other than the first meaningful line.",
996
+ fix: "Move `scoped` to the top of the `.skin`, or delete it."
997
+ },
998
+ {
999
+ code: "WD802",
1000
+ title: "Page-level declaration in a scoped skin",
1001
+ cause: "A declaration with no enclosing selector would write to the whole page.",
1002
+ fix: "Nest it under a selector, or move it to a global skin."
1003
+ },
1004
+ {
1005
+ code: "WD803",
1006
+ title: "Page-level selector in a scoped skin",
1007
+ cause: "A top-level `page`/`body`/`html`/`*`/`::selection` selector escapes the scope.",
1008
+ fix: "Move page-level styles to a global (non-scoped) skin."
1009
+ },
1010
+
1011
+ // --- WD9xx: project, routing, CLI ----------------------------------------
1012
+ {
1013
+ code: "WD901",
1014
+ title: "Duplicate route",
1015
+ cause: "Two source files map to the same public route (often `.md` plus `.wd`).",
1016
+ fix: "Rename or delete one of the files."
1017
+ },
1018
+ {
1019
+ code: "WD902",
1020
+ title: "Route escapes the build output",
1021
+ cause: "A route resolved outside `dist`, which would write outside the build.",
1022
+ fix: "Remove traversal segments from the route."
1023
+ },
1024
+ {
1025
+ code: "WD903",
1026
+ title: "Unknown init template",
1027
+ cause: "`darkmown init --template` names a template that does not ship.",
1028
+ fix: "Pick a bundled template; the message lists every available name."
1029
+ },
1030
+ {
1031
+ code: "WD904",
1032
+ title: "Unknown deploy target",
1033
+ cause: "`darkmown deploy` names a platform Darkmown does not drive.",
1034
+ fix: "Deploy to `vercel` or `cloudflare`."
1035
+ },
1036
+ {
1037
+ code: "WD905",
1038
+ title: "Deploy CLI not signed in",
1039
+ cause: "The platform CLI rejected the deploy with an authentication failure.",
1040
+ fix: "Run the platform login command, then re-run the deploy."
1041
+ },
1042
+ {
1043
+ code: "WD906",
1044
+ title: "Deploy command failed",
1045
+ cause: "The platform CLI exited non-zero for a reason other than authentication.",
1046
+ fix: "Read the CLI output above the error, fix the cause, and re-run."
1047
+ }
1048
+ ];
1049
+
1050
+ /**
1051
+ * The subsystem an error belongs to, derived from its code so the `area` can
1052
+ * never disagree with the number.
1053
+ * @param {string} code
1054
+ * @returns {string}
1055
+ */
1056
+ function areaOf(code) {
1057
+ const block = Number(code[2]);
1058
+ const area = ERROR_AREAS.find((a) => a.block === block);
1059
+ return area ? area.name : "unknown";
1060
+ }
1061
+
1062
+ /**
1063
+ * The full error-code registry, in code order. Each entry carries the stable
1064
+ * code, its subsystem, a short title, what causes it, and how to fix it.
1065
+ * @returns {ErrorEntry[]}
1066
+ */
1067
+ export function errorCatalog() {
1068
+ return ENTRIES.map((entry) => ({ ...entry, area: areaOf(entry.code) }));
1069
+ }
1070
+
1071
+ /**
1072
+ * Look up one code, or undefined when it is not registered.
1073
+ * @param {string} code
1074
+ * @returns {ErrorEntry | undefined}
1075
+ */
1076
+ export function errorForCode(code) {
1077
+ return errorCatalog().find((entry) => entry.code === code);
1078
+ }
1079
+
1080
+ /**
1081
+ * Render `docs/errors.md` from the registry. Generated, never hand-edited, so
1082
+ * the reference page cannot drift from the codes the compiler actually throws
1083
+ * (`scripts/gen-errors.mjs` writes it; tests/error-codes.test.js re-checks it).
1084
+ * @returns {string}
1085
+ */
1086
+ export function errorsMarkdown() {
1087
+ const entries = errorCatalog();
1088
+ const out = [];
1089
+ out.push("# Darkmown compile error codes");
1090
+ out.push("");
1091
+ out.push(
1092
+ "Every Darkmown compile error carries a stable `WDxxx` code. It is the first",
1093
+ "thing in the message (`[WD201] Malformed :state in …`) and it is mirrored on",
1094
+ "the thrown error as `err.wd.code`, alongside `file`, `line`, `hint`, and a",
1095
+ "concrete compilable `example`."
1096
+ );
1097
+ out.push("");
1098
+ out.push("```");
1099
+ out.push("[WD201] Malformed :state in site/pages/index.wd:1: :state x.");
1100
+ out.push(" Use: :state name = value [persist] — e.g. :state count = 0");
1101
+ out.push("```");
1102
+ out.push("");
1103
+ out.push(
1104
+ "Codes are grouped by subsystem, and a shipped code is permanent: it is never",
1105
+ "renumbered and a retired number is never reused. Add new errors at the next",
1106
+ "free number in their block."
1107
+ );
1108
+ out.push("");
1109
+ out.push("| Range | Subsystem |");
1110
+ out.push("| --- | --- |");
1111
+ for (const area of ERROR_AREAS) out.push(`| \`${area.range}\` | ${area.title} |`);
1112
+ out.push("");
1113
+ out.push(
1114
+ "> This page is generated from `src/errors.js` by `node scripts/gen-errors.mjs`.",
1115
+ "> Edit the registry, not this file."
1116
+ );
1117
+ out.push("");
1118
+
1119
+ for (const area of ERROR_AREAS) {
1120
+ const rows = entries.filter((entry) => entry.area === area.name);
1121
+ if (!rows.length) continue;
1122
+ out.push(`## ${area.range} — ${area.title}`);
1123
+ out.push("");
1124
+ out.push("| Code | Error | Cause | Fix | Example |");
1125
+ out.push("| --- | --- | --- | --- | --- |");
1126
+ for (const row of rows) {
1127
+ const example = row.example ? `\`${escapeCell(row.example)}\`` : "";
1128
+ out.push(
1129
+ `| \`${row.code}\` | ${escapeCell(row.title)} | ${escapeCell(row.cause)} | ${escapeCell(row.fix)} | ${example} |`
1130
+ );
1131
+ }
1132
+ out.push("");
1133
+ }
1134
+
1135
+ out.push("## Errors without a code");
1136
+ out.push("");
1137
+ out.push(
1138
+ "A handful of throws are framework invariants rather than authoring mistakes:",
1139
+ "the expression re-parser (`src/compiler/expr-ast.js`) consumes the compiler's",
1140
+ "own already-validated output, and the source reader (`src/compiler/reader.js`)",
1141
+ "guards the host contract behind `compileFromMemory`. Hitting one of those is a",
1142
+ "bug in Darkmown, not something a page can fix, so they stay uncoded and should",
1143
+ "be reported as issues."
1144
+ );
1145
+ out.push("");
1146
+ return out.join("\n");
1147
+ }
1148
+
1149
+ /**
1150
+ * Escape a value for a markdown table cell (pipes would split the row).
1151
+ * @param {string} value
1152
+ * @returns {string}
1153
+ */
1154
+ function escapeCell(value) {
1155
+ return value.replaceAll("|", "\\|");
1156
+ }