<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://alisina.is-a.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://alisina.is-a.dev/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-09-24T10:07:44+02:00</updated><id>https://alisina.is-a.dev/feed.xml</id><title type="html">Field Notes — Alisina Karimi</title><subtitle>Engineering notes on production systems, distributed software, and making technical claims legible.</subtitle><entry><title type="html">An architecture graph should know what it doesn’t know.</title><link href="https://alisina.is-a.dev/notes/an-architecture-graph-should-say-i-dont-know/" rel="alternate" type="text/html" title="An architecture graph should know what it doesn’t know." /><published>2026-09-23T14:00:00+02:00</published><updated>2026-09-23T14:00:00+02:00</updated><id>https://alisina.is-a.dev/notes/an-architecture-graph-should-say-i-dont-know</id><content type="html" xml:base="https://alisina.is-a.dev/notes/an-architecture-graph-should-say-i-dont-know/"><![CDATA[<p>Architecture diagrams are persuasive. A line from a route to a handler looks like a fact even if someone drew it from memory six months ago. A tool that generates the line has the same responsibility: show where it came from, or make the gap visible.</p>

<p>That principle shapes <a href="https://github.com/AlisinaDevelo/CARTOGRAPH">CARTOGRAPH</a>, a local tool for scanning supported TypeScript projects, producing a deterministic architecture graph, and comparing two Git revisions. Every emitted relationship carries repository-relative source evidence—or an explicit reason it could not be resolved.</p>

<figure>
  <svg viewBox="0 0 620 166" role="img" aria-labelledby="graph-title graph-desc">
    <title id="graph-title">Three states for an architecture edge</title>
    <desc id="graph-desc">A verified relationship with source evidence, an unresolved relationship with a reason, and an unsupported construct retained as a diagnostic.</desc>
    <path d="M185 49h74m-74 57h74m-74 57h74" stroke="#a8afa4" stroke-width="1.2" stroke-dasharray="4 4" />
    <g font-family="monospace">
      <rect x="6" y="12" width="179" height="45" fill="#f8f7f1" stroke="#a8afa4" /><text x="20" y="31" font-size="9" fill="#20241f">route → handler</text><text x="20" y="47" font-size="8" fill="#31488f">VERIFIED · source: routes.ts:18</text>
      <rect x="6" y="69" width="179" height="45" fill="#f8f7f1" stroke="#a8afa4" /><text x="20" y="88" font-size="9" fill="#20241f">module → target</text><text x="20" y="104" font-size="8" fill="#69716a">UNRESOLVED · dynamic import</text>
      <rect x="6" y="126" width="179" height="34" fill="#f8f7f1" stroke="#a8afa4" /><text x="20" y="147" font-size="8" fill="#69716a">DIAGNOSTIC · no guessed edge</text>
      <circle cx="285" cy="34" r="9" fill="#d1ed62" stroke="#20241f" /><circle cx="345" cy="34" r="9" fill="#f8f7f1" stroke="#20241f" /><circle cx="412" cy="34" r="9" fill="#f8f7f1" stroke="#20241f" />
      <circle cx="285" cy="91" r="9" fill="#d1ed62" stroke="#20241f" /><circle cx="345" cy="91" r="9" fill="#f8f7f1" stroke="#20241f" />
      <circle cx="285" cy="143" r="9" fill="#d1ed62" stroke="#20241f" /><circle cx="345" cy="143" r="9" fill="#f8f7f1" stroke="#20241f" /><circle cx="412" cy="143" r="9" fill="#f8f7f1" stroke="#20241f" />
      <path d="M294 34h42m18 0h49M294 91h42M294 143h42" stroke="#31488f" stroke-width="1.7" />
      <text x="474" y="37" font-size="8" fill="#20241f">edge + evidence</text><text x="474" y="94" font-size="8" fill="#20241f">reason, no edge</text><text x="474" y="146" font-size="8" fill="#20241f">uncertainty kept</text>
    </g>
  </svg>
  <figcaption>Resolved, unresolved, and unsupported should remain different states.</figcaption>
</figure>

<h2 id="static-analysis-has-an-edge">Static analysis has an edge</h2>

<p>No analyzer sees every runtime path. Dynamic imports, reflective calls, framework conventions, and generated code can all hide relationships from a static pass. There are two tempting mistakes: silently omit the relationship, or infer a plausible edge and make the graph look complete.</p>

<p>Both choices erase useful information. Omission looks like absence. A guess looks like knowledge. CARTOGRAPH keeps unresolved constructs visible as diagnostics instead. The output can say what the analyzer understood, which evidence supports it, and where its current model stops.</p>

<h2 id="diff-the-shape-not-just-the-files">Diff the shape, not just the files</h2>

<p>A revision diff can show that files changed; an architecture diff asks what those edits did to the relationships the tool can model. CARTOGRAPH compares two Git revisions and reports added, removed, or changed graph nodes and edges. Deterministic output makes the same source state produce the same snapshot, which gives the diff a stable basis for review.</p>

<p>That is still a structural view, not a complete account of runtime behavior. The graph does not execute the repository. The supported slice is explicit, and unsupported constructs remain visible rather than being presented as covered.</p>

<div class="callout"><strong>Current boundary:</strong> CARTOGRAPH is pre-alpha. Its local TypeScript/Express slice and bounded adapters are implemented; stable identity across refactors and broader framework coverage remain open work. The project page lists the support table and limitations.</div>

