<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: frank</title>
    <description>The latest articles on DEV Community by frank (@mdfold).</description>
    <link>https://dev.to/mdfold</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4051706%2Ff7721660-f842-4411-ae4e-5036d351321f.jpg</url>
      <title>DEV Community: frank</title>
      <link>https://dev.to/mdfold</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mdfold"/>
    <language>en</language>
    <item>
      <title>Which AI Works: A Practical AI Tools Directory</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sun, 27 Sep 2026 00:22:30 +0000</pubDate>
      <link>https://dev.to/mdfold/which-ai-works-a-practical-ai-tools-directory-4o4i</link>
      <guid>https://dev.to/mdfold/which-ai-works-a-practical-ai-tools-directory-4o4i</guid>
      <description>&lt;p&gt;Which AI Works is an AI tools directory for discovering products by task, comparing categories, and building a shortlist to test against your workflow. Use search and rankings to find candidates, then verify them on a representative task.&lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://whichaiworks.com/" rel="noopener noreferrer"&gt;Which AI Works homepage&lt;/a&gt;, products are arranged around categories, and the search box accepts product, category, and feature terms. There is also a popularity leaderboard, individual product pages, and a compact library of builder resources. Here is a practical way to use those parts—and keep discovery separate from product evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the job, not the model
&lt;/h2&gt;

&lt;p&gt;Before opening a directory, write down the outcome you want in one sentence. “Turn customer calls into searchable notes” is more useful than “find an AI app.” “Generate a first draft of a React form from a design” is more useful than “find the best coding model.” A concrete job gives you something to compare against when product pages use similar language.&lt;/p&gt;

&lt;p&gt;Then open the &lt;a href="https://whichaiworks.com/" rel="noopener noreferrer"&gt;Which AI Works homepage&lt;/a&gt;. The directory presents product discovery alongside categories such as AI, analytics, automation, design, and developer tools. At the time of review, the homepage displayed 15 categories and a search box whose prompt includes products, categories, and features. Those are useful entry points when you have either a broad area (“design”) or a capability in mind (“transcription”).&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable way to find and compare tools
&lt;/h2&gt;

&lt;p&gt;Use this short workflow when you are evaluating a category you have not explored before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the task and the constraint.&lt;/strong&gt; Note the input you have, the output you need, and any non-negotiable limits: budget, data sensitivity, integrations, latency, or human review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search by task language.&lt;/strong&gt; Try the phrase you would use to describe the work, then try a feature or category term. Start at &lt;a href="https://whichaiworks.com/search" rel="noopener noreferrer"&gt;Search&lt;/a&gt; or browse the category links from the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open more than one product detail page.&lt;/strong&gt; Read the overview and key features, then follow the product’s own website link. Similar tag labels do not guarantee similar workflows or pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the vendor’s current documentation and terms.&lt;/strong&gt; Confirm what the product actually accepts and returns, what its free or paid plan includes, and how it handles your data. A directory listing is a discovery aid, not an independent security review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test one representative task.&lt;/strong&gt; Use a small, non-sensitive sample and a clear success condition. For example, check whether a transcription tool preserves speaker names and timestamps, or whether a coding assistant respects the project’s existing conventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the result.&lt;/strong&gt; Keep the product, test input, outcome, price checked, and date. That makes a later comparison fair when product features or plans change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process is intentionally modest. It does not promise that browsing a directory will identify a universal winner. It helps you narrow the field, then gives you a repeatable way to make the final choice against your own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rankings can—and cannot—tell you
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://whichaiworks.com/ranking" rel="noopener noreferrer"&gt;Which AI Works rankings&lt;/a&gt; show a product leaderboard with All Time and month-specific views, plus category filters. The page describes the ordering as based on real catalog engagement. That makes the leaderboard useful for noticing what other visitors are exploring and for spotting products you might otherwise miss.&lt;/p&gt;

&lt;p&gt;Treat that signal as attention, not as a quality score. Engagement does not tell you whether a tool is accurate on your task, handles private data appropriately, fits your budget, or remains reliable after an update. A product near the top of a list can be a good candidate for evaluation; the ranking alone is not a reason to adopt it. When you compare positions, keep the time window and category in view, and verify the product’s current behavior on its own site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product pages are the bridge to evaluation
&lt;/h2&gt;

&lt;p&gt;A directory is most useful when it shortens the path from “I have a problem” to “I can test a relevant product.” On a listing’s detail page, use the overview and feature description to understand its stated purpose, note its categories, and follow the outbound link to inspect the product directly. For example, the &lt;a href="https://whichaiworks.com/products/best-jev-ai" rel="noopener noreferrer"&gt;Best Jev AI listing&lt;/a&gt; describes a browser playground and an independent API gateway; the page also separates its overview, features, and usage information. Those details help frame what to verify next, but they are still the listing’s published description—not proof of a benchmark result.&lt;/p&gt;

&lt;p&gt;For a fair comparison, choose two or three candidates and give each the same task, inputs, and time budget. Check the output against a short rubric you write first. If the tools handle sensitive information, review their data policies before uploading anything real. The right product is the one that meets your requirements consistently, not necessarily the one with the most impressive demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful extra for builders
&lt;/h2&gt;

&lt;p&gt;Which AI Works also maintains a &lt;a href="https://whichaiworks.com/resources" rel="noopener noreferrer"&gt;builder resources page&lt;/a&gt; with links to primary documentation for technologies used to build and ship products, including Next.js, Cloudflare, Drizzle, and Stripe. It is a compact reference shelf rather than a general tutorial library. If you are evaluating developer tools or assembling an application stack, it gives you a direct route to the underlying platform documentation.&lt;/p&gt;

&lt;p&gt;The site also has a path for creators who want to submit a product, with submission and pricing information under &lt;a href="https://whichaiworks.com/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;. That matters because directories serve two audiences: people looking for a tool and teams trying to make a product discoverable. As a reader, pay attention to labels such as promoted or sponsored when they appear, and use the same evaluation criteria for every candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use Which AI Works
&lt;/h2&gt;

&lt;p&gt;Use the directory when you need a starting point, a category overview, or a shortlist of products to investigate. It is especially useful when your task crosses categories—such as automating a design handoff or adding AI to an analytics workflow—because a task-first search can reveal options that a model-only list would miss.&lt;/p&gt;

&lt;p&gt;Use vendor documentation, trials, and your own tests to answer the questions a directory cannot settle: Does it work on your data? Does it integrate with your stack? Are the limits and terms acceptable? How often does it fail on edge cases? Those answers depend on your workflow and can change over time.&lt;/p&gt;

&lt;p&gt;A good AI tools directory should make discovery faster while leaving room for judgment. &lt;a href="https://whichaiworks.com/" rel="noopener noreferrer"&gt;Which AI Works&lt;/a&gt; offers category browsing, task-oriented search, engagement-based rankings, product detail pages, and builder references in one place. Start with the job you need done, use the directory to find plausible candidates, and let a small, dated test—not a headline or leaderboard position—make the final call.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Reliable Decision Workflows with Jev AI: A TypeScript Guide</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:17:24 +0000</pubDate>
      <link>https://dev.to/mdfold/building-reliable-decision-workflows-with-jev-ai-a-typescript-guide-1883</link>
      <guid>https://dev.to/mdfold/building-reliable-decision-workflows-with-jev-ai-a-typescript-guide-1883</guid>
      <description>&lt;p&gt;Many product workflows do not need another paragraph from an AI model. They need a bounded answer that application code can use: which queue should receive a ticket, how urgent is it, or should a person review the next step?&lt;/p&gt;

&lt;p&gt;Jev is designed for that narrower job. Send a piece of application state with questions whose answer shapes are defined in advance, then use the typed results in your own code. It is a decision API, not a chatbot or a video-generation model.&lt;/p&gt;

&lt;p&gt;This guide follows the learning path in the &lt;a href="https://huggingface.co/blog/sora-2/how-to-use-the-jev-ai-model-a-step-by-step-develop" rel="noopener noreferrer"&gt;Hugging Face step-by-step Jev guide&lt;/a&gt;—define a decision, prepare state, choose a question type, experiment, then integrate server-side. It extends that path with a typed TypeScript service, failure handling, three architecture diagrams, a comparison of the five sites supplied for this review, and a clearly dated benchmark snapshot. The API example below follows the endpoint in the &lt;a href="https://docs.typesafe.ai/api" rel="noopener noreferrer"&gt;current TypeSafe API reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A production shape for a decision workflow
&lt;/h2&gt;

&lt;p&gt;Keep the model behind a server-side boundary. Your application validates its answer and decides whether the next step is an automatic, reversible action, a human review, or a separate generative-model task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhs03h27vosh2h30c65h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffhs03h27vosh2h30c65h.png" alt="Decision workflow architecture with an application-owned policy boundary" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. A decision architecture that keeps permissions and execution in application code.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Jev returns
&lt;/h2&gt;

&lt;p&gt;The request contains three top-level fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;: the text or structured application data to evaluate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model&lt;/code&gt;: a model alias such as &lt;code&gt;jev-latest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;questions&lt;/code&gt;: named questions with explicit answer types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TypeSafe documents three primitives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;choice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Choose a route from a fixed set&lt;/td&gt;
&lt;td&gt;Selected option, option probabilities, and confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate the state against ordered levels&lt;/td&gt;
&lt;td&gt;Probability-weighted score, level probabilities, and confidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;noul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Judge one yes/no proposition&lt;/td&gt;
&lt;td&gt;A yes probability from 0 to 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Several questions can share one request. They are evaluated independently against the same state, so a question in that request cannot depend on another answer. If a later decision needs an earlier result, express that dependency in application code. See the &lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;TypeSafe introduction&lt;/a&gt; and &lt;a href="https://docs.typesafe.ai/api" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; for the request and response contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Call the API from a TypeScript server
&lt;/h2&gt;

&lt;p&gt;Store the key in a server-side secret such as &lt;code&gt;TYPESAFE_API_KEY&lt;/code&gt;. The official endpoint is &lt;code&gt;POST https://api.typesafe.ai/v1/systemone&lt;/code&gt; with a Bearer token. Do not copy API keys into browser code or an article.&lt;/p&gt;

