<?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: Curtis Zhang</title>
    <description>The latest articles on DEV Community by Curtis Zhang (@curtis_zhang_f52bce500a3e).</description>
    <link>https://dev.to/curtis_zhang_f52bce500a3e</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%2F4114963%2Fb412a668-1b63-4c14-b039-778068c68a84.png</url>
      <title>DEV Community: Curtis Zhang</title>
      <link>https://dev.to/curtis_zhang_f52bce500a3e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/curtis_zhang_f52bce500a3e"/>
    <language>en</language>
    <item>
      <title>Why MindMapAny Generates Markdown Before Building a Mind Map</title>
      <dc:creator>Curtis Zhang</dc:creator>
      <pubDate>Tue, 15 Sep 2026 01:07:09 +0000</pubDate>
      <link>https://dev.to/curtis_zhang_f52bce500a3e/why-mindmapany-generates-markdown-before-building-a-mind-map-3c86</link>
      <guid>https://dev.to/curtis_zhang_f52bce500a3e/why-mindmapany-generates-markdown-before-building-a-mind-map-3c86</guid>
      <description>&lt;p&gt;A mind map editor needs structured data: node IDs, parent IDs, sibling order and source references. That does not mean the language model needs to generate the editor's entire data structure.&lt;/p&gt;

&lt;p&gt;MindMapAny uses an intermediate format: an indented Markdown outline. Application code turns that outline into nodes. The interesting part is the boundary between those two steps—especially what happens when the model produces something slightly wrong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance using the project's implementation as source material.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the model a smaller output contract
&lt;/h2&gt;

&lt;p&gt;Here is an illustrative outline in the format the generation prompt requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Battery Research
- Test conditions
  - Temperature: Cells were tested at room temperature. ^c3
  - Load: Each cell used the same discharge profile. ^c4
- Limitations
  - Sample size: The experiment used twelve cells. ^c8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text describes the content, indentation expresses the hierarchy, and each leaf ends with a source chunk reference. The application assigns IDs and sibling order after parsing.&lt;/p&gt;

&lt;p&gt;That keeps mechanical bookkeeping out of the generation task. The model chooses topics and groups facts. Code decides how those choices become a tree the editor can store.&lt;/p&gt;

&lt;p&gt;The resulting representation still uses structured objects. Markdown is an intermediate language, not the persistence format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parser makes the recovery decisions
&lt;/h2&gt;

&lt;p&gt;The parser scans the outline and maintains a stack of potential parents. When it reaches a node at the same or a shallower level, it removes deeper entries until it finds the appropriate parent.&lt;/p&gt;

&lt;p&gt;This excerpt shows the parent-selection step from the implementation:&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&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;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;level&lt;/code&gt; has already been derived from indentation. The root starts at level &lt;code&gt;-1&lt;/code&gt;, which gives top-level topics a parent without requiring a separate special case for each one.&lt;/p&gt;

&lt;p&gt;The parser also has explicit policies for imperfect output. It skips unrecognized lines, drops duplicate sibling titles after lowercasing them, enforces a depth budget, and limits the number of parsed non-root nodes. It returns warnings for several of these cases.&lt;/p&gt;

&lt;p&gt;Those choices deserve review. Dropping a malformed line may preserve the surrounding outline, but it can also discard information. A parsed tree should not automatically be presented as a complete account of the document.&lt;/p&gt;

&lt;p&gt;For applications where missing a point matters, a useful next step is to turn selected warnings into visible incomplete-output states or regeneration triggers. That is an application policy, not something Markdown supplies for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  One extra space can change the tree
&lt;/h2&gt;

&lt;p&gt;The prompt asks for two spaces per level. The parser nevertheless allows some variation.&lt;/p&gt;

&lt;p&gt;Before creating nodes, it collects the indentation widths used by bullet lines. It sorts those widths and groups neighboring values when the difference is at most one space. Tabs count as two spaces.&lt;/p&gt;

&lt;p&gt;Consider this illustrative input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Test conditions
  - Temperature
   - Load
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two child lines use two and three spaces. This parser assigns them the same level. Without that normalization, an implementation that treats every indentation increase as a new level could make “Load” a child of “Temperature.”&lt;/p&gt;

&lt;p&gt;There is a cost: genuinely distinct adjacent indentation widths can be merged. The grouping is also transitive across neighboring widths. If several widths differ by one space, they can collapse into the same level.&lt;/p&gt;

