<?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: YQteam</title>
    <description>The latest articles on DEV Community by YQteam (@yqteamdyq).</description>
    <link>https://dev.to/yqteamdyq</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%2F4109553%2F6063d7ea-7520-4225-b869-ac2d91352fa3.png</url>
      <title>DEV Community: YQteam</title>
      <link>https://dev.to/yqteamdyq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yqteamdyq"/>
    <language>en</language>
    <item>
      <title>Building multi-user + security on shared hosting: filesystem as the only state layer</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:33:22 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/building-multi-user-security-on-shared-hosting-filesystem-as-the-only-state-layer-449f</link>
      <guid>https://dev.to/yqteamdyq/building-multi-user-security-on-shared-hosting-filesystem-as-the-only-state-layer-449f</guid>
      <description>&lt;p&gt;The self-hosted utility space shares a quiet truth: once a tool grows past the "one admin logs in" stage, the real work is not adding pages — it's deciding how to let a few people cooperate, who gets to approve dangerous operations, and how to persist all that state when you can't bank on a daemon, a database, or Redis. I recently read through the source of an open-source panel built for PHP shared hosting, and it answered all three with little more than &lt;code&gt;.json&lt;/code&gt; files and file locks.&lt;/p&gt;

&lt;p&gt;This is not a feature review. It's a look at the implementation trade-offs I found, and a few places where I genuinely don't know the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default-deny is where multi-user gets done right
&lt;/h2&gt;

&lt;p&gt;The permission model is two layers: a role rank (viewer &amp;lt; operator &amp;lt; admin) plus an action whitelist. Ranking is just integer comparison — boring. The interesting part is the companion default policy: &lt;strong&gt;any endpoint not explicitly listed in the ACL map is admin-only&lt;/strong&gt;. A lot of old panels default to "if it's not written, everyone can call it" — one unguarded endpoint and you leak. Here the default is rejection, and you opt in. That direction matters more than the fancy parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A path allowlist that's string-prefix matching raises flags
&lt;/h2&gt;

&lt;p&gt;To limit read-only users to a subtree, the check looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;rtrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's raw string-prefix comparison. It does not resolve &lt;code&gt;..&lt;/code&gt;, and it does not resolve symlinks. If an allowed directory contains a symlink pointing outside the allowed tree, can the read-only user traverse it? That depends on whether the file layer above normalizes via &lt;code&gt;realpath&lt;/code&gt;. I stopped at the ACL layer and didn't chase the whole chain, so it stays an open question. Even a one-line comment ("the layer above normalizes paths") would save everyone the audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double approval is, in effect, a re-dispatch with the gate bypassed
&lt;/h2&gt;

&lt;p&gt;Sensitive operations (DB import, bulk purge, app uninstall) are intercepted and routed through a second-admin approval. Two decisions here are right: a single-admin deployment gets &lt;code&gt;409&lt;/code&gt; outright instead of a "just let it through" fallback, and the requester cannot be the approver.&lt;/p&gt;

&lt;p&gt;The execution path is the part worth unpacking. When the approver clicks approve, the code rebuilds the router, overrides the request body with the stored payload, opens a capture buffer, sets a bypass flag, and &lt;strong&gt;synchronously re-dispatches the request inside the approver's own HTTP call&lt;/strong&gt;. Two consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A slow operation blocks the approver's request until it finishes. For a large import that's a UX and timeout question.&lt;/li&gt;
&lt;li&gt;The payload is a snapshot taken at submit time; by the time it runs, the underlying resource may have changed. So "approve" strictly means "approve the action as-of submit", not "apply to current state". Fine in most cases, worth remembering for auditing and error handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rate limiter is honestly not a sliding window
&lt;/h2&gt;

&lt;p&gt;Rate limiting applies only to non-admin roles. State lives in &lt;code&gt;{userid}_{minute-bucket}.json&lt;/code&gt;, and the check sums the &lt;strong&gt;current bucket plus the previous one&lt;/strong&gt; to approximate "roughly two minutes":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bucket&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$cur&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;count&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;That's not a sliding window; accuracy depends on where requests land relative to minute boundaries, and it skews at the seams. Fine as coarse shielding, but don't describe it as precise. One thing that caught my eye: reads and writes each count as 1 (no write weighting), while the read-only role's write quota is one request per minute. That is aggressive and likely to trip legitimate users — worth confirming it's intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  An in-process WAF lives and dies by its regexes
&lt;/h2&gt;