&lt;p&gt;Endpoint note: the Hugging Face guide uses &lt;code&gt;https://thejevai.com/v1/systemone&lt;/code&gt; in its example, while TypeSafe's current API reference documents &lt;code&gt;https://api.typesafe.ai/v1/systemone&lt;/code&gt;. These are distinct service hosts. Use the endpoint and key issued by the provider you have selected, and verify that route's operator and data terms.&lt;/p&gt;

&lt;p&gt;The Node.js 18+ example below evaluates a support ticket with one routing question, one urgency score, and one human-review check. It validates the fields the application will use before branching on them. &lt;code&gt;enqueueForHumanReview&lt;/code&gt; and &lt;code&gt;routeToTeam&lt;/code&gt; are application functions you would implement around your own queue and ticket system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;technical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TriageResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isProbability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isTriageResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;TriageResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;needs_human&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProbability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProbability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;isProbability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestJev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TYPESAFE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TYPESAFE_API_KEY is not configured&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.typesafe.ai/v1/systemone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;529&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Jev request failed with HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hintedDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retryAfterMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hintedDelay&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Jev retry window exceeds the request budget&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;triageTicket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;failedAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jev-latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Send only fields needed for these decisions; keep ticket.id in our system.&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;failed_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failedAttempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Which team should handle this support ticket?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment, invoice, or refund issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A shipment or delivery issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Login, access, or account settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;technical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A product bug or integration failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;None of these categories clearly fit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How urgent is the issue for the customer?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Routine; no near-term impact is described&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Time-sensitive; follow up during the current business day&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service is blocked or a deadline is at immediate risk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Should a person review this ticket before an automated action is taken?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;true&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The facts are ambiguous or the next action could materially affect the customer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;false&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The next step is a reversible, low-risk routing action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestJev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log a redacted error in production; do not log the API key or raw ticket state.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueueForHumanReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jev_unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isTriageResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueueForHumanReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_jev_response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;needs_human&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviewRequired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;needs_human&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewRequired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enqueueForHumanReview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;needs_human&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;routeToTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;routed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry loop only retries the documented transient statuses, &lt;code&gt;429&lt;/code&gt; and &lt;code&gt;529&lt;/code&gt;, and stops when the delay exceeds this example's budget. Timeouts, invalid responses, missing credentials, and other HTTP errors enter the application's review path. Production services should add structured, redacted error logging and align timeouts and retry budgets with their own latency objectives.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;0.8&lt;/code&gt; and &lt;code&gt;0.75&lt;/code&gt; values are demonstration thresholds, not Jev defaults. &lt;code&gt;confidence&lt;/code&gt; is derived from an answer distribution; it is not a guarantee of correctness. Calibrate decision thresholds on examples from your own workload. For consequential actions such as refunds, account access, or deletion, require application permissions or human approval regardless of model confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the provider as part of your architecture
&lt;/h2&gt;

&lt;p&gt;The API contract, key issuer, billing, and data path depend on which service endpoint you choose. The &lt;a href="https://docs.typesafe.ai/api" rel="noopener noreferrer"&gt;TypeSafe API reference&lt;/a&gt; documents the direct endpoint used above. The five sites below were supplied for this comparison; their public pages describe service entry points, playgrounds, or gateways rather than five independently benchmarked models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yhiri7ynfocpvrbf91n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yhiri7ynfocpvrbf91n.png" alt="Provider boundaries between the direct TypeSafe API and Jev-branded services" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Direct API access and Jev-branded services are separate routes to evaluate.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Public positioning observed for this review&lt;/th&gt;
&lt;th&gt;Check before integrating&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://thejevai.com/" rel="noopener noreferrer"&gt;thejevai.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Advertises a free unlimited playground and API plans; its page says the service is independently operated and not affiliated with TypeSafe.&lt;/td&gt;
&lt;td&gt;Confirm the endpoint and key issuer for your account, plus the current usage limits and data terms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://jevaimodel.net/" rel="noopener noreferrer"&gt;jevaimodel.net&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Describes a playground and API credits around the TypeSafe model, and presents itself as an independent service.&lt;/td&gt;
&lt;td&gt;Check credit accounting, required account scope, upstream provider, and request retention.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://jevaimodel.org/" rel="noopener noreferrer"&gt;jevaimodel.org&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Presents a Jev decision-model workbench with playground, API, and workflow material.&lt;/td&gt;
&lt;td&gt;Verify the actual API host, model provider, processing region, and billing terms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://bestjevai.com/" rel="noopener noreferrer"&gt;bestjevai.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Describes an OpenRouter-powered playground and API gateway.&lt;/td&gt;
&lt;td&gt;Confirm the routed model, gateway markup, key ownership, and gateway data policy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://jevmodel.net/" rel="noopener noreferrer"&gt;jevmodel.net&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Advertises a playground and API access plans.&lt;/td&gt;
&lt;td&gt;Define what “unlimited” covers and verify the service operator, rate limits, and retention terms.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a feature and positioning comparison based on public site descriptions, not a security or compliance audit. Site terms, endpoints, and pricing can change. A familiar name or compatible request shape does not establish common ownership or identical privacy behavior. For production, inspect the exact endpoint, key issuer, upstream provider, region, retention and logging policy, rate limits, price, support, and terms before sending sensitive state. Use synthetic examples during initial evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate before automation, then roll out gradually
&lt;/h2&gt;

&lt;p&gt;A successful playground result only proves that one example produced an answer. Build a labeled evaluation set that includes routine cases, boundary cases, ambiguous inputs, and cases where the correct outcome is human review. Track per-class precision and recall, calibration, review rate, p95 latency, timeout and overload rates, and cost per accepted decision.&lt;/p&gt;

&lt;p&gt;Run a shadow phase first: record what Jev would have selected without taking the action, then compare it with the human or rules-based outcome. If that meets your acceptance criteria, canary a small percentage of eligible, low-risk traffic. Keep a safe fallback and a rollback path. Revisit the evaluation set as products, rubrics, languages, and request distributions change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qng26pe46daxq3gcywt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qng26pe46daxq3gcywt.png" alt="Evaluation and gradual release loop for a decision workflow" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. A measured release loop feeds reviewed production outcomes back into the evaluation set.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Six systems in a JevBench snapshot
&lt;/h2&gt;

&lt;p&gt;“Top six” only has meaning after naming a scoring rule and a measurement date. The &lt;a href="https://github.com/fstandhartinger/jevbench/blob/main/RESULTS-v1.2.md" rel="noopener noreferrer"&gt;JevBench v1.3.0 results&lt;/a&gt; combine intelligence, calibration, speed, and cost at 25% each using a geometric mean. The table below reproduces the first six systems in the results file as reviewed on September 23, 2026; the run covers 534 frozen decisions per system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;JevBench score&lt;/th&gt;
&lt;th&gt;Evaluated setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Jev 1.13.0&lt;/td&gt;
&lt;td&gt;74.4&lt;/td&gt;
&lt;td&gt;TypeSafe production API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SemIf (Qwen3.5-4B)&lt;/td&gt;
&lt;td&gt;73.1&lt;/td&gt;
&lt;td&gt;Self-hosted GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;djev (Maisa, diffusion-gemma)&lt;/td&gt;
&lt;td&gt;73.0&lt;/td&gt;
&lt;td&gt;Hosted API preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Winnow-12B Q8&lt;/td&gt;
&lt;td&gt;71.2&lt;/td&gt;
&lt;td&gt;Self-hosted GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;reflex 4B&lt;/td&gt;
&lt;td&gt;70.3&lt;/td&gt;
&lt;td&gt;Self-hosted GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;jqv (Qwen3-32B, zero-shot)&lt;/td&gt;
&lt;td&gt;68.6&lt;/td&gt;
&lt;td&gt;Self-hosted GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat this as a dated benchmark snapshot, not a universal buying guide or a ranking of the five sites above. The benchmark notes that some self-hosted and demo latencies are adjusted by ×2, plus 0.15 seconds on the benchmark author's own servers, to approximate production load. That is an assumption, not a direct measurement. Your result may differ with your hardware, request mix, language, provider, concurrency, and the relative weight you give to accuracy, calibration, speed, and cost. Re-run your own workload before selecting a production route.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Jev fits—and when it does not
&lt;/h2&gt;

&lt;p&gt;Use a typed decision API when the answer space is known in advance and your application needs a classification, score, or focused yes/no judgment. Use a generative model for drafting, explanations, open-ended interaction, and work that needs extended reasoning. A reliable system can use both: Jev chooses among allowed paths, and application code decides whether to proceed, ask a person, or call a generative model.&lt;/p&gt;

&lt;p&gt;The useful boundary is simple: a model can provide a decision signal; your application still owns the policy and the action.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/blog/sora-2/how-to-use-the-jev-ai-model-a-step-by-step-develop" rel="noopener noreferrer"&gt;Hugging Face: How to Use the Jev AI Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.typesafe.ai/introduction" rel="noopener noreferrer"&gt;TypeSafe introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.typesafe.ai/api" rel="noopener noreferrer"&gt;TypeSafe API reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;TypeSafe: Introducing System One Models &amp;amp; Jev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fstandhartinger/jevbench/blob/main/RESULTS-v1.2.md" rel="noopener noreferrer"&gt;JevBench v1.3.0 results and methodology&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A DOCX Style Map Is an API Contract, Not a Font Heuristic</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Thu, 17 Sep 2026 03:10:40 +0000</pubDate>
      <link>https://dev.to/mdfold/a-docx-style-map-is-an-api-contract-not-a-font-heuristic-53je</link>
      <guid>https://dev.to/mdfold/a-docx-style-map-is-an-api-contract-not-a-font-heuristic-53je</guid>
      <description>&lt;p&gt;A paragraph can look like a heading without declaring a heading style. A converter has to choose whether to preserve the document's explicit structure or guess the author's intent. Those are different products, and mixing them silently makes migrations hard to audit.&lt;/p&gt;

&lt;p&gt;I tested a small example on September 17, 2026, using docx 9.7.1 to generate inputs, Mammoth 1.12.0 to read them, and Turndown 7.2.4 to serialize HTML as Markdown. The interesting result was not just a missing heading: a table survived the first stage and disappeared structurally in the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with two independent signals
&lt;/h2&gt;

