<?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: Fabibi</title>
    <description>The latest articles on DEV Community by Fabibi (@fabibi).</description>
    <link>https://dev.to/fabibi</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3282899%2Fee7643cb-d230-4b2f-8788-b472ce28b0e8.png</url>
      <title>DEV Community: Fabibi</title>
      <link>https://dev.to/fabibi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fabibi"/>
    <language>en</language>
    <item>
      <title>Your Coding Agent Doesn't Need Better Prompts. It Needs a Contract.</title>
      <dc:creator>Fabibi</dc:creator>
      <pubDate>Sat, 02 May 2026 18:31:44 +0000</pubDate>
      <link>https://dev.to/fabibi/your-coding-agent-doesnt-need-better-prompts-it-needs-a-contract-572k</link>
      <guid>https://dev.to/fabibi/your-coding-agent-doesnt-need-better-prompts-it-needs-a-contract-572k</guid>
      <description>&lt;p&gt;&lt;em&gt;How I structured a repo to make AI agent drift visible before it ships.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The most dangerous failure mode I've seen in agentic coding workflows is not broken code. Broken code is at least visible.&lt;/p&gt;

&lt;p&gt;The dangerous failure is plausible code: code that passes tests, implements something close to the request, and quietly expands the product surface in a direction nobody approved.&lt;/p&gt;

&lt;p&gt;No bug. No crash. Just drift.&lt;/p&gt;

&lt;p&gt;After a few months of fighting this, I stopped trying to write better prompts. The issue wasn't that the agent needed another instruction. The issue was that the repo didn't make authority clear enough. When behavior is implicit, agents fill gaps. The fix is not to ask them to stop doing that. The fix is to remove the gaps.&lt;/p&gt;

&lt;p&gt;By "contract," I don't mean a legal document or a heavyweight framework. I mean a written, testable description of observable behavior: commands, outputs, exit codes, schemas, determinism rules, and the boundaries of what implementation is allowed to change. It is not the full design. It is the part external consumers can observe and rely on.&lt;/p&gt;

&lt;p&gt;Prompts tell the agent how to behave. Contracts tell the repo how to reject behavior it did not authorize.&lt;/p&gt;




&lt;h2&gt;
  
  
  What quiet drift actually looks like
&lt;/h2&gt;

&lt;p&gt;I'm implementing a &lt;code&gt;scan --json&lt;/code&gt; command. The spec says the output should have three keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anchors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ask an agent to implement it. The agent implements it correctly, but also adds a &lt;code&gt;meta&lt;/code&gt; key with runtime diagnostics, because it seemed useful for debugging. The agent's implicit reasoning is understandable: the spec doesn't forbid it, and it makes the output more informative.&lt;/p&gt;

&lt;p&gt;The tests pass. All three expected keys are present and correct. No test checks for the absence of extra keys, because nobody thought to write that test. A permissive test like this would pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchors&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the actual output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anchors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/me/project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"durationMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff looks clean. Code review approves it. The feature ships.&lt;/p&gt;

&lt;p&gt;Three weeks later, a downstream consumer expecting the exact JSON schema starts rejecting responses because the schema has an unexpected field. Or worse: the &lt;code&gt;meta&lt;/code&gt; key leaks internal path information somewhere it shouldn't.&lt;/p&gt;

&lt;p&gt;The bug was not that the agent failed to implement the request. The bug was that the repo never made the boundary machine-checkable. A closed contract would have caught it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anchors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mappings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;findings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with JSON Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"anchors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"anchors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With structure, &lt;code&gt;docs/contract.md&lt;/code&gt; explicitly lists the output schema as closed: no extra keys allowed. &lt;code&gt;docs/evals.md&lt;/code&gt; validates byte-for-byte against a golden. Before handoff, &lt;code&gt;npm run check:goldens&lt;/code&gt; fails because the golden doesn't include &lt;code&gt;meta&lt;/code&gt;. The drift is caught before it ships.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fguwd9htutvn0kx76ymej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fguwd9htutvn0kx76ymej.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Permissive tests validate what they know to expect. A contract also rejects behavior nobody authorized.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The difference is not just "more tests." It's a repo where tests are derived from an explicit behavioral contract, so the constraint is machine-checkable before the agent has a chance to be helpful in the wrong direction. This does not make drift impossible. It makes drift visible: either the contract changes explicitly, or the evals fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three principles
&lt;/h2&gt;