&lt;p&gt;The WAF runs on every PHP request, scanning query, body and URI with regex. Three implications worth separating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It blocks inside the PHP process. By the time it fires, PHP has already handled the request; it just stops it with &lt;code&gt;exit()&lt;/code&gt; before business code runs. This is an application-layer filter, not an edge firewall.&lt;/li&gt;
&lt;li&gt;The rules are keyword regexes, not semantic analysis. The command-injection rule contains &lt;code&gt;\b(?:ls|dir|cat|grep|find|exec|system|...)\b&lt;/code&gt;, so any plaintext containing "cat" or "find" trips it — search results, documents, an ordinary English sentence. And URL encoding or comment obfuscation sidesteps plain-text regex. It's a coarse sieve.&lt;/li&gt;
&lt;li&gt;It ships a per-IP 1000 req/hour all-up limit via a locked JSON file — a pragmatic last line of defense on shared hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For shared hosting, the survival metric for this kind of WAF is the false-positive rate. Killing legitimate business costs more than the attacks it stops. "Rules can be turned off and scoped down" beats "the bigger the rule surface the better".&lt;/p&gt;

&lt;h2&gt;
  
  
  A convention everyone should copy: say plainly when a capability is missing
&lt;/h2&gt;

&lt;p&gt;When the host lacks a capability, the API returns &lt;code&gt;501&lt;/code&gt; with a machine-readable code (think &lt;code&gt;composer_unavailable&lt;/code&gt;, &lt;code&gt;fpm_not_applicable&lt;/code&gt;). Shared hosts routinely miss extensions or disable functions, and most panels just throw a generic 500 that sends you guessing. Separating "this host can't do it" from "the software is broken" is more valuable than a dozen more pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open questions I'm keeping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does the file layer above the path allowlist actually normalize &lt;code&gt;realpath&lt;/code&gt; and symlinks?&lt;/li&gt;
&lt;li&gt;With snapshot-replay approvals, when the resource changed after submit — replay anyway, or error and ask to resubmit?&lt;/li&gt;
&lt;li&gt;All state in JSON with file locks: at what concurrency and volume does that break, and where's the line where SQLite becomes worth it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source I read is an open PHP panel; each module lives as one file under its &lt;code&gt;backend/&lt;/code&gt; directory, easy to browse: &lt;a href="https://github.com/YQteam-dyq/Go.js-Lite/" rel="noopener noreferrer"&gt;https://github.com/YQteam-dyq/Go.js-Lite/&lt;/a&gt;. If you've measured any of the above, I'd like to hear it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>Why we wrote our own 8.5 kB web component runtime instead of pulling in a framework</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Mon, 07 Sep 2026 12:23:55 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/why-we-wrote-our-own-85-kb-web-component-runtime-instead-of-pulling-in-a-framework-6jh</link>
      <guid>https://dev.to/yqteamdyq/why-we-wrote-our-own-85-kb-web-component-runtime-instead-of-pulling-in-a-framework-6jh</guid>
      <description>&lt;p&gt;Hi, we're YQteam 👋 We just open-sourced &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;yq-sanyi&lt;/a&gt; (v0.2.0, our initial release): &lt;strong&gt;define a component as a native HTML tag, then drop&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;&amp;lt;yq-counter&amp;gt;&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;into a plain page — zero dependencies, zero build.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first question people ask us is: there are already so many component solutions — why build your own?&lt;/p&gt;

&lt;p&gt;This post is not about the API. It's about the decision process behind the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scenario we kept hitting
&lt;/h2&gt;

&lt;p&gt;The need is simple: a reusable counter, a panel, a todo list — instead of copy-pasting the same HTML plus interaction logic into every page.&lt;/p&gt;

