@zvndev/darkmown 2.1.0 → 2.3.1-next.1.g3a4a8dc

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 (111) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +65 -0
  3. package/docs/cli.md +8 -0
  4. package/docs/spec-alignment.md +18 -0
  5. package/grammar/wd-directives.gbnf +58 -0
  6. package/package.json +17 -10
  7. package/site/pages/docs/index.wd +29 -0
  8. package/site/pages/playground/index.js +47 -46
  9. package/src/builder.js +5 -0
  10. package/src/catalog.js +601 -0
  11. package/src/cli.js +8 -0
  12. package/src/compiler/actions.js +87 -34
  13. package/src/compiler/body.js +36 -14
  14. package/src/compiler/collections.js +16 -10
  15. package/src/compiler/context.js +70 -1
  16. package/src/compiler/fetch.js +51 -19
  17. package/src/compiler/format.js +5 -2
  18. package/src/compiler/forms.js +169 -50
  19. package/src/compiler/frontmatter.js +11 -2
  20. package/src/compiler/fs-reader.js +2 -1
  21. package/src/compiler/image-size.js +340 -0
  22. package/src/compiler/includes.js +8 -4
  23. package/src/compiler/interpolation.js +15 -7
  24. package/src/compiler/loops.js +78 -39
  25. package/src/compiler/markdown.js +4 -3
  26. package/src/compiler/media.js +38 -10
  27. package/src/compiler/page.js +8 -6
  28. package/src/compiler/predicates.js +74 -30
  29. package/src/compiler/reader.js +12 -2
  30. package/src/compiler/state.js +83 -18
  31. package/src/compiler/structure.js +57 -24
  32. package/src/deploy.js +8 -4
  33. package/src/errors.js +1156 -0
  34. package/src/grammar.js +131 -0
  35. package/src/index.js +1 -0
  36. package/src/router.js +7 -4
  37. package/src/scaffold.js +4 -2
  38. package/src/skin.js +17 -6
  39. package/types/api-runner.d.ts +94 -0
  40. package/types/builder.d.ts +141 -10
  41. package/types/catalog.d.ts +97 -0
  42. package/types/compiler/actions.d.ts +41 -0
  43. package/types/compiler/body.d.ts +11 -0
  44. package/types/compiler/collections.d.ts +76 -0
  45. package/types/compiler/context.d.ts +647 -0
  46. package/types/compiler/directives.d.ts +6 -0
  47. package/types/compiler/expr-ast.d.ts +20 -0
  48. package/types/compiler/fetch.d.ts +29 -0
  49. package/types/compiler/format.d.ts +49 -0
  50. package/types/compiler/forms.d.ts +94 -0
  51. package/types/compiler/frontmatter.d.ts +33 -0
  52. package/types/compiler/fs-reader.d.ts +7 -0
  53. package/types/compiler/image-size.d.ts +14 -0
  54. package/types/compiler/includes.d.ts +100 -0
  55. package/types/compiler/index.d.ts +5 -0
  56. package/types/compiler/interpolation.d.ts +115 -0
  57. package/types/compiler/loops.d.ts +36 -0
  58. package/types/compiler/markdown.d.ts +34 -0
  59. package/types/compiler/media.d.ts +32 -0
  60. package/types/compiler/page.d.ts +109 -0
  61. package/types/compiler/predicates.d.ts +50 -0
  62. package/types/compiler/reader.d.ts +48 -0
  63. package/types/compiler/state.d.ts +68 -0
  64. package/types/compiler/structure.d.ts +56 -0
  65. package/types/compiler.d.ts +35 -223
  66. package/types/dev.d.ts +47 -2
  67. package/types/errors.d.ts +100 -0
  68. package/types/feeds.d.ts +150 -0
  69. package/types/headers.d.ts +44 -0
  70. package/types/highlight.d.ts +24 -0
  71. package/types/router.d.ts +42 -3
  72. package/types/scaffold.d.ts +15 -3
  73. package/types/skin.d.ts +28 -1
  74. package/types/src/api-runner.d.ts +94 -0
  75. package/types/src/builder.d.ts +170 -0
  76. package/types/src/catalog.d.ts +97 -0
  77. package/types/src/compiler/actions.d.ts +41 -0
  78. package/types/src/compiler/body.d.ts +11 -0
  79. package/types/src/compiler/collections.d.ts +76 -0
  80. package/types/src/compiler/context.d.ts +647 -0
  81. package/types/src/compiler/directives.d.ts +6 -0
  82. package/types/src/compiler/expr-ast.d.ts +20 -0
  83. package/types/src/compiler/fetch.d.ts +29 -0
  84. package/types/src/compiler/format.d.ts +49 -0
  85. package/types/src/compiler/forms.d.ts +94 -0
  86. package/types/src/compiler/frontmatter.d.ts +33 -0
  87. package/types/src/compiler/fs-reader.d.ts +7 -0
  88. package/types/src/compiler/image-size.d.ts +14 -0
  89. package/types/src/compiler/includes.d.ts +100 -0
  90. package/types/src/compiler/index.d.ts +5 -0
  91. package/types/src/compiler/interpolation.d.ts +115 -0
  92. package/types/src/compiler/loops.d.ts +36 -0
  93. package/types/src/compiler/markdown.d.ts +34 -0
  94. package/types/src/compiler/media.d.ts +32 -0
  95. package/types/src/compiler/page.d.ts +109 -0
  96. package/types/src/compiler/predicates.d.ts +50 -0
  97. package/types/src/compiler/reader.d.ts +48 -0
  98. package/types/src/compiler/state.d.ts +68 -0
  99. package/types/src/compiler/structure.d.ts +56 -0
  100. package/types/src/compiler.d.ts +48 -0
  101. package/types/src/config.d.ts +45 -0
  102. package/types/src/dev.d.ts +58 -0
  103. package/types/src/errors.d.ts +100 -0
  104. package/types/src/feeds.d.ts +150 -0
  105. package/types/src/headers.d.ts +44 -0
  106. package/types/src/highlight.d.ts +24 -0
  107. package/types/src/router.d.ts +78 -0
  108. package/types/src/scaffold.d.ts +20 -0
  109. package/types/src/skin.d.ts +48 -0
  110. package/types/src/statics.d.ts +24 -0
  111. package/types/statics.d.ts +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 2.3.0 — 2026-08-09