&lt;p&gt;The workflow I built in &lt;a href="https://github.com/fstepho/anchormap" rel="noopener noreferrer"&gt;AnchorMap&lt;/a&gt; runs on three principles, stated at the top of &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This repo is document-driven. The working mode is &lt;code&gt;contract-first&lt;/code&gt;, &lt;code&gt;eval-driven&lt;/code&gt;, &lt;code&gt;scope-closed&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Contract-first&lt;/strong&gt; means observable behavior is defined before implementation begins. &lt;code&gt;docs/contract.md&lt;/code&gt; specifies commands, preconditions, outputs, exit codes, JSON schema, canonical key order, mutation guarantees, and determinism rules. If you want to add or change a behavior, you change the contract first. If an agent implements something that isn't in the contract, the workflow treats it as drift, not initiative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval-driven&lt;/strong&gt; means the contract is verified before implementation is considered done. &lt;code&gt;docs/evals.md&lt;/code&gt; defines fixtures, goldens, and release gates derived directly from &lt;code&gt;docs/contract.md&lt;/code&gt;. Successful JSON output is compared byte-for-byte. Failure cases require exact exit codes. Determinism is tested. Golden diffs are never accepted as noise. Any divergence is either classified as a defect or requires an explicit contract change first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope-closed&lt;/strong&gt; means agents cannot invent behavior. Not because they're trying to help. Not by inference. Not because something "seems right." Any observable behavior without traceability to &lt;code&gt;contract.md&lt;/code&gt; and &lt;code&gt;evals.md&lt;/code&gt; gets refused. This sounds restrictive. It is. That's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Copy this pattern: the minimal four-file bootstrap
&lt;/h2&gt;

&lt;p&gt;You don't need the full AnchorMap workflow to get most of the benefit. This is the smallest version that works: the four files I wish someone had handed me at the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt;: entry map only, not authority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Agent Instructions&lt;/span&gt;

This repo is document-driven.
Working mode: contract-first, eval-driven, scope-closed.

&lt;span class="gu"&gt;## This file is an entry map. docs/ wins on conflict.&lt;/span&gt;

&lt;span class="gu"&gt;## Work intake&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Product implementation → identify a task in docs/tasks.md first.
&lt;span class="p"&gt;-&lt;/span&gt; No task ID, no implementation.

&lt;span class="gu"&gt;## Authority&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; docs/contract.md: observable behavior
&lt;span class="p"&gt;-&lt;/span&gt; docs/evals.md: verification gates
&lt;span class="p"&gt;-&lt;/span&gt; docs/tasks.md: execution plan and current task state

&lt;span class="gu"&gt;## Never&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Modify docs/contract.md without explicit instruction.
&lt;span class="p"&gt;-&lt;/span&gt; Add observable behavior without traceability to docs/contract.md.
&lt;span class="p"&gt;-&lt;/span&gt; Auto-pick a task or auto-commit unless explicitly asked.
&lt;span class="p"&gt;-&lt;/span&gt; Fix a failing test before classifying the failure.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;docs/contract.md&lt;/code&gt;&lt;/strong&gt;: observable behavior only, no implementation details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Contract&lt;/span&gt;

&lt;span class="gu"&gt;## Commands&lt;/span&gt;

&lt;span class="gu"&gt;### scan&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Exit 0 on success.
&lt;span class="p"&gt;-&lt;/span&gt; stdout: JSON object with exactly these keys: anchors, mappings, findings.
&lt;span class="p"&gt;-&lt;/span&gt; No extra keys. Closed schema.
&lt;span class="p"&gt;-&lt;/span&gt; Exit 1 on error, stdout empty, stderr single-line diagnostic.