&lt;p&gt;But in practice you usually have only two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adopt a framework.&lt;/strong&gt; For a "lightweight reuse" need, you pay for a whole runtime plus a build toolchain. For existing static pages, multi-page sites, or server-rendered template pages, that feels like moving house just to buy a shelf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hand-rolled copy-paste.&lt;/strong&gt; Great the first time, painful from the second: boilerplate, styles bleeding into each other, event listeners nobody remembers to clean up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wanted a third way: &lt;strong&gt;as light as copy-paste, with the reuse experience of a framework.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three paths we walked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Native Web Components: the right direction, but parts, not a product
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;customElements&lt;/code&gt; standardized "UI as an element" — that's genuinely great. But to actually use a component comfortably you still have to solve a long list of problems yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How do attributes and internal state stay in sync?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How should the event system be designed?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where does reactive state come from?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What about style isolation? If you reach for Shadow DOM, form-element piercing, CSS-variable theming, and injecting external styles all become design problems of their own;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who cleans up listeners and side effects on unmount?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard doesn't build those wheels for you. Using it raw means boilerplate; filling the gaps means writing a framework anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  A framework runtime: the cost doesn't match the benefit
&lt;/h3&gt;

&lt;p&gt;Our actual need was "a few reusable interactive widgets", not page-wide state management, routing, and an ecosystem. Bringing in the latter for the former is like opening a supermarket to buy a bottle of water.&lt;/p&gt;

&lt;p&gt;This is especially true when the target is &lt;strong&gt;plain HTML / multi-page / static pages, or a page embedded inside someone else's system&lt;/strong&gt; — a framework runtime plus build output can easily weigh more than the components themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  A minimal self-built loop: do just one thing — components
&lt;/h3&gt;

&lt;p&gt;So we implemented it ourselves, shrinking the scope to the smallest coherent unit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Let template, behavior and scoped style be one definition, one native tag, and one shared lifecycle.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The name "yq-sanyi" ("trinity") comes exactly from that idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules we set for ourselves
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web-standard APIs only.&lt;/strong&gt; No JSX, no virtual DOM, no compiler, no framework runtime;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single source of truth.&lt;/strong&gt; Template, behavior and style live in one unit — easy to read, reuse and audit;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mutate state, the view updates itself.&lt;/strong&gt; Tags auto-mount, auto-update and auto-cleanup;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Style isolation without mandatory Shadow DOM.&lt;/strong&gt; Selector-scoping by default, CSS-variable theming, opt-in Shadow DOM when you need strong encapsulation;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure isolation.&lt;/strong&gt; A broken component renders an error placeholder plus a structured warning while the rest of the page keeps working;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Size is a budget, not a souvenir.&lt;/strong&gt; The core is ~8.5 kB gzipped, and the repo ships dependency-graph and bundle-size gates plus benchmarks so it can't quietly bloat.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yq-counter&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;button yq-on:click="inc"&amp;gt;count {{ count }}&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button { font-size: 18px; padding: 8px 18px; }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;Define it once. After that, the usage site is a single line:&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;yq-counter&amp;gt;&amp;lt;/yq-counter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For template syntax, state, lists and scoped styles, the &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;README&lt;/a&gt; and the &lt;a href="https://github.com/YQteam-dyq/yq-sanyi/blob/main/docs/tutorial.md" rel="noopener noreferrer"&gt;English tutorial&lt;/a&gt; cover everything — no need to repeat it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  We're not telling anyone to abandon their framework
&lt;/h2&gt;

&lt;p&gt;Different constraints call for different choices. If your project is deeply embedded in a framework ecosystem, needs SSR, or depends on a full CLI-driven pipeline — a framework is right for you, and we explicitly list those cases as "deliberately not supported" in our docs.&lt;/p&gt;