6
+
7
+ A trust release. Every compile error now carries a stable, searchable code; the dependency tree carries zero known advisories; and CI proves the framework on three operating systems and three browser engines instead of one of each. The new coverage found real bugs, including one that had silently broken every Windows user since the project started. No runtime change (7654 B gzipped, unchanged) and no breaking changes.
8
+
9
+ - **Stable `WDxxx` compile-error codes.** All 138 author-facing compile errors now carry a permanent identifier — `[WD201] Malformed :state in site/pages/index.wd:1: …` — mirrored on the thrown error as `err.wd.code` alongside the existing `file`, `line`, `hint`, and `example`. Codes are grouped by subsystem (`WD1xx` loops, `WD4xx` forms, `WD5xx` fetch, and so on), and a shipped code is a public contract: never renumbered, never reused. The registry (`src/errors.js`) is the single source of truth, [`docs/errors.md`](docs/errors.md) is generated from it, and the catalog exposes it as data. Drift guards run in both directions — an uncoded author-facing throw, a duplicate code, or an undocumented one all fail the build. Genuine internal invariants (the AST parser, the reader contract) deliberately stay plain `Error`s, and that split is enforced by test.
10
+ - **Zero known advisories.** `npm audit --audit-level=high` now exits 0. `image-size` carried a high-severity advisory affecting *every* published version including the latest — no upgrade existed — so its one call site (reading `<img>` dimensions at build time) is now served by an inlined header reader for PNG, JPEG, GIF, WebP, and SVG. Its segment walkers are explicitly bounded, which is the exact failure class the advisory described. `linkify-it` and `esbuild` advisories cleared by upgrade. Runtime code dependencies drop from three to two.
11
+ - **BUGFIX: CRLF source files no longer lose their frontmatter.** `parseFrontmatter` tested for an LF-shaped `---\n` opener, so a file authored on Windows — or checked out anywhere with git's `core.autocrlf`, the default *on* Windows — silently had its entire frontmatter block reclassified as body text. No error was raised: the page just lost its title, layout, `draft` flag, and collection schema fields. Source text is now newline-normalized at the reader boundary and in `parseFrontmatter` itself. If you have ever edited a Darkmown page on Windows, this is the release that fixes it.
12
+ - **CI runs on Windows, macOS, and Linux, across Chromium, Firefox, and WebKit.** Previously ubuntu-only and Chromium-only. The browser matrix is sharded so three engines cost no more wall-clock than one, and browsers are cached by resolved Playwright version. The whole 19-job matrix completes in about two minutes. The Windows job found the CRLF bug on its first run.
13
+ - **TypeScript types actually resolve now.** The `exports` map had no `types` conditions, so consumers on `moduleResolution: node16` / `nodenext` / `bundler` — the default for new TypeScript projects — resolved *no types at all*, and the `./catalog` subpath shipped no declarations. Both entrypoints now carry `types` conditions, `types/catalog.d.ts` is authored, and `@types/markdown-it` moved to `dependencies` (types-only, no runtime code) so the root entrypoint stops erroring. Fixing this exposed that `npm run types` had been failing under TypeScript 6 and emitting nothing, leaving the committed declarations stale; generation is repaired and the output regenerated.
14
+ - **A canary channel.** Merges to `master` publish a prerelease to the `next` dist-tag, so a fix can be exercised before it becomes stable: `npm i @zvndev/darkmown@next`. The tag-triggered stable path is untouched, and the canary job asserts after publishing that it did not become `latest`.
15
+ - **Property tests for the skin compiler and the router,** in the seeded, reproducible style of the existing fuzz suite. They immediately found a real bug: `.skin` token modifiers quoted the attribute *value* but never validated the attribute *name*, so `tokens [{=v]` emitted `:root[{="v"]` — a stray brace that unbalances the stylesheet and makes a CSS parser swallow every rule after it. Fixed, with the case kept as a permanent regression guard.
16
+ - **Internal:** the VS Code extension's `.vsix` is now built and uploaded by CI on every push, so it can be installed from a run without a marketplace listing. `.gitattributes` pins the working tree to LF. The test suite grew from 892 to 989 tests, holding 100% `src` line coverage.
17
+
18
+ ## 2.2.0 — 2026-07-12
19
+
20
+ AI authoring becomes a first-class framework surface: a machine-readable directive catalog, a generated prompt cheatsheet, a shipped GBNF grammar for constrained decoding, and compile errors that teach by example — all generated from the compiler's own whitelist tables, so none of it can drift from what actually compiles. Zero runtime impact (7654 B, unchanged).
21
+
22
+ - **`darkmown catalog` — the language, as data.** `directiveCatalog()` (also `import '@zvndev/darkmown/catalog'`) returns every directive, loop clause, action op, format pipe, and predicate operator with syntax, description, a concrete example, and whether it makes a page reactive — 25 directives, 13 action ops, 20 pipes, sourced from the compiler's real tables with drift-guard tests in both directions. `darkmown catalog --llms` renders the same data as a ~92-line cheatsheet, and every build emits it as `dist/llms.txt` — the exact artifact to hand an LLM (or stuff into a small on-device model's system prompt).
23
+ - **A shipped GBNF grammar (`grammar/wd-directives.gbnf`).** Generated from the same tables: directive-line shape, clause order, and the closed op/pipe/action vocabularies as literal alternations, with free-form values left permissive. Point llama.cpp's `grammar` field at it and a local model becomes structurally unable to emit JS/HTML muscle-memory syntax inside `.wd` directives. Kept in sync by test; regenerate with `scripts/gen-grammar.mjs`.
24
+ - **Compile errors now teach by example.** All 24 corrective `Use:` hints with bracket-placeholders gained a trailing concrete line — `… Use: @loop src into item [where …] — e.g. @loop /products.json into p where p.price < 50` — and a test extracts every example from the real thrown error strings and compiles it, so the examples can never rot. Empirically motivated: small local models copy error-message examples verbatim, brackets and all.
25
+ - **Structured error metadata.** Compile errors carry `err.wd = { file, line, hint, example }` alongside the unchanged message string — no more parsing error text in tooling (the playground, editors, agent harnesses).
26
+
5
27
  ## 2.1.0 — 2026-07-11