&lt;span class="gu"&gt;## Determinism&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Identical input → identical output, byte-for-byte.
&lt;span class="p"&gt;-&lt;/span&gt; No timestamps, PIDs, random IDs, or environment-derived values in output.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;docs/evals.md&lt;/code&gt;&lt;/strong&gt;: how the contract is verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Evals&lt;/span&gt;

&lt;span class="gu"&gt;## Principles&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Contract-first: oracles test observable outputs only.
&lt;span class="p"&gt;-&lt;/span&gt; Closed objects: goldens validate absence of extra keys.
&lt;span class="p"&gt;-&lt;/span&gt; No golden drift: any difference is a defect or an explicit contract change.

&lt;span class="gu"&gt;## Fixtures&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; fx01_scan_clean: empty repo, expect exit 0,
  golden: {"anchors":[],"mappings":[],"findings":[]}
&lt;span class="p"&gt;-&lt;/span&gt; fx02_scan_error: missing config, expect exit 1, stdout empty

&lt;span class="gu"&gt;## Release gates&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Gate A: all fixtures pass
&lt;span class="p"&gt;-&lt;/span&gt; Gate B: goldens match byte-for-byte
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;docs/tasks.md&lt;/code&gt;&lt;/strong&gt;: execution plan with a live cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Tasks&lt;/span&gt;

&lt;span class="gu"&gt;## Execution State&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Current active task: None
&lt;span class="p"&gt;-&lt;/span&gt; Last completed task: None
&lt;span class="p"&gt;-&lt;/span&gt; Blocked tasks: None
&lt;span class="p"&gt;-&lt;/span&gt; Open deviations: None

&lt;span class="gu"&gt;## M1: Core scan command&lt;/span&gt;

&lt;span class="gu"&gt;### T1.1: Implement scan exit codes and JSON schema&lt;/span&gt;

Contract refs: contract.md §Commands/scan
Eval refs: evals.md fx01, fx02, Gate A, Gate B
Done when: fx01 and fx02 pass, goldens match, no extra keys in output.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these four files in place, an agent reading &lt;code&gt;AGENTS.md&lt;/code&gt; knows immediately: find an explicit task first, read the contract before coding, don't add behavior that isn't in the contract, classify failures before fixing them. That's most of the anti-drift value with a fraction of the setup.&lt;/p&gt;

&lt;p&gt;Documentation only helps agents when it is authoritative, scoped, and executable through evals. Otherwise it's just more context for the agent to reinterpret.&lt;/p&gt;




&lt;h2&gt;
  
  
  The document hierarchy
&lt;/h2&gt;

&lt;p&gt;Which documents are authoritative for what? In most repos, this is implicit. That is exactly why agents drift. In AnchorMap, authority is explicit and domain-scoped. Each document owns a specific class of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docs/contract.md&lt;/code&gt;: observable runtime behavior. If code contradicts it, the code is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/evals.md&lt;/code&gt;: verification gates. If a release gate doesn't pass, the release isn't ready.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/brief.md&lt;/code&gt;: product scope. It arbitrates what v1.0 is trying to prove.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/design.md&lt;/code&gt;: compatible implementation design. It can change as long as the contract stays satisfied.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/operating-model.md&lt;/code&gt;: production method, deviation taxonomy, review protocol, and done criteria.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/tasks.md&lt;/code&gt;: execution plan and current task state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/adr/&lt;/code&gt;: locked technical decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is explicitly demoted.&lt;/strong&gt; It's the entry map, not the authority. That sounds counterintuitive. Many agentic repos treat the instruction file as the highest authority. I don't. Durable product rules live in &lt;code&gt;docs/&lt;/code&gt;. If &lt;code&gt;AGENTS.md&lt;/code&gt; conflicts with anything in &lt;code&gt;docs/&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt; wins, and the file says so.&lt;/p&gt;