&lt;p&gt;But if you're on the &lt;strong&gt;"I just want to reuse a piece of UI, lightly"&lt;/strong&gt; side — no build, plain HTML pages, curious about how components actually work under the hood — come say hi at the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Repo: &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;https://github.com/YQteam-dyq/yq-sanyi&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bilingual tutorials and runnable examples live in the repo&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.2.0 is our initial release and we will keep iterating&lt;/strong&gt;: fixing known limitations first (event binding inside &lt;code&gt;yq-for&lt;/code&gt; rows), evolving the template layer and devtools, and keeping the English/Chinese docs in sync. Stars, issues, and pull requests are all welcome — and if the project saves you time, you can &lt;a href="https://afdian.com/a/yqteam?utm_source=copylink&amp;amp;utm_medium=link" rel="noopener noreferrer"&gt;support us on Afdian&lt;/a&gt; to help keep it free, open and zero-dependency.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>Why a New Compiler Splits Into AST, HIR, and TIR (and Caches by Content)</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Sat, 05 Sep 2026 14:03:46 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/why-a-new-compiler-splits-into-ast-hir-and-tir-and-caches-by-content-ki8</link>
      <guid>https://dev.to/yqteamdyq/why-a-new-compiler-splits-into-ast-hir-and-tir-and-caches-by-content-ki8</guid>
      <description>&lt;p&gt;Compiler beginners easily imagine compilation as a straight line: read source, parse, generate machine code. In real engineering, that line is split into several intermediate representations (IRs), where each layer does one set of jobs and layers are joined by lowering. Why go through all this trouble? Because a single representation always hits a conflict between expressiveness and information at some stage: too close to source and it is hard to optimize, too close to machine code and it is hard to analyze. This post uses YQ, an unreleased LLVM-based systems language we are developing, to explain what the AST, HIR, and TIR layers each do, why their order cannot be shuffled, and how a content-addressed cache keeps a multi-layer pipeline from becoming too slow to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of questions one compilation answers
&lt;/h2&gt;

&lt;p&gt;Splitting "turn YQ source into an executable" apart, a compiler is really answering three different kinds of questions in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What does this text mean&lt;/strong&gt; (syntax layer): what does the tree of &lt;code&gt;a + b * c&lt;/code&gt; look like?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are these names and types right&lt;/strong&gt; (semantic layer): is &lt;code&gt;b&lt;/code&gt; an &lt;code&gt;f32&lt;/code&gt; or a user-defined type? Does &lt;code&gt;+&lt;/code&gt; have a matching implementation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How does this logic land on the machine&lt;/strong&gt; (code generation layer): is addition an &lt;code&gt;add&lt;/code&gt; instruction or a function call? How is a struct laid out in memory?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one AST alone ran the whole way, the first kind of question would be easy to answer and the third would be very hard, because the AST is full of syntactic sugar (&lt;code&gt;if&lt;/code&gt;, pattern matching, operators, indentation blocks) and the optimizer would have to guess semantics every time. The industry-standard approach is therefore to let representations "desugar" layer by layer: the AST keeps the full syntactic shape of the source, and the further down the IR you go, the closer you get to a plain instruction-level form, until you reach LLVM IR, a representation that is optimizable and can generate code for many backends.&lt;/p&gt;

&lt;p&gt;YQ's pipeline is designed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YQ source
  |  lexing/parsing (Unicode identifiers, indentation-aware, error recovery)
  v
Token -&amp;gt; AST (carries Spans: every node remembers its start/end location in source)
  |  semantic analysis: scope resolution, desugaring
  v
HIR (high-level IR: explicit control flow and calls, syntactic sugar flattened)
  |  type checking (HM inference + trait solving) + ownership/borrow checking
  v
TIR (typed IR: every node carries a concrete type)
  |  generic specialization (monomorphization) + lowering to LLVM IR
  v
LLVM IR -&amp;gt; object files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  AST: faithfully preserving syntax, Span is its soul
&lt;/h2&gt;

&lt;p&gt;The AST's job is not to be "convenient", but to be "complete". The parser (ours is a hand-written recursive descent one) turns source into a tree where every node hangs on a Span, the start/end line and column range of that node in the source file. A Span looks like a tiny structure, but it is the foundation of all diagnostics quality downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;when types mismatch, the error can point precisely at the offending subexpression instead of the whole function;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;editor features like hover, go-to-definition, and completion are, at bottom, "map a cursor position onto an AST node";&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if an error message wants to show a source snippet with terminal coloring, the only reliable source of that is the correspondence between Spans and source text.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why even adding one field at the AST stage deserves caution: its structure flows through HIR and TIR and affects diagnostics and tooling all the way down. Many new language projects rush with a minimal AST to get something running, and later find that retrofitting the diagnostics experience means almost rewriting the front end. That bill is not worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  HIR: flattening "code written by humans" into "code a compiler can handle"
&lt;/h2&gt;