&lt;p&gt;The semantic fixture uses a paragraph style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Paragraph&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HeadingLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEADING_1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Real heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visual fixture uses direct run formatting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Paragraph&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextRun&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Visual heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
  &lt;span class="p"&gt;})]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are structural test fixtures, not a claim that two rendered pages are pixel-identical. Mammoth returned an &lt;code&gt;h1&lt;/code&gt; for the first and a paragraph containing &lt;code&gt;strong&lt;/code&gt; for the second. With ATX headings enabled, the Markdown became &lt;code&gt;# Real heading&lt;/code&gt; and &lt;code&gt;**Visual heading**&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;That distinction follows the separation between &lt;a href="https://learn.microsoft.com/en-us/office/open-xml/word/working-with-paragraphs" rel="noopener noreferrer"&gt;paragraph properties&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/office/open-xml/word/working-with-runs" rel="noopener noreferrer"&gt;run properties&lt;/a&gt; in WordprocessingML. Reading a font size is not the same as reading a section level.&lt;/p&gt;

&lt;h2&gt;
  
  
  A custom style needs an agreed meaning
&lt;/h2&gt;

&lt;p&gt;The next fixture used a custom paragraph style named &lt;code&gt;Section Label&lt;/code&gt;, with bold 16pt formatting and outline level 0. Mammoth's default conversion produced a paragraph and an unknown-style warning. Adding this mapping changed that paragraph to an &lt;code&gt;h1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mammoth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convertToHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;styleMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p[style-name='Section Label'] =&amp;gt; h1:fresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mwilliamson/mammoth.js" rel="noopener noreferrer"&gt;Mammoth documents this mapping mechanism&lt;/a&gt;. The map is only justified when the document template actually uses that style for first-level headings. It is not permission to infer a heading from every unfamiliar style name.&lt;/p&gt;

&lt;p&gt;This suggests treating template changes like schema changes: version the style map, retain unknown-style warnings, and keep fixtures from the templates you support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the intermediate representation
&lt;/h2&gt;

&lt;p&gt;My difficult fixture also contained a two-level numbered list and a 2-by-2 table. Mammoth's HTML contained both nested lists and a &lt;code&gt;table&lt;/code&gt;. Default Turndown, without a table plugin, preserved the list but emitted the cell texts as separate paragraphs. All four cell values survived; their row/column relationship did not.&lt;/p&gt;

&lt;p&gt;This is why a text-presence assertion is insufficient. Check the HTML table first, then check the Markdown representation expected by your destination. &lt;a href="https://github.com/mixmark-io/turndown" rel="noopener noreferrer"&gt;Turndown exposes plugins&lt;/a&gt;, but enabling an extension still requires a new compatibility test.&lt;/p&gt;

&lt;p&gt;I reproduced these fixtures in &lt;a href="https://mdfold.com/word-to-markdown" rel="noopener noreferrer"&gt;MDFold's Word-to-Markdown converter&lt;/a&gt;, which I develop. The live table output had the same limitation. This is a boundary to review, not evidence of lossless conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries that change the acceptance criteria
&lt;/h2&gt;

&lt;p&gt;An independent comparison with LibreOfficeDev 26.8.0.0.alpha0's HTML exporter produced an &lt;code&gt;h1&lt;/code&gt; for the built-in heading and a visually formatted paragraph for the custom style. For an empty Heading 1, LibreOffice retained an empty heading with line breaks while Mammoth omitted it. This development build and these small fixtures are not a ranking of converter quality.&lt;/p&gt;

&lt;p&gt;A deliberately invalid DOCX produced a ZIP error locally. In the live UI, selecting it after a valid file displayed an error but left the previous output available. That is a separate state-management defect: successful old text must not be mistaken for a successful new conversion. It was observed, not fixed, in this test.&lt;/p&gt;

&lt;p&gt;My acceptance checks now separate three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did the reader recognize the declared structure and surface warnings?&lt;/li&gt;
&lt;li&gt;Did serialization preserve the structures the destination supports?&lt;/li&gt;
&lt;li&gt;Does the displayed result still belong to the currently selected input?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Images, tracked changes, footnotes, and merged cells were outside this experiment. A green result on these fixtures says nothing about those features.&lt;/p&gt;

&lt;p&gt;Would you make visual heading inference opt-in, or show proposed headings for review before changing the output?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>javascript</category>
      <category>html</category>
      <category>testing</category>
    </item>
    <item>
      <title>HTML-to-Markdown Round Trips Are a Semantic Contract, Not a Backup</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:20:01 +0000</pubDate>
      <link>https://dev.to/mdfold/html-to-markdown-round-trips-are-a-semantic-contract-not-a-backup-4gbm</link>
      <guid>https://dev.to/mdfold/html-to-markdown-round-trips-are-a-semantic-contract-not-a-backup-4gbm</guid>
      <description>&lt;p&gt;An HTML-to-Markdown conversion can preserve every visible word and still lose the document.&lt;/p&gt;

&lt;p&gt;The reason is structural: HTML can encode presentation, application metadata, arbitrary element attributes, interactive behavior, and table spans. Markdown has a deliberately smaller document model. Conversion is therefore a policy for choosing information, not the inverse of HTML parsing.&lt;/p&gt;

&lt;p&gt;I tested a browser-local converter with Node.js 25.3.0, Turndown 7.2.4, and Marked 18.0.7. Each fixture records the input HTML, emitted Markdown, and HTML rendered from that Markdown. The goal was not visual similarity. It was to identify which semantic invariants survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a case that should work
&lt;/h2&gt;

&lt;p&gt;This input uses structures with direct Markdown counterparts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Release notes&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Ship the &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;reviewed&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt; draft with the
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/spec"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;source specification&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Preserve hierarchy
    &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&amp;lt;li&amp;gt;&lt;/span&gt;Verify the final link&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Release notes&lt;/span&gt;

Ship the &lt;span class="gs"&gt;**reviewed**&lt;/span&gt; draft with the &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;source specification&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;span class="p"&gt;
-&lt;/span&gt;   Preserve hierarchy
&lt;span class="p"&gt;    -&lt;/span&gt;   Verify the final link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendering it again restored one &lt;code&gt;h1&lt;/code&gt;, one &lt;code&gt;strong&lt;/code&gt;, the same link target, and two list levels. The HTML whitespace and tag spelling changed, but the document meaning under test survived.&lt;/p&gt;

&lt;p&gt;That distinction matters. Byte equality is neither expected nor useful here. A migration test should ask whether the required structure survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  One difficult fixture exposes the many-to-one mapping
&lt;/h2&gt;

&lt;p&gt;Now add presentation metadata, inline elements without portable Markdown equivalents, and a merged table cell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"release-card"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:grid"&lt;/span&gt; &lt;span class="na"&gt;data-build=&lt;/span&gt;&lt;span class="s"&gt;"42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"color:red"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Styled release&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;mark&amp;gt;&lt;/span&gt;Highlighted&lt;span class="nt"&gt;&amp;lt;/mark&amp;gt;&lt;/span&gt; H&lt;span class="nt"&gt;&amp;lt;sub&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/sub&amp;gt;&lt;/span&gt;O and x&lt;span class="nt"&gt;&amp;lt;sup&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/sup&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Item&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&lt;/span&gt;Status&lt;span class="nt"&gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;rowspan=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Parser&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;Ready&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;Checked&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual Markdown was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Styled release&lt;/span&gt;

Highlighted H2O and x2.

Item

Status

Parser

Ready

Checked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text order survived. The class, inline style, build metadata, highlight, subscript, superscript, table grid, and &lt;code&gt;rowspan&lt;/code&gt; did not. Re-rendering produced a heading followed by plain paragraphs. No later converter can infer which paragraphs used to be cells.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mixmark-io/turndown" rel="noopener noreferrer"&gt;Turndown documentation&lt;/a&gt; explains why: recognized elements use conversion rules; the default rule for an unrecognized element emits its text content. Table support is supplied by a GFM plugin or custom rules rather than the base CommonMark rule set.&lt;/p&gt;

&lt;p&gt;GFM tables still would not solve every case. The &lt;a href="https://github.github.com/gfm/#tables-extension-" rel="noopener noreferrer"&gt;GFM specification&lt;/a&gt; defines a header row, delimiter row, and data rows whose cells contain inline content. It does not provide HTML-style row spans, column spans, or arbitrary block structure inside cells.&lt;/p&gt;

&lt;h2&gt;
  
  
  Malformed HTML adds a parsing stage you may forget
&lt;/h2&gt;

&lt;p&gt;I also tested invalid nesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Before&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Inside&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;After&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&amp;lt;li&amp;gt;&lt;/span&gt;One&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Two&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Before

Inside

After
&lt;span class="p"&gt;
-&lt;/span&gt;   One
&lt;span class="p"&gt;-&lt;/span&gt;   Two
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML parser first constructed a corrected DOM tree. The converter then traversed that tree. The &lt;a href="https://html.spec.whatwg.org/multipage/parsing.html" rel="noopener noreferrer"&gt;WHATWG parsing algorithm&lt;/a&gt; defines error-recovery behavior, so the converter may preserve the browser's interpretation rather than the author's exact source boundaries.&lt;/p&gt;

&lt;p&gt;If a failed migration is inspected only after rendering, these stages become indistinguishable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the source HTML was already invalid;&lt;/li&gt;
&lt;li&gt;the HTML parser repaired it;&lt;/li&gt;
&lt;li&gt;the conversion rule flattened a node;&lt;/li&gt;
&lt;li&gt;the destination Markdown dialect interpreted the output differently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the original fixture and every intermediate representation if you need to locate the first semantic change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary behavior needs exact output, not a green message
&lt;/h2&gt;

&lt;p&gt;The fourth fixture combined code metadata, non-breaking spaces, a line break, a script, and a style block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;.release&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__shouldNotRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;pre&amp;gt;&amp;lt;code&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"language-js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;if (a &lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt; b) {
  console.log("x");
}&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;alpha&lt;span class="ni"&gt;&amp;amp;nbsp;&amp;amp;nbsp;&lt;/span&gt;beta&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;gamma&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the tested rules, script and style content were removed. The code and its &lt;code&gt;js&lt;/code&gt; language survived, &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; became a Markdown hard break, and both &lt;code&gt;U+00A0&lt;/code&gt; characters remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;js
&lt;/span&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

