JLongshore's picture
Deploy intentsolutions.io from CI — 51a73eaa5ce48cc2db63fd6bf93ed17b9ab737f1
a3d6e8c
Raw
History Blame Contribute Delete
53.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="generator" content="Astro v5.18.2">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- SEO -->
<title>Engine to Product: Three Interfaces, One Codebase | Field Notes</title>
<link rel="canonical" href="https://startaitools.com/posts/engine-to-product-three-interfaces-one-codebase/">
<meta name="description" content="Building CLI, REST API, and frontend wizard interfaces on top of a comparison engine. The jump from library to usable product in cad-dxf-agent.">
<meta name="robots" content="index, follow">
<meta property="og:title" content="Engine to Product: Three Interfaces, One Codebase | Field Notes">
<meta property="og:type" content="website">
<meta property="og:image" content="https://intentsolutions.io/og-image.svg">
<meta property="og:url" content="https://startaitools.com/posts/engine-to-product-three-interfaces-one-codebase/">
<meta property="og:description" content="Building CLI, REST API, and frontend wizard interfaces on top of a comparison engine. The jump from library to usable product in cad-dxf-agent.">
<meta property="og:site_name" content="intent solutions io">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Engine to Product: Three Interfaces, One Codebase | Field Notes">
<meta name="twitter:image" content="https://intentsolutions.io/og-image.svg">
<meta name="twitter:description" content="Building CLI, REST API, and frontend wizard interfaces on top of a comparison engine. The jump from library to usable product in cad-dxf-agent.">
<!-- Site-level structured data (Organization + WebSite) -->
<script type="application/ld+json">[{"@context":"https://schema.org","@type":"Organization","name":"Intent Solutions","url":"https://intentsolutions.io/","logo":"https://intentsolutions.io/favicon.svg","sameAs":["https://github.com/jeremylongshore","https://linkedin.com/in/jeremylongshore","https://claudecodeplugins.io"]},{"@context":"https://schema.org","@type":"WebSite","name":"Intent Solutions","url":"https://intentsolutions.io/"}]</script>
<!-- Umami analytics — self-hosted at analytics.intentsolutions.io (Intent
Solutions VPS). Replaces the Firebase Analytics block on 2026-06-10 to
remove the last GCP dependency from the marketing site. Umami auto-
tracks page views; emit custom events with window.umami.track(name, props). -->
<script defer src="https://analytics.intentsolutions.io/script.js" data-website-id="474bce85-f97d-409c-aba5-1e1ff36ee571" data-domains="intentsolutions.io"></script>
<link rel="stylesheet" href="/_astro/a2a.CeHTlFWO.css">
<style>.nav-panel[data-astro-cid-4lunwx2e]{position:absolute;top:100%;left:50%;transform:translate(-50%) translateY(6px);padding-top:10px;opacity:0;visibility:hidden;transition:opacity .18s ease,transform .18s ease,visibility .18s;z-index:60}.nav-group[data-astro-cid-4lunwx2e]:hover .nav-panel[data-astro-cid-4lunwx2e],.nav-group[data-astro-cid-4lunwx2e]:focus-within .nav-panel[data-astro-cid-4lunwx2e],.nav-group[data-astro-cid-4lunwx2e].open .nav-panel[data-astro-cid-4lunwx2e]{opacity:1;visibility:visible;transform:translate(-50%) translateY(0)}.nav-panel-inner[data-astro-cid-4lunwx2e]{min-width:270px;background:#09090bf5;border:1px solid rgba(63,63,70,.6);border-radius:.75rem;padding:.5rem;box-shadow:0 20px 50px #0000008c,0 0 0 1px #f973160f;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px)}.nav-item[data-astro-cid-4lunwx2e]{display:flex;flex-direction:column;gap:.1rem;padding:.55rem .7rem;border-radius:.5rem;text-decoration:none;transition:background .15s ease}.nav-item[data-astro-cid-4lunwx2e]:hover{background:#f9731614}.nav-item-label[data-astro-cid-4lunwx2e]{font-size:.86rem;font-weight:600;color:#e4e4e7;transition:color .15s ease}.nav-item[data-astro-cid-4lunwx2e]:hover .nav-item-label[data-astro-cid-4lunwx2e]{color:#ff8a2a}.nav-item-desc[data-astro-cid-4lunwx2e]{font-size:.72rem;color:#71717a}.nav-ext[data-astro-cid-4lunwx2e]{font-size:.7rem;color:#71717a;margin-left:.3rem}.nav-chevron[data-astro-cid-4lunwx2e]{transition:transform .18s ease}.nav-group[data-astro-cid-4lunwx2e]:hover .nav-chevron[data-astro-cid-4lunwx2e],.nav-group[data-astro-cid-4lunwx2e]:focus-within .nav-chevron[data-astro-cid-4lunwx2e],.nav-group[data-astro-cid-4lunwx2e].open .nav-chevron[data-astro-cid-4lunwx2e]{transform:rotate(180deg)}.burger-bar[data-astro-cid-4lunwx2e]{display:block;width:18px;height:1.6px;background:currentColor;border-radius:2px;transition:transform .2s ease,opacity .2s ease}#nav-burger[data-astro-cid-4lunwx2e][aria-expanded=true] .burger-bar[data-astro-cid-4lunwx2e]:nth-child(1){transform:translateY(6.6px) rotate(45deg)}#nav-burger[data-astro-cid-4lunwx2e][aria-expanded=true] .burger-bar[data-astro-cid-4lunwx2e]:nth-child(2){opacity:0}#nav-burger[data-astro-cid-4lunwx2e][aria-expanded=true] .burger-bar[data-astro-cid-4lunwx2e]:nth-child(3){transform:translateY(-6.6px) rotate(-45deg)}
</style></head>
<body>
<nav class="sticky top-0 z-50 backdrop-blur-md bg-zinc-950/70 border-b border-zinc-800/60" data-astro-cid-4lunwx2e>
<div class="container mx-auto px-6 sm:px-8" data-astro-cid-4lunwx2e>
<div class="flex items-center justify-between h-16" data-astro-cid-4lunwx2e>
<a href="/" class="home-link" aria-label="Intent Solutions home" style="gap: 0.6rem;" data-astro-cid-4lunwx2e>
<!-- Brand mark — actual logo render -->
<img src="/images/logo-mark.png" alt="" width="28" height="28" aria-hidden="true" style="flex-shrink: 0; display: block;" data-astro-cid-4lunwx2e>
<span class="hidden sm:inline-block font-display font-bold tracking-tight text-zinc-50" style="font-size: 1.02rem; line-height: 1;" data-astro-cid-4lunwx2e>
Intent<span style="color: #FF7A1A;" data-astro-cid-4lunwx2e>&nbsp;Solutions</span>
</span>
</a>
<!-- Desktop navigation -->
<div class="hidden lg:flex items-center gap-1" data-astro-cid-4lunwx2e>
<div class="nav-group relative" data-astro-cid-4lunwx2e>
<button type="button" class="nav-trigger flex items-center gap-1.5 px-3 py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth rounded-md" aria-expanded="false" aria-haspopup="true" data-astro-cid-4lunwx2e>
Solutions
<svg class="nav-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" data-astro-cid-4lunwx2e>
<path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" data-astro-cid-4lunwx2e></path>
</svg>
</button>
<div class="nav-panel" role="menu" data-astro-cid-4lunwx2e>
<div class="nav-panel-inner" data-astro-cid-4lunwx2e>
<a href="/#solutions" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Claude Code Systems
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Custom setups, plugins &amp; team training</span>
</a><a href="/agents" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
AI Agents
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Intent Agent Models for any workflow</span>
</a><a href="/private-ai" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Private AI
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>ChatGPT-class AI on your own cloud</span>
</a><a href="/automation" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Automation
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>n8n workflows that erase manual work</span>
</a><a href="/cloud" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Cloud &amp; Data
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Google Cloud-native architectures</span>
</a>
</div>
</div>
</div><div class="nav-group relative" data-astro-cid-4lunwx2e>
<button type="button" class="nav-trigger flex items-center gap-1.5 px-3 py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth rounded-md" aria-expanded="false" aria-haspopup="true" data-astro-cid-4lunwx2e>
Resources
<svg class="nav-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" data-astro-cid-4lunwx2e>
<path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" data-astro-cid-4lunwx2e></path>
</svg>
</button>
<div class="nav-panel" role="menu" data-astro-cid-4lunwx2e>
<div class="nav-panel-inner" data-astro-cid-4lunwx2e>
<a href="/field-notes" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Field Notes
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Notes from the build</span>
</a><a href="/projects" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Projects
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Shipped work &amp; case studies</span>
</a><a href="https://claudecodeplugins.io" target="_blank" rel="noopener noreferrer" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Plugin Marketplace
<span class="nav-ext" aria-hidden="true" data-astro-cid-4lunwx2e></span>
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>270+ Claude Code plugins</span>
</a>
</div>
</div>
</div><div class="nav-group relative" data-astro-cid-4lunwx2e>
<button type="button" class="nav-trigger flex items-center gap-1.5 px-3 py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth rounded-md" aria-expanded="false" aria-haspopup="true" data-astro-cid-4lunwx2e>
Work With Us
<svg class="nav-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" data-astro-cid-4lunwx2e>
<path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" data-astro-cid-4lunwx2e></path>
</svg>
</button>
<div class="nav-panel" role="menu" data-astro-cid-4lunwx2e>
<div class="nav-panel-inner" data-astro-cid-4lunwx2e>
<a href="/learn" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Learn
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Executive onramps &amp; team enablement</span>
</a><a href="/colab" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Colab
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Implementation partner in your repo</span>
</a><a href="/resellers" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Resellers
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Distribution partner program</span>
</a><a href="/#contact" class="nav-item" role="menuitem" data-astro-cid-4lunwx2e>
<span class="nav-item-label" data-astro-cid-4lunwx2e>
Contact
</span>
<span class="nav-item-desc" data-astro-cid-4lunwx2e>Start a conversation</span>
</a>
</div>
</div>
</div>
<a href="/about" class="px-3 py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>About</a>
<a href="https://calendar.app.google/Wqbt8EJuEh5xvvV58" target="_blank" rel="noopener" class="btn-primary btn-sm text-sm ml-4" data-astro-cid-4lunwx2e>
Book a Call
</a>
</div>
<!-- Mobile / tablet: CTA + hamburger -->
<div class="lg:hidden flex items-center gap-2" data-astro-cid-4lunwx2e>
<a href="https://calendar.app.google/Wqbt8EJuEh5xvvV58" target="_blank" rel="noopener" class="btn-primary btn-sm text-sm whitespace-nowrap" data-astro-cid-4lunwx2e>
Book a Call
</a>
<button type="button" id="nav-burger" class="flex flex-col items-center justify-center w-11 h-11 gap-[5px] rounded-md text-zinc-300" aria-expanded="false" aria-controls="nav-mobile" aria-label="Open menu" data-astro-cid-4lunwx2e>
<span class="burger-bar" data-astro-cid-4lunwx2e></span>
<span class="burger-bar" data-astro-cid-4lunwx2e></span>
<span class="burger-bar" data-astro-cid-4lunwx2e></span>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<div id="nav-mobile" class="lg:hidden hidden border-t border-zinc-800/60 bg-zinc-950/95 backdrop-blur-md" data-astro-cid-4lunwx2e>
<div class="container mx-auto px-6 py-5 flex flex-col gap-5" data-astro-cid-4lunwx2e>
<div data-astro-cid-4lunwx2e>
<p class="font-display text-[0.62rem] font-bold uppercase tracking-[0.26em] mb-2" style="color: #FF7A1A;" data-astro-cid-4lunwx2e>
Solutions
</p>
<div class="flex flex-col" data-astro-cid-4lunwx2e>
<a href="/#solutions" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Claude Code Systems
</a><a href="/agents" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
AI Agents
</a><a href="/private-ai" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Private AI
</a><a href="/automation" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Automation
</a><a href="/cloud" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Cloud &amp; Data
</a>
</div>
</div><div data-astro-cid-4lunwx2e>
<p class="font-display text-[0.62rem] font-bold uppercase tracking-[0.26em] mb-2" style="color: #FF7A1A;" data-astro-cid-4lunwx2e>
Resources
</p>
<div class="flex flex-col" data-astro-cid-4lunwx2e>
<a href="/field-notes" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Field Notes
</a><a href="/projects" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Projects
</a><a href="https://claudecodeplugins.io" target="_blank" rel="noopener noreferrer" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Plugin Marketplace ↗
</a>
</div>
</div><div data-astro-cid-4lunwx2e>
<p class="font-display text-[0.62rem] font-bold uppercase tracking-[0.26em] mb-2" style="color: #FF7A1A;" data-astro-cid-4lunwx2e>
Work With Us
</p>
<div class="flex flex-col" data-astro-cid-4lunwx2e>
<a href="/learn" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Learn
</a><a href="/colab" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Colab
</a><a href="/resellers" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Resellers
</a><a href="/#contact" class="py-2 text-sm text-zinc-300 hover:text-zinc-50 transition-smooth" data-astro-cid-4lunwx2e>
Contact
</a>
</div>
</div>
<a href="/about" class="py-1 text-sm font-medium text-zinc-200" data-astro-cid-4lunwx2e>About</a>
</div>
</div>
</nav>
<script type="module">function d(){const n=document.getElementById("nav-burger"),a=document.getElementById("nav-mobile");n&&a&&!n.dataset.bound&&(n.dataset.bound="1",n.addEventListener("click",()=>{const e=n.getAttribute("aria-expanded")==="true";n.setAttribute("aria-expanded",String(!e)),n.setAttribute("aria-label",e?"Open menu":"Close menu"),a.classList.toggle("hidden",e)})),document.querySelectorAll(".nav-group").forEach(e=>{const t=e.querySelector(".nav-trigger");!t||t.dataset.bound||(t.dataset.bound="1",t.addEventListener("click",s=>{s.stopPropagation();const r=e.classList.contains("open");document.querySelectorAll(".nav-group.open").forEach(o=>{o.classList.remove("open"),o.querySelector(".nav-trigger")?.setAttribute("aria-expanded","false")}),e.classList.toggle("open",!r),t.setAttribute("aria-expanded",String(!r))}))}),document.documentElement.dataset.navBound||(document.documentElement.dataset.navBound="1",document.addEventListener("click",()=>{document.querySelectorAll(".nav-group.open").forEach(e=>{e.classList.remove("open"),e.querySelector(".nav-trigger")?.setAttribute("aria-expanded","false")})}),document.addEventListener("keydown",e=>{e.key==="Escape"&&document.querySelectorAll(".nav-group.open").forEach(t=>{t.classList.remove("open"),t.querySelector(".nav-trigger")?.setAttribute("aria-expanded","false")})}))}document.addEventListener("DOMContentLoaded",d);document.addEventListener("astro:page-load",d);</script>
<main class="min-h-screen bg-zinc-950">
<article class="py-16 sm:py-24">
<div class="container mx-auto px-6 sm:px-8 max-w-3xl">
<a href="/field-notes/" class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-300 transition-smooth mb-10">
&larr; Field Notes
</a>
<header class="mb-12">
<div class="flex flex-wrap items-center gap-3 text-sm text-zinc-500 mb-4">
<time datetime="2026-02-28">February 28, 2026</time>
<span class="text-zinc-700">&middot;</span>
<div class="flex flex-wrap gap-2">
<span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
ai-agents
</span><span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
python
</span><span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
fastapi
</span><span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
architecture
</span><span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
full-stack
</span><span class="text-xs px-2 py-0.5 rounded-full bg-zinc-800/60 text-zinc-500 border border-zinc-700/30">
cad
</span>
</div>
</div>
<h1 class="text-h1 font-bold text-zinc-50 mb-4">Engine to Product: Three Interfaces, One Codebase</h1>
<p class="text-body-lg text-zinc-400">Building CLI, REST API, and frontend wizard interfaces on top of a comparison engine. The jump from library to usable product in cad-dxf-agent.</p>
</header>
<div class="prose-field-notes">
<p>You built the engine. Four layers of deterministic comparison, 1,875 tests, confidence scoring, alignment ladder. Great. Nobody can use it.</p>
<p>A library isn’t a product. The comparison engine in <a href="https://github.com/jeremylongshore/cad-dxf-agent">cad-dxf-agent</a> could compare two DXF files with sub-thousandth precision — but only if you imported it in a Python script, instantiated the right classes, and knew the method signatures. That’s fine for me. It’s useless for the machinist reviewing revision changes.</p>
<p>This week was about wiring three interfaces onto one engine: a CLI for power users, a REST API for integrations, and a frontend wizard for everyone else. Four PRs, 76 new tests.</p>
<h2 id="the-problem-that-forced-manual-alignment">The Problem That Forced Manual Alignment</h2>
<p>The automatic alignment ladder (identity → anchor → feature) handles most drawings. But some files have huge coordinate offsets — one sheet drafted at origin, the revision drafted 10,000 inches away — and no shared anchor blocks to triangulate from.</p>
<p>The ladder tries all three strategies and fails gracefully. But “we can’t align these” isn’t an acceptable answer when the user <em>knows</em> which points correspond.</p>
<p>PR #52 added <code>--control-points</code> — user supplies 2-3 point pairs, and the system computes the transform directly:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#6A737D"># 1 point pair = pure translation</span></span>
<span class="line"><span style="color:#6A737D"># 2+ point pairs = Kabsch SVD for full rigid transform (rotation + translation)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">def</span><span style="color:#B392F0"> align_with_control_points</span><span style="color:#E1E4E8">(</span></span>
<span class="line"><span style="color:#E1E4E8"> pairs: list[tuple[Point2D, Point2D]],</span></span>
<span class="line"><span style="color:#E1E4E8">) -> AlignmentResult:</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#79B8FF"> len</span><span style="color:#E1E4E8">(pairs) </span><span style="color:#F97583">==</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8"> src, tgt </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> pairs[</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">]</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8"> AlignmentResult(</span></span>
<span class="line"><span style="color:#FFAB70"> translation</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">tgt </span><span style="color:#F97583">-</span><span style="color:#E1E4E8"> src,</span></span>
<span class="line"><span style="color:#FFAB70"> rotation</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">np.eye(</span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#FFAB70"> confidence</span><span style="color:#F97583">=</span><span style="color:#79B8FF">1.0</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#FFAB70"> method</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"manual_translation"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> )</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"> # 2+ pairs: Kabsch algorithm</span></span>
<span class="line"><span style="color:#E1E4E8"> src_pts </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> np.array([p[</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">for</span><span style="color:#E1E4E8"> p </span><span style="color:#F97583">in</span><span style="color:#E1E4E8"> pairs])</span></span>
<span class="line"><span style="color:#E1E4E8"> tgt_pts </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> np.array([p[</span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">for</span><span style="color:#E1E4E8"> p </span><span style="color:#F97583">in</span><span style="color:#E1E4E8"> pairs])</span></span>
<span class="line"><span style="color:#E1E4E8"> rotation, translation </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> kabsch_align(src_pts, tgt_pts)</span></span>
<span class="line"><span style="color:#E1E4E8"> residual </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> compute_rms_residual(src_pts, tgt_pts, rotation, translation)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8"> AlignmentResult(</span></span>
<span class="line"><span style="color:#FFAB70"> translation</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">translation,</span></span>
<span class="line"><span style="color:#FFAB70"> rotation</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">rotation,</span></span>
<span class="line"><span style="color:#FFAB70"> confidence</span><span style="color:#F97583">=</span><span style="color:#79B8FF">max</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">1.0</span><span style="color:#F97583"> -</span><span style="color:#E1E4E8"> residual),</span></span>
<span class="line"><span style="color:#FFAB70"> method</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"manual_rigid"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> )</span></span></code></pre>
<p>Manual path takes priority over the automatic ladder. If you supply control points, the ladder doesn’t even run. 10 new tests covering single-point translation, multi-point rigid transform, and degenerate inputs.</p>
<h2 id="integration-tests-through-the-real-user-path">Integration Tests Through the Real User Path</h2>
<p>PR #53 filled a gap that was bothering me. The alignment ladder had unit tests for each step in isolation — anchor matching, feature extraction, Kabsch math. But nobody was testing through <code>ComparisonEngine</code>, the class users actually call.</p>
<p>7 integration tests covering the real paths:</p>
<ul>
<li>Identical pair → identity alignment (no transform needed)</li>
<li>Offset pair → anchor or feature alignment kicks in</li>
<li>Rotated pair → rigid transform with rotation matrix</li>
<li>Manual control points → bypass the ladder entirely</li>
<li>Impossible alignment → confidence 0.0 with guidance message</li>
</ul>
<p>The impossible-alignment test matters most. When the system can’t align two files, it should say so clearly — not return a garbage transform with 0.12 confidence that silently corrupts downstream matching.</p>
<h2 id="the-api-six-granular-endpoints">The API: Six Granular Endpoints</h2>
<p>This is where the product lives. PR #54 added the revision pipeline as REST endpoints — not one monolithic <code>/compare</code> endpoint, but six steps the client controls:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>POST /api/revision/upload — upload revision DXF to existing session</span></span>
<span class="line"><span>POST /api/revision/align — run alignment (auto or manual control points)</span></span>
<span class="line"><span>POST /api/revision/diff — compare &#x26; generate revision ops</span></span>
<span class="line"><span>POST /api/revision/approve — approve/reject individual ops by op_id</span></span>
<span class="line"><span>POST /api/revision/apply — apply approved ops, export bundle</span></span>
<span class="line"><span>GET /api/revision/download — download bundle as zip</span></span></code></pre>
<p>Each endpoint advances session state by exactly one step. The client can’t call <code>/diff</code> before <code>/align</code>. Can’t call <code>/apply</code> before <code>/approve</code>. State machine enforced server-side.</p>
<p>The session object grew to track the pipeline:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#F97583">class</span><span style="color:#B392F0"> SessionState</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#6A737D"> # ... existing fields ...</span></span>
<span class="line"><span style="color:#E1E4E8"> revision_path: Path </span><span style="color:#F97583">|</span><span style="color:#79B8FF"> None</span></span>
<span class="line"><span style="color:#E1E4E8"> alignment_result: AlignmentResult </span><span style="color:#F97583">|</span><span style="color:#79B8FF"> None</span></span>
<span class="line"><span style="color:#E1E4E8"> revision_ops: list[RevisionOp] </span><span style="color:#F97583">|</span><span style="color:#79B8FF"> None</span></span>
<span class="line"><span style="color:#E1E4E8"> approval_set: dict[</span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">, Literal[</span><span style="color:#9ECBFF">"approve"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"reject"</span><span style="color:#E1E4E8">]]</span></span>
<span class="line"><span style="color:#E1E4E8"> apply_result: ApplyResult </span><span style="color:#F97583">|</span><span style="color:#79B8FF"> None</span></span>
<span class="line"><span style="color:#E1E4E8"> bundle_dir: Path </span><span style="color:#F97583">|</span><span style="color:#79B8FF"> None</span></span></code></pre>
<p>Why granular endpoints instead of one big POST? Because the user needs to <em>review</em> between steps. Upload → align → see the diff → approve some ops, reject others → apply only what’s approved → download. Each step is a decision point.</p>
<h2 id="what-gemini-caught-in-review">What Gemini Caught in Review</h2>
<p>This is the kind of thing that justifies AI code review. In the approval endpoint, I had:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#E1E4E8">approval_set: dict[</span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">, Literal[</span><span style="color:#9ECBFF">"approve"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"reject"</span><span style="color:#E1E4E8">]]</span></span></code></pre>
<p>Gemini flagged it: <code>Literal["approve", "reject"]</code> as a type hint is fine for static analysis, but FastAPI deserializes request bodies at runtime. A client sending <code>"approv"</code> (typo) would pass through without validation because <code>Literal</code> isn’t enforced at the Pydantic level unless you explicitly configure it.</p>
<p>The fix was adding a Pydantic validator:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="python"><code><span class="line"><span style="color:#F97583">class</span><span style="color:#B392F0"> ApprovalRequest</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">BaseModel</span><span style="color:#E1E4E8">):</span></span>
<span class="line"><span style="color:#E1E4E8"> op_id: </span><span style="color:#79B8FF">str</span></span>
<span class="line"><span style="color:#E1E4E8"> decision: </span><span style="color:#79B8FF">str</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0"> @field_validator</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"decision"</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#B392F0"> @</span><span style="color:#79B8FF">classmethod</span></span>
<span class="line"><span style="color:#F97583"> def</span><span style="color:#B392F0"> validate_decision</span><span style="color:#E1E4E8">(cls, v: </span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">) -> </span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> v </span><span style="color:#F97583">not</span><span style="color:#F97583"> in</span><span style="color:#E1E4E8"> (</span><span style="color:#9ECBFF">"approve"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"reject"</span><span style="color:#E1E4E8">):</span></span>
<span class="line"><span style="color:#F97583"> raise</span><span style="color:#79B8FF"> ValueError</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">f</span><span style="color:#9ECBFF">"decision must be 'approve' or 'reject', got '</span><span style="color:#79B8FF">{</span><span style="color:#E1E4E8">v</span><span style="color:#79B8FF">}</span><span style="color:#9ECBFF">'"</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8"> v</span></span></code></pre>
<p>It also caught dead <code>_control_points</code> code — a parameter that was being accepted but never passed through to the alignment function. Two real bugs, caught before merge.</p>
<h2 id="the-frontend-wizard-step-4">The Frontend: Wizard Step 4</h2>
<p>The React frontend wraps these endpoints in a step-by-step wizard. The interesting piece is step 4 — applying approved changes and downloading the bundle.</p>
<p><code>api.js</code> got two new functions:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="javascript"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> revisionApply</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">sessionId</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#B392F0"> post</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`/api/revision/apply`</span><span style="color:#E1E4E8">, { session_id: sessionId });</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> revisionDownloadUrl</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">sessionId</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583"> return</span><span style="color:#9ECBFF"> `${</span><span style="color:#79B8FF">API_BASE</span><span style="color:#9ECBFF">}/api/revision/download?session_id=${</span><span style="color:#E1E4E8">sessionId</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">;</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
<p><code>useSession.js</code> tracks the new state: <code>revisionOps</code>, <code>revisionApplyResult</code>, <code>bundleReady</code>. The <code>PreviewPanel</code> shows an apply button gated on whether any ops have been approved, with badge counts showing approved/rejected/pending.</p>
<p>Nothing fancy. That’s the point. The frontend is thin because the API does the work.</p>
<h2 id="three-interfaces-one-engine">Three Interfaces, One Engine</h2>
<p>The CLI (<code>cad-revision</code>) has 6 subcommands that mirror the API: <code>upload</code>, <code>align</code>, <code>diff</code>, <code>approve</code>, <code>apply</code>, <code>download</code>. Same session state, same pipeline, same validation.</p>
<p>The REST API exposes that pipeline over HTTP with session management.</p>
<p>The frontend wizard wraps the API in a guided flow.</p>
<p>All three call the same <code>ComparisonEngine</code> and <code>RevisionApplier</code> underneath. The engine doesn’t know or care which interface is driving it. That’s the whole architecture: a thick core with thin shells.</p>
<h2 id="the-numbers">The Numbers</h2>
<table><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody><tr><td>PRs merged</td><td>4 (#52–#54 + frontend)</td></tr><tr><td>New tests</td><td>76 (10 + 7 + 21 + 38)</td></tr><tr><td>API endpoints added</td><td>6</td></tr><tr><td>CLI subcommands</td><td>6</td></tr><tr><td>Gemini review catches</td><td>2 real bugs</td></tr></tbody></table>
<p>The test count for PR #54 alone — 21 tests including a full end-to-end pipeline integration — is higher than many projects have total. That’s what happens when each endpoint has state preconditions that need to be verified.</p>
<h2 id="meanwhile-searchcarriers-gets-serious">Meanwhile: searchcarriers Gets Serious</h2>
<p>Parallel track: <a href="https://github.com/jeremylongshore/searchcarriers">searchcarriers</a> (freight broker MCP servers) got 80 behavioral integration tests across all 5 handlers — carrier-intel, risk-engine, ops-reporter, watchdog, api-bridge. Uses <code>respx</code> to intercept HTTP at the transport layer so tests hit real handler logic without live API calls.</p>
<p>The ops-reporter work exposed a classic integration bug: the real API returns <code>snake_case</code> fields, but the handlers expected <code>camelCase</code>. A field normalization layer fixes the mismatch at the boundary. Also shipped curated CSV exports (~22 columns instead of the 143-column raw dump) and PDF reports via Jinja2 + WeasyPrint.</p>
<p>45 new tests there, bringing searchcarriers to 197 total.</p>
<h2 id="the-lesson">The Lesson</h2>
<p>Making something usable takes as much engineering as making it work. The comparison engine was four PRs of dense algorithm work. The product layer — CLI, API, frontend, tests, docs — was another four PRs of equal density.</p>
<p>The difference: the engine PRs were fun. The product PRs were discipline. Validation, state management, error messages, approval workflows, bundle packaging, download endpoints. None of it is glamorous. All of it is necessary.</p>
<p>A library that only you can use is a prototype. A product that a machinist can point-and-click through is software.</p>
<hr>
<p><em>Related posts:</em></p>
<ul>
<li><a href="https://startaitools.com/posts/deterministic-dxf-comparison-engine-one-day-build/">Building a Deterministic DXF Comparison Engine in One Day</a> — the engine this post builds on: E1-E4 layers, 1,875 tests</li>
<li><a href="https://startaitools.com/posts/silent-killer-bare-catch-blocks-hide-failures/">The Silent Killer: How Bare catch {} Blocks Hide Failures</a> — debugging invisible frontend failures in the same codebase</li>
<li><a href="https://startaitools.com/posts/python-class-identity-mismatch-ci-debugging-guide/">Python Class Identity Mismatch: The CI Bug That Broke 9 PRs</a> — another case where identity semantics caused subtle breakage</li>
</ul>
</div>
</div>
</article>
</main>
<style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="ZHlEDc" prefix="r1" component-url="/_astro/Footer.D_bfTucM.js" component-export="default" renderer-url="/_astro/client.B_PwMJWB.js" props="{}" ssr client="load" opts="{&quot;name&quot;:&quot;Footer&quot;,&quot;value&quot;:true}" await-children><footer style="background:rgb(9 9 11);border-top:1px solid rgba(39,39,42,0.6);padding:4.5rem 0 2rem"><div style="max-width:72rem;margin:0 auto;padding:0 2rem"><div class="footer-top" style="display:grid;grid-template-columns:1.4fr 0.8fr 0.9fr 1.4fr;gap:2.5rem;margin-bottom:3.5rem"><div><div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem"><span style="width:0.5rem;height:0.5rem;border-radius:50%;background:rgb(251 146 60);box-shadow:0 0 8px rgba(249,115,22,0.55);flex-shrink:0"></span><span style="font-family:&#x27;Syne&#x27;, system-ui, sans-serif;font-size:0.95rem;font-weight:700;letter-spacing:0.04em;color:rgb(228 228 231)">Intent <span style="color:#FF7A1A">Solutions</span></span></div><p style="font-size:0.875rem;color:rgb(82 82 91);line-height:1.7;max-width:22rem;margin-bottom:1.5rem">creating industries that don&#x27;t exist</p><p style="font-family:&#x27;Syne&#x27;, system-ui, sans-serif;font-size:0.6rem;font-weight:700;letter-spacing:0.18em;text-transform:uppercase;color:rgb(63 63 70);margin:0">gulf shores, alabama</p></div><div><p style="font-family:&#x27;Syne&#x27;, system-ui, sans-serif;font-size:0.6rem;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:rgb(82 82 91);margin-bottom:1.25rem">Services</p><a href="/learn" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Learn</a><a href="/colab" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Colab</a><a href="/agents" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Agents</a><a href="/private-ai" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Private AI</a><a href="/automation" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Automation</a><a href="/cloud" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Cloud &amp; Data</a><a href="/resellers" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Resellers</a></div><div><p style="font-family:&#x27;Syne&#x27;, system-ui, sans-serif;font-size:0.6rem;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:rgb(82 82 91);margin-bottom:1.25rem">Resources</p><a href="/field-notes" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Field Notes</a><a href="https://claudecodeplugins.io" target="_blank" rel="noopener noreferrer" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Claude Code Plugins</a><a href="https://github.com/jeremylongshore" target="_blank" rel="noopener noreferrer" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">GitHub</a><a href="https://huggingface.co/intent-solutions-io" target="_blank" rel="noopener noreferrer" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Hugging Face</a><a href="https://whop.com/claude-code-cowork-accelerator/" target="_blank" rel="noopener noreferrer" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">Cowork Accelerator</a><div style="margin-top:1.25rem"><a href="mailto:jeremy@intentsolutions.io" style="font-size:0.875rem;color:rgb(113 113 122);text-decoration:none;transition:color 0.2s ease;display:block;padding-bottom:0.5rem">jeremy@intentsolutions.io</a></div></div><div><p style="font-family:&#x27;Syne&#x27;, system-ui, sans-serif;font-size:0.6rem;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:rgb(82 82 91);margin-bottom:1.25rem">FAQ</p><div style="display:flex;flex-direction:column"><details class="footer-faq"><summary><span>What exactly is a Claude Code setup?</span><svg class="faq-chevron" width="11" height="11" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></summary><p>We configure Claude Code for your specific tech stack, team size, and workflows — custom CLAUDE.md files, curated plugin packs, slash commands, MCP connections, and hooks, tuned so your team gets value from day one.</p></details><details class="footer-faq"><summary><span>Do I need to know how to code?</span><svg class="faq-chevron" width="11" height="11" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></summary><p>Not at all. The Learn track is built for decision-makers and team leads — we show you what Claude Code can do and how it fits your org, no coding required.</p></details><details class="footer-faq"><summary><span>What&#x27;s included in the discovery call?</span><svg class="faq-chevron" width="11" height="11" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></summary><p>A 30-minute conversation where we learn about your team, workflows, and goals. You&#x27;ll get honest recommendations — or an honest &#x27;not a fit.&#x27; No pressure, no pitch deck.</p></details><details class="footer-faq"><summary><span>What happens after the initial setup?</span><svg class="faq-chevron" width="11" height="11" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M1.5 3.5 L5 7 L8.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path></svg></summary><p>Every engagement includes a support window. We stay available for questions, plugin tweaks, and workflow adjustments — enterprise clients get ongoing retainers with quarterly workshops.</p></details></div></div></div><div aria-hidden="true" style="height:1px;background:linear-gradient(to right, transparent, rgba(39,39,42,0.8), transparent);margin-bottom:1.75rem"></div><div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem"><p style="font-size:0.75rem;color:rgb(63 63 70);margin:0">© 2025 intent solutions io. all rights reserved.</p><div style="display:flex;align-items:center;gap:1.5rem"><a href="/terms" style="font-size:0.72rem;color:rgb(63 63 70);text-decoration:none;transition:color 0.2s ease">Terms of Service</a><a href="/privacy" style="font-size:0.72rem;color:rgb(63 63 70);text-decoration:none;transition:color 0.2s ease">Privacy Policy</a><a href="/acceptable-use" style="font-size:0.72rem;color:rgb(63 63 70);text-decoration:none;transition:color 0.2s ease">Acceptable Use</a></div></div></div></footer><!--astro:end--></astro-island>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"Engine to Product: Three Interfaces, One Codebase","description":"Building CLI, REST API, and frontend wizard interfaces on top of a comparison engine. The jump from library to usable product in cad-dxf-agent.","datePublished":"2026-02-28","author":{"@type":"Person","name":"Jeremy Longshore"},"publisher":{"@type":"Organization","name":"intent solutions io","url":"https://intentsolutions.io"},"mainEntityOfPage":"https://startaitools.com/posts/engine-to-product-three-interfaces-one-codebase/"}</script>
</body></html>