&lt;p&gt;The mistake is treating &lt;code&gt;AGENTS.md&lt;/code&gt; as the constitution. I treat it as a signpost.&lt;/p&gt;

&lt;p&gt;A repo that relies on a single instruction file gives an agent enough room to drift if it skims that file and stops there. In this setup, an agent that reads &lt;code&gt;AGENTS.md&lt;/code&gt; only learns where to go next.&lt;/p&gt;




&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;For a product task, the loop has six moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Identify an explicit task.&lt;/strong&gt; The agent can propose work, but it cannot start product implementation without a task ID in &lt;code&gt;docs/tasks.md&lt;/code&gt;. This shuts down the "let me just do something useful while I'm here" pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read within bounds, not as little as possible.&lt;/strong&gt; The agent reads the sections explicitly linked to the task, not the entire documentation tree. The goal is not to starve the agent of context. It is to prevent unrelated context from becoming accidental authority. Broader reading is allowed when a concrete failure demands it, or when the diff touches a critical surface like the parser, renderer, contract, or eval machinery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Declare before patching.&lt;/strong&gt; Before touching a file, the agent states the target task, binding references, smallest useful check, expected handoff checks, expected patch boundary, and explicit out-of-scope items. An agent that can't declare a clean patch boundary isn't ready to edit. This is the most effective anti-drift guardrail in the workflow. A real declaration looks 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;Task: T7.5: Assemble exact scan JSON output

Binding refs:
- contract.md §13.2 Exact success schema
- contract.md §§13.3–13.7 scan JSON sections and canonical serialization
- evals.md §6.1 Mandatory JSON goldens
- evals.md fx01, fx02, fx09, fx10
- evals.md Gate B

Patch boundary:
- scan result projection
- JSON output assembly
- renderer integration for scan success
- focused scan JSON tests or goldens required by the task

Smallest check:
- run fx10_scan_closed_objects before broadening beyond schema assembly

Handoff checks:
- run B-scan success fixtures
- run JSON golden checks for the touched fixtures

Out of scope:
- human scan output
- semantic JSON comparison
- new JSON keys outside the contract
- diagnostics metadata
- changing scan semantics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That declaration changes the interaction. The agent is no longer free-floating in the repo. It has a task, sources of authority, a bounded patch surface, and known refusal conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Implement only the traced surface.&lt;/strong&gt; Not the adjacent improvement it noticed. Not the cleanup that seems obvious. Not the extra diagnostic that might be useful. If something outside scope needs to change, it needs its own task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Classify failures before fixing them.&lt;/strong&gt; When something breaks, the instinct is to fix it immediately. The workflow requires naming the failure class first: &lt;code&gt;contract violation&lt;/code&gt;, &lt;code&gt;spec ambiguity&lt;/code&gt;, &lt;code&gt;design gap&lt;/code&gt;, &lt;code&gt;eval defect&lt;/code&gt;, &lt;code&gt;product question&lt;/code&gt;, &lt;code&gt;tooling problem&lt;/code&gt;, &lt;code&gt;out-of-scope discovery&lt;/code&gt;. The label determines the correct action. Fixing before classifying is how you accidentally weaken a fixture, paper over a spec ambiguity, or turn an out-of-scope discovery into a silent product change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Submit a bounded diff to fresh review.&lt;/strong&gt; The review context is separate from the implementation context. Same-session review gives the model the intent, tradeoffs, and partial reasoning that produced the patch. That is exactly the context that makes it easier to rationalize the change instead of challenging it. The invariant isn't a specific tool. It's separation: review must inspect the diff from a clean context and issue a decision before rework begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  What gets stricter when the workflow scales
&lt;/h2&gt;