alpha&amp;nbsp;&amp;nbsp;beta  
gamma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those observations are version- and rule-specific. They are not proof that arbitrary HTML is safe, that every class becomes a language, or that every renderer treats non-breaking spaces identically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test a semantic contract
&lt;/h2&gt;

&lt;p&gt;Instead of comparing HTML strings, define the structures your migration promises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Release notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;source specification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/spec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;listDepth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;console.log("x")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a real content migration, I would assert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;heading count, level, and order;&lt;/li&gt;
&lt;li&gt;link text and destination;&lt;/li&gt;
&lt;li&gt;list item count and nesting depth;&lt;/li&gt;
&lt;li&gt;code text, fence, and language;&lt;/li&gt;
&lt;li&gt;image source, alt text, and relative-URL base;&lt;/li&gt;
&lt;li&gt;table row and column counts;&lt;/li&gt;
&lt;li&gt;first and last meaningful text;&lt;/li&gt;
&lt;li&gt;an explicit inventory of discarded scripts, styles, and metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I repeated the four public, non-sensitive fixtures on &lt;a href="https://mdfold.com/html-to-markdown" rel="noopener noreferrer"&gt;MDFold's HTML-to-Markdown tool&lt;/a&gt;. Its browser output matched the local Turndown script exactly. Headings, links, nested lists, code fences, and the tested language marker survived. The tested table flattened to text, so table structure remains a manual-review boundary in the current base converter. I also verified the workflow at a 390px viewport with no horizontal overflow.&lt;/p&gt;

&lt;p&gt;That last limitation is why a converter's success message should never be the acceptance test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the source when the source matters
&lt;/h2&gt;

&lt;p&gt;Markdown is a good derived format for editing, diffing, documentation, and long-term text maintenance. It is not a forensic copy of a webpage.&lt;/p&gt;

&lt;p&gt;If you must preserve CSS, interactive controls, form behavior, arbitrary attributes, complex tables, or exact invalid source spelling, retain the original HTML alongside the Markdown. Record the converter version, options, plugins, fixtures, and warnings so the derivation can be reproduced.&lt;/p&gt;

&lt;p&gt;The engineering question is not “Did the converter finish?” It is “Which semantic contract did it satisfy, and which information did we intentionally abandon?”&lt;/p&gt;

&lt;p&gt;When visual fidelity and semantic maintainability conflict in a migration, which one belongs in your acceptance criteria?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>html</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Source Positions Are a Coordinate System, Not Just Line and Column</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:24:16 +0000</pubDate>
      <link>https://dev.to/mdfold/source-positions-are-a-coordinate-system-not-just-line-and-column-2epj</link>
      <guid>https://dev.to/mdfold/source-positions-are-a-coordinate-system-not-just-line-and-column-2epj</guid>
      <description>&lt;p&gt;A Markdown linter can detect the right problem and still highlight the wrong character.&lt;/p&gt;

&lt;p&gt;The usual cause is not parsing. It is a coordinate-system mismatch: one component counts UTF-8 bytes, another counts Unicode code points, and JavaScript indexes UTF-16 code units.&lt;/p&gt;

&lt;p&gt;I tested this with Node.js 25.3.0, unified 11.0.5 + remark-parse 11.0.0, commonmark.js 0.31.2, markdown-it 15.0.1 (&lt;code&gt;commonmark&lt;/code&gt; preset), and Marked 18.0.11.&lt;/p&gt;

&lt;h2&gt;
  
  
  One emoji, three valid offsets
&lt;/h2&gt;

&lt;p&gt;Consider this source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# A😀B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The position immediately before &lt;code&gt;B&lt;/code&gt; is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript UTF-16 code units&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unicode code points&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UTF-8 bytes&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# A😀B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;utf16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codePoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;utf16&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;utf16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;utf16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codePoints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// { utf16: 5, codePoints: 4, bytes: 7 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those numbers is universally wrong. The bug appears when an API calls all of them &lt;code&gt;offset&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Positions should be half-open ranges
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/syntax-tree/unist" rel="noopener noreferrer"&gt;unist specification&lt;/a&gt; defines a position with &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; points. Lines and columns are one-based, offsets are zero-based, and &lt;code&gt;end&lt;/code&gt; points to the first character after the source region. Its definition of a character is a UTF-16 code unit.&lt;/p&gt;

&lt;p&gt;remark-parse produced this position for the &lt;code&gt;A😀B&lt;/code&gt; text node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes source recovery unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating &lt;code&gt;end&lt;/code&gt; as inclusive introduces an off-by-one error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four parsers, four position capabilities
&lt;/h2&gt;

&lt;p&gt;The same input produced materially different public metadata:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser&lt;/th&gt;
&lt;th&gt;Block positions&lt;/th&gt;
&lt;th&gt;Inline positions&lt;/th&gt;
&lt;th&gt;Absolute offset&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;remark-parse 11.0.0&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;UTF-16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;commonmark.js 0.31.2&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown-it 15.0.1&lt;/td&gt;
&lt;td&gt;line ranges&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marked 18.0.11&lt;/td&gt;
&lt;td&gt;no standard field&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;commonmark.js reported the heading as &lt;code&gt;[[1,1],[1,6]]&lt;/code&gt;, but its &lt;code&gt;A😀B&lt;/code&gt; text node had no &lt;code&gt;sourcepos&lt;/code&gt;. markdown-it reported &lt;code&gt;[0,1]&lt;/code&gt; on the inline token, while every inline child had a null &lt;code&gt;map&lt;/code&gt;. Marked exposed &lt;code&gt;raw&lt;/code&gt;, not a unique source location.&lt;/p&gt;

&lt;p&gt;This is a capability difference, not a rendering-quality ranking. Block ranges are enough for scroll synchronization. Character-accurate quick fixes need inline ranges and explicit offsets.&lt;/p&gt;

&lt;p&gt;Do not reconstruct positions with &lt;code&gt;indexOf(token.raw)&lt;/code&gt;. Repeated text makes the result ambiguous. Accumulating &lt;code&gt;raw.length&lt;/code&gt; also breaks when a parser normalizes newlines, ignores a BOM, or merges text nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test failure syntax, not only valid syntax
&lt;/h2&gt;

&lt;p&gt;I used four fixtures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# Title&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;A paragraph.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;difficult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# A😀B&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Use **e&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u0301** and `code`.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# Broken [link](&amp;lt;oops&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Tail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;boundary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;uFEFF# Zero&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200BWidth&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s2"&gt;End&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failed link never became a link node. remark kept the failed construct in one text node; commonmark.js split it into several text nodes; markdown-it still only identified the containing line. A diagnostic implementation cannot assume malformed syntax has the AST shape of successful syntax.&lt;/p&gt;

&lt;p&gt;The boundary fixture exposed another divergence. With a leading BOM, remark still recognized the heading, while commonmark.js parsed the first line as a paragraph. The zero-width character remained part of the text, and CRLF consumed two UTF-16 code units.&lt;/p&gt;

&lt;p&gt;When the first question is whether a real file is Markdown, plain text, or affected by encoding, this &lt;a href="https://mdfold.com/guides/what-is-a-markdown-file" rel="noopener noreferrer"&gt;Markdown file structure guide&lt;/a&gt; is the contextual checklist I use. It does not imply that parsers normalize those boundaries consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical range contract
&lt;/h2&gt;

&lt;p&gt;For a JavaScript editor, I would make the unit part of the field name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourcePoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;offsetUtf16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;columnUtf16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourceRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourcePoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourcePoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// exclusive&lt;/span&gt;
  &lt;span class="nl"&gt;sourceVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a Rust or Go service returns byte offsets, call the field &lt;code&gt;offsetUtf8Bytes&lt;/code&gt; and convert against the exact same source text. &lt;code&gt;sourceVersion&lt;/code&gt; matters because every range becomes suspect after formatting or editing.&lt;/p&gt;

&lt;p&gt;For occasional lookups, scan from the start to compute line and column. For many diagnostics, precompute every line-start offset and use binary search. Decide how CRLF is counted and encode that decision in tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would regression-test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;an emoji before the target;&lt;/li&gt;
&lt;li&gt;a decomposed character such as &lt;code&gt;e\u0301&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;LF and CRLF;&lt;/li&gt;
&lt;li&gt;a leading BOM;&lt;/li&gt;
&lt;li&gt;a zero-width character;&lt;/li&gt;
&lt;li&gt;repeated identical text;&lt;/li&gt;
&lt;li&gt;malformed links and emphasis;&lt;/li&gt;
&lt;li&gt;generated AST nodes with no source range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The invariant is stronger than a snapshot: slicing the original source with a node's half-open offsets should reproduce that node's source spelling whenever the node genuinely came from one continuous region.&lt;/p&gt;

&lt;p&gt;Source positions are a protocol between parser, analyzer, editor, and formatter. Define the unit, range semantics, and source version before trusting the number.&lt;/p&gt;

&lt;p&gt;After a formatter rewrites the document, would you invalidate every diagnostic and reparse, or maintain a source map from the previous version?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Snapshot Tests Lie About Markdown: Assert Structure Instead</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:22:56 +0000</pubDate>
      <link>https://dev.to/mdfold/snapshot-tests-lie-about-markdown-assert-structure-instead-2d2l</link>
      <guid>https://dev.to/mdfold/snapshot-tests-lie-about-markdown-assert-structure-instead-2d2l</guid>
      <description>&lt;p&gt;A Markdown parser upgrade can pass every “does it render?” check and still change the document.&lt;/p&gt;

&lt;p&gt;The dangerous regressions are quiet: a malformed link starts auto-linking a URL, a list changes nesting, an invisible character survives normalization, or a plugin turns previously literal text into an extension node.&lt;/p&gt;

&lt;p&gt;An HTML snapshot will notice some of those changes. The problem is that it also notices quote style, optional closing tags, whitespace, attribute order, and renderer-specific formatting. Once a dependency upgrade changes hundreds of snapshots, “update all” becomes tempting—and that can approve the semantic regression with the noise.&lt;/p&gt;