&lt;p&gt;HIR exists to remove syntactic sugar and implicit behavior. Code written by humans is full of conveniences: &lt;code&gt;if&lt;/code&gt; is an expression, operators call trait methods, pattern matching hides a chain of branch decisions, and indentation blocks must become explicit scopes. These are friendly to readers, but not to analyzers. The HIR stage does a few typical things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;scope and name resolution&lt;/strong&gt;: resolve names like &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;foo&lt;/code&gt; to definite definitions, producing binding relations for every symbol, preparing for type checking;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;explicitness&lt;/strong&gt;: expand syntactic sugar into plainer structures so later stages only face a few node shapes;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;keeping what type checking needs&lt;/strong&gt;: HIR nodes still have no types, but the call structure they record must be clean enough for the type checker to walk smoothly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common design question is whether HIR should keep Spans. The answer is almost always yes. Diagnostics at ever later stages need to point back at source, and since HIR has already flattened syntactic sugar, dropping position information here means errors would point at "desugared intermediate nodes" that users cannot understand at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIR: the "final draft" after type and ownership checking
&lt;/h2&gt;

&lt;p&gt;Once type checking (HM-style inference plus trait constraint solving) and ownership checking finish, the compiler has enough information to pin every expression to a concrete type. That representation is TIR. Compared with HIR, the key increment of TIR is that every node carries a definite type, which makes it safe to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;generic specialization&lt;/strong&gt;: &lt;code&gt;add 1 2&lt;/code&gt; and &lt;code&gt;add 1.5 2.5&lt;/code&gt; are the same function node in HIR; in TIR the instantiation information is clear, so a dedicated copy can be cloned for &lt;code&gt;(i32, i32)&lt;/code&gt; and for &lt;code&gt;(f32, f32)&lt;/code&gt; and handed to LLVM for optimization;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;memory layout decisions&lt;/strong&gt;: struct field offsets and enum discriminant layouts can only really be computed once types are known;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ownership-related move/borrow decisions&lt;/strong&gt;: where a value moves and where it drops needs type information combined with region inference.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing specialization at the TIR level instead of the LLVM level is so that "clone code by type" happens on our own IR. On our own IR we can freely copy, rename, and instrument nodes, while the LLVM layer is better at scalar optimization and instruction selection. The two jobs do not overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hook up to LLVM instead of writing a whole back end
&lt;/h2&gt;

&lt;p&gt;Everything up to TIR is language-specific. The work after that, instruction selection, register allocation, optimization, and object file generation, is highly generic and an enormous amount of engineering. Calling into LLVM is what most new languages choose, and YQ does too: at the code generation layer, TIR is lowered to LLVM IR through LLVM's C API bindings, and LLVM produces object files. The benefit is getting an industrial-grade optimizer and multi-backend support for free (x86_64, WASM, and even GPU/TPU directions all build on the LLVM ecosystem). The cost is that you have to describe your language in LLVM's world view: you are responsible for struct layout decisions, calling conventions, and translating YQ's error handling model into control flow that LLVM can express.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of a multi-layer pipeline: slowness, and how a cache covers it
&lt;/h2&gt;

&lt;p&gt;The most direct cost of multiple IR layers is slower compilation: every layer walks the whole tree once, and every lowering allocates again. Early new-language projects usually do not care, but once the compiler starts compiling itself (bootstrapping) and the test suite grows into the hundreds, slowness becomes a development-efficiency problem. Our approach is a content-addressed compilation cache: cache intermediate artifacts per file rather than per whole project.&lt;/p&gt;

&lt;p&gt;In the compile cache directory, each compilation unit leaves a group of files like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1423ba0b-e3b0c442.meta     # metadata
1423ba0b-e3b0c442.bin      # intermediate artifact
1423ba0b-e3b0c442.o        # object file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.meta&lt;/code&gt; content is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;v&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;source_hash&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;SHA256 of the source&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;flags_hash&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;SHA256 of the compile options&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;list of source files this unit depends on&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;corresponding .o file name&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;corresponding .bin file name&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword of this design is "content addressing": the cache key is not a file name or timestamp but a hash of the input content. Change one byte of source and &lt;code&gt;source_hash&lt;/code&gt; changes, so that unit is automatically invalidated and rebuilt; units that did not change are reused directly from &lt;code&gt;.o&lt;/code&gt;/&lt;code&gt;.bin&lt;/code&gt;. The &lt;code&gt;deps&lt;/code&gt; field records dependency relations, which makes "only recompile affected modules" possible. That is the skeleton of incremental compilation, and it lays the groundwork for toolchain goals such as "a full rebuild after changing one line stays under 500 ms". Note that what is hashed is the recomputable input, not the artifact path; otherwise the cache would silently hand you stale results.&lt;/p&gt;

