<?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>Forem: Fabibi</title>
    <description>The latest articles on Forem by Fabibi (@fabibi).</description>
    <link>https://forem.com/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>Forem: Fabibi</title>
      <link>https://forem.com/fabibi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/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://forem.com/fabibi/your-coding-agent-doesnt-need-better-prompts-it-needs-a-contract-572k</link>
      <guid>https://forem.com/fabibi/your-coding-agent-doesnt-need-better-prompts-it-needs-a-contract-572k</guid>
      <description>&lt;p&gt;Stopping silent drift with testable boundaries&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How I structured a repo so AI agent drift fails before it ships.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The worst failure mode I see in agentic coding is not broken code. Broken code usually announces itself.&lt;/p&gt;

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

&lt;p&gt;After a few months of fighting that pattern, I stopped trying to tune prompts. The agent did not need one more instruction. The repo needed clearer authority. When behavior is implicit, agents guess. Asking them not to guess is not enough. The repo has to remove the room for guessing.&lt;/p&gt;

&lt;p&gt;By "contract," I mean a written, testable description of observable behavior: commands, outputs, exit codes, schemas, determinism rules, and the boundaries of what implementation may change. It is not the whole design. It is the part external consumers can observe and depend on.&lt;/p&gt;

&lt;p&gt;A prompt asks the agent to behave. A contract gives the repo a way to reject behavior it never approved.&lt;/p&gt;




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

&lt;p&gt;I am implementing a &lt;code&gt;scan --json&lt;/code&gt; command. The spec lists three output 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 does that, then adds a &lt;code&gt;meta&lt;/code&gt; key with runtime diagnostics because it looks useful for debugging. The reasoning is understandable. The spec does not forbid it, and diagnostics make 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 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 javascript"&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. Review approves it. It 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 should not.&lt;/p&gt;

&lt;p&gt;The agent did what it was asked to do. The repo was the weak point: the boundary existed only in prose, so nothing could enforce it. 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 javascript"&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;In the version I use, &lt;code&gt;docs/contract.md&lt;/code&gt; says the output schema is closed: no extra keys. &lt;code&gt;docs/evals.md&lt;/code&gt; validates the JSON byte-for-byte against a golden. Before handoff, &lt;code&gt;npm run check:goldens&lt;/code&gt; fails because the golden does not 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%2F2vfgynlahv0ctlkr0t2p.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%2F2vfgynlahv0ctlkr0t2p.png" alt="Diagram comparing coding-agent drift with and without a contract. Without a contract, an extra meta field passes permissive tests and ships. With a closed schema and golden check, the same drift is caught before handoff." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The useful part is the derivation: tests come from an explicit behavioral contract, so the constraint is machine-checkable before the agent can be helpful in the wrong direction. Drift can still happen. It just has to show itself: update the contract, or fail the evals.&lt;/p&gt;




&lt;h2&gt;
  
  
  The operating rules
&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; uses three rules, 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 starts. &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. To add or change behavior, change the contract first. If an agent implements behavior that is not in the contract, the workflow treats it as drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval-driven&lt;/strong&gt; means the contract is verified before implementation is done. &lt;code&gt;docs/evals.md&lt;/code&gt; defines fixtures, goldens, and release gates derived 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 not accepted as noise. Any divergence is either a defect or a contract change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope-closed&lt;/strong&gt; means agents cannot invent behavior, even when the addition looks harmless or useful. Any observable behavior with no trace back to &lt;code&gt;contract.md&lt;/code&gt; and &lt;code&gt;evals.md&lt;/code&gt; gets refused. It is restrictive. That is intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four-file bootstrap
&lt;/h2&gt;

&lt;p&gt;You do not need the full AnchorMap workflow to get most of the value. This is the smallest version I would copy into a new repo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&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;code&gt;docs/contract.md&lt;/code&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 -&amp;gt; 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;code&gt;docs/evals.md&lt;/code&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;code&gt;docs/tasks.md&lt;/code&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 what to do next: find an explicit task, read the contract before coding, avoid behavior that is not in the contract, and classify failures before fixing them.&lt;/p&gt;

&lt;p&gt;Documentation only helps agents if it has authority and if evals can enforce it. Otherwise it is just more context for the agent to reinterpret.&lt;/p&gt;




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

&lt;p&gt;Which document answers which question? In many repos, nobody writes that down. That is where agents drift. In AnchorMap, authority is explicit and scoped by domain:&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 does not pass, the release is not 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;code&gt;AGENTS.md&lt;/code&gt; is demoted on purpose. It is the entry map, not the source of truth. Many agentic repos treat the instruction file as the highest authority. I do not. 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 one instruction file gives an agent room to drift if it skims the file and stops there. In this setup, &lt;code&gt;AGENTS.md&lt;/code&gt; only tells the agent where to go next.&lt;/p&gt;




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

&lt;p&gt;For a product task, I use this loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Identify an explicit task.&lt;/strong&gt;&lt;/p&gt;

&lt;p&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 am here" pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read within bounds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&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. The goal is to stop unrelated context from becoming accidental authority.&lt;/p&gt;

&lt;p&gt;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;&lt;/p&gt;

&lt;p&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.&lt;/p&gt;

&lt;p&gt;An agent that cannot declare a clean patch boundary is not ready to edit. 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;&lt;/p&gt;

&lt;p&gt;The patch covers only the surface tied to the task. Adjacent improvements, obvious cleanups, and useful diagnostics wait for their own task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Classify failures before fixing them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&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;, or &lt;code&gt;out-of-scope discovery&lt;/code&gt;.&lt;/p&gt;

&lt;p&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;&lt;/p&gt;

&lt;p&gt;The review context is separate from the implementation context. If the same session reviews its own patch, it carries the intent, tradeoffs, and partial reasoning that produced the patch. That context makes rationalization easier.&lt;/p&gt;

&lt;p&gt;The rule is separation: review inspects the diff from a clean context and issues a decision before rework begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  What gets stricter at scale
&lt;/h2&gt;

&lt;p&gt;The four-file bootstrap is enough to start. AnchorMap goes further because the workflow has to handle repeated implementation cycles, fixture diagnosis, task-plan maintenance, and bounded automation. At that point, I tighten these parts.&lt;/p&gt;

&lt;p&gt;Review starts from a clean context. 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 stays the same: the session that produced the patch does not approve the patch.&lt;/p&gt;

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

&lt;p&gt;Autopilot is opt-in and bounded. Automation can run the loop, but it cannot 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 does not 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 structure a weekend prototype or a throwaway script. Exploratory work needs room. Agents need to try things, follow weak signals, and make useful jumps before the product shape 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, or any surface 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;If people or tools depend on a behavior, agents need explicit authority. The repo should say which document governs each class of decision, how failures are classified, and what "done" means. Leave those implicit and drift will find the gap.&lt;/p&gt;

&lt;p&gt;Once agents write product code, this workflow becomes product infrastructure.&lt;/p&gt;




&lt;p&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;/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://forem.com/fabibi/file-conversion-apis-are-a-mess-heres-what-i-learned-1pkm</link>
      <guid>https://forem.com/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>