&lt;p&gt;I ran a small fixed-version experiment to separate structure from serialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;Environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 25.3.0&lt;/li&gt;
&lt;li&gt;commonmark.js 0.31.2&lt;/li&gt;
&lt;li&gt;markdown-it 15.0.0 with the &lt;code&gt;commonmark&lt;/code&gt; preset&lt;/li&gt;
&lt;li&gt;Marked 18.0.9 with default options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corpus deliberately includes four classes of input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;# Release&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;- parse&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- render&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;difficult&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Read [the *nested* case](https://example.com/a_(b)).&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Broken [link](&amp;lt;https://example.com&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge-nul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u0000right&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of first comparing the entire HTML string, the harness extracts the contract we actually care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;structuralFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;h1&lt;/span&gt;&lt;span class="se"&gt;(?:\s&lt;/span&gt;&lt;span class="sr"&gt;|&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;ul&lt;/span&gt;&lt;span class="se"&gt;(?:\s&lt;/span&gt;&lt;span class="sr"&gt;|&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;li&lt;/span&gt;&lt;span class="se"&gt;(?:\s&lt;/span&gt;&lt;span class="sr"&gt;|&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;a&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+href=/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;�&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 12 parser-fixture assertions passed after the expected capability differences were made explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The differences were the useful result
&lt;/h2&gt;

&lt;p&gt;All three parsers produced the same structures for the normal document and the nested-emphasis link.&lt;/p&gt;

&lt;p&gt;For the unclosed link destination, commonmark.js and markdown-it emitted literal text. Marked did not create the intended Markdown link either, but its default URL auto-linking created an anchor for the URL inside the broken syntax.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;U+0000&lt;/code&gt; case, commonmark.js and markdown-it emitted &lt;code&gt;U+FFFD&lt;/code&gt;, matching CommonMark's input-character rule. The HTML string returned by Marked still contained &lt;code&gt;U+0000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That does not make differential testing a vote. Two implementations agreeing does not establish the product contract. The correct reference depends on the layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CommonMark core: the pinned CommonMark spec examples&lt;/li&gt;
&lt;li&gt;GFM or another extension: that extension's spec and the enabled configuration&lt;/li&gt;
&lt;li&gt;product behavior: an explicit, reviewed project decision&lt;/li&gt;
&lt;li&gt;unresolved divergence: a report, not an automatically accepted snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start with upstream conformance fixtures
&lt;/h2&gt;

&lt;p&gt;The CommonMark specification repository embeds more than 500 examples that act as conformance tests. Its test tool can dump them as JSON records containing the Markdown input, expected HTML, section, and example number.&lt;/p&gt;

&lt;p&gt;That is a much stronger base than a hand-written “Markdown basics” file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;example&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;commonmarkSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`CommonMark example &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But conformance is not the whole product. Those fixtures do not cover your sanitizer, editor transactions, plugin ordering, export template, or platform-specific extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add one permanent fixture per real bug
&lt;/h2&gt;

&lt;p&gt;Every parser bug should leave behind:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the smallest input that reproduces it;&lt;/li&gt;
&lt;li&gt;parser, plugin, preset, and option versions;&lt;/li&gt;
&lt;li&gt;the expected structural facts;&lt;/li&gt;
&lt;li&gt;the issue or commit that explains why the expectation exists;&lt;/li&gt;
&lt;li&gt;the boundary: spec requirement, extension, or product policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not delete that fixture when the implementation changes. If the intended behavior changes, update the contract in a reviewable change that explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use snapshots as evidence, not as the oracle
&lt;/h2&gt;

&lt;p&gt;Raw HTML snapshots are still useful when exact serialization is a public API. Keep them beside structural assertions.&lt;/p&gt;

&lt;p&gt;For most editor and publishing systems, stronger assertions target:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;node types and nesting;&lt;/li&gt;
&lt;li&gt;heading levels;&lt;/li&gt;
&lt;li&gt;link and image destinations;&lt;/li&gt;
&lt;li&gt;literal text preservation;&lt;/li&gt;
&lt;li&gt;source spans when diagnostics depend on them;&lt;/li&gt;
&lt;li&gt;sanitizer results after rendering;&lt;/li&gt;
&lt;li&gt;stable IDs or attributes that downstream code consumes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a table fixture is needed, I use a visual generator only to create the source, then test the generated Markdown in the target parsers. In a September 2 check, the &lt;a href="https://mdfold.com/markdown-table-generator" rel="noopener noreferrer"&gt;MDFold Markdown Table Generator&lt;/a&gt; escaped &lt;code&gt;Maya | UX&lt;/code&gt; as &lt;code&gt;Maya \| UX&lt;/code&gt;; that proves the current generator's output for this fixture, not universal table support. The same page also stayed within a 390-pixel viewport without page-level horizontal overflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Property tests need real invariants
&lt;/h2&gt;

&lt;p&gt;“Every parser must produce identical HTML” is not a valid property across Markdown dialects.&lt;/p&gt;

&lt;p&gt;Better properties include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parsing bounded input never crashes;&lt;/li&gt;
&lt;li&gt;the same version and options produce a deterministic result;&lt;/li&gt;
&lt;li&gt;the AST contains no parent-child cycles;&lt;/li&gt;
&lt;li&gt;disabling raw HTML produces no raw-HTML nodes;&lt;/li&gt;
&lt;li&gt;bounded input size and nesting do not produce unbounded work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Property-based testing tools combine generated inputs with predicates. Record the seed for every failure, shrink it, and add the minimal result to the permanent regression corpus. Otherwise the interesting failure disappears after the random run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep pathological tests separate
&lt;/h2&gt;

&lt;p&gt;Large unmatched delimiter runs, deep brackets, nested lists, and unclosed HTML comments test complexity rather than ordinary correctness. markdown-it's own repository separates CommonMark fixtures, implementation fixtures, and pathological tests; the pathological suite uses an isolated worker and a timeout.&lt;/p&gt;

&lt;p&gt;That separation is important. Run fast semantic fixtures on every commit. Run larger complexity suites nightly or before releases, using a stable input scale and a generous budget. A noisy wall-clock threshold on shared CI is not a reliable algorithm test.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical test layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixtures/
  commonmark/      # upstream, versioned conformance data
  regressions/     # one directory per real bug
  extensions/      # GFM tables, task lists, footnotes...
  security/        # raw HTML, protocols, remote resources
  pathological/    # deep, long, and unclosed inputs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dependency upgrades, generate a report with three sections: new differences, removed differences, and unclassified differences. Block the upgrade on the last section. That makes “update snapshots” a reviewed decision instead of a reflex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CommonMark 0.31.2 specification and examples: &lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;https://spec.commonmark.org/0.31.2/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CommonMark spec test tooling: &lt;a href="https://github.com/commonmark/commonmark-spec" rel="noopener noreferrer"&gt;https://github.com/commonmark/commonmark-spec&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;markdown-it test layout and pathological cases: &lt;a href="https://github.com/markdown-it/markdown-it/tree/master/test" rel="noopener noreferrer"&gt;https://github.com/markdown-it/markdown-it/tree/master/test&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;fast-check property model: &lt;a href="https://fast-check.dev/docs/core-blocks/properties/" rel="noopener noreferrer"&gt;https://fast-check.dev/docs/core-blocks/properties/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What does your Markdown suite protect today: the document's meaning, or one renderer's current whitespace?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Regex Cannot Sanitize Markdown: Put the Security Boundary After Parsing</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Mon, 31 Aug 2026 07:10:45 +0000</pubDate>
      <link>https://dev.to/mdfold/regex-cannot-sanitize-markdown-put-the-security-boundary-after-parsing-54pm</link>
      <guid>https://dev.to/mdfold/regex-cannot-sanitize-markdown-put-the-security-boundary-after-parsing-54pm</guid>
      <description>&lt;p&gt;Deleting &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; from Markdown with a regular expression is not an HTML security boundary. The browser eventually consumes a DOM, not the original Markdown string. A regex that matches one attribute spelling can miss another spelling the HTML parser accepts, while an aggressive replacement can damage text that was supposed to remain inside a code span.&lt;/p&gt;

&lt;p&gt;The reliable order is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Markdown source
  -&amp;gt; pinned parser
  -&amp;gt; HTML sanitizer for the real output policy
  -&amp;gt; controlled DOM sink
  -&amp;gt; CSP / Trusted Types as defense in depth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A regex that passes the easy test
&lt;/h2&gt;

&lt;p&gt;I tested this deliberately narrow cleaner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;naiveRegexClean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;script&lt;/span&gt;&lt;span class="se"&gt;\b[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;script&amp;gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;on&lt;/span&gt;&lt;span class="se"&gt;\w&lt;/span&gt;&lt;span class="sr"&gt;+="&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*"/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/javascript:/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It removes a plain &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and a double-quoted &lt;code&gt;onerror&lt;/code&gt;. It does not remove the same event attribute when it is unquoted or single-quoted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img src=x onerror="alert(1)"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;event attribute removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img src=x onerror=alert(1)&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;event attribute remains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;img src=x onerror='alert(1)'&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;event attribute remains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Replacing &lt;code&gt;javascript:&lt;/code&gt; also turns the URL into a different string. It does not prove that the resulting URL matches the application's protocol or navigation policy.&lt;/p&gt;

&lt;p&gt;The point is not that every regex is short. The point is that string matching and browser HTML parsing operate at different abstraction levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixed-version test
&lt;/h2&gt;

&lt;p&gt;I used Node.js 25.3.0, Marked 18.0.7, DOMPurify 3.4.12, and MDFold's current browser-side Markdown-to-HTML path. The fixture covered ordinary formatting, raw scripts, quoted and unquoted event handlers, Markdown and raw-HTML &lt;code&gt;javascript:&lt;/code&gt; links, inline code, a remote image, and a fixed-position style.&lt;/p&gt;

&lt;p&gt;Marked preserved the raw HTML in its generated output. That is expected: the Marked documentation explicitly warns that it does not sanitize output HTML.&lt;/p&gt;

&lt;p&gt;The real browser preview then sanitized the parsed HTML. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserved bold text and an HTTPS link;&lt;/li&gt;
&lt;li&gt;removed the script element and event handler;&lt;/li&gt;
&lt;li&gt;removed unsafe &lt;code&gt;href&lt;/code&gt; values while preserving link text;&lt;/li&gt;
&lt;li&gt;preserved the attack-shaped string inside inline code as escaped text;&lt;/li&gt;
&lt;li&gt;preserved the remote image URL;&lt;/li&gt;
&lt;li&gt;preserved the tested &lt;code&gt;style&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more informative than checking whether an alert happened to appear. The test inspected the resulting elements, attributes, URLs, and text nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing, sanitizing, and resource policy are separate
&lt;/h2&gt;

&lt;p&gt;CommonMark defines how raw HTML participates in Markdown parsing. It does not define a web application's XSS policy. OWASP recommends output encoding when data should remain text and HTML sanitization when authors are allowed to provide HTML. DOMPurify works on a parsed, inert DOM and applies an allow-list to elements and attributes.&lt;/p&gt;

&lt;p&gt;An application that does not need raw HTML can reject HTML nodes earlier. That reduces the surface but does not answer every downstream question. Images can still cause network requests. Links can still leave the site. Plugins can still generate HTML. The final sink still owns the final policy.&lt;/p&gt;

&lt;p&gt;The two preserved values in my test show the remaining boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A sanitized remote image can still make a request. XSS prevention and privacy are different controls.&lt;/li&gt;
&lt;li&gt;A permitted style can still obscure content. Script safety and visual-integrity policy are different controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Do not modify sanitized HTML afterward
&lt;/h2&gt;

&lt;p&gt;This sequence destroys the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;untrustedValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every untrusted value needs the defense for the context where it is inserted. Prefer safe sinks such as &lt;code&gt;textContent&lt;/code&gt; for plain text. If a CMS or template engine reparses or mutates the HTML later, validate at that boundary too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pin parser and sanitizer versions separately.&lt;/li&gt;
&lt;li&gt;Disable raw HTML when the product does not need it.&lt;/li&gt;
&lt;li&gt;When HTML is required, start with a narrow allow-list.&lt;/li&gt;
&lt;li&gt;Define protocol rules for &lt;code&gt;href&lt;/code&gt; and resource rules for &lt;code&gt;src&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Decide whether inline styles belong in the content model.&lt;/li&gt;
&lt;li&gt;Avoid post-sanitization string concatenation.&lt;/li&gt;
&lt;li&gt;Inspect the final DOM and network behavior, not only the source string.&lt;/li&gt;
&lt;li&gt;Retest in the destination CMS, browser, and mobile layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I reproduced the browser portion with the &lt;a href="https://mdfold.com/markdown-to-html" rel="noopener noreferrer"&gt;MDFold Markdown-to-HTML converter&lt;/a&gt;. At a 390-pixel viewport the tested preview stayed within the page width, but that result covers this fixture and current version only.&lt;/p&gt;

&lt;p&gt;The engineering question is not "which regex catches every payload?" It is "which component owns the policy for this exact output sink?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Primary sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark 0.31.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://marked.js.org/" rel="noopener noreferrer"&gt;Marked security warning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP XSS Prevention Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cure53/DOMPurify/blob/main/README.md" rel="noopener noreferrer"&gt;DOMPurify README&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>markdown</category>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Invisible Unicode Characters Can Break Markdown Without Looking Different</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 22 Aug 2026 22:44:45 +0000</pubDate>
      <link>https://dev.to/mdfold/invisible-unicode-characters-can-break-markdown-without-looking-different-2e3d</link>
      <guid>https://dev.to/mdfold/invisible-unicode-characters-can-break-markdown-without-looking-different-2e3d</guid>
      <description>&lt;p&gt;Two Markdown lines can look identical and still produce different HTML. Before blaming the parser, inspect the code points.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# heading
#​heading
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second line contains U+200B ZERO WIDTH SPACE after &lt;code&gt;#&lt;/code&gt;. CommonMark requires the ATX marker sequence to be followed by a space, tab, or line ending, so this is a paragraph rather than a heading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the invisible input observable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codePoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`U+&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;codePoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;u200Bheading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// U+0023 U+200B U+0068 ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the source came from an unknown editor, first verify that it is actually a text file and inspect its encoding. This &lt;a href="https://mdfold.com/guides/how-to-open-a-markdown-file" rel="noopener noreferrer"&gt;guide to opening Markdown files&lt;/a&gt; is a useful checklist; the important step is inspection, not conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What three fixed parser versions produced
&lt;/h2&gt;