&lt;p&gt;It is worth stressing: cache correctness matters far more than cache hit rate. Better to invalidate too much and rebuild than to let an old artifact slip into a new build. That is why compile options are folded into the hash as well (&lt;code&gt;flags_hash&lt;/code&gt;), ruling out the classic bug of "switching optimization levels yet eating the old cache".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looking back over the pipeline: the AST is responsible for faithfully preserving syntax and carrying Spans, HIR for flattening syntactic sugar so it can be analyzed, TIR for finalizing after type and ownership checking and performing specialization, and LLVM for the last mile of optimization and code generation. Each layer boundary corresponds to a clear analysis phase in the compiler, and once the order is reversed you are forced to do work on a representation that it was never meant for. The content-addressed cache then pays off the "performance debt" of the multi-layer pipeline, keeping iteration speed usable after bootstrapping and test growth. For new language projects, planning this "layering plus content addressing" combination early is more valuable than jumping straight to a toy compiler, because once the front-end structure is fixed, later changes mean rewriting layer by layer.&lt;/strong&gt;&lt;br&gt;
**&lt;/p&gt;

</description>
      <category>programming</category>
      <category>compilers</category>
      <category>llvm</category>
    </item>
    <item>
      <title>I built a local memory layer for AI agents: no vector DB, one SQLite file</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:34:48 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/i-built-a-local-memory-layer-for-ai-agents-no-vector-db-one-sqlite-file-27jf</link>
      <guid>https://dev.to/yqteamdyq/i-built-a-local-memory-layer-for-ai-agents-no-vector-db-one-sqlite-file-27jf</guid>
      <description>&lt;p&gt;If you build AI agents, you know the feeling: the model itself has no memory. Every conversation starts from zero. To make an agent remember user preferences, pick up where a task left off, or reuse knowledge across sessions, you have to bolt on a memory system yourself.&lt;/p&gt;

&lt;p&gt;I looked at the existing options, and none of them felt right for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Vector databases (Pinecone, Qdrant, Weaviate…) are powerful, but standing up a distributed service just for memory is a lot of weight for a small project or a solo dev.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cloud memory services (Mem0 and similar) keep your data on someone else's servers and bill per use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rolling my own in-memory store loses everything on restart — and forget semantic search entirely.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I actually wanted was pretty boring: a local memory layer that runs from one file and one command, works out of the box, and keeps the data fully in my hands. I couldn't find one I liked, so I wrote it. That's how &lt;strong&gt;yq-nova-agent&lt;/strong&gt; started, open sourced on August 3rd: &lt;a href="https://github.com/YQteam-dyq/yq-nova-agent" rel="noopener noreferrer"&gt;github.com/YQteam-dyq/yq-nova-agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;In one sentence: a single-file SQLite memory and state layer for agents, built around three operations — &lt;strong&gt;remember&lt;/strong&gt;, &lt;strong&gt;recall&lt;/strong&gt;, &lt;strong&gt;forget&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Things an agent learns persist across conversations and survive restarts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can recall semantically relevant information from past sessions using natural language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It tracks entities and their relationships, so you get lightweight graph reasoning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stale or low-importance memories are cleaned up automatically, so the store doesn't grow forever.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No external service to deploy. The only runtime dependency is one SQLite file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions worth talking about
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Three operations, one mental model
&lt;/h3&gt;

&lt;p&gt;The API is deliberately small. HTTP, the Rust SDK, and the CLI all expose the same semantics, so there's no conceptual overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Store a fact with tags and an importance score&lt;/span&gt;
yq-nova remember &lt;span class="s2"&gt;"User is a Rust developer who prefers lightweight tools"&lt;/span&gt; &lt;span class="nt"&gt;--tag&lt;/span&gt; user-profile &lt;span class="nt"&gt;--importance&lt;/span&gt; 0.9

