@zvndev/darkmown 2.6.1-next.12.ge562f5d → 2.7.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.
- package/AGENTS.md +392 -77
- package/CHANGELOG.md +47 -0
- package/README.md +316 -49
- package/docs/cli.md +51 -17
- package/docs/spec-alignment.md +25 -4
- package/grammar/wd-directives.gbnf +13 -5
- package/package.json +7 -3
- package/site/_/base.skin +1 -1
- package/site/_/footer.wd +1 -1
- package/site/pages/api-errors/index.wd +81 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/attr-binding/index.wd +84 -0
- package/site/pages/blog/darkmown-2-7-0.md +176 -0
- package/site/pages/blog/darkmown-2-7-0.skin +127 -0
- package/site/pages/blog/no-unsafe-eval.md +66 -0
- package/site/pages/blog/no-unsafe-eval.skin +127 -0
- package/site/pages/blog/zero-js-by-default.md +1 -1
- package/site/pages/branch-state/index.wd +68 -0
- package/site/pages/docs/index.wd +320 -19
- package/site/pages/index.wd +3 -1
- package/site/pages/playground/index.js +63 -56
- package/site/pages/pulse/index.wd +1 -1
- package/site/pages/settings/index.wd +97 -0
- package/site/pages/showcase/index.wd +37 -1
- package/site/pages/upload/index.wd +63 -0
- package/site/pages/url-state/index.wd +70 -0
- package/src/builder.js +222 -34
- package/src/catalog.js +217 -21
- package/src/cli.js +6 -3
- package/src/compiler/actions.js +80 -12
- package/src/compiler/body.js +104 -28
- package/src/compiler/collections.js +29 -13
- package/src/compiler/context.js +9 -2
- package/src/compiler/expr-ast.js +14 -0
- package/src/compiler/fetch.js +8 -4
- package/src/compiler/forms.js +133 -14
- package/src/compiler/frontmatter.js +28 -7
- package/src/compiler/includes.js +46 -6
- package/src/compiler/interpolation.js +56 -1
- package/src/compiler/loops.js +489 -35
- package/src/compiler/markdown.js +457 -44
- package/src/compiler/page.js +8 -3
- package/src/compiler/predicates.js +84 -14
- package/src/compiler/state.js +167 -18
- package/src/compiler/structure.js +159 -29
- package/src/errors.js +111 -3
- package/src/feeds.js +140 -7
- package/src/grammar.js +53 -10
- package/src/runtime.js +509 -130
- package/src/runtime.min.js +2 -0
- package/src/runtime.min.js.map +1 -0
- package/src/statics.js +46 -2
- package/src/templates/blog/README.md +2 -0
- package/src/templates/dashboard/README.md +2 -0
- package/src/templates/landing/README.md +2 -0
- package/src/templates/starter/README.md +1 -1
- package/src/templates/store/README.md +2 -0
- package/src/tools/validate.js +14 -4
- package/types/builder.d.ts +6 -2
- package/types/catalog.d.ts +12 -0
- package/types/compiler/actions.d.ts +1 -1
- package/types/compiler/body.d.ts +0 -3
- package/types/compiler/forms.d.ts +12 -1
- package/types/compiler/frontmatter.d.ts +8 -1
- package/types/compiler/includes.d.ts +16 -2
- package/types/compiler/interpolation.d.ts +23 -0
- package/types/compiler/loops.d.ts +47 -0
- package/types/compiler/markdown.d.ts +63 -1
- package/types/compiler/predicates.d.ts +7 -1
- package/types/compiler/state.d.ts +8 -2
- package/types/compiler/structure.d.ts +32 -1
- package/types/feeds.d.ts +68 -2
- package/types/statics.d.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
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.7.0 (2026-09-04)
|
|
6
|
+
|
|
7
|
+
A release about silent failure. An audit ran the compiler, the builder and the runtime against one question: when this goes wrong, does the author find out? Too often the answer was no. A `{ url }` inside a link destination stayed literal text with no diagnostic. A line that was exactly `:state` rendered as prose. A static `@loop` over table rows produced paragraphs beside a header-only table. A `:every` inside a reactive loop registered one timer per row, and a removed row kept ticking. Every one of those now either compiles correctly or is refused with a coded error that names the fix. Separately, the runtime started shipping minified, which returned about 2 KB of the 8 KB budget, and this release spent it on five things that used to be "Darkmown cannot do that": attribute binding, the URL as state, bound fields outside a form, real server error messages, and file upload. The shipped runtime is **6584 B gzipped** against the unchanged **8192 B** budget. Thirteen new error codes, several behavior changes, and no breaking API change.
|
|
8
|
+
|
|
9
|
+
- **A link or image destination is a live binding.** `[Open the docs](/docs/{ region }/)`, where `region` is `:state`, `:store`, `:computed`, a reactive loop row (`{ p.slug }`) or a loop meta variable (`{ $index }`), now updates in the browser. The compiler emits the destination as a small template (`data-wd-attr` for a page-level value, `data-wd-each-attr` for a per-row one) and the runtime rebuilds the whole value on every render, so a value in the middle of a path behaves exactly like one that fills the destination. The build-time paint is still the seed, so the link works before hydration and a crawler sees a real URL. Values in URL position are percent-encoded (a `)` in the data can no longer end the link and hand the remainder back to the parser), and the runtime refuses `javascript:`, `data:` and `vbscript:` at apply time, with control characters stripped first, because the compiler cannot see a value that only exists at runtime. Costs 185 B. Demo: [/attr-binding/](https://darkmown.com/attr-binding/).
|
|
10
|
+
- **`:state name = value from-url` puts a value in the query string.** A filter nobody can link to is half a feature. `from-url` is a third word in the persistence vocabulary, on `:state` only. The key mirrors into the query string (`cart:items` becomes the parameter `cart.items`) through `history.replaceState`, so filtering never fills the back button with one entry per keystroke; a value back at its declared seed drops its parameter entirely; `popstate` re-reads, and a missing parameter restores the seed. It composes with `persist`, and the boot precedence is URL, then storage, then seed, so a link somebody sent you beats what this browser remembers. `WD260` refuses `from-url` on `:store` and `:theme`, which are shared by every page and every tab while a query parameter belongs to one page's address. Demo: [/url-state/](https://darkmown.com/url-state/).
|
|
11
|
+
- **`:select`, `:radio` and `:checkbox` bind to state outside a `:form`.** The same three lines already meant "form field" inside a `:form`; outside one they used to emit an orphan control that nothing read. They now bind to a declared `:state` or `:store` the way `:bind` and `:slider` do: move the control and the state changes, change the state and the control moves. A bound `:checkbox` is a single boolean (one `- Label` line), while the multi-value checkbox group stays the in-form behavior. In-form output is byte-identical to 2.6.0, guarded by a golden test. Costs 45 B. Demo: [/settings/](https://darkmown.com/settings/).
|
|
12
|
+
- **A failed `:fetch` or `:form` says what the server said.** `<name>_error` used to be `Error: HTTP 422` no matter what came back. When the response body is JSON it is now the body's own `error` field, then its `message` field, falling back to `HTTP <status>`; and the whole parsed body lands in a new auto-declared `<name>_error_body`, so `{ signup_error_body.fields.email }` renders a field-level message with no JavaScript of your own. Both keys are cleared on the next success. Costs 63 B. Demo: [/api-errors/](https://darkmown.com/api-errors/).
|
|
13
|
+
- **File upload.** A `:form` containing a file field (`:input photo type=file`, or a raw `<input type="file">` on an `html: true` page) compiles with `enctype="multipart/form-data"`, and the runtime posts real `FormData` with no content type of its own, so the browser writes the boundary. Before this, the submit handler always built a urlencoded body, which carries a file's *name* and nothing else: the kind of failure that looks like it worked. Bound controls inside the form carry no `name`, so they are appended by state key rather than silently dropped. `WD452` refuses a file field on a `method="get"` form, because a GET request has no body. Costs 61 B. Demo: [/upload/](https://darkmown.com/upload/).
|
|
14
|
+
- **A `:state`, `:store` or `:theme` declared inside a closed `:if` branch now works.** A closed branch lives in a `<template>` and `querySelectorAll` does not descend into one, so a declaration in there was never hydrated: undefined for the life of the page, with `persist` ignored. Seeds are now claimed when a render changes structure, once per **key** rather than per node, so re-opening a branch does not reset the value the reader set. The claimed seed is also that key's `reset` baseline, and storage is read at claim time, so a `persist` value declared in a branch comes back the first time the branch opens. Demo: [/branch-state/](https://darkmown.com/branch-state/).
|
|
15
|
+
- **`:::` containers and `:button` accept accessibility attributes.** Exactly three names, always with a double-quoted static value: `role="…"`, any `aria-…="…"`, and `title="…"`. On a container they interleave with the `.class` / `#id` tokens and coexist with `.class when <predicate>`; on a `:button` they sit between the label and the `->`. Values are HTML-escaped on emit, so nothing an author writes can close the attribute or open another. Everything outside the whitelist (`onclick=`, `style=`, `href=`, `class=`, `id=`, `data-…=`) is `WD650`, and a value without double quotes is `WD651`. This is compile-time only: zero runtime bytes, and a static page carrying attributes stays `runtime: false`. There is deliberately no `{ state }` interpolation inside an aria value in this release.
|
|
16
|
+
- **A static `@loop` can fill markdown table rows.** Write the header in prose and the rows in the loop, or put the whole table inside the loop as bare rows: either way the result is ONE `<table>` with one `<tr>` per row. Before, markdown-it needed a header plus a `|---|` separator before it would read a line as a table row at all, so the rows rendered as `<p>| Ash | 3 | 12 |</p>` paragraphs under a header-only table. `@empty` over an empty list leaves the header table alone and renders its own branch. Known gap, stated plainly: a pipe row written in prose *after* `@endloop` (a totals row) does not join the table.
|
|
17
|
+
- **`rss_limit:` on the home page.** `rss_limit: 50` sets how many newest posts `rss.xml` carries; the default is still 20. Anything that is not a positive whole number of items is `WD950`, digits only, so `1e3`, `0x10` and `7.0` are refused rather than silently coerced.
|
|
18
|
+
- **BEHAVIOR CHANGE: the runtime ships minified.** `/__wd/runtime.js` used to be the comment-stripped readable source. It is now `src/runtime.min.js`, a committed esbuild artifact, served with an external sourcemap at `/__wd/runtime.js.map` that carries `sourcesContent`, so DevTools still shows the readable source with real identifiers. If you were reading the served file directly, read the map (or `src/runtime.js` in the package) instead. Dev serves the same bytes as production. The 8192 B budget is unchanged but is now measured on the artifact that actually ships: 6584 B gzipped, against 8886 B for the comment-stripped source carrying the same features. `npm run build:runtime` regenerates it, `predev` runs it before `npm run dev`, and CI fails on a stale artifact, so the bytes in the tarball are always the bytes the source produces.
|
|
19
|
+
- **BEHAVIOR CHANGE: interpolation resolves in link and image destinations and in raw HTML.** A page that previously showed the literal text `/docs/{ item.slug }/` in an `href`, or `{ x }` inside an html block, now shows the value. Every brace in a destination resolves, not only one at position zero, and raw HTML attributes and html blocks resolve too, HTML-escaped. Fenced code blocks are never rewritten, and neither are inline code spans, so a page can still document the syntax. Raw HTML attributes remain a paint rather than a binding: a reactive value there is painted once and warned, and the warning names the position that does bind. A raw-HTML `href`/`src`/`action`/`formaction`/`xlink:href` filled from data refuses dangerous schemes, emitting the attribute empty with a warning.
|
|
20
|
+
- **BEHAVIOR CHANGE: a heading with an interpolated value gets a real anchor.** `## { post.title }` used to slug before interpolation and produce `id="section"`, `section-1`, and so on. It now slugs the resolved text. Anchor links that pointed into an interpolated heading change with it.
|
|
21
|
+
- **BEHAVIOR CHANGE: a static `@loop` emits one list, not one per row.** Each row body used to compile as its own markdown document, so three list rows produced three `<ul>`s, three ordered rows were each numbered "1.", and a per-row table produced three tables. Adjacent list and table seams are now spliced: one `<ul>`, one `<ol>` numbered 1..n, one `<table>` when every row is a full table with the same header. Every other body shape is byte-identical. A page working around the old shape in CSS can drop that CSS.
|
|
22
|
+
- **BEHAVIOR CHANGE: a line that is only a directive keyword is an error.** A line that was exactly `:state` (or `:store`, `:input`, `:fetch`, `:button`, `@loop`, `:if`, and fifteen more) rendered as literal text with no warning at all, which is the worst outcome for an author and for an AI edit loop alike. It is now handed to that directive's own handler, so it throws that directive's coded malformed error with that directive's `Use:` hint: `:state` gives `WD201`, `:button` gives `WD301`, `@loop` gives `WD101`. `:::`, `:theme` and `:carousel` stay valid bare, because they mean something bare. **To show a directive name as text, escape it: `\:fetch`**, the ordinary CommonMark backslash escape. An indented keyword is still prose, and a keyword inside a fenced block is still code.
|
|
23
|
+
- **BEHAVIOR CHANGE: `:state b = a` where `a` is declared state is `WD251`.** It used to compile and seed the literal string `"a"`, tracking nothing. The error names the fix: `:computed b = a` to derive it, or quotes to keep the literal text. A plain word that is *not* declared state is unaffected, so `:state status = draft` and `:state title = Hello world` still work exactly as before. A page that relied on the literal word being stored needs quotes around it.
|
|
24
|
+
- **BEHAVIOR CHANGE: `:every` and `:effect` inside a reactive `@loop` body are `WD315`.** They used to compile, and each cloned row registered its own timer or watcher, so three rows meant three intervals and a removed row's interval kept firing. Neither directive has a per-row meaning to salvage (an effect watches a top-level key and an action targets one; neither can name the row), so the placement is refused with the fix spelled out: declare it once outside the loop, at page level or inside the `:::` section, and act on the whole list. Still allowed and unchanged: page level, inside a `:::`, inside a **static** loop (N literal copies that never churn), the loop's `@empty` branch, and `:button` anywhere inside a reactive loop.
|
|
25
|
+
- **BEHAVIOR CHANGE: a reactive `@loop` over markdown pipe rows is `WD191`.** A reactive row is cloned into a `<div>`, which is not a legal child of `<table>`, so there is no correct HTML to emit; it used to produce silent paragraph junk. The error names both ways out: loop a static source (a JSON file, a frontmatter list, or a collection) for a markdown table, or build reactive rows from containers (`::: trow` / `::: td`) instead of `|` cells.
|
|
26
|
+
- **BEHAVIOR CHANGE: `:note` and `:sprint` no longer exist.** They were undocumented demo-only directives that rendered an `<aside>`. Those lines now render as text and warn that they look like a directive but match none. `:try` is the only demo directive left. `:slider` and `:carousel` are known names, and a stray `:endcarousel` with no opener is `WD010` instead of a spelling warning.
|
|
27
|
+
- **BEHAVIOR CHANGE: quoted numeric frontmatter stays a string.** `sku: "007"` and `ver: "1.10"` became `7` and `1.1` in collection rows, which also made a `sku: string` schema unbuildable. A quoted scalar is now a string everywhere. Unquoted `sku: 007` still coerces to a number.
|
|
28
|
+
- **`sortable` is a bare `@loop` clause, never `:sortable`.** The compiler has only ever accepted `@loop items into p sortable`, and the generated grammar always agreed, but `README.md` and the docs page each wrote it with a leading colon in three places, which does not compile. Corrected on both. This is a documentation fix, not a language change: nothing that compiled before compiles differently.
|
|
29
|
+
- **BUGFIX: a render now settles.** `renderNow` repeats a pass (hard cap 3, and a pass that exits dirty schedules one more render, bounded and warned under `wd.debug`) while a pass clones rows or injects a branch. The `:if` and `.class when` passes are document-wide queries, so a row the reconcile cloned in the same pass was not in the tree when they ran, and it kept the compile-time branch baked into its template until the next unrelated state change. Nested loops revealed by a per-row `:if` are reconciled in the same render too.
|
|
30
|
+
- **BUGFIX: a `:fetch`, `:effect` or `:every` inside an `:if` branch never fired.** They were claimed by a load-time query that could not see inside a `<template>`, so a fetch in a closed branch simply never ran when the branch opened. `scan()` now claims them on every render. The other half of that bug is closed as well: a directive in a swapped-out branch, a removed loop row, or a toggled `@empty` branch is **unregistered** and its intervals cleared, so re-opening never double-fires.
|
|
31
|
+
- **BUGFIX: a re-render no longer steals focus or scroll position.** Every row used to be re-appended on every render. Row placement is now prefix-stable: a row already in the right place is left alone, which keeps the caret in a row's input and the page where the reader put it.
|
|
32
|
+
- **BUGFIX: blocked `localStorage` no longer kills the runtime.** Every access goes through one guarded helper, so a sandboxed iframe or a browser configured to block storage degrades to in-memory state instead of throwing during boot. A corrupt `:store` entry is dropped the way a corrupt `persist` entry always was.
|
|
33
|
+
- **BUGFIX: a slow response can no longer overwrite a newer one.** `:form` submits got the same generation guard `:fetch` already had, and `<name>_error` clears at the start of every submit. The refetch debounce keeps its queue across a timer reset, so a mutation queued just before a later one is not dropped, and `wd.set` triggers refetch checks like the other write paths do. `:effect` and `:every` clone their action values per fire, so a literal array or object can no longer be mutated across fires.
|
|
34
|
+
- **BUGFIX: a reserved name could poison the state object (`WD250`).** `:state __proto__ = {"polluted": true}` compiled, and the runtime's boot `Object.assign` wrote through the prototype. `__proto__`, `constructor` and `prototype` are refused as a declaration name at the one choke point every declaring directive goes through (`:state`, `:store`, `:computed`, `:theme`, `:fetch`, `:form … into`, `:slider`). Relatedly, `{ toString }` is literal text again instead of resolving off `Object.prototype` and rendering a function body, and `@include /p.wd with v={ toString }` is `WD604`.
|
|
35
|
+
- **BUGFIX: the include sandbox was a string-prefix check.** A symlink inside `site/_` pointing outside the project was followed. The canonical (realpath) location is now checked after the lexical one, so an escape is `WD601`. A symlinked project root still works.
|
|
36
|
+
- **BUGFIX: a clause keyword inside a quoted `where` operand was read as a clause.** `@loop items into p where p.title contains "we sort by hand"` was `WD102`; `"please reverse it"`, `"a sortable b"` and `"a paginate 5 b"` failed the same way. Keywords inside a quoted operand are text now, and a real clause after such a string still applies. `where a contains "x and y"` no longer splits the string on ` and `, and exponent notation in a static operand (`1e21`) reports `WD190` with a file and line instead of throwing a raw `Error`.
|
|
37
|
+
- **BUGFIX: two loop keys could collide.** A row genuinely keyed `a#1` collided with the second duplicate of `a` (also `a#1`), so reconcile matched the wrong node and moved the wrong row. A literal `#` in a key is doubled before the duplicate suffix now, and compiler and runtime share the scheme under a parity test.
|
|
38
|
+
- **BUGFIX: a per-row `:if` painted the wrong branch until the first render.** The build evaluates the predicate per row now, so the first paint matches what the runtime will do: no hydration flash, and a crawler or a no-JS reader sees the correct branch. In the same area, `:if` and `.class when` inside a loop resolve the loop item **before** static scope (include arguments), matching the order `{ … }` and `@loop … where` already documented.
|
|
39
|
+
- **BUGFIX: sorting.** `sort by { sortKey }` painted rows unsorted until the runtime ran, and equal keys could reorder on every render. The initial paint resolves the state key, and the comparator is stable in both directions, so ties keep source order.
|
|
40
|
+
- **BUGFIX: every state key emitted into a `data-wd-*` attribute is escaped.** A container written `::: card #a"onmouseover=alert(1)x` put a raw quote into `data-wd-bind`, `data-wd-target`, `data-wd-if` and the rest. All eleven emit sites escape now.
|
|
41
|
+
- **BUGFIX: a reactive array bind painted a different string than it repainted.** `{ tags }` for `:state tags = ["a","b"]` was painted `a, b` at build and repainted `a,b` by the runtime on the first change. Reactive binds paint `String(array)` from the start; static `{ meta.tags }` keeps its documented `, ` join, because it never repaints.
|
|
42
|
+
- **BUGFIX: `append` was read as `prepend`.** The parser searched the whole expression for the word, so `:button "A" -> list append "we prepend things"` prepended. The operator token is captured now, and the value may contain either word.
|
|
43
|
+
- **BUGFIX: incremental rebuilds could ship a stale page.** Two values baked into every page were not guarded as whole-site inputs: `site_url` (every canonical and `og:url`) and ancestor titles (every descendant's breadcrumb JSON-LD). Editing one page's title left every descendant stale while the sitemap re-emitted correctly. The dependency map records both, a `site_url` change forces a full rebuild, and a title change fans out precisely to the pages whose breadcrumb trail names the renamed route. The map format is version 2, and an old map falls back to a full rebuild.
|
|
44
|
+
- **BUGFIX: `darkmown dev`/`serve` died on a route with a dot in its last segment.** The extension of `/v1.2/` is `.2`, so `index.html` was never appended, the path resolved to a directory, and the read stream had no error handler. A directory now resolves to its `index.html`, a non-file is a 404, and both the preview and dev read paths handle stream errors.
|
|
45
|
+
- **BUGFIX: feeds and the sitemap.** A paginated dated listing no longer leaks into `rss.xml` as duplicate posts; `<lastmod>` is validated by round trip rather than `Date.parse` (a bad `date:` is omitted with a hint instead of corrupting the entry) and XML-escaped; a sitemap over 50,000 URLs shards into `sitemap-N.xml` behind a `<sitemapindex>`; and a static `/docs/*` header glob no longer shadows a nested reactive route. `WD612` also stopped printing its own code twice on an include cycle.
|
|
46
|
+
- **A standalone `.skin` or `.js` on the shelf publishes to `/__wd/media`.** Only a `.skin`/`.js` with a `.wd`/`.md` sibling of the same basename is skipped, because that one is already compiled into its page, so `<script src="/__wd/media/lib/helper.js">`, which the docs have always promised, resolves.
|
|
47
|
+
- **`darkmown validate` reports warnings in its headline.** It prints `compiles with N warnings.` plus a bullet per warning, instead of `ok: true` with the warnings buried in `data.warnings` where the model driving it never looked.
|
|
48
|
+
- **The coverage gate cannot score an unmeasured file as perfect.** A file with zero instrumented lines used to pass as 100%, so four shipped files were never measured at all. A zero-line file must now be declared in `NOT_MEASURED` with a reason, and a declared file that this run *did* instrument fails the gate telling you to remove the declaration.
|
|
49
|
+
- **Thirteen new error codes.** `WD190` (an expression that could not be re-parsed), `WD191` (a reactive `@loop` over table rows), `WD250` (a reserved declaration name), `WD251` (state seeded from other state), `WD260` (`from-url` on `:store`/`:theme`), `WD261` (`persist` and `ephemeral` at once, or the same word twice), `WD315` (`:every`/`:effect` inside a reactive loop), `WD450` (a bound field with no state), `WD451` (a bound `:checkbox` with several options), `WD452` (a file field in a GET form), `WD650` (an attribute outside the accessibility whitelist), `WD651` (an accessibility attribute with no quoted value), and `WD950` (an invalid `rss_limit`). Each carries a cause, a fix and a compilable example in `docs/errors.md` and `darkmown catalog --llms-full`. As always, a shipped code is a public contract: never renumbered, never reused.
|
|
50
|
+
- **Docs.** `README.md`, the docs page and `AGENTS.md` document every feature above, and every `wd` fence in all three is compiled by a test, which also rejects an arrow or `#` annotation on a directive line (`.wd` has no comment syntax, so such a note is parsed as part of the value). The **Limits** section is honest again: URL-as-state and container/button attributes have left it, while raw-HTML attribute binding, static aria values, and reactive table rows have joined it with the workaround named. Six new demo pages ship with the site, plus browser end-to-end coverage for attribute binding and URL state.
|
|
51
|
+
|
|
5
52
|
## 2.6.0 (2026-08-18)
|
|
6
53
|
|
|
7
54
|
A correctness release for the persistence surface, prompted by a report from outside the framework: the on-device planner research drives the real compiler as a ground-truth oracle, and while chasing a case where its checker accepted an edit the compiled page rejected, it found that Darkmown had two vocabularies for one concept and silently corrupted data when you picked the wrong one. No runtime change (7654 B gzipped, unchanged) and no breaking changes.
|