6
28
 
7
29
  Two headlines. Reactive pages now run under the **same strict, eval-free CSP as static pages** — the runtime interprets validated expressions instead of building functions, so `'unsafe-eval'` is gone from every page the framework ships. And darkmown.com gained a **browser playground**: the real compiler, bundled, compiling `.wd` live on every keystroke — built on a new fs-free `compileFromMemory` API.
package/README.md CHANGED
@@ -39,6 +39,10 @@ npx darkmown dev
39
39
 
40
40
  The package is `@zvndev/darkmown`; the command it installs is plain `darkmown`.
41
41
 
42
+ Every merge to `master` also publishes a prerelease to the `next` tag, so you can try a
43
+ fix before it ships stable: `npm install -D @zvndev/darkmown@next`. Use `@latest` (the
44
+ default) for anything you care about.
45
+
42
46
  Prefer to see it run before you install? The [browser playground](https://darkmown.com/playground/)
43
47
  compiles `.wd`/`.md` live in your browser — the same compiler the CLI ships — with
44
48
  no install and no build step.
@@ -59,6 +63,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
59
63
  - `darkmown build [--target cloudflare] [--drafts]` writes static output to `dist` (plus `sitemap.xml`/`rss.xml`/`robots.txt`); `--drafts` includes `draft: true` pages for staging.
60
64
  - `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
61
65
  - `darkmown serve` previews the built `dist` locally.
66
+ - `darkmown catalog [--llms]` prints the machine-readable directive catalog (JSON), or with `--llms` a compact cheatsheet — see [AI authoring](#ai-authoring).
62
67
  - `darkmown version` prints the installed package version.
63
68
  - `darkmown help` prints CLI usage.
64
69
 
@@ -984,6 +989,66 @@ const { html, assets } = compileFromMemory(
984
989
 
985
990
  Includes, colocated `.skin`/`.js` detection, and `@loop` JSON-data reads all resolve against the map (anything not in it is simply "absent"), and it throws the **same `file:line` compile errors** as the CLI — the error DX is identical. This is exactly the entry point the [browser playground](https://darkmown.com/playground/) is built on: markdown-it and the compiler bundled to one asset, compiling `.wd`/`.md` on every keystroke and rendering the result into an iframe. On disk, `compilePage(file, paths)` is the same compile with a filesystem reader injected for you.
986
991
 
992
+ ### Structured compile errors — `err.wd`
993
+
994
+ Every compile error is still a plain `Error` with the same `file:line` message, but it now also carries a machine-readable mirror on `err.wd` — so an AI edit loop (or an editor) gets the facts without re-parsing the prose:
995
+
996
+ ```js
997
+ import { compileFromMemory } from "@zvndev/darkmown";
998
+
999
+ try {
1000
+ compileFromMemory({ "site/pages/index.wd": ":state x" }, "site/pages/index.wd");
1001
+ } catch (err) {
1002
+ err.message; // "[WD201] Malformed :state in …/index.wd:1: :state x. Use: :state name = value [persist] — e.g. :state count = 0"
1003
+ err.wd; // { code: "WD201", file: "…/index.wd", line: 1, hint: ":state name = value [persist] — e.g. …", example: ":state count = 0" }
1004
+ }
1005
+ ```
1006
+
1007
+ `example` is always a concrete, **compilable** line (never a `[placeholder]`), and every corrective `Use:` hint that contains bracket-placeholders ends with a matching `— e.g. <valid line>`. This is a deliberate affordance for small local models, which otherwise copy `[optional]`-style placeholders into source verbatim.
1008
+
1009
+ ## Compile error codes
1010
+
1011
+ Every author-facing compile error opens with a stable `WDxxx` code, so it is searchable, linkable, and matchable by tooling without parsing prose:
1012
+
1013
+ ```
1014
+ [WD201] Malformed :state in site/pages/index.wd:1: :state x.
1015
+ Use: :state name = value [persist] — e.g. :state count = 0
1016
+ ```
1017
+
1018
+ The full list lives in [`docs/errors.md`](docs/errors.md) — code, cause, fix, and a compilable example each. Codes are grouped by subsystem:
1019
+
1020
+ | Range | Subsystem |
1021
+ | --- | --- |
1022
+ | `WD0xx` | Source, frontmatter & block structure |
1023
+ | `WD1xx` | Loops & collections |
1024
+ | `WD2xx` | State & expressions |
1025
+ | `WD3xx` | Button, effect & timer actions |
1026
+ | `WD4xx` | Forms & form fields |
1027
+ | `WD5xx` | Data fetching & URL safety |
1028
+ | `WD6xx` | Includes & page structure |
1029
+ | `WD7xx` | Media & embeds |
1030
+ | `WD8xx` | Skins & styling |
1031
+ | `WD9xx` | Project, routing & CLI |
1032
+
1033
+ **A shipped code is a public contract:** it is never renumbered and a retired number is never reused, so an error found in an old log or issue always resolves to a meaningful entry. `docs/errors.md` is generated from the compiler's own registry, and drift guards fail the build if an author-facing error is added without a code, if two errors share one, or if a code goes undocumented.
1034
+
1035
+ Genuine *internal* invariants — the expression-AST parser reading the compiler's own validated output, the reader contract — deliberately stay plain `Error`s and carry no code, because they signal a framework bug rather than something you can fix in a page.
1036
+
1037
+ ## AI authoring
1038
+
1039
+ Darkmown ships its own description of the `.wd` language — the same tables the compiler validates against — so a tool can teach a model to write `.wd` and constrain its output to what compiles.
1040
+
1041
+ - **Directive catalog** — `darkmown catalog` prints structured JSON: every directive, `@loop` clause, loop variable, button action, format pipe, and predicate operator, each with a syntax template, a one-line description, one concrete example, and whether it needs the reactive runtime. Importable too: `import { directiveCatalog } from "@zvndev/darkmown/catalog"`.
1042
+ - **Cheatsheet (`llms.txt`)** — `darkmown catalog --llms` prints a compact (~90-line) markdown cheatsheet generated from the same data — the artifact you stuff into a model's system prompt. Every build also writes it to `dist/llms.txt`.
1043
+ - **GBNF grammar** — [`grammar/wd-directives.gbnf`](grammar/wd-directives.gbnf) is a generated [GBNF](https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md) grammar for `.wd` directive lines. Point llama.cpp (or any grammar-constrained decoder) at it to make invalid directive lines — JS idioms like `cart.filter(...)`, HTML muscle-memory like `::: card class="…"` — literally unrepresentable during generation. Regenerate with `node scripts/gen-grammar.mjs`.
1044
+
1045
+ ```sh
1046
+ darkmown catalog --llms > system-prompt.md # paste into a model's context
1047
+ darkmown build # also emits dist/llms.txt
1048
+ ```
1049
+
1050
+ Because the catalog, cheatsheet, grammar, and error `example`s are all generated from the compiler's own tables, they cannot drift from what actually compiles (enforced by tests).
1051
+
987
1052
  ## Editor support
988
1053
 
989
1054
  A VS Code extension in [`editors/vscode`](editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — so a `.wd` file reads as Markdown-plus-directives, never as broken Markdown.
package/docs/cli.md CHANGED
@@ -82,6 +82,14 @@ The deploy URL is printed when the CLI reports it. If the platform CLI isn't sig
82
82
 
83
83
  Serves the already-built `dist` directory for local preview (static only — use `darkmown dev` to exercise `api/*` locally). Run `darkmown build` first.
84
84
 
85
+ ### `darkmown catalog [--llms]`
86
+
87
+ Prints the machine-readable `.wd` directive catalog as JSON — every directive, `@loop` clause, loop variable, button action, format pipe, and predicate operator, each with a syntax template, description, and concrete example. With `--llms` it prints a compact (~90-line) markdown cheatsheet instead — the artifact to paste into an AI model's system prompt. Every `darkmown build` also writes the cheatsheet to `dist/llms.txt`, and Darkmown ships a generated GBNF grammar (`grammar/wd-directives.gbnf`) for constrained decoding.
88
+
89
+ ```sh
90
+ darkmown catalog --llms > system-prompt.md
91
+ ```
92
+
85
93
  ## Smoke checks
86
94
 
87
95
  From this repository, run:
@@ -2,6 +2,16 @@
2
2
 
3
3
  This audit compares the current Darkmown implementation with the original MARROW/WhateverDown vision and the follow-up decisions made on 2026-06-11.
4
4
 
5
+ ## 2.3.0 additions — error codes, supply chain, platform coverage (2026-08-09)
6
+
7
+ - **Every author-facing compile error carries a stable `WDxxx` code.** The original vision treated compile errors as a first-class DX surface ("errors include file path + corrective suggestion"); this closes the last gap by making them *identifiable*. 138 codes, grouped by subsystem, mirrored on `err.wd.code` beside the existing `file`/`line`/`hint`/`example`. `src/errors.js` is the single source of truth, `docs/errors.md` is generated from it, and `directiveCatalog()` exposes it as data for tooling. Drift guards run both directions: an author-facing throw without a code, a duplicate code, or an undocumented code all fail the build. Internal invariants (the AST parser reading the compiler's own validated output, the reader contract) stay plain `Error`s by design — they signal a framework bug, not an authoring mistake — and that split is itself enforced by test.
8
+ - **Zero known advisories, and one fewer runtime dependency.** `image-size` carried a high-severity advisory affecting every published version with no upgrade path, so its single build-time call site is now served by an inlined header reader (PNG/JPEG/GIF/WebP/SVG) with explicitly bounded parse loops. This keeps the project's "few runtime dependencies" posture honest rather than deferring to an unfixable upstream.
9
+ - **BUGFIX — CRLF sources lost their frontmatter.** `parseFrontmatter` tested an LF-shaped `---\n` opener, so any file authored on Windows or checked out with `core.autocrlf` had its whole frontmatter block silently reclassified as body text: no error, but the page lost its title, layout, `draft` flag, and collection schema fields. Newline normalization now happens at the reader boundary and in `parseFrontmatter`. This had been latent since the first commit; it surfaced within minutes of adding a Windows CI job.
10
+ - **Platform coverage is now part of the contract.** CI runs the suite on Linux, Windows, and macOS, and the browser e2e suite against Chromium, Firefox, and WebKit (sharded, so three engines cost roughly one engine's wall-clock). The reactive runtime had only ever been proven in one engine; the CLI's path handling had never executed on Windows at all.
11
+ - **Types resolve for modern TypeScript consumers.** The `exports` map lacked `types` conditions, so `moduleResolution: node16`/`nodenext`/`bundler` consumers got no types from either entrypoint. Fixed, `types/catalog.d.ts` authored, and the declaration build itself repaired (it had been failing silently under TypeScript 6, leaving the committed `.d.ts` stale).
12
+ - **Property tests extended to the skin compiler and the router,** in the existing seeded/reproducible style. They found a real defect on the first run: `.skin` token modifiers quoted the attribute value but never validated the attribute *name*, letting `tokens [{=v]` emit an unbalanced `:root[{="v"]` selector that a CSS parser would use to swallow every following rule.
13
+ - **Release channels:** merges to `master` publish a prerelease to the `next` dist-tag; the tag-triggered stable path is unchanged and the canary job verifies after publishing that it did not take `latest`.
14
+
5
15
  ## 2.1.0 additions — eval-free expression interpreter (2026-07-11)
6
16
 
7
17
  - **Reactive pages run under a strict CSP with no `'unsafe-eval'`.** This is the headline. Through 2.0.x, the runtime executed validated `:computed` / `@loop … where` / `.class when` / `:if` expressions by building a `new Function` — which forced `script-src 'unsafe-eval'` on every reactive page, the #1 objection from security-minded adopters. The grammar was already a **closed** whitelist (comparisons, `and`/`or`/`not`, arithmetic in `:computed`, the `sum`/`avg`/`min`/`max`/`count` aggregates, literals, and dotted paths with proto-pollution guards), so it is small enough to interpret directly. The compiler now serializes each already-validated expression into a compact **AST** (nested arrays, tags `L`/`S`/`I`/`C`/`A` + the fixed operators — see `src/compiler/expr-ast.js`) emitted into the `data-wd-*` attributes, and the runtime **walks** it with a tiny closed evaluator (`src/runtime.js`). No `new Function`, no `eval`, anywhere on the page. The evaluator applies the *actual* JavaScript operators, so semantics (loose `==`/`!=`, `NaN`/`null` handling, `&&`/`||` short-circuit, string coercion, division) are byte-identical to the old path — guarded by a parity battery (`tests/expr-ast.test.js`) that compares old-`new Function` vs new-AST results across a wide input matrix, plus the fuzz suite (`tests/fuzz.test.js`) which now proves accepted expressions carry only closed op tags (dangerous text can survive only as inert `["L", …]` data) and walk to a pure value.
@@ -204,6 +214,14 @@ The 1.3.0 cycle closes table-stakes gaps a new user expects out of the box. Trac
204
214
  - **`compileFromMemory(files, entryPath, options)`** — a new public API (exported from `src/compiler.js` and the package root) that compiles a page from an in-memory map of project-relative path → source, with **zero `node:fs`**. It is the minimal-clean vfs refactor of the compiler: a `Reader` abstraction (`src/compiler/reader.js`) is threaded through the `Compilation`/`Ctx` and the page-shell helpers, replacing every direct `node:fs` call in `src/compiler/{page,includes,loops,collections}.js`. The default **`fsReader()`** (`src/compiler/fs-reader.js`, the ONLY compiler module importing `node:fs`) is injected by the Node-facing barrel, so `compilePage`/`compileDocument`/`enhanceImages` keep their exact signatures and the on-disk build stays **byte-identical** (golden: the demo `dist/` is unchanged across the refactor). `memoryReader(files, cwd)` backs the fs-free path: includes, colocated `.skin`/`.js` detection, and `@loop` JSON-data reads all resolve against the map; missing files are simply "absent"; image measuring degrades to no-dimensions (no image bytes in memory). It throws the **same `file:line` compile errors** as the CLI (a compile↔memory parity test asserts identical HTML and error text). This is strategic surface — a planned mobile host needs exactly a filesystem-free compile.
205
215
  - **Browser playground (`/playground/`)** — a live editor + preview page on the demo site. `esbuild` (a new **devDependency only**) bundles `compileFromMemory` + markdown-it + a common-languages highlight.js subset into one self-contained ESM asset (`scripts/build-playground.mjs` → the committed colocated `site/pages/playground/index.js`, rebuilt with `npm run build:playground`); `node:path`/`node:crypto`/`image-size` are aliased to tiny browser shims (`scripts/playground/shims/`). The bundle (~128 KB gzipped) loads **only** on `/playground/` and does **not** count against the 8 KB runtime budget (`src/runtime.js` is untouched by the playground). The preview is an `iframe srcdoc` of the compiled HTML; reactive previews load `/__wd/runtime.js` by same-origin URL and inherit the page's CSP — which since 2.1 is the same strict, **eval-free** policy on every page (the runtime interprets expression ASTs; see the 2.1 entry above), so the preview runs under the shipped CSP with no special allowance (`frame-src` does not block `about:srcdoc`; no `'unsafe-inline'` anywhere). The playground page itself dogfoods a real reactive island (a `:state`/`:button` example picker bridged to the bundle via `window.wd.subscribe`), so it is `runtime: true`.
206
216
 
217
+ ## AI authoring — catalog, llms.txt, grammar, structured errors (2026-07-12)
218
+
219
+ - **Machine-readable directive catalog** — `src/catalog.js` (`directiveCatalog()`, exported from the package root and via the `@zvndev/darkmown/catalog` exports subpath) returns structured JSON describing the whole `.wd` surface: every directive, `@loop` clause, loop variable, button-action op, format pipe, and predicate operator, each with a syntax template, one-line description, one **concrete compilable example**, and a reactivity flag. It is sourced from the compiler's own tables — `FORMATTER_NAMES` (format.js), `PREDICATE_OPS` (predicates.js), `LOOP_META` (context.js), the action vocabulary (`ACTION_USE`, actions.js) — and from the shared `*_EXAMPLE` constants the error hints use, so it cannot drift from what compiles (drift-guarded in `tests/catalog.test.js`, which also compiles every example).
220
+ - **`llms.txt` cheatsheet** — `llmsText()` renders a compact (~90-line) markdown cheatsheet from the same catalog; `darkmown catalog` prints the JSON and `darkmown catalog --llms` the cheatsheet, and every build writes `dist/llms.txt`. This is the artifact an app pastes into a small model's system prompt.
221
+ - **Generated GBNF grammar** — `src/grammar.js` (`generateGrammar()`, wrapped by `scripts/gen-grammar.mjs`) emits `grammar/wd-directives.gbnf`, a llama.cpp grammar for `.wd` **directive lines** (plus the `{ path | pipe }` interpolation token). It constrains a line's *shape* — the leading keyword must be a real directive, `@loop` must be `@loop <src> into <item> …clauses`, a condition must be `operand <op> operand`, a `:button` must be `"…" -> <action-chain>` — which deletes the on-device failure class (JS/HTML muscle-memory openers become unrepresentable) while staying small; free-form values remain `text`. Well-formedness + catalog coverage are tested in `tests/grammar.test.js`, and the committed file is kept in sync by the same test.
222
+ - **Structured compile errors** — errors carry a machine-readable mirror on `err.wd = { file, line?, hint?, example? }` (helper `wdError` in context.js) alongside the unchanged string message. Applied across loops, includes, predicates, forms, state, actions, media, fetch, and structure. `example` is always a compilable line.
223
+ - **Concrete examples in error hints** — every corrective `Use:` hint that contained bracket-placeholders now ends with ` — e.g. <concrete valid line>`, drawn from the same `*_EXAMPLE` constants. `tests/error-examples.test.js` extracts each `e.g.` example from the *real* thrown error string and compiles it (enforcement, not convention). Motivation: the on-device model bench (`bench/on-device/RESULTS.md`) found small models echo bracket-placeholders into source verbatim.
224
+
207
225
  ## Demo-only directives (not part of the public surface)
208
226
 
209
227
  The compiler recognizes three directives — `:note`, `:try`, and `:sprint` — that exist **only to power the demo site** (darkmown.com). They are **not** part of the supported public directive set, are not documented for authors, and may change or be removed without notice. Do not build pages that depend on them. The supported public directives are the ones catalogued above: `:state`, `:store`, `:computed`, `:effect`, `:every`, `:theme`, `:button`, `:bind`, `:if`/`:else if`/`:else`/`:endif`, `:fetch` (with `headers`/`refresh`), `:form`/`:input`/`:submit`, `:slider`, `:carousel`, `:video`/`:audio`/`:embed`, `@loop`/`@empty`/`@endloop` (with `where`, `sort by` — including reactive `{ state }` keys —, `reverse`, `offset`, `limit`, `sortable`), `@include`, and `::: section` (with reactive `.class when`). Interpolation supports **format pipes** (`{ value | name:arg }`) and `:computed`/list-pipe **aggregates** (`sum`/`avg`/`min`/`max`/`count`). The runtime escape hatch is `window.wd` (`get`/`set`/`subscribe`/`state`/`render`).
@@ -0,0 +1,58 @@
1
+ # Darkmown .wd directive-line grammar — generated by src/grammar.js
2
+ # Constrains a directive line (or a { path | pipe } interpolation). Do not edit by hand.
3
+
4
+ root ::= directive | binding
5
+ directive ::= dir-include | dir-loop | dir-container | dir-if | dir-state | dir-store | dir-computed | dir-fetch | dir-effect | dir-every | dir-theme | dir-button | dir-form | dir-input | dir-textarea | dir-select | dir-checkbox | dir-radio | dir-submit | dir-bind | dir-slider | dir-video | dir-audio | dir-embed | dir-carousel
6
+
7
+ dir-loop ::= "@loop" sp operand sp "into" sp ident loop-clauses
8
+ dir-if ::= ":if" sp ( condition (sp joiner sp condition)* | path )
9
+ dir-button ::= ":button" sp dqstring sp "->" sp action-chain
10
+ dir-container ::= ":::" restofline
11
+
12
+ dir-include ::= "@include" restofline
13
+ dir-state ::= ":state" restofline
14
+ dir-store ::= ":store" restofline
15
+ dir-computed ::= ":computed" restofline
16
+ dir-fetch ::= ":fetch" restofline
17
+ dir-effect ::= ":effect" restofline
18
+ dir-every ::= ":every" restofline
19
+ dir-theme ::= ":theme" restofline
20
+ dir-form ::= ":form" restofline
21
+ dir-input ::= ":input" restofline
22
+ dir-textarea ::= ":textarea" restofline
23
+ dir-select ::= ":select" restofline
24
+ dir-checkbox ::= ":checkbox" restofline
25
+ dir-radio ::= ":radio" restofline
26
+ dir-submit ::= ":submit" restofline
27
+ dir-bind ::= ":bind" restofline
28
+ dir-slider ::= ":slider" restofline
29
+ dir-video ::= ":video" restofline
30
+ dir-audio ::= ":audio" restofline
31
+ dir-embed ::= ":embed" restofline
32
+ dir-carousel ::= ":carousel" restofline
33
+
34
+ loop-clauses ::= ( sp loop-clause )*
35
+ loop-clause ::= where-clause | "sort by" sp path ( sp ( "asc" | "desc" ) )? | "reverse" | "offset" sp number | "limit" sp number | "paginate" sp number | "sortable"
36
+ where-clause ::= "where" sp condition ( sp joiner sp condition )*
37
+ condition ::= operand sp op sp operand
38
+ op ::= "contains" | "==" | "!=" | ">=" | "<=" | ">" | "<"
39
+ joiner ::= "and" | "or"
40
+
41
+ action-chain ::= action ( wsopt ";" wsopt action )*
42
+ action ::= ident action-op
43
+ action-op ::= "++" | "--" | sp "+=" sp value | sp "-=" sp value | sp "=" sp value | sp "toggle" ( sp value )? | sp "append" sp value | sp "prepend" sp value | sp "remove" sp value | sp "clear" | sp "merge" sp ident | sp "delete" sp value | sp "reset" | sp "refetch"
44
+
45
+ binding ::= "{" wsopt path pipe-chain wsopt "}"
46
+ pipe-chain ::= ( wsopt "|" wsopt pipe ( ":" value )* )*
47
+ pipe ::= "money" | "number" | "percent" | "round" | "date" | "time" | "datetime" | "upper" | "lower" | "capitalize" | "truncate" | "trim" | "pluralize" | "default" | "sum" | "avg" | "min" | "max" | "count" | "join"
48
+
49
+ operand ::= path | dqstring | number
50
+ value ::= dqstring | number | path
51
+ ident ::= [a-zA-Z_$] [a-zA-Z0-9_$]*
52
+ path ::= [a-zA-Z0-9_$/.~-]+
53
+ number ::= [0-9]+
54
+ dqstring ::= "\"" [^"]* "\""
55
+ restofline ::= [^\n]*
56
+ sp ::= " "+
57
+ wsopt ::= " "*
58
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "2.1.0",
3
+ "version": "2.3.1-next.1.g3a4a8dc",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,7 +12,14 @@
12
12
  },
13
13
  "main": "./src/index.js",
14
14
  "exports": {
15
- ".": "./src/index.js",
15
+ ".": {
16
+ "types": "./types/index.d.ts",
17
+ "default": "./src/index.js"
18
+ },
19
+ "./catalog": {
20
+ "types": "./types/catalog.d.ts",
21
+ "default": "./src/catalog.js"
22
+ },
16
23
  "./package.json": "./package.json"
17
24
  },
18
25
  "types": "./types/index.d.ts",
@@ -45,6 +52,7 @@
45
52
  "files": [
46
53
  "src",
47
54
  "types",
55
+ "grammar",
48
56
  "site",
49
57
  "!site/_/og*.png",
50
58
  "docs/cli.md",
@@ -59,18 +67,17 @@
59
67
  "node": ">=20"
60
68
  },
61
69
  "dependencies": {
70
+ "@types/markdown-it": "^14.1.2",
62
71
  "highlight.js": "^11.11.1",
63
- "image-size": "^1.2.1",
64
- "markdown-it": "^14.2.0"
72
+ "markdown-it": "^14.3.0"
65
73
  },
66
74
  "devDependencies": {
67
- "@biomejs/biome": "^2.5.1",
68
- "@playwright/test": "^1.49.0",
69
- "@types/markdown-it": "^14.1.2",
70
- "@types/node": "^26.0.0",
71
- "esbuild": "^0.24.2",
75
+ "@biomejs/biome": "^2.5.7",
76
+ "@playwright/test": "^1.61.1",
77
+ "@types/node": "^26.1.1",
78
+ "esbuild": "^0.28.2",
72
79
  "husky": "^9.1.7",
73
- "lint-staged": "^16.4.0",
80
+ "lint-staged": "^17.0.0",
74
81
  "typescript": "^6.0.3"
75
82
  },
76
83
  "repository": {
@@ -47,6 +47,7 @@ Darkmown is a Markdown-native framework for mostly-static sites with tiny reacti
47
47
  - [Syntax highlighting](#syntax-highlighting)
48
48
  - [Trust boundary](#trust-boundary)
49
49
  - [Colocation](#colocation)
50
+ - [Compile error codes](#compile-error-codes)
50
51
  - [CLI reference](#cli-reference)
51
52
  - [Spec status](#spec-status)
52
53
  </nav>
@@ -604,6 +605,33 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) i
604
605
  - Both work for included fragments too, by basename.
605
606
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
606
607
 
608
+ ## AI authoring
609
+
610
+ Darkmown ships a machine-readable description of its own language, so an AI tool can learn `.wd` and be constrained to output that compiles. It is all generated from the compiler's own tables, so it never drifts from the real grammar.
611
+
612
+ - **`darkmown catalog`** prints structured JSON: every directive, `@loop` clause, loop variable, button action, format pipe, and predicate operator — each with a syntax template, a one-line description, one concrete example, and whether it needs the reactive runtime. Import it too: `import { directiveCatalog } from "@zvndev/darkmown/catalog"`.
613
+ - **`darkmown catalog --llms`** prints a compact (~90-line) cheatsheet — the artifact you paste into a model's system prompt. Every build also writes it to `dist/llms.txt`.
614
+ - **`grammar/wd-directives.gbnf`** is a generated GBNF grammar for `.wd` directive lines. Point a grammar-constrained decoder (llama.cpp and friends) at it to make invalid directive lines — JS idioms, HTML muscle-memory — impossible to generate.
615
+
616
+ ```sh
617
+ darkmown catalog --llms > system-prompt.md
618
+ ```
619
+
620
+ Compile errors help here too: every corrective `Use:` hint ends with a concrete `— e.g. <valid line>`, and each thrown error carries a structured `err.wd = { file, line, hint, example }` mirror alongside its message — so an edit-and-retry loop gets the fix without parsing prose.
621
+
622
+ ## Compile error codes
623
+
624
+ Every author-facing compile error opens with a stable `WDxxx` code, so it is searchable and matchable by tooling without parsing prose:
625
+
626
+ ```
627
+ [WD201] Malformed :state in site/pages/index.wd:1: :state x.
628
+ Use: :state name = value [persist] — e.g. :state count = 0
629
+ ```
630
+
631
+ The code is mirrored on the thrown error as `err.wd.code`, alongside `file`, `line`, `hint`, and a compilable `example`. Codes are grouped by subsystem — `WD0xx` source and frontmatter, `WD1xx` loops, `WD2xx` state and expressions, `WD3xx` actions, `WD4xx` forms, `WD5xx` fetching, `WD6xx` includes and structure, `WD7xx` media, `WD8xx` skins, `WD9xx` project and CLI.
632
+
633
+ A shipped code is a public contract: never renumbered, never reused. The full reference — cause, fix, and an example per code — is generated from the compiler's own registry, so it cannot drift from what actually throws.
634
+
607
635
  ## CLI reference
608
636
 
609
637
  The `darkmown` command (installed by `npx @zvndev/darkmown`) is the whole tool — every command is one word:
@@ -612,6 +640,7 @@ The `darkmown` command (installed by `npx @zvndev/darkmown`) is the whole tool
612
640
  - `darkmown dev` — the live compiler: browser reload, an in-browser error overlay, incremental rebuilds, and a local runner for `api/*` functions.
613
641
  - `darkmown build [--target cloudflare] [--drafts]` — write 100% static output to `dist/` (plus `sitemap.xml` / `rss.xml` / `robots.txt`); `--drafts` includes `draft: true` pages for a staging deploy.
614
642
  - `darkmown serve` — preview the built `dist/` locally (static only; use `dev` to exercise `api/*`).
643
+ - `darkmown catalog [--llms]` — print the `.wd` directive catalog as JSON, or with `--llms` a compact cheatsheet for priming an AI model (see [AI authoring](#ai-authoring)).
615
644
  - `darkmown deploy <vercel|cloudflare> [--prod]` — build (target-aware) and deploy through the platform CLI, printing your URL; an unauthenticated CLI surfaces the login to run first.
616
645
  - `darkmown version` / `darkmown help` — print the installed version, or the full command summary and directive syntax.
617
646