&lt;p&gt;The four-file bootstrap is enough to start. AnchorMap goes further because the workflow also handles repeated implementation cycles, fixture diagnosis, task-plan maintenance, and bounded automation. Three constraints become important at that point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review starts from a clean context.&lt;/strong&gt; In AnchorMap, that means native Codex review on the bounded diff, or a fresh interactive session where review is the first step. In another stack, the mechanism can differ. The rule is the same: the session that produced the patch doesn't approve the patch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow tools don't count as review.&lt;/strong&gt; AnchorMap has local skills for implementation, fixture diagnosis, task updates, and task validation. They make specific paths repeatable and they can help produce work. They can't approve their own output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autopilot is opt-in and bounded.&lt;/strong&gt; Automation can run the loop, but it can't blur the boundaries. Each implementation and review still runs in a task-scoped context. The coordinator retains task-level state, not an ever-growing transcript of implementation reasoning. Automation doesn't relax the contract. It makes the boundaries more important.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this is overkill
&lt;/h2&gt;

&lt;p&gt;This is not how I would structure a weekend prototype or a throwaway script. For exploratory work, agents need room: to try things, follow weak signals, make useful jumps before the shape of the product is known.&lt;/p&gt;

&lt;p&gt;The workflow starts paying for itself when the repo has observable behavior that other people or tools depend on: CLI output, public APIs, migration scripts, generated files, config formats, release gates, anything where "almost correct" can become a compatibility problem. The more stable the surface, the more expensive quiet drift becomes.&lt;/p&gt;

&lt;p&gt;For product behavior that people depend on, agents need clear authority. A repo that tells an agent which document governs each class of decision, how failures must be classified, and what "done" means will produce more consistent, traceable, reviewable output. A repo that leaves those questions implicit invites drift.&lt;/p&gt;

&lt;p&gt;For agent-written code, the workflow is not process overhead. It is part of the product.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AnchorMap is a CLI tool for anchor-based dependency mapping in TypeScript repositories. The full workflow documentation lives in the &lt;a href="https://github.com/fstepho/anchormap" rel="noopener noreferrer"&gt;public repo&lt;/a&gt; under &lt;code&gt;docs/&lt;/code&gt;. A follow-up article will cover the fresh review protocol and bounded autopilot in detail.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>File conversion APIs are a mess - here's what I learned</title>
      <dc:creator>Fabibi</dc:creator>
      <pubDate>Sun, 22 Jun 2025 00:27:12 +0000</pubDate>
      <link>https://dev.to/fabibi/file-conversion-apis-are-a-mess-heres-what-i-learned-1pkm</link>
      <guid>https://dev.to/fabibi/file-conversion-apis-are-a-mess-heres-what-i-learned-1pkm</guid>
      <description>&lt;p&gt;I've been researching file conversion solutions for a project, and honestly? The pricing is wild. Here's what I discovered and why I'm thinking about building an alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recurring problem
&lt;/h2&gt;

&lt;p&gt;Every web project eventually needs file conversions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users want to download invoices as PDF&lt;/li&gt;
&lt;li&gt;Data needs exporting to Excel
&lt;/li&gt;
&lt;li&gt;Someone uploads a DOCX that needs to become HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And we always face the same choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pay for an API (expensive)&lt;/li&gt;
&lt;li&gt;DIY with open source (time sink)&lt;/li&gt;
&lt;li&gt;Say no to the feature (not great)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Current market options
&lt;/h2&gt;

&lt;p&gt;I spent last week analyzing the main players:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudMersive&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts at $19.99/month for 10k calls&lt;/li&gt;
&lt;li&gt;$199.99/month for 100k calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden catch:&lt;/strong&gt; Complex conversions can use multiple API calls&lt;/li&gt;
&lt;li&gt;A single PDF conversion might count as 2-5 calls depending on complexity&lt;/li&gt;
&lt;li&gt;Enterprise pricing gets steep fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ConvertAPI&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~$35/month for 1,000 conversions&lt;/li&gt;
&lt;li&gt;~$90/month for 5,000 conversions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overage penalty:&lt;/strong&gt; ~$0.06 per extra conversion (ouch!)&lt;/li&gt;
&lt;li&gt;At least they're transparent about pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Zamzar&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$12/month for ~1,500 conversions (50/day limit)&lt;/li&gt;
&lt;li&gt;$39/month for ~15,000 conversions (500/day limit)&lt;/li&gt;
&lt;li&gt;Daily limits instead of monthly - weird for APIs&lt;/li&gt;
&lt;li&gt;Unclear API vs desktop app pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt; For a small SaaS doing 5,000 conversions/month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudMersive: $20-100 (depends on complexity multiplier)&lt;/li&gt;
&lt;li&gt;ConvertAPI: ~$90/month + brutal overage fees
&lt;/li&gt;
&lt;li&gt;Zamzar: $39/month (if you stay under 500/day)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And none of them make it simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "just use LibreOffice" trap
&lt;/h2&gt;