<h2 id="unknown-is-a-useful-result">“Unknown” is a useful result</h2>

<p>I want architecture tools to help people ask better questions, not replace judgment with a polished picture. An evidence-backed edge can be inspected. An unresolved edge can be investigated. A guessed edge can quietly become part of someone’s mental model.</p>

<p>For this kind of tool, “I don’t know yet” is not a broken report. It is part of the report—and often the most useful thing in it.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[A diagram becomes dangerous when it draws a route it has not verified. For architecture tooling, uncertainty belongs in the output.]]></summary></entry><entry><title type="html">A copy is not convergence.</title><link href="https://alisina.is-a.dev/notes/a-copy-is-not-convergence/" rel="alternate" type="text/html" title="A copy is not convergence." /><published>2026-09-23T13:00:00+02:00</published><updated>2026-09-23T13:00:00+02:00</updated><id>https://alisina.is-a.dev/notes/a-copy-is-not-convergence</id><content type="html" xml:base="https://alisina.is-a.dev/notes/a-copy-is-not-convergence/"><![CDATA[<p>A successful write is a local fact. A system with more than one node has a second question to answer: did the other copies get there, too—and what happens if they didn’t?</p>

<p>That’s why I keep the <em>put</em> path and the repair path separate in <a href="https://github.com/AlisinaDevelo/StreamHive">StreamHive</a>. A one-shot put waits for a matching acknowledgement and retries within a finite budget. That can handle a lost acknowledgement or a short interruption. It cannot promise that a peer which was offline for hours will catch up.</p>

<figure>
  <svg viewBox="0 0 620 132" role="img" aria-labelledby="repair-title repair-desc">
    <title id="repair-title">The repair loop in four bounded steps</title>
    <desc id="repair-desc">A write is acknowledged, peers compare inventories, missing keys are requested, and repair continues within a bounded budget.</desc>
    <defs><marker id="repair-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0 10 5 0 10z" fill="#31488f" /></marker></defs>
    <path d="M118 57H177M273 57h59M426 57h58" stroke="#31488f" stroke-width="1.5" marker-end="url(#repair-arrow)" />
    <g font-family="monospace" text-anchor="middle">
      <rect x="12" y="25" width="106" height="64" rx="2" fill="#f8f7f1" stroke="#a8afa4" /><text x="65" y="51" fill="#20241f" font-size="10">WRITE</text><text x="65" y="69" fill="#69716a" font-size="8">ack or retry</text>
      <rect x="177" y="25" width="96" height="64" rx="2" fill="#f8f7f1" stroke="#a8afa4" /><text x="225" y="51" fill="#20241f" font-size="10">COMPARE</text><text x="225" y="69" fill="#69716a" font-size="8">key inventory</text>
      <rect x="332" y="25" width="94" height="64" rx="2" fill="#d1ed62" stroke="#20241f" /><text x="379" y="51" fill="#20241f" font-size="10">REPAIR</text><text x="379" y="69" fill="#4f5b3b" font-size="8">missing blobs</text>
      <rect x="484" y="25" width="124" height="64" rx="2" fill="#f8f7f1" stroke="#a8afa4" /><text x="546" y="51" fill="#20241f" font-size="10">CONTINUE</text><text x="546" y="69" fill="#69716a" font-size="8">within a budget</text>
    </g>
  </svg>
  <figcaption>One write has a finite retry budget. Catch-up is a separate, bounded loop.</figcaption>
</figure>

<h2 id="two-loops-two-guarantees">Two loops, two guarantees</h2>

<p>StreamHive’s peer inventory exchange is the slower loop. It runs at startup and periodically, compares paged inventories, and asks for keys the local store is missing. Repair responses are bounded; a large exchange can continue later instead of growing into unbounded work.</p>

<p>That distinction changes what a healthy signal means. “The write was accepted” describes one operation. “The peers are converging” describes a continuing process across time, restarts, network limits, and storage state. I want the system to expose enough counters and status to tell those stories apart.</p>

<h2 id="integrity-before-repair">Integrity before repair</h2>

<p>There is little value in repairing a replica with bytes that do not match the key they claim to represent. For content-addressed blobs, StreamHive checks key and content at apply and repair-source boundaries. A damaged source is skipped rather than sent as a valid repair.</p>

<p>The hash does not solve every storage problem. It gives one precise check: these bytes match this content-derived key. It does not tell a node how to resolve conflicting application-level records or discover every peer in a changing network.</p>

<div class="callout"><strong>Boundary matters:</strong> StreamHive experiments with static-peer replication, bounded repair, and durable blob storage. It does not implement global discovery or conflict resolution. I prefer that boundary written down over a broader claim the code cannot support.</div>

<h2 id="repair-is-normal-work">Repair is normal work</h2>

<p>Distributed storage is often introduced as “copy this object to another machine.” That is the postcard version. The operational version is a loop: accept, acknowledge, compare, repair, and report when the loop is limited or incomplete.</p>

<p>A copy gets the bytes somewhere once. A repair path gives the system a way to notice when reality drifted—and a bounded way to move it closer again.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Replication moves bytes. Convergence is the work that starts after a peer misses one, loses an acknowledgement, or comes back late.]]></summary></entry></feed>