&lt;p&gt;I tested Node.js 25.3.0 with Marked 18.0.7, markdown-it 14.1.0, and commonmark.js 0.31.2. These are observed outputs under default options, not claims about every Markdown platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Marked&lt;/th&gt;
&lt;th&gt;markdown-it&lt;/th&gt;
&lt;th&gt;commonmark.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;# heading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;leading U+FEFF before &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;paragraph, FEFF retained&lt;/td&gt;
&lt;td&gt;paragraph, FEFF omitted in output&lt;/td&gt;
&lt;td&gt;paragraph, FEFF omitted in output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+200B after &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+00A0 after &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fullwidth &lt;code&gt;＃&lt;/code&gt; or &lt;code&gt;＊&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The NBSP result is the useful warning: a character that looks like a space creates a real cross-parser difference. “Whitespace” is not one universal Markdown category.&lt;/p&gt;

&lt;h2&gt;
  
  
  A BOM is positional
&lt;/h2&gt;

&lt;p&gt;UTF-8 uses &lt;code&gt;EF BB BF&lt;/code&gt; as its optional signature. After decoding, that is U+FEFF. Unicode says a recognized initial BOM should be removed before text processing. The same code point in the middle of text is not automatically a BOM and must not be globally deleted.&lt;/p&gt;

&lt;p&gt;In the experiment, a leading U+FEFF prevented &lt;code&gt;#&lt;/code&gt; from being the first character seen by the Markdown grammar. An internal &lt;code&gt;alpha\uFEFFbeta&lt;/code&gt; remained present in all three outputs. Fix BOM handling at the decoding boundary, not with &lt;code&gt;replaceAll('\uFEFF', '')&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero width does not mean zero semantics
&lt;/h2&gt;

&lt;p&gt;U+200B is a format control used to indicate a word or line-break opportunity. Its effect depends on position:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after &lt;code&gt;#&lt;/code&gt;, it prevents ATX-heading recognition;&lt;/li&gt;
&lt;li&gt;inside &lt;code&gt;**bo\u200Bld**&lt;/code&gt;, all three parsers still create &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;, while preserving U+200B in the text node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a blanket “remove invisible characters” rule is both too broad and too weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look-alike punctuation is different punctuation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;＃&lt;/code&gt; is U+FF03, not U+0023. &lt;code&gt;＊&lt;/code&gt; is U+FF0A, not U+002A. All three parsers treated the fullwidth examples as ordinary text.&lt;/p&gt;

&lt;p&gt;NFKC normalization can map some compatibility characters to ASCII, but silently normalizing user source changes data. A safer editor warns, previews the proposed change, and lets the author approve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bidirectional controls require a different review model
&lt;/h2&gt;

&lt;p&gt;UAX #9 distinguishes logical order from display order. Directional controls can change what reviewers see while parsers continue reading logical code-point order.&lt;/p&gt;

&lt;p&gt;With U+202E inside a Markdown URL, none of the three parsers simply discarded it. Marked and commonmark.js percent-encoded it; markdown-it serialized the host differently through IDNA processing. The safe review target is therefore not just the visible source. Inspect the code points, parsed &lt;code&gt;href&lt;/code&gt;, and final resolved URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical ingestion policy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Handle an initial BOM during byte decoding.&lt;/li&gt;
&lt;li&gt;Preserve raw bytes in regression fixtures.&lt;/li&gt;
&lt;li&gt;Flag &lt;code&gt;Cf&lt;/code&gt; characters and non-ASCII look-alike punctuation near Markdown delimiters.&lt;/li&gt;
&lt;li&gt;Show code point, line, column, and Unicode name in diagnostics.&lt;/li&gt;
&lt;li&gt;Preview repairs instead of silently rewriting source.&lt;/li&gt;
&lt;li&gt;Validate links after parsing and URL resolution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Invisible-character bugs are boundary bugs: the decoder, editor, Markdown grammar, HTML serializer, and browser may each make a different decision. Good tooling makes those decisions observable.&lt;/p&gt;

&lt;p&gt;Would you prefer an editor that automatically cleans invisible characters, or one that always preserves the source and only warns?&lt;/p&gt;

</description>
      <category>security</category>
      <category>markdown</category>
      <category>webdev</category>
      <category>unicode</category>
    </item>
    <item>
      <title>Markdown Images Are Network Requests, Not Embedded Assets</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Thu, 20 Aug 2026 06:08:34 +0000</pubDate>
      <link>https://dev.to/mdfold/markdown-images-are-network-requests-not-embedded-assets-5580</link>
      <guid>https://dev.to/mdfold/markdown-images-are-network-requests-not-embedded-assets-5580</guid>
      <description>&lt;p&gt;The line &lt;code&gt;![diagram](../assets/flow.png)&lt;/code&gt; does not embed an image. It creates an image node whose destination still has to be resolved, fetched, allowed, and rendered by another system.&lt;/p&gt;

&lt;p&gt;That distinction explains why a document can parse perfectly and still show broken images—or make network requests its author did not account for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing and URL resolution are separate stages
&lt;/h2&gt;

&lt;p&gt;I fixed the experiment to Node 25.3.0 and Marked 18.0.7 with GFM enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;![diagram](../assets/flow.png "Flow")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// &amp;lt;p&amp;gt;&amp;lt;img src="../assets/flow.png" alt="diagram" title="Flow"&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marked preserves the destination. A browser or publisher later resolves it against a base URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../assets/flow.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.example.test/guides/setup/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
&lt;span class="c1"&gt;// https://docs.example.test/guides/assets/flow.png&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../assets/flow.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.example.test/guides/setup.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
&lt;span class="c1"&gt;// https://docs.example.test/assets/flow.png&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same Markdown, different request. The parser did its job in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six cases that need different policies
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Parser output&lt;/th&gt;
&lt;th&gt;What the host must decide&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;../assets/flow.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;relative &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;base URL and asset copy rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/assets/logo.svg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;root-relative &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;which origin owns the root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://.../pixel.png?doc=42&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absolute &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;remote trust and request privacy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data:image/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;data URL&lt;/td&gt;
&lt;td&gt;sanitizer and CSP policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty alt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt=""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decorative or accessibility defect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty destination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src=""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reject, rewrite, or host behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CommonMark defines image syntax and maps the description to alt text. It does not upload files or promise that a URL exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote images are third-party dependencies
&lt;/h2&gt;