&lt;p&gt;Everyone's first instinct: "Just shell out to LibreOffice!"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`soffice --headless --convert-to pdf "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&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;Simple, right? Here's what Stack Overflow and GitHub issues taught me about production reality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common problems people report:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory leaks after ~100 conversions&lt;/li&gt;
&lt;li&gt;Random hangs on complex documents&lt;/li&gt;
&lt;li&gt;Processes that won't terminate&lt;/li&gt;
&lt;li&gt;Can't handle concurrent requests well&lt;/li&gt;
&lt;li&gt;Different outputs on different OS versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One developer's solution I found shows the real complexity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From a GitHub issue - handling the edge cases&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pkill -9 soffice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Nuclear option&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;convertWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A smarter approach I'm considering
&lt;/h2&gt;

&lt;p&gt;Based on my research, here's what seems to work best:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Use specialized libraries where possible
&lt;/h3&gt;

&lt;p&gt;Many conversions don't need heavy tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Images: Sharp is fantastic&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// CSV/Excel: XLSX handles most cases&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xlsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workbook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data.xlsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;XLSX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workbook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Markdown: Marked.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdownContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Cache everything cacheable
&lt;/h3&gt;

&lt;p&gt;Many apps convert the same templates repeatedly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simple caching strategy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertWithCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;h3&gt;
  
  
  3. Hybrid approach
&lt;/h3&gt;

&lt;p&gt;Use APIs only for complex stuff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;smartConvert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFormat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Simple formats = use libraries&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSimpleConversion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFormat&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="nf"&gt;convertLocally&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFormat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Complex stuff = use API&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;convertViaAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetFormat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Making the numbers work
&lt;/h2&gt;

&lt;p&gt;For a hypothetical SaaS with 5,000 monthly conversions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~70% might be cacheable (same templates)&lt;/li&gt;
&lt;li&gt;~25% could use simple libraries&lt;/li&gt;
&lt;li&gt;~5% would need proper API/LibreOffice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Theoretical cost:&lt;/strong&gt; ~$20-30 vs $80-100/month&lt;/p&gt;

&lt;p&gt;But more importantly: &lt;strong&gt;no daily limits, no overage surprises, no complexity multipliers&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm thinking about building something
&lt;/h2&gt;

&lt;p&gt;After all this research, I keep thinking: why isn't there a simple, fairly-priced conversion API?&lt;/p&gt;

&lt;p&gt;What I'd want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One endpoint for all conversions&lt;/li&gt;
&lt;li&gt;Pay only for what you use&lt;/li&gt;
&lt;li&gt;No monthly minimums&lt;/li&gt;
&lt;li&gt;Handles the edge cases properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But before I spend months building this, I need to know: &lt;strong&gt;do others have this problem too?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions for you
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How do you handle file conversions currently?&lt;/li&gt;
&lt;li&gt;What's your monthly volume?&lt;/li&gt;
&lt;li&gt;Would you pay for a simpler solution?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm genuinely curious about your experiences. Every project seems to solve this differently!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; If you think a simpler conversion API would be useful, I'm collecting feedback at &lt;a href="https://fileconvert.dev" rel="noopener noreferrer"&gt;fileconvert.dev&lt;/a&gt;. No product exists yet - just trying to see if this is worth building.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>api</category>
      <category>pdf</category>
    </item>
  </channel>
</rss>