&lt;span class="c"&gt;# Recall with natural language&lt;/span&gt;
yq-nova recall &lt;span class="s2"&gt;"user's technical background"&lt;/span&gt; &lt;span class="nt"&gt;--top-k&lt;/span&gt; 5

&lt;span class="c"&gt;# Check what's in the store&lt;/span&gt;
yq-nova stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't want an HTTP server? The CLI works standalone. Building a Rust app? Pull in &lt;code&gt;yq-nova-core&lt;/code&gt; as a library and call it in-process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid retrieval, not just vectors
&lt;/h3&gt;

&lt;p&gt;An early version that only did vector similarity missed too much: exact keyword matches and graph relationships between entities don't show up in pure semantic search. So recall fuses three signals with RRF (Reciprocal Rank Fusion):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Semantic search (embedding similarity)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keyword search (SQLite FTS5 full-text index)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Graph signals (entity-relation relatedness)&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Hybrid mode with graph enhancement&lt;/span&gt;
yq-nova recall &lt;span class="s2"&gt;"storage solutions related to SQLite"&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; hybrid &lt;span class="nt"&gt;--graph&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embeddings are pluggable: OpenAI-compatible endpoints by default, plus a built-in mock provider so you can develop and test fully offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entities and relations: memory with context
&lt;/h3&gt;

&lt;p&gt;Isolated memory entries aren't enough — pieces of information relate to each other. The project keeps an entity-relation graph with recursive BFS traversal. Store "React is a UI library" and "Vue is a UI library", and a recall can walk the graph to find neighboring concepts. That's context pure vector search can't give you.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite is underrated
&lt;/h3&gt;

&lt;p&gt;A lot of people write SQLite off as a toy, but with WAL mode, composite indexes, and FTS5 it's genuinely enough for a single-node memory layer — and the operational cost is close to zero. Persistence, transactions, and schema migrations are all solved problems, so I didn't have to reinvent any of them. This is also what makes the "zero external dependencies" promise hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v0.2.0 added
&lt;/h2&gt;

&lt;p&gt;v0.2.0 shipped on August 6th, and it closed most of the gap between "works on my machine" and "usable in production":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Embedded SDK mode (&lt;code&gt;EmbeddedNova&lt;/code&gt;) — use it in-process without spinning up an HTTP server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local ONNX inference via FastEmbed, so you don't need an OpenAI API key to get vectors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API token auth middleware on the HTTP server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQLite vector index backed by sqlite-vec's HNSW&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integration tests plus Criterion benchmarks (KNN, graph traversal, embedding)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker image and docker-compose for one-command startup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A small Python client (&lt;code&gt;yq_nova&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the server in Docker, persisting data to /data&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 7999:7999 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; yq-nova-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;YQ_NOVA_EMBEDDING__DEFAULT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mock &lt;span class="se"&gt;\&lt;/span&gt;
  yq-nova serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth, OpenTelemetry tracing, and benchmarks are the "invisible" features — but they're exactly what you need before trusting something in production, so I prioritized them in v0.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;The project is new. It's been open source since August 3rd, and today it has zero stars and an issue tracker that only I talk to. I'm the only maintainer, and there are 18 commits so far. I'm not writing this to claim I built something impressive — I'm writing it because I hit a real problem (agent memory) that I think the "lightweight + local + single file" approach genuinely solves, and I want more people who feel the same pain to find it.&lt;/p&gt;

&lt;p&gt;If you're building agents and have opinions about memory, or you try it and think something is designed wrong, please open an issue. What I'd most like to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle agent memory today, and what hurts the most about it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your answers will directly shape what I build next — v0.3 is in active development, and I'd rather prioritize based on real scenarios than my own guesses.&lt;/p&gt;

&lt;p&gt;If the project is useful to you, or the direction sounds interesting, a star is the easiest way to help.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/YQteam-dyq/yq-nova-agent" rel="noopener noreferrer"&gt;github.com/YQteam-dyq/yq-nova-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm YQteam-dyq on GitHub — happy to chat about agent memory, Rust, or anything in between.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>sqlite</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