&lt;p&gt;When HTML contains a remote &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;, the browser requests that resource. The remote server observes the request; the page's Referrer Policy controls how much referring-page information is sent. Query parameters can also encode a document or campaign identifier.&lt;/p&gt;

&lt;p&gt;Treat remote images like any other external dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allow only reviewed schemes and hosts;&lt;/li&gt;
&lt;li&gt;proxy or self-host assets when appropriate;&lt;/li&gt;
&lt;li&gt;set a deliberate Referrer Policy;&lt;/li&gt;
&lt;li&gt;restrict image sources with CSP &lt;code&gt;img-src&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;sanitize final HTML after AST transformations;&lt;/li&gt;
&lt;li&gt;never put sensitive identifiers in image URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parser configuration cannot replace those controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative paths need a publishing contract
&lt;/h2&gt;

&lt;p&gt;A stable workflow defines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;where the Markdown file lives;&lt;/li&gt;
&lt;li&gt;what base URL the final page uses;&lt;/li&gt;
&lt;li&gt;how referenced assets are copied;&lt;/li&gt;
&lt;li&gt;whether root-relative paths are allowed;&lt;/li&gt;
&lt;li&gt;what happens when an image is missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are debugging a real document, this &lt;a href="https://mdfold.com/guides/add-images-in-markdown" rel="noopener noreferrer"&gt;Markdown image path guide&lt;/a&gt; provides a concrete path checklist. The important step is to test the published URL, not just the editor preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data URLs trade path stability for other costs
&lt;/h2&gt;

&lt;p&gt;Data URLs avoid a separate file lookup, but they make source files larger and harder to review, prevent independent caching, and may be blocked by a sanitizer or CSP. They are reasonable for small controlled assets, not a universal fix for image portability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the final request graph
&lt;/h2&gt;

&lt;p&gt;My recommended pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;parse Markdown to an AST;&lt;/li&gt;
&lt;li&gt;inspect image destinations;&lt;/li&gt;
&lt;li&gt;resolve them against the declared publication base;&lt;/li&gt;
&lt;li&gt;validate scheme, host, path, and query;&lt;/li&gt;
&lt;li&gt;copy or rewrite managed assets;&lt;/li&gt;
&lt;li&gt;sanitize rendered HTML and apply CSP/referrer policy;&lt;/li&gt;
&lt;li&gt;open the public page and verify actual responses and layout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AST tells you what the author referenced. URL resolution tells you where it points. A browser test tells you what really happened.&lt;/p&gt;

&lt;p&gt;Should Markdown hosts block cross-origin images by default, or preserve compatibility and require each application to opt into a stricter policy?&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/#images" rel="noopener noreferrer"&gt;CommonMark 0.31.2: Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://url.spec.whatwg.org/" rel="noopener noreferrer"&gt;WHATWG URL Standard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img" rel="noopener noreferrer"&gt;MDN: img element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy" rel="noopener noreferrer"&gt;MDN: Referrer Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP" rel="noopener noreferrer"&gt;MDN: Content Security Policy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tooling</category>
      <category>markdown</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Markdown Parser Is Not Your XSS Boundary</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:36:27 +0000</pubDate>
      <link>https://dev.to/mdfold/your-markdown-parser-is-not-your-xss-boundary-2gpk</link>
      <guid>https://dev.to/mdfold/your-markdown-parser-is-not-your-xss-boundary-2gpk</guid>
      <description>&lt;p&gt;A Markdown parser can produce exactly the right HTML and still leave your application exposed to XSS. Parsing answers what the input means. Sanitization decides which parts of that meaning are allowed to reach an HTML sink.&lt;/p&gt;

