<?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: Maxim Berenshtein</title>
    <description>The latest articles on DEV Community by Maxim Berenshtein (@shteynu).</description>
    <link>https://dev.to/shteynu</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%2F1777226%2F60580910-44c9-43a7-a9cc-e9ac842d5f86.jpg</url>
      <title>DEV Community: Maxim Berenshtein</title>
      <link>https://dev.to/shteynu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shteynu"/>
    <language>en</language>
    <item>
      <title>Guardrails for AI-Assisted Development: Skills, Gates, Hooks and Mutation Tests</title>
      <dc:creator>Maxim Berenshtein</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:09:10 +0000</pubDate>
      <link>https://dev.to/shteynu/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests-171</link>
      <guid>https://dev.to/shteynu/guardrails-for-ai-assisted-development-skills-gates-hooks-and-mutation-tests-171</guid>
      <description>&lt;p&gt;When a model writes the code, writing it stops being the expensive part. Proving you didn't break something nobody was looking at becomes the expensive part.&lt;/p&gt;

&lt;p&gt;That sentence took me a few months of building to arrive at, and once I had it, most of my tooling decisions stopped being arguments.&lt;/p&gt;

&lt;p&gt;This is a walkthrough of the verification layer in a real project of mine: a wellbeing analytics platform with a Next.js core and a Python AI service. Four agent skills, sixteen repository gates, a hook that runs them at edit time, and a narrow mutation testing pilot. Everything here is running code, and I'll be specific about what each piece does not catch, because that turns out to be the more useful half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why conventions in a README don't survive
&lt;/h2&gt;

&lt;p&gt;Let me start with the failure that convinced me.&lt;/p&gt;

&lt;p&gt;My repo has an &lt;code&gt;AGENTS.md&lt;/code&gt; saying that &lt;code&gt;.agents/skills/&lt;/code&gt; is the single source of agent instructions. A month earlier, &lt;code&gt;GEMINI.md&lt;/code&gt; had said in plain prose: don't create client-local copies of these skills.&lt;/p&gt;

&lt;p&gt;A 3,442-byte partial copy of one skill showed up in &lt;code&gt;.gemini/skills/&lt;/code&gt; anyway. Two weeks later there was still an empty leftover directory next to it.&lt;/p&gt;

&lt;p&gt;Here's the part that matters: &lt;code&gt;.claude/&lt;/code&gt; and &lt;code&gt;.gemini/&lt;/code&gt; are in &lt;code&gt;.gitignore&lt;/code&gt;. The copy was invisible in every diff and every code review. Not missed by a reviewer. Invisible to the mechanism of reviewing.&lt;/p&gt;

&lt;p&gt;Two copies both claiming to be canonical, drifting apart with nothing comparing them. Prose has no enforcement mechanism. That's not a discipline problem, it's a category problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: a skill tree with exactly one root
&lt;/h2&gt;

&lt;p&gt;Skills are markdown files that tell an agent how to work in this repository; mine cover product invariants, session/branch state, verification, and the gates themselves. The design has four rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One canonical location.&lt;/strong&gt; &lt;code&gt;.agents/skills/&lt;/code&gt; and nowhere else. There is no list of skills in any config file. The canonical set is the directories on disk:&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;skillNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SKILLS_ROOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;withFileTypes&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;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;())&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;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A folder appears, a skill exists. This is why a copy elsewhere is dangerous rather than merely redundant: if the filesystem is the source of truth, any folder with a familiar name becomes a second source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing in the entrypoint.&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt; carries a plain markdown list mapping tasks to skills:&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; Verifying, testing, proving a fix, checking readiness, reviewing evidence, or
  about to claim a substantive change is complete →
  &lt;span class="sb"&gt;`.agents/skills/shalomut-verification/SKILL.md`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother if some clients discover skills automatically? Because a client with discovery and a human reading the file directly must arrive at the same rules. Discovery changes how a file is found, never what it says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reading map inside each skill.&lt;/strong&gt; Every skill has a &lt;em&gt;How to read this skill&lt;/em&gt; section splitting its own sections into "always in force" and "open when this condition holds." That map, not the entrypoint, decides how much of a skill a task needs. The rule next to it: loading a section is cheap, skipping a rule is not, so when a condition is ambiguous, open the section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A check that all of the above holds.&lt;/strong&gt; &lt;code&gt;npm run lint:skills&lt;/code&gt; refuses: a skill copy outside the canonical root (including an empty directory named after one), a broken or orphaned link in &lt;code&gt;references/&lt;/code&gt;, a &lt;code&gt;##&lt;/code&gt; section the reading map never classifies, and any client entrypoint that routes nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: gates, or how a convention becomes unignorable
&lt;/h2&gt;