&lt;p&gt;That is a tolerance decision tailored to this outline format. It is not a general-purpose Markdown parsing rule. A strict parser might reject the input instead; that would be a reasonable choice for a different product.&lt;/p&gt;

&lt;h2&gt;
  
  
  A chunk reference identifies a location, not a truth
&lt;/h2&gt;

&lt;p&gt;The generation prompt asks for references such as &lt;code&gt;^c3&lt;/code&gt;. The parser looks that identifier up in an application-supplied chunk index:&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;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunkIndex&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="nx"&gt;ref&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&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;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;warnings&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="s2"&gt;`unknown chunk ref ^&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ref&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="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;The application restores the source location from that lookup. It does not ask the model to invent the final page number.&lt;/p&gt;

&lt;p&gt;An unknown reference generates a warning when an index is available. The node can still exist without a resolved source. The implementation also does not reject every leaf that lacks a reference, even though the prompt asks for one.&lt;/p&gt;

&lt;p&gt;More fundamentally, a valid chunk ID does not prove that the chunk supports the claim. A model can cite an existing passage and summarize it incorrectly. Reference validation and factual verification are separate checks.&lt;/p&gt;

&lt;p&gt;This distinction matters when writing interface copy. “Open the cited source” describes a capability. “Verified fact” would require additional evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown does not make streaming automatic
&lt;/h2&gt;

&lt;p&gt;A line-oriented format suggests a convenient streaming approach: accumulate text until a newline arrives, parse completed lines, and hold the unfinished tail in a buffer.&lt;/p&gt;

&lt;p&gt;But this parser first examines indentation across the supplied outline. Its final hierarchy can therefore depend on lines that have not arrived yet. The implementation discussed here parses a supplied string; it should not be described as proof of a stable incremental renderer.&lt;/p&gt;

&lt;p&gt;A streaming implementation using this design would need an additional decision: enforce fixed indentation, accept provisional hierarchy and reconcile later, or delay final parent assignment until generation finishes.&lt;/p&gt;

&lt;p&gt;JSON can also be streamed with suitable tooling. The relevant question is which partial results the application is prepared to interpret and revise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured output is still a reasonable alternative
&lt;/h2&gt;

&lt;p&gt;Schema-constrained generation can provide structured model responses. OpenAI's documentation describes schema adherence and supported recursive structures, so a tree is not inherently a reason to avoid that approach. &lt;a href="https://openai.com/index/introducing-structured-outputs-in-the-api/" rel="noopener noreferrer"&gt;Structured Outputs overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The choice here is about where complexity lives. An outline gives the application control over indentation recovery and node construction. A schema gives the model a more explicit object contract. Both still need application checks for useful grouping, missing content and unsupported claims.&lt;/p&gt;

&lt;p&gt;MindMapAny itself uses a JSON object for a separate hierarchy-planning prompt. That task assigns existing node IDs to groups rather than regenerating all the document's content. Different stages can use different output contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions this design raises
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does successful parsing mean the map is complete?
&lt;/h3&gt;

&lt;p&gt;No. The parser can skip lines or drop nodes because of limits. Completion needs its own checks, including the generation result and any parsing warnings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can this parser accept arbitrary Markdown?
&lt;/h3&gt;

&lt;p&gt;No. It recognizes a deliberately limited outline format. Its treatment of headings, indentation and fenced content belongs to that format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Markdown cheaper or more accurate than JSON?
&lt;/h3&gt;

&lt;p&gt;This article provides no benchmark establishing either claim. Compare formats using representative documents, the same quality criteria and the actual models used by the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the boundary you can inspect
&lt;/h2&gt;

&lt;p&gt;The useful part of this Markdown outline pipeline is that its recovery behavior is visible in code. It makes concrete decisions about malformed indentation, duplicate siblings, depth limits and unknown references.&lt;/p&gt;

&lt;p&gt;For another AI application, start by writing down those failure decisions. Then choose an intermediate format that makes them manageable. A valid object is only the beginning of a usable result.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Implementation discussed: &lt;a href="https://mindmapany.com/" rel="noopener noreferrer"&gt;MindMapAny&lt;/a&gt;, an AI mind mapping application. The examples above are illustrative, not benchmark results.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Designing AI Mind Maps You Can Actually Verify</title>
      <dc:creator>Curtis Zhang</dc:creator>
      <pubDate>Fri, 11 Sep 2026 12:37:48 +0000</pubDate>
      <link>https://dev.to/curtis_zhang_f52bce500a3e/designing-ai-mind-maps-you-can-actually-verify-2k0f</link>
      <guid>https://dev.to/curtis_zhang_f52bce500a3e/designing-ai-mind-maps-you-can-actually-verify-2k0f</guid>
      <description>&lt;p&gt;AI can turn a long document into a neat hierarchy in seconds. The harder problem is knowing whether the hierarchy is faithful to the source.&lt;/p&gt;

&lt;p&gt;A useful AI mind map should do more than summarize. It should preserve enough provenance for a reader to move from any important node back to the page, slide, chapter, or timestamp that supports it. This article lays out a practical architecture for building that behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real output is a structure, not a picture
&lt;/h2&gt;

&lt;p&gt;A mind map is often treated as a visual export. For an AI system, however, the graphic should be the last step. The core output is a tree with explicit relationships:&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;SourceRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;slide&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chapter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timestamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&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="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;MindMapNode&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&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;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MindMapNode&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceRef&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 model separates three concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extraction: turning a source into ordered text blocks;&lt;/li&gt;
&lt;li&gt;reasoning: grouping those blocks into topics and subtopics;&lt;/li&gt;
&lt;li&gt;presentation: laying the resulting tree out on a canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these stages separate makes the system easier to test. It also stops presentation choices from silently changing the meaning of the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach provenance before calling the model
&lt;/h2&gt;

&lt;p&gt;Source references are most reliable when they enter the pipeline with the text. A PDF extractor should attach a page number to each block. A slide deck parser should retain the slide index. A video transcript should keep the start time of each caption passage.&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;SourceBlock&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceRef&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;Do not ask the model to reconstruct these locations after summarization. Once multiple passages have been compressed into a short statement, the original position may no longer be recoverable.&lt;/p&gt;

&lt;p&gt;For long inputs, chunking should respect source boundaries where possible. If a chunk spans pages 12 and 13, keep both references. When the model creates a node from that chunk, copy those references into the node instead of inventing a cleaner-looking citation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make unsupported nodes visible
&lt;/h2&gt;

&lt;p&gt;A generated hierarchy may include useful organizing labels such as “Implementation risks” or “Key themes.” Those labels can help readers navigate even when the exact phrase never appeared in the source.&lt;/p&gt;

&lt;p&gt;The interface should not pretend that such a label has direct evidence. A simple rule works well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodes derived from source passages display their references;&lt;/li&gt;
&lt;li&gt;organizational nodes may have no reference;&lt;/li&gt;
&lt;li&gt;a reference-free node is visually distinguishable from a sourced claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more honest than assigning a nearby page number to every node. Provenance is valuable only when its absence is also meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve references through editing
&lt;/h2&gt;

&lt;p&gt;Users will rename nodes, merge branches, delete details, and ask AI to reorganize the map. Provenance should survive these edits.&lt;/p&gt;

&lt;p&gt;For direct text edits, keep the existing references. For a merged node, combine and deduplicate the references of its inputs. For newly generated content, require the transformation to return the IDs of the source nodes it used.&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;function&lt;/span&gt; &lt;span class="nf"&gt;mergeSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MindMapNode&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;SourceRef&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;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&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;SourceRef&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;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;node&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&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;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="k"&gt;of&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;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&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;source&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="s2"&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="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;unique&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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 an edit cannot be traced to existing nodes, mark the result as unsourced. That constraint is better than displaying a confident but false citation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn citations into navigation
&lt;/h2&gt;

&lt;p&gt;A source marker should be an action, not decoration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A PDF page reference can reopen the document at that page.&lt;/li&gt;
&lt;li&gt;A slide reference can focus the corresponding slide.&lt;/li&gt;
&lt;li&gt;A chapter reference can identify the section in an EPUB.&lt;/li&gt;
&lt;li&gt;A video timestamp can open the video at that second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This changes the review workflow. Readers can skim the map first, then verify only the branches that matter. The map becomes an index into the source rather than a replacement for it.&lt;/p&gt;

&lt;p&gt;That is the approach used in &lt;a href="https://mindmapany.com/" rel="noopener noreferrer"&gt;MindMapAny&lt;/a&gt;: document nodes retain page, slide, or chapter locations, while YouTube-derived nodes can link back to timestamps. The product also keeps the generated tree editable so provenance remains useful after the first pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the pipeline at its boundaries
&lt;/h2&gt;

&lt;p&gt;End-to-end output quality is subjective, but the provenance pipeline contains deterministic behavior that can be tested.&lt;/p&gt;

&lt;p&gt;Useful checks include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every extracted block has a valid source reference.&lt;/li&gt;
&lt;li&gt;Every cited node points to a reference that existed in its input chunks.&lt;/li&gt;
&lt;li&gt;Merge operations preserve the union of their source references.&lt;/li&gt;
&lt;li&gt;Deleting a node does not remove references from unrelated branches.&lt;/li&gt;
&lt;li&gt;Timestamp links and page links open the intended location.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These tests will not tell you whether a summary is insightful. They will catch a more dangerous failure: presenting a plausible statement with evidence that does not support it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does every mind-map node need a citation?
&lt;/h3&gt;

&lt;p&gt;No. Organizational labels may be useful without representing a sourced claim. The important rule is to distinguish them clearly from nodes that do carry evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the model generate page numbers or timestamps?
&lt;/h3&gt;

&lt;p&gt;No. Extractors should attach location metadata before model processing. The model should select from known references, not invent new ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when several passages support one node?
&lt;/h3&gt;

&lt;p&gt;Keep all relevant references and deduplicate exact matches. The interface can show the first reference by default and reveal the rest on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can provenance prevent hallucinations?
&lt;/h3&gt;

&lt;p&gt;It cannot prevent every unsupported statement. It makes unsupported or incorrectly sourced output easier to detect and review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The most important design decision in a verifiable AI mind map is to treat provenance as part of the data model. Attach source locations during extraction, carry them through generation and editing, and turn them into navigation in the interface. The result is still a fast visual summary, but it remains connected to the material it represents.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building an Award Flight Finder: What I Learned Turning 365 Date Checks Into One Search</title>
      <dc:creator>Curtis Zhang</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:53:58 +0000</pubDate>
      <link>https://dev.to/curtis_zhang_f52bce500a3e/building-an-award-flight-finder-what-i-learned-turning-365-date-checks-into-one-search-50j6</link>
      <guid>https://dev.to/curtis_zhang_f52bce500a3e/building-an-award-flight-finder-what-i-learned-turning-365-date-checks-into-one-search-50j6</guid>
      <description>&lt;p&gt;Reward flights are one of those problems that look simple until you try to build a useful search experience around them.&lt;/p&gt;

&lt;p&gt;A traveller usually starts with a straightforward question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which dates can I fly this route using miles?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But airline websites often make people check one date at a time. Availability changes frequently, different cabins behave differently, and a seat that appears today may be gone tomorrow. The user does not really want another booking form. They want a fast way to understand a large, changing set of dates.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://mileseat.com/en" rel="noopener noreferrer"&gt;MileSeat&lt;/a&gt;, I learned that the best interface for this problem is not a conventional list of search results. It is a calendar, supported by filters and alerts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the real user job
&lt;/h2&gt;

&lt;p&gt;The obvious product brief is “build an award flight search tool.” That description is too broad to guide many design decisions.&lt;/p&gt;

&lt;p&gt;The actual job is closer to this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check a route across a wide date range.&lt;/li&gt;
&lt;li&gt;See which dates have seats in the desired cabin.&lt;/li&gt;
&lt;li&gt;Compare nearby dates without repeating the search.&lt;/li&gt;
&lt;li&gt;Return later when availability changes.&lt;/li&gt;
&lt;li&gt;Complete the redemption on the relevant airline or loyalty-program website.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This framing matters. MileSeat is not an airline or a travel agent, and it does not complete bookings. Its job is to reduce the discovery work before booking.&lt;/p&gt;

&lt;h2&gt;
  
  
  A year is a better unit than a single date
&lt;/h2&gt;

&lt;p&gt;Most flight forms begin with a departure date. That works well when cash fares are widely available and the user is optimizing price or schedule.&lt;/p&gt;

&lt;p&gt;Award travel is different. Availability itself is often the constraint.&lt;/p&gt;

&lt;p&gt;If someone wants two Business Class award seats between London and New York, asking for one exact date too early can hide the useful answer: there may be no seats on Friday, but there may be seats on Thursday or Sunday.&lt;/p&gt;

&lt;p&gt;That led us toward a 365-day calendar view. A calendar changes the interaction from repeated querying into visual scanning. Users can identify clusters, gaps, and nearby alternatives without submitting dozens of nearly identical searches.&lt;/p&gt;

&lt;p&gt;The broader product lesson is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When flexibility is part of the user's strategy, show the whole decision space before asking them to narrow it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Availability and price are different questions
&lt;/h2&gt;

&lt;p&gt;Another important distinction is the difference between award-seat availability and the final redemption price.&lt;/p&gt;

&lt;p&gt;A seat can be available while the number of points, taxes, fees, or booking rules vary by program. Partner-airline access can differ as well. Trying to compress all of that into a single “best deal” number creates false certainty.&lt;/p&gt;

&lt;p&gt;A clearer flow is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find dates with award seats.&lt;/li&gt;
&lt;li&gt;Filter by airline, route, cabin, and number of seats.&lt;/li&gt;
&lt;li&gt;Confirm the current points price and fees with the program used for booking.&lt;/li&gt;
&lt;li&gt;Complete the redemption on the official site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these stages separate makes the product easier to explain and reduces the risk of users treating an availability signal as a guaranteed booking quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filters should reflect traveller decisions
&lt;/h2&gt;

&lt;p&gt;A technically impressive filter is not necessarily a useful filter. The highest-value controls are the ones that map directly to decisions travellers already make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Origin and destination&lt;/li&gt;
&lt;li&gt;One-way or return&lt;/li&gt;
&lt;li&gt;Economy, Premium Economy, Business, or First Class&lt;/li&gt;
&lt;li&gt;Number of seats&lt;/li&gt;
&lt;li&gt;Airline or loyalty-program context&lt;/li&gt;
&lt;li&gt;Flexible date range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number-of-seats filter is especially important. Showing a date with one seat is not useful to a couple looking for two. The calendar should reflect the party's actual requirement rather than presenting any positive inventory as a match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerts complete the workflow
&lt;/h2&gt;

&lt;p&gt;A search only answers “what is available now.” For popular premium cabins, that is often not enough.&lt;/p&gt;

&lt;p&gt;Availability can return after cancellations or inventory updates, so the product needs a second mode: monitoring. A traveller specifies the route, cabin, date range, and seat count, then receives an alert when a matching result appears.&lt;/p&gt;

&lt;p&gt;This is more than a notification feature. It changes the product from a one-time query tool into an asynchronous workflow:&lt;/p&gt;

&lt;p&gt;Search now → no suitable dates → save criteria → monitor changes → notify when a match appears.&lt;/p&gt;

&lt;p&gt;For products built around volatile data, the same pattern can be useful in many domains: appointments, event tickets, rentals, inventory, and pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would measure
&lt;/h2&gt;

&lt;p&gt;A search product can easily optimize the wrong metric. Page views alone do not tell you whether people found a usable result.&lt;/p&gt;

&lt;p&gt;The more meaningful signals are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Percentage of searches that return at least one matching date&lt;/li&gt;
&lt;li&gt;Time from landing to the first calendar result&lt;/li&gt;
&lt;li&gt;Filter changes after results appear&lt;/li&gt;
&lt;li&gt;Alert creation following a search with no suitable result&lt;/li&gt;
&lt;li&gt;Return visits from an alert&lt;/li&gt;
&lt;li&gt;Outbound clicks to continue the booking process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics follow the user's progress rather than the site's content volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main takeaway
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from building MileSeat is that search UX should match the shape of the underlying decision.&lt;/p&gt;

&lt;p&gt;For award travel, the user is not choosing from a stable list. They are exploring sparse availability across time. A full-year calendar makes that structure visible, while cabin and seat-count filters make it relevant. Alerts handle the fact that the answer can change after the user leaves.&lt;/p&gt;

&lt;p&gt;If you are building any product around limited, changing availability, begin by asking whether users really want a better search box—or a better view of the entire opportunity space.&lt;/p&gt;

&lt;p&gt;You can see the current implementation at &lt;a href="https://mileseat.com/en/reward-flight-finder" rel="noopener noreferrer"&gt;MileSeat&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>startup</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