&lt;p&gt;I tested that boundary with Node.js 25.3.0, Marked 18.0.7, DOMPurify 3.4.12, and jsdom 30.0.1. The important comparison is not a screenshot. It is the HTML before and after sanitization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest useful pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;USE_PROFILES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;SANITIZE_NAMED_PROPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deliberately keeps two responsibilities separate. Marked parses Markdown. DOMPurify applies an allow-list to the HTML structure that will approach the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five cases that expose the boundary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Normal content survives
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Hello&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Safe&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heading and HTTPS link survive both stages. A sanitizer should preserve allowed document structure, not flatten every document to text.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Raw HTML is valid Markdown, not necessarily safe HTML
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marked 18.0.7 returns the element and its event attribute unchanged. DOMPurify returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser did not fail. CommonMark supports raw HTML. The unsafe step would be treating syntactic validity as authorization to insert every attribute.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. URL schemes need their own policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;javascript:alert(1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;click&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanitized HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&amp;gt;&lt;/span&gt;click&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An element allow-list alone is insufficient. URL-bearing attributes need scheme validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Code examples must not be cleaned as attacks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;html
&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser escapes the payload inside &lt;code&gt;pre &amp;gt; code&lt;/code&gt;. A regex that removes attack-looking source before parsing would damage legitimate security documentation. Context has to be established first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. XSS defenses extend beyond &lt;code&gt;script&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"attributes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;SANITIZE_NAMED_PROPS&lt;/code&gt;, the result becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-content-attributes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"user-content-action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This addresses DOM clobbering: attacker-controlled names can interfere with properties that application code expects to resolve normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put sanitization after the last unsafe transform
&lt;/h2&gt;

&lt;p&gt;A practical pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;untrusted Markdown
  -&amp;gt; parser
  -&amp;gt; Markdown/HTML AST transforms
  -&amp;gt; sanitizer
  -&amp;gt; serializer
  -&amp;gt; matching HTML sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rehype-sanitize&lt;/code&gt; documentation makes the ordering rule explicit: sanitize after the last unsafe operation, because a later plugin can reintroduce unsafe properties. DOMPurify's current threat model adds another constraint: do not sanitize and then freely post-process the result. The policy and the sink must stay aligned.&lt;/p&gt;

&lt;p&gt;Turning off raw HTML is a useful reduction in attack surface, but it is not a universal sanitizer. Plugins, link protocols, generated IDs, and later transforms still deserve explicit policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I verify in a conversion workflow
&lt;/h2&gt;

&lt;p&gt;When checking &lt;a href="https://mdfold.com/markdown-to-html" rel="noopener noreferrer"&gt;Markdown to HTML&lt;/a&gt;, I separate three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did normal Markdown preserve the intended structure?&lt;/li&gt;
&lt;li&gt;Did fenced examples remain inert code?&lt;/li&gt;
&lt;li&gt;Is untrusted output safe for &lt;em&gt;this application's&lt;/em&gt; sink and policy?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are conversion checks. The third belongs to the embedding application. A converter producing structurally correct HTML does not automatically promise that arbitrary input is safe to inject into another site's DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat external Markdown as untrusted by default.&lt;/li&gt;
&lt;li&gt;Disable raw HTML where the product does not need it.&lt;/li&gt;
&lt;li&gt;Sanitize the final HTML tree with a maintained allow-list sanitizer.&lt;/li&gt;
&lt;li&gt;Model URL schemes, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and styling capabilities explicitly.&lt;/li&gt;
&lt;li&gt;Do not run arbitrary HTML-mutating plugins after sanitization.&lt;/li&gt;
&lt;li&gt;Test AST shape, rendered HTML, and sanitized HTML separately.&lt;/li&gt;
&lt;li&gt;Pin and update sanitizer versions; security fixes are part of the boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design question I keep coming back to is this: should Markdown renderers disable raw HTML by default, or should they expose it only behind an explicit host-supplied security policy?&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark 0.31.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/markedjs/marked/blob/master/docs/USING_ADVANCED.md" rel="noopener noreferrer"&gt;Marked advanced usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rehypejs/rehype-sanitize" rel="noopener noreferrer"&gt;rehype-sanitize&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cure53/DOMPurify/wiki/Security-Goals-%26-Threat-Model" rel="noopener noreferrer"&gt;DOMPurify security goals and threat model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Markdown Heading Linter on the AST, Not With Regex</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 15 Aug 2026 22:13:26 +0000</pubDate>
      <link>https://dev.to/mdfold/build-a-markdown-heading-linter-on-the-ast-not-with-regex-2o8f</link>
      <guid>https://dev.to/mdfold/build-a-markdown-heading-linter-on-the-ast-not-with-regex-2o8f</guid>
      <description>&lt;p&gt;A heading linter sounds like a one-line regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/^&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;gm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works until a documentation page contains a fenced example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Real heading&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;md
&lt;/span&gt;&lt;span class="gu"&gt;## This is code, not navigation&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex finds two headings. A Markdown parser finds one heading and one code block.&lt;/p&gt;

&lt;p&gt;That difference is why I prefer to put document automation behind an AST boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source -&amp;gt; parser -&amp;gt; mdast -&amp;gt; transformer -&amp;gt; hast -&amp;gt; HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A small, testable plugin
&lt;/h2&gt;

&lt;p&gt;For this experiment I pinned &lt;code&gt;unified@11.0.5&lt;/code&gt;, &lt;code&gt;remark-parse@11.0.0&lt;/code&gt;, &lt;code&gt;remark-rehype@11.1.2&lt;/code&gt;, &lt;code&gt;rehype-stringify@10.0.1&lt;/code&gt;, and &lt;code&gt;unist-util-visit@5.1.0&lt;/code&gt; on Node 25.3.0.&lt;/p&gt;

&lt;p&gt;The plugin has two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;collect headings for a table of contents;&lt;/li&gt;
&lt;li&gt;report a diagnostic when heading depth jumps by more than one level.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;headingAudit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;textOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Heading jumps from h&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;previousDepth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to h&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headings&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser decides what a heading is. The plugin only evaluates heading nodes. This keeps syntax decisions separate from project policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four cases worth keeping in CI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Normal structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;## Install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: two heading nodes, no diagnostic, and two TOC entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inline markup and duplicate labels
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;## *API* Reference&lt;/span&gt;

&lt;span class="gu"&gt;## API Reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first heading does not contain one flat text node. Its children include an &lt;code&gt;emphasis&lt;/code&gt; node, so a robust text extractor must walk descendants. Both visible labels normalize to the same slug; the second needs a stable suffix such as &lt;code&gt;api-reference-1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Valid Markdown that violates a project rule
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;### Internals&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parsing and HTML rendering succeed, but the plugin reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heading jumps from h1 to h3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a useful distinction. A parser error means the syntax cannot be interpreted under the configured grammar. A linter diagnostic means the syntax is valid but conflicts with a documentation policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A heading marker inside code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Real heading&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;md
&lt;/span&gt;&lt;span class="gu"&gt;## Not a heading&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top-level mdast nodes are &lt;code&gt;heading&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. The TOC contains only &lt;code&gt;Real heading&lt;/code&gt;. No special-case regex is needed because the parser has already resolved the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep plugins on the right side of the bridge
&lt;/h2&gt;

&lt;p&gt;Here is the complete processing order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unified&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remarkParse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headingAudit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remarkRehype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addHeadingIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rehypeStringify&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;headingAudit&lt;/code&gt; expects mdast and therefore runs before &lt;code&gt;remarkRehype&lt;/code&gt;. &lt;code&gt;addHeadingIds&lt;/code&gt; writes HTML properties and therefore runs on hast after the bridge. Plugin order is part of the contract, not cosmetic configuration.&lt;/p&gt;

&lt;p&gt;When checking the same samples with a &lt;a href="https://mdfold.com/markdown-to-html" rel="noopener noreferrer"&gt;Markdown-to-HTML converter&lt;/a&gt;, I compare structural output—headings, code blocks, links—not only whether the preview looks plausible. Two pages can look similar while exposing different trees to a TOC generator, sanitizer, or editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AST does not solve
&lt;/h2&gt;

&lt;p&gt;An AST removes a lot of context guessing, but it does not make a pipeline automatically safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two plugins can mutate the same nodes;&lt;/li&gt;
&lt;li&gt;generating a TOC before another plugin rewrites headings creates stale data;&lt;/li&gt;
&lt;li&gt;slug behavior is not defined by CommonMark and differs across platforms;&lt;/li&gt;
&lt;li&gt;raw HTML still needs an explicit trust and sanitization policy;&lt;/li&gt;
&lt;li&gt;syntax-extension plugins can change what inputs the parser accepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule I use is to document every plugin's input tree, output tree, mutations, diagnostics, and ordering constraints. Then test both the tree shape and the serialized output.&lt;/p&gt;

&lt;p&gt;CommonMark 0.31.2 supports the underlying separation: block structure is resolved before inline structure. Unified and remark make that structure available as mdast and provide a plugin pipeline around it. The experiment above only claims the pinned versions and inputs listed here; it is not proof that every Markdown dialect shares the same tree or slug rules.&lt;/p&gt;

&lt;p&gt;Where would you draw the boundary between plugin freedom and syntax stability in a long-lived documentation pipeline?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Markdown Dialects Need a Capability Matrix, Not Just a Name</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:41:50 +0000</pubDate>
      <link>https://dev.to/mdfold/markdown-dialects-need-a-capability-matrix-not-just-a-name-18lg</link>
      <guid>https://dev.to/mdfold/markdown-dialects-need-a-capability-matrix-not-just-a-name-18lg</guid>
      <description>&lt;p&gt;Two tools can both claim to support “Markdown” and still disagree about tables, task lists, bare URLs, and even the HTML element used for strikethrough.&lt;/p&gt;

&lt;p&gt;That is usually not a parser bug. It is a contract problem.&lt;/p&gt;

&lt;p&gt;“Markdown support” often collapses three separate layers into one label:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a base syntax such as CommonMark;&lt;/li&gt;
&lt;li&gt;extensions such as GitHub Flavored Markdown;&lt;/li&gt;
&lt;li&gt;platform-specific configuration, sanitization, and post-processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran a small differential test to make those boundaries concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed environment
&lt;/h2&gt;

&lt;p&gt;Tested on August 15, 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commonmark&lt;/code&gt; 0.31.2&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;markdown-it&lt;/code&gt; 14.3.0, default preset, no third-party plugins&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;marked&lt;/code&gt; 18.0.7, default options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comparison records generated HTML, not screenshots. That distinction matters: two results can look similar while producing different DOM structures for accessibility, export, or later transforms.&lt;/p&gt;

&lt;p&gt;Minimal setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commonmark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;markdownit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;markdown-it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;before ~~deleted~~ after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HtmlRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;markdownit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are versioned defaults, not permanent labels for the libraries. Options and plugins can change the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A pipe table is not base CommonMark
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| 1 | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commonmark 0.31.2&lt;/td&gt;
&lt;td&gt;One paragraph; pipes remain text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown-it 14.3.0&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;table&lt;/code&gt; with &lt;code&gt;thead&lt;/code&gt; and &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marked 18.0.7&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;table&lt;/code&gt; with &lt;code&gt;thead&lt;/code&gt; and &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CommonMark 0.31.2 does not define pipe tables. GFM adds them as an extension.&lt;/p&gt;

&lt;p&gt;When I need a controlled table input for a compatibility test, I use a &lt;a href="https://mdfold.com/markdown-table-generator" rel="noopener noreferrer"&gt;Markdown table generator&lt;/a&gt; to avoid accidental delimiter mistakes, then verify the output with the actual target renderer. Generating valid source is not the same as proving platform compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A pipe inside a code span is a hard case
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| &lt;span class="sb"&gt;`x|y`&lt;/span&gt; | a&lt;span class="se"&gt;\|&lt;/span&gt;b |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the tested markdown-it and Marked defaults, the unescaped pipe inside the code span still splits the row. One cell receives the opening backtick plus &lt;code&gt;x&lt;/code&gt;; the next receives &lt;code&gt;y&lt;/code&gt; plus the closing backtick, instead of one code span.&lt;/p&gt;

&lt;p&gt;GFM’s table examples require escaping that pipe even inside a code span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| &lt;span class="sb"&gt;`x\|y`&lt;/span&gt; | a&lt;span class="se"&gt;\|&lt;/span&gt;b |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why splitting a table row with &lt;code&gt;source.split('|')&lt;/code&gt; is not a parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A table-like shape can fail correctly
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
| 1 | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three parsers emitted a paragraph. Without the delimiter row, none guessed that the author intended a table.&lt;/p&gt;

&lt;p&gt;For editor diagnostics, checking for a real table node is safer than checking whether the source contains pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Strikethrough has two kinds of differences
&lt;/h2&gt;

&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;before ~~deleted~~ after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;commonmark keeps the tildes as text;&lt;/li&gt;
&lt;li&gt;markdown-it emits &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Marked emits &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the boundary case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;before ~not deleted~ after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only Marked recognized strikethrough in this test.&lt;/p&gt;

&lt;p&gt;There are two contracts here: whether the syntax is recognized, and which HTML structure represents it. Screenshot tests can miss the second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Task lists may be plain text or form controls
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [x] shipped
&lt;span class="p"&gt;-&lt;/span&gt; [ ] pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;commonmark and the markdown-it default produced normal list items containing &lt;code&gt;[x]&lt;/code&gt; and &lt;code&gt;[ ]&lt;/code&gt;. Marked produced disabled checkbox inputs and marked the first one as checked.&lt;/p&gt;

&lt;p&gt;Whether a host then makes those boxes interactive is a UI and security decision, not something the Markdown syntax alone can decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Angle-bracket autolinks and bare URLs are different features
&lt;/h2&gt;

&lt;p&gt;All three parsers linked this CommonMark form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nv"&gt;&amp;lt;https://example.com/a_(b)&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Visit https://example.com/a_(b).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only Marked linked the bare URL under the tested defaults. It also excluded the trailing period from the destination.&lt;/p&gt;

&lt;p&gt;markdown-it can add this behavior with &lt;code&gt;linkify&lt;/code&gt;; that option was deliberately left off so the matrix describes the default preset.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Raw HTML policy is not HTML safety
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xmp&amp;gt;&lt;/span&gt;&lt;span class="gs"&gt;**not emphasis**&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/xmp&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;commonmark and Marked preserved the &lt;code&gt;xmp&lt;/code&gt; element. markdown-it escaped the tags because raw HTML is disabled by default. All three still parsed the emphasis inside.&lt;/p&gt;

&lt;p&gt;GFM has a tag-filter extension for names such as &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;iframe&lt;/code&gt;, and &lt;code&gt;xmp&lt;/code&gt;. That filter is not a complete HTML sanitizer. Marked’s own documentation also warns that its output is not sanitized.&lt;/p&gt;

&lt;p&gt;A production pipeline therefore needs two explicit answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What HTML can the parser generate?&lt;/li&gt;
&lt;li&gt;What HTML does the sanitizer allow?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The resulting capability matrix
&lt;/h2&gt;

&lt;p&gt;For these exact versions and defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;commonmark&lt;/th&gt;
&lt;th&gt;markdown-it&lt;/th&gt;
&lt;th&gt;Marked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Angle-bracket autolink&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe tables&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~~x~~&lt;/code&gt; strikethrough&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~x~&lt;/code&gt; strikethrough&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task-list checkboxes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare-URL autolinking&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML enabled by default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complete HTML sanitization&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“No” is not automatically a defect. Disabling raw HTML can be a deliberate safety boundary. markdown-it can gain more syntax through plugins. The important point is that version and configuration belong in the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A more useful declaration
&lt;/h2&gt;

&lt;p&gt;Instead of only storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"flavor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gfm"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;an application could expose something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commonmark-0.31.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strikethrough"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single-or-double-tilde"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"taskList"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extendedAutolink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tagfilter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parse-then-sanitize"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That object is testable, versionable, and much harder to misunderstand than a dialect name.&lt;/p&gt;

&lt;p&gt;The open question is whether Markdown ecosystems should standardize a machine-readable capability manifest—or whether every platform will continue documenting its dialect through examples and surprises.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
  </channel>
</rss>