&lt;p&gt;A gate is a script that reads the repository and exits non-zero when it finds a violation. Nothing executes. No arguments get fuzzed, no mutants get generated. It reads — not just paths, but anything knowable without running the program: where a repository gets constructed (&lt;code&gt;lint:composition&lt;/code&gt;), what a response body contains (&lt;code&gt;lint:error-bodies&lt;/code&gt;), whether a document still agrees with the configuration it quotes (&lt;code&gt;lint:doc-numbers&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually deserves a gate
&lt;/h3&gt;

&lt;p&gt;This is the decision rule I'd most want to hand to someone starting out, and it took me a while to get right:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind of rule&lt;/th&gt;
&lt;th&gt;Where it belongs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lives inside a module, expressed through its API&lt;/td&gt;
&lt;td&gt;An ordinary test. No gate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About the shape of the repository: what may be imported, where a literal may stand, which interpreter runs, where skills live&lt;/td&gt;
&lt;td&gt;A gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editable source with a derived copy&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;--check&lt;/code&gt; mode on the generator, not a second equality test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A machine cannot judge it: is this architecture right, is this audit record genuinely closed&lt;/td&gt;
&lt;td&gt;Prose. And say so in the gate's doc comment, so a green gate never reads as proof of what it never checked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tell that you need a gate: the violation is silent. Tests green, build passing, reviewer sees nothing, and the rule is already broken. If a violation fails the suite anyway, a test is enough and a gate is overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  One gate in full
&lt;/h3&gt;

&lt;p&gt;The audit of my repo counted 21 route handlers interpolating a raw &lt;code&gt;error.message&lt;/code&gt; into what they sent back. On &lt;code&gt;/api/auth/login&lt;/code&gt; that went to anyone. Every one was written by somebody being helpful.&lt;/p&gt;

&lt;p&gt;The naive check is a regex for &lt;code&gt;error.message&lt;/code&gt;. I wrote that first. It let &lt;code&gt;(error as Error).message&lt;/code&gt; straight through, and the audit itself had missed &lt;code&gt;error?.message&lt;/code&gt; for the same reason.&lt;/p&gt;

&lt;p&gt;So the rule became two rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a &lt;code&gt;catch&lt;/code&gt; in a route handler binds the name &lt;code&gt;error&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the argument of &lt;code&gt;NextResponse.json(...)&lt;/code&gt; never mentions it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule 1 exists to make rule 2 complete. Refusing the whole identifier makes the spelling irrelevant, and an identifier rule is only as good as the identifier, so rule 1 stops a handler slipping past with &lt;code&gt;catch (e)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The implementation is not a regex either. It's three passes:&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="c1"&gt;// 1. Strip strings and comments, but KEEP ${...} inside templates.&lt;/span&gt;
&lt;span class="c1"&gt;//    Without stripping, { error: 'Internal error' } fails on its own wording.&lt;/span&gt;
&lt;span class="c1"&gt;//    Stripping templates whole would let `failed: ${error}` through,&lt;/span&gt;
&lt;span class="c1"&gt;//    and that is the leak itself.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripTextAndComments&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="cm"&gt;/* hand-written scanner */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Find the argument region of a literal call by counting parens.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;argumentRegions&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;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NextResponse.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="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Only now, a pattern. A property access is fine (`produced.error` is our&lt;/span&gt;
&lt;span class="c1"&gt;//    own field). A spread is not: { ...error } puts the whole thing in the body.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`(?&amp;lt;![&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;w$])(?&amp;lt;!(?&amp;lt;!&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.)&lt;/span&gt;&lt;span class="se"&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;BINDING&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;b`&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;readable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two places in the codebase used the name &lt;code&gt;error&lt;/code&gt; for the product's own refusal wording. They got renamed rather than exempted. An exemption list is a place the next leak hides.&lt;/p&gt;

&lt;p&gt;And the blind spot, stated in the doc comment rather than discovered later: a body assembled into a variable and passed by name goes unnoticed. That gap is accepted, because the alternative is parsing TypeScript in a fitness check and every occurrence the audit found was a literal at the call site. With the follow-up that matters: if that stops being true, this needs a parser, not a wider regular expression.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shape every gate shares
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/check-&amp;lt;subject&amp;gt;.mjs&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * &amp;lt;The rule. Why it exists — name the actual incident. What this cannot see.&amp;gt;
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findViolations&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;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* pure, returns string[] */&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;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* reads files, prints, process.exit(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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;fileURLToPath&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure exported functions so the test can import them. &lt;code&gt;main()&lt;/code&gt; guarded so importing doesn't run it. And on success, a line saying how much was checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error-body fitness check passed: 47 route handlers, no caught error in a response body.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A silent success is indistinguishable from a check that read nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wiring, and why every gate has its own test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"lint:error-bodies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test scripts/check-error-bodies.test.mjs &amp;amp;&amp;amp; node scripts/check-error-bodies.mjs"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"verify:core"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run lint:literals &amp;amp;&amp;amp; ... &amp;amp;&amp;amp; npm run typecheck &amp;amp;&amp;amp; npm test &amp;amp;&amp;amp; npm run lint &amp;amp;&amp;amp; npm run build"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate's test runs before the gate. That order is the whole point: prove the check can fail, then trust it. A check whose pattern never matches anything is worse than no check, because it manufactures confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gates that guard the gates
&lt;/h3&gt;

&lt;p&gt;The cheapest way to fix a red gate is to delete it from the chain. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lint:gate-inventory refuses:
  - a gate that isn't a step of verify:core
  - a gate missing from the inventory table
  - an inventory row with no gate behind it
  - a lint:* command that doesn't run its own test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, the whole safety system rests on good faith, which means it doesn't rest on anything.&lt;/p&gt;

&lt;p&gt;Same principle inside the skills: the guardrails skill says outright that weakening a check, adding a file to an exemption list or narrowing its scope is a change to the rule, not a fix to the build — do it deliberately, update the doc comment and the tests on both sides. And: never bring a gate's test in line with current behaviour to make things green. The gate's test is the record of the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: the hook, so the agent hears it now
&lt;/h2&gt;

&lt;p&gt;CI is the record. But CI tells you at 11:00 about something written at 10:00, by which point it's buried under later edits made on top of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt; is the one file under &lt;code&gt;.claude/&lt;/code&gt; that Git tracks — &lt;code&gt;.gitignore&lt;/code&gt; un-ignores exactly it:&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;"hooks"&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;"PostToolUse"&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;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit|NotebookEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node scripts/gate-hook.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&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;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;&lt;code&gt;gate-hook.mjs&lt;/code&gt; holds no rules. It takes the path of the file just written, maps it to the gates that could care, and runs those:&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;RULES&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;gate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lint:error-bodies&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;when&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^src&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;app&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api&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;route&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;ts$/&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;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On refusal it exits with code 2, which feeds stderr back to the agent, and the message points to the relevant skill section and explicitly says not to weaken the check or rewrite its test to make it pass. That last line is defence against the first thing an agent tries.&lt;/p&gt;

&lt;p&gt;Three limits, all stated in the file itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;15 of 16 gates are mapped.&lt;/strong&gt; &lt;code&gt;lint:literals&lt;/code&gt; is deliberately out: its Python half needs an interpreter from &lt;code&gt;.venv&lt;/code&gt;, and a missing local environment must not read as a violation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mapping is narrow on purpose.&lt;/strong&gt; A gate that fires on every edit is a gate people learn to wait out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs in one client only.&lt;/strong&gt; A rule that lives in a hook doesn't exist for Copilot, for Gemini, or for a person in a terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One implementation detail worth stealing: the hook runs only the check half of each command, not the &lt;code&gt;node --test&lt;/code&gt; half. The first version buried a one-line violation under sixty lines of passing TAP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: tests, and proving the tests are worth anything
&lt;/h2&gt;

&lt;p&gt;Gates check shape. Tests check behaviour. Neither checks whether the tests are any good. That's mutation testing: deliberately corrupt the code, see whether tests notice. Mine runs on two files — the AI contract validator and the scoring bands — and the full run isn't a CI gate: the score moves when a function migrates between files or a test file enters the list, which has nothing to do with test strength.&lt;/p&gt;

&lt;p&gt;The checks around it keep that number meaningful. &lt;code&gt;lint:mutation-config&lt;/code&gt; re-derives &lt;code&gt;tap.testFiles&lt;/code&gt; from the repo, because a missing entry doesn't lower the score honestly — it reports as survived a mutant a real test would have killed. &lt;code&gt;lint:contract-refusals&lt;/code&gt; demands a negative-test suite for every contract version; it proves a suite exists, not that it is complete. And when a product rule leaves a mutated file, &lt;code&gt;mutate&lt;/code&gt; follows it in the same change, or the rule silently drops out of measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: how much checking is enough
&lt;/h2&gt;

&lt;p&gt;This is the part I'd defend hardest, and the part most teams skip.&lt;/p&gt;

&lt;p&gt;My verification skill carries a selection matrix: rows are areas a diff touched, cells are the mandatory minimum. The principle: choose the smallest set of checks that proves the changed behaviour, then widen in proportion to risk.&lt;/p&gt;

&lt;p&gt;A few rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Changed&lt;/th&gt;
&lt;th&gt;Mandatory minimum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Markdown / skills only&lt;/td&gt;
&lt;td&gt;Frontmatter and links, &lt;code&gt;git diff --check&lt;/code&gt;, &lt;code&gt;lint:skills&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A repository gate&lt;/td&gt;
&lt;td&gt;Its paired test and the gate, &lt;code&gt;lint:gate-inventory&lt;/code&gt;. Weakening the check = changing the rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;src/app/api&lt;/code&gt;, services&lt;/td&gt;
&lt;td&gt;Nearest tests, then &lt;code&gt;npm test&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI contract&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lint:contract-refusals&lt;/code&gt;, contract/registry/client tests, Python tests, local boundary E2E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python dependencies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lint:python-deps&lt;/code&gt;; production runs 3.11 and dev machines usually don't have it, so the one real proof is &lt;code&gt;docker build&lt;/code&gt; plus the suite inside that image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth or secrets&lt;/td&gt;
&lt;td&gt;Unauthorized / missing-secret / tenant-isolation tests plus a security-focused diff review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Running everything for a docs edit is the same mistake as running one small test for an auth change. Both look like diligence. Neither is.&lt;/p&gt;

&lt;p&gt;One check belongs to no row: &lt;code&gt;typecheck&lt;/code&gt; is mandatory for any &lt;code&gt;.ts&lt;/code&gt; change. &lt;code&gt;build&lt;/code&gt; types only the application graph, &lt;code&gt;lint&lt;/code&gt; doesn't check types at all, and &lt;code&gt;npm test&lt;/code&gt; runs through &lt;code&gt;tsx&lt;/code&gt;, which strips types without checking them. A green test run tells you nothing about types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Say what counts as evidence
&lt;/h3&gt;

&lt;p&gt;With an agent this stops being pedantry. Models report affirmatively by default, so the rules are written down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Label the context — local, test, or deployed — and never mix them silently.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run dev&lt;/code&gt; starts a runtime. It is not evidence.&lt;/li&gt;
&lt;li&gt;A mock MCP server is not proof of real transport, and a wrapper that delegates to the same command is not a second piece of evidence.&lt;/li&gt;
&lt;li&gt;Don't promise repo-wide mutation coverage. Separate killed / survived / no-coverage / runtime-error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verify:core&lt;/code&gt; is an &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chain: it stops at the first failure. The steps after it did not run — don't report them as passed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Every rule carries its incident
&lt;/h2&gt;

&lt;p&gt;The habit I'd transplant into any codebase regardless of AI: each check's doc comment names what broke, with a date.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fonts.&lt;/strong&gt; Until 2026-08-12 the build downloaded five &lt;code&gt;.woff2&lt;/code&gt; files from a Google host; when a runner got a stale stylesheet, all five 404'd and the build failed with a message mentioning neither fonts nor the network. The same commit built cleanly in the neighbouring job, so the gate had become a coin toss instead of a red light.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant chokepoint.&lt;/strong&gt; A context loader recorded the visit from the request rather than the answer, so an administrator reading the only tenant left no audit row at all, and no test failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corollary is a cleanup rule: a rule with no incident behind it is a candidate for deletion. Either it isn't needed, or nobody remembers what it defends — and the first person it inconveniences will remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gates read text.&lt;/strong&gt; A determined rename or a dynamic call walks around them. They defend against accidental violation, not adversarial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presence checks don't check agreement.&lt;/strong&gt; A client adapter passes if it contains the string &lt;code&gt;.agents/skills&lt;/code&gt;, even if the surrounding sentence says the opposite. Stated in the code; it stays a review question.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;None of this judges whether an architectural decision is right.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs.&lt;/strong&gt; Sixteen gates is sixteen scripts plus sixteen tests plus an inventory plus a gate guarding all of it. On a two-week prototype that's absurd. In a regulated domain where the question is "prove this data couldn't leak," it's the cheapest answer I know.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you're starting tomorrow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Take the last silent bug you shipped. Write the smallest script that would have refused it. One rule.&lt;/li&gt;
&lt;li&gt;Give it a test that proves it can fail, and wire the test to run first.&lt;/li&gt;
&lt;li&gt;Put it in one verify chain with everything else, and put that chain in CI.&lt;/li&gt;
&lt;li&gt;Write the incident in the doc comment, with the date, and write down what the check can't see.&lt;/li&gt;
&lt;li&gt;Only then hook it into your agent's edit loop, so the refusal arrives while the context is still warm.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 through 4 work with no AI involved at all. Step 5 is just the feedback arriving earlier. That ordering is deliberate: the reason any of this holds is that the rules live where every client and every human reads them, and the agent integration is the last mile rather than the foundation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>What Angular Actually Re-renders When a Model Streams Your UI</title>
      <dc:creator>Maxim Berenshtein</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:22:36 +0000</pubDate>
      <link>https://dev.to/shteynu/what-angular-actually-re-renders-when-a-model-streams-your-ui-1fph</link>
      <guid>https://dev.to/shteynu/what-angular-actually-re-renders-when-a-model-streams-your-ui-1fph</guid>
      <description>&lt;p&gt;When a model streams a UI into an Angular app, the spec behind the screen changes many times per second. Two questions follow from that, and both are easy to answer by guessing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Angular has no &lt;code&gt;render(node)&lt;/code&gt; function that calls itself. So how does it draw a tree when nobody knows how deep it goes?&lt;/li&gt;
&lt;li&gt;On every patch, does Angular redraw everything? And when one deep child changes, how does Angular even know?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I measured instead of guessing, and read Angular's source where the answer wasn't obvious.&lt;/p&gt;

&lt;p&gt;The examples come from &lt;a href="https://github.com/shteynu/ngx-json-render" rel="noopener noreferrer"&gt;ngx-json-render&lt;/a&gt;, an Angular renderer for json-render specs. If the format is new to you, &lt;a href="https://dev.to/shteynu/generative-ui-in-angular-the-llm-streams-a-json-spec-you-render-real-components-4lgf"&gt;my first article&lt;/a&gt; explains it. But the mechanics below are plain Angular: &lt;code&gt;NgComponentOutlet&lt;/code&gt;, &lt;code&gt;@for&lt;/code&gt;, dependency injection and signals.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Angular draws a tree it has never seen
&lt;/h2&gt;

&lt;p&gt;A spec describes a tree as a flat map of elements:&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;"root"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"elements"&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;"card"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"children"&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="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"list"&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;"title"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&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;span class="nl"&gt;"list"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"List"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"children"&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="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b"&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;"a"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&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;span class="nl"&gt;"b"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&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;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;There is no recursive function in the renderer. The recursion is made of three ordinary Angular tools that call each other &lt;strong&gt;through templates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool 1: &lt;code&gt;NgComponentOutlet&lt;/code&gt;&lt;/strong&gt; picks a component class at runtime. One small component does only that: take a key, find the element, create the matching component.&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jr-element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NgComponentOutlet&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    @if (component()) {
      &amp;lt;ng-container *ngComponentOutlet="component(); injector: injector" /&amp;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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JrElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;elementKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// "Card" → CardComponent, looked up in the registry&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tool 2: &lt;code&gt;@for&lt;/code&gt;&lt;/strong&gt; renders the children, one &lt;code&gt;jr-element&lt;/code&gt; per child key:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jr-children&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;JrElement&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    @for (key of childKeys(); track key) {
      &amp;lt;jr-element [elementKey]="key" /&amp;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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JrChildren&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tool 3: your own component&lt;/strong&gt; puts &lt;code&gt;&amp;lt;jr-children /&amp;gt;&lt;/code&gt; wherever its children should go:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;JrChildren&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;section class="card"&amp;gt;&amp;lt;jr-children /&amp;gt;&amp;lt;/section&amp;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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardComponent&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put them together and you get the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jr-element "card"  → app-card
  jr-children      → jr-element "title" → app-text
                   → jr-element "list"  → app-list
                       jr-children      → jr-element "a" → app-text
                                        → jr-element "b" → app-text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody calls anything recursively. Components create components, and the tree grows as deep as the data says.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does a child know where it is?
&lt;/h3&gt;

&lt;p&gt;The parent passes only one input: the key. Everything else travels through &lt;strong&gt;dependency injection&lt;/strong&gt;. Each &lt;code&gt;jr-element&lt;/code&gt; creates a small injector for the component it renders:&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;injector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Injector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;providers&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;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RENDER_CONTEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderContext&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// props, events&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RENDER_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;             &lt;span class="c1"&gt;// my ancestry&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Injector&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;Your component calls &lt;code&gt;injectRenderContext()&lt;/code&gt; and gets its props as signals. The next &lt;code&gt;jr-element&lt;/code&gt; down injects the parent's &lt;code&gt;RENDER_PATH&lt;/code&gt; and adds itself to it.&lt;/p&gt;

&lt;p&gt;That's the part I find elegant: &lt;strong&gt;Angular's injector tree plays the role of the call stack.&lt;/strong&gt; In a recursive function, each call sees its caller's arguments. Here, each level sees its parent's providers.&lt;/p&gt;

&lt;p&gt;And since every component was compiled ahead of time, the runtime never compiles anything. It only decides which already-compiled component to create next.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost of recursion that lives in Angular
&lt;/h3&gt;

&lt;p&gt;Angular creates and checks views with plain recursive JavaScript functions. And each level of the JSON becomes several nested Angular views: &lt;code&gt;jr-element&lt;/code&gt;, its &lt;code&gt;@if&lt;/code&gt; block, your component, &lt;code&gt;jr-children&lt;/code&gt;, its &lt;code&gt;@for&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;the depth of the JSON becomes the depth of the JavaScript call stack.&lt;/strong&gt; That has a price: two elements that name each other as children are enough to run the stack out and kill the tab. Where that recursion has to stop is a story of its own.&lt;/p&gt;




&lt;h2&gt;
  
  
  Streaming: does Angular redraw everything?
&lt;/h2&gt;

&lt;p&gt;With streaming, the model doesn't send a finished spec. It sends small patches, one per line:&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/elements/card"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"props"&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="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"list"&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/elements/title"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hello"&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/elements/a/props/content"&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="s2"&gt;"A2"&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;Each patch produces a &lt;strong&gt;new spec object&lt;/strong&gt;, which reaches the renderer through a signal. The natural fear: new object at the top means every component redraws, dozens of times per second.&lt;/p&gt;

&lt;p&gt;I measured it instead of guessing. The spec above, with counters in every component: how many components were created, and how many component templates ran again.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the patch changed&lt;/th&gt;
&lt;th&gt;Components created&lt;/th&gt;
&lt;th&gt;Templates re-run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First render, 5 elements&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A prop of a deep leaf (&lt;code&gt;a&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (only &lt;code&gt;a&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A new child key in &lt;code&gt;list&lt;/code&gt;, element not arrived yet&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;list&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;That child's element arrives&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 (the new child)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New spec object, nothing inside changed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So no, it doesn't redraw everything. Four ideas make that work, and the first one isn't about Angular at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 1: a patch replaces only what it touches
&lt;/h3&gt;

&lt;p&gt;The patch doesn't mutate the old spec, and it doesn't deep-copy it either. It copies only the objects on the path to the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before:  spec  → elements  → card, title, list, a,  b
patch:   replace /elements/a/props/content
after:   spec' → elements' → card, title, list, a', b
                              (same objects)   (new)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;card&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are &lt;strong&gt;the same objects in memory&lt;/strong&gt; as before. So "did this element change?" becomes a single &lt;code&gt;===&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 2: each element watches only its own piece
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;jr-element&lt;/code&gt; reads its element through a &lt;code&gt;computed&lt;/code&gt;:&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elementKey&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the spec changes, all these computeds run again, and each one is a single lookup. But a &lt;code&gt;computed&lt;/code&gt; only reports a change when the new value is a different object. For &lt;code&gt;card&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; the lookup returns the same object, so the change stops right there. Nothing that depends on them runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 3: Angular refreshes views, not the whole tree
&lt;/h3&gt;

&lt;p&gt;This is the answer to "how does Angular know a deep child changed?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't search for it.&lt;/strong&gt; In modern Angular every view has its own reactive consumer, which remembers exactly which signals its template read. I checked this in Angular 21's source. When one of those signals changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;that view is marked dirty;&lt;/li&gt;
&lt;li&gt;its parents only get a flag saying "a child below needs checking". Their own templates don't run (&lt;code&gt;markAncestorsForTraversal&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;before refreshing a dirty view, Angular checks whether the signals it read really got new values, and skips the view if not (&lt;code&gt;consumerPollProducersForChange&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So on a deep change, Angular walks down the path to that one view, runs its template, and updates only the DOM bindings whose values actually differ. The parents stay untouched.&lt;/p&gt;

&lt;p&gt;In a zoneless app, and zoneless is the default for new apps since Angular 21, there's nothing else that could trigger a check. The signal notification is what schedules the update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea 4: new children don't rebuild the list
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@for (key of childKeys(); track key)&lt;/code&gt; compares children &lt;strong&gt;by key&lt;/strong&gt;. When &lt;code&gt;list&lt;/code&gt; gets a third key, Angular keeps the two existing views and creates one new one.&lt;/p&gt;

&lt;p&gt;Streaming adds a twist: a parent often lists a child before the child's element has arrived. That's fine. The &lt;code&gt;jr-element&lt;/code&gt; for the key is created and renders nothing. When the element arrives, only that one component is created. That's rows 3 and 4 of the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  What isn't free
&lt;/h3&gt;

&lt;p&gt;To be fair about the limits of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every patch still does a small amount of work per element.&lt;/strong&gt; Each element's computed runs once, and Angular walks the tree to find dirty views. It's cheap, but it grows with the size of the spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data is not structure.&lt;/strong&gt; That one deserves its own section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the data changes, not the UI
&lt;/h2&gt;

&lt;p&gt;Specs also carry data. A text can read &lt;code&gt;{ "$state": "/user/name" }&lt;/code&gt;, a list can repeat over &lt;code&gt;/todos&lt;/code&gt;. A user types into a field, or the stream sends a new value, and the &lt;em&gt;data&lt;/em&gt; changes while the structure stays the same.&lt;/p&gt;

&lt;p&gt;The state store itself does this well. It copies only the path to the changed value, and writing the same value again does nothing at all.&lt;/p&gt;

&lt;p&gt;The renderer layer is where precision gets lost today. Every element resolves its props against the current state, and resolving always builds a &lt;strong&gt;new props object&lt;/strong&gt;, even when every value inside it is the same. A new object counts as a change, so Angular re-runs every component's template. The DOM still changes only where a value really differs, but the template work covers the whole tree.&lt;/p&gt;

&lt;p&gt;I measured it on a small spec: a card, a static text, a text bound to &lt;code&gt;/user/name&lt;/code&gt;, and a todo list. Then I tried one small change: treat two props objects as equal when all their values are the same (a shallow compare on the props &lt;code&gt;computed&lt;/code&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What changed in the data&lt;/th&gt;
&lt;th&gt;Templates re-run today&lt;/th&gt;
&lt;th&gt;With a shallow compare on props&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/user/name&lt;/code&gt; (read by one text)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6 of 6&lt;/strong&gt;, the static text included&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (only that text)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same value written again&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A todo appended to &lt;code&gt;/todos&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;7 of 7&lt;/strong&gt; (and 1 new component)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (the new todo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same idea as with patches: &lt;strong&gt;keep identity, and "nothing changed" becomes cheap.&lt;/strong&gt; The fix just has to restore identity one step later, after props are resolved.&lt;/p&gt;

&lt;p&gt;When I wrote this, it was an experiment, not a shipped change. It isn't free of trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every element still &lt;em&gt;re-resolves&lt;/em&gt; its props on every data change. The compare saves Angular's work, not that JavaScript. The next step would be remembering which state paths each element reads, and skipping the rest.&lt;/li&gt;
&lt;li&gt;Props that resolve to nested objects or arrays are new objects every time, so the shallow compare doesn't help them. It's no worse than today, though.&lt;/li&gt;
&lt;li&gt;An external store that mutates objects in place could be shown stale, because the reference stays the same while the content changes. That case is exactly why the renderer treats every store update as a change today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Update (0.5.1):&lt;/strong&gt; this is now shipped. A resolved element keeps its previous object when its props hold the same values, and the right-hand column of the table is what the release's tests measure. Three details differ from the experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With the built-in store, object and array values compare by reference. The store copies every path it writes, so a &lt;code&gt;{ "$state": "/todos" }&lt;/code&gt; prop keeps its identity until &lt;code&gt;/todos&lt;/code&gt; itself changes. Literal objects with expressions inside, and &lt;code&gt;$computed&lt;/code&gt; results, are still rebuilt on every resolution.&lt;/li&gt;
&lt;li&gt;With an external &lt;code&gt;store&lt;/code&gt;, object and array values always count as changed, because such a store may write into its snapshot in place. Primitives still compare by value.&lt;/li&gt;
&lt;li&gt;Elements with a &lt;code&gt;$bindState&lt;/code&gt; or &lt;code&gt;$bindItem&lt;/code&gt; prop keep the old behaviour. Their input can hold text that state hasn't seen yet, and the component's props effect has to run to put the DOM right.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How Angular recurses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no recursive function: &lt;code&gt;NgComponentOutlet&lt;/code&gt; → your component → &lt;code&gt;@for&lt;/code&gt; → &lt;code&gt;NgComponentOutlet&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The injector tree works as the call stack. Each level sees its parent's context.&lt;/li&gt;
&lt;li&gt;JSON depth becomes JavaScript stack depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What streaming re-renders&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patches keep unchanged objects, so "unchanged" is a &lt;code&gt;===&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each element watches only its own piece through a &lt;code&gt;computed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Angular doesn't search for changes. It refreshes only views whose signals really changed, and walks through the parents without re-running them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@for ... track key&lt;/code&gt; keeps existing children and creates only new ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What data changes re-render&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before 0.5.1, every template. The DOM still changed only where values differed.&lt;/li&gt;
&lt;li&gt;A shallow compare on resolved props brought it down to the one affected element in my test, and 0.5.1 ships it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fast streaming in Angular isn't about rendering fast. It's about &lt;strong&gt;keeping object identity&lt;/strong&gt;, so that "nothing changed" is a &lt;code&gt;===&lt;/code&gt; and Angular can skip almost everything.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/shteynu/ngx-json-render" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/ngx-json-render" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://shteynu.github.io/ngx-json-render/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>performance</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Dynamic Rendering in Angular Is Easy. Trusting Dynamic UI Is Not.</title>
      <dc:creator>Maxim Berenshtein</dc:creator>
      <pubDate>Thu, 03 Sep 2026 12:25:37 +0000</pubDate>
      <link>https://dev.to/shteynu/dynamic-rendering-in-angular-is-easy-trusting-dynamic-ui-is-not-29mc</link>
      <guid>https://dev.to/shteynu/dynamic-rendering-in-angular-is-easy-trusting-dynamic-ui-is-not-29mc</guid>
      <description>&lt;p&gt;Dynamic rendering in Angular sounds like a fairly narrow technical problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I don't know which component I need until runtime.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Angular already gives us several good tools for that.&lt;/p&gt;

&lt;p&gt;But there is a big difference between dynamically choosing a component and dynamically constructing an entire UI from a runtime specification.&lt;/p&gt;

&lt;p&gt;And that difference becomes especially important with Server-Driven UI and Generative UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;ngComponentOutlet&lt;/code&gt;: when the problem is really just component selection
&lt;/h2&gt;

&lt;p&gt;For simple cases Angular already gives us:&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;ng-container&lt;/span&gt; &lt;span class="na"&gt;*ngComponentOutlet=&lt;/span&gt;&lt;span class="s"&gt;"componentType"&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;This works very well when the application already knows its possible components and runtime logic only decides which one to display.&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="nx"&gt;componentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;UserCardComponent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdminCardComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantages are obvious: very little infrastructure, normal Angular lifecycle, AOT-compatible components and a relatively declarative template.&lt;/p&gt;

&lt;p&gt;But this approach starts becoming uncomfortable when the runtime input is no longer:&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="nx"&gt;UserCardComponent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and instead becomes:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"children"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name"&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;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;Now we are no longer selecting a component.&lt;/p&gt;

&lt;p&gt;We are interpreting a UI description.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. &lt;code&gt;ViewContainerRef.createComponent()&lt;/code&gt;: more control, more responsibility
&lt;/h2&gt;

&lt;p&gt;Angular also allows components to be instantiated programmatically:&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;viewContainerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;componentType&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="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;label&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="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&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 is a powerful primitive.&lt;/p&gt;

&lt;p&gt;We control where the component is created, which component is used, how inputs are assigned and when the component is destroyed.&lt;/p&gt;

&lt;p&gt;For relatively contained dynamic behavior, this can be exactly what we need.&lt;/p&gt;

&lt;p&gt;But once a runtime specification controls many components, application code often starts evolving into something like:&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;switch &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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dialog&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;Then we add input mapping.&lt;/p&gt;

&lt;p&gt;Then events.&lt;/p&gt;

&lt;p&gt;Then nested components.&lt;/p&gt;

&lt;p&gt;Then state.&lt;/p&gt;

&lt;p&gt;Then validation.&lt;/p&gt;

&lt;p&gt;Soon the difficult part isn't &lt;code&gt;createComponent()&lt;/code&gt; anymore.&lt;/p&gt;

&lt;p&gt;It is everything around it.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Component Registry: separating runtime intent from Angular implementation
&lt;/h2&gt;

&lt;p&gt;A natural next step is a registry:&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;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CardComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InputComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the runtime specification doesn't need to know anything about Angular classes.&lt;/p&gt;

&lt;p&gt;It only says:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Input"&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;and the application decides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Input"
   ↓
InputComponent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation is more important than it initially appears.&lt;/p&gt;

&lt;p&gt;The external system describes intent.&lt;/p&gt;

&lt;p&gt;The frontend controls implementation.&lt;/p&gt;

&lt;p&gt;And the registry also starts becoming a security boundary: only explicitly registered components can be instantiated.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Recursive rendering: when UI becomes a tree
&lt;/h2&gt;

&lt;p&gt;Once a specification can describe nested content, recursion becomes the obvious model.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Card
 └─ Form
     ├─ Input
     ├─ Select
     └─ Button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A renderer can conceptually do:&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="nf"&gt;render&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;registry&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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&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;child&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;children&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&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;This is where dynamic rendering becomes significantly more powerful.&lt;/p&gt;

&lt;p&gt;Forms, dashboards, dialogs, layouts and even entire workflows can now be represented as data.&lt;/p&gt;

&lt;p&gt;But this is also where I think an important architectural mistake can happen:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A recursive renderer should not blindly render whatever tree it receives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The UI tree should be validated before it is trusted.&lt;/p&gt;




&lt;h2&gt;
  
  
  AOT doesn't compile the runtime UI tree
&lt;/h2&gt;

&lt;p&gt;There is sometimes confusion around this part.&lt;/p&gt;

&lt;p&gt;If the structure is dynamic, how can Angular's AOT compiler know what to render?&lt;/p&gt;

&lt;p&gt;The answer is: it doesn't need to know the future structure.&lt;/p&gt;

&lt;p&gt;AOT compiles the building blocks.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BUILD TIME

CardComponent
InputComponent
ButtonComponent
DialogComponent
      ↓
     AOT
      ↓
compiled Angular components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, a JSON specification only decides how those already compiled components are composed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUNTIME

JSON specification
       ↓
Component Registry
       ↓
Recursive Renderer
       ↓
Card + Input + Button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the important distinction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The UI composition is dynamic. The Angular component implementations are not.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The runtime is not compiling new Angular components.&lt;/p&gt;

&lt;p&gt;It is assembling already compiled components.&lt;/p&gt;

&lt;p&gt;This also means we don't need to ship Angular's JIT compiler just to support dynamic UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about sending HTML or Angular templates from the backend?
&lt;/h2&gt;

&lt;p&gt;This is another tempting approach.&lt;/p&gt;

&lt;p&gt;Why not return something like:&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;app-user-card&lt;/span&gt; &lt;span class="na"&gt;[user]=&lt;/span&gt;&lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/app-user-card&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from the server?&lt;/p&gt;

&lt;p&gt;Because injecting that HTML into the page does not make it an Angular template.&lt;/p&gt;

&lt;p&gt;Angular doesn't suddenly compile arbitrary HTML received from an API into AOT components.&lt;/p&gt;

&lt;p&gt;And trying to introduce runtime template compilation changes the trust model completely.&lt;/p&gt;

&lt;p&gt;There is a major architectural difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server describes UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server sends executable Angular templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I strongly prefer the first model.&lt;/p&gt;

&lt;p&gt;Especially when AI becomes part of the system.&lt;/p&gt;




&lt;h1&gt;
  
  
  Dynamic UI also creates a trust problem
&lt;/h1&gt;

&lt;p&gt;This is the part I find more interesting than the rendering itself.&lt;/p&gt;

&lt;p&gt;Imagine that the UI specification comes from a backend, CMS or AI model.&lt;/p&gt;

&lt;p&gt;That specification is now external input.&lt;/p&gt;

&lt;p&gt;Even if it is “just JSON”, it controls what the application creates and potentially what the user can do.&lt;/p&gt;

&lt;p&gt;So I think dynamic UI should be treated similarly to any other untrusted runtime input.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unknown components
&lt;/h2&gt;

&lt;p&gt;The specification should not be able to instantiate arbitrary Angular classes.&lt;/p&gt;

&lt;p&gt;This:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AdminPanel"&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;should only work if &lt;code&gt;AdminPanel&lt;/code&gt; belongs to an explicitly controlled catalog.&lt;/p&gt;

&lt;p&gt;The registry therefore isn't only a convenience.&lt;/p&gt;

&lt;p&gt;It is an allowlist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON
 ↓
"Input"
 ↓
Component Registry
 ↓
InputComponent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No registry entry?&lt;/p&gt;

&lt;p&gt;Nothing gets instantiated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Props are also external input
&lt;/h2&gt;

&lt;p&gt;Even a trusted component can expose dangerous inputs.&lt;/p&gt;

&lt;p&gt;Imagine a component receiving:&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="nx"&gt;html&lt;/span&gt;
&lt;span class="nx"&gt;url&lt;/span&gt;
&lt;span class="nx"&gt;redirect&lt;/span&gt;
&lt;span class="nx"&gt;resourceUrl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fact that the component itself is trusted does not automatically mean every possible value supplied to it should be trusted.&lt;/p&gt;

&lt;p&gt;So a component catalog should ideally define not only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Button" → ButtonComponent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but also the valid shape of its props.&lt;/p&gt;

&lt;p&gt;For example:&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="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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;Now the runtime contract becomes much stronger.&lt;/p&gt;




&lt;h2&gt;
  
  
  Actions are probably the most important boundary
&lt;/h2&gt;

&lt;p&gt;Rendering UI is one thing.&lt;/p&gt;

&lt;p&gt;Allowing generated UI to execute application behavior is another.&lt;/p&gt;

&lt;p&gt;I would never want a specification like:&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;"click"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deleteUser()"&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;and definitely not anything remotely resembling:&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;"click"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eval(...)"&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;Instead, the specification should only describe an action:&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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saveProfile"&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;and the application resolves it through another controlled registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"saveProfile"
      ↓
Action Registry
      ↓
known application code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The external specification can request a capability.&lt;/p&gt;

&lt;p&gt;It cannot invent one.&lt;/p&gt;

&lt;p&gt;This gives us a very useful separation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Component Registry controls what UI can exist.&lt;/p&gt;

&lt;p&gt;The Action Registry controls what that UI is allowed to do.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Validate the UI tree before recursively rendering it
&lt;/h1&gt;

&lt;p&gt;This is one part I think deserves much more attention.&lt;/p&gt;

&lt;p&gt;Suppose a backend or model produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container
 └─ Container
     └─ Container
         └─ ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thousands of levels deep.&lt;/p&gt;

&lt;p&gt;Or maybe the tree contains hundreds of thousands of nodes.&lt;/p&gt;

&lt;p&gt;There may be no XSS.&lt;/p&gt;

&lt;p&gt;No JavaScript injection.&lt;/p&gt;

&lt;p&gt;The JSON may even be structurally valid.&lt;/p&gt;

&lt;p&gt;But rendering it can still freeze the browser.&lt;/p&gt;

&lt;p&gt;And if the specification uses references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C → A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;a malformed graph may create recursive cycles.&lt;/p&gt;

&lt;p&gt;So validation shouldn't stop at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON.parse(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or even basic schema validation.&lt;/p&gt;

&lt;p&gt;A robust runtime boundary may eventually need to reason about component types, props, missing references, illegal parent/child combinations, cycles, maximum depth, maximum node count and potentially maximum repeat expansion.&lt;/p&gt;

&lt;p&gt;The architecture I prefer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend / AI
     ↓
Untrusted UI specification
     ↓
Schema validation
     ↓
Structural validation
     ↓
Catalog / props validation
     ↓
Runtime limits and policies
     ↓
Trusted specification
     ↓
Angular renderer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the last step should create Angular views.&lt;/p&gt;




&lt;h1&gt;
  
  
  Streaming makes this even more interesting
&lt;/h1&gt;

&lt;p&gt;Generative UI adds another complication.&lt;/p&gt;

&lt;p&gt;The model may not send the entire UI at once.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;patch
patch
patch
patch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gradually builds the screen.&lt;/p&gt;

&lt;p&gt;That means a half-generated UI can naturally contain temporary inconsistencies: a parent may reference a child that simply hasn't arrived yet.&lt;/p&gt;

&lt;p&gt;So validating every intermediate state with exactly the same rules as a completed UI can produce false errors.&lt;/p&gt;

&lt;p&gt;I think the model should instead be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stream patches
      ↓
build partial UI
      ↓
generation completes
      ↓
validate completed specification
      ↓
accept / reject / persist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Potentially with additional lightweight limits while streaming to prevent a generation from growing without bounds.&lt;/p&gt;

&lt;p&gt;This becomes much more than “dynamic component rendering”.&lt;/p&gt;

&lt;p&gt;It is a small UI runtime.&lt;/p&gt;




&lt;h1&gt;
  
  
  This is the problem that led me to ngx-json-render
&lt;/h1&gt;

&lt;p&gt;While exploring these patterns, I realized that Angular itself already solves the lowest-level problem very well.&lt;/p&gt;

&lt;p&gt;Angular knows how to create components.&lt;/p&gt;

&lt;p&gt;What I wanted was the layer above that.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;ngx-json-render&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The library takes a different approach from runtime Angular template generation.&lt;/p&gt;

&lt;p&gt;An external system produces a JSON UI specification.&lt;/p&gt;

&lt;p&gt;The Angular application provides a catalog of components and actions.&lt;/p&gt;

&lt;p&gt;The renderer connects the two.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI / Backend
     ↓
JSON UI specification
     ↓
Catalog / Schema
     ↓
Component Registry
     ↓
Action Registry
     ↓
Angular Renderer
     ↓
AOT-compiled Angular components
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to send executable Angular templates, use &lt;code&gt;innerHTML&lt;/code&gt; as a component mechanism or evaluate generated JavaScript.&lt;/p&gt;

&lt;p&gt;The external system generates &lt;strong&gt;UI intent&lt;/strong&gt;, not frontend code.&lt;/p&gt;

&lt;p&gt;The current library already provides the main building blocks around this model: a controlled component registry, catalog-defined component props, registered actions, recursive composition, state/bindings, streaming JSON patches and optional structural validation that can reject a malformed completed specification before it is accepted.&lt;/p&gt;

&lt;p&gt;But I don't think the security story should be overstated.&lt;/p&gt;

&lt;p&gt;There are still boundaries worth strengthening.&lt;/p&gt;

&lt;p&gt;In particular, catalog-level validation and structural validation are different concerns, validation currently has to be explicitly enabled, and resource limits such as maximum graph depth, maximum rendered nodes or explicit cycle protection are areas I consider important for a hardened dynamic UI runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update, Sep 14:&lt;/strong&gt; those limits have since shipped in ngx-json-render 0.4.0 — an unconditional cycle guard plus opt-in &lt;code&gt;maxElements&lt;/code&gt;, &lt;code&gt;maxDepth&lt;/code&gt; and &lt;code&gt;maxRepeatItems&lt;/code&gt; — and catalog checks now run in the same pipeline as the structural ones. Validation itself is still opt-in. What they cover, and what is still up to your app, is in the README's &lt;a href="https://github.com/shteynu/ngx-json-render/blob/main/projects/ngx-json-render/README.md#security" rel="noopener noreferrer"&gt;Security section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And maybe that's the larger point.&lt;/p&gt;

&lt;p&gt;The interesting question is no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can Angular dynamically render components?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course it can.&lt;/p&gt;

&lt;p&gt;The more interesting question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do we safely turn an untrusted runtime UI description into a predictable Angular component tree?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As Generative UI moves from demos into real applications, I suspect this boundary will matter much more than the component creation API itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic UI does not have to mean dynamic trust.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The structure may be generated at runtime.&lt;/p&gt;

&lt;p&gt;The capabilities should still belong to the application.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>security</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Generative UI in Angular: the LLM streams a JSON spec, you render real components</title>
      <dc:creator>Maxim Berenshtein</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:48:51 +0000</pubDate>
      <link>https://dev.to/shteynu/generative-ui-in-angular-the-llm-streams-a-json-spec-you-render-real-components-4lgf</link>
      <guid>https://dev.to/shteynu/generative-ui-in-angular-the-llm-streams-a-json-spec-you-render-real-components-4lgf</guid>
      <description>&lt;p&gt;There are two ways to let a model build a UI, and only one of them is a good idea.&lt;/p&gt;

&lt;p&gt;The first: ask it for HTML and drop the result into &lt;code&gt;innerHTML&lt;/code&gt;. It works in a demo and falls apart everywhere else. You get no design system, no event handlers, no type safety, and a permanent XSS surface with the model on the wrong side of it.&lt;/p&gt;

&lt;p&gt;The second: give the model a &lt;strong&gt;vocabulary&lt;/strong&gt; — a catalog of components you already ship — and let it describe the UI it wants in a constrained JSON format. Your renderer maps that description onto your actual components. The model never emits markup, never emits code, and literally cannot reference anything outside the catalog you handed it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vercel-labs/json-render" rel="noopener noreferrer"&gt;json-render&lt;/a&gt; from Vercel Labs is a well-built implementation of the second idea. It had official renderers for React, Vue, Solid and Svelte. It did not have one for Angular.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/shteynu/ngx-json-render" rel="noopener noreferrer"&gt;&lt;strong&gt;ngx-json-render&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft4xw19a0zjuu53hbb36h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft4xw19a0zjuu53hbb36h.gif" alt="A SpecStream of RFC 6902 patches rendering progressively into an Angular dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the thing
&lt;/h2&gt;

&lt;p&gt;Three pieces, and it's worth being precise about which one does what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catalog&lt;/strong&gt; is your vocabulary — component names, Zod schemas for their props, a description of each so the model knows when to reach for it, and optionally a set of actions:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&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="s1"&gt;ngx-json-render&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;z&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="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;catalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCatalog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;slots&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="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A card container&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;Button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;description&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 button that emits a 'press' event&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;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reload the data&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;code&gt;catalog.prompt()&lt;/code&gt; turns this into the system prompt that teaches the model the vocabulary and the output format. &lt;code&gt;catalog.jsonSchema()&lt;/code&gt; gives you a JSON Schema if you'd rather use structured output or a tool call. &lt;code&gt;catalog.validate(spec)&lt;/code&gt; checks a finished spec against the catalog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The spec&lt;/strong&gt; is what comes back — a flat map of elements, each with a type from the catalog, props, and child keys:&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;"root"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"root"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&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;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"elements"&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;"root"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&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;"children"&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="s2"&gt;"btn"&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;"btn"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"props"&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;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tap me"&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;"on"&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;"press"&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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"setState"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"params"&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;"statePath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&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="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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"children"&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="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="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;Flat, not nested — which matters more than it looks, and I'll come back to it when we get to streaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The renderer&lt;/strong&gt; walks that spec and instantiates your components:&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;json-render&lt;/span&gt; &lt;span class="na"&gt;[spec]=&lt;/span&gt;&lt;span class="s"&gt;"spec()"&lt;/span&gt; &lt;span class="na"&gt;[registry]=&lt;/span&gt;&lt;span class="s"&gt;"registry"&lt;/span&gt; &lt;span class="na"&gt;[handlers]=&lt;/span&gt;&lt;span class="s"&gt;"handlers"&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 catalog components are ordinary Angular components. They read their props from an injected render context and render their children wherever they like:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;JrChildren&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;section class="card"&amp;gt;
      @if (ctx.props().title) { &amp;lt;h3&amp;gt;{{ ctx.props().title }}&amp;lt;/h3&amp;gt; }
      &amp;lt;jr-children /&amp;gt;
    &amp;lt;/section&amp;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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CardComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;injectRenderContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="kr"&gt;string&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;jr-children /&amp;gt;&lt;/code&gt; is a &lt;code&gt;router-outlet&lt;/code&gt; for the spec tree — it renders this element's children at that position. Named slots work the same way: &lt;code&gt;&amp;lt;jr-children slot="header" /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defineRegistry&lt;/code&gt; ties catalog names to component classes, and it's typed against the catalog — register a component the catalog doesn't declare and the compiler stops you:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CardComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ButtonComponent&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&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="cm"&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;h2&gt;
  
  
  What the spec can actually express
&lt;/h2&gt;

&lt;p&gt;A format like this lives or dies on whether it can describe a &lt;em&gt;real&lt;/em&gt; screen rather than a static mock. The baseline json-render contract is fairly rich, and ngx-json-render implements all of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic props&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "$state": "/user/name" }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-way binding&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ "$bindState": "/form/email" }&lt;/code&gt;, &lt;code&gt;{ "$bindItem": "done" }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditionals&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "$cond": {...}, "$then": ..., "$else": ... }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "$template": "Hello, ${/user/name}" }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visibility&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"visible": { "$state": "/count", "gte": 5 }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"repeat": { "statePath": "/todos", "key": "id" }&lt;/code&gt;, nestable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events → actions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"on": { "press": { "action": "...", "confirm": {...}, "onSuccess": ... } }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"watch": { "/country": { "action": "loadCities" } }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;field rules, &lt;code&gt;validateForm&lt;/code&gt;, &lt;code&gt;injectFieldValidation&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every &lt;code&gt;&amp;lt;json-render&amp;gt;&lt;/code&gt; owns a state store addressed by JSON Pointer. &lt;code&gt;setState&lt;/code&gt;, &lt;code&gt;pushState&lt;/code&gt;, &lt;code&gt;removeState&lt;/code&gt; and &lt;code&gt;validateForm&lt;/code&gt; are built in, so the model can wire up a working form without you writing a handler for every button. Anything beyond that is a named action you implement in TypeScript — the model can only &lt;em&gt;request&lt;/em&gt; it by name, with params validated against your Zod schema.&lt;/p&gt;

&lt;p&gt;Actions can declare a confirmation: &lt;code&gt;"confirm": { "title": "Delete this?" }&lt;/code&gt; renders a real dialog and waits for the user before the handler runs. Which is the general theme — the model proposes, your code disposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming is the interesting part
&lt;/h2&gt;

&lt;p&gt;Here's the thing that makes this feel different from ordinary server-driven UI.&lt;/p&gt;

&lt;p&gt;A model generating a dashboard takes several seconds. You could wait for the whole spec and then render it — spinner, pause, pop. Instead, json-render streams the spec as &lt;strong&gt;JSONL: one RFC 6902 patch per line&lt;/strong&gt;:&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/root"&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="s2"&gt;"page"&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/elements/page"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"props"&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="s2"&gt;"title"&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="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/elements/title"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Heading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Weekly sales"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each line is applied to the spec signal as it arrives, so the UI assembles on screen while the model is still generating. This is where the flat element map earns its keep: a patch can add a deeply nested element without rewriting its ancestors, and &lt;code&gt;add /elements/foo&lt;/code&gt; is a single, order-independent line.&lt;/p&gt;

&lt;p&gt;On the client that's one hook:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;json-render [spec]="ui.spec()" [registry]="registry" [loading]="ui.isStreaming()" /&amp;gt;
    &amp;lt;button (click)="ui.send('A dashboard for weekly sales')"&amp;gt;Generate&amp;lt;/button&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;JsonRenderer&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GeneratePage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;ui&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectUIStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;registry&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;code&gt;injectUIStream&lt;/code&gt; POSTs &lt;code&gt;{ prompt, context, currentSpec }&lt;/code&gt; and expects a JSONL body back. The &lt;code&gt;loading&lt;/code&gt; input tells the renderer to tolerate dangling child references — mid-stream, an element routinely names children that haven't arrived yet, and that's normal rather than a bug to warn about.&lt;/p&gt;

&lt;p&gt;The server is whatever streams text. With the AI SDK it's genuinely this small:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&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="nf"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&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;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="s1"&gt;text/plain; charset=utf-8&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="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;There's also &lt;code&gt;injectChatUI&lt;/code&gt; for the chat case, where an assistant message can carry prose &lt;em&gt;and&lt;/em&gt; a spec — the stream is split into text lines and fenced JSONL, and each message ends up with a &lt;code&gt;text&lt;/code&gt; and an optional &lt;code&gt;spec&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's built the way it is
&lt;/h2&gt;

&lt;p&gt;The renderer is signals all the way down: &lt;code&gt;input()&lt;/code&gt; / &lt;code&gt;output()&lt;/code&gt;, &lt;code&gt;computed&lt;/code&gt;, &lt;code&gt;OnPush&lt;/code&gt; on every component, no &lt;code&gt;zone.js&lt;/code&gt; requirement. That isn't box-ticking. A streaming spec means dozens of small state changes per second, and a &lt;code&gt;computed&lt;/code&gt; graph over a flat element map only recomputes the elements a patch actually touched.&lt;/p&gt;

&lt;p&gt;Everything sits on &lt;code&gt;@json-render/core&lt;/code&gt; — the same spec grammar, expression evaluator, state store, action dispatcher and stream compiler the React, Vue, Solid and Svelte renderers use. A catalog is framework-specific (it names your components), but a &lt;strong&gt;spec is portable&lt;/strong&gt;: the same generated JSON renders in any of them. That's also why I didn't invent a dialect. Parity with the baseline contract was the whole point, and the library is laid out so &lt;code&gt;src/lib&lt;/code&gt; could be adapted into a &lt;code&gt;packages/angular&lt;/code&gt; PR upstream.&lt;/p&gt;

&lt;p&gt;If you'd rather not write a catalog before seeing anything work, there's a companion package — &lt;code&gt;ngx-json-render-material&lt;/code&gt;, a ready-made catalog of 28 Angular Material components — so you can generate and render a spec on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from ngx-gen-ui
&lt;/h2&gt;

&lt;p&gt;There is one other Angular library that turns up under "generative UI", and the two get filed together, so it is worth saying plainly which problem each one solves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alessiopelliccione/ngx-gen-ui" rel="noopener noreferrer"&gt;ngx-gen-ui&lt;/a&gt; puts the model call in the component. You attach a directive to an element, give it a prompt, and the response streams into the template. Its structured mode constrains the model to a schema of &lt;strong&gt;markup primitives&lt;/strong&gt; — the README's own example is &lt;code&gt;{ "tag": "h1", "content": "Title" }&lt;/code&gt; — and converts that JSON into DOM elements. Firebase with Vertex AI is the default provider, with an &lt;code&gt;AiAdapter&lt;/code&gt; interface for swapping in another one, though &lt;code&gt;firebase&lt;/code&gt; and &lt;code&gt;@firebase/ai&lt;/code&gt; are non-optional peer dependencies either way. MIT, Angular ≥ 17.&lt;/p&gt;

&lt;p&gt;ngx-json-render never talks to a model at all. Generation happens on your server; the library only consumes the resulting stream. And the vocabulary is not HTML — it is &lt;strong&gt;your component catalog&lt;/strong&gt;, so what comes out the other end is your &lt;code&gt;Card&lt;/code&gt;, your &lt;code&gt;DataTable&lt;/code&gt;, your &lt;code&gt;ConfirmButton&lt;/code&gt;, with their own inputs, their own styles, and their own event handlers.&lt;/p&gt;

&lt;p&gt;That difference decides what the generated UI can &lt;em&gt;do&lt;/em&gt;. Markup primitives give you a rendered document. A component spec gives you an interface: state bindings, two-way &lt;code&gt;$bindState&lt;/code&gt;, events mapped to actions you implemented in TypeScript, confirm dialogs, form validation, &lt;code&gt;repeat&lt;/code&gt; over a state array. The model can hand you a working form rather than a picture of one.&lt;/p&gt;

&lt;p&gt;So, roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want a model's prose streamed into a page, with Firebase already in your stack? ngx-gen-ui is less work — a directive, and no server of your own.&lt;/li&gt;
&lt;li&gt;Want the model to assemble screens out of your existing design system, with real handlers behind the buttons, in a format that is portable to React/Vue/Solid/Svelte? That is what the json-render spec is for, and this is the Angular renderer for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are not really competitors. If anything, the interesting thing is that Angular now has two takes on this at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://shteynu.github.io/ngx-json-render/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/strong&gt; — an interactive spec with bindings, repeat, confirm and watch, plus a replayable SpecStream. The Streaming tab is the one to look at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/shteynu/ngx-json-render" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/ngx-json-render" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://stackblitz.com/github/shteynu/ngx-json-render" rel="noopener noreferrer"&gt;Open in StackBlitz&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;ngx-json-render @json-render/core zod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular ≥ 20, Apache-2.0 — matching upstream.&lt;/p&gt;

&lt;p&gt;It's young. It has full parity with the baseline renderer contract and a real test suite, but &lt;code&gt;@json-render/core&lt;/code&gt; moves fast and I track it release by release. Issues, PRs, and "why didn't you just…" questions are all welcome.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>ai</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
