• maiweb v0.1.0
  • ★
  • Feedback

#dev-to

1 source tagged with this.

  • DEV Community
  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:39

    ↗

    XML Tagging in Prompts: The Secret to Getting Better Output from Claude and GPT A simple structuring trick that turns messy, unpredictable LLM outputs into clean, reliable ones. If you've spent any time writing prompts for Claude, GPT, or any other large language model,...

    XML Tagging in Prompts: The Secret to Getting Better Output from Claude and GPT

    A simple structuring trick that turns messy, unpredictable LLM outputs into clean, reliable ones.

    If you've spent any time writing prompts for Claude, GPT, or any other large language model, you've probably hit this wall: your prompt works fine for a simple ask, but the moment you pack in multiple instructions — some context, a few examples, formatting rules, and the actual task — the model starts mixing things up. It answers the wrong part of the question. It ignores your formatting instructions. It treats your example output as part of the actual task.

    The fix is almost embarrassingly simple: wrap your prompt sections in XML tags.

    Why XML Tags Work So Well

    LLMs are trained on enormous amounts of code, documentation, and markup. XML (and HTML) syntax is deeply embedded in that training data, which means models are very good at recognizing where one tagged section ends and another begins. Unlike plain paragraphs — where the boundary between "here's my context" and "here's my instruction" is fuzzy — a tag creates an unambiguous boundary.

    Anthropic actually recommends this explicitly for Claude: wrapping distinct parts of a prompt (instructions, context, examples, output format) in tags like <instructions>, <context>, <example>, and <output_format> measurably improves consistency, especially in longer or more complex prompts.

    Think of it like the difference between handing someone a wall of text versus handing them a form with labeled fields. Both contain the same information, but one is far easier to parse correctly — for a human, and for a model.

    A Before-and-After Example

    Without tags:

    Summarize the article below in 3 bullet points. Keep it under 50 words.
    Use a neutral tone. Here's an example of the style I want:
    "- Company X raised $10M in Series A funding."
    Now here's the article: [long article text]
    

    The model has to guess where the instructions end and the article begins — and with a long article, it sometimes starts summarizing the example instead of the real content.

    With tags:

    <instructions>
    Summarize the article in 3 bullet points, under 50 words total.
    Use a neutral tone.
    </instructions>
    
    <example_style>
    - Company X raised $10M in Series A funding.
    </example_style>
    
    <article>
    [long article text]
    </article>
    

    Now there's zero ambiguity. The model knows exactly what's an instruction, what's a style reference, and what's the raw content to work on.

    Common Tags Worth Using

    You don't need a formal schema — these are just semantic containers the model can recognize. Some of the most useful:

    • <instructions> — the actual task
    • <context> — background info the model needs but shouldn't act on directly
    • <example> / <examples> — sample inputs/outputs (few-shot prompting)
    • <document> or <article> — source text to analyze/transform
    • <output_format> — exactly how you want the response structured
    • <thinking> — for models that support step-by-step reasoning before the final answer

    You can also nest them, e.g., multiple <document index="1">, <document index="2"> blocks when feeding several sources at once — attributes work too, and models parse them correctly.

    Bonus: Ask for Tagged Output Too

    This trick isn't just for input — you can ask the model to return its answer in tags, which makes parsing the response programmatically trivial:

    <answer>
    Your final response here.
    </answer>
    <confidence>high</confidence>
    

    If you're building this into an app (say, a Next.js API route calling the AI SDK), this turns a fragile "hope the model formats it right" problem into a simple string-extraction problem — no need for a full JSON schema when you just need one or two fields.

    When Not to Bother

    For a single, simple instruction ("Translate this to French"), tags are overkill — they add noise for no benefit. Reach for XML tagging when your prompt has multiple distinct components that could be confused with each other: instructions + context + examples + a document to process, all in one message.

    Takeaway

    XML tagging isn't a hack — it's closer to good API design applied to prompts. You're giving the model an explicit contract instead of hoping it infers your intent from prose. The next time a prompt feels like it's "almost working," try wrapping its parts in tags before you start rewriting the wording. Often, structure — not phrasing — was the actual problem.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:36

    ↗

    I wanted to learn cloud security the way it actually sticks: by building something real. So I built PostureGuard, a web application that scans a domain and returns a security posture report covering TLS, HTTP security headers and open ports, with a 0-100 score and an A-F...

    I wanted to learn cloud security the way it actually sticks: by building something real. So I built PostureGuard, a web application that scans a domain and returns a security posture report covering TLS, HTTP security headers and open ports, with a 0-100 score and an A-F grade. This post walks through the architecture and the decisions I found most interesting.

    Update: Phase 1 is done. PostureGuard now runs on Azure Container Apps and is live at app.samdossou.com. The write-up is the next post in this series.

    The shape of the system

    PostureGuard has three moving parts:

    • A Next.js web app (App Router, TypeScript) where users sign up, add a domain, and request scans.
    • A PostgreSQL database that stores users, domains and scans.
    • A Python worker that runs the actual scans in the background.

    The web app never runs a scan itself. When a user clicks "Scan", the app just inserts a row into a scans table with the status queued and returns immediately. The worker picks the job up a moment later. This keeps the request fast and the two halves of the system decoupled.

    Using PostgreSQL as a job queue

    The part I like most is that there is no separate message broker. The scans table doubles as the queue. The worker claims one job at a time with a single query:

    SELECT s.id, d.name
    FROM scans s JOIN domains d ON d.id = s.domain_id
    WHERE s.status = 'queued'
    ORDER BY s.requested_at
    FOR UPDATE OF s SKIP LOCKED
    LIMIT 1
    

    FOR UPDATE locks the row so no one else can grab it, and SKIP LOCKED tells other workers to ignore locked rows and move on to the next job. That means I can run several workers in parallel and they will never process the same scan twice, without any extra infrastructure. For a project at this scale, a table plus SKIP LOCKED is simpler and more than enough.

    The scanners

    The worker runs three checks, all built on the Python standard library to keep dependencies light:

    • TLS: it opens a TLS connection, reads the certificate expiry and the negotiated protocol version, and flags expired certs or outdated TLS.
    • HTTP headers: it fetches the site and checks for the security headers that matter (HSTS, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy).
    • Ports: it attempts TCP connections to a list of common ports and flags sensitive ones like Telnet, RDP or an exposed database.

    Each check returns findings tagged with a severity. The score starts at 100 and loses points per finding based on severity, then maps to a grade. Because the scoring is pure logic with no I/O, I pulled it into its own module and covered it with unit tests.

    Only scan what you own

    A domain scanner is one if statement away from being an attack tool, so ownership matters. Before a domain can be scanned, the user has to prove they control it by adding a DNS TXT record containing a unique token I generate for them. The verification step does a live DNS lookup and only marks the domain verified if the token is present. Since only the domain owner can edit DNS records, this is a clean proof of control.

    Authentication done simply

    Auth is email and password. Passwords are hashed with bcrypt and only the hash is stored. Sessions live server-side in the database; the browser only holds an opaque session id in an httpOnly cookie, so it is not reachable from JavaScript. Nothing fancy, but the fundamentals are right.

    Running it like production

    To get a feel for operations, I did not just run the worker in a terminal. I turned it into a systemd service so it starts on boot and restarts if it crashes, with its logs captured by journald. A bash script dumps and compresses the database, keeps the last seven backups, and a systemd timer runs it every night. After a full machine restart, the worker came back on its own and the backup fired overnight without me touching anything.

    One small gotcha worth noting: under systemd, Python buffers stdout, so my log lines never reached journald until I forced unbuffered output. A classic first surprise when you move a script into a service.

    What is next

    This is Phase 0, the local MVP. From here the plan is to deploy it to Azure, then AWS, refactor the infrastructure to Terraform, run it on Kubernetes, wrap it in a DevSecOps pipeline, and wire it into a SOC with Microsoft Sentinel. Each step adds a layer, and each layer is a chance to learn a piece of cloud security by building instead of just reading about it.

    The code is on GitHub: https://github.com/Dkls7777/postureguard

    If you are learning cloud security too, I would recommend the same approach: pick one project and take it deep.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:32

    ↗

    You've paginated through a big result set before. Page 2 loads fine. Page 200 takes a beat. Nobody stops to ask why — until something like this shows up instead of a slow page: { "message": "Only the first 1000 search results are available.", "documentation_url":...

    You've paginated through a big result set before. Page 2 loads fine. Page 200 takes a beat. Nobody stops to ask why — until something like this shows up instead of a slow page:

    {
      "message": "Only the first 1000 search results are available.",
      "documentation_url": "https://docs.github.com/v3/search/",
      "status": "422"
    }
    

    That's GitHub's own search API. Ask it for page 9 or page 10 of a search and you get real results. Ask for page 11 and you get that, on purpose, every time.

    I went and looked at why. Full video below, written breakdown after it for anyone who'd rather read.

    Reproducing it

    Not a guess — this is live, right now:

    curl "https://api.github.com/search/repositories?q=javascript&page=9&per_page=100"
    # -> real results
    
    curl "https://api.github.com/search/repositories?q=javascript&page=10&per_page=100"
    # -> real results
    
    curl "https://api.github.com/search/repositories?q=javascript&page=11&per_page=100"
    # -> 422, the message above
    

    GitHub documents this cap directly — it's not rate limiting, and it's not a bug that slipped through. Once you understand the mechanism it's protecting against, the same wall shows up in a lot of places that don't bother telling you about it — including, probably, a table you've built yourself.

    The query behind the page number

    Most pagination UIs turn a page number into something like this:

    SELECT * FROM videos
    ORDER BY id
    LIMIT 20 OFFSET 999980;
    

    Twenty rows per page, page 50,000 requested — that's an offset of nearly a million. It's easy to assume the database can jump straight to row 999,980. It can't, and the reason is in how the underlying index is built, not in anything about this specific query.

    What an index actually is

    A sorted column is backed by a B-tree — a structure built for one thing: walking rows in order, fast. The leaf level is a chain of rows in sorted sequence. That chain is what makes ORDER BY cheap.

    It is also the whole problem. There's no operation in a B-tree for "give me whatever's at position 999,980." Only for "give me whatever's next after this value." Position and value are not the same axis, and only one of them has a shortcut.

    So OFFSET 999980 does the only thing it can: start at the beginning of the scan, count a row, discard it, count the next, discard it — one at a time, nearly a million times — before it starts collecting the 20 rows you actually asked for.

    Cost = O(offset), not O(page size). Page 10 is nearly free. Page 50,000 means walking and throwing away roughly a million rows just to reach the part you wanted.

    What actually happens, measured

    Theory's cheap. Here's a real table — 5 million rows, local Postgres:

    docker run --name pagination-demo -e POSTGRES_PASSWORD=demo -p 5432:5432 -d postgres:16
    docker exec -it pagination-demo psql -U postgres
    
    CREATE TABLE videos (
      id BIGINT PRIMARY KEY,
      title TEXT,
      created_at TIMESTAMP
    );
    
    INSERT INTO videos (id, title, created_at)
    SELECT g, 'Video ' || g, NOW() - (random() * interval '3 years')
    FROM generate_series(1, 5000000) AS g;
    
    EXPLAIN ANALYZE
    SELECT * FROM videos
    ORDER BY id
    LIMIT 20 OFFSET 999980;
    
    Limit  (cost=33323.00..33323.67 rows=20 width=29) (actual time=121.583..121.586 rows=20 loops=1)
      ->  Index Scan using videos_pkey on videos  (cost=0.43..166616.30 rows=4999991 width=29)
            (actual time=0.012..94.512 rows=1000000 loops=1)
    Planning Time: 0.201 ms
    Execution Time: 121.603 ms
    

    rows=1000000 on the scan node isn't an estimate — that's the actual count the engine walked through before it could return anything, exactly matching the mechanism above.

    The fix everyone reaches for, and what it actually does

    The usual advice is "use cursor pagination instead of OFFSET." Here's the query that advice produces:

    EXPLAIN ANALYZE
    SELECT * FROM videos
    WHERE id < 845923
    ORDER BY id DESC
    LIMIT 20;
    
    Limit  (cost=0.43..1.15 rows=20 width=29) (actual time=0.477..0.484 rows=20 loops=1)
      ->  Index Scan Backward using videos_pkey on videos  (cost=0.43..30516.09 rows=851752 width=29)
            (actual time=0.475..0.480 rows=20 loops=1)
            Index Cond: (id < 845923)
    Planning Time: 0.129 ms
    Execution Time: 0.503 ms
    

    Same table. Same 20 rows back. 121.603 ms → 0.503 ms — roughly 242x, and it's not a rounding error, it's a different cost curve entirely.

    What changed underneath: instead of walking the leaf chain from the start, the engine descends the tree directly — root, branch, leaf — to id = 845923, then reads 20 rows backward from there. A seek, not a scan. That's the entire mechanism. Nothing about the table changed, nothing about the index changed — only the shape of the question changed, from "what's at position N" to "what comes after this value."

    This is also why Instagram says "load more" and X says "load older posts" instead of showing page numbers. Under the hood, that's a cursor — after=<id> — not a position.

    Where this breaks silently

    Keyset pagination only holds up if the sort key is unique and strictly ordered — a primary key like id qualifies. The moment you sort by something that can repeat, like created_at, where two rows can land on the exact same millisecond, a single-column cursor can silently skip a row or hand back a duplicate. Nothing errors. It just quietly returns the wrong set.

    The fix is comparing a pair of columns, not one:

    SELECT * FROM videos
    WHERE (created_at, id) < (:last_created_at, :last_id)
    ORDER BY created_at DESC, id DESC
    LIMIT 20;
    

    That line is the gap between "works in the demo" and "works in production."

    What to check before you reach for either

    In order:

    1. Does the page number itself need to exist? If users are scrolling a feed, not jumping to "page 4,721," you don't need OFFSET's random access at all — cursor pagination is strictly better here, no tradeoff to weigh.
    2. Do you actually need random access? Admin dashboards, audit tools, support panels — anywhere someone legitimately needs "go to page 42" — OFFSET's random access is the feature, not the bug, and the fix is usually capping how deep it's allowed to go rather than replacing it outright.
    3. If you're building a public API, assume someone will request the deepest possible page. Elasticsearch refuses to paginate past 10,000 results by default and points you at search_after instead — the same fix GitHub ships, in a different system. Capping the depth is cheaper than absorbing the cost of someone finding it by accident.

    Not a universal fix

    OFFSET isn't wrong. It's the right tool when random access matters more than depth — small tables, internal tools, anywhere nobody's going past page 20 anyway. Cursor pagination isn't strictly "better," it trades away the ability to jump anywhere for a flat cost curve at any depth. The mistake isn't picking one — it's not knowing there's a choice, and finding out which one you picked at the exact moment your table crosses a few million rows.

    If this was useful, the video walks through the same benchmark live — GitHub's actual API response, the terminal running both queries against the same 5M-row table, EXPLAIN ANALYZE output as it prints. Drop a comment if you want the follow-up on why COUNT(*) gets slow on the exact same kind of table — different query, same root cause.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:24

    ↗

    A comment can be marked done without changing Word text Text comparison is necessary for document review, but it is not sufficient for every stored WordprocessingML change. A document can retain its visible text, comment anchor, ordinary comment content, relationships, and...

    A comment can be marked done without changing Word text

    Text comparison is necessary for document review, but it is not sufficient for every stored WordprocessingML change. A document can retain its visible text, comment anchor, ordinary comment content, relationships, and package member set while a persisted comment state changes elsewhere in the package.

    Document Change Assurance Benchmark (DCAB) 0.12.0 adds a small, deterministic case for exactly that boundary: review.modern_comment_done_state_changed.

    Both packages hold fixed:

    • the classic comment and its document range start/end/reference anchor;
    • the matching paragraph identifier and internal package relationships;
    • content-type declarations, package members, and every stored w:t value.

    Only word/commentsExtended.xml changes. Its sole Office 2013 w15:commentEx record moves from explicit done="0" to done="1".

    Why this belongs in static review

    Microsoft describes the commentsExtended part as carrying additional information about comments represented in the classic comments part. Its w15:commentEx schema reference says the construct is available in Office 2013 and later, associates paraId with the comment's final paragraph, and defines done="1" as a user indication that the associated comment is done.

    That makes the stored metadata reviewable. It does not make it a safe basis for overclaiming. The fixture does not open Word, render or update comments, resolve a thread, authenticate a person, communicate with a service, or assert a particular client UI or workflow behavior. It tests one direct package-data boundary.

    A target-free, reproducible contract

    The public truth file says only that the stored done state changed and that the reference disposition is review. It excludes fixture-specific comment text, author metadata, paragraph identifiers, relationship paths, and raw serialization details.

    DCAB's structural verifier checks the package topology and the exact one-member delta. Its independent python-docx check opens all 44 .docx fixtures and its OPC reader opens all 46 packages. The optional DocFence 0.27 adapter observes only aggregate inventory evidence: one classic comment, one extension record, and a resolved-comment count moving from zero to one, while people, thread, reaction, comments-ID, and extensible-comment counts remain zero.

    The 0.12.0 release passed hosted Python 3.11–3.13 CI, a clean DocFence adapter install, fresh wheel and source-distribution validation, and a fresh Hugging Face dataset download.

    Try it

    python -m pip install https://github.com/SybilGambleyyu/document-change-benchmark/releases/download/v0.12.0/document_change_benchmark-0.12.0-py3-none-any.whl
    dcab validate
    dcab docfence-observations --executable docfence --output observations.json
    dcab score --observations observations.json --strict
    

    The benchmark now contains 23 paired synthetic cases and remains on fixture schema version 1. You can find the release artifacts and full static-scope contract on GitHub, and the corpus is mirrored on Hugging Face.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:22

    ↗

    I built elm.chat around a narrow idea: a conversation should not automatically become a permanent server-side asset. It is not a replacement for a contact network or a claim that metadata can disappear. It is a disposable room for one live conversation. The interesting...

    I built elm.chat around a narrow idea: a conversation should not automatically become a permanent server-side asset.

    It is not a replacement for a contact network or a claim that metadata can disappear. It is a disposable room for one live conversation. The interesting engineering question was how to make that room reliable without giving the server a readable or persistent transcript.

    The current answer is:

    • React and Web Crypto in the browser
    • one Cloudflare Worker for the app and API
    • one Durable Object per room
    • one WebSocket per participant
    • encrypted content relayed, but not persisted, by the Durable Object

    The complete project is AGPL-3.0 source.

    One coordination boundary per room

    A room is a natural Durable Object boundary. One object owns:

    • room policy and lifecycle
    • participant presence
    • creator capabilities
    • single-use invite state
    • WebSocket relay
    • expiry and destruction

    The Worker creates room metadata, addresses the object by room ID, and routes subsequent API and WebSocket traffic to it.

    The object stores the room policy, status, creator token, and invite records. It does not store message envelopes, encrypted file chunks, or a transcript.

    That distinction matters. “Encrypted at rest” would still make the server a durable archive. elm.chat instead asks currently connected clients to hold the conversation in memory.

    The room secret stays in the fragment

    Room creation starts in the browser:

    1. Generate a random 256-bit room secret.
    2. Create room metadata through the Worker.
    3. Navigate to /c/:roomId#<room_secret>.
    4. Derive the room key locally with HKDF-SHA-256.

    The fragment is not included in normal HTTP requests, so the Worker receives the room ID but not the room secret. A strict Referrer-Policy: no-referrer reduces accidental capability leakage to other origins.

    The browser uses the derived AES-GCM-256 key for message and file content:

    • each message gets a fresh random 96-bit nonce
    • files are divided into 64 KiB chunks
    • each file chunk is encrypted independently

    The relay receives ciphertext envelopes and routing information, not plaintext.

    One WebSocket carries the live room

    After joining, a client uses one WebSocket for:

    • presence
    • encrypted messages
    • encrypted file chunks
    • transcript-sync requests and responses
    • participant removal
    • destruction events
    • keepalive traffic

    WebSocket attachments are the source of truth for live membership. They survive Durable Object hibernation, so the object can wake and continue targeted routing without relying on a process-local participant map.

    When a new participant joins, the server does not load a transcript. The client requests one from peers that are already connected. A peer sends its current encrypted history through the relay, with a hard cap on the number of synced messages.

    If nobody connected still has an item, it is gone. That is intentional.

    Why I did not use direct WebRTC

    Peer-to-peer sounds like the obvious privacy choice, but the transport tradeoff is more complicated.

    Direct WebRTC ICE negotiation can reveal participant IP addresses to other people in the room. Connectivity can also fail on mobile carriers, symmetric NAT, and restrictive networks unless the application adds a TURN relay.

    I chose a Durable Object relay because:

    • room members never connect directly
    • participants do not learn one another’s IP addresses
    • the same transport works across restrictive networks
    • there is no separate STUN/TURN service to operate

    This does not eliminate metadata.

    Cloudflare still sees each connection’s IP address, timing, sizes, and presence. An observer can infer activity bursts even without decrypting content. The architecture moves content trust away from the server while accepting that the relay remains a metadata observer.

    That is the actual claim—not “no trace.”

    One-time membership

    The room creator holds a creator capability in local browser storage and can:

    • issue single-use invites
    • revoke invites
    • remove participants
    • destroy the room

    A non-creator needs a valid invite to join. The invite is consumed by the first new session that uses it. That same session can reconnect after a reload, but another session cannot reuse the link.

    This makes an elm.chat URL a handoff for one participant, not a permanent public room address.

    What the current design does not solve

    End-to-end encryption and ephemerality do not make a system invulnerable.

    The current build does not solve:

    • screenshots or copied plaintext
    • compromised devices or browser extensions
    • malicious recipients
    • traffic analysis
    • denial of service
    • strong anonymous routing
    • verified human identity
    • forward secrecy beyond the shared room key

    The clients generate ephemeral identity keys, but those keys are not yet used to authenticate messages. Replay and duplicate protection are also unfinished. The project has also not had an independent security audit.

    Those are not footnotes. They define where the current system should and should not be trusted.

    Run it or review it

    The repository includes:

    • the architecture
    • the threat model
    • a one-click Cloudflare deployment path
    • manual Wrangler deployment instructions
    • contributor guidance and scoped issues

    You can try the live room, inspect the source, or read the current security status and limitations.

    The feedback I am most interested in is on:

    1. the encrypted-relay versus WebRTC tradeoff
    2. one Durable Object per room as the coordination boundary
    3. client-supplied transcript sync after object hibernation
    4. the message-authentication path that should come next

    If you find a security issue, please use the repository’s private reporting instructions rather than publishing an exploitable detail.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:18

    ↗

    Some developers treat Claude like autocomplete: paste some code, get some code back, iterate blindly. The gap between a mediocre response and a genuinely great one usually isn't the model — it's how you're prompting it. Here are six concrete techniques, each with a real...

    Some developers treat Claude like autocomplete: paste some code, get some code back, iterate blindly. The gap between a mediocre response and a genuinely great one usually isn't the model — it's how you're prompting it. Here are six concrete techniques, each with a real before/after you can use in the API, Claude Code, or claude.ai.

    1. Give explicit success criteria, not just a task

    Vague prompts get vague code, because Claude has to guess what "done" means — and it'll guess conservatively.

    Before: "Write a function to validate emails."

    After:

    Write a TypeScript function `validateEmail(input: string): boolean`.
    Requirements:
    - RFC 5322-compatible, but reject addresses without a TLD
    - No external dependencies
    - Include 3 unit tests covering valid, invalid, and edge cases (e.g. plus-addressing)
    

    Now you get a function that matches your actual constraints instead of a generic regex you have to rewrite anyway.

    2. Wrap code and context in XML tags

    When a prompt mixes instructions with pasted source, Claude can blur which is which — especially in long files. Tags remove the ambiguity.

    <file path="auth/session.ts">
    [paste file contents]
    </file>
    
    <error>
    TypeError: Cannot read properties of undefined (reading 'userId')
    </error>
    
    Using only the file above, find the line that causes this error and explain why.
    

    This is also how you should structure multi-file context: one <file> block per file, each labeled with its path.

    3. Ask for a plan before code on nontrivial tasks

    For anything beyond a one-liner — a new feature, a refactor, a tricky bug — ask Claude to reason through the approach before writing code. This surfaces bad assumptions before they're baked into 200 lines.

    Before: "Add caching to this API endpoint."

    After: "Before writing code, outline your approach to caching this endpoint: what to cache, invalidation strategy, and where the cache lives. Then implement it."

    You catch a wrong assumption ("cache the whole response" when only one field is expensive) at the plan stage, not the PR review stage.

    4. Use few-shot examples to lock in your code style

    If you want Claude's output to match your codebase's conventions, show it a pattern instead of describing it.

    Before: "Write an error handler for this endpoint."

    After:

    Follow this existing pattern from our codebase:
    
    export const getUser = async (req, res) => {
      try {
        const user = await db.users.find(req.params.id);
        if (!user) return res.status(404).json({ error: 'not_found' });
        return res.json(user);
      } catch (e) {
        return res.status(500).json({ error: 'internal_error' });
      }
    };
    
    Now write `getOrder` following the exact same structure and error shape.
    

    One good example saves you a round trip of "actually, use our error format."

    5. Chain big tasks instead of one mega-prompt

    Asking for an entire feature in one shot tends to produce something shallow or inconsistent. Splitting into stages — plan, implement, review, fix — gets a stronger result because each step has one job.

    Before: "Build a rate limiter for our API."

    1. "Propose 2-3 rate-limiting strategies for a Node/Redis stack, with tradeoffs."
    2. "Implement the sliding-window approach as Express middleware."
    3. "Review this middleware for race conditions and edge cases."
    4. "Fix the issues you found."

    Each step is easy to verify on its own, which is exactly why the end result holds together.

    6. Manage context deliberately (especially in Claude Code)

    Long agentic sessions degrade when context fills with irrelevant history — a fix for bug A that's still sitting in context while you debug unrelated bug B.

    • Fix: run /clear between unrelated tasks in Claude Code instead of continuing the same thread.
    • Fix: if you've corrected Claude twice on the same issue and it's still wrong, /clear and write a better initial prompt with what you learned, rather than correcting a third time.
    • For instructions that should apply to every session (coding conventions, test commands, directory layout), put them in a CLAUDE.md file instead of repeating them in every prompt.

    The takeaway

    None of this is a magic prompt template — it's giving Claude the same things you'd give a new teammate: clear requirements, relevant context, your existing patterns, and room to think before acting. Try one of these on your next prompt and see how much less cleanup you have to do afterward.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:13

    ↗

    You've seen this error. npm install fails with ERESOLVE, you search it, find a GitHub issue, copy an overrides snippet into package.json, and the red text disappears on your next run. Nobody asks what that block actually did. I hit this exact error yesterday setting up a...

    You've seen this error. npm install fails with ERESOLVE, you search it, find a GitHub issue, copy an overrides snippet into package.json, and the red text disappears on your next run.

    Nobody asks what that block actually did.

    I hit this exact error yesterday setting up a cloned repo, and instead of moving on once the install went green, I went and looked. Full video below, written breakdown after it for anyone who'd rather read.

    The error

    npm error code ERESOLVE
    npm error ERESOLVE unable to resolve dependency tree
    npm error
    npm error While resolving: forced-error-demo@1.0.0
    npm error Found: react@19.2.8
    npm error node_modules/react
    npm error   react@"^19.0.0" from the root project
    npm error
    npm error Could not resolve dependency:
    npm error peer react@"^16.9.0 || ^17.0.0" from @testing-library/react-hooks@8.0.1
    

    Two lines in here matter, and they're not making the same kind of claim.

    Found: react@19.2.8 — this is just what's already resolved, because it's what my own package.json asked for (^19.0.0).

    peer react@"^16.9.0 || ^17.0.0" from @testing-library/react-hooks@8.0.1 — this is a different thing entirely: a peer dependency.

    What a peer dependency actually is

    Straight from @testing-library/react-hooks's own package.json:

    {
      "name": "@testing-library/react-hooks",
      "version": "8.0.1",
      "peerDependencies": {
        "react": "^16.9.0 || ^17.0.0"
      }
    }
    

    A peerDependencies entry is a dependency the package needs, but expects the consumer to provide — the package isn't bundling its own copy of React, it's reaching into whatever React is already running in your project, and declaring the only versions it was built to reach into safely.

    Compare that to a normal dependencies entry, which npm installs as its own separate copy in node_modules. A peer dependency installs nothing. It just checks.

    npm has enforced that check by default since v7. Before that, npm 3 through 6 mostly let mismatches like this through silently — which is exactly why the --legacy-peer-deps flag is named what it is: it tells npm to go back to that old, unchecked behavior.

    So this error isn't npm being broken. It's npm accurately reporting that a package in your tree is making a claim about your React version that isn't true.

    The fix everyone copy-pastes

    Here's the snippet you'll actually find on GitHub issues for this exact situation:

    "overrides": {
      "@testing-library/react-hooks": {
        "react": "$react"
      }
    }
    

    The $react syntax means "whatever version I already have installed as a direct dependency, use that." So this line says: wherever @testing-library/react-hooks declares a requirement on react — including its peer requirement — force it to match root's react@19.0.0 instead.

    Run npm install again. It succeeds. No red text. Looks fixed.

    What actually changed (and what didn't)

    overrides didn't touch a single line of code inside @testing-library/react-hooks. It didn't check whether that package's internals can actually run against React 19. It went into the resolved dependency graph and rewrote the version number the peer check compares against — from ^16.9.0 || ^17.0.0 to 19.0.0 — so the check has nothing left to disagree with.

    That's the entire mechanism. overrides doesn't resolve incompatibility. It rewrites the number two packages are being compared on, whether or not the code underneath agrees.

    What did not happen:

    • No code inside the overridden package ran
    • No check that its React-16/17-era internals still work against React 19's scheduling model
    • No test suite executed to confirm any of it

    Why the gap matters

    @testing-library/react-hooks was built before hook-testing support existed in @testing-library/react itself, and it's effectively frozen at how React 16 and 17 scheduled renders. React 19's internals have moved since then. The override doesn't close that gap — it just moves when you find out about it.

    Without the override: you read two lines of log, thirty seconds, and you're acting on real information.

    With the override: install succeeds clean, ships to CI, ships to prod — and the actual discovery happens whenever a hook test misbehaves or a render warning shows up with no line in your own code pointing back to it. Same incompatibility, if it's real. Just a much more expensive place to find it.

    What to check before you reach for overrides

    In order:

    1. Does the conflicting package still need to exist? In this case — no. @testing-library/react has supported hook testing natively for a long time now, which makes @testing-library/react-hooks a dependency that's outlived its reason for being. Deleting it is a better fix than patching around it.
    2. Can it be upgraded instead? A newer major of the conflicting package may not have this peer conflict at all. You might have to touch your own code too, since not every upgrade is backward-compatible — but that's still cheaper than a silent runtime bug.
    3. If it's genuinely unavoidable, scope the override. Don't apply it globally:
    // unscoped — forces every package in the tree that touches react
    "overrides": {
      "react": "$react"
    }
    
    // scoped — forces it only where the actual conflict is
    "overrides": {
      "@testing-library/react-hooks": {
        "react": "$react"
      }
    }
    

    Everything else in your tree keeps the version npm originally resolved. Smaller blast radius, easier to reason about, easier to remove later.

    A temporary patch. Not permanent config.

    If you do add an override, treat it like debt, not like settings. Leave a note on why it's there — package.json doesn't support comments, so put it in a README or an OVERRIDES.md: what it's patching, when to revisit it, who added it. Put it on your dependency-audit schedule, whatever that already is for your team, and pull it out the moment the upstream package makes it unnecessary.

    An override with no expiry date doesn't leave. It just becomes the next person's mystery to reverse-engineer.

    If this was useful, the video covers the same ground with the terminal running live — installing with and without the override, reading the actual npm ls output, watching what changes. Drop a comment if you want the follow-up on npm's resolution algorithm itself — why these conflicts happen before any override enters the picture.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:09

    ↗

    Is writing code with an agent the same thing as pair programming? That question has been going around lately, and there's a practical consequence sitting inside it that I don't see many people chasing down. On a lot of teams, code that a pair of devs wrote gets a lighter code...

    Is writing code with an agent the same thing as pair programming?

    That question has been going around lately, and there's a practical consequence sitting inside it that I don't see many people chasing down.

    On a lot of teams, code that a pair of devs wrote gets a lighter code review.

    I personally have never seen that written down anywhere. It's not in a policy doc and nobody voted on it. But everywhere I have worked, when a PR came from two people who built and tested it together, the review was lighter. Reviewers trusted the code more. Fewer issues got found in review. Fewer bugs slipped through afterward. So the lighter review kept looking like the right call, and it stuck.

    That's a risk decision, even though it never feels like one. Somebody looked at a category of change and decided it needed less scrutiny than everything else.

    And here is the part worth sitting with. Pairing earned that discount.

    Pairing earned that discount

    Researchers have studied pair programming for a long time, and the results are less magical than the marketing but pretty consistent.

    Cockburn and Williams (2000) found pairing costs roughly 15% more development time and buys back roughly 15% fewer defects. They also measured statistically significant gains in design quality, technical skill, team communication, and resilience when someone leaves, since knowledge stops living in one person's head.

    The 2009 meta-analysis from Hannay, Dybå, Arisholm and Sjøberg is more nuanced. A small positive effect on quality. A medium positive effect on duration, so pairs finish faster on the clock. A medium negative effect on effort, because they burn more total person-hours. Complexity moves the needle too: pairs are faster when the work is simple, and produce higher quality when the work is hard.

    A couple caveats here, since I'm using this research to defend a practice. Neither study measured code review specifically. They measured defects and duration, and "so review gets shorter" is my personal inference from fewer defects showing up at the door. The authors of the meta-analysis also flag signs of publication bias in the pair programming literature, which is worth knowing before anyone treats these numbers as completely settled.

    Even with these caveats, there's a track record here. The lighter review is resting on something real.

    It was never a flat discount

    The other thing about that discount is that it was never a one size fits all deal.

    Two juniors pairing usually produce better code than either of them would have alone. Genuinely better. But still not as good as what comes out when a senior is in the pair. I've watched that difference play out enough times to treat it as a hard rule rather than a hunch.

    So the shortcut was never "pairing gets a lighter review." It was closer to "this pair, working on this, has earned a lighter review." Everybody doing the reviewing knew that, even if nobody ever said it.

    Which raises the question that made me want to write all of this stuff down...if a developer plus an agent counts as a pair, which pair is it?

    AI code has not built that record

    I want to be careful here, because there's a lot to consider and the tooling is moving fast. But from what I've seen up to this point, AI generated code on its own comes with a lot of problems in it. Security issues. Functional bugs. And a slower one that worries me more than the rest...people losing their understanding of their own systems, which makes those systems harder to fix the longer it goes on.

    None of that earns a lighter review.

    The cost moved the other direction at the same time, too. We've all read and seen how producing code got cheap. Evaluating it didn't. Nothing about an agent made reading code faster, made holding a system in your head easier, or made it quicker to spot a wrong assumption (no matter how confident the agent sounds).

    So put those together. A lot of developers are working with agents right now. If that counts as pairing, it gets the lighter review. So we read less of the code, right when a lot more of it is showing up. And it's the kind that's hardest to spot problems in just by reading it.

    That cost doesn't stay with the team that filed it that way. It lands on users, on clients, and on the business, usually much later.

    Nobody is going to decide this in a meeting

    That's the part I'm mulling over today.

    No one on the engineering team is going to stand up and propose reviewing AI written code less carefully (at least I hope not). That's not how it happens. It happens when a category you already had quietly absorbs a new kind of work, and the rule attached to that category comes along for the ride. Dev + agent starts getting called pairing, pairing already had a discount, and the discount transfers without anyone explicitly deciding anything.

    Most teams inherited their review shortcuts, and I'd be surprised if anyone wrote down what earned them. So when something new shows up asking to be filed under an existing category, there's nothing to check it against.

    So here is what I would actually ask you to do, and then a real question.

    Go find your team's review shortcuts. The paired code one. The "it's only a config change" one. The "this person's PRs are always clean" one. They exist, they're mostly unwritten, and you probably inherited at least one of them from a team you're no longer on.

    Then ask what earned each one, and whether that thing is still true for the code you're merging today.

    What did you find when you went looking? I am very curious whether other teams have caught this happening, or whether it's already too far along to see clearly. If you would rather talk it through than leave a comment, my inbox is open.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:09

    ↗

    Three weeks ago I wrote about the difference between AI training crawlers and AI retrieval agents: GPTBot collects data to train models, but it's ChatGPT-User and OAI-SearchBot that fetch your page live when ChatGPT answers a question — and only those can cite and link you....

    Three weeks ago I wrote about the difference between AI training crawlers and AI retrieval
    agents
    :
    GPTBot collects data to train models, but it's ChatGPT-User and OAI-SearchBot that fetch
    your page live when ChatGPT answers a question — and only those can cite and link you.
    Block them and you don't exist in AI answers, no matter how well you rank on Google.

    A commenter said they hadn't realized these were different bots. That made me wonder how many
    professional publishers haven't either. So I measured it.

    Method

    On August 1, 2026 I ran an automated check against the 50 biggest English-language news and
    tech publishers. For each site:

    1. Fetch robots.txt and evaluate it for 15 AI crawlers — 7 retrieval agents (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Amazonbot) and 8 training crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, Applebot-Extended, Bytespider, meta-externalagent, anthropic-ai) — using Google's documented robots semantics (most specific group wins, longest rule wins, Allow wins ties), for path /.
    2. Fetch the homepage without executing JavaScript — because that's what retrieval agents do — and check whether the served HTML contains any readable text at all.

    A site counts as citable only if both hold: no retrieval agent blocked, and actual text in
    the served HTML. The tool is an open audit Actor I
    built
    ; four sites (NYT, Guardian, FT, Daily
    Mail) bot-wall their homepage, so for those only the robots.txt half was evaluated — which
    already settles their verdict.

    Results

    38 of 50 are not citable.

    9 sites block all seven retrieval agents: CNN, NBC News, USA Today, HuffPost, The
    Telegraph, Daily Mail, CNET, ZDNet, Mashable. Whatever these newsrooms publish, no AI answer
    can ever quote it or link to it.

    25 sites block in the wrong direction. They allow at least one training crawler while
    blocking retrieval agents — meaning their content may train models, but the one thing that
    sends readers back (a citation with a link) is off. The Verge, Wired, Ars Technica, The
    Atlantic, Vox, The Guardian, The Washington Post and WSJ are all in this group. I doubt a
    single one chose that trade on purpose.

    5 sites fail by exactly one agent. ABC News and TechCrunch block only ChatGPT-User;
    Axios, Tom's Hardware and VentureBeat block only Amazonbot. One robots.txt line away from
    citable.

    3 sites have the door open and the room empty. NPR, Politico and The Information allow
    all seven retrieval agents — and serve a homepage whose HTML contains essentially no readable
    text without JavaScript. Retrieval agents don't run JavaScript. A browser shows a normal page;
    an AI agent gets nothing to quote. This failure is invisible in every browser-based audit.

    Who gets blocked most tells its own story. PerplexityBot is blocked by 30 of 50 sites,
    Amazonbot by 28, Anthropic's two retrieval agents by 24–25 — but OpenAI's OAI-SearchBot by
    only 14. That's the licensing-deal era in one number: publishers with OpenAI deals let OpenAI's
    citation bot in and block everyone else's.

    The 12 citable sites: Fox News, CBS News, Business Insider, LA Times, Time, Slate, The
    Independent, The Daily Beast, Semafor, Engadget, Gizmodo, PCMag.

    Full table

    Retrieval = citation agents allowed (of 7). Training = training crawlers allowed (of 8).
    * = robots.txt only (homepage bot-walled).

    Site Retrieval Training Citable Why not
    cnet.com 0/7 1/8 ❌ robots.txt
    cnn.com 0/7 1/8 ❌ robots.txt
    dailymail.co.uk * 0/7 0/8 ❌ robots.txt
    huffpost.com 0/7 0/8 ❌ robots.txt
    mashable.com 0/7 1/8 ❌ robots.txt
    nbcnews.com 0/7 0/8 ❌ robots.txt
    telegraph.co.uk 0/7 0/8 ❌ robots.txt
    usatoday.com 0/7 0/8 ❌ robots.txt
    zdnet.com 0/7 1/8 ❌ robots.txt
    bloomberg.com 1/7 0/8 ❌ robots.txt
    economist.com 1/7 1/8 ❌ robots.txt
    nytimes.com * 1/7 0/8 ❌ robots.txt
    arstechnica.com 2/7 1/8 ❌ robots.txt
    bbc.com 2/7 0/8 ❌ robots.txt
    cnbc.com 2/7 0/8 ❌ robots.txt
    marketwatch.com 2/7 1/8 ❌ robots.txt
    newyorker.com 2/7 2/8 ❌ robots.txt
    reuters.com 2/7 0/8 ❌ robots.txt
    theatlantic.com 2/7 1/8 ❌ robots.txt
    theverge.com 2/7 1/8 ❌ robots.txt
    vox.com 2/7 1/8 ❌ robots.txt
    wired.com 2/7 2/8 ❌ robots.txt
    wsj.com 2/7 1/8 ❌ robots.txt
    apnews.com 3/7 3/8 ❌ robots.txt
    nypost.com 3/7 1/8 ❌ robots.txt
    theguardian.com * 3/7 2/8 ❌ robots.txt
    newsweek.com 4/7 2/8 ❌ robots.txt
    forbes.com 5/7 1/8 ❌ robots.txt
    ft.com * 5/7 1/8 ❌ robots.txt
    washingtonpost.com 5/7 2/8 ❌ robots.txt
    abcnews.go.com 6/7 3/8 ❌ blocks only ChatGPT-User
    axios.com 6/7 6/8 ❌ blocks only Amazonbot
    techcrunch.com 6/7 1/8 ❌ blocks only ChatGPT-User
    tomshardware.com 6/7 6/8 ❌ blocks only Amazonbot
    venturebeat.com 6/7 5/8 ❌ blocks only Amazonbot
    npr.org 7/7 8/8 ❌ no text without JavaScript
    politico.com 7/7 8/8 ❌ no text without JavaScript
    theinformation.com 7/7 8/8 ❌ no text without JavaScript
    businessinsider.com 7/7 4/8 ✅
    cbsnews.com 7/7 7/8 ✅
    engadget.com 7/7 8/8 ✅
    foxnews.com 7/7 8/8 ✅
    gizmodo.com 7/7 5/8 ✅
    independent.co.uk 7/7 8/8 ✅
    latimes.com 7/7 5/8 ✅
    pcmag.com 7/7 8/8 ✅
    semafor.com 7/7 7/8 ✅
    slate.com 7/7 8/8 ✅
    thedailybeast.com 7/7 8/8 ✅
    time.com 7/7 8/8 ✅

    Caveats, honestly

    • Some of this is deliberate. The NYT is suing OpenAI; several publishers are negotiating licenses. For them, blocking is leverage, not an accident. But "we allow model training and forbid citations" (25 sites) is a hard position to defend as strategy — and the sites that copied 2023's "block the AI bots" lists inherited these rules with none of the leverage.
    • This is a snapshot (Aug 1, 2026) of path / and the homepage. Sections may differ.
    • "Citable" means technically reachable for citation, not "gets cited".

    Check your own site

    Two failure modes, both invisible in a browser and in classic SEO tools:

    1. robots.txt: look for your citation agents (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Amazonbot). Blanket AI-blocklists and one-click CDN blockers usually hit these too. The 5-minute manual check is in my previous post.
    2. Server-rendered text: curl your page and look for your content in the HTML. If it only appears after JavaScript runs, retrieval agents see an empty shell.

    For a single site you honestly don't need a tool — the manual check takes five minutes.
    Where it stops being trivial:

    • You manage a portfolio. An agency with 50 client sites doesn't read 50 robots.txt files quarterly. This entire study — 50 sites, both checks — ran automated in about 40 minutes.
    • The answer changes behind your back. Robots.txt files drift: CDN one-click "block AI bots" switches, CMS updates, replatforming. Cloudflare's managed robots.txt added rules to a site of mine that I never wrote. A check that was green in March can be red in June with nobody having touched anything.

    That's what I built SEO Health Auditor for:
    it runs both checks (plus regular technical SEO) across any list of sites, from $0.05 per
    page, and on a schedule it diffs against the previous run — so you learn about the drift
    before your traffic does.

    Raw data for all 50 sites available on request — happy to share the JSON.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 20:00

    ↗

    Introduction: The Monolithic Illusion in Modern Multi-Agent Architecture If you have spent any time building localized agentic workflows using frameworks like LangGraph, you are likely familiar with the cozy comfort of a single-node memory space. In that localized paradigm,...

    Introduction: The Monolithic Illusion in Modern Multi-Agent Architecture

    If you have spent any time building localized agentic workflows using frameworks like LangGraph, you are likely familiar with the cozy comfort of a single-node memory space. In that localized paradigm, state mutations feel completely trivial. Every worker node, supervisor orchestration loop, and tool-use reflection routine reads from and writes to a monolithic, in-memory graph state object under the absolute protection of a single local event loop.

    Accessing a variable, updating a chat history, or appending a scraped DOM element happens instantly and completely free of concurrency hazards. Everything occurs sequentially or within a single, predictable thread.

    However, the moment your architecture scales out—moving from a cozy single-node execution environment to a distributed, multi-node cluster—that monolithic illusion shatters entirely. Imagine deploying specialized agents across different edge nodes, handling asynchronous browser automation tasks, or executing Model Context Protocol (MCP) tool servers across distinct cloud regions. When these disparate entities must collaborate on a single long-running task, the local StateGraph paradigm completely collapses.

    Without a rigorous, mathematically sound distributed context layer, your systems will inevitably suffer from catastrophic split-brain scenarios, painful race conditions, lost updates during tool-use reflection loops, and desynchronized supervisor routing paths. To truly master modern, production-grade AI systems, you must understand how to architect distributed context management and agent state synchronization.

    The Microservices Parallel: Monolithic State vs. Distributed Caching

    To truly grasp why distributed context management is such a formidable challenge, we can look to a powerful analogy from modern web development: Microservices and Distributed Caching vs. Monolithic State.

    Imagine a traditional monolithic web application where every component of the system—the user session manager, the shopping cart, the product catalog, and the checkout processor—shares a single, massive global JavaScript object in memory. Accessing and updating the shopping cart is instantaneous and free of race conditions because everything happens inside a single memory space. This is precisely analogous to our single-node StateGraph.

    Now, scale that exact same application into a distributed microservices architecture deployed across a Kubernetes cluster. You have a Cart Service, a User Service, and a Payment Service, all running on separate containers, communicating over the network via gRPC or HTTP. If the Cart Service needs to know the user's current loyalty tier managed by the User Service, it cannot simply read a variable from local memory. It must query across the network, handle network latency, deal with unexpected network partitions, and resolve race conditions when two services try to update the user's state simultaneously.

    Distributed context management in multi-agent MCP systems is the exact equivalent of solving the microservices data consistency problem. The StateGraph is no longer a localized data structure; it is a distributed, eventually consistent state machine. Every agent, supervisor, and MCP tool server acts as a distributed node that must agree upon the current reality of the browser DOM, active tool outputs, and internal agent reasoning steps.

    The Anatomical Layers of Distributed Context

    To construct a robust distributed context management system for agentic workflows, we must break down the architecture into three foundational layers:

    1. The State Representation Layer: How the graph state is modeled so it can traverse complex networks.
    2. The Synchronization Layer: How conflicting mutations from parallel agents are resolved without human intervention.
    3. The Persistence and Fault-Tolerance Layer: How long-running browser automation tasks survive network drops, node crashes, and MCP server restarts.

    1. State Representation and the Distributed Graph State

    In a localized environment, the StateGraph maintains a mutable dictionary or object. In a distributed system, this object must be serialized, transmitted, and reconstructed across heterogeneous runtimes. Furthermore, when dealing with Model Context Protocol (MCP) servers, the context includes not just text messages and variables, but complex binary payloads, DOM snapshots, screenshot buffers, and dynamic tool schemas.

    Consider the role of the Supervisor Node in this distributed topology. The Supervisor acts as the central traffic controller, making routing decisions based on the current Graph State. In a distributed setting, the Supervisor does not hold the true state in its own volatile memory; rather, it queries a distributed view of the state. If two worker agents—say, one executing a web scraper via a browser automation MCP server and another analyzing financial data—both complete their tasks simultaneously, they generate parallel state updates.

    This is structurally similar to handling state synchronization in a collaborative real-time editing application like Figma or Google Docs. When two users type in the same text box simultaneously, the application cannot simply overwrite one user's input with the other's. It must track every keystroke as an operation, merge them logically, and maintain a coherent document state across all connected clients. In our agentic system, when Worker Agent A extracts a table from a webpage and Worker Agent B clicks a pagination button, these two actions mutate the shared browser context. If their states are not synchronized precisely, the Supervisor will route the next task based on stale or contradictory information, causing the agentic workflow to hallucinate, loop infinitely, or crash.

    2. The Mechanics of State Synchronization: CRDTs vs. Distributed Locks

    When multiple agents attempt to modify the shared graph state concurrently, we face the classic concurrency problem of computer science. There are two primary architectural philosophies for solving this in distributed systems: Pessimistic Concurrency Control (Distributed Locking) and Optimistic Replicated Data Types (Conflict-free Replicated Data Types, or CRDTs).

    Pessimistic Concurrency via Distributed Locking

    In a pessimistic locking model, before an agent can invoke a tool via the Model Context Protocol or modify a section of the StateGraph, it must acquire a distributed lock (often implemented using Redis, ZooKeeper, or etcd).

    • The Process: Worker Agent A requests an exclusive lease on the browser navigation state. The distributed lock manager grants the lease with a Time-To-Live (TTL) to prevent deadlocks if the agent crashes. While Agent A holds the lock, Worker Agent B’s request to click a DOM element is blocked or rejected. Once Agent A finishes its tool execution and updates the graph state, it releases the lock, allowing Agent B to proceed.
    • The Trade-off: While this guarantees absolute safety and zero conflict, it introduces severe latency bottlenecks. Browser automation tasks are inherently slow; waiting for network round-trips to acquire and release distributed locks for every single DOM interaction creates an unacceptable performance drag, crippling the real-time responsiveness required by complex multi-agent workflows.

    Optimistic Concurrency via CRDTs

    To achieve high-throughput, low-latency collaboration without blocking agents, advanced distributed MCP architectures rely on Conflict-free Replicated Data Types (CRDTs).

    • The Process: CRDTs are specialized data structures mathematically proven to converge to the same value across all replicas without requiring locks, regardless of the order in which network messages arrive. Every agent maintains a local replica of the graph state and the MCP context. When an agent performs a mutation, it applies the mutation locally and broadcasts a state delta to all other nodes.
    • Mathematical Convergence: CRDTs rely on commutative, associative, and idempotent operations. Whether state update $A$ arrives before state update $B$ on Node 1, but update $B$ arrives before update $A$ on Node 2, the underlying mathematical structure guarantees that after both updates are processed, both nodes will arrive at an identical state representation.
    • Application to Agents: In the context of our agentic system, conversation histories, tool output registries, and state variables are structured as state-based CRDTs (CvRDTs) or operation-based CRDTs (CmRDTs). For example, a chat history is modeled as a Grow-Only Set (G-Set) or Observed-Remove Set (OR-Set), ensuring that messages appended by parallel worker agents are never lost, even during network partitions.

    Concurrency Comparison Matrix

    Dimension Distributed Locking (Pessimistic) CRDTs (Optimistic)
    Concurrency Model Mutual exclusion; one agent writes at a time. Concurrent writes allowed everywhere; merged automatically.
    Network Latency Impact High. Requires synchronous round-trips to acquire/release leases. Low. Asynchronous fire-and-forget delta broadcasting.
    Fault Tolerance Vulnerable to deadlocks if nodes crash holding locks (requires TTL timeouts). Highly resilient; nodes operate completely offline and sync upon reconnection.
    Ideal Use Case Financial transactions, exclusive hardware resource allocation (e.g., single browser instance control). Collaborative agent workspaces, shared memory graphs, tool output logs, chat histories.

    3. Event-Driven State Replication and Persistence

    A distributed agentic system is only as reliable as its event replication and persistence layers. Long-running browser automation tasks—such as scraping thousands of pages, filling out multi-step enterprise forms, or monitoring dynamic dashboards—can span hours or even days. During such extended runs, individual worker nodes, MCP tool servers, or network switches are bound to fail.

    To ensure fault tolerance and seamless session recovery, the distributed state management layer must implement Event-Driven State Replication.

    • The Event Sourcing Pattern: Instead of merely saving the current snapshot of the StateGraph to a database, every state transition, tool call, and tool-use reflection observation is recorded as an immutable event in an append-only log (such as Apache Kafka, Redis Streams, or NATS).
    • State Reconstruction (Rehydration): If a worker node running a browser automation MCP server crashes mid-task, a supervisor node or a standby worker can instantly spin up, read the event stream from the distributed log, and replay every event in chronological order to reconstruct the exact graph state up to the millisecond of the failure. This process, known as event sourcing and state rehydration, ensures that an agent never loses its "train of thought" or the context gathered by expensive tool invocations.

    Furthermore, this event-driven architecture empowers the Tool Use Reflection loop in a distributed setting. When a worker agent executes a tool via an MCP server, the raw output (e.g., a massive JSON payload or a base64-encoded screenshot of a broken webpage) is published as an event. A distributed reflection service consumes this event, evaluates the success or failure of the tool call against the graph state, and emits a correction event. This decoupled, event-driven feedback loop allows multiple supervisor nodes to monitor agent health and dynamically re-route failing tasks to healthier worker nodes without interrupting the main execution thread.

    Deep Architectural Dive: The Lifecycle of a Distributed Agentic Task

    To synthesize these theoretical foundations, let us trace the complete lifecycle of a complex task traversing a distributed MCP and browser automation environment.

    1. Task Initialization and State Bootstrap:
      A user submits a high-level goal: "Audit all competitor pricing pages across 50 e-commerce domains and compile a unified market report." The primary entry point receives this request and initializes the distributed StateGraph state, committing the initial goal vector and configuration parameters to the distributed CRDT store and appending the creation event to the event log.

    2. Supervisor Routing and Distributed Locking:
      The central Supervisor Node analyzes the graph state. It determines that the task requires parallel execution and splits the 50 domains into batches of 10. It assigns each batch to a distinct Worker Agent running on a separate cluster node. Before dispatching the browser automation commands, each worker acquires a non-blocking lease or registers its intent in the CRDT state vector to prevent duplicate scraping of the same domain.

    3. MCP Tool Execution and State Mutation:
      Worker Agent 1 connects to its local Browser Automation MCP Server. It launches a headless browser instance, navigates to competitor URL A, and extracts the pricing table. The raw DOM data and a visual screenshot are returned to the MCP server. The worker agent packages this output into a state delta and broadcasts it via the CRDT synchronization engine. Across the cluster, all other worker nodes and replica supervisors instantly integrate this delta into their local views of the graph state without locking the system.

    4. Tool Use Reflection and Error Handling:
      Simultaneously, Worker Agent 2 encounters a CAPTCHA challenge on competitor URL B. The MCP server returns a tool output indicating failure. In a localized system, this would trigger a simple try-catch block. In our distributed architecture, this failure event is published to the event-driven replication bus. The Tool Use Reflection service intercepts the failure event, analyzes the observation, and determines that a human-in-the-loop intervention or a proxy rotation MCP tool must be invoked.

    5. Consensus and Session Recovery:
      As workers complete their sub-tasks, their state mutations converge deterministically via the CRDT engine. The supervisor continuously evaluates the converged graph state. If a worker node abruptly loses power, the cluster's heartbeat monitor detects the drop, the event log replays the last known state to a newly spawned container, and the browser automation task resumes seamlessly from the exact point of failure.

    Practical Implementation: Building a Distributed Context Manager

    To understand how distributed context management and state synchronization operate within a modern Model Context Protocol (MCP) infrastructure, we must examine a clean, self-contained implementation.

    In a SaaS web application context—such as a collaborative browser-automation workspace where multiple AI agents concurrently inspect DOM nodes, execute navigation actions, and modify shared system state—race conditions can corrupt session context. Below is a foundational, fully self-contained TypeScript implementation illustrating a distributed state synchronization mechanism using a simplified Conflict-free Replicated Data Type (CRDT)-inspired state container paired with a distributed locking utility.

    /**
     * @file distributed-context.ts
     * @description A self-contained TypeScript implementation of a distributed context 
     * manager and state synchronizer for multi-agent browser automation tasks.
     */
    
    import { randomUUID } from 'crypto';
    
    // ============================================================================
    // Types & Interfaces
    // ============================================================================
    
    /**
     * Represents a single piece of context or artifact generated by an agent.
     */
    interface ContextArtifact {
        id: string;
        agentId: string;
        key: string;
        value: unknown;
        vector: number; // Logical clock vector component
        timestamp: number;
    }
    
    /**
     * Represents a distributed lock acquired by an agent to modify critical context.
     */
    interface DistributedLock {
        resourceKey: string;
        ownerAgentId: string;
        expiresAt: number;
    }
    
    /**
     * Log entry for event-driven state replication.
     */
    interface ReplicationEvent {
        eventId: string;
        type: 'SET' | 'DELETE' | 'LOCK' | 'UNLOCK';
        payload: unknown;
        vector: number;
        timestamp: number;
    }
    
    // ============================================================================
    // Core Implementation
    // ============================================================================
    
    /**
     * Manages distributed agent context, state synchronization, and concurrency control.
     */
    export class DistributedContextManager {
        private store: Map<string, ContextArtifact> = new Map();
        private locks: Map<string, DistributedLock> = new Map();
        private eventLog: ReplicationEvent[] = [];
        private nodeLogicalClock: number = 0;
        private readonly nodeIdentity: string;
    
        constructor(nodeIdentity?: string) {
            this.nodeIdentity = nodeIdentity || `node-${randomUUID().slice(0, 8)}`;
        }
    
        /**
         * Attempts to acquire a distributed lock on a specific resource key.
         * Prevents race conditions during parallel tool execution.
         * 
         * @param resourceKey The key representing the shared resource or state segment.
         * @param agentId The identifier of the agent requesting the lock.
         * @param ttlMs Time-to-live for the lock in milliseconds.
         * @returns boolean indicating success or failure.
         */
        public async acquireLock(resourceKey: string, agentId: string, ttlMs: number = 5000): Promise<boolean> {
            const now = Date.now();
            const existingLock = this.locks.get(resourceKey);
    
            // Check if lock exists and is still valid
            if (existingLock && existingLock.expiresAt > now) {
                if (existingLock.ownerAgentId !== agentId) {
                    return false; // Held by another agent
                }
                // Renewable by the same owner
                existingLock.expiresAt = now + ttlMs;
                return true;
            }
    
            // Acquire new or expired lock
            const newLock: DistributedLock = {
                resourceKey,
                ownerAgentId: agentId,
                expiresAt: now + ttlMs
            };
    
            this.locks.set(resourceKey, newLock);
            this.nodeLogicalClock++;
    
            this.recordEvent({
                eventId: randomUUID(),
                type: 'LOCK',
                payload: newLock,
                vector: this.nodeLogicalClock,
                timestamp: now
            });
    
            return true;
        }
    
        /**
         * Releases a distributed lock on a resource key.
         */
        public async releaseLock(resourceKey: string, agentId: string): Promise<boolean> {
            const existingLock = this.locks.get(resourceKey);
            if (!existingLock || existingLock.ownerAgentId !== agentId) {
                return false;
            }
    
            this.locks.delete(resourceKey);
            this.nodeLogicalClock++;
    
            this.recordEvent({
                eventId: randomUUID(),
                type: 'UNLOCK',
                payload: { resourceKey, ownerAgentId: agentId },
                vector: this.nodeLogicalClock,
                timestamp: Date.now()
            });
    
            return true;
        }
    
        /**
         * Sets a context artifact using Last-Write-Wins (LWW) with logical clocks 
         * to resolve conflicts deterministically.
         */
        public async setContext(agentId: string, key: string, value: unknown): Promise<ContextArtifact> {
            const now = Date.now();
            this.nodeLogicalClock++;
    
            const existing = this.store.get(key);
    
            // Conflict Resolution: Last-Write-Wins based on logical vector, then timestamp
            if (existing) {
                if (
                    existing.vector > this.nodeLogicalClock || 
                    (existing.vector === this.nodeLogicalClock && existing.timestamp > now)
                ) {
                    // Reject out-of-order stale update
                    return existing;
                }
            }
    
            const artifact: ContextArtifact = {
                id: randomUUID(),
                agentId,
                key,
                value,
                vector: this.nodeLogicalClock,
                timestamp: now
            };
    
            this.store.set(key, artifact);
    
            this.recordEvent({
                eventId: randomUUID(),
                type: 'SET',
                payload: artifact,
                vector: this.nodeLogicalClock,
                timestamp: now
            });
    
            return artifact;
        }
    
        /**
         * Retrieves a context artifact by key.
         */
        public getContext(key: string): unknown | undefined {
            return this.store.get(key)?.value;
        }
    
        /**
         * Replicates incoming remote state changes into the local node store.
         */
        public applyRemoteEvent(event: ReplicationEvent): void {
            // Update local logical clock to maintain causality
            this.nodeLogicalClock = Math.max(this.nodeLogicalClock, event.vector) + 1;
    
            if (event.type === 'SET') {
                const artifact = event.payload as ContextArtifact;
                const existing = this.store.get(artifact.key);
    
                // Apply LWW conflict resolution rule
                if (!existing || 
                    artifact.vector > existing.vector || 
                    (artifact.vector === existing.vector && artifact.timestamp > existing.timestamp)) {
                    this.store.set(artifact.key, artifact);
                }
            } else if (event.type === 'LOCK') {
                const lock = event.payload as DistributedLock;
                this.locks.set(lock.resourceKey, lock);
            } else if (event.type === 'UNLOCK') {
                const unlockData = event.payload as { resourceKey: string; ownerAgentId: string };
                const existing = this.locks.get(unlockData.resourceKey);
                if (existing && existing.ownerAgentId === unlockData.ownerAgentId) {
                    this.locks.delete(unlockData.resourceKey);
                }
            }
    
            this.eventLog.push(event);
        }
    
        /**
         * Appends an event to the internal audit and replication log.
         */
        private recordEvent(event: ReplicationEvent): void {
            this.eventLog.push(event);
        }
    
        /**
         * Exports the entire state for node bootstrap or recovery.
         */
        public exportState(): { store: [string, ContextArtifact][]; locks: [string, DistributedLock][]; clock: number } {
            return {
                store: Array.from(this.store.entries()),
                locks: Array.from(this.locks.entries()),
                clock: this.nodeLogicalClock
            };
        }
    }
    
    // ============================================================================
    // Execution Demonstration (SaaS Browser Automation Context)
    // ============================================================================
    
    async function runDemo() {
        console.log("Initializing Distributed Context Manager for Browser Automation Agents...");
        const manager = new DistributedContextManager("node-primary-us-east");
    
        const agentId = "agent-browser-worker-01";
        const targetResource = "dom_snapshot_login_page";
    
        // 1. Attempt to acquire lock before scraping/modifying page context
        const hasLock = await manager.acquireLock(targetResource, agentId, 10000);
        console.log(`Agent ${agentId} acquired lock on '${targetResource}': ${hasLock}`);
    
        if (hasLock) {
            // 2. Set shared context state after interacting with the browser
            const artifact = await manager.setContext(agentId, targetResource, {
                url: "https://saas.example.com/login",
                domTitle: "Sign In - Enterprise Portal",
                inputElementsDetected: 2,
                formInteractive: true
            });
            console.log(`Context artifact successfully synchronized:`, artifact);
    
            // 3. Release lock upon completion
            const released = await manager.releaseLock(targetResource, agentId);
            console.log(`Agent ${agentId} released lock on '${targetResource}': ${released}`);
        }
    }
    
    // Execute the simulation
    runDemo().catch(console.error);
    

    Conclusion: Engineering Resilient Agent Networks

    Moving beyond single-node prototypes into production-grade multi-agent architectures requires a fundamental shift in how we approach state, concurrency, and network partitions. As we have explored throughout this guide, distributed context management and agent state synchronization are not merely optional optimizations—they are the core pillars that prevent distributed agent networks from collapsing into race conditions, conflicting tool outputs, and unrecoverable split-brain states.

    By carefully selecting between pessimistic distributed locks and optimistic CRDTs, implementing event-driven replication logs for flawless session rehydration, and structuring your Model Context Protocol (MCP) servers to handle asynchronous mutations natively, you lay the groundwork for truly bulletproof enterprise automation. Whether you are coordinating dozens of browser automation workers across global cloud regions or scaling complex supervisor-worker hierarchies, mastering these distributed primitives ensures your agentic systems remain robust, scalable, and ready for production at a global scale.

    The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the book Model Context Protocol (MCP) & Computer Use. Standardizing Tool Integration, Vision-Driven Browser Automation, and Agent Governance in TypeScript, you can find it here. Check also the many other ebooks.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:59

    ↗

    Introduction Imagine sending 100 from Alice to Bob. If both accounts are in one database, this is easy: start a transaction, update both balances, and commit. But this example puts Alice in MySQL and Bob in PostgreSQL. One database cannot finish—or undo—the other database's...

    Introduction

    Imagine sending 100 from Alice to Bob.

    If both accounts are in one database, this is easy: start a transaction, update both balances, and commit. But this example puts Alice in MySQL and Bob in PostgreSQL. One database cannot finish—or undo—the other database's work.

    That is the problem two-phase commit, usually called 2PC, tries to solve.

    This article follows a small Go program that performs the transfer and then crashes on purpose. The two crashes happen almost at the same moment, but recovery gives them opposite answers. That small difference is the easiest way to understand 2PC.

    The promise we need

    At the start:

    Alice in MySQL:      500
    Bob in PostgreSQL:   100
    

    After a successful transfer:

    Alice in MySQL:      400
    Bob in PostgreSQL:   200
    

    If anything goes wrong, both balances should stay as they were. We never want this:

    Alice in MySQL:      400
    Bob in PostgreSQL:   100
    

    That would mean Alice lost money and Bob never received it.

    Two-phase commit in simple words

    2PC has one person in charge—the coordinator—and the databases doing the work.

    First, the coordinator asks each database, "Can you do your part?" The databases save their work, hold on to it, and answer yes or no. This is the prepare phase.

    If everyone says yes, the coordinator decides to commit. If anyone says no, it decides to undo everything. This is the decision phase.

    The important thing is that a yes vote does not mean the database has committed. It means:

    "I have saved my work, I am keeping the row locked, and I will wait for your final answer."

    Where XA fits in

    2PC is the idea. XA is a standard set of commands that helps a coordinator use that idea with different database systems.

    MySQL calls its commands XA PREPARE and XA COMMIT. PostgreSQL uses PREPARE TRANSACTION and COMMIT PREPARED. Different names, same story:

    What needs to happen MySQL PostgreSQL
    Start work XA START BEGIN
    Say “I am ready” XA PREPARE PREPARE TRANSACTION
    Finish successfully XA COMMIT COMMIT PREPARED
    Undo the work XA ROLLBACK ROLLBACK PREPARED

    You do not need to memorize those commands to understand the example. The Go program uses the right command for each database and treats both answers in the same way.

    Running the example

    The program starts MySQL and PostgreSQL locally, creates the two accounts, and runs the normal transfer plus both crash tests.

    docker run -d --name xa-mysql -e MYSQL_ROOT_PASSWORD=xa -e MYSQL_DATABASE=bank \
      -p 13306:3306 mysql:8
    docker run -d --name xa-pg -e POSTGRES_PASSWORD=xa -e POSTGRES_DB=bank \
      -p 15432:5432 postgres:16 -c max_prepared_transactions=20
    go run .
    

    The PostgreSQL setting is needed because prepared transactions are turned off by default in many installations.

    Reading the Go program

    Setup

    connect() opens one connection pool for MySQL and one for PostgreSQL. reset() starts every run with Alice at 500 and Bob at 100.

    The transfer is given one shared name, such as tx-ok. That name lets recovery find the pieces of the same transfer later.

    Doing the transfer

    Inside transfer(), the program first makes the two local changes:

    UPDATE accounts SET balance = balance - ? WHERE id='alice'
    UPDATE accounts SET balance = balance + $1 WHERE id='bob'
    

    The changes are not visible as a completed transfer yet. They are still waiting inside each database.

    Asking both databases to prepare

    Next, the coordinator asks both databases to get ready:

    XA PREPARE 'tx','my',1
    PREPARE TRANSACTION 'tx'
    

    When those calls work, MySQL and PostgreSQL have both said yes. They have saved their changes and are waiting. They also keep locks on the affected rows, so nobody else can change Alice or Bob in a conflicting way.

    The one line that matters most

    After both yes votes, the coordinator writes its answer to a small file named decisions.log:

    if err := decide(gtrid, "commit"); err != nil {
        rollback(my, pg, gtrid)
        return err
    }
    

    Inside decide(), the program calls f.Sync(). That makes sure the decision really reaches disk.

    This is the real commit point. Not the later database commands. Once commit is safely written to the log, the coordinator has made its choice forever. If it crashes after this, recovery must finish the commit.

    Two crashes that explain everything

    The program crashes in two places: once just before the log is written, and once just after. Only one f.Sync() sits between them.

    Crash before the decision is saved

    Both databases have prepared their work. But the coordinator crashes before writing commit to decisions.log.

    When the program comes back, it asks the databases, "Do you still have unfinished work?" They both say yes. Then it checks the log. There is no commit decision there.

    So it rolls both sides back:

    mysql:     XA ROLLBACK
    postgres:  ROLLBACK PREPARED
    

    This is called presumed abort. In everyday language: if the coordinator did not save a decision to commit, assume the transfer did not happen.

    Crash after the decision is saved

    Now move the crash one line later. Both databases prepare their work. The coordinator writes commit to the log and safely syncs it to disk. Then it crashes before it can tell either database.

    When it starts again, the balances may still look unchanged because the databases are still waiting. But the log says commit, so recovery has only one safe choice:

    mysql:     XA COMMIT
    postgres:  COMMIT PREPARED
    

    The coordinator is not deciding again. It is simply delivering the decision it already made.

    How recovery works

    recoverInDoubt() is the cleanup worker in this example. Its rule is short:

    Does the log say commit?
    
    Yes -> commit the unfinished work.
    No  -> roll it back.
    

    It asks MySQL about unfinished XA transactions and PostgreSQL about unfinished prepared transactions. Then it checks decisions.log and gives both databases the same answer.

    This works because the code always saves the commit decision before it starts sending commit commands to the databases.

    The downside: other work can wait

    While a database is prepared, it keeps locks. In the crash-after test, Alice's row is still locked while the coordinator is down.

    showBlocking() opens another connection and tries to update Alice. It waits and then gets this error:

    Error 1205 (HY000): Lock wait timeout exceeded
    

    That is the real cost of 2PC. The databases are safe, but they cannot decide by themselves while they wait for the coordinator. This is why people call 2PC a blocking protocol.

    Should you use it?

    2PC is useful when a small number of dependable systems truly need one all-or-nothing decision. It is not the usual choice for every application.

    Many systems use a transactional outbox or a saga instead. Those patterns are often easier to run because they avoid keeping locks across systems, but they make a different trade-off: they do not give the same single global commit.

    The one thing to remember

    A database saying yes has not committed. It has promised to wait. The transfer becomes committed when the coordinator safely records its decision.

    In this program, one call to f.Sync() separates a transfer that recovery must roll back from a transfer that recovery must commit.

    Complete main.go

    The following source is embedded verbatim from the demo.

    // One global transaction across two different databases: 100 leaves alice in
    // MySQL and arrives at bob in Postgres, atomically. Then we kill the coordinator
    // at the worst possible moment and let recovery clean up.
    //
    //  docker run -d --name xa-mysql -e MYSQL_ROOT_PASSWORD=xa -e MYSQL_DATABASE=bank \
    //    -p 13306:3306 mysql:8
    //  docker run -d --name xa-pg -e POSTGRES_PASSWORD=xa -e POSTGRES_DB=bank \
    //    -p 15432:5432 postgres:16 -c max_prepared_transactions=20
    //  go run .
    package main
    
    import (
        "bufio"
        "context"
        "database/sql"
        "errors"
        "fmt"
        "log"
        "os"
        "strings"
    
        _ "github.com/go-sql-driver/mysql"
        _ "github.com/lib/pq"
    )
    
    const (
        mysqlDSN = "root:xa@tcp(127.0.0.1:13306)/bank"
        pgDSN    = "postgres://postgres:xa@127.0.0.1:15432/bank?sslmode=disable"
    
        // The coordinator's durable decision log. This file is the source of truth
        // for whether a global transaction committed — not the databases.
        decisionLog = "decisions.log"
    )
    
    var errCrashed = errors.New("coordinator crashed")
    
    var ctx = context.Background()
    
    func main() {
        my, pg := connect()
        defer my.Close()
        defer pg.Close()
        reset(my, pg)
    
        fmt.Println("\n=== 1. a normal cross-database commit ===")
        balances(my, pg)
        if err := transfer(my, pg, "tx-ok", 100, crashNever); err != nil {
            log.Fatal(err)
        }
        balances(my, pg)
    
        fmt.Println("\n=== 2. the coordinator dies just AFTER the commit point ===")
        fmt.Println("   transfer returned:", transfer(my, pg, "tx-after", 100, crashAfter))
        fmt.Println("   the transfer is invisible — balances unchanged from step 1:")
        balances(my, pg)
        fmt.Println("   ...but both databases hold locks, so unrelated writes stall:")
        showBlocking(my)
    
        fmt.Println("\n=== 3. recovery must COMMIT it: the decision was already made ===")
        recoverInDoubt(my, pg)
        balances(my, pg)
    
        fmt.Println("\n=== 4. now the coordinator dies just BEFORE the commit point ===")
        fmt.Println("   same two YES votes, but nothing was written down.")
        fmt.Println("   transfer returned:", transfer(my, pg, "tx-before", 100, crashBefore))
    
        fmt.Println("\n=== 5. recovery must ROLL BACK: no decision means it never committed ===")
        recoverInDoubt(my, pg)
        balances(my, pg)
        fmt.Println("\n   Same votes, one instant apart, opposite outcomes. That's 2PC.")
    }
    
    // Where to kill the coordinator. The two crash points sit on either side of a
    // single fsync, and they must lead to opposite recoveries.
    type crashPoint int
    
    const (
        crashNever crashPoint = iota
        crashBefore
        crashAfter
    )
    
    // transfer runs the full two-phase commit.
    func transfer(my, pg *sql.DB, gtrid string, amount int, crash crashPoint) error {
        // Each branch of the transaction needs its own dedicated connection.
        myConn, err := my.Conn(ctx)
        if err != nil {
            return err
        }
        defer myConn.Close()
        pgConn, err := pg.Conn(ctx)
        if err != nil {
            return err
        }
        defer pgConn.Close()
    
        // ---- do the work, inside a branch on each database ----
        // MySQL speaks the XA dialect: 'gtrid','branch-qualifier',format-id.
        xid := fmt.Sprintf("'%s','my',1", gtrid)
        if err := exec(myConn, "XA START "+xid); err != nil {
            return err
        }
        if err := exec(myConn, "UPDATE accounts SET balance = balance - ? WHERE id='alice'", amount); err != nil {
            return err
        }
        // XA END says "no more work on this branch".
        if err := exec(myConn, "XA END "+xid); err != nil {
            return err
        }
    
        // Postgres has no XA API at all — just the same two phases as plain SQL,
        // identified by a single string instead of a structured XID.
        if err := exec(pgConn, "BEGIN"); err != nil {
            return err
        }
        if err := exec(pgConn, "UPDATE accounts SET balance = balance + $1 WHERE id='bob'", amount); err != nil {
            return err
        }
    
        // ---- PHASE 1: ask both databases to vote ----
        // A successful prepare means: "my changes are on disk, I've kept my locks,
        // and I will do whatever you say next — even if I crash in between."
        if err := exec(myConn, "XA PREPARE "+xid); err != nil {
            fmt.Println("   mysql voted NO:", err)
            rollback(my, pg, gtrid)
            return err
        }
        fmt.Println("   phase 1: XA PREPARE           -> mysql voted YES")
    
        if err := exec(pgConn, "PREPARE TRANSACTION '"+gtrid+"'"); err != nil {
            fmt.Println("   postgres voted NO:", err)
            rollback(my, pg, gtrid)
            return err
        }
        fmt.Println("   phase 1: PREPARE TRANSACTION  -> postgres voted YES")
    
        if crash == crashBefore {
            return errCrashed
        }
    
        // ---- THE COMMIT POINT ----
        // The transaction commits the instant this write is durable, even though
        // neither database knows it yet. Before this line, recovery is free to
        // abort. After it, recovery MUST commit. Everything below is bookkeeping.
        if err := decide(gtrid, "commit"); err != nil {
            rollback(my, pg, gtrid)
            return err
        }
        fmt.Printf("   COMMIT POINT: %q fsynced to %s\n", gtrid+" commit", decisionLog)
    
        if crash == crashAfter {
            return errCrashed
        }
    
        // ---- PHASE 2: deliver the news ----
        // A prepared transaction belongs to the server, not to the session that
        // created it, so these can run on any connection — or after a reboot.
        if err := exec(my, "XA COMMIT "+xid); err != nil {
            return err
        }
        fmt.Println("   phase 2: XA COMMIT            -> mysql done")
        if err := exec(pg, "COMMIT PREPARED '"+gtrid+"'"); err != nil {
            return err
        }
        fmt.Println("   phase 2: COMMIT PREPARED      -> postgres done")
        return decide(gtrid, "forget")
    }
    
    // recoverInDoubt asks each database what it has prepared but never resolved,
    // then looks up the verdict. The rule is asymmetric, and that asymmetry is the
    // whole protocol:
    //
    //  "commit" in the log -> commit the branch, retrying until it succeeds
    //  nothing in the log  -> roll it back ("presumed abort")
    //
    // It is safe because the decision is always logged before phase 2, so a
    // prepared branch with no recorded decision cannot have committed anywhere.
    func recoverInDoubt(my, pg *sql.DB) {
        decisions := decisions()
    
        // MySQL: XA RECOVER returns formatID, gtrid_length, bqual_length, data.
        rows, err := my.Query("XA RECOVER")
        if err != nil {
            log.Fatal(err)
        }
        var myPending []string
        for rows.Next() {
            var format, gtridLen, bqualLen int
            var data string
            if err := rows.Scan(&format, &gtridLen, &bqualLen, &data); err != nil {
                log.Fatal(err)
            }
            myPending = append(myPending, data[:gtridLen])
        }
        rows.Close()
    
        for _, gtrid := range myPending {
            fmt.Printf("   mysql is in doubt about %q; decision log says %q\n", gtrid, decisions[gtrid])
            xid := fmt.Sprintf("'%s','my',1", gtrid)
            if decisions[gtrid] == "commit" {
                must(exec(my, "XA COMMIT "+xid))
                fmt.Println("     -> XA COMMIT")
            } else {
                must(exec(my, "XA ROLLBACK "+xid))
                fmt.Println("     -> XA ROLLBACK (presumed abort)")
            }
        }
    
        // Postgres: the same question, asked of pg_prepared_xacts.
        rows, err = pg.Query("SELECT gid FROM pg_prepared_xacts")
        if err != nil {
            log.Fatal(err)
        }
        var pgPending []string
        for rows.Next() {
            var gid string
            if err := rows.Scan(&gid); err != nil {
                log.Fatal(err)
            }
            pgPending = append(pgPending, gid)
        }
        rows.Close()
    
        for _, gtrid := range pgPending {
            fmt.Printf("   postgres is in doubt about %q; decision log says %q\n", gtrid, decisions[gtrid])
            if decisions[gtrid] == "commit" {
                must(exec(pg, "COMMIT PREPARED '"+gtrid+"'"))
                fmt.Println("     -> COMMIT PREPARED")
            } else {
                must(exec(pg, "ROLLBACK PREPARED '"+gtrid+"'"))
                fmt.Println("     -> ROLLBACK PREPARED (presumed abort)")
            }
        }
    }
    
    // rollback abandons a transaction on both databases, at any stage.
    func rollback(my, pg *sql.DB, gtrid string) {
        _ = exec(my, fmt.Sprintf("XA ROLLBACK '%s','my',1", gtrid))
        _ = exec(pg, "ROLLBACK PREPARED '"+gtrid+"'")
        _ = decide(gtrid, "abort")
    }
    
    // ---------------------------------------------------------------------------
    // the coordinator's decision log
    // ---------------------------------------------------------------------------
    
    // decide appends a verdict and fsyncs it. The fsync is the entire point: an
    // unflushed decision is not a decision.
    func decide(gtrid, verdict string) error {
        f, err := os.OpenFile(decisionLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
        if err != nil {
            return err
        }
        defer f.Close()
        if _, err := fmt.Fprintf(f, "%s %s\n", gtrid, verdict); err != nil {
            return err
        }
        return f.Sync()
    }
    
    // decisions replays the log. A later "forget" clears a resolved transaction.
    func decisions() map[string]string {
        out := map[string]string{}
        f, err := os.Open(decisionLog)
        if err != nil {
            return out
        }
        defer f.Close()
        sc := bufio.NewScanner(f)
        for sc.Scan() {
            parts := strings.Fields(sc.Text())
            if len(parts) != 2 {
                continue
            }
            if parts[1] == "forget" {
                delete(out, parts[0])
                continue
            }
            out[parts[0]] = parts[1]
        }
        return out
    }
    
    // ---------------------------------------------------------------------------
    // plumbing
    // ---------------------------------------------------------------------------
    
    type execer interface {
        ExecContext(context.Context, string, ...any) (sql.Result, error)
    }
    
    func exec(db execer, q string, args ...any) error {
        _, err := db.ExecContext(ctx, q, args...)
        return err
    }
    
    func must(err error) {
        if err != nil {
            log.Fatal(err)
        }
    }
    
    func connect() (*sql.DB, *sql.DB) {
        my, err := sql.Open("mysql", mysqlDSN)
        must(err)
        if err := my.Ping(); err != nil {
            log.Fatalf("mysql not reachable: %v\n(see the docker command at the top of this file)", err)
        }
        pg, err := sql.Open("postgres", pgDSN)
        must(err)
        if err := pg.Ping(); err != nil {
            log.Fatalf("postgres not reachable: %v", err)
        }
        return my, pg
    }
    
    // reset gives every run a clean slate, including any prepared transactions and
    // decisions left behind by a previous run.
    func reset(my, pg *sql.DB) {
        os.Remove(decisionLog)
        recoverInDoubtQuietly(my, pg)
        must(exec(my, `CREATE TABLE IF NOT EXISTS accounts (
            id VARCHAR(32) PRIMARY KEY, balance INT NOT NULL) ENGINE=InnoDB`))
        must(exec(pg, `CREATE TABLE IF NOT EXISTS accounts (
            id TEXT PRIMARY KEY, balance INT NOT NULL)`))
        must(exec(my, "DELETE FROM accounts"))
        must(exec(pg, "DELETE FROM accounts"))
        must(exec(my, "INSERT INTO accounts VALUES ('alice', 500)"))
        must(exec(pg, "INSERT INTO accounts VALUES ('bob', 100)"))
    }
    
    func recoverInDoubtQuietly(my, pg *sql.DB) {
        rows, err := my.Query("XA RECOVER")
        if err == nil {
            var gtrids []string
            for rows.Next() {
                var format, gl, bl int
                var data string
                if rows.Scan(&format, &gl, &bl, &data) == nil {
                    gtrids = append(gtrids, data[:gl])
                }
            }
            rows.Close()
            for _, g := range gtrids {
                _ = exec(my, fmt.Sprintf("XA ROLLBACK '%s','my',1", g))
            }
        }
        rows, err = pg.Query("SELECT gid FROM pg_prepared_xacts")
        if err == nil {
            var gids []string
            for rows.Next() {
                var g string
                if rows.Scan(&g) == nil {
                    gids = append(gids, g)
                }
            }
            rows.Close()
            for _, g := range gids {
                _ = exec(pg, "ROLLBACK PREPARED '"+g+"'")
            }
        }
    }
    
    func balances(my, pg *sql.DB) {
        var a, b int
        must(my.QueryRow("SELECT balance FROM accounts WHERE id='alice'").Scan(&a))
        must(pg.QueryRow("SELECT balance FROM accounts WHERE id='bob'").Scan(&b))
        fmt.Printf("   alice (mysql) = %-4d bob (postgres) = %d\n", a, b)
    }
    
    // showBlocking demonstrates 2PC's real cost: a prepared branch keeps its row
    // locks, so unrelated traffic stalls until someone resolves the transaction.
    func showBlocking(my *sql.DB) {
        conn, err := my.Conn(ctx)
        must(err)
        defer conn.Close()
        must(exec(conn, "SET SESSION innodb_lock_wait_timeout = 1"))
        err = exec(conn, "UPDATE accounts SET balance = 999 WHERE id='alice'")
        fmt.Println("     ", err)
    }
    
  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:56

    ↗

    I’m writing this to teach myself how to learn. This post is a test of the “learn by teaching” theory. Please don’t use this as an educational resource — if something here interests you, do your own independent research on it. Links to the advice videos I’m following are at...

    I’m writing this to teach myself how to learn. This post is a test of the “learn by teaching” theory. Please don’t use this as an educational resource — if something here interests you, do your own independent research on it.

    Links to the advice videos I’m following are at the end of this post.

    Trust your gut. Follow the advice that sounds the most fun and engaging to you.

    Develop your own advice, made specifically for you.
    Watch at least one tutorial on the topic you want to learn, and write down what you think are the most important parts. How many tutorials you need varies from person to person, so experiment to see what works for you.

    Study for 5 seconds to 10 minutes. A popular theory says it’s more important to build the habit of starting than to force long study sessions you dread. Increase your time on the topic gradually.
    Do things that inspire you and connect to what you’re learning — hobbies related to the subject count.

    Identify as someone who does the thing you want to learn. Replace “I’m bad at game development” with “I am someone who develops games.” Use phrasing that feels natural. If “I am the greatest video game developer on the planet” feels off, try “I am someone who develops video games” instead — it’s more believable and easier to internalize. Then reinforce the identity with action. This pairs well with the 5-second-to-10-minute study method above.

    Break information into chunks. Organize what you’re learning into smaller, connected groups instead of isolated facts — this makes it easier to comprehend. For example: learning a language, study 5 sentences instead of 50 isolated words. Learning to code, build one program that uses 10 functions together instead of memorizing them separately. Chunking helps you build patterns, and patterns help you learn.

    Use the Rabbit Hole Technique. Don’t just study the skill itself — study subjects related to it too. Start with the fundamentals, then branch into related areas that all loop back to the main thing you’re trying to learn.

    Learn by teaching — also known as the entire reason I’m writing this. Study something, then explain it out loud to someone willing to listen, or to an empty room. Notice where your explanation gets confusing or where you stumble — that tells you exactly what to focus on next.
    Make something before you’re ready. Don’t chase perfection. Embrace the messy, awkward parts of learning. Finish what you make, and take pride in it.

    Thank you for reading!

    Videos referenced:

    7 Days to Master Any Skill with These Brain Tricks — YouTube channel mindshiftdaily022 https://www.youtube.com/watch?v=ySr-IQQIMp4&list=PLfHLOLxeSxwY9m7vRHB_du58yuHn31kA-&index=2
    _Create Something BEFORE You’re Ready _https://www.youtube.com/watch?v=5UUredw3NkM&list=PLfHLOLxeSxwY9m7vRHB_du58yuHn31kA-&index=2

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:50

    ↗

    This header block is where most people's understanding of email authentication falls apart: Authentication-Results: mx.google.com; spf=pass smtp.mailfrom=bounce@esp-vendor.net; dkim=pass header.d=esp-vendor.net; dmarc=fail (p=REJECT) header.from=yourcompany.com Two passes and...

    This header block is where most people's understanding of email authentication falls apart:

    Authentication-Results: mx.google.com;
      spf=pass smtp.mailfrom=bounce@esp-vendor.net;
      dkim=pass header.d=esp-vendor.net;
      dmarc=fail (p=REJECT) header.from=yourcompany.com
    

    Two passes and a fail. No malformed record anywhere. Nothing to fix in DNS. And the mail is being rejected.

    Every part of that is correct behaviour.

    The question DMARC actually asks

    DMARC does not ask "did SPF pass?"

    It asks: did SPF or DKIM pass for the same domain that appears in the From: header?

    There are three domains in play in any message, and only one of them is visible to a human:

    Identifier Lives in Who sees it Checked by
    From: The message headers Your recipient Nothing, on its own
    Envelope sender (MAIL FROM) The SMTP conversation Nobody SPF
    DKIM d= The signature header Nobody DKIM

    SPF authenticates the envelope sender. DKIM authenticates the signing domain. Neither of them is the From: header. DMARC exists entirely to insist that at least one of them matches it.

    In the header above, SPF authenticated esp-vendor.net and DKIM signed as esp-vendor.net. The recipient sees yourcompany.com. Nothing authenticated yourcompany.com, so DMARC fails.

    It has to work this way. If DMARC accepted "SPF passed" without the matching step, anyone with a mailbox at a provider with a valid SPF record could send as you and pass.

    How you get here without doing anything wrong

    You sign up for a marketing platform, an invoicing tool, or a helpdesk. You set the From address to hello@yourcompany.com because that is what your customers should see.

    The platform sends from its own infrastructure with its own envelope domain — it has to, because that is where bounces go. SPF passes, for them. Your From: says you.

    Those do not align. If nothing is DKIM-signed with your domain, every message from that platform fails DMARC.

    This is the single most common cause of "we set up SPF and it still doesn't work".

    The fix that feels right and does nothing

    Adding include:esp-vendor.net to your SPF record.

    It is the intuitive move. It changes nothing.

    That include authorises their servers to send for your envelope domain. Their mail still uses their envelope domain, so the alignment comparison is untouched. You have spent one of your ten SPF DNS lookups for no benefit at all.

    If your reaction to a dmarc=fail is to edit your SPF record, stop and read the Authentication-Results header first.

    Relaxed vs strict, and the trap in the middle

    Alignment has two modes, set per-mechanism with aspf= and adkim= in your DMARC record:

    • Relaxed (r, the default) — organisational domains must match. mail.example.com aligns with example.com.
    • Strict (s) — exact match only. mail.example.com does not align with example.com.

    Relaxed is right for almost everyone. Strict is worth it only if you are confident every sender uses the exact apex domain, and it breaks the day someone starts sending from a subdomain.

    Here is the part that matters if you are implementing this yourself: "organisational domain" is not "the last two labels".

    attacker.co.uk and victim.co.uk share their last two labels and have no relationship whatsoever. Getting this right means consulting the Public Suffix List, not counting dots. A checker that counts dots will tell you two unrelated domains are aligned, which is the worst possible direction to be wrong in.

    Reading it from your own mail

    Send a message to a mailbox at a different provider. Open the delivered copy and view the source — ⋮ → Show original in Gmail, View message details in Outlook.

    Then compare exactly three lines:

    From: Your Company <hello@yourcompany.com>     <- the domain that must be matched
    spf=pass smtp.mailfrom=bounce@esp.net          <- authenticated esp.net
    dkim=pass header.d=esp.net                     <- signed by esp.net
    

    If neither the smtp.mailfrom domain nor the DKIM d= matches your From domain, DMARC fails regardless of how correct your SPF record is.

    Use a message you received, not one from your Sent folder. Sent copies have no Authentication-Results header at all — that verdict is written by the receiving server. Nearly everyone trips on this the first time and concludes their headers are broken.

    Fixing it properly

    Get DKIM signing with your own domain. Every serious sending platform supports it. It is usually called "authenticate your domain", "domain authentication" or "custom DKIM", and it hands you a selector to publish in DNS. Once the platform signs with d=yourcompany.com, DKIM aligns and DMARC passes no matter what the envelope sender says.

    There is a second reason to prefer DKIM, and it is the one that bites later: DKIM survives forwarding and SPF does not. The moment a message is forwarded, the forwarding server is not in your SPF record. Every mailing list and every user-configured forward becomes a DMARC failure if SPF alignment is all you have.

    Optionally, also set a custom return-path. Some platforms let you point the envelope sender at a subdomain of yours — "custom return-path" or "custom bounce domain". You publish a CNAME, the envelope domain becomes bounce.yourcompany.com, and that aligns under relaxed. This fixes only the SPF half and still dies on forwarding, so do it as well as DKIM, not instead.

    Before you enforce

    This is the actual reason the p=none → quarantine → reject order exists.

    Run at p=none with a reporting address and read the aggregate reports. They tell you which sources pass SPF and DKIM with alignment — a different and much smaller set than the sources that merely pass SPF. Move to p=quarantine only when that list matches the systems you expect to be there.

    Skip it and you will find your misaligned senders the hard way: by having their mail rejected.

    I maintain notspoofed — a free, no-signup checker for SPF, DKIM and DMARC. It has an in-browser header analyzer that does the three-way alignment comparison above and shows relaxed vs strict as a table; headers are parsed locally and never uploaded. It uses the Public Suffix List for organisational domains, because counting labels is wrong. Source is on GitHub under MIT.

    If you have hit an alignment failure with a cause that is not on this list, I would like to hear it — those are the interesting ones.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:47

    ↗

    A friend dared me to build Pass the Pigs during the apéro. Two LLMs reviewed my plan. Or: a non-technical friend wanted to see what AI could do. He watched it plan — and watched two AIs shred the plan. The challenge It happened at an apéritif, in front of a non-technical...

    A friend dared me to build Pass the Pigs during the apéro. Two LLMs reviewed my plan.

    Or: a non-technical friend wanted to see what AI could do. He watched it plan — and watched two AIs shred the plan.

    The challenge

    It happened at an apéritif, in front of a non-technical friend who wanted to see what AI could do. We were playing le jeu de cochons (Pass the Pigs) — the apéro game where you throw two tiny pigs, they land in one of seven positions, and you push your luck against the scoreboard. He picked it up, turned to me, and dared: "go on — make an app out of it. Right now. Show me what AI can do."

    The desktop wakes up. Challenge accepted. It looks trivial. It is not.

    My concept was "one-line to code": give an LLM a short prompt, let it plan, let it build, ship it. A modern party trick.

    Honesty clause #1: it wasn't literally one line. It took a few short exchanges — still almost no hand-holding from me, but I won't pretend it was a single prompt. The pigs are simple, right?

    The prompts (zero engineering)

    The prompts were apéro-grade: casual, naive, written in French over drinks — no templates, no role-play framing, no few-shot examples, no chain-of-thought coaxing. Translated from the originals:

    1. The spec ask: "I'd like you to analyze this game — https://fr.wikipedia.org/wiki/Jeu_de_cochons — and think about how to turn it into a game. Don't code anything: just analyze the rules and define what would need to be done, in product-definition mode." — the entire game description was one Wikipedia link; the model read the rules itself.
    2. The plan ask: "I like approach B, detail it, prepare an initial dev plan to get a playable game, bug-free, tested and robust, that reproduces the base game's experience as faithfully as possible… give me that plan so I can have it reviewed. If the review is bad, I'll have a bad image of deepseek pro." — my entire incentive system, in one sentence.
    3. The review ask: "Do an adversarial review of this plan, with Codex as the lead and Claude as secondary, with 2 loops."

    That's the whole prompt-engineering budget: zero. No magic incantations — the quality came from the process: two independent models, a gate at every stage, disagreement as a signal. Anyone can type these three prompts.

    The catch

    Pass the Pigs has a famously ambiguous rulebook, and the game is really about probabilities — when to bank, when to roll again. The scoring semantics (Bon Jambon, Cochon à Cheval, Pig Out, the "Somme" rules) are exactly where a happy few-lines plan goes to die.

    So instead of trusting the plan those few prompts produced, I ran it through an adversarial review pipeline, orchestrated by Hermes Agent — the framework where these skills live. My Hermes runs on DeepSeek Flash for orchestration: the cheap part of the loop. The real work is done by the two specialist models — the plan was written by Codex as the architect, then reviewed independently by Claude Fable 5 as the inspector; a synthesis pass merged, categorized, and ranked their findings.

    The cost story is deliberately mundane: the whole loop ran on basic consumer plans — a €20/month Claude subscription and a €20/month Codex subscription, each model's turn scheduled inside its quota window. No API credits, no enterprise accounts. The pipeline is designed to fit inside the limits a hobbyist already pays for.

    Neither model is special, either: the pipeline accepts any LLM CLI on either side — Claude, Codex, Gemini, GLM, or fully local models served by llama.cpp (my Hermes already routes to local models). The only real requirement is two different model families: two copies of the same model share the same blind spots, and the whole point is a second opinion that isn't a copy of your first one.

    The numbers

    Reviewer Findings
    Codex alone (architect) 7
    Claude Fable 5 alone (inspector) 11
    Both, merged 16 unique findings
    • 3 findings were found independently by both models — the highest-confidence class. All three are major bugs in the probability model.
    • 6 more reached consensus after discussion; 4 were partial (agreement on the issue, disagreement on severity); 3 were genuinely disputed.

    The duo found +5 findings over the best single model (16 vs 11), and — more important than the count — the two models independently converged on exactly the three most dangerous bugs.

    The timeline (one apéro)

    Everything started at the apéritif and finished the next morning — the session log tells the story: dare at 21:12, plan and adversarial review that night, then a quota wall (one of the two subscriptions ran out mid-loop), and I went to bed while the pipeline kept running. A playable build was committed at 01:38; the real finish came in the morning, when the GitHub Pages URL needed one more fix at 10:32 (the classic Vite base-path 404) — so the first people to try the link that morning saw nothing. Five commits across an evening, a night, and a morning. The commit for the playable version reads, in part:

    Moteur de règles complet (push-your-luck, probabilités calibrées) … 73 tests (unitaires + calibration 1M tirages + Playwright E2E) — Build Vite ~16KB gzip. Développé via adversarial dev loop (Codex DEV + Claude Fable 5 REVIEW)

    An apéritif game that fits in 16KB gzipped, tested 73 ways, calibrated against a million simulated rolls — and the project's own commit history credits the adversarial loop. The full review trail (647-line plan, both models' findings, synthesis) is committed right next to the code.

    The full chain (spec → plan → code)

    The plan review was one gate in a chain. The session opened with a spec pass — the product-definition analysis of the game's rules (scoring table, the three possible approaches, the four traps of going digital) — and everything downstream ran on it. After the plan was corrected, the code itself went through the same two-model loop, phase by phase (that's what adversarial-code-loop is for). The last phase's review caught two real major bugs the test suite had missed — in fact, the tests were asserting the buggy behavior: player names were HTML-escaped at the wrong layer (a name like A&B <Bob> was stored and displayed as A&amp;B &lt;Bob&gt;), and the localStorage high-score loader trusted unvalidated names. Both fixed, the tests corrected, all 73 green.

    Spec → plan → code: every stage is a gate where two independent models disagree in your place. That's the chaining the skills encode — adversarial-spec drafts the stage, adversarial-plan gates it, adversarial-code-loop keeps the code honest.

    The three findings both models caught

    1. The flank probabilities don't add up. The plan assigned 12.25% to each of two flank outcomes — but the two outcomes together total 12.25%, not 24.5%. A player's whole luck-push strategy was built on a doubled probability.
    2. The override rule silently kills a result. Sequential checks for Bon Jambon / Cochon à Cheval overrides change the effective distribution — Cochon à Cheval ends up at 0.78% unless modeled as a disjoint probability partition.
    3. The Jambon rule contradicts itself. One section of the plan treats Bon Jambon as a turn-ending catastrophe; another treats it as a negative score clamped with Math.max(0, score). The game's most exciting rule was unimplementable as specified.

    A single model can flag these. A single model cannot confirm them. Two models converging on the same bug is the closest thing to a second opinion you can get from an LLM — and it cost nothing but one extra review pass.

    Honesty clause #2: the review made the probabilities internally consistent — it did not make them true. The corrected values still come from the models' reasoning, not from rolling actual pigs. The tests do run a million-roll calibration, but those are simulated rolls: they stress-test the code, not the physics of real pigs. I am genuinely not sure the produced probabilities are right; they need calibration against real throwing data before I'd trust the strategy advice the game displays.

    The part nobody talks about: the disputes

    The adversarial setup does something a solo review can't: it produces disagreement. Three findings ended up disputed — e.g., "can nextPlayer() loop forever if everyone is eliminated?" — where the reviewer called it a real bug and the architect showed it's unreachable under valid transitions.

    That dispute signal is precious: it tells you exactly which parts of your plan are genuinely ambiguous and need a human decision, instead of silently shipping your assumptions.

    Honesty clause #3

    More findings ≠ better, if they're noise. The 16 findings were categorized precisely because of that: 3 cross-validated, 6 consensus, 4 partial, 3 disputed. The 13 that survived discussion were real, and the plan (v1.1) was corrected around them — probability model reworked, scoring made explicit, state transitions centralized — before a single line of game code was written.

    The apéritif game went from dare to playable build by the end of the night — built from a plan that two models shredded, argued about, and rebuilt. That's the "few prompts to code" I actually want: a couple of short exchanges in, a reviewed, correct plan out.

    Tools: adversarial-code-loop (build → review → fix pipeline, model-agnostic, one-line installer), adversarial-plan, adversarial-spec — all orchestrated by Hermes Agent. The game — play it, read the code — plan v1.1 after review, full review artifacts in review/.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:46

    ↗

    You've got one CRM integration in production and it's been fine for months. Now there's a second one to wire up. Same contract, same schema, same node shape. Should be a couple of days. You pull the first payload from CRM B and put it next to CRM A out of habit. CRM A, a...

    You've got one CRM integration in production and it's been fine for months. Now there's a second one to wire up. Same contract, same schema, same node shape. Should be a couple of days.

    You pull the first payload from CRM B and put it next to CRM A out of habit.

    CRM A, a monetary field:

    { "key": "estimated_cost", "value": 1275.43, "spec": { "type": "number", "format": "decimal" } }
    

    CRM B, same kind of field:

    { "key": "amount", "value": "6624", "spec": { "type": "number" } }
    

    Huh.

    One's a JSON number. One's a string. One tags the format, one doesn't. Both say "type": "number". And every validator between those adapters and your UI is perfectly happy with both.

    So what's the issue

    This is the part that takes a minute to sit with. You go looking for who got it wrong, and nobody did.

    Go read the contract. It says there's a field called value. That's it. It never said what could go in value. So the person building CRM A looked at a number and sent a number. The person building CRM B looked at the same thing and sent a string, probably because that CRM's own API hands back strings. Both reasonable. Both shipped. Both correct against the spec as written.

    The contract wasn't violated. It just didn't have an opinion, and two people filled the silence differently.

    That's the whole category of problem I want to talk about, because it doesn't show up in code review and it doesn't show up in CI. It shows up eight months later when someone asks why one CRM's totals are off by a cent.

    The good idea underneath

    Before piling on, credit where it's due, because the foundation here is right.

    Every source produces the same node. Consumers never branch on where the data came from.

    type FieldNode = {
      key: string
      label: string
      value: ???
      spec: ???
      treePath: string
      children: FieldNode[]
    }
    

    One shape, one tree, one set of consumers. Adding a CRM is adapter work and nothing above the boundary moves. This genuinely scales, and if you're designing this today, start here.

    But notice what it does to your risk. Two fields carry all the meaning — value and spec. The rest is plumbing. Leave those two vague and the uniformity is a paint job.

    Rule 1: name the field, then actually say what goes in it

    Here's the type that caused everything above:

    value: string | number | null
    

    Looks like a decision. Isn't one. It's a union that permits both answers, which means you'll get both answers the moment two adapters get written in parallel by people who never talk.

    So say the thing:

    • value is a decimal string, or null. Never a JSON number.
    • Containers (array, object) carry null.
    • Numeric nodes carry a format.
    • format: 'money' also carries currency, ISO 4217.

    That last one isn't bureaucracy. format: 'decimal' can't tell a cost from a tax rate. They're both decimals. Only the source system knows which one is money, and that fact turns out to constrain your whole architecture. Hold that thought for Rule 3.

    And if you're about to argue you don't need strings because you're not doing currency math: value: number still eats integers past 2^53, which is where record IDs live. It mangles anything past 15 significant digits. It'll hand you 1e+21 when you least want it. A decimal string has none of those problems and costs you nothing.

    Rule 2: shut the door on undeclared fields

    Here's the sequel to the story, and it's more common than the first part.

    CRM A's adapter stamps format: "decimal" on every numeric field. Handy. Consumers start reading it. Someone builds a formatting rule on top of it.

    Except format was never declared in the schema. It's just riding along in the payload — invisible to validation, missing from your generated client types, guaranteed by nobody. And CRM B doesn't send it at all, so the formatting rule works for half your customers.

    Whatever flavor you get, it's one bug: data present, schema silent.

    One flag closes the whole category. additionalProperties: false in OpenAPI, .strict() in zod, whatever yours is called. Undeclared fields stop being possible. Everything else you do about this is whack-a-mole.

    Fair warning: turning it on will break things that currently work, because at least one of those undeclared fields is load-bearing somewhere. That's not a reason to skip it. The dependency was already there — you just couldn't see it.

    Rule 3: the normalization layer can't save you

    Okay, so the obvious fix. Build a normalization layer. One place that takes whatever each adapter produces and forces it into canonical form. Money becomes a decimal string, numerics get a format tag, everyone goes home.

    You do need that layer. It also can't do the job on its own, and this is the bit I'd most want to hand to past-me.

    Picture a money field whose true value is 450.20.

    • Adapter one does JSON.parse on the CRM response. The value lands in a float64, comes back out as "450.2". Normalization checks it: it's a string, it matches a decimal literal, format's present. Passes.
    • Adapter two reads the response as text and never touches a float. Emits "450.20". Normalization checks it. Passes, identically.

    Your layer cannot tell these apart. Both are well-formed strings. One of them quietly threw information away, and no amount of schema work will catch it, because a validator can only confirm the shape of what you handed it. It can't reconstruct what got dropped before it ever saw the data.

    So this isn't one boundary. It's two, doing different jobs:

    Boundary Job Why it can't fold into the other
    Read-time — inside each adapter Decode source numerics as text. json.Number in Go, a reviver in JS, never bare JSON.parse. Tag format and currency from provider field metadata — HubSpot's Properties API, Salesforce describe(). It's the only layer that touches the raw HTTP response. Precision lives or dies here and nowhere else. It's also the only place that knows Amount is money and source_user_id isn't.
    Enforcement — the shared service Strict schema. Validate your own output. Fail closed. Derive format when an adapter didn't send one. Own the spec. It's the only layer that sees every adapter side by side. One stamping format while another skips it is completely invisible from inside either adapter.

    Read-time on its own is just a promise everyone makes and nobody keeps, because "be careful with floats" isn't a mechanism. Enforcement on its own validates already-damaged data and hands you a green checkmark.

    If you keep one line from this post: the guarantee gets created upstream of where it gets checked. Any design that stuffs all the normalization into one shared service has a hole in it that more schema won't fill.

    This is also, incidentally, why "which service owns this fix?" can sit open in a doc for weeks. Every answer is partly right and none of them is enough. When that happens, the question is usually the problem. This one quietly assumed there was one owner.

    Rule 4: the mapper that quietly eats your fields

    Different failure, same silence.

    Early on, your wire type and your domain type are the same type. The only mapper that compiles is a spread:

    fields.map(f => ({ ...f, label: f.display_label }))
    

    Nothing can go missing. Not because anyone's being careful — because there's no other way to write it.

    Then the domain type picks up something the wire doesn't have. A render hint, say, or a UI kind. Perfectly reasonable in isolation. But now the two types have forked, so you need a translation function, and a translation function is a list of fields:

    const map = (f: ApiField): FieldNode => ({
      key: f.key,
      label: f.display_label,
      type: f.children?.length ? 'group' : 'leaf',
      value: String(f.value),
      treePath: f.path,
      children: f.children?.map(map) ?? [],
    })
    

    Whatever isn't on that list is gone. Including every field anyone adds to the contract next quarter. And you won't catch it in review, because that mapper is correct for the types it was handed.

    Derive the domain type from the wire type instead, and forgetting a field becomes a compile error:

    type FieldNode = Omit<WireField, 'display_label'> & { label: string }
    

    Worth saying the other half too: the spread version preserved fields by being transparent, not by being right. It happily carried undeclared junk along with everything else, which is Rule 2's problem wearing a different hat. You want both halves. Parse strictly, then pass through.

    Rule 5: don't add a second way to say what something is

    Tempting move: put a declared "kind" field next to your structural type. spec.type plus tree position already tells you everything behavioral — expandable, container, nested — but a kind field feels more explicit, so it goes in.

    Two things go wrong. It drifts from the structure it's supposed to describe, which is the boring failure. And it tends to land on a property name the wire already uses for something else. Look at type in that mapper above: it's the UI kind in the domain type and the source record type on the wire. That collision is exactly what forced the field-enumerating translation in Rule 4.

    Cheap thing to try on your own code right now: for every enum on a core domain type, grep for reads of each member. The ones that get written and never read are telling you the type is carrying a distinction nothing actually needs.

    The schema - what it should enforce

    value never gets coerced. z.coerce.string() would quietly accept a raw number and enforce absolutely nothing. You want a bad value to fail, not get tidied up behind your back.

    The decimal-literal regex is your precision tripwire. Anything that went through a float and back emits exponent notation at large magnitudes, and 1e+21 doesn't match. It only catches the loud cases. The quiet ones are Rule 3's job.

    spec is required. Optional-chaining a discriminator is itself the bug. spec?.type makes "field missing" and "value unknown" look identical, and both slide into the same default branch.

    currency sits next to format, not inside it. Rule 1's point: only the source system knows which decimals are money, so that tag has to be born at read time.

    Where to actually enforce this

    Writing the schema is the easy part. Where you run it decides whether it does anything.

    The producer validates its own output. Your shared service checks its response before sending and fails closed. Skip this and consumer-side validation just turns a silent bug into a loud one, later, in somebody else's service, at 2am.

    The consumer parses at the boundary, per node. A recursive strict schema blows up the entire tree over one malformed grandchild, which means a whole panel disappears because of one bad leaf. Walk the tree, validate node by node, keep the bad ones and mark them unavailable, and report them. Silently dropping them is its own kind of lie — the user just sees a shorter list and no explanation.

    And know what none of this covers. A perfectly-shaped value that lost precision upstream passes every check on that list. The read-time boundary needs a shared decode helper and human review, not a test. Fixture-based tests have a cousin of the same blind spot: they'll stay green while the same code fails on the live route, because your fixtures carry fields the real projection drops. Make at least one test exercise the projection itself.

    The short version

    • For every field in your contract: is the content specified, or just the name? A union that permits two encodings is an unspecified field wearing a type annotation.
    • Is additionalProperties: false on? If not, you have undeclared fields in production right now, and one of them matters.
    • Where does precision get created versus checked? Different layers means your validator can't verify the thing it looks like it's verifying.
    • Does any mapper enumerate fields? It's dropping something today and it'll drop everything you add tomorrow.
    • Does the producer validate its own output, or only the consumer?
    • Which members of your core enums actually get read?

    A contract isn't the fields you named. It's the behaviors you made impossible.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:45

    ↗

    A coding agent with read access to your check scripts will predict them instead of running them. Prediction costs more and gets the answer wrong. A session I was directing had a small decision in front of it: whether to switch on an optional guard that nudges delegated...

    A coding agent with read access to your check scripts will predict them instead of running them. Prediction costs more and gets the answer wrong.

    A session I was directing had a small decision in front of it: whether to switch on an optional guard that nudges delegated sessions toward a particular decision shape. The guard ships inert. Its owning brief says so in one sentence: switching it on is the consumer's own registration, and absent that the thing is inert prose, which is the stated default for this repository.

    The session never read that sentence. Its first attempt to pull the section came back empty, and rather than retry the query with a better heading, it opened the guard's implementation and read all twenty-eight lines, satisfying itself that the thing exits cleanly on the common path and is harmless to enable. Then it wired the guard into a governed configuration file. On the way it wrote down, in its own reasoning, a prediction about the two checks that police that surface: those checks govern the tracked settings file, this one is untracked, so nothing will fire.

    I caught the edit and asked one question:

    The project aims to give brief instructions, let gates cheaply validate them while providing additional context upon failures. Analyzing gate code to prevent them from firing is the expensive anti-pattern. Was your settings.local.json edit not an anti-pattern?

    Then the checks were actually run. Both were green — with the edit in place and with it removed. No check in the battery could have caught the mistake, because an advisory guard is harmless by construction. The only authority on the question was the brief, and the brief was the one cheap source the session skipped in favor of archaeology on the source code.

    That is the whole failure in one episode. The session paid tokens to read an implementation, produced an answer, and the answer was wrong in a way the mechanism itself would never have flagged. Its own post-mortem line is the sharpest statement of the rule I have seen from any session:

    The tell to catch myself: if I'm reasoning about whether a gate will fire, I've already left the rails.

    An enforcement script is not inert infrastructure

    The reflexive model of a check is that it sits there and either fires or doesn't. It is plumbing. Nobody thinks about what it costs to have plumbing.

    But a check script in a repository an agent can read is not plumbing to that agent. It is text, and text is the thing the agent is best at consuming. Faced with "take this action and find out what the check says," a model has a cheaper-looking option always in reach: read the check and work out what it would say. That option looks cheaper because reading is what the model does all day. It is not cheaper. It burns tokens on reasoning the check performs deterministically for nothing, and it produces a worse answer, because the model is inferring behavior from an implementation instead of observing it.

    I have watched this happen enough times to stop treating it as a slip.

    It recurs, and prose does not stop it

    Six days before the config edit, a different session had to decide whether to dispatch a delegated stage. A budget guard runs on every delegation call as a pre-tool hook, the same interception point I have written about before, and it rules authoritatively at the moment of the attempt. The session built a waiting loop to pre-compute the guard's verdict instead of attempting the dispatch and letting the guard answer — and it read an advisory verdict as blocking, when the owning document says that verdict never blocks. My response:

    on your point above about being over-cautious - this is exact source of unwarranted token spending, you should try the action and see if you get blocked, not try to anticipate and burn tokens, I thought this is clearly stated in instructions

    The session agreed, and its concession names the structural reason attempting always wins:

    a fail-closed guard is exactly what makes anticipation unnecessary … attempting is strictly cheaper than pre-computing, and the only thing my polling loop could buy was information the hook would hand me anyway.

    Two costs, and the second is the one people miss. The first is the token spend on a loop computing something free. The second is that the computed answer was wrong: nothing in the documentation said that verdict blocks, and the session invented the caution. Prediction is not a slower path to the same result. It is a different, less reliable result, arrived at expensively.

    There is a family resemblance here to something I have written about before: a report from an agent that its own work passed is a claim, and wants verifying rather than believing. This is the same distrust aimed at a different object. There the suspect artifact is the agent's account of what it did. Here it is the agent's model of what a mechanism would have said.

    What made me stop looking for a prompt-shaped fix was the next exchange. The session offered to note and correct. I pointed out that it was not the first time, and it conceded the point better than I made it:

    You're right that "noted and corrected" is worthless — it dies with this session.

    An instruction that must be re-applied every session, against a readable and always-available alternative that looks cheaper, is a suggestion competing with a temptation. It does not function as a rail. I have argued this at length before, and I am not going to re-argue it here beyond the obvious corollary: if prose does not bind for the domain vocabulary, it does not bind for this either.

    The turn: opacity as a design property

    Which leaves the uncomfortable option. If the source is what invites the prediction, remove the source.

    This came up on its own, in the middle of an entirely ordinary assessment of whether to move a shell-based check battery onto a compiled binary. The reasons on the table were the boring ones: portability past Linux, a toolchain of independent utilities with independent release cycles, a real compiler instead of a linter. Then this, listed last:

    As a cherry on top, we have been trying to discourage coding agents from reading check scripts trying to predict execution results instead of just doing the lifecycle actions that those gates check with a failing gate providing the correction instructions. Yet, the presence of script source code keeps biting us on a regular basis causing unnecessary token waste.

    A week of measurement later, the ranking had inverted. Every other benefit on that list is ordinary engineering. This one changes what kind of thing the check is:

    An agent can't read a binary's source. In a customer project the gate ships compiled, so an agent cannot inspect the implementation and reason about what it would say — it has to run it. That makes oracle-first structurally enforced instead of a doctrine line an agent can quietly route around.

    Stated generally: a check's designed output is its verdict and its correction text. The source is the implementation, not the interface. A consumer that reads the implementation to predict the interface is consuming the wrong surface, and making that surface unavailable is a legitimate design choice rather than a confession that the check is weak.

    This is the part that scrapes against engineering instinct, and it should. Readable tooling is good. Auditable tooling is better. Neither is in dispute here. The claim is narrower, and it is about one specific consumer: a probabilistic text generator that will read whatever sits in front of it. For that reader the readable implementation is a channel competing with the designed one, winning on apparent cost and losing on accuracy.

    The boundary: hide the implementation, never the brief

    The obvious over-correction is to hide everything, and I proposed exactly that. My suggestion, in the same session, was to block access to the specification documents and the source. The session refused half of it, and the refusal is the most useful thing in these transcripts:

    Blocking SPEC and source access would break the thing the whole method rests on — spec-over-precedent, "the owner doc is ground truth." Take away doc access and I don't stop overthinking; I start guessing from precedent, which is the failure you flagged me for earlier this session.

    That is right, and it draws the line the argument actually needs. Three surfaces, three different answers:

    Surface What it is Should the agent read it?
    The brief What the mechanism is for, and whether you should touch it Yes — maximally readable. It is ground truth.
    The implementation How the verdict gets computed No. It invites inference where observation was available.
    The verdict and its correction text The designed feedback channel This is the interface.

    Remove the brief and you have not fixed the agent, you have changed which failure it produces: it stops predicting the check and starts imitating whatever the last comparable change looked like. That trades an expensive-but-visible failure for a cheap-and-invisible one. The same session that wrongly read the guard's source had the distinction exactly right when it wrote its own post-mortem: source tells you what a thing does, the brief tells you what it is for, and only the second answers whether you should touch it.

    What this is not

    Three neighbors sit close enough that the argument has to meet them rather than nod at them.

    Kerckhoffs's principle is the real objection. A cryptosystem should be secure even if everything about it except the key is public, and Shannon's version tells you to assume the enemy will immediately gain full familiarity with your design. Security through obscurity fails, and it fails for good reasons. I am not disputing any of it, because I am answering a different question. Kerckhoffs asks whether a hostile reader can break your system. This asks what a cooperative reader's reading costs you. The agent is not attacking the check. It is taking the most helpful-looking path available, which happens to be the most expensive one, and a check whose correctness depended on nobody reading it would be a bad check no matter which side of this you take. Opacity here is not buying strength against an adversary. It is removing a tempting wrong path from a collaborator. And it is a shallow kind of opacity: nothing in the argument asks for the source to be secret from the world, only for it to stop sitting in the working tree the agent is already reading.

    Goodhart's law is adjacent, and not the same failure. Marilyn Strathern's phrasing — "when a measure becomes a target, it ceases to be a good measure" — describes optimizing the metric instead of the thing it measures. An agent editing config to pre-empt a check would be a Goodhart failure. What I keep seeing is a step earlier and stranger: the agent substituted inference for observation, and got the inference wrong. Nothing was being gamed. The measure was being emulated, badly, at a moment when running it was free.

    Compilers settled this decades ago. Nobody reads a compiler's source to predict whether their build will fail, and nobody calls that opacity a design smell. The diagnostic is the interface. Most compilers are in fact open source, which is exactly the point worth being honest about: what opacity buys is the removal of a tempting channel, not an impossible one.

    One neighbor is on the same side of the argument and got there first. Birgitta Böckeler's account of maintainability sensors already treats a check's output as the mechanism, feedback that lets the agent self-correct, which she calls a good kind of prompt injection. My addition is small and slightly awkward: that designed channel has a competitor, and the competitor is the sensor's own implementation sitting in the same repository.

    Limits

    The cure is filed, not shipped. The compiled-binary port is a deferred roadmap entry in my own queue, marked design-pending, filed at the end of July on my instruction and not started. The diagnosis rests on four sessions across three weeks of my own transcripts. The remedy is a direction I have argued myself into, and you should read it as exactly that.

    The beneficiary is the consumer, not me. My own phrasing when I listed the port's advantages was "for customer projects, not this one which will still have the source code." A project that authors compiled checks still has the source sitting in its own tree, one read away. Opacity in the authoring repository is a friction increase that reinforces the right habit. It is not a guarantee, and I would not describe it as one.

    Opacity to agents is opacity to human adopters. This is the honest cost, and it is large. Today someone can read a shell check before letting it near their commit hook. "Run this opaque binary on every commit" is a materially harder trust ask.

    The way back is the one the compilers already showed. Keep the source public and the build reproducible, publish checksums, let anyone rebuild the artifact and confirm it matches what they installed. Then the opacity is not secrecy at all, only distribution: an adopter who wants to audit the check reads it on the public repository or builds it themselves, while the agent working in their tree has the verdict and nothing else. That is a thinner claim than "opaque binary" makes it sound, and a better trust story. It is also weaker enforcement than it sounds, since a determined agent could go fetch the same public source. Friction, again, rather than a wall.

    What that does not solve is consumer extensibility. A shell battery is trivially extensible because anyone can drop in another script, and a binary closes that door until you decide what replaces it. That question is why the entry is still marked design-pending rather than scheduled.

    Speed is a real benefit, and it is not the reason. The port's original justification led on wall-clock time, and when a scope pass finally measured the battery instead of assuming, that justification collapsed. Starting the check processes was about one percent of the run. The most expensive check in the battery was spending most of its time launching a fresh interpreter once per page, which an ordinary shell-level batch fixed, and the shell fix landed a faster battery than the model of a perfect port had predicted.

    Then the correction went the other way, and I should report that as plainly as the first half. The largest remaining third-party cost turned out to be a linter running over the project's own shell, which a port deletes along with the shell. My queue now carries three more levers that a compiled binary gets nearly free and that shell reaches only with effort: running the battery across cores, caching a check's result against the inputs it already declares, and one shared walk of the tracked tree feeding many readers instead of each check walking it alone. So the port does buy performance. Mis-costed in both directions is how the entry words it now.

    What the measurement changed is the ranking, not the sign. Every performance win in that list is also reachable without a rewrite, more slowly and less completely. The enforcement-model change is the one item nothing cheaper delivers, and it is the only one I am building an argument on here.

    Building domain-dense systems by directing coding agents is where I spend my time, and questions like which surfaces an agent should be able to read are a large part of what makes that work. If that is your problem too, I am reachable on LinkedIn.

    Written by directing an AI agent, the same way the toolkit it describes was built. The editing and the judgment are mine.

    References

    • Birgitta Böckeler, "Maintainability Sensors for Coding Agents," martinfowler.com, 19 May 2026 (accessed 1 Aug 2026).
    • "Kerckhoffs's principle," Wikipedia (accessed 1 Aug 2026).
    • "Goodhart's law," Wikipedia (accessed 1 Aug 2026). Popular phrasing attributed to Marilyn Strathern, 1997.
    • Vasyl Tretiakov, "Rails, Not Rules," vasyltretiakov.dev, 1 Jun 2026.
    • Vasyl Tretiakov, "Block, Steer, Rewrite," vasyltretiakov.dev, 15 Jul 2026.
    • Vasyl Tretiakov, "Verify the Work," vasyltretiakov.dev, 24 Jun 2026.

    Published at vasyltretiakov.dev.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:35

    ↗

    I run PDFHaul, a free browser and mobile PDF toolkit, as a solo founder. A few months ago I kept running into the same friction point: every time I wanted an AI assistant to help with a PDF task, I had to manually download the file, upload it somewhere, run the tool, then...

    I run PDFHaul, a free browser and mobile PDF toolkit, as a solo founder. A few months ago I kept running into the same friction point: every time I wanted an AI assistant to help with a PDF task, I had to manually download the file, upload it somewhere, run the tool, then feed the result back into the conversation. For anything involving more than one file, that loop got old fast.

    So I built an MCP server for PDFHaul. This post covers what it does, how it's built, and a few of the harder decisions along the way.

    What MCP Actually Solves Here

    Model Context Protocol gives an AI assistant a standard way to call external tools directly, instead of a person acting as the manual bridge between the assistant and every service it needs. For PDF work specifically, that means an assistant like Claude or Cursor can merge, split, compress, or convert a file as part of a conversation, without a human stepping out to run the tool separately.

    The PDFHaul MCP server exposes 12 tools across four categories: file management, editing, conversion, and compression. It's live at pdfhaul.com/mcp-server, where you can find setup instructions and generate an API key.

    Architecture

    The server runs on the same Node.js/TypeScript and GCP Cloud Run infrastructure as the rest of PDFHaul, which kept the initial build simpler than starting a separate service from scratch. The tools themselves wrap the existing PDF processing logic, so I wasn't rebuilding PDF handling, just exposing it through a new interface.

    A few decisions that took more thought than I expected:

    Auth. API keys are hashed with bcrypt before storage, and requests are authenticated with a combination of the API key and a JWT. I didn't want to store anything resembling a plaintext credential, even for a free product, since API keys tend to get pasted into config files and committed by accident more often than anyone would like to admit.

    Rate limiting. In-memory rate limiting per key, tuned conservatively at first. Cloud Run's stateless nature means in-memory limits reset on cold starts, which is a real tradeoff worth knowing about if you're building something similar. I accepted it for now rather than standing up Redis for a v1.

    SSRF prevention. Several of the tools accept a URL as input (for example, fetching a PDF to process rather than uploading bytes directly). Any tool that accepts a URL from the caller is a potential server-side request forgery vector, so requests are validated against a blocklist of internal IP ranges and non-HTTP(S) schemes before the server ever fetches anything.

    Idempotency keys. AI assistants sometimes retry tool calls, whether from a timeout, a dropped connection, or the assistant itself deciding to try again. Without idempotency handling, a retried "merge these files" call could produce duplicate output or double-charge a rate limit. Each mutating tool call accepts an idempotency key so a retry returns the original result instead of redoing the work.

    Audit logging. Every tool call is logged with enough detail to reconstruct what happened, without logging the file contents themselves. This matters more for a PDF tool than it might for other APIs, since the whole product's trust model is built around not retaining user files longer than necessary.

    Getting Listed

    The server is listed on the official MCP registry, which so far has been the main discovery channel. Compared to building a plugin or waiting on marketplace approval in other ecosystems, the registry listing process was refreshingly lightweight.

    Where This Leaves Things

    Foxit and Nitro are the other PDF vendors I've seen building MCP servers, both larger companies with existing enterprise PDF products. Being early here as a solo founder is less about outcompeting them head-on and more about being genuinely useful to the AI-assistant-tooling crowd before the space gets crowded.

    If you're working with PDFs inside an AI assistant workflow and want to try it, the server is free to use: pdfhaul.com/mcp-server. I'm happy to answer questions about the implementation in the comments, especially on the auth or SSRF handling if anyone's building something similar.

    Peter, founder of PDFHaul

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:33

    ↗

    Most people trust a green checkmark in a CI pipeline. If every job is marked as successful, we usually believe that everything worked correctly. I was doing the same. Then I found a case where a completely green workflow was hiding an important problem. While testing...

    Most people trust a green checkmark in a CI pipeline. If every job is marked as successful, we usually believe that everything worked correctly. I was doing the same. Then I found a case where a completely green workflow was hiding an important problem.

    While testing hafiza-kur across different platforms, my main goal wasn't just to see green checkmarks. I wanted to build a full list of what broke on which platform. I needed to see every single error, without stopping the pipeline at the first failure.

    To do this, I added continue-on-error: true to the test steps. It was a conscious choice: let the tests run to the end on every platform, collect the raw logs, and check the results later.

    Then came Windows with Python 3.11 and 3.13. The test t_y42 ran 58 test scenarios. It took 91 seconds on Py3.11 and 110 seconds on Py3.13 to finish. The heavy lifting was completely done and the assertions ran. Then came the final step: printing the summary of the results to the console.

    Right on the first line of the summary loop, Python hit a character that the default Windows console couldn't encode. UnicodeEncodeError.

    The process crashed instantly. All 58 test results vanished before reaching the log file. And because continue-on-error: true was active, CI quietly ignored the crash, cleaned up, and moved forward.

    The situation got worse when I checked the workflow data using the GitHub Actions API. I expected the step to show a failure, even if the main job continued.

    It didn't. The API returned:

    conclusion: success

    In GitHub Actions, if a step has continue-on-error: true, its conclusion field is recorded as success. The actual failure was hidden in a different, rarely checked field called outcome.

    That meant one-third of our cross-platform data was completely invisible — but CI was showing it as a green, successful run.

    I created a separate verification step right after test execution: hukum_kapisi.py (The Gate of Verdicts).

    This gate runs without continue-on-error. Instead of running the tests again, it reads the raw output logs printed to the screen. It explicitly looks for the lines that print the final test summary.

    If the log ends before those lines appear, hukum_kapisi.py stops the build immediately with an error.

    There is a very important limit to how this gate works: this gate never tells you that the test results were green. It only tells you that they were not lost.

    Confusing "the tests passed" with "the test results were successfully recorded" leads to hidden bugs. hukum_kapisi.py makes sure the log data actually exists; the next tool decides if the tests passed or failed.

    This problem is not unique to my project, or to Windows encoding bugs. Any CI pipeline that relies on reading screen output, summary loops, or permissive error settings can suffer from silent data loss.

    If your pipeline depends on log output to collect data, ask yourself this question:

    Are you sure your tests actually passed — or are you just assuming your reporting script didn't crash before showing the results?

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:31

    ↗

    A stranger messaged me on LinkedIn about a job in the Web3 space. Friendly, low pressure, said it sounded like a good fit. To show me what the team was building, they shared a private GitHub repository and asked me to spend "15 to 30 minutes running it and looking around" to...

    A stranger messaged me on LinkedIn about a job in the Web3 space. Friendly, low pressure, said it sounded like a good fit. To show me what the team was building, they shared a private GitHub repository and asked me to spend "15 to 30 minutes running it and looking around" to see whether the project made sense to me.

    That repository was not a job assessment. It was a trap designed to steal from the developer who runs it. Here is what it actually was, how it works in plain language, and the red flags that let you spot this without being a security expert.

    The short version

    The repo looks like a real, working crypto staking app. Most of it genuinely functions. That is the whole point: it needs to be convincing enough that you clone it and run it.

    Hidden inside is a single booby-trapped file, disguised as a harmless styling plugin. The moment you run or build the project on your own machine, that file executes as a program with full access to your computer. It then quietly:

    • steals the passwords saved in your browser,
    • steals cryptocurrency wallet data and browser wallet extensions like MetaMask,
    • hunts your hard drive for anything that looks like a secret (seed phrases, private keys, .env files, SSH keys),
    • and opens a hidden remote connection so the attacker can control your machine and browse your files at will.

    Crucially, none of this targets the app's "users". There is no scam website draining visitors' wallets. The victim is you, the developer, the moment you try it out.

    What actually happened to me

    I was suspicious from the first message. A recruiter I have never spoken to, handing me a whole codebase and asking me to run it before we have even had a real conversation, is odd. Running someone's code is not the same as reading it. But the ask seemed reasonable on the surface, "just have a look", so I started to look.

    Here is the part I want other developers to sit with, because it is where I nearly got caught.

    I did not open the project and start typing commands myself. I asked my AI coding agent to look at the repo: check the architecture, read the code, and explain what the project is and how it works. This is now a completely normal way to evaluate an unfamiliar codebase. Delegate the first pass to the agent.

    The problem: modern coding agents can run code, not just read it. To "see how it works", an agent can quite reasonably decide to build the project or start the dev server. In this repo, that is exactly the trigger. The agent could have sprung the trap for me, without me ever typing the fatal command.

    I watched the agent work and noticed it was mostly running read-only commands to describe the project. Then it clicked: this thing can execute code, and this is unfamiliar code I just downloaded from a stranger. That was the moment I stopped, and rewrote my instructions to tell the agent to treat the entire repository as hostile and never run or build anything. I then had it analyse the code statically, and that is when the payload turned up.

    If you take one thing from this: "just ask the AI to look at it" is not automatically safe. An agent that can run commands is a loaded gun pointed at your own machine when the code is untrusted.

    How the trap works (plain language, then the details)

    The plain version

    When you set up a JavaScript project, there are two separate steps:

    1. Install the building blocks the project depends on (npm install).
    2. Run or build the project to actually start it (npm run dev, npm start, and so on).

    Most people assume the install step is the dangerous one. This attack deliberately leaves the install step clean and safe, and hides the trap in the run step, the thing you naturally do next to see the app working.

    To style a web page, this project uses a popular tool called Tailwind. Tailwind lets projects load small "plugins". Six of those plugins are real and tiny. The seventh is the weapon. When the project builds its styling, it loads that plugin, and the plugin is not styling code at all. It is a program that runs on your computer, with your permissions, doing everything listed above.

    The technical details

    • The malicious file is theme/js/auron-core.min.js, registered as a Tailwind plugin in tailwind.config.ts.
    • Its six sibling plugins are 101 to 299 bytes each. This one is 4,073,468 bytes, roughly 14,000 times larger. That size difference alone is visible from ls -la before you read a single line.
    • npm install does nothing (there are no install hooks). The payload fires on npm run dev, npm start, or next build, because any CSS compilation loads the plugin.
    • It only activates when NODE_ENV === "development", so it targets developers evaluating the repo and stays dormant in anything deployed to production, where it might get noticed.
    • The file is heavily obfuscated (an obfuscator.io build with a ~24,000-entry string table, control-flow flattening, and anti-debugging traps) so that a casual look, or even a naive dump, reads as noise rather than as a decoded payload.
    • It fetches its real dependencies at runtime with npm install ... --no-save, so they never appear in package.json or the lockfile, then spawns a detached background process by piping the script over stdin, so nothing malicious is ever written to disk.
    • Command-and-control runs over a bare IP address (153.75.87.26, ports 8085 to 8087) with no domain name. That means there is no DNS lookup, so Pi-hole, NextDNS, AdGuard and corporate DNS filtering never see it. A clean DNS log here proves nothing.

    The whole repo is a stage set

    What makes this dangerous is not cleverness in the payload, it is the plausibility around it. The attackers built an entire believable project to buy the few seconds between "run it" and "infected".

    • Fabricated git history. 245 commits faking a maintained project. The tell: every backdated commit is stamped at exactly 12:00:00, one per day, with templated, nonsensical messages like "Fix bug in metamask.svg" and "Polish code in placeholder.svg". The payload was slipped in under an innocuous "Polish code" commit.
    • A pre-emptive alibi. The README contains a tidy "Security Note" explaining away an unrelated backend folder as harmless legacy code. A comment that answers a suspicious question before you have asked it is itself a warning sign.
    • Load-bearing real code. Genuinely clean, competent code sits right next to the payload, in exactly the files a reviewer is most likely to open. The authenticity is the camouflage.
    • A facade that is hollow where nobody looks. The "smart contracts" cannot even compile. The custom theme emits CSS classes the app never uses. The "legacy backend" is 90 files of an unrelated e-commerce app used as padding.

    Red flags you can spot without being a security expert

    1. A stranger asks you to run their code. A recruiter you have never spoken to, sharing a whole repo and asking you to run it as a "task", is the entire scam. Reading code is low risk. Running it is not.
    2. Web3 or crypto plus urgency-free friendliness. These campaigns lean on a plausible, pleasant job offer. The niceness is part of the method.
    3. One file wildly bigger than its neighbours. ls -la is a security tool. A single file thousands of times larger than its siblings is a giant flashing sign.
    4. Comments or notes that pre-emptively reassure you. "Don't worry, this only runs in Node, not the browser." That is not reassurance. In Node it has your filesystem, your network, and no browser sandbox.
    5. A config file that loads local files. tailwind.config, postcss.config, next.config, vite.config and webpack.config all run real code at build time. Every require() of a local path in a config file is worth a look.
    6. Perfect-looking but empty history. Identical commit timestamps, templated messages that do not match the files they touch, and author names with empty emails are cheap to spot once you know to look.

    What to do if you have already run something like this

    If you cloned and installed but never built or ran it, you are almost certainly fine. Installing alone did not trigger it. Delete the checkout, remove any dev container or volume, and report the repo.

    If you did run or build it, treat the machine as fully compromised and act quickly:

    • Disconnect the machine from the network before doing anything else.
    • Assume every password saved in any Chromium-based browser is stolen. Rotate all of them.
    • Move funds out of any wallet whose seed phrase or keystore was anywhere on that machine, and treat those seeds as burned.
    • Rotate SSH keys, cloud API keys, and every secret in every .env on the machine.
    • Rotate anything that was in your clipboard during the exposure window (it watches the clipboard continuously, which is enough to catch a pasted wallet address).
    • If you use WSL, treat the entire Windows host as in scope, not just the Linux side. This malware pivots from WSL into C: drive user folders.

    If in any doubt, get help from someone who does incident response. Rotating credentials is cheap. Regret is not.

    The takeaways

    • A safe npm install does not mean a safe build. Supply-chain awareness fixates on install hooks. This attack ignores them and uses ordinary build configuration, which is just executable code with no sandbox.
    • Read the config files, not only the source. That is where untrusted code gets to run.
    • Compare file sizes. One outlier was the entire tell.
    • Treat pre-emptive "this is safe" comments as signals, not comfort.
    • Your AI agent can be the attack vector. If you delegate "have a look at this repo" to an agent that can execute commands, you have handed untrusted code a way to run. Sandbox it, or explicitly tell the agent to treat the repo as hostile and never build or run it.
    • When you evaluate anything untrusted, do it in a throwaway virtual machine or container. Isolation is what saved me. Note that a bind-mounted folder still exposes that folder, so isolation is a spectrum, not a switch.

    The friendly job offer, the working app, the detailed roadmap and the busy commit history all exist for one reason: to earn the few seconds of trust between "run it" and execution. Do not give it to a stranger.

    Indicators of compromise (safe to share): repo BitAngelsLabs/auron; malicious file theme/js/auron-core.min.js at 4,073,468 bytes; C2 at 153.75.87.26 on ports 8085, 8086 (/upload), 8087; runtime install of sql.js, socket.io-client, node-pty via --no-save; background process node --max-old-space-size=4096 --no-warnings -. If you found this repo, report it to GitHub as malware and warn anyone who may have cloned it. The malicious file itself should not be shared or re-hosted.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:19

    ↗

    An editable range can change without changing Word text A Word document can store an editable-range marker around text and associate that marker with an individual editor. The covered text can remain exactly the same while the stored editor assignment changes. Text-only...

    An editable range can change without changing Word text

    A Word document can store an editable-range marker around text and associate
    that marker with an individual editor. The covered text can remain exactly the
    same while the stored editor assignment changes. Text-only review sees no
    change, while package-level review can report the marker without pretending to
    know whether an identity is actually authorized.

    Document Change Assurance Benchmark 0.9.0
    adds its twentieth deterministic paired package:
    review.permission_range_editor_changed. Both sides retain one
    paired w:permStart/w:permEnd boundary, numeric marker
    ID, covered stored text, package-member set, and stored w:t
    sequence. Only word/document.xml changes, in one synthetic
    w:ed editor assignment.

    Stored markers, not effective access

    Microsoft’s Open XML documentation for w:permStart
    describes a range-permission start marker paired to a later end marker by a
    shared ID. The matching w:permEnd contract
    describes the reverse pairing requirement. The standard permits the compact
    paragraph-level marker shape used in this pair.

    DCAB fixes one boundary, numeric ID, and synthetic covered run, then changes
    only a synthetic individual-editor attribute. It does not enable document
    protection, authenticate an editor, resolve a group, calculate editable cells,
    open Word, start an application, or claim that an Office client will honor the
    marker. This is a stored-markup review case, not an access-control or
    client-behavior test.

    A deterministic permission-markup boundary

    The independent verifier checks marker attributes, order and pairing, the
    covered run, deterministic package bytes, stable members, unchanged Word text,
    and the one-member pair boundary. Standard python-docx opens all
    38 .docx fixtures, and its lower-level OPC reader opens all 40
    packages.

    The optional local DocFence 0.27.0
    adapter maps aggregate word_permission_range_inventory_changed
    evidence. It verifies that both sides retain one story, start, end, paired
    range, and individual-editor assignment, with no group, table-column selector,
    or unmatched marker. It uses counts and private local signatures only; it does
    not expose the marker ID or editor value.

    DCAB 0.9.0 retains fixture schema version 1 and extends the corpus from 19 to
    20 cases. It does not claim an editor is authenticated, allowed to edit,
    active, known to a client, or protected by a particular policy.

    The source, generated corpus, and verifier are MIT-licensed on
    GitHub. Read the
    canonical release note
    for the full boundary and install command.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:13

    ↗

    Germany's Energy Revolution: Wind and Solar Overtake Fossil Fuels for the First Time In 2026, Germany crossed a historic threshold: for the first time, wind and solar photovoltaic (PV) systems generated more electricity than fossil fuels across the year. This isn't just a...

    Germany's Energy Revolution: Wind and Solar Overtake Fossil Fuels for the First Time

    In 2026, Germany crossed a historic threshold: for the first time, wind and solar photovoltaic (PV) systems generated more electricity than fossil fuels across the year. This isn't just a statistic—it's a fundamental shift in how Europe's largest economy powers itself. According to data from the Fraunhofer Institute for Solar Energy Systems (ISE), renewable sources (wind, solar, hydro, and biomass) contributed over 62% of Germany's net public electricity generation, with wind and solar alone accounting for roughly 54%. Meanwhile, fossil fuels (coal, gas, and oil) fell to just 28% of the mix.

    But what does this milestone actually mean? Is it a fluke of favorable weather, or a structural transformation? And what technical and policy lessons can other nations draw from Germany's 'Energiewende'?

    The Numbers Behind the Milestone

    Let's break down the figures that made headlines on Hacker News and across the energy world.

    • Wind power (onshore + offshore) contributed about 38% of net electricity generation in 2025.
    • Solar PV contributed around 16%.
    • Fossil fuels (mostly hard coal, lignite, and natural gas) contributed roughly 28%.
    • The remaining share came from hydro, biomass, and other renewables.

    On an annual basis, wind and solar generated approximately 286 TWh compared to fossil's ~150 TWh. This is not a one-off blip; it's the culmination of a decade-long trend where the renewables share has grown by roughly 2-3% per year, while coal has been systematically phased down.

    The Role of Weather and Grid Flexibility

    A skeptic might ask: "Wasn't 2025 unusually windy and sunny?" In fact, 2025 had slightly above-average wind speeds in the North and Baltic Seas, but solar irradiation was close to normal. However, the key is not a single year's weather. The structural factors are:

    1. Capacity additions: Germany installed ~10 GW of new solar PV in 2025 alone, and offshore wind added another 2 GW.
    2. Retirement of coal plants: The last hard coal plants were retired in 2024, and lignite (brown coal) is being phased out faster than planned.
    3. Grid and storage improvements: Battery storage capacity doubled to 12 GW, enabling better integration of intermittent renewables.

    Thus, even a mediocre weather year would have produced a similar result. The milestone is structural.

    How Germany's Grid Managed the Transition

    Germany's grid is known for its high reliability (99.9% uptime). How did it handle a 58% variable renewable share? The answer lies in a combination of technologies and market design.

    1. Grid Interconnection

    Germany sits in the heart of Europe's synchronous grid. It can export excess wind power to France, Poland, and the Czech Republic, and import hydro from Scandinavia when wind is low. In 2025, Germany was a net exporter of 22 TWh, proving that renewables can be a net positive for grid stability.

    2. Energy Storage: Batteries, Hydro, and Green Hydrogen

    Utility-scale battery storage has exploded in Germany. As of late 2025, ~1.5 GW/2.5 GWh of new batteries were added, helping to shift solar from midday to evening peak. Pumped hydro (9 GW) remains the backbone for longer storage. Green hydrogen is still on the horizon, with pilot projects like the GET H2 pipeline.

    3. Market Design: Negative Prices and Flexibility

    With high solar generation, often electricity prices go negative at noon. This incentivizes flexible consumers (e.g., electric vehicle chargers, heat pumps) to shift load. The German market has adapted with time-of-use tariffs and smart meters, now installed in over 25% of households.

    The Role of Technical Innovation

    Germany's energy transition isn't just about building turbines and panels. It's also about grid operations. Here's a simplified example of how a virtual power plant (VPP) might aggregate distributed solar and battery systems using Python:

    import requests
    
    class VirtualPowerPlant:
        def __init__(self, api_key):
            self.api_key = api_key
            self.assets = []
    
        def add_asset(self, asset_id, asset_type, max_power_mw):
            self.assets.append({
                "id": asset_id,
                "type": asset_type,  # "solar" or "wind" or "battery"
                "max_power": max_power_mw,
                "current_output": 0
            })
    
        def update_forecast(self, solar_forecast_mw, wind_forecast_mw):
            for asset in self.assets:
                if asset["type"] == "solar":
                    asset["current_output"] = min(asset["max_power"], solar_forecast_mw / len(self.assets))
                elif asset["type"] == "wind":
                    asset["current_output"] = min(asset["max_power"], wind_forecast_mw / len(self.assets))
                # battery logic would be more complex, but simplified here
            return self.assets
    
    # Example: aggregate 100 MW solar, 80 MW wind, 20 MW battery
    vpp = VirtualPowerPlant("demo")
    vpp.add_asset("solar1", "solar", 100)
    vpp.add_asset("wind1", "wind", 80)
    vpp.add_asset("bat1", "battery", 20)
    
    forecast = vpp.update_forecast(60, 40)  # 60 MW solar, 40 MW wind
    print(json.dumps(forecast, indent=2))
    

    This shows how software helps aggregate and manage distributed assets, a key reason Germany can handle high renewable penetration.

    Policy and Economic Drivers

    Germany's success isn't accidental. It's the result of two decades of policy:

    • The Renewable Energy Act (EEG): Introduced in 2000, it provided feed-in tariffs that guaranteed grid access and fixed payments for renewable producers. This kickstarted the industry.
    • Carbon pricing: The EU Emissions Trading System (ETS) has made coal increasingly costly. In 2025, the CO2 price hit €80/tonne, making coal uncompetitive.
    • Accelerated permitting: In 2024, Germany streamlined wind and solar approval processes, reducing average project time from 5 years to 2 years.

    Economic Impact: Lower Electricity Prices?

    Wholesale prices have actually fallen in Germany when renewables are high. In 2025, average day-ahead prices were €78/MWh, down from €120/MWh during the 2022 energy crisis. However, household prices remain high due to grid fees and taxes. The next step is to align retail pricing with wholesale costs, which is a political challenge.

    What This Means for the Global Energy Transition

    Germany's milestone offers several lessons:

    1. Grid stability is solvable: With proper planning, high wind and solar penetration doesn't cause blackouts. Germany's system operator, TenneT, has managed 80% renewable peaks.
    2. Coal phase-out is feasible: Germany's coal power has dropped from 44% of generation in 2010 to 25% in 2025. The 2038 phase-out target is likely to be moved to 2030.
    3. Storage is now a necessity: Battery costs have fallen 90% since 2010, making them the key enabler. Germany's example shows that market signals (negative prices) drive storage deployment.

    However, challenges remain:

    • Winter supply: In the dark and windless months (called 'Dunkelflaute'), Germany still relies on gas. The country is building 10 GW of hydrogen-ready gas plants to be converted to green hydrogen by 2035.
    • Grid expansion: The north-south grid bottleneck continues to cause congestion and redispatch costs. The SuedLink HVDC line is still under construction, but when completed, it will reduce waste.
    • Energy efficiency: Germany's electricity demand has not fallen as much as hoped. The transport and heating sectors need to electrify further.

    The Road Ahead: 2030 and Beyond

    By 2030, Germany targets 80% renewable electricity. To reach that, it needs to:

    • Install 215 GW of solar (currently ~90 GW)
    • Install 50 GW of offshore wind (currently ~10 GW)
    • Triple storage capacity to 10 GWh

    These are ambitious but achievable. The 2026 milestone is a psychological boost, proving that the transition is possible without compromising economic output.

    Conclusion

    Germany's wind and solar overtaking fossil fuels is not a single event but a turning point in the energy landscape. It demonstrates that a modern industrial economy can run on renewable energy, with a solid grid and market design. As other countries, including the US and Japan, grapple with their own energy transitions, Germany's example offers a blueprint—and a warning: the transition requires persistent policy, investment in storage, and public acceptance.

    The Hacker News community has been buzzing with discussions about this milestone, and rightly so. It's a rare piece of good news in the climate change narrative. But the work is far from over. The next decade will determine if Germany can go from 50% to 80% and beyond. If it does, it will be a model for the world.

    For those interested in the raw data, the Fraunhofer ISE publishes weekly charts and the ENTSO-E transparency platform offers real-time generation data. The trend is clear: the future is renewable, and Germany is leading the way.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:11

    ↗

    Celebrating 45 Years of Kermit: The Protocol That Refuses to Die, Now Reborn in C In the pantheon of early network protocols, few have enjoyed the staying power of Kermit. Born in the early 1980s at Columbia University, Kermit was a simple, robust file transfer protocol that...

    Celebrating 45 Years of Kermit: The Protocol That Refuses to Die, Now Reborn in C

    In the pantheon of early network protocols, few have enjoyed the staying power of Kermit. Born in the early 1980s at Columbia University, Kermit was a simple, robust file transfer protocol that conquered the world of dial-up modems, serial ports, and heterogeneous computing. As we mark its 45th anniversary in 2026, the protocol is not just a nostalgic relic—it's being actively reimagined with a brand-new, modern C implementation that promises to keep Kermit relevant for another generation.

    The Remarkable History of Kermit

    Kermit was developed by Frank da Cruz and Bill Catchings at Columbia University in 1981. The name, a nod to the Muppet, was chosen to avoid clashing with any existing protocol. The goal was simple: create a file transfer protocol that could work over any serial connection, regardless of the hardware, operating system, or network. In an era of wildly incompatible systems—CP/M, Apple II, TRS-80, Unix, DEC VMS—Kermit's portability was its killer feature.

    The protocol was designed to be minimal and resilient. It used a packet-based system with checksums, but crucially, it could adapt to the underlying link's constraints. It supported both text and binary transfers, and it was easily extended. The reference implementation was written in C, which made it portable across many platforms.

    Kermit became the de facto standard for transferring files between mainframes, minicomputers, and personal computers. It was used in academic, government, and corporate environments. Even the US Navy used Kermit for ship-to-shore communications. Its popularity peaked in the 1980s and 1990s, but it never fully disappeared. The protocol was later extended to support network connections (Kermit over TCP/IP), and even today, it remains a dependable fallback for embedded systems and legacy applications.

    Why Kermit Still Matters

    In an era of gigabit fiber and wireless networks, why would anyone care about a protocol designed for 2400 baud modems? The answer lies in its simplicity and reliability. Kermit is not just a file transfer protocol; it's a philosophy. It works where modern protocols fail—on noisy, low-bandwidth, or unusual links. It has minimal overhead and can be implemented in a few hundred lines of code. For embedded systems, industrial controllers, and retrocomputing enthusiasts, Kermit is a lifeline.

    Moreover, the protocol's design is a masterclass in pragmatic engineering. It handles flow control, error detection, and recovery without the complexity of TCP/IP. It's a perfect candidate for learning about networking fundamentals. And for those who need to transfer files to or from a vintage machine, Kermit is often the only viable solution.

    The First New C Implementation in 2026

    Now, to celebrate its 45th anniversary, a new open-source project has emerged: a clean, modern, and fully-featured Kermit implementation written in C. Titled kermit-c, this project aims to bring Kermit into the modern era, preserving the original protocol's spirit while adopting contemporary software engineering practices.

    The author, a long-time Kermit enthusiast, decided to rewrite the protocol from scratch. The original Kermit implementation was monolithic and tightly coupled to the terminal I/O of its time. The new version is modular, thread-safe, and designed to be cross-platform—running on Windows, Linux, macOS, and embedded RTOSes.

    Key Features of the New C Implementation

    • Portability: Written in ISO C11, it compiles on virtually any platform with a C compiler, including both 32-bit and 64-bit systems.
    • Modular architecture: The protocol core is separate from the transport layer, allowing you to plug in serial, TCP, or even Bluetooth transport.
    • Modern I/O: Supports both synchronous and asynchronous operations, and can be used as a library or a command-line tool.
    • Extensibility: The packet format is fully configurable, allowing for experimentation with new features.
    • Testing: Includes a comprehensive test suite that validates the protocol against the original Kermit 95 reference implementation.
    • Documentation: Extensive comments and a full user guide, making it easy to integrate into other projects.

    The library exposes a simple API that mirrors the classic Kermit functions but with a modern flavor. Here's a quick example of how to transfer a file using the new kermit-c library:

    #include <kermit/kermit.h>
    
    int main(void) {
        kermit_t *k = kermit_init(KERMIT_TRANSPORT_SERIAL, "/dev/ttyUSB0");
        if (!k) {
            fprintf(stderr, "Failed to initialize Kermit\n");
            return EXIT_FAILURE;
        }
    
        kermit_set_baud(k, 115200);
        kermit_set_parity(k, KERMIT_PARITY_NONE);
        kermit_set_packet_size(k, 512);
    
        // Send a file
        if (kermit_send(k, "local_file.txt", "remote_file.txt") != KERMIT_OK) {
            fprintf(stderr, "Transfer failed\n");
            kermit_close(k);
            return EXIT_FAILURE;
        }
    
        // Receive a file
        if (kermit_recv(k, "incoming.bin") != KERMIT_OK) {
            fprintf(stderr, "Receive failed\n");
            kermit_close(k);
            return EXIT_FAILURE;
        }
    
        kermit_close(k);
        return EXIT_SUCCESS;
    }
    

    Under the hood, the implementation carefully follows the original Kermit packet format, but it uses a modern state machine to handle the session. The library is designed to be thread-safe, so you can run multiple transfers concurrently, a feature that the original protocol could not easily achieve.

    The Legacy Lives On

    The new C implementation is not just a clone; it's a thoughtful overhaul. It preserves the simplicity and reliability of the original while incorporating lessons learned from decades of software engineering. The project has already gained traction on Hacker News, where it trended in early 2026, sparking discussions about the protocol's enduring relevance and the beauty of clean C code.

    One of the most compelling aspects of the project is its commitment to backward compatibility. You can use kermit-c to communicate with an old 1980s Kermit running on a Commodore 64, and the transfer will work flawlessly. This is a testament to the original protocol's design and the new implementation's fidelity.

    Conclusion

    Kermit has seen 45 years of continuous use, a rare feat for any technology. The new C implementation breathes fresh life into this classic protocol, proving that good engineering never goes out of style. Whether you're a retrocomputing hobbyist, an embedded systems engineer, or a student of protocol design, kermit-c offers a robust, portable, and educational tool. As we celebrate Kermit's anniversary, we salute the protocol that taught us how to move files over uncertain links—and the new C code that keeps it alive for the future.

    So, if you're at all interested in the history of computing or the art of writing efficient C, check out kermit-c. It's a beautiful piece of code, and it's a fitting tribute to a protocol that has been quietly working in the background for 45 years—and will continue to do so for many more.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 19:10

    ↗

    Ten Breakthroughs in Mathematics and Theoretical Computer Science (2026) The year 2026 has already witnessed a remarkable confluence of human intuition and machine intelligence, reshaping the landscape of pure and applied mathematics. From formal proof verification to quantum...

    Ten Breakthroughs in Mathematics and Theoretical Computer Science (2026)

    The year 2026 has already witnessed a remarkable confluence of human intuition and machine intelligence, reshaping the landscape of pure and applied mathematics. From formal proof verification to quantum complexity, these ten advances are not just incremental—they are paradigm shifts that will echo for decades. Here's a deep dive into the breakthroughs that have captured the attention of Hacker News and the scientific community.

    1. The First AI-Discovered Proof of a Major Conjecture

    In a historic collaboration, a team at the Institute for Advanced Study and DeepMind used an AI system called LeanMind to discover a proof of the Sylvester-Gallai conjecture in a special Euclidean geometry. The proof, which involved a novel construction of an auxiliary algebraic curve, was initially rejected by human mathematicians for its perceived lack of elegance. However, the formal verification system Coq confirmed its correctness, and the result was published in the Annals of Mathematics. This marks the first time a major open problem has been solved by AI without substantial human guidance.

    theorem sylvester_gallai : forall (P : set Point),
      finite P ->
      (forall p q : Point, p in P -> q in P -> p != q ->
        exists r : Point, r in P /\ r != p /\ r != q /\ collinear p q r) ->
      exists line, line_contains P line
    

    The implications are staggering: AI can now generate proofs that not only verify but also inspire new mathematical frameworks.

    2. Resolution of the Hadamard Conjecture for All Orders > 4

    For over a century, the Hadamard conjecture—which posits that a Hadamard matrix exists for every order divisible by 4—remained open. In 2026, a team from Oxford and MIT used a combination of combinatorial design theory and quantum annealing to construct explicit Hadamard matrices for all orders up to 1000, and then extended the method to prove the conjecture for all orders. The key was a new recursive construction based on signed graph symmetries that reduced the problem to a finite set of base cases.

    This breakthrough has immediate applications in error-correcting codes, signal processing, and even quantum state tomography.

    3. A New Lower Bound for Matrix Multiplication: Omega(n^2.3727)

    After decades of incremental improvements, the matrix multiplication exponent ω finally fell below 2.3727. The breakthrough came from a surprising source: tensor decomposition combined with deep reinforcement learning. The AlphaTensor system, introduced in 2022, was refined to discover a new algorithm that achieves ω = 2.3719. While the improvement is small, the theoretical significance is huge: it demonstrates that machine learning can outperform human intuition in algebraic complexity.

    # Pseudo-code for the new matrix multiplication algorithm
    def matmul(A, B):
        # Use RL-optimized tensor decomposition
        return tensor_contract(A, B, rank=2.3719)
    

    4. The Formal Verification of the Kepler Conjecture

    The Kepler conjecture, which states that the densest packing of equal spheres is the face-centered cubic lattice, was formally verified in Lean by a team at Carnegie Mellon University. This is a monumental achievement in formal mathematics, as the original proof by Thomas Hales relied on extensive computer calculations. The new proof is fully machine-checked and includes a new, more elegant proof of the local density inequalities using interval arithmetic and linear programming.

    This marks a paradigm shift: formal verification is no longer just for toy examples but can handle the most complex human proofs.

    5. New Advances in the P vs. NP Problem: A New Separation for Exponential Time

    While P vs. NP remains unresolved, a team of researchers at the University of Copenhagen and IIT Kanpur proved a new separation: EXP ≠ NEXP under a plausible derandomization assumption. More importantly, they introduced a novel technique called quantum circuit lower bounds via local Hamiltonian complexity, which shows that certain quantum circuits cannot be simulated by classical circuits of subexponential size. This has implications for both complexity theory and quantum computing.

    # Pseudo-code for the separation proof
    if (quantum_circuit has low-entanglement) then
        NEXP != P
    

    6. A Breakthrough in the Collatz Conjecture: A Billion-Case Verification

    While not a full proof, the Collatz conjecture received a major boost when a new algorithm based on automated theorem proving and GPU acceleration verified the conjecture for all numbers up to 2^70 (about 1.18 × 10^21). The verification used a novel technique called modular arithmetic pruning that reduces the search space by 99.999%. The results, published in Mathematics of Computation, provide strong evidence for the conjecture and have inspired new approaches using dynamical systems.

    7. A Quantum Algorithm for Solving Differential Equations in Polynomial Time

    A team at Caltech and Google Quantum AI unveiled a quantum algorithm that solves a large class of nonlinear partial differential equations (PDEs) in polynomial time, whereas classical algorithms require exponential time. The algorithm leverages quantum linear algebra and a new encoding of PDE solutions as quantum states. This has already been applied to model fluid turbulence in a simulation that would have taken a classical supercomputer 1,000 years—now done in 2 minutes on a 100-qubit machine.

    # Quantum algorithm sketch
    from qiskit import QuantumCircuit
    qc = QuantumCircuit(10)
    # encode initial conditions
    qc.initialize(...)
    # apply quantum PDE solver
    qc.append(QPDE_solver, range(10))
    

    8. A New Proof of the Twin Prime Conjecture (finite distance bound)

    In 2013, Yitang Zhang proved that there are infinitely many prime pairs within a finite distance. In 2026, a team led by a young mathematician at Princeton improved the result to a distance of 2—the full twin prime conjecture—but under the assumption of the Generalized Riemann Hypothesis. While conditional, this is seen as a massive step. The proof introduces a new sieve method based on additive combinatorics and non-trivial bounds on exponential sums.

    9. The Development of "Catastrophe Theory 2.0" for Machine Learning

    Building on the classic catastrophe theory, a new mathematical framework called Catastrophe Learning has been introduced to explain the sharp phase transitions in neural network training. The theory, developed by a collaboration of mathematicians and AI researchers, uses singularity theory to analyze the loss landscape. It provides a rigorous explanation for the "grokking" phenomenon and the sudden generalization in transformer models. This has led to new training algorithms that avoid catastrophic forgetting by using topological data analysis.

    10. The Rise of "Formalized Mathematics" in Mainstream Publishing

    While not a single breakthrough, the cumulative effect of formal verification tools (Lean, Coq, Isabell) has reached a tipping point. In 2026, the top three mathematics journals now require formalization for any proof that relies on heavy computation. This has led to a massive collaboration: the Formal Math Project has formalized over 500,000 theorems, including the entire curriculum for abstract algebra. The project has even proposed a "mathematical proof" but is now a standard for rigor.

    Conclusion

    These ten advances represent a new era where mathematics and computer science are not just partners but are fused. AI is no longer a tool; it is a co-author. Formal proof is becoming the gold standard. And the boundaries between continuous and discrete, between quantum and classical, are blurring. For the Hacker News crowd, this is the frontier. The future is not just about faster CPUs—it's about deeper logic.

    As we look ahead, one thing is certain: the next breakthrough is just around the corner, and it will likely be discovered by a human-AI collaboration, exactly as these were.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:57

    ↗

    Your Secrets Need a VDP, Not Just a Bug Bounty Bug bounty programs are valuable -- until they replace disclosure policies. Learn how unreasonable PoC demands or scope exclusions create security blind spots when it comes to leaked secrets. By Gaetan Ferry • 6 Feb 2026 • 8 min...

    Your Secrets Need a VDP, Not Just a Bug Bounty

    Bug bounty programs are valuable -- until they replace disclosure policies. Learn how unreasonable PoC demands or scope exclusions create security blind spots when it comes to leaked secrets.

    By Gaetan Ferry • 6 Feb 2026 • 8 min read

    Your Secrets Need a VDP, Not Just a Bug Bounty

    In recent years, more and more companies have launched bug bounty programs as proof of their commitment to security and as a way to implement continuous monitoring of their corporate attack surface. Those programs sometimes offer generous payouts to vulnerability reporters, and often partner with dedicated platforms that offer various services such as:

    • Payment and billing management
    • Triaging as a Service
    • Investigation assistance

    Platforms rely on a "hacker community", a group of people who hack on the programs to discover vulnerabilities and earn bounty money. Most of those "hackers" are self-employed in a way that allows them to comply with local applicable tax laws.

    Bug bounty programs are a great way to have a corporate perimeter or set of applications audited by a large set of people, nearly continuously. They can be a great addition to a company's security policy. In fact, GitGuardian has been running a bug bounty program for multiple years, as a complement to our periodic audits and overall security strategy.

    Bug Bounty Done Wrong

    The problem with bug bounty programs starts when they try to substitute for a proper Vulnerability Disclosure Policy. When they do, they no longer improve your security posture; they undermine it.

    Bug bounties, by design, are selective. From a "hacker" perspective, they come with limited scopes, opaque triage processes, gatekeeping platforms, or even eligibility requirements. As a result, valid, good-faith vulnerability reports can get ignored, rejected, or buried -- not because they lack accuracy or merit, but because they fall outside of the boundaries of the programs' terms or the opaque decision of a third-party triager. Payout levels also undermine this testing model, turning continuous monitoring into a blind spot shaped by market incentives; why search for or report vulnerabilities when they pay little or nothing?

    Using a bug bounty platform as the only possible communication channel for vulnerability disclosure creates unnecessary friction:

    • Mandatory registration forces researchers to trade their privacy for participation.
    • Non-disclosure clauses can silence conversation about systemic risks, and more generally hinder information sharing.
    • Platform gatekeeping can discourage reporters.
    • Worse: out-of-scope dismissals allow serious vulnerability reports to be voided, and never reported to security teams

    These blind spots don't make an organization more secure. They make it easier to overestimate the security posture, thinking that fewer reports mean fewer problems.

    A good Vulnerability Disclosure Policy (VDP) should promote openness. It should be a clear and accessible way for anyone -- a professional researcher, a student, or a concerned user -- to report a security issue safely, privately, and without process complexity. A good disclosure policy should enable communication rather than controlling it.

    One particular issue that highlights how bug bounty can fail as a disclosure channel lies in how they handle secret leak reports.

    GitGuardian's experience

    One of the core foundations of GitGuardian is the detection and remediation of secrets leaked in public spaces. Over the course of the past year, while working on improving our understanding of the secret sprawl issue, we performed responsible disclosures to hundreds of companies.

    GitGuardian's cybersecurity research team is not a bug bounty crew. We do not seek any reward for reporting incidents. For this reason, we usually attempt to contact affected companies directly, preferably via email, and sometimes through online forms dedicated to security incident reporting. We only fallback to the bug bounty program channel as a last resort, or when directly prompted to do so.

    While working with platforms, we experienced a variety of situations and answers that illustrate how bug bounty can fail as a disclosure channel.

    400 PoC or GTFO

    As a result of a large-scale research project, we recently reported leaked private keys related to valid X.509 certificates. The risk of such incidents can generally be considered high, as a leaked key can be used to set up Man-In-The-Middle attacks against the company's public assets. Some of our reports had to go through bug bounty platforms, which already create friction. As much as we can automate the sending of hundreds of e-mails, filling bug bounty reports at scale is challenging.

    In all our reports, the triagers asked for a proof of concept exploitation.

    HackerOne response asking for a proof of concept after private key leak

    HackerOne response asking for a proof of concept after private key leak

    BugCrowd response asking for a proof of concept after valid credential leak

    BugCrowd response asking for a proof of concept after valid credential leak

    First, proving a credential's impact has a clear ethical boundary: demonstrate the potential for harm without causing actual harm. This means verifying credentials are valid, confirming what resources they access, and documenting their privilege level, but without reading production data, modifying systems, or performing harmful actions. This is not always possible, depending on the credential type. In the case of leaked X.509 certificate private keys, creating such a proof-of-concept would have required decrypting real traffic or impersonating production services -- crossing from validation into active attack -- which could have severe legal consequences.

    Then, the main question is: what happens after the report gets closed as informative? There is a chance that no action will be taken. In some cases, the issue might never pass the triaging filter and reach the corporate security team.

    In our case, most reports were actually closed as informative, and none of the related certificates were revoked. Worst of all, some GitHub repositories containing leaked private keys have never been deleted. We later contacted the related certificates' issuer authorities to have the keys black listed and certificates revoked.

    403 Private Program

    Bug bounty programs can either be public or private. Public programs can be viewed, accessed, and interacted with by anyone. On the other hand, private programs are invite-only, so only selected members of the platform's community can report vulnerabilities.

    In that case, obviously, the program can not be considered a proper disclosure channel. However, there is a reporting flow that overlooks this issue:

    • You discover a vulnerability and attempt to report it through standard channels (security@, contact forms).
    • You receive a response: 'Please submit via our Bug Bounty Program.'
    • You navigate to the platform, only to find it's private and invitation-required.
    • Without an invitation, you hit a dead end with no alternative channel.

    Our team has faced this situation once, making the reporting process painful and highlighting how companies often lack awareness about vulnerability disclosure practices.

    Similarly, a documented program can have expired or been decommissioned. In this case, the communication channel is effectively nonexistent. This was the case when we reported a leaked API key to xAI in 2025.

    404 Secret Not Found In Scope

    The scope of a program includes the list of assets that are authorized to be worked on. It also includes the list of vulnerabilities that are accepted in reports. The purpose of this restriction is to limit the number of low-quality reports or reports for vulnerabilities that are widely recognized as lacking real-world impact.

    However, if the scope of a program is too restricted, valid and severe issues might get discarded by the triaging team without further notice. In that spirit, we faced bug bounty programs that explicitly marked leaked credentials as out of scope. The platforms sometimes even encourage their customers to ban secrets. The reason behind this is tied to the origin of the credentials, as we discussed with a platform representative:

    We strongly advise our clients to exclude leaked secrets from their bug bounty program scope. The reality is that compromised credentials frequently originate from illicit sources. There's a thriving underground market for stolen credentials, and by offering bounties for leaked secrets, we risk inadvertently incentivizing and legitimizing a secondary marketplace for compromised authentication data.

    While this concern is understandable, excluding leaked secrets creates a dangerous blind spot. Valid credentials represent immediate security risks: unauthorized access, data breaches, or compromised systems.

    The solution isn't to ban secret reports -- it's to require source transparency. Researchers should disclose where the credentials were found. This approach enables security teams to investigate the leak's origin and take appropriate remediation action, while distinguishing legitimate research from illicit activity.

    500 Triager error

    Triager gatekeeping can also be an issue in case of a misunderstanding about a security issue. While misunderstandings can occur with corporate security teams, triagers can close the communication channel when they deem an issue uninteresting. While it is often possible to ask to reopen closed reports or ask for mediation, this can prevent legitimate reports from reaching the corporate teams and create unnecessary friction.

    Triager closing issue while credentials were still valid

    In the above case, the triager closed the issue while the affected credentials were still valid. Such behaviors create frustration, discourage reporters and, again, prevent secrets from being reported.

    302 Redirect To Bug Bounty

    Even when a direct communication channel with corporate security teams exists, it happens that those teams redirect mailed reports to a bug bounty platform. The rationale is understandable: centralizing all vulnerability reports in one place simplifies triaging and tracking.

    Doing so not only slows the remediation process down, but also creates a dangerous bottleneck as the submission will likely have to comply with the bug bounty rules and scope definition, with the same pitfall as issues directly reported on platforms.

    It also goes against the potential privacy requirements of the reporter, who would have to create an account on the bug bounty platform and sometimes even fill out tax regulation documents.

    We received such a response when we contacted xAI for a leaked token last year. In that case, the corporate team also fixed the issue in the background, even before we could submit it to their program, demonstrating a clear lack of transparency.

    Dear Gaëtan,
    Thank you for your email.
    For us to analyze and also for you to receive proper credit, if applicable, would you please submit this to xAI's Bug Bounty Program on HackerOne?
    https://hackerone.com/x?type=team
    Thanks!
    xAI Team

    Vulnerability Disclosure Policy done right

    Writing a clear Vulnerability Disclosure Policy that provides an open and transparent communication channel is of prime importance to ensure your company receives vulnerability reports properly. As we explained above, such a policy should promote openness, transparency, and reporters' safety. Privacy is also a core concept of any proper VDP and should be emphasized, as is explained in documents from the US Cybersecurity & Infrastructure Security Agency:

    How should my agency treat vulnerability reports from anonymous sources?
    These reports should be treated the same as all other reports: like a gift. Knowing the source of a report can be a real benefit because it allows for rapport to develop. However, if the person who submits a report isn't known, the claim should simply be evaluated on its merits -- like every other report.

    When bug bounty platforms are your only security communication channel, such privacy can not be appropriately granted to vulnerability reporters.

    In fact, CISA published a complete template for Vulnerability Disclosure Policy that emphasizes those openness and transparency concepts. The document is meant to be a regulatory requirement for government agencies, but it can be used as a basis to write the VDP of any company.

    At GitGuardian, we are not against bug bounty programs, as we think they can be a great addition to a company's security policy. However, it is of prime importance to understand the limits and blind spots created by those platforms.

    Most importantly, bug bounty programs must complement -- not replace -- a public Vulnerability Disclosure Policy. Private, invitation-only programs create insurmountable barriers for new researchers and should never be the sole disclosure channel. Companies should maintain accessible public VDPs alongside any BBP, with clear escalation paths that allow critical reports to bypass platform restrictions when necessary. Direct reports to security@ should remain direct -- triaged by internal teams who understand the full context of their infrastructure, not filtered through external platform scopes that may dismiss legitimate threats on technicalities.

    Especially, if you manage a bug bounty program, make sure to include leaked credentials in its scope. Credentials-based attacks have become the number one cyber threat in the modern world, so those incidents should not be disregarded. Asking and verifying the source of the leaks will allow better investigation of the leak issue while reducing the risk of buying stolen credentials from the black market.

    To conclude, whatever communication channel you choose for your vulnerability reports, make sure to promote it and make it as visible as possible, for example, with an RFC 9116 security.txt file. There is nothing worse than a communication channel no one knows about.

    GitGuardian Interactive Demo

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:54

    ↗

    When running code reviews with local LLMs, a single model can either hallucinate non-existent bugs or generate generic advice you end up ignoring. To make local AI code review more useful, I built a closed Reviewer vs. Verifier loop for local Ollama workflows. The...

    When running code reviews with local LLMs, a single model can either hallucinate non-existent bugs or generate generic advice you end up ignoring.

    To make local AI code review more useful, I built a closed Reviewer vs. Verifier loop for local Ollama workflows.

    The Architecture: Two Local Agents, One Loop

    Instead of trusting one model's output, the workflow splits the job into two roles:

    1. Agent 1 (Reviewer): Reads the git diff or file changes. It searches specifically for logical flaws, security vulnerabilities, edge cases, or missing unit tests.
    2. Agent 2 (Verifier): Takes the Reviewer's list of findings and actively challenges them. If a finding is weak or unsupported, the Verifier pushes it out of the action list. If it holds up, the next step stays visible.

    The goal is not to make the model "always right". The goal is to make weak claims easier to catch before you act on them.

    Why Local-First?

    Many agent workflows eventually ask you to move private workspace context into somebody else's control plane.

    I packaged this workflow into HAICHI, a desktop workspace for Windows and Linux that connects to local Ollama models and keeps the workflow state inspectable.

    Key features:

    • Local-first workflow: Run Reviewer and Verifier style loops around local models.
    • Visible evidence trail: Keep task, review, challenge, and result in one workspace instead of scattered chat tabs.
    • Scoped execution: Keep actions bounded to the workflow you explicitly run.
    • Practical limits: Control how much concurrent agent work runs on your machine.

    Try it on your own code

    HAICHI Personal is free to try.

    • Website: https://haichi.app
    • Supported OS: Windows 10/11, Linux (Ubuntu/Debian/Arch)

    If you're already using Ollama for real development work, test the Reviewer vs. Verifier loop on one change and let me know where it helps, where it is too noisy, and what your local setup looks like.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:47

    ↗

    A few months back I was running Sales Navigator searches for a client project — filtering down to "VP Sales, fintech, based in Italy or Spain" type lists — and the results were genuinely good. 60, 80 leads that actually matched. Then I hit the part nobody warns you about:...

    A few months back I was running Sales Navigator searches for a client project — filtering down to "VP Sales, fintech, based in Italy or Spain" type lists — and the results were genuinely good. 60, 80 leads that actually matched. Then I hit the part nobody warns you about: there's no button on that page that says "save this."

    So I did what everyone does. Opened a spreadsheet, alt-tabbed back and forth, typed names and job titles by hand. Around profile 40 I gave up and went looking for a better way. This is what I found, roughly in the order I found it, including the tool I ended up building because none of the existing options quite fit what I needed.

    First: the export LinkedIn actually gives you

    LinkedIn has a real, built-in data export, and most people don't realize how narrow it is. It's under your profile photo → Settings & Privacy → Data Privacy → Get a copy of your data. From there you either tick specific categories (that email usually lands within minutes) or request the full archive, which takes closer to a day and sometimes arrives in two batches. Either way you get a download link that expires after 72 hours — and it's desktop only, the mobile app won't let you request one.

    What you get back is genuinely thorough: connections, messages, your own profile history, activity, even the ad-targeting data LinkedIn holds on you. A couple of quirks worth knowing before you rely on it: some connections' email addresses will just be missing, because sharing an email on download is something each person opts into individually, and you won't get a list of who viewed your profile or any "People You May Know" data. If you're in the EU, EEA, or Switzerland, LinkedIn also runs a separate API for pulling your data on a schedule rather than as a one-off request.

    Here's what this export is not built for, though: it has no idea what you searched for yesterday. It's an archive of your own account, not a way to capture a live search. Run a Sales Navigator query and pull 80 leads, and this tool won't touch them — those results aren't "your data," they're a page LinkedIn rendered for you a minute ago. If you want a yearly backup of your own connections and messages, or you're about to deactivate and want a copy first, start here and you're done. If you're trying to get a search result out of the browser, keep reading.

    Second: the manual way, which is what most people actually do

    Copy the name. Copy the title. Copy the company. Paste into a column. Repeat.

    It's fine for five profiles before a call. It's genuinely miserable past fifteen — you lose track of who you've already copied, columns drift out of alignment, and an hour later you've got a spreadsheet that looks like it survived a fall down some stairs. I don't think anyone picks this method so much as ends up in it by default, since it's the only option that needs zero setup.

    If you only do this occasionally, don't overthink it. A blank spreadsheet and fifteen minutes beats installing anything. It only turns into a real problem once you're doing it every week.

    Third: browser extensions, which read what's already on the page

    Once you're pulling data regularly, the next step people reach for is a browser extension, and it's worth understanding what these actually do under the hood — "LinkedIn tool" covers a wide range of behavior, and the range matters.

    The category I trust reads the DOM of the page you're already looking at, the same rendered HTML your browser downloaded to show you the search results, and turns the visible fields into rows in a file. It doesn't call a private API, doesn't need anything beyond the session you're already logged into, and doesn't do anything you didn't ask it to.

    The category I don't trust is the one that also automates actions on your behalf: auto-sending connection requests, auto-messaging, "warming up" a profile with likes. That's a fundamentally different product, and it's the kind of behavior that gets accounts restricted, because LinkedIn's abuse detection is watching for exactly that pattern — a lot of actions, very fast, in a rhythm no human clicks in. Reading a page you're already viewing and writing what you see to a CSV is a much smaller ask than pretending to be a human sending 200 connection requests an hour.

    What I ended up building

    I looked for something that just read search results — Profile, Company, Jobs, and Posts on regular LinkedIn, plus Lead and Account search on Sales Navigator — and either couldn't find it or found it bundled with a dozen automation features I didn't want anywhere near my account. So I built the Mastros LinkedIn & Sales Navigator exporter.

    It's read-only, on purpose. You run a search, the extension recognizes the page and shows you every field it found before you export anything, so you know what you're getting instead of finding out after. Set a limit, hit export, and it drops a CSV, JSON, or JSONL file. CSV opens straight in Excel or Sheets and imports cleanly into a CRM — something like:

    full_name,job_title,company,location
    Marco Bianchi,Head of Partnerships,Nordic Robotics,Turin
    Sara Klein,VP Marketing,Fjord & Co,Copenhagen
    Tomás Silva,Founder,Casa Verde,Lisbon
    

    A few decisions I made on purpose, mostly because they were the things that annoyed me about other tools:

    • Nothing leaves your machine. Extraction runs in your browser; the data never touches a Mastros server. We don't see the names or profiles you save, only that a save happened.
    • No email guessing. It doesn't derive or buy anyone's email address. What's on the page is what you get.
    • Only new records. Turn this on and it skips anything you've already exported, so re-running a search doesn't just duplicate last week's file.
    • Safety pacing. There's an hourly cap that's separate from your monthly plan quota, specifically so a big export doesn't turn into the kind of rapid-fire activity that flags an account. Same reasoning as the automation point above: reading slower, on purpose.
    • No actions, ever. It doesn't send invites, messages, or InMails, and it doesn't like, follow, or apply to anything. You trigger every export yourself; it never acts on your behalf.

    It's free for 250 records a month across all six search types, no card required. Pro is $9/month for 10,000 records with rollover on unused ones, and Scale is $18/month with no record cap from our side — LinkedIn's own rate limits still apply, because claiming otherwise would just be a lie.

    Picking one

    • Backing up your own connections and messages, or prepping to deactivate → LinkedIn's own export. It's free, official, and already good at this.
    • A handful of profiles before a call, once in a while → copy-paste. Don't install anything for five rows.
    • Recurring prospecting, recruiting pipelines, or keeping a CRM fed from live searches → an extension built specifically to read search results, with pacing and dedup baked in. That's the gap the LinkedIn exporter is built to fill.

    One last thing, easy to skip past: whichever method you use, the data is still about real people who didn't sign up to end up in your spreadsheet. Export what you're actually allowed to see, follow LinkedIn's terms, and don't turn a clean CSV into a cold-email blast nobody asked for. That's not a legal disclaimer, it's just the difference between doing research and being the reason someone locks down their privacy settings next week.

    If you want to try it: it's a free Chrome install, 250 records a month, no card needed.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:43

    ↗

    After working on enterprise applications and distributed microservices, I have realized that the biggest challenges rarely come from writing business logic. They come from handling production traffic, failures, concurrency, and unexpected edge cases. Here are seven lessons...

    After working on enterprise applications and distributed microservices, I have realized that the biggest challenges rarely come from writing business logic. They come from handling production traffic, failures, concurrency, and unexpected edge cases.

    Here are seven lessons that every Spring Boot developer should know before calling themselves a senior engineer.

    1. Never Assume an API Will Be Called Only Once

    One of the most common mistakes is assuming a client sends exactly one request.

    In reality:

    • Users refresh the page.
    • Mobile apps retry automatically.
    • API gateways retry requests.
    • Kafka consumers may reprocess events.
    • Network failures cause duplicate submissions.

    If your endpoint creates an order, payment, or booking every time it receives a request, duplicates are almost guaranteed.

    Better Approach

    Design APIs to be idempotent.

    For example:

    • Use an Idempotency-Key.
    • Store processed request IDs.
    • Ignore duplicate requests safely.

    Production systems should always expect duplicate requests.

    2. Database Transactions Are Not Enough

    Many developers believe this solves everything:

    @Transactional
    public void createOrder() {
        ...
    }
    

    It doesn't.

    A transaction protects changes inside a single database.

    It does not protect:

    • Kafka publishing
    • Email sending
    • External REST APIs
    • Redis updates
    • File uploads

    If your database commits successfully but Kafka publishing fails, your system is already inconsistent.

    Better Approach

    Use patterns such as:

    • Transactional Outbox
    • Saga Pattern
    • Event-driven architecture
    • Retry with dead-letter queues

    3. Don't Trust External APIs

    Every external service will eventually fail.

    Your payment provider.

    Your authentication service.

    Your notification service.

    Even your own internal microservices.

    Never assume another service is always available.

    Add Protection

    • Timeouts
    • Retries
    • Circuit Breakers
    • Fallback logic
    • Monitoring

    Failing fast is usually better than waiting forever.

    4. Logging Is More Valuable Than You Think

    When production goes down, nobody asks:

    "Was the code clean?"

    Everyone asks:

    "What happened?"

    Poor logging turns a five-minute issue into a five-hour investigation.

    Good Logs Include

    • Correlation ID
    • Request ID
    • User ID (where appropriate)
    • Service name
    • Execution time
    • Error details

    Avoid logging entire request bodies or sensitive information.

    Logs should help you debug—not create new security problems.

    5. Performance Problems Usually Start in the Database

    Most slow APIs aren't caused by Java.

    They're caused by:

    • Missing indexes
    • N+1 queries
    • Loading unnecessary data
    • Multiple database calls inside loops

    Before optimizing Java code:

    • Check SQL execution plans.
    • Measure database latency.
    • Cache frequently used data.
    • Fetch only what you need.

    Always measure before optimizing.

    6. Handle Concurrency Explicitly

    Concurrency bugs are among the hardest to reproduce.

    Imagine two requests arriving at exactly the same time:

    Request A
    Request B
    
    Both check:
    Balance = ₹100
    
    Both withdraw ₹100
    
    Final Balance = -₹100
    

    Everything worked correctly from each request's perspective.

    Together, they corrupted the data.

    Solutions

    • Optimistic Locking
    • Pessimistic Locking
    • Distributed Locks
    • Atomic database updates
    • Idempotent operations

    Concurrency isn't a rare edge case.

    It's a daily production reality.

    7. Monitoring Is Part of the Application

    If you can't observe your application, you can't operate it.

    Every production service should expose:

    • Health checks
    • Metrics
    • Request latency
    • Error rates
    • JVM metrics
    • Database latency
    • Kafka consumer lag

    Modern observability tools include:

    • Micrometer
    • Prometheus
    • Grafana
    • OpenTelemetry
    • ELK Stack

    The best production incidents are the ones users never notice because your monitoring detected them first.

    Final Thoughts

    Being a senior Spring Boot developer isn't about memorizing annotations or frameworks.

    It's about designing systems that continue to work when networks fail, traffic spikes, duplicate requests arrive, and dependencies become unavailable.

    Production engineering is less about writing more code and more about building software that remains reliable under real world conditions.

    If you're just starting your backend journey, focus on these concepts early. They'll have a much bigger impact on your career than learning another framework.

    Java #SpringBoot #Microservices #Backend #SoftwareEngineering #SystemDesign #DistributedSystems #Kafka #Programming #DevOps

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:35

    ↗

    Una discusión sobre un archivo digital casi nunca se pierde por lo que el archivo dice. Se pierde una pregunta antes: ¿Cómo sabemos que ese es el archivo que usted recibió, y no el que editó anoche? Si la respuesta es "confíe en mí", ya perdiste. Y da igual cuánta razón...

    Una discusión sobre un archivo digital casi nunca se pierde por lo que el archivo
    dice. Se pierde una pregunta antes:

    ¿Cómo sabemos que ese es el archivo que usted recibió, y no el que editó anoche?

    Si la respuesta es "confíe en mí", ya perdiste. Y da igual cuánta razón tengas en
    el fondo.

    Este problema no es exclusivo de un juzgado. Lo tiene el auditor que recibe un
    volcado de logs, el equipo que documenta un incidente, quien conserva la copia de
    un contrato firmado por correo. En todos los casos hace falta lo mismo: poder
    demostrar que un conjunto de bytes no cambió desde un momento determinado, y que
    lo demuestre alguien que no seas tú.

    Para eso escribí Tunjo: una
    herramienta en Rust que recorre un material en solo lectura, calcula su huella y
    firma un acta verificable por cualquiera.

    Por qué un árbol y no un hash

    Lo obvio sería concatenar todo y sacar un SHA-256. Funciona, y es inútil en la
    práctica.

    Cuando alguien discute un archivo —un correo concreto entre cuatro mil— con
    un hash único solo puedes ofrecer dos cosas: o entregas el conjunto completo para
    que se recalcule, o pides que te crean. La primera opción expone material que no
    tiene por qué exponerse; la segunda no es una prueba.

    Un árbol de Merkle resuelve exactamente eso. Cada archivo es una hoja, cada par
    de nodos se combina hacia arriba y queda una raíz. Para demostrar que una hoja
    pertenece a esa raíz basta con exhibir esa hoja y el camino de hashes hasta
    arriba: unos pocos kilobytes. El resto del conjunto no se toca.

    Dos detalles del árbol que no son opcionales:

    // Separación de dominio: una hoja nunca puede hacerse pasar por nodo interno.
    h.update([0x00]);          // hoja
    h.update([0x01]);          // nodo interno
    
    // Y la raíz ata el número de hojas.
    h.update([0x02]);
    h.update(n.to_be_bytes());
    

    Sin lo primero, un hash de hoja podría presentarse como si fuera un nodo del
    árbol. Sin lo segundo aparece la ambigüedad clásica de los árboles con número
    impar de hojas: dos conjuntos distintos pueden producir la misma raíz. Es un
    error viejo y conocido, y sigue apareciendo en implementaciones nuevas.

    La huella cubre el estado, no solo el contenido

    La hoja no es el hash del archivo: es el hash del elemento completo —ruta,
    tamaño, fecha, estado y hash del contenido—.

    La diferencia importa. Si la huella fuera solo del contenido, mover un archivo de
    carpeta, renombrarlo o sustituirlo por un enlace que apunta al mismo contenido
    dejaría la raíz intacta. Y esos tres movimientos cambian lo que el conjunto
    significa: dónde estaba un documento es parte del hecho que se documenta, no un
    detalle de presentación.

    Firmar para dentro de diez años

    El acta se firma con la firma triple-híbrida de
    Quipu: Ed25519 + ML-DSA-87 (FIPS 204)

    • SLH-DSA-SHA2-256s (FIPS 205), y las tres deben validar.

    No es coleccionismo de algoritmos. Es que la vida útil de esto no se mide en
    meses: un expediente puede tardar años en resolverse, y la firma tiene que seguir
    verificándose al final. Las tres piezas fallan por motivos distintos —Ed25519
    frente a un ordenador cuántico; ML-DSA por ser reciente y basada en retículos;
    SLH-DSA solo si se rompe la función hash— y hacen falta las tres a la vez para
    que el sello valga. Que caiga una no tumba el acta.

    El coste es honesto: la firma pesa unos 34 KB. Para sellar un conjunto de
    archivos, es ruido.

    Verificar la firma no basta

    Este es el error que más fácil se comete al implementar algo así. La firma cubre
    el JSON completo del acta, incluida la raíz de integridad. Si al verificar te
    limitas a comprobar la firma, das por buena una raíz que nadie recalculó: alguien
    con la clave podría firmar un acta cuya raíz no corresponde a los elementos que
    lista, y pasaría el control.

    Por eso la verificación recalcula el árbol siempre, y solo después mira la firma.
    Hay una prueba dedicada a ese caso exacto: firma auténtica sobre raíz mentirosa
    debe fallar.

    $ tunjo verificar acta.json --origen ./evidencia
    SELLO VÁLIDO
      contenido:  4 elementos, 3 con contenido verificable
      raíz:       d9f6f68f591c6af087838dc27049a4194ab70525c350b79ec22446f9c12f9e33
    
    1 DISCREPANCIA(S) contra evidencia:
    
    ALTERADO   adjuntos/c.pdf
               acta: 3fdbaf9c795e22f14e16974c37b62ed381b9c8c4ac7bcbe1a01f13d08ec46643
               disco: 9af5d94042eafbf2c335aa874085b263ff0201aee5e5033fd4c432e92de0093d
    

    Ante la ausencia de un dato, ruido

    Una herramienta de integridad que disimula sus fallos es peor que no tenerla,
    porque produce confianza sin respaldo. Tres decisiones al respecto:

    Un archivo ilegible detiene el sellado. No se salta en silencio. Si de verdad
    es ilegible, hay que pedirlo explícitamente y entonces el acta lo registra como
    error: de ese elemento consta que existía y que la lectura falló, y nada más.

    Del reloj se dice la verdad. El acta pide declarar cómo se contrastó con una
    fuente externa. Si no se declara, escribe "NO VERIFICADO" en lugar de callarlo.
    Sin sello de tiempo de un tercero, esto prueba orden relativo, no fecha cierta —y
    también lo dice.

    Los enlaces simbólicos no se siguen. Se registra a dónde apuntan. Seguirlos
    sacaría la adquisición del material que se recibió.

    Lo que deliberadamente no hace

    No detecta intrusiones, no atribuye autoría y no concluye nada. Podría añadirle
    heurísticas que dijeran "aquí hubo un ataque", y sería un error: quien firma un
    informe tiene que poder defender cada afirmación línea por línea, y nadie defiende
    una heurística que no escribió. Cuando esa afirmación se cae, arrastra al resto
    del informe.

    Tampoco prueba el pasado. Acredita desde el instante de la adquisición: si el
    material ya venía alterado, el sello certifica fielmente material alterado. Está
    escrito en el acta que genera, no en la letra pequeña.

    El verificador es público, y no por generosidad

    Si el único que puede comprobar un sello es quien lo emitió, no es una prueba: es
    una afirmación con formato técnico. Por eso el verificador es software libre y su
    código está publicado.

    Lo comprobé de la única forma que vale: cloné el repositorio público en una
    máquina limpia, lo compilé desde cero y con ese binario verifiqué un acta
    sellada por otro. Válida. Después alteré un byte de un adjunto y el mismo binario
    señaló ese archivo y solo ese.

    git clone https://github.com/isazajuancarlos/tunjo
    cd tunjo && cargo build --release
    ./target/release/tunjo verificar acta.json --origen ./evidencia
    

    Rust, AGPL-3.0, y las pruebas incluyen una simulación de 240 contrastes: se altera
    un byte de cada uno de 120 archivos y se exige que señale ese y solo ese, y que al
    restaurarlo no queden falsos positivos. Detectar es fácil; discriminar es el
    trabajo.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:27

    ↗

    Canonical URL: https://blog.1001020.xyz/ Suggested cover image: use a recent image from https://blog.1001020.xyz/gallery I have been building a small publishing system called 1001020, a serverless blog and AI gallery running on Cloudflare Workers. The live site is here:...

    Canonical URL: https://blog.1001020.xyz/

    Suggested cover image: use a recent image from https://blog.1001020.xyz/gallery

    I have been building a small publishing system called 1001020, a serverless blog and AI gallery running on Cloudflare Workers.

    The live site is here: 1001020 — AI Gallery & Cloudflare Experiments

    The goal was not to build another static blog generator. I wanted something that could publish articles, serve an image gallery, manage uploaded assets, expose structured sitemaps, and stay operational without a traditional server.

    The basic architecture

    The whole public site runs on Cloudflare Workers. Articles, settings, comments, gallery metadata, and telemetry live in Cloudflare KV. Managed images are stored in R2 and served through a dedicated image domain.

    The main pieces are:

    • Cloudflare Workers for request routing and rendering
    • Cloudflare KV for article and site metadata
    • Cloudflare R2 for managed image uploads
    • A theme system for different frontend layouts
    • XML sitemap and image sitemap generation
    • A small local AI drafting tool for preparing and publishing content

    The gallery is a first-class part of the site, not just a media folder. You can browse it here: AI Gallery on 1001020

    Why Workers instead of a conventional backend?

    For this project, Workers are a good fit because the workload is mostly request routing, HTML generation, metadata reads, and small API writes. A conventional server would work, but it would add deployment and maintenance overhead that I did not need.

    Cloudflare Workers also make it easy to keep the app close to the edge while still handling dynamic behavior. The blog can render pages server-side, expose APIs, and support admin operations without a separate Node or container deployment.

    KV as the content store

    The project stores persistent content in KV using explicit keys for articles, gallery records, settings, telemetry, comments, newsletter subscribers, and other small datasets.

    This shape works well for a personal publishing system because the access pattern is simple:

    • read the article index
    • read individual article records
    • write admin updates
    • render HTML or markdown responses
    • regenerate sitemap output from current content

    The main tradeoff is that KV is not a relational database. I keep data models small and explicit, and avoid pretending it can do arbitrary query workloads.

    R2 for images

    Images are uploaded as managed assets and served from R2. Article content can reference those managed image URLs, and the system tracks image references so unused assets can be identified and cleaned up.

    That part matters because image-heavy blogs tend to accumulate stale files quickly. Treating image references as part of the content model keeps the gallery and article system easier to maintain.

    SEO basics that are built in

    The site now ships with the boring but important search plumbing:

    • sitemap.xml
    • image-sitemap.xml
    • robots.txt
    • server-rendered article content
    • image dimensions for layout stability
    • canonical article URLs
    • Google Search Console verification token injection through the admin settings page

    One article that explains part of the agent workflow direction is here: Agent Harness Loop and Graph Engineering

    What I learned

    The biggest lesson is that a serverless blog should not be treated as a toy static page. Once publishing, images, metadata, admin operations, analytics, and sitemaps enter the picture, the system starts to look like a small CMS.

    Cloudflare Workers can handle that shape well, but only if the storage model stays simple and the routes remain deliberate.

    For 1001020, the result is a compact publishing stack that can run globally without a traditional backend server:

    • live site: https://blog.1001020.xyz/
    • AI gallery: https://blog.1001020.xyz/gallery
    • example article: https://blog.1001020.xyz/article/agent-harness-loop-graph-engineering

    I am still iterating on the publishing workflow, but the core system is now stable enough to share.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:09

    ↗

    Half the market is arguing about whether RAG or a semantic layer is the right foundation for enterprise AI. They are not competing. They answer different questions, and most teams need both. Two shapes of question Every question an agent receives breaks into one of two forms:...

    Half the market is arguing about whether RAG or a semantic layer is the right foundation for enterprise AI.

    They are not competing. They answer different questions, and most teams need both.

    Two shapes of question

    Every question an agent receives breaks into one of two forms:

    • "What did we say about X?" — lives in contracts, policies, tickets, docs. Unstructured. RAG was built for this.
    • "What is true about X?" — lives in your warehouse and governed metrics. Structured. A semantic layer was built for this.

    Treating them as rivals is how teams end up with a system that can quote the pricing policy but cannot tell you this quarter's realised price.

    Where each one breaks

    RAG Semantic layer
    Good at Retrieving relevant prose Resolving definitions and joins
    Fails on Aggregation, math, current state Anything not modelled as data
    Permissions Flattened at ingest, rebuilt at query time Compiled per person, per query
    Answer stability Varies with retrieval ranking Identical by construction
    Audit story Cites a chunk Reproduces the exact SQL

    The permissions row is the one that ends pilots. A retrieval index that ingested everything has, by construction, assembled your most sensitive object — and reconstructing entitlement at query time is guesswork.

    The layer that actually decides

    Neither a document chunk nor a metric definition is worth much until something compiles it into a governed query and runs it.

    That is the piece most architectures are missing: intent → context resolution → constrained planning → governed execution. RAG can feed the first step. It cannot perform the last three.

    Point an agent at raw tables and the best models score in the low teens on real enterprise data. Give the same model compiled, governed context and it clears the high nineties. The retrieval quality was never the bottleneck.

    The full breakdown — the precise division of labour, why hybrid architectures win, and how compile-time governance closes the gap RAG cannot — is here:

    👉 RAG vs. Semantic Layer: Why AI Needs Deterministic Governance

    Originally published at colrows.com/blogs/rag-vs-semantic-layer

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:09

    ↗

    Industrializing the disassembly of an undocumented processor from a raw binary is a complex task that can be broken down into four key steps: Verify that the binary does not belong to a known processor. Verify that the binary is not obfuscated, compressed, or encrypted code...

    Industrializing the disassembly of an undocumented processor from a raw binary is a complex task that can be broken down into four key steps:

    1. Verify that the binary does not belong to a known processor.
    2. Verify that the binary is not obfuscated, compressed, or encrypted code for a known processor.
    3. Build an undocumented processor generator.
    4. Create the analysis pipeline and custom disassembler generation process.

    For the first phase of this project, the goal is to build dedicated, lightweight disassemblers—since, for bare-metal binaries, tools like Ghidra require manual processor target selection before analysis can begin.

    1. Why Build a Custom Disassembler?

    To determine whether a binary was compiled for a specific architecture, the strategy consists of disassembling the binary (both statically and dynamically) against candidate instruction sets until:

    • One or more bytes fail to match any valid instruction for that architecture, allowing us to rule it out.
    • The disassembly succeeds completely. (Note: a successful disassembly does not guarantee that the binary was originally intended for that CPU; control flow validity must also be verified).

    Static disassembly is the first line of defense. However, if it fails due to obfuscation, compression, or encryption, we must escalate to dynamic execution and analysis.

    Only after systematically eliminating all known architectures can we confidently conclude that we are dealing with a custom or undocumented processor.

    2. How to Build Your Custom Disassembler

    Before deploying heavy machinery for undocumented processors, the logical first step was to check against known architectures.

    Approach 1: Ghidra and SLAgh

    Ghidra relies on the SLAgh specification language and maintains an extensive library of processor definitions. The original plan was to leverage its API to extract a normalized opcode mapping table.

    However, after several attempts, Ghidra proved unsuitable for this specific pipeline for two reasons:

    1. Operand Type Loss: Detailed metadata regarding operand types is lost or abstract during Ghidra's generic disassembly phase.
    2. Lack of Specification Standardization: Across different processor modules, .slaspec files are not uniformly structured.

    Ghidra is a remarkable tool, and its underlying codebase is a work of art. But when required metadata is missing from the .slaspec or .pspec definitions, it must be added manually. At that point, implementing a dedicated, lightweight disassembler becomes a far more practical alternative.

    Approach 2: Native 8051 Disassemblers (dis51)

    The second attempt involved generating a raw .HEX file containing all possible byte combinations and passing it through dis51.

    This approach failed because dis51 is an execution-tracing disassembler: it follows control flow rather than performing linear sweeping. If a JMP instruction branches backward, any bytes immediately following the jump that are not reached by other execution paths are categorized as raw data blocks. To effectively use dis51, one cannot simply feed it a linear array of opcodes; it expects a valid, structured program flow.

    Approach 3: LLM-Assisted Table Normalization (Successful)

    The winning strategy was prompting Gemini to generate the normalized instruction mapping table. After refining the prompt, the model generated a Python script containing the full opcode mapping alongside a processor-specific lookup table for Special Function Registers (SFR).

    Using this generated table, writing the functional static disassembler took under an hour. It also laid the foundation for the dynamic disassembly simulator. While the table contained a few minor bugs, the time saved was substantial.

    Although this approach is not fully generic out-of-the-box, it is straightforward. The core structure of the disassembler and execution loop remains virtually identical when porting to other architectures. Furthermore, building small, single-purpose utilities makes parallelizing multi-architecture scanning trivial.

    3. Epilogue

    This experiment provides a clear demonstration of how Large Language Models (LLMs) can accelerate lower-level systems development and reverse engineering tasks by handling structural boilerplate without sacrificing control over execution logic.

    4. Disassembler Python Code

    
    python
    import re
    
    INSTRUCTION_TABLE = {
        # -------------------------------------------------------------------------
        # 1 BYTE INSTRUCTIONS
        # -------------------------------------------------------------------------
        0x00: (1, "NOP", "NOP"),
        0x03: (1, "RR A", "RR A"),
        0x04: (1, "INC A", "INC A"),
        0x06: (1, "INC @R0", "INC @R0"),
        0x07: (1, "INC @R1", "INC @R1"),
        0x08: (1, "INC R0", "INC register"),
        0x09: (1, "INC R1", "INC register"),
        0x0A: (1, "INC R2", "INC register"),
        0x0B: (1, "INC R3", "INC register"),
        0x0C: (1, "INC R4", "INC register"),
        0x0D: (1, "INC R5", "INC register"),
        0x0E: (1, "INC R6", "INC register"),
        0x0F: (1, "INC R7", "INC register"),
    
        0x13: (1, "RRC A", "RRC A"),
        0x14: (1, "DEC A", "DEC A"),
        0x16: (1, "DEC @R0", "DEC @R0"),
        0x17: (1, "DEC @R1", "DEC @R1"),
        0x18: (1, "DEC R0", "DEC register"),
        0x19: (1, "DEC R1", "DEC register"),
        0x1A: (1, "DEC R2", "DEC register"),
        0x1B: (1, "DEC R3", "DEC register"),
        0x1C: (1, "DEC R4", "DEC register"),
        0x1D: (1, "DEC R5", "DEC register"),
        0x1E: (1, "DEC R6", "DEC register"),
        0x1F: (1, "DEC R7", "DEC register"),
    
        0x22: (1, "RET", "RET"),
        0x23: (1, "RL A", "RL A"),
        0x26: (1, "ADD A, @R0", "ADD A, @R0"),
        0x27: (1, "ADD A, @R1", "ADD A, @R1"),
        0x28: (1, "ADD A, R0", "ADD A, register"),
        0x29: (1, "ADD A, R1", "ADD A, register"),
        0x2A: (1, "ADD A, R2", "ADD A, register"),
        0x2B: (1, "ADD A, R3", "ADD A, register"),
        0x2C: (1, "ADD A, R4", "ADD A, register"),
        0x2D: (1, "ADD A, R5", "ADD A, register"),
        0x2E: (1, "ADD A, R6", "ADD A, register"),
        0x2F: (1, "ADD A, R7", "ADD A, register"),
    
        0x32: (1, "RETI", "RETI"),
        0x33: (1, "RLC A", "RLC A"),
        0x36: (1, "ADDC A, @R0", "ADDC A, @R0"),
        0x37: (1, "ADDC A, @R1", "ADDC A, @R1"),
        0x38: (1, "ADDC A, R0", "ADDC A, register"),
        0x39: (1, "ADDC A, R1", "ADDC A, register"),
        0x3A: (1, "ADDC A, R2", "ADDC A, register"),
        0x3B: (1, "ADDC A, R3", "ADDC A, register"),
        0x3C: (1, "ADDC A, R4", "ADDC A, register"),
        0x3D: (1, "ADDC A, R5", "ADDC A, register"),
        0x3E: (1, "ADDC A, R6", "ADDC A, register"),
        0x3F: (1, "ADDC A, R7", "ADDC A, register"),
    
        0x46: (1, "ORL A, @R0", "ORL A, @R0"),
        0x47: (1, "ORL A, @R1", "ORL A, @R1"),
        0x48: (1, "ORL A, R0", "ORL A, register"),
        0x49: (1, "ORL A, R1", "ORL A, register"),
        0x4A: (1, "ORL A, R2", "ORL A, register"),
        0x4B: (1, "ORL A, R3", "ORL A, register"),
        0x4C: (1, "ORL A, R4", "ORL A, register"),
        0x4D: (1, "ORL A, R5", "ORL A, register"),
        0x4E: (1, "ORL A, R6", "ORL A, register"),
        0x4F: (1, "ORL A, R7", "ORL A, register"),
    
        0x56: (1, "ANL A, @R0", "ANL A, @R0"),
        0x57: (1, "ANL A, @R1", "ANL A, @R1"),
        0x58: (1, "ANL A, R0", "ANL A, register"),
        0x59: (1, "ANL A, R1", "ANL A, register"),
        0x5A: (1, "ANL A, R2", "ANL A, register"),
        0x5B: (1, "ANL A, R3", "ANL A, register"),
        0x5C: (1, "ANL A, R4", "ANL A, register"),
        0x5D: (1, "ANL A, R5", "ANL A, register"),
        0x5E: (1, "ANL A, R6", "ANL A, register"),
        0x5F: (1, "ANL A, R7", "ANL A, register"),
    
        0x66: (1, "XRL A, @R0", "XRL A, @R0"),
        0x67: (1, "XRL A, @R1", "XRL A, @R1"),
        0x68: (1, "XRL A, R0", "XRL A, register"),
        0x69: (1, "XRL A, R1", "XRL A, register"),
        0x6A: (1, "XRL A, R2", "XRL A, register"),
        0x6B: (1, "XRL A, R3", "XRL A, register"),
        0x6C: (1, "XRL A, R4", "XRL A, register"),
        0x6D: (1, "XRL A, R5", "XRL A, register"),
        0x6E: (1, "XRL A, R6", "XRL A, register"),
        0x6F: (1, "XRL A, R7", "XRL A, register"),
    
        0x73: (1, "JMP @A+DPTR", "JMP @A+DPTR"),
    
        0x83: (1, "MOVC A, @A+PC", "MOVC A, @A+PC"),
        0x84: (1, "DIV AB", "DIV AB"),
    
        0x93: (1, "MOVC A, @A+DPTR", "MOVC A, @A+DPTR"),
        0x96: (1, "SUBB A, @R0", "SUBB A, @R0"),
        0x97: (1, "SUBB A, @R1", "SUBB A, @R1"),
        0x98: (1, "SUBB A, R0", "SUBB A, register"),
        0x99: (1, "SUBB A, R1", "SUBB A, register"),
        0x9A: (1, "SUBB A, R2", "SUBB A, register"),
        0x9B: (1, "SUBB A, R3", "SUBB A, register"),
        0x9C: (1, "SUBB A, R4", "SUBB A, register"),
        0x9D: (1, "SUBB A, R5", "SUBB A, register"),
        0x9E: (1, "SUBB A, R6", "SUBB A, register"),
        0x9F: (1, "SUBB A, R7", "SUBB A, register"),
    
        0xA3: (1, "INC DPTR", "INC register"),
        0xA4: (1, "MUL AB", "MUL AB"),
    
        0xC3: (1, "CLR C", "CLR C"),
        0xC4: (1, "SWAP A", "SWAP A"),
        0xC6: (1, "XCH A, @R0", "XCH A, @R0"),
        0xC7: (1, "XCH A, @R1", "XCH A, @R1"),
        0xC8: (1, "XCH A, R0", "XCH A, register"),
        0xC9: (1, "XCH A, R1", "XCH A, register"),
        0xCA: (1, "XCH A, R2", "XCH A, register"),
        0xCB: (1, "XCH A, R3", "XCH A, register"),
        0xCC: (1, "XCH A, R4", "XCH A, register"),
        0xCD: (1, "XCH A, R5", "XCH A, register"),
        0xCE: (1, "XCH A, R6", "XCH A, register"),
        0xCF: (1, "XCH A, R7", "XCH A, register"),
    
        0xD3: (1, "SETB C", "SETB C"),
        0xD4: (1, "DA A", "DA A"),
        0xD6: (1, "XCHD A, @R0", "XCHD A, @R0"),
        0xD7: (1, "XCHD A, @R1", "XCHD A, @R1"),
    
        0xE4: (1, "CLR A", "CLR A"),
        0xE6: (1, "MOV A, @R0", "MOV A, @R0"),
        0xE7: (1, "MOV A, @R1", "MOV A, @R1"),
        0xE8: (1, "MOV A, R0", "MOV A, register"),
        0xE9: (1, "MOV A, R1", "MOV A, register"),
        0xEA: (1, "MOV A, R2", "MOV A, register"),
        0xEB: (1, "MOV A, R3", "MOV A, register"),
        0xEC: (1, "MOV A, R4", "MOV A, register"),
        0xED: (1, "MOV A, R5", "MOV A, register"),
        0xEE: (1, "MOV A, R6", "MOV A, register"),
        0xEF: (1, "MOV A, R7", "MOV A, register"),
    
        0xF4: (1, "CPL A", "CPL A"),
        0xF6: (1, "MOV @R0, A", "MOV @R0, A"),
        0xF7: (1, "MOV @R1, A", "MOV @R1, A"),
        0xF8: (1, "MOV R0, A", "MOV register, A"),
        0xF9: (1, "MOV R1, A", "MOV register, A"),
        0xFA: (1, "MOV R2, A", "MOV register, A"),
        0xFB: (1, "MOV R3, A", "MOV register, A"),
        0xFC: (1, "MOV R4, A", "MOV register, A"),
        0xFD: (1, "MOV R5, A", "MOV register, A"),
        0xFE: (1, "MOV R6, A", "MOV register, A"),
        0xFF: (1, "MOV R7, A", "MOV register, A"),
    
        # -------------------------------------------------------------------------
        # 2 BYTE INSTRUCTIONS
        # -------------------------------------------------------------------------
        0x05: (2, lambda b: f"INC {b[1]:02X}h", "INC direct"),
        0x15: (2, lambda b: f"DEC {b[1]:02X}h", "DEC direct"),
        0x24: (2, lambda b: f"ADD A, #{b[1]:02X}h", "ADD A, #data"),
        0x25: (2, lambda b: f"ADD A, {b[1]:02X}h", "ADD A, direct"),
        0x34: (2, lambda b: f"ADDC A, #{b[1]:02X}h", "ADDC A, #data"),
        0x35: (2, lambda b: f"ADDC A, {b[1]:02X}h", "ADDC A, direct"),
        0x40: (2, lambda b: f"JC {b[1]:02X}h", "JC offset"),
        0x44: (2, lambda b: f"ORL A, #{b[1]:02X}h", "ORL A, #data"),
        0x45: (2, lambda b: f"ORL A, {b[1]:02X}h", "ORL A, direct"),
        0x50: (2, lambda b: f"JNC {b[1]:02X}h", "JNC offset"),
        0x54: (2, lambda b: f"ANL A, #{b[1]:02X}h", "ANL A, #data"),
        0x55: (2, lambda b: f"ANL A, {b[1]:02X}h", "ANL A, direct"),
        0x60: (2, lambda b: f"JZ {b[1]:02X}h", "JZ offset"),
        0x64: (2, lambda b: f"XRL A, #{b[1]:02X}h", "XRL A, #data"),
        0x65: (2, lambda b: f"XRL A, {b[1]:02X}h", "XRL A, direct"),
        0x70: (2, lambda b: f"JNZ {b[1]:02X}h", "JNZ offset"),
        0x74: (2, lambda b: f"MOV A, #{b[1]:02X}h", "MOV A, #data"),
        0x76: (2, lambda b: f"MOV @R0, #{b[1]:02X}h", "MOV @R0, #data"),
        0x77: (2, lambda b: f"MOV @R1, #{b[1]:02X}h", "MOV @R1, #data"),
        0x78: (2, lambda b: f"MOV R0, #{b[1]:02X}h", "MOV register, #data"),
        0x79: (2, lambda b: f"MOV R1, #{b[1]:02X}h", "MOV register, #data"),
        0x7A: (2, lambda b: f"MOV R2, #{b[1]:02X}h", "MOV register, #data"),
        0x7B: (2, lambda b: f"MOV R3, #{b[1]:02X}h", "MOV register, #data"),
        0x7C: (2, lambda b: f"MOV R4, #{b[1]:02X}h", "MOV register, #data"),
        0x7D: (2, lambda b: f"MOV R5, #{b[1]:02X}h", "MOV register, #data"),
        0x7E: (2, lambda b: f"MOV R6, #{b[1]:02X}h", "MOV register, #data"),
        0x7F: (2, lambda b: f"MOV R7, #{b[1]:02X}h", "MOV register, #data"),
    
        0x80: (2, lambda b: f"SJMP {b[1]:02X}h", "SJMP offset"),
        0x82: (2, lambda b: f"ANL C, {b[1]:02X}h", "ANL C, bit"),
        0x86: (2, lambda b: f"MOV R0, {b[1]:02X}h", "MOV register, direct"),
        0x87: (2, lambda b: f"MOV R1, {b[1]:02X}h", "MOV register, direct"),
        0x88: (2, lambda b: f"MOV {b[1]:02X}h, R0", "MOV direct, register"),
        0x89: (2, lambda b: f"MOV {b[1]:02X}h, R1", "MOV direct, register"),
        0x8A: (2, lambda b: f"MOV {b[1]:02X}h, R2", "MOV direct, register"),
        0x8B: (2, lambda b: f"MOV {b[1]:02X}h, R3", "MOV direct, register"),
        0x8C: (2, lambda b: f"MOV {b[1]:02X}h, R4", "MOV direct, register"),
        0x8D: (2, lambda b: f"MOV {b[1]:02X}h, R5", "MOV direct, register"),
        0x8E: (2, lambda b: f"MOV {b[1]:02X}h, R6", "MOV direct, register"),
        0x8F: (2, lambda b: f"MOV {b[1]:02X}h, R7", "MOV direct, register"),
    
        0x92: (2, lambda b: f"MOV {b[1]:02X}h, C", "MOV bit, C"),
        0x94: (2, lambda b: f"SUBB A, #{b[1]:02X}h", "SUBB A, #data"),
        0x95: (2, lambda b: f"SUBB A, {b[1]:02X}h", "SUBB A, direct"),
    
        0xA0: (2, lambda b: f"ORL C, /{b[1]:02X}h", "ORL C, /bit"),
        0xA2: (2, lambda b: f"MOV C, {b[1]:02X}h", "MOV C, bit"),
        0xA5: (1, "RESERVED (0xA5)", "RESERVED"), # Unassigned Intel Opcode
        0xA6: (2, lambda b: f"MOV @R0, {b[1]:02X}h", "MOV @R0, direct"),
        0xA7: (2, lambda b: f"MOV @R1, {b[1]:02X}h", "MOV @R1, direct"),
        0xA8: (2, lambda b: f"MOV R0, {b[1]:02X}h", "MOV R0, direct"),
        0xA9: (2, lambda b: f"MOV R1, {b[1]:02X}h", "MOV R1, direct"),
        0xAA: (2, lambda b: f"MOV R2, {b[1]:02X}h", "MOV R2, direct"),
        0xAB: (2, lambda b: f"MOV R3, {b[1]:02X}h", "MOV R3, direct"),
        0xAC: (2, lambda b: f"MOV R4, {b[1]:02X}h", "MOV R4, direct"),
        0xAD: (2, lambda b: f"MOV R5, {b[1]:02X}h", "MOV R5, direct"),
        0xAE: (2, lambda b: f"MOV R6, {b[1]:02X}h", "MOV R6, direct"),
        0xAF: (2, lambda b: f"MOV R7, {b[1]:02X}h", "MOV R7, direct"),
    
        0xB0: (2, lambda b: f"ANL C, /{b[1]:02X}h", "ANL C, /bit"),
        0xB2: (2, lambda b: f"CPL {b[1]:02X}h", "CPL bit"),
        0xB3: (1, "CPL C", "CPL C"),
    
        0xC0: (2, lambda b: f"PUSH {b[1]:02X}h", "PUSH direct"),
        0xC2: (2, lambda b: f"CLR {b[1]:02X}h", "CLR bit"),
        0xC5: (2, lambda b: f"XCH A, {b[1]:02X}h", "XCH A, direct"),
    
        0xD0: (2, lambda b: f"POP {b[1]:02X}h", "POP direct"),
        0xD2: (2, lambda b: f"SETB {b[1]:02X}h", "SETB bit"),
        0xD8: (2, lambda b: f"DJNZ R0, {b[1]:02X}h", "DJNZ register, offset"),
        0xD9: (2, lambda b: f"DJNZ R1, {b[1]:02X}h", "DJNZ register, offset"),
        0xDA: (2, lambda b: f"DJNZ R2, {b[1]:02X}h", "DJNZ register, offset"),
        0xDB: (2, lambda b: f"DJNZ R3, {b[1]:02X}h", "DJNZ register, offset"),
        0xDC: (2, lambda b: f"DJNZ R4, {b[1]:02X}h", "DJNZ register, offset"),
        0xDD: (2, lambda b: f"DJNZ R5, {b[1]:02X}h", "DJNZ register, offset"),
        0xDE: (2, lambda b: f"DJNZ R6, {b[1]:02X}h", "DJNZ register, offset"),
        0xDF: (2, lambda b: f"DJNZ R7, {b[1]:02X}h", "DJNZ register, offset"),
    
        0xE0: (1, "MOVX A, @DPTR", "MOVX A, @DPTR"),
        0xE2: (1, "MOVX A, @R0", "MOVX A, @R0"),
        0xE3: (1, "MOVX A, @R1", "MOVX A, @R1"),
        0xE5: (2, lambda b: f"MOV A, {b[1]:02X}h", "MOV A, direct"),
    
        0xF0: (1, "MOVX @DPTR, A", "MOVX @DPTR, A"),
        0xF2: (1, "MOVX @R0, A", "MOVX @R0, A"),
        0xF3: (1, "MOVX @R1, A", "MOVX @R1, A"),
        0xF5: (2, lambda b: f"MOV {b[1]:02X}h, A", "MOV direct, A"),
    
        # -------------------------------------------------------------------------
        # 3 BYTE INSTRUCTIONS (16-bit addresses & 3-parameter instructions)
        # -------------------------------------------------------------------------
        0x02: (3, lambda b: f"LJMP {b[1]:02X}{b[2]:02X}h", "LJMP addr16"),
        0x10: (3, lambda b: f"JBC {b[1]:02X}h, {b[2]:02X}h", "JBC bit, offset"),
        0x12: (3, lambda b: f"LCALL {b[1]:02X}{b[2]:02X}h", "LCALL addr16"),
        0x20: (3, lambda b: f"JB {b[1]:02X}h, {b[2]:02X}h", "JB bit, offset"),
        0x30: (3, lambda b: f"JNB {b[1]:02X}h, {b[2]:02X}h", "JNB bit, offset"),
        0x42: (2, lambda b: f"ORL {b[1]:02X}h, A", "ORL direct, A"),
        0x43: (3, lambda b: f"ORL {b[1]:02X}h, #{b[2]:02X}h", "ORL direct, #data"),
        0x52: (2, lambda b: f"ANL {b[1]:02X}h, A", "ANL direct, A"),
        0x53: (3, lambda b: f"ANL {b[1]:02X}h, #{b[2]:02X}h", "ANL direct, #data"),
        0x62: (2, lambda b: f"XRL {b[1]:02X}h, A", "XRL direct, A"),
        0x63: (3, lambda b: f"XRL {b[1]:02X}h, #{b[2]:02X}h", "XRL direct, #data"),
        0x72: (2, lambda b: f"ORL C, {b[1]:02X}h", "ORL C, bit"),
        0x75: (3, lambda b: f"MOV {b[1]:02X}h, #{b[2]:02X}h", "MOV direct, #data"),
        0x85: (3, lambda b: f"MOV {b[2]:02X}h, {b[1]:02X}h", "MOV direct, direct"),
        0x90: (3, lambda b: f"MOV DPTR, #{b[1]:02X}{b[2]:02X}h", "MOV DPTR, #data16"),
        0xB4: (3, lambda b: f"CJNE A, #{b[1]:02X}h, {b[2]:02X}h", "CJNE A, #data, offset"),
        0xB5: (3, lambda b: f"CJNE A, {b[1]:02X}h, {b[2]:02X}h", "CJNE A, direct, offset"),
        0xB6: (3, lambda b: f"CJNE @R0, #{b[1]:02X}h, {b[2]:02X}h", "CJNE @R0, #data, offset"),
        0xB7: (3, lambda b: f"CJNE @R1, #{b[1]:02X}h, {b[2]:02X}h", "CJNE @R1, #data, offset"),
        0xB8: (3, lambda b: f"CJNE R0, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xB9: (3, lambda b: f"CJNE R1, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBA: (3, lambda b: f"CJNE R2, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBB: (3, lambda b: f"CJNE R3, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBC: (3, lambda b: f"CJNE R4, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBD: (3, lambda b: f"CJNE R5, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBE: (3, lambda b: f"CJNE R6, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xBF: (3, lambda b: f"CJNE R7, #{b[1]:02X}h, {b[2]:02X}h", "CJNE register, #data, offset"),
        0xD5: (3, lambda b: f"DJNZ {b[1]:02X}h, {b[2]:02X}h", "DJNZ direct, offset"),
    }
    
    sfr_dict = {
        0x80: "P0",     # Port 0
        0x81: "SP",     # Stack Pointer
        0x82: "DPL",    # Data Pointer Low
        0x83: "DPH",    # Data Pointer High
        0x87: "PCON",   # Power Control
        0x88: "TCON",   # Timer Control
        0x89: "TMOD",   # Timer Mode
        0x8A: "TL0",    # Timer 0 Low
        0x8B: "TL1",    # Timer 1 Low
        0x8C: "TH0",    # Timer 0 High
        0x8D: "TH1",    # Timer 1 High
        0x90: "P1",     # Port 1
        0x98: "SCON",   # Serial Control
        0x99: "SBUF",   # Serial Buffer
        0xA0: "P2",     # Port 2
        0xA8: "IE",     # Interrupt Enable
        0xB0: "P3",     # Port 3
        0xB8: "IP",     # Interrupt Priority
        0xD0: "PSW",    # Program Status Word
        0xE0: "ACC",    # Accumulator (A)
        0xF0: "B",      # B Register
    }
    
    def decode_bytes(byte_list):
        """Decodes a byte sequence according to the 8051 instruction set."""
        first_byte = byte_list[0]
    
        # Handle AJMP / ACALL instructions (Page addresses encoded in the upper opcode bits)
        if (first_byte & 0x1F) == 0x01:
            addr = ((first_byte & 0xE0) << 3) | byte_list[1]
            return 2, f"AJMP {addr:04X}h", "AJMP addr11"
        if (first_byte & 0x1F) == 0x11:
            addr = ((first_byte & 0xE0) << 3) | byte_list[1]
            return 2, f"ACALL {addr:04X}h", "ACALL addr11"
    
        if first_byte in INSTRUCTION_TABLE:
            length, asm, generic = INSTRUCTION_TABLE[first_byte]
            if callable(asm):
                asm = asm(byte_list)
            return length, asm, generic
    
        return len(byte_list), "UNKNOWN", "UNKNOWN"
    
    def int_to_hex_4(number):
        if not (0 <= number <= 65535):
            raise ValueError("Number must be between 0 and 65535")
        return format(number, '04x')
    
    def buffer_to_hex_text(buffer: bytes) -> str:
        return " ".join(f"{b:02X}" for b in buffer)
    
    def process_opcodes(buffer, reset_vector=0):
        rows = []
        idx = 0
        done = False
    
        while not done: 
            address = reset_vector + idx
            opcode = buffer[reset_vector + idx]
    
            if opcode in INSTRUCTION_TABLE:
                length, asm, generic = INSTRUCTION_TABLE[opcode]
                byte_vals = buffer[reset_vector + idx : reset_vector + idx + length]
                opcodes_str = buffer_to_hex_text(byte_vals)
    
                length, asm, generic = decode_bytes(byte_vals)
    
                rows.append({
                    "address": address,
                    "opcodes": opcodes_str,
                    "asm": asm,
                    "generic": generic
                })  
    
                idx += length
                if idx >= len(buffer):
                    done = True
            else:
                print(f"Opcode 0x{opcode:02X} not found in INSTRUCTION_TABLE")
                done = True
    
        return rows         
    
    if __name__ == "__main__":
        input_file = "./test1_8051.rom"
        with open(input_file, "rb") as fs:
            buffer = fs.read()
    
        result_rows = process_opcodes(buffer)
    
        line_format = "{:<10} {:<20} {:<30}"
        for r in result_rows:
            print(line_format.format(int_to_hex_4(r['address']), r['opcodes'], r['asm']))
    
  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:03

    ↗

    Data pipeline automation has moved from a technical aspiration to a business imperative. As organisations deploy more AI agents that need fresh, reliable data, the volume and complexity of data pipelines has grown beyond what manual management can sustain. AI-powered pipeline...

    Data pipeline automation has moved from a technical aspiration to a business imperative. As organisations deploy more AI agents that need fresh, reliable data, the volume and complexity of data pipelines has grown beyond what manual management can sustain. AI-powered pipeline automation — using AI to build, monitor, and repair data pipelines — is the emerging solution.

    Key Insight: AI-automated data pipelines reduce pipeline development time by 60%, decrease pipeline failures by 45%, and enable data teams to manage 3x more pipelines per engineer compared to manual approaches.

    The Pipeline Scaling Challenge

    The average enterprise now maintains 1,500+ data pipelines, according to research by Barracuda Networks, and this number is growing 25% annually as organisations add new data sources, new AI use cases, and new reporting requirements. Each pipeline has an average of 4.2 transformation steps, 2.1 data quality checks, and connects an average of 2.8 systems. The total pipeline infrastructure is complex, fragile, and increasingly beyond the capacity of manual management.

    The scaling challenge manifests in three ways. First, pipeline development backlog — the average data team has a 3-6 month backlog of pipeline requests from business users. Second, pipeline failures — the average enterprise experiences 15-20 pipeline failures per week, each requiring manual investigation and repair that consumes 30-40% of the data engineering team's capacity. Third, pipeline maintenance — as source systems change (schema updates, API modifications, deprecated fields), pipelines break silently and produce incorrect results until someone notices. This 'silent failure' problem is particularly dangerous because it erodes trust in data without anyone being aware that anything is wrong.

    The root cause of these challenges is that data pipelines have been built as static, manually-maintained infrastructure. A pipeline is coded, tested, and deployed. When the source or destination changes, a human must update the pipeline code. When the data quality check thresholds need adjustment, a human must modify the configuration. This manual approach cannot scale with the growing demand for data, and AI-powered automation is the solution.

    How AI Automates Pipeline Development

    AI-powered pipeline development uses LLMs to generate pipeline code from natural language specifications. Instead of a data engineer writing Apache Spark or SQL code to extract data from an API, transform it, and load it into a data warehouse, a data engineer describes the pipeline in natural language: 'Create a daily pipeline that extracts customer data from Salesforce, joins it with order data from the ERP, calculates customer lifetime value using the standard finance definition, and loads the result into the customer analytics table in Snowflake.' The AI generates the pipeline code, including error handling, retry logic, and data quality checks.

    The quality of AI-generated pipelines depends critically on two factors. First, the semantic layer — the AI must understand the business definitions ('customer lifetime value using the standard finance definition') to generate correct transformation logic. Without a semantic layer, the AI must guess at business definitions, producing pipelines that may technically work but produce incorrect business results. Second, MCP connectors — the AI must know what data sources are available and how to access them. MCP's standardised connector descriptions give the AI the information it needs to generate correct data access code without requiring the data engineer to specify connection details, authentication methods, and schema information.

    Organisations deploying AI-powered pipeline development report 60% reduction in pipeline development time. A pipeline that previously took a data engineer 2-3 days to build can be generated in 2-4 hours, including review and testing. More importantly, the generated pipelines follow consistent patterns and include comprehensive error handling that even experienced engineers sometimes omit. The result is not just faster development but more reliable pipelines that fail less often and are easier to maintain.

    AI-Powered Pipeline Monitoring and Self-Repair

    The most impactful application of AI in pipeline automation is not development but monitoring and self-repair. AI-powered pipeline monitoring goes beyond simple failure detection to understand the context and cause of pipeline issues. When a pipeline fails, the AI analyses the error message, examines the data at the point of failure, checks recent changes to source systems, and identifies the root cause. In many cases, the AI can then implement a repair automatically — adjusting a schema mapping when a source field is renamed, increasing a timeout when a source system is slow, or routing around a failed component.

    The self-repair capability works on a confidence-based model. For issues that the AI has high confidence in diagnosing and repairing (schema changes, timeout adjustments, retry scheduling), it implements the repair automatically and logs the action for human review. For issues with lower confidence (data quality anomalies, unexpected data patterns, potential security issues), it alerts the data engineering team with a detailed diagnosis and recommended repair, reducing investigation time from hours to minutes. For complex, novel issues, it provides full diagnostic context to help human engineers resolve the problem faster.

    Organisations deploying AI-powered pipeline monitoring and self-repair report 45% reduction in pipeline failures and 70% reduction in mean time to repair. The combination means that data teams spend dramatically less time on pipeline firefighting and more time on building new capabilities. A data engineering manager at a global retailer reported that after deploying AI-powered pipeline automation, their team of 8 engineers was managing the same pipeline volume that previously required 20 engineers — a 2.5x productivity improvement that allowed the team to take on new strategic initiatives without hiring.

    Integrating Pipeline Automation with Conversational BI

    Pipeline automation and conversational BI have a synergistic relationship. When a business user asks a question through conversational BI and the AI cannot answer because a data pipeline has failed, the system should not simply return an error — it should explain what data is unavailable, when it is expected to be restored, and offer alternative data sources if available. This requires integration between the pipeline monitoring system and the conversational BI platform.

    The integration works through MCP connectors. The pipeline monitoring system exposes its status through MCP connectors, allowing the conversational BI platform to query pipeline health as part of answering user questions. When the AI agent receives a query that requires data from a failed pipeline, it checks pipeline status, explains the situation to the user, and provides the most recent available data with appropriate caveats. This transparency builds trust and prevents the frustration of unexplained data unavailability. Beehive Strategy's platform provides this integration natively — MCP connectors provide unified data access, the semantic layer ensures consistent definitions, and the conversational interface delivers transparent, context-aware answers even when underlying data systems have issues.

    Practical Implementation Guide

    Organisations should implement AI-powered pipeline automation in three phases. Phase one focuses on pipeline monitoring and alerting — deploy AI-powered monitoring on existing pipelines to detect failures, diagnose root causes, and alert data engineers with actionable information. This phase delivers immediate value by reducing investigation time and provides the operational visibility needed to prioritise subsequent automation investments. Phase two adds self-repair capabilities for the most common failure modes — typically schema changes, timeout issues, and data format changes. Phase three implements AI-powered pipeline development, where engineers specify pipelines in natural language and AI generates the implementation.

    The key success factor is building the semantic layer and MCP connector infrastructure that AI-powered pipeline automation depends on. Without a semantic layer, AI-generated pipelines may produce technically correct but business-incorrect results. Without MCP connectors, the AI cannot discover available data sources or generate correct access code. Investing in these foundational capabilities — which Beehive Strategy's platform provides — ensures that pipeline automation delivers reliable, business-accurate data rather than fast but untrustworthy automation.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:03

    ↗

    The LLM framework landscape has matured significantly in 2026. With enterprise adoption accelerating, the right framework choice determines development velocity, production reliability, and total cost of ownership for AI applications. This guide ranks the 8 best LLM...

    The LLM framework landscape has matured significantly in 2026. With enterprise adoption accelerating, the right framework choice determines development velocity, production reliability, and total cost of ownership for AI applications. This guide ranks the 8 best LLM frameworks based on enterprise readiness, ecosystem maturity, multi-model support, and production deployment capabilities.

    TL;DR: Ranked 8 LLM frameworks: LangChain leads in ecosystem breadth, LlamaIndex for RAG workflows, Semantic Kernel for Microsoft shops, CrewAI for multi-agent systems, and AutoGen for conversational agents. Enterprise choice depends on your model strategy, team skills, and use case complexity.

    Evaluation Criteria

    We assessed frameworks across five dimensions critical for enterprise use: production readiness (observability, error handling, deployment patterns), multi-model support (model-agnostic design, provider switching), RAG capabilities (indexing strategies, retrieval optimization), agent framework quality (tool use, reasoning chains, memory), and ecosystem maturity (integrations, community, documentation, enterprise support).

    • Production readiness: Observability, error handling, deployment patterns, monitoring
    • Multi-model support: Model-agnostic design, easy provider switching
    • RAG capabilities: Indexing strategies, retrieval optimization, hybrid search
    • Agent framework: Tool use, reasoning chains, memory, multi-agent coordination
    • Ecosystem: Integrations, community size, documentation quality, support

    Ranking: The 8 Best LLM Frameworks

    1. ### 1. LangChain

    LangChain remains the most widely-adopted LLM framework with the largest ecosystem of integrations (500+ components). Its modular architecture allows developers to compose custom chains from pre-built components. The 2026 LangGraph extension provides stateful agent orchestration with persistent memory, retry logic, and human-in-the-loop capabilities. LangSmith provides enterprise-grade observability and evaluation.

     * **Best for:** Teams wanting maximum flexibility and ecosystem breadth
     * **Pros:** Largest ecosystem, LangGraph for complex agents, LangSmith observability, model-agnostic
     * **Cons:** Abstraction can obscure behavior, version churn, steeper learning curve
    
    1. ### 2. LlamaIndex

    LlamaIndex has established itself as the premier framework for RAG (Retrieval-Augmented Generation) workflows. Its data connector library (200+ sources) and advanced indexing strategies (tree, list, keyword, knowledge graph) make it the strongest choice for building production RAG systems. The 2026 release adds advanced query routing, multi-document reasoning, and integration with vector databases via MCP connectors.

     * **Best for:** Teams building RAG-centric applications with complex data sources
     * **Pros:** Best RAG framework, 200+ data connectors, advanced indexing, MCP integration
     * **Cons:** Less suited for non-RAG workflows, agent capabilities still maturing
    
    1. ### 3. Microsoft Semantic Kernel

    Semantic Kernel is Microsoft's enterprise-grade LLM orchestration framework, deeply integrated with Azure OpenAI and the Microsoft ecosystem. Its strength lies in enterprise features: native Azure AD authentication, compliance with Microsoft security standards, and seamless integration with Microsoft 365 and Copilot. For organizations committed to the Microsoft stack, it provides the most natural developer experience.

     * **Best for:** Microsoft-centric enterprises building on Azure OpenAI
     * **Pros:** Native Microsoft integration, enterprise security, C# and Python support, Azure AD
     * **Cons:** Microsoft ecosystem dependency, less model-agnostic than alternatives
    
    1. ### 4. CrewAI

    CrewAI specializes in multi-agent AI systems, allowing developers to define teams of AI agents with distinct roles, goals, and tools that collaborate to solve complex tasks. The framework handles agent-to-agent communication, task delegation, and result synthesis. The 2026 release adds enterprise features including persistent agent memory, audit logging, and governance controls.

     * **Best for:** Teams building multi-agent systems with role-based collaboration
     * **Pros:** Best multi-agent framework, intuitive role-based design, good documentation
     * **Cons:** Narrower scope than LangChain, newer platform with smaller community
    
    1. ### 5. AutoGen (Microsoft Research)

    AutoGen from Microsoft Research provides a flexible framework for building conversational AI agent systems. It excels at creating agents that can chat with each other, humans, and tools to solve tasks. The framework supports both single-agent and multi-agent configurations with customizable conversation patterns. Its strength is research-backed agent reasoning and problem-solving capabilities.

     * **Best for:** Teams building conversational multi-agent research applications
     * **Pros:** Flexible conversation patterns, research-backed, supports human-AI collaboration
     * **Cons:** Research-oriented design, less production-hardened than commercial frameworks
    
    1. ### 6. Haystack (deepset)

    Haystack by deepset provides a production-focused NLP framework that has expanded to cover full LLM application development. Its pipeline-based architecture makes it easy to build, test, and deploy NLP pipelines. The framework excels at search-heavy applications and provides excellent evaluation and testing tools for RAG quality assurance.

     * **Best for:** Teams building search-heavy AI applications with rigorous testing needs
     * **Pros:** Production focus, excellent testing tools, good documentation, pipeline architecture
     * **Cons:** Smaller ecosystem than LangChain, less agent framework depth
    
    1. ### 7. Vercel AI SDK

    The Vercel AI SDK is optimized for building AI-powered web applications, particularly those using Next.js and the Vercel platform. It provides streaming responses, edge function deployment, and seamless integration with popular AI providers. Its strength is the developer experience for building AI features into web applications rather than complex backend pipelines.

     * **Best for:** Web developers building AI features into Next.js/Vercel applications
     * **Pros:** Excellent DX, streaming support, edge deployment, Next.js integration
     * **Cons:** Web-focused, limited for complex backend AI pipelines
    
    1. ### 8. Beehive Strategy MCP Toolkit

    Beehive Strategy's MCP Toolkit provides a framework for building LLM applications that connect to enterprise data through the Model Context Protocol. Rather than building data integration from scratch, developers use MCP servers to connect to data sources and the toolkit to orchestrate LLM interactions. This approach is particularly valuable for enterprise AI applications that need governed data access.

     * Best for: Enterprise apps needing governed, protocol-standard data access for LLMs
    
    
    • Pros: Protocol-standard data access, built-in governance, any AI model compatibility
    • Cons: Narrower scope, focused on data access layer rather than full agent framework

    Framework Selection Guide

    • Maximum flexibility: LangChain with LangGraph
    • RAG-focused: LlamaIndex
    • Microsoft stack: Semantic Kernel
    • Multi-agent systems: CrewAI
    • Web applications: Vercel AI SDK
    • Enterprise data access: Beehive Strategy MCP Toolkit

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:02

    ↗

    Vector databases have become the backbone of AI applications, powering semantic search, RAG systems, recommendation engines, and multi-modal AI. With the market maturing rapidly in 2026, choosing the right vector database impacts everything from query latency to operational...

    Vector databases have become the backbone of AI applications, powering semantic search, RAG systems, recommendation engines, and multi-modal AI. With the market maturing rapidly in 2026, choosing the right vector database impacts everything from query latency to operational costs. We ranked the 8 best options based on performance, scalability, ease of use, and enterprise readiness.

    TL;DR: Ranked 8 best vector databases: Pinecone leads for managed simplicity, Weaviate for flexibility, Milvus for open-source scale, and pgvector for PostgreSQL-native teams. Selection depends on your scale, latency requirements, and existing infrastructure.

    Key Evaluation Criteria for Vector Databases

    Modern vector databases must excel across multiple dimensions. We evaluated each option on: query performance (latency at various scales, index types supported), scalability (horizontal scaling, multi-tenancy), data type support (dense vectors, sparse vectors, multi-modal embeddings), integration ecosystem (SDKs, LangChain/LlamaIndex support), and operational maturity (hosting options, backup, monitoring).

    • Query performance: P95 latency at 1M, 10M, and 100M vector scales
    • Scalability: Horizontal scaling, sharding, multi-tenancy
    • Data type support: Dense vectors, sparse vectors, binary vectors, multi-modal
    • Integration: SDK availability, LLM framework support, MCP compatibility
    • Operations: Self-hosted vs. managed, backup, monitoring, compliance

    Ranking: The 8 Best Vector Databases for AI Applications

    1. ### 1. Pinecone

    Pinecone remains the most popular fully-managed vector database, known for its simplicity and reliability. The 2026 release adds sparse-dense hybrid search, serverless tier with sub-millisecond P99 latency, and namespace-based multi-tenancy. Its serverless pricing model makes it cost-effective for variable workloads.

     * **Best for:** Teams wanting fully managed vector search without operational overhead
     * **Pros:** Zero operations, excellent performance, simple API, strong ecosystem
     * **Cons:** Vendor lock-in, limited customization, costs scale with usage
    
    1. ### 2. Weaviate

    Weaviate offers the best balance of flexibility and features in the vector database market. Its modular architecture supports multiple vectorization providers, built-in RAG capabilities, and GraphQL-based querying. The 2026 release adds native multi-modal search, improved hybrid search, and Weaviate Agents for autonomous data workflows.

     * **Best for:** Teams wanting flexibility with both managed and self-hosted options
     * **Pros:** Flexible deployment, built-in vectorization, GraphQL API, strong RAG features
     * **Cons:** Configuration complexity, resource-intensive for self-hosted at scale
    
    1. ### 3. Milvus

    Milvus is the leading open-source vector database, designed for billion-scale vector similarity search. It supports multiple index types (IVF, HNSW, DiskANN) and provides cloud-native architecture with separate storage and compute. Zilliz Cloud offers a managed version for teams that want open-source flexibility without operational burden.

     * **Best for:** Large-scale deployments requiring billion-vector capacity
     * **Pros:** Billion-scale support, multiple index types, cloud-native, open-source
     * **Cons:** Operational complexity for self-hosted, steeper learning curve
    
    1. ### 4. pgvector

    pgvector extends PostgreSQL with vector similarity search, making it the most accessible option for teams already using Postgres. While not the fastest at extreme scale, it excels in environments where vector search needs to coexist with relational queries, transactions, and existing Postgres tooling. The 2026 improvements include better HNSW indexing and approximate nearest neighbor performance.

     * **Best for:** PostgreSQL-native teams wanting to add vector search without new infrastructure
     * **Pros:** Zero new infrastructure, ACID transactions, familiar tooling, MCP-compatible via Postgres MCP
     * **Cons:** Not optimized for billion-scale, limited index types
    
    1. ### 5. Qdrant

    Qdrant is a high-performance vector database written in Rust, offering excellent low-latency search. Its filtering system is among the most advanced, allowing complex metadata filtering combined with vector similarity. The 2026 release adds real-time updates, improved quantization, and Qdrant Cloud with multi-region deployment.

     * **Best for:** Applications requiring low-latency vector search with complex filtering
     * **Pros:** Excellent performance, advanced filtering, Rust-based reliability, good SDKs
     * **Cons:** Smaller community than Pinecone/Weaviate, managed offering newer
    
    1. ### 6. Chroma

    Chroma has become the default vector store for AI prototyping and development, favored for its simplicity and Python-native design. It is ideal for RAG applications, local development, and embedded use cases. While not designed for production-scale deployments, its developer experience is unmatched for getting started quickly.

     * **Best for:** AI prototyping, local development, and embedded vector search
     * **Pros:** Extremely simple API, Python-native, great for prototyping, lightweight
     * **Cons:** Not production-grade at scale, limited distributed capabilities
    
    1. ### 7. Zilliz Cloud (Managed Milvus)

    Zilliz Cloud provides a fully managed Milvus experience with enterprise features including SSO, RBAC, and compliance certifications. It inherits Milvus's billion-scale capability while eliminating operational complexity. The managed service includes automatic scaling, backup, and monitoring.

     * **Best for:** Enterprises wanting Milvus scale without self-hosting
     * **Pros:** Managed operations, billion-scale, enterprise security features
     * **Cons:** Premium pricing, Milvus learning curve still applies
    
    1. ### 8. Elasticsearch Vector Search

    Elasticsearch's vector search capabilities have matured significantly, making it a strong option for organizations already using the Elastic stack. Its advantage is combining full-text search, vector search, and structured search in a single platform. The 2026 release improves native vector indexing performance.

     * Best for: Organizations combining traditional search with vector search
    
    
    • Pros: Combined search types, existing ecosystem, mature tooling, good hybrid search
    • Cons: Vector performance lags specialized databases, resource-intensive

    Comparison Summary

    • Pinecone: Best managed experience | Zero ops, serverless pricing
    • Weaviate: Best flexibility | Managed or self-hosted, built-in RAG
    • Milvus: Best open-source scale | Billion-vector support
    • pgvector: Best for Postgres teams | Zero new infrastructure
    • Qdrant: Best performance | Rust-based, advanced filtering
    • Chroma: Best for prototyping | Simplest getting started
    • Zilliz Cloud: Best managed Milvus | Enterprise features
    • Elasticsearch: Best hybrid search | Full-text + vector combined

    How to Choose

    If your team already uses PostgreSQL and needs basic vector search, start with pgvector. For production AI applications requiring managed simplicity, Pinecone is the safe choice. For maximum flexibility and scale, Weaviate or Milvus excel. Always benchmark with your actual data and query patterns before committing.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:01

    ↗

    Listmargin works out your eBay final value fees, ad fees, and what you actually keep on a sale. Most fee calculators copy a four-line summary of eBay's rates. This one reads the full published schedule: all 46 category rates, the store subscription tables, and the four...

    Listmargin works out your eBay final value fees, ad fees, and what you
    actually keep on a sale. Most fee calculators copy a four-line summary of
    eBay's rates. This one reads the full published schedule: all 46 category
    rates, the store subscription tables, and the four categories where crossing
    a price threshold re-rates the entire sale.

    It's free, with no signup and no paid tier. There's an embeddable version if
    you run a blog or a tool site. A weekly monitor re-reads eBay's own fee
    pages, so when a rate moves the calculator gets corrected instead of drifting
    out of date.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 18:01

    ↗

    Originally published on tamiz.pro. The current generation of AI coding assistants operates on a fundamental paradox: they are trained on the entirety of public code, yet they struggle to understand the specific codebase they are embedded in. For years, the industry has relied...

    Originally published on tamiz.pro.

    The current generation of AI coding assistants operates on a fundamental paradox: they are trained on the entirety of public code, yet they struggle to understand the specific codebase they are embedded in. For years, the industry has relied on prompt guessing—feeding the LLM a ragged collection of nearby code lines, hoping the semantic context is implicit. This approach is brittle. It fails when symbols are imported, when types are inferred, or when the logic spans multiple files.\n\nThe solution isn't a bigger model; it's a better protocol. The Language Server Protocol (LSP) is the missing link between static analysis and generative AI. By integrating LSP into AI agents, we move from probabilistic guessing to deterministic understanding. This article explores why LSP is critical for reliable coding agents, how to architect an LSP-augmented agent, and the technical pitfalls of this integration.\n\n## The Semantic Gap: Why Prompts Aren't Enough\n\nTo understand why LSP is necessary, we must first diagnose the failure modes of prompt-only AI coding agents. An LLM is a probabilistic next-token predictor. It does not "know" your code; it has seen patterns similar to your code in its training data. When you ask an AI agent to \"refactor this function,\" it relies on the context window to provide relevant information.\n\n### The Context Window Bottleneck\n\nThe primary limitation is the context window. Even with 128k tokens, you cannot fit an entire modern codebase. Agents must select a subset of files to include. Without explicit semantic queries, this selection is often heuristic-based (e.g., \"include the last 50 lines\") or simple semantic similarity (vector search). Both approaches miss critical structural relationships.\n\nConsider this example:\n\n

    python\n# file: user_service.py\nclass UserService:\n def get_user(self, user_id: int):\n # ... logic ...\n return db.query(User).filter(id=user_id)\n\n# file: controllers.py\ndef handle_request(user_id: int):\n user = UserService().get_user(user_id)\n # AI Agent needs to know the return type of get_user\n # to safely access user.email\n send_welcome_email(user.email)\n

    \n\nIf the AI agent only sees controllers.py, it might hallucinate the structure of the User object. If it uses vector search, it might pull in irrelevant files that share the string \"email\" but not the semantic relationship. It needs to know that UserService.get_user returns a User object, which has an email attribute, defined elsewhere.\n\n### The Hallucination of Structure\n\nLLMs are notorious for hallucinating APIs. They might invent a method user.get_profile() because it sounds plausible, even if the actual method is user.profile(). In a web browser, this is a minor bug. In a banking application, it’s a security vulnerability. The LLM lacks a single source of truth for the project’s schema.\n\n## What is LSP and Why Does It Matter for AI?\n\nThe Language Server Protocol is a standard established by Microsoft that defines how a development tool (like VS Code) communicates with a language server. The language server is a separate process that understands the programming language’s semantics, syntax, and structure.\n\nFor AI agents, the LSP provides deterministic queries over the codebase. Instead of guessing, the agent asks:\n\n1. What is the definition of this symbol? (Definition/Declaration)\n2. Where is this symbol used? (References)\n3. What are the parameters and return types of this function? (Signature)\n4. What are the imports and dependencies? (Workspace Symbols)\n\nThese queries are fast, accurate, and language-aware. They turn the codebase from a text blob into a navigable graph.\n\n## Architecting an LSP-Augmented AI Agent\n\nIntegrating LSP into an AI agent is not just about calling a few APIs. It requires a robust architecture that handles the asynchronous nature of LSP, manages state, and integrates the results into the LLM’s context effectively.\n\n### High-Level Architecture\n\n

    mermaid\ngraph TD\n User[Developer] --> IDE[IDE Plugin / Agent Interface]\n IDE --> Agent[AI Agent Core]\n Agent --> LSPClient[LSP Client]\n LSPClient --> LSPServer[Language Server Process]\n LSPServer --> Codebase[(Codebase Index)]\n \n Agent --> LLM[LLM API]\n LLM --> Agent\n \n Agent --> ContextBuilder[Context Builder]\n LSPClient -.-> ContextBuilder\n ContextBuilder --> LLM\n

    \n\n1. Agent Core: Orchestrates the task. It decides what information is needed.\n2. LSP Client: Manages the connection to the language server. It sends requests (e.g., textDocument/definition) and parses responses.\n3. Language Server: The heavy lifter. It parses the AST, builds the symbol table, and answers queries.\n4. Context Builder: Formats the LSP responses into a structure the LLM can understand (e.g., Markdown, JSON, or specific prompt templates).\n\n### Step 1: Establishing the LSP Connection\n\nMost modern editors (VS Code, Neovim, JetBrains) have built-in LSP clients. However, for a standalone AI agent, you may need to implement an LSP client or use an existing library. For Python, pygls is a popular choice. For JavaScript/TypeScript, typescript-language-server or ts-morph can be used.\n\nHere is a simplified example of how an agent might query for the definition of a symbol using a hypothetical LSP client in Python:\n\n

    python\nimport asyncio\nfrom pygls.lsp.methods import TEXT_DOCUMENT_DEFINITION\nfrom pygls.workspace import Workspace\n\nclass AISemanticEngine:\n def __init__(self, client):\n self.client = client\n self.workspace = Workspace(root_uri=None)\n\n async def get_symbol_definition(self, file_path, line, col):\n \"\"\"\n Query the language server for the definition of a symbol\n at the given position.\n \"\"\"\n uri = f\"file://{file_path}\"\n \n # Prepare the request parameters\n position = {\n \"line\": line,\n \"character\": col\n }\n \n # Send the request to the LSP server\n try:\n # Note: This is pseudo-code for illustration.\n # Actual implementation depends on the LSP client library.\n definition = await self.client.send_request(\n TEXT_DOCUMENT_DEFINITION,\n {\n \"textDocument\": {\"uri\": uri},\n \"position\": position\n }\n )\n return definition\n except Exception as e:\n print(f\"LSP Query Failed: {e}\")\n return None\n

    \n\n### Step 2: Resolving References and Dependencies\n\nOnce you have the definition, you often need the references to understand how a function is used. This helps the LLM understand the contract of the function.\n\n

    python\n async def get_function_usage(self, file_path, line, col):\n \"\"\"\n Find all usages of a symbol.\n \"\"\"\n uri = f\"file://{file_path}\"\n position = {\"line\": line, \"character\": col}\n \n try:\n references = await self.client.send_request(\n TEXT_DOCUMENT_REFERENCES,\n {\n \"textDocument\": {\"uri\": uri},\n \"position\": position,\n \"context\": {\"includeDeclaration\": True}\n }\n )\n return references\n except Exception as e:\n return []\n

    \n\n### Step 3: Context Enrichment for the LLM\n\nThe raw LSP response is often structured data (JSON). The LLM needs this data in a human-readable or structured format that fits into the prompt. This is the Context Builder phase.\n\nA good context enrichment strategy includes:\n\n1. Code Snippets: Extract the relevant lines from the definition and reference files.\n2. Type Information: Include type signatures if available (e.g., from TypeScript or Python type hints).\n3. Import Paths: Show where the symbol is imported from.\n\nExample prompt construction:\n\n

    text\nUser: Refactor the `get_user` function to return a Pydantic model.\n\nAssistant: I need to understand the current structure of `get_user` and the `User` model.\n\n[Context Provided by Agent]:\n1. Definition of `get_user` in `user_service.py`:\n

    python\n def get_user(self, user_id: int) -> Optional[dict]:\n return db.query(User).filter(id=user_id)\n

    \n2. Definition of `User` model in `models.py`:\n

    python\n class User(Base):\n id = Column(Integer, primary_key=True)\n email = Column(String)\n

    \n3. Usage of `get_user` in `controllers.py`:\n

    python\n user = UserService().get_user(user_id)\n send_welcome_email(user.email) # Note: user is expected to have 'email'\n

    \n\nAssistant: Based on the context, here is the refactored code...\n

    \n\n## Advanced Techniques: Symbol Graphs and Dependency Resolution\n\nFor larger codebases, simple definition/references queries are not enough. You need to build a symbol graph or leverage the language server’s ability to resolve cross-file dependencies.\n\n### Using Workspace Symbols\n\nThe WORKSPACE_SYMBOL query allows you to search for symbols across the entire project. This is useful for finding all classes that implement a specific interface or all functions that match a certain pattern.\n\n

    python\n async def search_symbols(self, query):\n \"\"\"\n Search for symbols matching a query across the workspace.\n \"\"\"\n try:\n symbols = await self.client.send_request(\n WORKSPACE_SYMBOL,\n {\"query\": query}\n )\n return symbols\n except Exception as e:\n return []\n

    \n\n### Handling Dynamic Languages\n\nDynamic languages (Python, JavaScript, Ruby) pose a challenge for LSP. The language server must perform static analysis on dynamic code, which can be inaccurate. For example, Python’s getattr() or JavaScript’s dynamic property access can confuse the LSP.\n\nTo mitigate this:\n1. Use Strong Typing: Encourage the use of type hints (Python) or TypeScript (JavaScript). This provides the LSP with more accurate information.\n2. Fallback to Vector Search: If the LSP query fails or returns incomplete data, fall back to semantic vector search to find potentially relevant code.\n3. Iterative Refinement: The agent can make multiple LSP queries. For example, if it gets a definition, it can then query the definition of the types mentioned in that definition.\n\n## Pitfalls and Best Practices\n\n### Latency and Performance\n\nLSP queries are not instantaneous. Network latency, server startup time, and large codebase indexing can add seconds to the agent’s response time. To mitigate this:\n- Cache Results: Cache LSP responses for symbols that haven’t changed.\n- Parallel Queries: If the agent needs multiple symbols, query them in parallel.\n- Async Processing: Ensure the agent doesn’t block the user interface while waiting for LSP responses.\n\n### Error Handling\n\nLSP servers can crash or return errors. The agent must handle these gracefully. If the LSP is unavailable, the agent should fall back to a less reliable method (e.g., regex-based parsing or vector search) and inform the user.\n\n### Security and Privacy\n\nLSP servers may expose internal file paths and code structure. Ensure that the LSP client is sandboxed and that sensitive code is not sent to external LSP servers if they are cloud-based.\n\n## The Future: LSP as a Standard for AI\n\nThe integration of LSP into AI coding agents is not just a best practice; it is becoming a standard. Tools like GitHub Copilot and Cursor are already leveraging semantic understanding to provide better suggestions. As LLMs become more integrated into the development workflow, the ability to query the codebase deterministically will be a key differentiator between \"guessing\" AI and \"understanding\" AI.\n\nWe are moving towards a future where AI agents are not just text generators, but code-aware collaborators. They will understand the architecture, the dependencies, and the types of your codebase. This requires a protocol that can bridge the gap between human-readable text and machine-understood structure. LSP is that protocol.\n\n## Frequently Asked Questions\n\n### Can I use LSP with any programming language?\nNo, LSP support depends on the availability of a language server for that language. Most major languages (Python, JavaScript, TypeScript, Java, C++, Go, Rust) have robust LSP implementations. For languages without LSP support, you may need to rely on other methods like vector search or static analysis tools.\n\n### Does LSP integration replace the need for good prompts?\nNo. LSP provides the agent with accurate context, but the agent still needs clear instructions. LSP reduces the hallucination rate, but it doesn’t replace the need for the developer to specify the desired outcome.\n\n### How does LSP improve code generation accuracy?\nLSP provides the agent with the exact definitions, types, and usage patterns of the code. This reduces the likelihood of the agent inventing non-existent methods or misusing APIs. It ensures that the generated code is consistent with the existing codebase.\n\n### Is LSP integration complex to implement?\nThe complexity depends on the language and the agent’s architecture. For simple use cases, using existing libraries like pygls or typescript-language-server can make integration straightforward. For more complex scenarios, building a custom LSP client and context builder may be necessary.\n\nFor more insights on AI engineering and developer tooling, visit Tamiz's Insights.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:56

    ↗

    You can tell when an LLM wrote an email. The "I hope this email finds you well" opener, the three polite paragraphs answering a one-line question. I wanted a reply-drafting agent that didn't do that, and "don't sound like an AI" turned out to be hard to put in a prompt....

    You can tell when an LLM wrote an email. The "I hope this email finds you well" opener, the three polite paragraphs answering a one-line question. I wanted a reply-drafting agent that didn't do that, and "don't sound like an AI" turned out to be hard to put in a prompt. Banning a few phrases is easy. The rest is judgment, and a single prompt that holds across a friendly dinner invite and a recruiter cold-email took more iterations than I'd guessed.

    This is not only an email problem. Some platforms down-rank content that reads as AI-generated, so teams publishing at scale have a real stake in prose that clears a detector, even when a human wrote it. The workflow here applies to any of that.

    So I stopped hand-tuning and let LaunchDarkly agent optimization search for the prompt. You give it a judge that scores "better," and it generates prompt variations and keeps the ones that beat the bar. For the reasoning behind the feature, read the agent optimization announcement. This tutorial is the how. If you don't have an account yet, sign up for LaunchDarkly to follow along.

    Two pieces do the work here. Claude (claude-haiku-4-5-20251001) runs both roles: it drafts the replies, and it writes each new candidate prompt when the loop asks for one. Scoring comes from GPTZero, which isn't a language model at all but a closed AI detector. I wired it in inverted, so the score is the probability a reply reads as AI and the optimizer drives it down. I went with a detector instead of an LLM-as-a-judge for a reason: grading one model's prose by asking another model whether it sounds human is exactly the call language models are unreliable at, and a tool trained for that one question gives a number you can defend.

    A run is cheap. Each iteration costs around $0.002 and a few seconds, so a full run lands near a penny or two, and the loop tries variations I'd never sit down and type by hand.

    This tutorial runs from a saved config

    You bootstrap the agent, the judge, and the optimization, then work in the UI. Every iteration streams to the optimization Results tab, the winner lands on the agent Variations tab, and you tune thresholds and inputs on the optimization itself. The code does two things: it scores AI-likeness with GPTZero, and it runs the optimization.

    What you will build

    • A reply-drafting agent, email-agent, seeded with one deliberately thin instruction
    • An inverted AI-likeness judge, ai-likeness, scored by GPTZero in code rather than by a prompt
    • A saved optimization, email-agent-opt, that runs as a candidate generator and streams to the Results tab
    • A path from the candidates it surfaces into a fuller offline eval, where the real measurement happens

    The GPTZero integration is the reusable part. The same shape works for any external scorer you might bring, whether a moderation API, a classifier you host, or a scoring endpoint of your own, so what you learn here isn't limited to email or to AI detection.

    The companion repo is agent-optimization-sample. Clone it to follow along.

    Prerequisites

    • Sign up for LaunchDarkly and request access to agent optimization
    • Python 3.11+ and uv
    • An Anthropic API key, which Claude uses to both draft replies and write each new prompt
    • A GPTZero API key for the AI-likeness detector
    • A LaunchDarkly SDK key and a REST API key, in a local .env

    Install the project and its dependencies:

    Terminal

    uv sync
    # .env holds LD_SDK_KEY, LD_API_KEY, LD_PROJECT_KEY,
    #            ANTHROPIC_API_KEY, AI_LIKENESS_API_KEY
    

    The repo aliases the short LD_* names to the LAUNCHDARKLY_* names the SDK expects, so the short names in .env are enough.

    How agent optimization works

    Agent optimization runs an iterative loop against an AgentControl config. It measures your current variation as a baseline, generates candidate variations, and scores each against your acceptance criteria. The loop has a simple shape:

    ┌─────────────┐     ┌──────────────┐     ┌─────────────┐
    │   Define    │────▶│   Explore    │────▶│   Commit    │
    │  "Better"   │     │  candidates  │     │  a winner   │
    └──────▲──────┘     └──────────────┘     └──────┬──────┘
           │                                        │
           └────────────────────────────────────────┘
    

    Define better. You set acceptance criteria with a judge and a threshold. A judge scores a response on one dimension. You reference a judge saved as an AgentControl config by its key.

    Explore candidates. Each iteration drafts against your inputs, scores the result, and writes the next candidate from what the scores tell it. The threshold here is a gate that keeps the loop generating. When a candidate clears it, the optimizer re-runs that same prompt against a few more of your input samples and keeps it only if it passes those too, so a prompt that got lucky on one message doesn't win. Even then, clearing the gate doesn't certify that a candidate is good enough to ship. A fuller eval decides that, later.

    Commit the winner. The recommended variation shows up in LaunchDarkly, and with autoCommit it publishes back to the agent's Variations tab so you can read what the optimizer wrote.

    You also pick an evaluation mode. Exploratory mode infers quality from the judge alone, which suits open-ended inputs that have no single correct output. Expected Output mode scores against known-correct answers. Replies have no single right answer, so this tutorial stays in Exploratory mode.

    The technique behind it

    Agent optimization is an instance of OPRO (Optimization by PROmpting), introduced in Google DeepMind's Large Language Models as Optimizers. A model reads the history of prompts and their scores, then writes the next candidate to try. It searches over prompts, not model weights, so each candidate is cheap to run and nothing is ever trained.

    How the sample is organized

    The companion repo keeps the moving parts in small files, so each piece is easy to find and swap:

    • bootstrap.py: seeds the three LaunchDarkly objects, the email-agent config, the ai-likeness judge, and the email-agent-opt optimization. It's safe to re-run, and it prints links to the configs and the Results tab.
    • optimize_from_config.py: the one run command. It reads the saved optimization, runs it, streams each iteration to the Results tab, and prints the tab's link at the end.
    • optimize.py: the two callbacks the run needs. handle_agent_call drafts replies on Claude, and writes the next candidate prompt on Claude too when the SDK asks for one. handle_judge_call scores AI-likeness with GPTZero and hands the optimizer the per-reply detector output.
    • detector.py: the GPTZero client. score_with_response returns the number the judge gates on and the full GPTZero JSON.
    • messages.py: the synthetic input messages.
    • gptzero_test.py: a standalone probe for scoring a draft by hand.
    • clients.py and env.py: the LaunchDarkly and Anthropic clients, built once each, and the .env loader.

    The saved optimization holds what you're optimizing for: the judge, the threshold, the inputs, and the model choices. The code holds how the work happens, drafting on Claude and scoring with GPTZero. You edit the what in the UI and the how in code, and the run command brings the two together.

    Step 1: Bootstrap the agent, judge, and optimization

    One command seeds everything this tutorial needs. bootstrap.py creates three objects in LaunchDarkly, and it's idempotent, so anything that already exists is left alone:

    • email-agent: the agent config whose instructions the optimizer tunes. The baseline gives the model the task and the output contract, a JSON {"replies": [...]} envelope and the {{messages}} variable, and nothing about tone. That's on purpose. It leaves the humanization, the part you want optimized, to the optimizer.
    • ai-likeness: the inverted judge config. GPTZero scores it from code, which leaves the judge prompt as a placeholder.
    • email-agent-opt: the saved optimization the Results tab runs. Thresholds, inputs, and model choices all live here.

    The baseline is thin on tone but carries the output contract the parser needs, on the Claude model the agent drafts with:

    bootstrap.py (variation)

    {
        "key": "baseline",
        "name": "Baseline",
        "model": {"modelName": "claude-haiku-4-5-20251001", "parameters": {}},
        "instructions": (
            "You are an email assistant. Write a reply to each message below. "
            'Return ONLY a JSON object {"replies": ["<reply to Message 1>", ...]} '
            "with one reply per message, in order.\n\n{{messages}}"
        ),
    }
    

    Run the bootstrap:

    Terminal

    uv run python bootstrap.py
    

    It prints links to the two configs and the one command that runs the optimization.

    One judge by design

    The New optimization form in the UI attaches a single judge, so this tutorial uses one: AI-likeness. The bootstrap creates the same single-judge optimization in code, so the run matches what the UI supports. To build it by hand instead, open Agent optimization, then New optimization, target email-agent, add the ai-likeness judge, set the threshold and the input messages, and save.

    Step 2: Define the input messages

    The agent drafts against a fixed set of messages, injected into the prompt through the {{messages}} variable. Keep them diverse on purpose, so a winning prompt generalizes instead of overfitting to one kind of message. The agent replies to all of them in one call and the judge averages the AI-likeness scores, and Step 3 explains why that averaging matters. messages.py holds the message list:

    messages.py

    SYNTHETIC_MESSAGES = [
        "Hey! Are you free Saturday for dinner? A few of us are getting together and it'd be great to see you.",
        "Hi, I came across your background and I'm hiring for a senior role that looks like a strong fit. Open to a quick call this week?",
        "It's been way too long! I'll be in town Thursday and Friday. Any chance you're around to grab coffee?",
        "Just a reminder that your dentist appointment is Tuesday at 2pm. Reply to confirm or reschedule.",
        "Hey neighbor, a package addressed to you was left at my door by mistake. Want to swing by this weekend?",
        # 5 more, spanning tone, intent, and length
    ]
    

    Step 3: Score AI-likeness with GPTZero

    AI-likeness is the target, and it's the one judge that has to live in code, because it isn't an LLM. You score the reply with GPTZero, an AI detector. The score is 1 - P(human): near 0 when GPTZero reads the reply as human, which is your goal, and near 1 for AI or mixed text. Lower is better, so the bootstrap marks the judge inverted and the optimizer drives the score down.

    GPTZero is also the more useful integration to learn, because it generalizes to any external scorer. The SDK gives a judge config no hook to reach outside code, so the config exists only so the optimization can attach a judge, its prompt stays a placeholder, and the real number comes from a small client you call yourself:

    detector.py

    def score_with_response(text: str):
        data = _api_call(text)                                    # POST the reply to GPTZero
        doc = (data.get("documents") or [{}])[0]
        return 1.0 - doc["class_probabilities"]["human"], data    # 1 - P(human), + full JSON
    

    score_with_response returns both the number the judge gates on and the full GPTZero JSON, so the callback can forward the detail to the optimizer. Set AI_LIKENESS_API_KEY to your GPTZero key.

    Two things here cost me time. GPTZero sits behind Cloudflare, and a plain urllib request comes back as a 403 with error code: 1010 before it reaches the API. That reads like a bad key, but it isn't. Sending any real User-Agent header clears it. The score also comes from 1 - P(human) rather than the average_generated_prob field, which is the fraction of sentences flagged AI and reports 1.0 even on text the detector still classifies as human. Gating on that field punishes replies that already passed.

    GPTZero is the only judge, so handle_judge_call doesn't branch on the judge key. Every call pulls the batch of replies, scores each one with GPTZero, and averages:

    optimize.py

    async def handle_judge_call(key, config, context, is_evaluation=True):
        text = _extract_candidate(context.user_input or "")
        replies = [r for r in (json.loads(text) or {}).get("replies", [])
                   if isinstance(r, str) and r.strip()]
        if not replies:                              # empty draft must FAIL the inverted gate
            return OptimizationResponse(output=json.dumps(
                {"score": 1.0, "rationale": "empty or degenerate candidate"}))
        results = []
        for reply in replies:                        # one GPTZero call per reply in the batch
            s, raw = detector.score_with_response(reply)        # 1 - P(human); + full JSON
            results.append({"reply": reply, "ai_likeness": s, "gptzero": raw})
        avg = round(sum(r["ai_likeness"] for r in results) / len(results), 4)
        # The gate is the AVERAGE; the full per-reply GPTZero JSON becomes the rationale.
        return OptimizationResponse(output=json.dumps(
            {"score": avg, "rationale": f"Average AI-likeness = {avg}. {json.dumps(results)}"}))
    

    Two choices in there are what make optimizing against a detector actually work.

    Score a batch and average. A single short reply's GPTZero score is noisy. The very same prompt can produce a reply it calls 99% human on one message and 99% AI on the next. Each turn drafts replies to all the messages in one batch, and the judge averages those scores into something steady enough to optimize against.

    Forward the whole detector response. The rationale you return goes straight to the model that writes the next prompt. Instead of a bare number, return the full GPTZero JSON, with its per-sentence probabilities and predicted class. The optimizer reads that directly and revises around whatever scored as AI, with no parsing on your side.

    The empty-draft case is the one I got wrong first. An empty reply scores 0.0, which the inverted gate reads as perfectly human, so an early version let the optimizer win by drafting nothing. Now an empty or malformed batch, or a detector error, returns 1.0 and fails the gate.

    To build intuition before you run the loop, probe GPTZero on a draft by hand:

    Terminal

    uv run python gptzero_test.py "your draft reply"
    

    A detector is a black box, so gate accordingly

    A detection service gives you a defensible score immediately, with no model to train. It also has real limits. You can't tune it, it leans toward calling short LLM text AI, and it bills per call across every iteration. That confidence is the reason to average over a batch instead of gating on a single reply.

    Step 4: Run the optimization

    The saved optimization holds the judge, the threshold, the inputs, and the model choices, so the run command takes none of them. The agent drafts on Claude, the optimizer writes each new prompt on Claude too, and the threshold keeps the loop generating rather than picking a winner. Here is the saved optimization:

    bootstrap.py (optimization)

    {
        "key": "email-agent-opt",
        "aiConfigKey": "email-agent",
        "maxAttempts": 10,
        "judgeModel": "claude-haiku-4-5-20251001",       # required by the API; the GPTZero judge never calls it
        "modelChoices": ["claude-haiku-4-5-20251001"],   # the Claude model the agent drafts with
        "judges": [{"key": "ai-likeness", "threshold": 0.5}],   # generator gate, not a winner test
        "variableChoices": [   # interpolated into the instructions; the optimizer must use every one
            {"sender_type": "friend", "respondent_name": "Jordan Lee", "messages": MESSAGES_BLOCK},
            {"sender_type": "professional contact", "respondent_name": "Jordan Lee", "messages": MESSAGES_BLOCK},
        ],
        "userInputOptions": ["Draft the replies now."],  # trigger turn; messages come from {{messages}}
        "autoCommit": True,
    }
    

    MESSAGES_BLOCK is the message list from Step 2, formatted and fed in through {{messages}}. respondent_name and sender_type are the other two variables, so replies come out signed and pitched to the right register. The optimizer has to use every variable you declare, which is what keeps {{messages}} and the JSON envelope intact through every rewrite.

    The threshold is 0.5, and that number came from watching GPTZero, not from theory. A confident detector scores even clearly human-sounding short replies well above 0, so a gate near 0 never trips and the loop never finds anything to keep. At 0.5, this run passed at iteration 6 with 0.43, while the earlier iterations landed between 0.54 and 1.00. That gave the loop room to explore without rubber-stamping every candidate.

    Run it from the saved config:

    Terminal

    OPTIMIZATION_KEY=email-agent-opt uv run python optimize_from_config.py
    

    Settings live on the optimization

    Thresholds, inputs, models, and maxAttempts are baked into email-agent-opt at bootstrap time. Change them by editing the optimization in the UI, or by deleting it and re-running bootstrap.py with the matching environment variables set. Committing the winner back as a variation needs the REST API key.

    The command prints a link to the Results tab. One callback drafts replies on Claude, and the SDK reuses that same callback to write the next prompt, also on Claude. Each iteration posts its prompt and score to the Results tab as a candidate.

    Step 5: Read the winner

    Open the Results tab from the printed link. Each iteration posts as it runs, with its candidate prompt and AI-likeness average alongside the variation the run currently recommends.

    The optimization Results tab showing a passed run on claude-haiku-4-5-20251001: iteration 6 scoring 0.43 against the 0.50 threshold and committing the optimistic-coyote variation, the per-iteration charts for AI-likeness, latency, tokens, and cost, and the config-iterations table listing scores of 0.64, 0.54, 1.00, 1.00, 0.63, and 0.43.

    The Results tab after a passing run. Iteration 6 cleared the 0.50 gate at 0.43 and committed optimistic-coyote, with per-iteration charts for AI-likeness, latency, tokens, and cost.

    Click any iteration to drill into its candidate prompt, the input it ran against, and the replies it produced. Iteration 1 is the baseline template itself, scoring 0.64. Its replies already read decently ("Thanks for the invite! I'd love to come to dinner Saturday."), but the thin prompt left enough AI signal for the detector to flag.

    The detail view for iteration 1 on claude-haiku-4-5-20251001, scoring 0.64 AI-likeness: the baseline instruction with its JSON replies envelope and {{messages}} variable, the

    Iteration 1, the baseline template: the JSON-and-{{messages}} instruction, the "Draft the replies now." trigger input, and the replies it produced. At 0.64 it didn't clear the gate.

    The optimization sets autoCommit, so on success the winner publishes back to email-agent as a new variation. Open the agent Variations tab to read what the optimizer wrote.

    What the optimizer changed

    The run committed a new variation, optimistic-coyote. The Variations tab shows it next to the baseline, so you can read the change directly. The optimizer kept the JSON envelope and the {{sender_type}} and {{messages}} variables, and built a full humanization spec around them:

    The Variations tab for the Email Agent, showing the baseline variation with its short JSON-and-{{messages}} template above the committed optimistic-coyote variation, whose instructions spell out humanization rules: use contractions, open sentences with And or But, vary sentence length and use fragments, ban AI-tell phrases like

    The Variations tab: the thin baseline above the committed winner optimistic-coyote, both on claude-haiku-4-5-20251001. The optimizer preserved the {{sender_type}} and {{messages}} variables and the JSON envelope while adding the humanization guidance.

    Every line of it is a humanization lever, and they map onto how GPTZero separates the two classes:

    • How people actually write. Contractions, first person, sharply varied sentence length, the occasional fragment, and opening with the point instead of a preamble.
    • A stop-list of AI tells. "I hope this email finds you well," "Thank you for reaching out," and "Best regards," plus connectors like "furthermore," "moreover," and "consequently."
    • Specific and signed. Match the sender's register, reference concrete details from the message, and sign with {{respondent_name}} instead of a placeholder.

    The optimizer took a prompt that said nothing about tone and built out a detailed spec, and GPTZero scored the resulting replies as more human.

    That's one of the candidates the loop surfaced. To decide whether it's worth shipping, take it into a fuller offline eval for real data, which the sections below cover.

    The UI, the saved config, and the SDK

    You can run all of this from the UI or from code. The New optimization form builds the same optimization by hand and streams to the same Results tab. This tutorial used optimize_from_config, which runs a saved optimization from code while its judge, threshold, inputs, and models stay editable in the UI. To define everything in code instead, optimize_from_options takes the settings directly and accepts more than one judge, and optimize_from_ground_truth_options handles Expected Output mode when you have correct answers to match.

    This demo leaves several controls unused: token_optimization and latency_optimization for a cost-and-latency pass, token_limit for a spend cap, variation_key, output_key, context_choices, and the on_turn, on_passing_result, on_failing_result, and on_status_update callbacks.

    Optimization is not evaluation

    Offline evals and optimization sit next to each other in AgentControl and do opposite jobs. An offline eval measures a configuration you already have: you run your agent over a dataset, score it with judges, and answer "how good is this, and did anything regress?" Optimization runs the other direction, generating new variations until one clears that same judge. Running a surfaced candidate back through an eval is that measuring job again, telling you how it actually performs on a fuller set.

    They're strongest together. Here's a path through AgentControl that gets the most out of both:

    1. Define judges for what "better" means, the way you defined human-sounding replies here.
    2. Baseline with offline evals over a dataset, so you know where the current agent stands and have a regression check to compare against. For a worked example, read Offline evaluation of RAG-grounded answers.
    3. Optimize against that judge to discover a stronger variation, which is what this tutorial walks through.
    4. Watch production with online evals and AI Insights, then feed what you learn back into the dataset and the next optimization. When to add online evals covers the tradeoffs.

    Run it long enough and production signals become the next round's eval data, so each optimization starts from what you actually saw in production rather than a guess.

    Wrap up

    You started with a prompt that said nothing about tone and let agent optimization rewrite it against GPTZero, then read the winning humanization spec off the Variations tab. The loop's job was to explore cheaply, and the trustworthy verdict comes from a proper offline eval over a comprehensive dataset.

    Agent optimization is one step in a larger workflow. You define judges for what "better" means, optimize against them to surface candidates, then check those candidates with offline and online evals. What you learn in production feeds the next round. If you're getting started, Build a LangGraph multi-agent system is a good place to begin.

    Sign up for LaunchDarkly and point the loop at your own prompts.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:43

    ↗

    DingTalk and Feishu have evolved far beyond messaging apps. In 2026, they function as the central nervous system for millions of Chinese enterprises — and both platforms are building increasingly sophisticated AI ecosystems. For organisations looking to deploy conversational...

    DingTalk and Feishu have evolved far beyond messaging apps. In 2026, they function as the central nervous system for millions of Chinese enterprises — and both platforms are building increasingly sophisticated AI ecosystems. For organisations looking to deploy conversational BI, understanding how to integrate with these platforms is no longer optional; it is the primary deployment model in China.

    Key Insight: DingTalk and Feishu AI ecosystems enable enterprises to deploy conversational BI directly inside the IM tools that employees already use daily, eliminating adoption barriers and delivering data insights through natural language queries in the workflow.

    The Two Titans of Chinese Enterprise IM

    China's enterprise collaboration market is dominated by two platforms, each backed by a technology giant:

    • DingTalk (Alibaba Group): With over 700 million individual users and 25 million enterprise organisations, DingTalk is the most widely deployed enterprise IM platform in China. It has particularly strong penetration in manufacturing, retail, and government sectors.
    • Feishu / Lark (ByteDance): Feishu (known internationally as Lark) has rapidly gained market share, particularly among technology companies, internet firms, and knowledge-intensive industries. Its document-first design philosophy and strong third-party integration ecosystem make it popular with startups and scale-ups.

    DingTalk's AI Assistant Ecosystem

    DingTalk has invested heavily in its AI capabilities through several key initiatives:

    • DingTalk AI Assistant Platform: Launched in 2023 and significantly expanded through 2026, this platform allows enterprises to build custom AI assistants that operate within DingTalk chat threads. It supports integration with Alibaba's Tongyi Qianwen (Qwen) LLM as well as third-party models.
    • Enterprise Agent Market: A marketplace where enterprises can discover, deploy, and share pre-built AI agents. As of mid-2026, the marketplace hosts thousands of agents covering HR, finance, operations, and analytics functions.
    • Action-based architecture: DingTalk's AI assistant framework supports "actions" — discrete API calls that the AI can trigger based on user requests. This is architecturally similar to MCP's tool-calling paradigm, making DingTalk a natural fit for MCP-powered conversational BI.

    Feishu's AI Capabilities

    Feishu has taken a different but equally powerful approach:

    • Feishu Intelligent Partner (Feishu Bot 2.0): Feishu's AI bot framework supports multi-turn conversations, context awareness across chat threads, and the ability to invoke external APIs. It supports ByteDance's Doubao model as well as other domestic and international LLMs.
    • Feishu AnyCross: An integration platform that connects Feishu to over 300 enterprise applications. AnyCross provides pre-built connectors for ERP, CRM, and database systems — complementing MCP's data source connectivity.
    • Document AI: Feishu's strength in collaborative documents extends to AI-powered document analysis, automated report generation, and data extraction from spreadsheets — capabilities that pair naturally with conversational BI for ad-hoc analysis.

    MCP Compatibility with Chinese IM Platforms

    The Model Context Protocol is fundamentally a client-server protocol that defines how AI models access data. Integrating MCP with DingTalk or Feishu requires a bridge layer — an adapter that translates between the IM platform's bot framework and the MCP server. Here is how it works:

    1. IM Bot receives a user query in a DingTalk group chat or Feishu thread.
    2. The bridge adapter (developed by Beehive Strategy or your integration team) receives the message via the IM platform's webhook or API.
    3. The LLM processes the query and determines which MCP servers need to be called to retrieve relevant data.
    4. MCP servers execute against enterprise data sources (MySQL, Snowflake, SAP, etc.) and return structured results.
    5. The LLM formats the response — including charts, tables, or natural language summaries — and the bridge adapter posts it back to the IM conversation.

    Both DingTalk and Feishu support rich message formats (cards, interactive elements, chart embeds), which means conversational BI responses can include visually formatted data visualisations, not just plain text.

    Platform Comparison for BI Deployment

    Dimension DingTalk Feishu / Lark
    Enterprise User Base 25M+ organisations 12M+ organisations
    Primary Sectors Manufacturing, retail, government Technology, internet, professional services
    Default LLM Qwen (Tongyi Qianwen) Doubao (ByteDance)
    Third-Party LLM Support Yes (via API) Yes (via API)
    Bot Framework AI Assistant Platform Feishu Bot 2.0
    Rich Message Cards Yes Yes
    Enterprise App Market DingTalk Open Platform Feishu App Store
    MCP Integration Via bridge adapter Via bridge adapter

    Practical Implementation Steps

    For enterprises looking to deploy conversational BI through DingTalk or Feishu, the implementation path is straightforward:

    1. Choose your IM platform. If your organisation already standardises on one, start there. If you operate across both, a unified backend with platform-specific adapters is the recommended architecture.
    2. Register an enterprise bot. Create a bot application through the DingTalk Open Platform or Feishu Developer Console. Configure webhook endpoints and permission scopes.
    3. Deploy the MCP bridge. Deploy the bridge adapter that connects your IM bot to the MCP server infrastructure. This handles message routing, authentication, and response formatting.
    4. Configure data connectors. Set up MCP servers for your key enterprise data sources — ERP, CRM, financial systems, and data warehouses.
    5. Test and iterate. Start with a pilot group of 10-20 power users. Collect feedback on query accuracy, response speed, and user experience before scaling organisation-wide.

    The Bottom Line

    DingTalk and Feishu are not just messaging platforms — they are the operating system for enterprise work in China. Deploying conversational BI through these platforms means meeting your users where they already work, eliminating the adoption friction that kills standalone analytics tools.

    At Beehive Strategy, we provide pre-built integrations for both DingTalk and Feishu, enabling you to deploy MCP-powered conversational BI directly into your team's daily workflow within days, not months. Book a free demo to see how.

    Frequently Asked Questions

    How does conversational BI integrate with DingTalk and Feishu?

    Conversational BI integrates with DingTalk and Feishu through their bot APIs. The AI agent connects to the enterprise semantic layer and responds to natural language queries within group chats or direct messages, delivering charts, tables, and narrative insights without requiring users to switch to a separate BI tool.

    What are the benefits of IM-native BI over standalone BI tools?

    IM-native BI eliminates the adoption barrier of switching to a separate application. Since employees already spend significant time in DingTalk or Feishu, delivering insights directly in their workflow dramatically increases engagement. Organisations deploying IM-native conversational BI see 3-5x higher adoption rates compared to standalone BI dashboard tools.

    How do you ensure data security when using IM-based BI?

    Data security is enforced at multiple levels: the semantic layer controls which data each user role can access, all queries are logged for audit, data never leaves the enterprise's controlled environment (the IM bot only receives pre-formatted results, not raw data), and optional row-level security ensures users only see data they are authorised to access.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:42

    ↗

    "Agentic" is an adjective that got sold as a noun, and most of the confusion around it comes from that one grammatical mistake. You cannot buy agentic AI, in the same way you cannot buy fast. Fast describes a car. Agentic describes how much of the deciding a system does...

    "Agentic" is an adjective that got sold as a noun, and most of the confusion around it comes from that one grammatical mistake.

    You cannot buy agentic AI, in the same way you cannot buy fast. Fast describes a car. Agentic describes how much of the deciding a system does without you. It is a measurement, and the useful question is never whether a system is agentic. It is how agentic, at which points, and what happens at those points when it gets it wrong.

    Once you look at it that way, a lot of vendor language becomes legible. And so does the reason a good share of these projects end badly: companies buy at one point on the scale and deploy as though they had bought a different one.

    The Word Comes From Agency, and That Is the Whole Idea

    Agency, in the ordinary sense, is the capacity to act on your own behalf. Applied to software, it describes systems that pursue a goal by choosing their own actions rather than executing prescribed ones.

    This is a genuine break from how software worked for sixty years. Traditional software is a set of instructions: if this, then that, in a sequence someone wrote. Generative AI extended the sentence but not the structure. It produces text, images and code on request. Remarkable output, zero agency. It waits.

    The agentic shift is not that the model got smarter. It is that we started letting model output determine what the program does next, instead of only what it prints. That is a much bigger architectural change than it sounds, and it is why the operational questions are so different.

    There is a clean way to hold the difference. Generative AI produces output. Agentic AI produces consequences. An output that is wrong is embarrassing. A consequence that is wrong has to be reversed, and somebody has to notice it first.

    The Dial Has Six Positions

    Autonomy is not on or off. In practice there are six recognisable positions on the dial, and knowing which one you are looking at is worth more than any feature comparison.

    Zero, fixed rules. If the customer types "invoice", show the invoice menu. No model involved. Still runs half the phone systems in Europe.

    One, the model as a component. The path is fixed by code, but a model does one job inside it. Classify this email. Summarise this document. Extract these five fields. Predictable, cheap, and vastly underrated. Most of the value being captured by AI in businesses right now sits here.

    Two, the model chooses within a fenced set. The system presents a small number of legitimate next steps and lets the model pick. Route to sales, route to support, route to a human. The model decides, but every branch was reviewed in advance by a person.

    Three, the model plans within limits. Here it gets to sequence its own steps and use tools in an order nobody prescribed, but inside hard boundaries: a step cap, a defined tool set, a spending limit, and an approval gate before anything irreversible. This is where serious production agents live.

    Four, the model plans and acts largely unsupervised. Real autonomy over a defined domain, humans reviewing outcomes rather than actions. Rare, and appropriate only where being wrong is cheap and recoverable.

    Five, open-ended autonomy. The system sets its own goals and acquires its own capabilities. This does not exist in any business deployment I have seen, and where it is being marketed the demonstration is doing a lot of work.

    The honest state of the field in 2026 is that almost everything that works in production sits at levels one, two and three, and almost everything that is marketed is described in the language of level four.

    That mismatch is not always dishonesty. Level three with a good gate is genuinely useful and sounds boring. Level four sounds like the future. Marketing departments choose accordingly.

    Gartner Went and Counted

    There is a number attached to this, and it is blunter than most analyst findings.

    When Gartner examined the market of vendors claiming agentic capability, it concluded that of thousands of such vendors, roughly 130 were building something that warranted the term. Everyone else had relabelled existing products. The firm coined "agent washing" for the practice, alongside its forecast that more than 40 percent of agentic AI projects will be cancelled by the end of 2027, driven by escalating costs, unclear business value and inadequate risk controls.

    The MIT Project NANDA work from August 2025 pointed at the same wall from a different angle: 95 percent of generative AI pilots at that point showed no measurable contribution to profit and loss. The technology worked. The projects did not.

    Both findings say the same thing. The failures are not capability failures. They are failures to match the level of autonomy purchased to the level of autonomy the process could actually absorb.

    The One Question That Settles It

    When a vendor says their system is agentic, there is a single question that resolves it faster than any demo:

    Given identical input twice, can the system take two different paths, and who is told when it chooses wrong?

    The first half tests whether the model is genuinely deciding. If the answer is no, the path is fixed and you are looking at a workflow with a model inside it. That is a perfectly good product. It is not an agentic one, and it should not carry agentic pricing.

    The second half is the one that matters more, and it is the one that gets a vague answer. "There is a dashboard" is not an answer. "The team reviews it weekly" is not an answer either, if the agent can issue refunds on Tuesday. A real answer names the class of action, the person, and the latency. Refunds above 200 euros go to the shift lead before execution. Anything touching a contract goes to a named human, always. Everything else is logged and sampled daily.

    A vendor who can answer that has thought about production. A vendor who cannot has built a demo.

    Why Less Autonomy Is Usually the Better Purchase

    There is a persistent assumption that more autonomy is more advanced, and therefore better. In deployment it is closer to the reverse.

    Anthropic's engineering guidance on this is unusually direct for a company that sells models. Their recommendation is to find the simplest solution that works and to increase complexity only when it demonstrably improves outcomes, on the grounds that agentic systems trade latency and cost for task performance. That is a vendor telling you to buy less of what they sell, which is worth noticing.

    The practical reasons stack up quickly. Every degree of autonomy you add multiplies the number of paths through the system, and every path is a path you cannot fully test. It increases cost, because deciding is expensive and a system that decides twelve times per task costs twelve decisions. It makes failure harder to locate, because the failure is in a sequence rather than a step. And it moves the burden of proof: at level one you can show what the system will do, at level four you can only show what it did.

    Three examples of the same problem solved at three levels make the point better than the theory.

    A company receives 400 applications a month. At level one, a model extracts qualifications and experience into a structured form and a human reads the form. Fast, cheap, auditable, and the recruiter still decides. At level three, an agent reads each application, checks it against the role, cross-references the candidate's public profile and produces a ranked shortlist with reasoning. More useful, more expensive, and it now needs bias review, because the ranking is a decision with legal weight. At level four, the agent schedules interviews on its own. Under EU rules that is a high-risk use in employment, and the compliance burden alone changes the economics of the whole project.

    Same task. Three completely different products. Only one of them is right for a given company, and it is usually not the most autonomous one.

    Where the EU Draws Its Own Line

    There is a legal dimension that changed on 2 August 2026, and it applies whether or not you consider your system agentic.

    The transparency obligations in Article 50 of the EU AI Act came into force on that date. Where a person interacts with an AI system, that has to be disclosed. Synthetic media has to be marked. These were not postponed by the Digital Omnibus, which deferred other parts of the regulation.

    Read against the autonomy dial, that is a floor rather than a ceiling. You do not need permission to run an agent. You do need the person on the other end to know they are talking to one. In practice this rules out one specific product design that some vendors still push, which is the agent that presents itself as a named human employee. Whatever you think of that as a marketing idea, in the EU it now has a compliance answer.

    The Version I Would Actually Recommend

    If I had to compress this into advice for a company deciding what to buy, it would be three sentences.

    Buy the lowest level of autonomy that solves your problem, because every level above it costs you money and testability. Put the human gate in front of the actions you cannot reverse, not in front of everything, because a gate on every action is just a slower employee. And make sure somebody owns the system after handover, because an agent nobody watches is correct until the day the process changes and then quietly wrong for months.

    That is how we build them. One task where the judgement is the bottleneck, a small tool set, hard limits on the loop, a gate before anything irreversible, and testing against real cases from the customer's own history rather than invented ones. Most of what we deliver sits at level two or three, deliberately. Our own operation runs on agents built that way, doing research, monitoring and preparation every day, and that is the standard worth applying to any supplier: ask whether they run the thing they are selling you.

    The word "agentic" will be diluted further, because words attached to budgets always are. The dial will not move. Somewhere in every system there is a point where either your code decides or the model decides, and knowing exactly where that point sits is the entire discipline. If you want to work out where it should sit for a specific process in your company, that is where our agent work begins.

    This article was originally published on studiomeyer.io.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:38

    ↗

    Building and maintaining enterprise knowledge graphs that connect disparate data sources, enabling AI systems to understand relationships and generate more accurate insights. Key Insight: Enterprise Knowledge Graphs: Unlocking AI-Driven Insights — as of 27 January 2025,...

    Building and maintaining enterprise knowledge graphs that connect disparate data sources, enabling AI systems to understand relationships and generate more accurate insights.

    Key Insight: Enterprise Knowledge Graphs: Unlocking AI-Driven Insights — as of 27 January 2025, enterprises worldwide are accelerating adoption of AI-powered solutions, with measurable improvements in efficiency, decision-making speed, and competitive positioning across technology, strategy, and industry-specific applications.

    The Technology Landscape in Early 2025

    The enterprise technology landscape has undergone a remarkable transformation as we move deeper into 2025. Organisations that once viewed artificial intelligence as an experimental initiative are now treating it as a core operational capability. The emergence of the Model Context Protocol (MCP) as a standardised way for AI systems to interact with data platforms has fundamentally changed how enterprises architect their AI infrastructure. Rather than building custom integrations for every data source, development teams can now leverage MCP connectors that provide consistent, secure, and governed access to data across the entire enterprise.

    According to recent industry surveys, over 67% of Fortune 500 companies have initiated at least one production-grade AI deployment, up from just 23% at the beginning of 2024. This acceleration is driven by several converging factors: the maturation of large language models (LLMs), the availability of enterprise-grade vector databases, and the growing realisation that conversational interfaces can dramatically reduce the time from question to insight. The competitive landscape has shifted from "should we adopt AI?" to "how quickly can we scale our AI capabilities?"

    • Model Context Protocol adoption has increased 340% year-over-year, with major cloud providers and data platform vendors announcing native MCP support
    • Vector database deployments have become standard infrastructure, with enterprises running an average of 3.2 vector stores for different use cases including semantic search, recommendation engines, and RAG pipelines
    • Small language models (sub-7B parameters) now handle 60% of enterprise NLP tasks, offering significant cost savings while maintaining 85-95% of large model accuracy on domain-specific tasks
    • MLOps maturity has advanced considerably, with 45% of enterprises now operating automated model retraining pipelines that trigger based on data drift detection

    Architectural Patterns and Implementation Strategies

    The most successful enterprise AI implementations in early 2025 share a common architectural pattern: a semantic layer that sits between natural language interfaces and the underlying data infrastructure. This semantic layer serves multiple critical functions. First, it translates natural language queries into optimised SQL or API calls, handling the complex mapping between business terminology and technical data models. Second, it enforces consistent business logic and metric definitions, eliminating the discrepancies that often arise when different teams calculate KPIs independently. Third, it provides a governance boundary that ensures all data access complies with organisational policies and regulatory requirements.

    Retrieval-Augmented Generation (RAG) has evolved from a simple pattern of "embed documents and search" to sophisticated multi-stage architectures. Modern RAG systems incorporate query decomposition, where complex questions are broken into sub-queries; hybrid search that combines vector similarity with traditional keyword matching; and multi-hop reasoning that synthesises information from multiple retrieved chunks. Enterprises report that these advanced RAG techniques have reduced hallucination rates by up to 78% compared to naive retrieval approaches, making AI-generated insights trustworthy enough for production decision-making.

    The decision between fine-tuning and RAG remains one of the most consequential choices enterprises face. Our analysis of 200+ enterprise AI deployments reveals a clear decision framework: use RAG when data changes frequently, when transparency and auditability are required, and when the knowledge base exceeds 100,000 documents. Fine-tuning is preferred when the task requires deep domain adaptation, when latency constraints rule out real-time retrieval, or when the model needs to internalise specific reasoning patterns rather than simply retrieve information.

    Security and Operational Considerations

    As AI systems become deeply embedded in enterprise operations, security considerations have moved to the forefront. Prompt injection attacks, where malicious inputs manipulate AI behaviour, have emerged as a significant threat vector. Enterprises are responding with multi-layered defences including input sanitisation, output validation, and runtime monitoring that detects anomalous model behaviour. The concept of "AI firewalls" has gained traction, with dedicated security layers that inspect and filter both inputs to and outputs from LLM systems.

    Operational excellence in AI requires robust monitoring and observability. Leading enterprises track model performance metrics including accuracy, latency, throughput, and cost per inference. More importantly, they monitor for concept drift and data quality degradation that can silently erode model performance over time. Automated alerting systems notify data science teams when model performance falls below predetermined thresholds, triggering investigation and potential retraining cycles. This operational discipline is what separates enterprises that successfully run AI in production from those that struggle with unreliable, degrading models.

    The total cost of ownership for enterprise AI extends far beyond model training expenses. Our benchmarking data shows that infrastructure costs (compute, storage, networking) typically represent 35-40% of total AI spend, while talent costs (data scientists, ML engineers, AI product managers) account for another 30-35%. The remaining 25-30% covers data preparation, governance, compliance, and ongoing maintenance. Enterprises that fail to account for these full costs often face budget overruns that can jeopardise entire AI programmes.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:34

    ↗

    Originally published on the BetaDrop blog. Apple's TestFlight is the gold standard for iOS beta testing. It's built right into the ecosystem and easy for users. But it has a hard ceiling: 10,000 external testers. For most apps, this is plenty. But for viral hits or...

    Originally published on the BetaDrop blog.

    Apple's TestFlight is the gold standard for iOS beta testing. It's built right into the ecosystem and easy for users. But it has a hard ceiling: 10,000 external testers.

    For most apps, this is plenty. But for viral hits or large-scale open betas, hitting this wall can stop your momentum cold. Here is how to manage and overcome these limits.

    Understanding the Limits

    • Internal Testers: Max 100. No Beta App Review required, and builds appear almost immediately. Each internal tester must be a member of your App Store Connect team and can install on up to 30 devices.
    • External Testers: Max 10,000 per app. Invited by email or a Public Link. The first build of each new version must clear Beta App Review before external testers can install it.

    The 10,000 ceiling is per app (Bundle ID), not per group. You can split external testers across up to 100 groups, but every group draws from the same 10,000-slot pool. If you only need to hand a build to a handful of QA folks or a client, an OTA install link sidesteps the review wait entirely.

    Strategy 1: Pruning Inactive Testers

    The 10k limit is for active slots, not lifetime adds. Many users install a beta and never open it again. You should aggressively prune these users to make room for fresh testers.

    How to prune:

    1. Go to App Store Connect → TestFlight.
    2. Select your External Group.
    3. Sort by Status or Sessions.
    4. Select users who haven't installed or have 0 sessions in 30 days.
    5. Click Delete.

    Pro Tip: You can automate this using fastlane pilot scripts to remove testers who haven't launched the latest build.

    Strategy 2: Use Enterprise Distribution

    If your goal is to test internally with a massive organization (e.g., a company with 50,000 employees), TestFlight isn't the right tool. Use the Apple Developer Enterprise Program.

    This allows unlimited distribution to devices owned by your organization, bypassing the 10,000 user limit entirely.

    Strategy 3: Apple Business Manager (Custom Apps)

    For B2B apps where you are distributing to specific partners or clients, use Apple Business Manager. This allows you to privately distribute specific apps to another organization's VPP (Volume Purchase Program) account. The receiving organization then distributes the app to their users via MDM.

    Strategy 4: "Rolling" Betas

    If you have a waiting list, create a "churn" system. Invite 1,000 users, give them 2 weeks to test, and then remove them to invite the next 1,000. This keeps feedback fresh and allows you to test with far more than 10k unique users over time.

    The "Nuclear Option": Multiple App IDs

    Technically, the limit is per App ID (Bundle ID). You could create com.app.beta1, com.app.beta2, etc., each with its own TestFlight group.

    Warning: This separates your analytics, crash reports, and requires managing multiple app records. It is messy and generally not recommended unless absolutely necessary.

    Skip the Cap With OTA Links

    If tester counts are the real bottleneck, you don't have to fight the 10,000-slot math at all. BetaDrop is a free TestFlight alternative: upload a signed .ipa and you get an instant over-the-air install link plus a QR code with no tester cap, no tester accounts, and no Beta App Review. Testers open the link in their phone browser, tap install, and they're running the build in seconds — the same OTA approach walked through in distributing iOS apps without TestFlight. Builds up to 512 MB are supported, and links are ephemeral, so stale betas clean themselves up instead of eating slots.

    Summary

    Hitting the 10,000 user limit is a "good problem" to have—it means your app is popular! Start by cleaning up inactive users, and if you truly need massive scale, consider if you are actually ready for the App Store production release instead of a beta.

    Frequently Asked Questions

    What is the limit for TestFlight external testers?

    TestFlight allows up to 10,000 external testers per app. External testers are invited by email or through a public link, and the first build of each new version must pass Apple's Beta App Review before they can install it.

    How many internal TestFlight testers can I have?

    You can have up to 100 internal testers, and each internal tester can install on up to 30 devices. Internal testers must be members of your App Store Connect team, and their builds are available immediately without Beta App Review.

    Do external testers require Beta App Review?

    Yes. The first build of each new version you send to external testers must pass Apple's Beta App Review, which can add a wait before testers get the build. Later builds of the same version usually go out without another full review.

    Does deleting a tester free up a spot?

    Yes. If you remove a tester from a group in App Store Connect, that spot becomes available for someone new immediately. Because the 10,000 cap counts active slots rather than lifetime invites, pruning inactive testers is the simplest way to make room.

    Can I pay for more TestFlight slots?

    No. Apple does not offer an option to purchase additional TestFlight tester slots. The 10,000 external tester cap is fixed, so scaling past it means pruning testers, switching distribution method, or moving to a public release.

    How can I distribute an iOS beta without a tester cap?

    Upload your signed .ipa to BetaDrop and you get an instant over-the-air install link and QR code with no tester limit, no tester accounts, and no Beta App Review. Testers open the link in their phone browser and install directly. Builds can be up to 512 MB; registered builds expire within about 30 days, and anonymous guest links expire after 24 hours.

    Have you hit TestFlight's tester cap? How did you work around it — Ad Hoc, Enterprise, or moving off TestFlight entirely?

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:33

    ↗

    Qwen 3.8 Max bills $2 per million input tokens and $6 per million output, and its cheapest reliable configuration is not the one the API seems to offer. Turning thinking off with reasoning_effort: "none" dropped our two-step arithmetic task from 4/4 correct to 1/6, while...

    Qwen 3.8 Max bills $2 per million input tokens and $6 per million output, and its cheapest reliable configuration is not the one the API seems to offer. Turning thinking off with reasoning_effort: "none" dropped our two-step arithmetic task from 4/4 correct to 1/6, while granting a hard budget of just 16 thinking tokens restored 6/6, averaging a fifth fewer output tokens than the default. Launch week produced loud capability claims and little to check them against: no model card, no public benchmark table, internal-only evals, which Hacker News was quick to flag. Billing behavior is different, because anyone with an API key can measure it. We measured qwen3.8-max on day one through the Synthorai gateway: every thinking control the surface accepts, the reasoning tax at each setting, the implicit cache's floor and build lag, the 1M-context claim, and what carries over from Qwen 3.7.

    TL;DR

    • qwen3.8-max's seven reasoning_effort values collapse into four measured behaviors: off, a 4,096-token cap, a 16,384 cap, and unbounded.
    • thinking_budget is exact: request 16 tokens and the meter reads 16; the ceiling is 262,144.
    • With thinking off, 2-hop math fell to 1/6 correct; a 16-token budget scored 6/6 for less.
    • The implicit cache builds in under 0.3 seconds and reads at $0.25/1M, but nothing caches below roughly a 4,300-token prompt.
    • Input caps are exact and fail-loud: 991,808 (thinking off), 983,616 (thinking on).

    What do the thinking controls on Qwen 3.8 Max actually do?

    Three parameters are live, and they are not the three the documentation lists. Third-party docs describe reasoning_effort with three values (low, medium, xhigh, default xhigh). The surface we measured accepts seven: none, minimal, low, medium, high, xhigh, and max; an invalid value is rejected with that exact allowlist. Alongside it, the native thinking_budget (a positive integer up to 262,144) and enable_thinking (boolean) pass through to the provider, which validates them itself: a budget of 0 or 262,145 comes back as a 400 quoting the bound.

    On ordinary tasks the effort levels are indistinguishable. Across trivial Q&A, 2-hop math, and a medium combinatorics problem, low, medium, high, xhigh, and the default all burned reasoning tokens in the same noisy band; nothing separated them. The separation only appears on a task that wants to think for tens of thousands of tokens. On a prime-counting problem whose unconstrained run burned 45,129 reasoning tokens, the levels finally started to bind:

    Setting Reasoning tokens on the deep task Correct?
    default (omitted) 45,129 yes
    minimal 4,096 (exact cap) no
    low 4,096 (exact cap) no
    medium 16,384 (exact cap) no
    high 44,348 (no cap reached) yes
    xhigh 38,029 (no cap reached) yes
    max 35,300 (no cap reached) yes

    The mental model that fits every observation: each effort level is a preset thinking-budget cap, and the seven names collapse into four behaviors. Off is one (none and enable_thinking: false behave identically). Minimal and low share the same 4,096-token cap. Medium quadruples it to 16,384. High, xhigh, max, and the default form the fourth tier: none of them bound on this task, burning between 35K and 45K tokens across runs, which is ordinary variance at this depth. If the top three differ at all, the split sits above 45K thinking tokens, deeper than most production traffic ever goes; the docs' claim that the default is xhigh is consistent with everything we measured. The split is stark: every capped run answered wrong, every uncapped run answered right. Below the cap, behavior is identical across levels, which is why the dial feels dead on everyday traffic. Above it, the cap truncates thinking mid-task. If you want a specific ceiling, skip the presets and set thinking_budget directly; the next section takes that parameter apart.

    How does thinking_budget actually work?

    It is enforced to the token, it is a ceiling rather than a quota, and a mid-sized cap can make a task cost more than setting no cap at all. thinking_budget is DashScope's native integer parameter (1 to 262,144, documented default 131,072, inherited from the open-weight Qwen3 family) that sets the maximum reasoning tokens for a call. Requests of 0 or 262,145 are rejected with a 400 quoting the range. Below the ceiling nothing changes: a budget of 8,192 on a task that naturally thinks for a few hundred tokens burned 331 and 485, exactly as if no budget were set. At the ceiling, enforcement is token-exact: budgets of 16, 64, and 256 stopped reasoning at precisely 16, 64, and 256 in every run.

    What happens at the cutoff is the interesting part. The model does not abandon the work; it stops reasoning and finishes the job in the visible answer. On a medium combinatorics task (count domino tilings of a 2x12 grid), every budget produced the right answer, but the totals are not what you would predict:

    Budget Reasoning burned Total completion tokens
    none set (default) 226-303 234-311
    16 16 (exact) 368-406
    64 64 (exact) 399-408
    256 256 (exact) 776-787
    8,192 331-485 (never bound) 339-493

    The curve is not monotonic. A 256-token budget cost 2.5x more than no budget: the model spent its allowance starting a reasoning chain, lost it mid-thought, and re-derived the answer step by step in the visible channel. The tightest budget beat the mid-sized ones because a 16-token allowance is too small to start anything, so the model goes straight to compact visible work. Three rules fall out. First, tiny budgets are a real lever on shallow-to-medium work: 16 tokens went 6/6 on our 2-hop math batch at 98-161 total tokens against the default's 126-207. Second, never deploy mid-sized caps on traffic of unknown depth; they land in the dead zone where the cap truncates real reasoning and you pay for the work twice (the deep-task rows above, 4,096 and 16,384, are the same failure at scale, and there they also answered wrong). Third, a binding budget changes the shape of the output: capped runs answer with their work shown, which matters if a parser expects a bare result.

    How much of the day-one documentation survives measurement?

    About half, and the split is worth publishing because nothing else about this launch is independently checkable yet. Every number below is from our own meter and probes:

    Documented claim Measured verdict
    Input caps: 991,808 (non-thinking) / 983,616 (thinking) Exact; oversized requests 400 quoting the bound
    thinking_budget range: positive integers up to 262,144 Exact; 0 and 262,145 both rejected
    List price $2 in / $6 out per 1M Meter matched to the fourth decimal on every call
    Cache reads at 0.25x credits Exact: $0.25/1M, no write premium
    reasoning_effort values: low, medium, xhigh Wrong: seven values accepted, including a full off switch
    Max output 131.07K "in both modes" Wrong both ways: thinking off rejects max_tokens above 65,536; thinking on accepted every value we tried, up to 393,216
    Multi-turn clients "must return unmodified reasoning_content" Unenforced: omitted and tampered histories accepted
    "Context caching supported" (no details) Real, but the load-bearing spec is undocumented: ≈4.3K floor, 15-45 min lifetime

    The pattern favors the billing plane: everything that decides what you pay is precise and honestly enforced, while the parameter documentation lags what the surface actually does.

    Does turning thinking off save money?

    It saves tokens and costs correctness, and there is a better trade two lines away. In our quotable batch of 2-hop arithmetic (1850 crates times 24 parts, 75% shipped, 3,120 arrive), the default configuration went 4/4 at 126-207 completion tokens per call. reasoning_effort: "none" produced 4-5 token answers and went 1/6. The same prompt with thinking_budget: 16 went 6/6 at 98-161 completion tokens, cheaper than the default and as accurate, on this task class. One-hop arithmetic stayed 3/3 even at none, so the off switch is safe for lookups and single-hop transforms; it is multi-step work that collapses. This is not a 3.8 regression: qwen3.7-max with thinking off went 3/6 on the identical batch.

    The dead zone described in the budget section has a dollar figure on hard tasks. On the deep prime-counting run, the low preset burned its 4,096 thinking tokens, spilled 13,882 more of visible candidate-checking, and still answered wrong: $0.11 for an incorrect answer, versus $0.27 for the default's correct one. The off switch shows the same physics on hard tasks: with thinking disabled outright, both 3.8 and 3.7 poured a 13-15K-token enumeration into the visible answer, one landing the count and one missing it by a single prime, in single shots each. A budget that binds mid-reasoning can raise total spend while lowering quality. Cap thinking on tasks you know are shallow; let deep tasks think.

    Is the 1M-token context window real?

    Effectively yes, with exact and honest limits. The API accepts up to 991,808 input tokens with thinking off and 983,616 with thinking on, and both bounds are enforced fail-loud: an oversized request is rejected with a 400 that quotes the exact limit, rather than silently truncating your document. Needle recall worked at every size we probed, 161K, 677K, and 919K tokens, returning the planted override code verbatim in 11 to 63 seconds. A 919K-token request costs about $1.84 at list price, so the window is real but a full-window call is a design decision, not a default.

    What does the implicit cache deliver, and where is the floor?

    The fastest cache build we have measured, behind an unusually high floor. Repeating a salted 6,103-token prompt produced a hit on the very next request 0.3 seconds later; there is no warm-up window to engineer around, unlike Gemini's tens-of-seconds build. Hits kept coming at +5 and +15 minutes with no re-prime, and the entry was gone by +45, so the working lifetime sits somewhere between 15 and 45 minutes of silence. Reads bill at $0.25 per million, 0.125x the input rate, and there is no write premium; the discount arrived automatically in the cached_tokens field and the metered cost.

    The floor is the catch. A 4,221-token prompt never produced a hit; a 4,360-token one did, and every first hit was exactly 4,096 tokens. Below roughly 4.3K tokens of prompt, this cache does not exist for you, a sharp contrast with Claude's 1,024-token minimum and Kimi K3's small-block automatic caching. Above the floor, hits quantize in 128-token blocks (we observed 4,096, 8,320, 12,544, and 16,768), but coverage of the primed prefix ranged from 51% to 96%, so budget on discounting most of a long prefix, not all of it.

    Do structured output and tool calls pay the reasoning tax?

    By default yes, and they are the safest place to cut it. Strict json_schema output works, and it is doing real enforcement: the same extraction without a schema came back wrapped in markdown fences. With the default configuration, a four-field invoice extraction burned 252 reasoning tokens before emitting 57 tokens of JSON. With reasoning_effort: "none" it produced valid, correct JSON in 54 total tokens, a 5.7x cut; thinking_budget: 16 sat in between. Tool selection behaved the same way: the model called the right function with thinking off at a third of the default's tokens. Single-step extraction and routing are exactly the shape where the off switch is safe, and at $6/1M output the habit compounds.

    One more billing note for agent builders: the API returns the full chain of thought in reasoning_content, and the docs instruct multi-turn clients to send it back unmodified. The instruction is not enforced. We replayed turns with the reasoning included, omitted, and deliberately tampered; all three were accepted, and short-chain accuracy was unaffected. Replayed reasoning bills as ordinary input tokens, so omitting it is a real saving on multi-turn traffic until you see quality reasons not to.

    What carries over from Qwen 3.7, and what changed?

    The tokenizer is unchanged and your token budgets port directly. Identical English, Chinese, Japanese, and code corpora tokenized to identical counts across qwen3.8-max, qwen3.7-max, qwen3.7-plus, qwen3.6-flash, and qwen3.5-flash, so per-language cost planning from our tokenizer-by-language study carries over unchanged.

    Two things did change. First, 3.8-max carries a fixed prompt overhead its siblings do not: the same one-character message counted 49 prompt tokens on 3.8-max against 11 on every other Qwen we probed, a constant +38-token rider per call. It is noise on long prompts and a measurable percentage on short, high-frequency ones. Second, thinking is always available rather than a mode switch, with the seven-level dial and exact budget parameter above; 3.7's controls were coarser. One pricing clarification, because two schemes circulated at the preview: the Token Plan subscriptions ($6 to $68 monthly, with deep off-peak discounts) price Alibaba's own apps, not the API. Through the API you pay the $2/$6 list rate, and our gateway meter matched it to the fourth decimal on every call in the study.

    FAQ

    Can you turn off thinking on Qwen 3.8 Max?

    Yes, fully: reasoning_effort: "none" (or enable_thinking: false) eliminates reasoning tokens entirely. Reserve it for single-hop work. On 2-hop arithmetic it scored 1/6 in our batch while a 16-token thinking_budget scored 6/6 at comparable or lower token counts, so the floor setting for multi-step traffic should be a small budget, not the off switch.

    What is the minimum prompt size for Qwen 3.8 Max's cache?

    About 4,300 tokens in our probes: a 4,221-token prompt never hit, a 4,360-token one did, and first hits are always exactly 4,096 tokens. Below the floor no discount exists; above it, reads bill at $0.25/1M with no write premium and the entry is readable 0.3 seconds after priming.

    Is reasoning_effort supported on Qwen 3.8 Max?

    Seven values are accepted (none, minimal, low, medium, high, xhigh, max), but levels behave as thinking-budget caps that only differ once a task thinks past them. For deterministic control, set thinking_budget directly: it is enforced token-exactly, rejects 0, and tops out at 262,144. Client tooling currently disagrees about which tiers exist; the list above is what the surface accepted on day one.

    Do you have to send reasoning_content back in multi-turn conversations?

    The documentation says yes; the API does not check. Omitted and even tampered reasoning history was accepted without error or short-chain accuracy loss in our probes, and replayed reasoning bills as normal input. Skipping the replay is a legitimate cost lever until your own evals show quality loss on long chains.

    Measured 2026-08-03 through the Synthorai gateway against qwen3.8-max (comparison arms on qwen3.7-max, qwen3.7-plus, qwen3.6-flash, qwen3.5-flash): dial-acceptance, garbage-value, and max_tokens boundary probes, a 45K-natural-burn deep task to bind the effort caps, a fixed quotable batch for the accuracy cliff (n=4-6 per arm, salted), salted cache pairs with 2-3s pacing plus gap ladders, needle and overflow probes at 161K-919K tokens, and identical four-corpus tokenizer counts. Dollar figures are billed-cost readings from the gateway meter at list rates ($2/$6 per 1M). Preview-period discounts, rates, and behavior may change; verify against your own usage records.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:32

    ↗

    Implementing the Model Context Protocol in your enterprise enables AI agents to interact with your data systems through standardized, secure, and composable tool interfaces. This step-by-step guide covers everything from initial architecture design through production...

    Implementing the Model Context Protocol in your enterprise enables AI agents to interact with your data systems through standardized, secure, and composable tool interfaces. This step-by-step guide covers everything from initial architecture design through production deployment, providing practical guidance for teams building MCP-based AI integration.

    Step 1: Architecture Design and Assessment

    Before writing any code, conduct a thorough assessment of your existing data infrastructure. Map all data sources that AI agents should access: databases (Snowflake, PostgreSQL, BigQuery), APIs (REST, GraphQL), file systems, real-time streams, and SaaS applications. Identify which data sources are most valuable for AI-driven analytical workflows and prioritize them for initial MCP server development.

    Design your MCP architecture around three patterns: direct MCP servers that connect to data sources, aggregation MCP servers that compose multiple data sources into unified analytical tools, and workflow MCP servers that orchestrate multi-step analytical processes. This layered approach provides flexibility and allows incremental adoption.

    • Inventory existing data sources: Catalog all databases, APIs, and data services suitable for AI access
    • Classify by priority: High-value, frequently-queried sources first; secondary sources in later phases
    • Design server topology: Direct servers for simple sources, aggregation servers for complex queries
    • Document security requirements: Map existing access controls to MCP authorization model

    Step 2: Environment Setup and SDK Selection

    The official MCP SDK supports TypeScript/JavaScript and Python, with community SDKs for Go, Rust, Java, and C#. Choose your SDK based on team expertise and existing infrastructure. TypeScript is recommended for most enterprises due to its type safety, strong ecosystem, and compatibility with most deployment environments.

    Set up your development environment with proper version control, CI/CD pipelines, and testing frameworks. Create a standardized project structure for MCP servers that includes tool definitions, input/output schemas using JSON Schema, and comprehensive error handling patterns.

    • TypeScript SDK: Recommended for most enterprises; strong typing and large ecosystem
    • Python SDK: Best for data-science-heavy teams with existing Python infrastructure
    • Project structure: Standardized layout with tool definitions, schemas, and error handling
    • CI/CD: Automated testing, schema validation, and deployment pipelines from day one

    Step 3: Building Your First MCP Server

    Start with a high-value, relatively simple data source to validate the approach. Define tools that expose the most common analytical operations: data retrieval with filtering and aggregation, metadata discovery (available tables, columns, relationships), and metric calculations. Each tool should have a clear JSON Schema definition for its input parameters and a well-documented output format.

    The key to effective MCP tool design is providing enough context for AI agents to use tools correctly without overwhelming them. Include descriptive names, detailed descriptions, and clear parameter constraints in your tool definitions. Test each tool independently before integrating into the broader MCP server.

    • Tool naming: Use descriptive, action-oriented names (e.g., query_sales_data, get_kpi_summary)
    • Schema design: JSON Schema with clear descriptions, type constraints, and examples
    • Context hints: Include descriptions that help AI agents understand when and how to use each tool
    • Independent testing: Validate each tool in isolation before server-level integration

    Step 4: Security Implementation

    Enterprise MCP deployments require robust security. Implement transport-level security (TLS 1.3) for all MCP connections. Layer authentication on top of the MCP transport using OAuth 2.0 bearer tokens or API key management systems. Implement resource-level access controls that map to your existing data governance policies.

    Critical security measures include: token-based authentication with short-lived tokens and refresh mechanisms, tool-level authorization ensuring users can only invoke tools they have permission for, comprehensive audit logging of all MCP tool invocations (who invoked what, when, with what parameters), and rate limiting to prevent abuse.

    • Transport security: TLS 1.3 mandatory for all MCP connections
    • Authentication: OAuth 2.0 or API keys integrated with enterprise identity provider
    • Authorization: Resource-level and tool-level access controls mapped to existing policies
    • Audit logging: Complete invocation logs for compliance and security monitoring

    Step 5: Testing Strategy

    Implement a comprehensive testing strategy covering three levels: unit tests for individual tool logic, integration tests for MCP server behavior, and end-to-end tests simulating real AI agent interactions. Unit tests should cover edge cases, error conditions, and boundary values. Integration tests validate the MCP protocol handshake, tool discovery, and tool invocation.

    End-to-end testing is critical for MCP deployments. Use actual AI models (Claude, GPT) to interact with your MCP servers and verify that tools are discovered correctly, invoked with appropriate parameters, and return results that AI agents can interpret. Log all AI-agent interactions for analysis and accuracy improvement.

    • Unit tests: Individual tool logic, edge cases, error handling, and boundary values
    • Integration tests: MCP protocol compliance, tool discovery, and invocation flows
    • End-to-end tests: Real AI model interactions validating the complete tool chain
    • Accuracy testing: 100+ representative queries from actual business users

    Step 6: Production Deployment

    Deploy MCP servers behind a load balancer with health checks and auto-scaling capabilities. Use containerized deployments (Docker/Kubernetes) for consistent environments and easy scaling. Implement monitoring for latency, error rates, and invocation volumes. Set up alerting for degradation patterns.

    Configure MCP server connections in your AI application layer. Most AI agent frameworks (Claude Desktop, LangChain, AutoGen) support MCP natively or through plugins. Ensure proper connection configuration including authentication, timeouts, and retry logic.

    • Container deployment: Docker images with standardized runtime environments
    • Load balancing: Distribute traffic across multiple server instances
    • Monitoring: Latency, error rates, invocation volumes, and resource utilization
    • AI framework integration: Configure MCP connections in Claude, LangChain, or AutoGen

    Step 7: Monitoring and Iterative Improvement

    Production monitoring goes beyond uptime tracking. Monitor tool invocation patterns to understand which analytical capabilities are most valuable. Track query accuracy by comparing AI-generated results with expected outcomes. Use invocation logs to identify and fix common failure patterns.

    Implement feedback loops that allow the system to improve over time. Analyze failed invocations to identify tool description improvements, add new tools based on recurring unmet needs, and refine existing tool parameters based on actual usage patterns. This iterative improvement cycle is essential for maintaining high accuracy as business needs evolve.

    • Usage analytics: Track which tools are most/least used and identify gaps
    • Accuracy monitoring: Compare AI outputs against expected results continuously
    • Feedback loops: Use failure analysis to improve tool descriptions and parameters
    • Iterative expansion: Add new tools and data sources based on real demand patterns

    Step 8: Scaling and Governance Maturity

    As your MCP deployment matures, establish governance frameworks that ensure consistency and quality across all MCP servers. Create a centralized tool registry documenting all available MCP tools, their purposes, and their data source dependencies. Implement standard naming conventions, schema patterns, and documentation requirements.

    Build a Center of Excellence (CoE) that manages the MCP ecosystem, curates best practices, and provides consulting to teams building new MCP tools. The CoE should maintain quality standards, conduct regular security reviews, and coordinate cross-team tool sharing to avoid duplication and maximize the value of your MCP investment.

    • Tool registry: Centralized catalog of all MCP tools with metadata and ownership
    • Governance standards: Naming conventions, schema patterns, documentation requirements
    • Center of Excellence: Cross-functional team managing MCP ecosystem quality
    • Continuous improvement: Regular reviews and updates based on production learnings

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:30

    ↗

    A comprehensive security analysis of LLM deployments covering prompt injection, data exfiltration, model poisoning, and supply chain attacks, with practical mitigation strategies. Key Insight: Securing LLMs in Enterprise Deployments: Threats and Countermeasures — as of 24...

    A comprehensive security analysis of LLM deployments covering prompt injection, data exfiltration, model poisoning, and supply chain attacks, with practical mitigation strategies.

    Key Insight: Securing LLMs in Enterprise Deployments: Threats and Countermeasures — as of 24 March 2025, enterprises worldwide are accelerating adoption of AI-powered solutions, with measurable improvements in efficiency, decision-making speed, and competitive positioning across technology, strategy, and industry-specific applications.

    The Technology Landscape in Early 2025

    The enterprise technology landscape has undergone a remarkable transformation as we move deeper into 2025. Organisations that once viewed artificial intelligence as an experimental initiative are now treating it as a core operational capability. The emergence of the Model Context Protocol (MCP) as a standardised way for AI systems to interact with data platforms has fundamentally changed how enterprises architect their AI infrastructure. Rather than building custom integrations for every data source, development teams can now leverage MCP connectors that provide consistent, secure, and governed access to data across the entire enterprise.

    According to recent industry surveys, over 67% of Fortune 500 companies have initiated at least one production-grade AI deployment, up from just 23% at the beginning of 2024. This acceleration is driven by several converging factors: the maturation of large language models (LLMs), the availability of enterprise-grade vector databases, and the growing realisation that conversational interfaces can dramatically reduce the time from question to insight. The competitive landscape has shifted from "should we adopt AI?" to "how quickly can we scale our AI capabilities?"

    • Model Context Protocol adoption has increased 340% year-over-year, with major cloud providers and data platform vendors announcing native MCP support
    • Vector database deployments have become standard infrastructure, with enterprises running an average of 3.2 vector stores for different use cases including semantic search, recommendation engines, and RAG pipelines
    • Small language models (sub-7B parameters) now handle 60% of enterprise NLP tasks, offering significant cost savings while maintaining 85-95% of large model accuracy on domain-specific tasks
    • MLOps maturity has advanced considerably, with 45% of enterprises now operating automated model retraining pipelines that trigger based on data drift detection

    Architectural Patterns and Implementation Strategies

    The most successful enterprise AI implementations in early 2025 share a common architectural pattern: a semantic layer that sits between natural language interfaces and the underlying data infrastructure. This semantic layer serves multiple critical functions. First, it translates natural language queries into optimised SQL or API calls, handling the complex mapping between business terminology and technical data models. Second, it enforces consistent business logic and metric definitions, eliminating the discrepancies that often arise when different teams calculate KPIs independently. Third, it provides a governance boundary that ensures all data access complies with organisational policies and regulatory requirements.

    Retrieval-Augmented Generation (RAG) has evolved from a simple pattern of "embed documents and search" to sophisticated multi-stage architectures. Modern RAG systems incorporate query decomposition, where complex questions are broken into sub-queries; hybrid search that combines vector similarity with traditional keyword matching; and multi-hop reasoning that synthesises information from multiple retrieved chunks. Enterprises report that these advanced RAG techniques have reduced hallucination rates by up to 78% compared to naive retrieval approaches, making AI-generated insights trustworthy enough for production decision-making.

    The decision between fine-tuning and RAG remains one of the most consequential choices enterprises face. Our analysis of 200+ enterprise AI deployments reveals a clear decision framework: use RAG when data changes frequently, when transparency and auditability are required, and when the knowledge base exceeds 100,000 documents. Fine-tuning is preferred when the task requires deep domain adaptation, when latency constraints rule out real-time retrieval, or when the model needs to internalise specific reasoning patterns rather than simply retrieve information.

    Security and Operational Considerations

    As AI systems become deeply embedded in enterprise operations, security considerations have moved to the forefront. Prompt injection attacks, where malicious inputs manipulate AI behaviour, have emerged as a significant threat vector. Enterprises are responding with multi-layered defences including input sanitisation, output validation, and runtime monitoring that detects anomalous model behaviour. The concept of "AI firewalls" has gained traction, with dedicated security layers that inspect and filter both inputs to and outputs from LLM systems.

    Operational excellence in AI requires robust monitoring and observability. Leading enterprises track model performance metrics including accuracy, latency, throughput, and cost per inference. More importantly, they monitor for concept drift and data quality degradation that can silently erode model performance over time. Automated alerting systems notify data science teams when model performance falls below predetermined thresholds, triggering investigation and potential retraining cycles. This operational discipline is what separates enterprises that successfully run AI in production from those that struggle with unreliable, degrading models.

    The total cost of ownership for enterprise AI extends far beyond model training expenses. Our benchmarking data shows that infrastructure costs (compute, storage, networking) typically represent 35-40% of total AI spend, while talent costs (data scientists, ML engineers, AI product managers) account for another 30-35%. The remaining 25-30% covers data preparation, governance, compliance, and ongoing maintenance. Enterprises that fail to account for these full costs often face budget overruns that can jeopardise entire AI programmes.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:30

    ↗

    "A Queue is a powerful design tool—but only when it solves the right problem." Throughout this mini-series, we've learned: why some work shouldn't happen immediately, how a Queue organizes background work, why Queues improve responsiveness, how to recognize Queue problems,...

    "A Queue is a powerful design tool—but only when it solves the right problem."

    Throughout this mini-series, we've learned:

    • why some work shouldn't happen immediately,
    • how a Queue organizes background work,
    • why Queues improve responsiveness,
    • how to recognize Queue problems,
    • and the design patterns where they naturally appear.

    At this point, it's easy to develop a dangerous habit:

    "Whenever I want a scalable system, I'll add a Queue."

    Experienced engineers know that's the wrong way to think.

    A Queue isn't the goal.

    It's one possible solution to a specific design problem.

    Let's look at the mistakes that often appear when teams use Queues without fully understanding the business requirements.

    Mistake 1: Putting Business-Critical Work Into a Queue

    Some work simply cannot be delayed.

    Imagine a login system.

    Login Request
    
    ↓
    
    Queue
    
    ↓
    
    Validate Password
    
    ↓
    
    Return Success
    

    This makes little sense.

    The user can't continue until authentication succeeds.

    The same applies to:

    • Processing payments
    • Reserving movie seats
    • Creating an order
    • Updating an account balance

    If the user depends on the result immediately, the work belongs in the request path—not in a Queue.

    Mistake 2: Assuming FIFO Always Matches the Business Rule

    A Queue follows First In, First Out.

    But business rules don't always.

    Imagine a hospital emergency department.

    Patient A
    
    Minor Injury
    
    ↓
    
    Patient B
    
    Heart Attack
    

    Should Patient B wait because they arrived later?

    Of course not.

    The business rule is priority, not arrival order.

    In that case, a Heap or another priority-based structure is a better fit.

    The lesson is simple:

    Choose the data structure that reflects the business rule—not the one you're most familiar with.

    Mistake 3: Ignoring Queue Backlogs

    Now imagine this situation.

    Producer creates 100 Tasks / Second
    
    ↓
    
    Queue
    
    ↓
    
    Consumer processes 20 Tasks / Second
    

    Every second, the Queue grows larger.

    Eventually:

    • processing delays increase,
    • memory usage grows,
    • users wait longer for background work to finish.

    A Queue isn't an infinite storage system.

    Whenever you introduce one, ask:

    Can the consumers keep up with the producers?

    Mistake 4: Assuming Work Always Succeeds

    Many developers think:

    "Once the task enters the Queue, I'm done."

    Not quite.

    Imagine this workflow.

    Queue
    
    ↓
    
    Email Worker
    
    ↓
    
    Email Service
    

    What happens if the email service is temporarily unavailable?

    The task still hasn't completed.

    Moving work into a Queue changes when it's processed.

    It doesn't guarantee that processing will succeed.

    Mistake 5: Creating Hidden Dependencies

    Imagine two background tasks.

    Generate Invoice
    
    ↓
    
    Send Invoice Email
    

    If invoice generation fails, the email shouldn't be sent.

    Background workflows often depend on one another.

    Those relationships need to be designed intentionally.

    Otherwise, failures become difficult to understand and debug.

    Mistake 6: Treating a Queue as a Scalability Shortcut

    Sometimes developers hear:

    "Large companies use Queues."

    So they add one immediately.

    But introducing a Queue also adds:

    • another component,
    • another responsibility,
    • another processing flow,
    • another failure path.

    If the work is simple and must happen immediately anyway, a Queue only makes the design more complicated.

    Good engineers add complexity only when it solves a real problem.

    A Simple Engineering Checklist

    Before introducing a Queue, ask yourself:

    Does the user need the result now?
    
    ↓
    
    Can the work happen later?
    
    ↓
    
    Does FIFO match the business rule?
    
    ↓
    
    Can consumers keep up?
    
    ↓
    
    What happens if processing fails?
    

    If you can't confidently answer these questions, you're probably not ready to introduce a Queue yet.

    How This Changes Your LLD Design

    A Queue shouldn't be the first thing you add to a design.

    Instead, begin with the business workflow.

    Separate:

    • critical work,
    • background work,
    • producer responsibilities,
    • consumer responsibilities.

    Only then decide whether a Queue naturally fits between those responsibilities.

    Design decisions should always follow business behavior.

    Engineering Perspective

    When experienced engineers review a design, they rarely ask:

    "Why didn't you use a Queue?"

    Instead, they ask questions like:

    • Why is this work asynchronous?
    • What happens if the consumer becomes slow?
    • Does FIFO actually match the business requirement?
    • How does the system recover from failures?

    Those discussions reveal whether the Queue is truly improving the design—or simply adding unnecessary complexity.

    The Most Important Insight

    Queues are not a shortcut to scalability.

    They're a way of organizing work.

    Whether they improve your system depends entirely on whether they match the business behavior you're trying to model.

    That's why experienced engineers always understand the problem before choosing the solution.

    One-Line Takeaway

    Great software engineers don't add Queues because they're scalable—they add them because asynchronous processing genuinely makes the design simpler, cleaner, and more responsive.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:27

    ↗

    Enterprise data architecture is undergoing a fundamental transformation as AI-native systems become the new standard. The Model Context Protocol (MCP) joins established protocols like REST and GraphQL as a critical integration layer. Understanding when and why to use each...

    Enterprise data architecture is undergoing a fundamental transformation as AI-native systems become the new standard. The Model Context Protocol (MCP) joins established protocols like REST and GraphQL as a critical integration layer. Understanding when and why to use each protocol is essential for architects building the next generation of enterprise data systems.

    Protocol Overview: REST, GraphQL, and MCP

    REST (Representational State Transfer) has been the backbone of web services since the early 2010s. Built on HTTP verbs (GET, POST, PUT, DELETE), REST provides a resource-oriented architecture where each endpoint represents a specific data entity. Its simplicity, ubiquity, and massive ecosystem make it the default choice for most API integrations. REST excels at CRUD operations, caching through HTTP infrastructure, and stateless request-response patterns.

    GraphQL emerged in 2015 as Facebook solution to mobile data fetching challenges. It provides a flexible query language that lets clients request exactly the data they need in a single request, eliminating over-fetching and under-fetching problems inherent in REST. GraphQL uses a typed schema, supports real-time subscriptions, and excels in scenarios with complex, nested data relationships.

    MCP (Model Context Protocol) was introduced by Anthropic in late 2024 as an open standard for connecting AI models to external tools and data sources. Unlike REST and GraphQL, which serve human-driven client applications, MCP is designed specifically for AI agent-tool interactions. It provides a standardized way for AI models to discover available tools, understand their capabilities through schemas, and invoke them with structured parameters. MCP natively supports context passing, tool composition, and multi-step reasoning workflows.

    • REST: Best for traditional client-server applications, public APIs, and simple CRUD operations
    • GraphQL: Best for complex data fetching, multi-platform clients, and nested relationship queries
    • MCP: Best for AI agent integration, tool orchestration, and AI-native data access patterns

    Technical Architecture Comparison

    The three protocols differ fundamentally in their communication model. REST uses a request-response pattern over HTTP with fixed endpoints. The server defines the response structure, and the client must work with what it receives. GraphQL inverts this with a client-driven approach where clients send queries describing the exact data shape needed, and the server returns precisely that shape through a single endpoint.

    MCP introduces an AI-agent-driven model. The AI model acts as the client, discovering tools through a capabilities manifest, then invoking them through structured tool calls. The protocol supports three core primitives: resources (data sources AI can read), tools (functions AI can invoke), and prompts (templates for structuring AI interactions). This design maps naturally to how AI agents reason and act.

    • Communication pattern: REST is server-defined, GraphQL is client-defined, MCP is agent-defined
    • Schema model: REST uses OpenAPI/Swagger, GraphQL uses SDL, MCP uses JSON Schema for tools
    • State management: REST is stateless, GraphQL supports subscriptions, MCP supports context-rich sessions
    • Discovery: REST requires documentation, GraphQL has introspection, MCP has built-in tool discovery

    When to Use REST API

    REST remains the correct choice for most traditional enterprise integrations. Use REST when building public-facing APIs for partner ecosystems, implementing microservices communication, creating simple data CRUD endpoints, or working with systems that have mature REST infrastructure. REST HTTP-native design provides excellent compatibility with load balancers, API gateways, caching layers, and monitoring tools.

    However, REST shows limitations when AI agents need to interact with systems. REST endpoints are designed for deterministic, human-understood operations. An AI agent calling a REST API must know the exact endpoint URL, understand the expected request format, and parse potentially complex responses, requiring explicit programming for each endpoint.

    • Ideal scenarios: Public APIs, microservices, simple CRUD, legacy system integration
    • Strengths: Simplicity, caching, statelessness, massive ecosystem, proven at scale
    • Limitations for AI: No tool discovery, rigid endpoint structure, requires explicit integration code

    When to Use GraphQL

    GraphQL excels with complex, interconnected data models where clients have varying data requirements. Use it when building dashboards that aggregate data from multiple domains, supporting mobile applications with bandwidth constraints, or implementing collaborative applications with real-time features. The typed schema serves as both documentation and contract.

    For AI integration, GraphQL offers advantages over REST through its introspection system and typed structure. However, GraphQL was designed for human developers writing queries, not AI agents orchestrating multi-step workflows. The query language adds complexity that does not align naturally with AI agent reasoning patterns.

    • Ideal scenarios: Complex data aggregation, multi-platform clients, real-time subscriptions
    • Strengths: Flexible queries, strong typing, introspection, eliminates over-fetching
    • Limitations for AI: Query complexity for agents, field-level auth challenges, single endpoint bottleneck

    Why MCP Excels for AI-Native Architectures

    MCP was purpose-built for the AI era. Its design reflects how AI agents work: discovering capabilities, reasoning about which tools to use, composing multi-step workflows, and passing context between operations. Unlike REST or GraphQL, MCP provides native support for these patterns without custom orchestration layers.

    The key architectural advantage is tool composability. In REST or GraphQL, integrating a new analytical capability requires writing custom code to call the API, parse the response, and feed it into the next step. With MCP, analytical capabilities are self-describing tools that AI agents can discover, understand, and chain dynamically. Adding a new capability means registering a new MCP server, and every connected AI agent immediately gains access.

    MCP also provides superior context management. AI agents working on complex analytical tasks need to maintain context across multiple tool invocations. MCP session model preserves context, allowing agents to reference previous results, refine queries based on intermediate outputs, and build complete analytical narratives.

    • Tool discovery: AI agents automatically discover available capabilities without documentation
    • Dynamic composition: Multi-step analytical workflows orchestrated by AI reasoning
    • Context preservation: Session state maintained across complex multi-tool interactions
    • Model agnostic: Works with Claude, GPT, Gemini, and open-source models equally

    Decision Framework and Migration Path

    The three protocols are not mutually exclusive. Most enterprises will employ all three in different contexts: REST for public APIs and microservices, GraphQL for frontend data aggregation, and MCP for AI agent integration. The key insight is that MCP serves as the AI-native integration layer on top of existing REST and GraphQL services.

    A practical migration path starts by building MCP servers that wrap existing REST and GraphQL APIs, exposing them as AI-consumable tools. This preserves existing API investments while enabling AI-native interaction patterns. Over time, new capabilities can be built as native MCP tools while legacy integrations continue through the wrapper pattern.

    This article was originally published on Beehive Strategy. Visit our blog for more insights on AI-powered analytics.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:25

    ↗

    A Feature List Is Half a Subtraction In One Component I Didn't Already Have, a plugin advertising eleven agents and fifty-four hooks came out, measured against an existing setup, at a delta of one component. Nothing in that result was about the plugin's quality. It was about...

    A Feature List Is Half a Subtraction

    In One Component I Didn't Already Have, a plugin advertising eleven agents and fifty-four hooks came out, measured against an existing setup, at a delta of one component.

    Nothing in that result was about the plugin's quality. It was about the other half of the subtraction — and that half lives on your machine, not on the project's page.

    This workshop runs the four checks against whatever tool is currently in your inbox. None of them requires installing it.

    A few terms, defined once:

    • Baseline — everything already wrapped around your model: instructions, skills, commands, hooks, memory files, tool connections.
    • Delta — what the tool would add after subtracting your baseline. The only number that answers "should I install this."
    • Issue tracker — the part of a project's repository the maintainer cannot curate without the gap being visible.
    • Reversibility — whether the uninstall actually removes what the install added. Frequently assumed, rarely checked.

    Exercise 1: Enumerate your baseline

    10 min

    Why this matters

    You cannot measure an addition without knowing the starting number. This is the check people skip, and skipping it is what makes every feature list look impressive — a list of fifty-four things is fifty-four gains only against zero.

    It is also the check that pays off permanently. You do it once, and every future tool evaluation becomes a reading exercise.

    The structure

    Whatever your agent loads at startup, get it into one readable list: instruction files, skills, commands, hooks, tool connections, and any helper binaries on your path.

    ls ~/.claude/skills/ ~/.claude/commands/ ~/.claude/hooks/ 2>/dev/null
    ls -1 ~/.local/bin
    

    If you followed Audit Your AI Coding Harness, its report is your baseline and you can skip ahead.

    The durable version of this is a repository rather than a command. Mine is public, which is the only reason the evaluation in the companion post took twenty minutes instead of an afternoon.

    Your turn

    Produce your list. Then answer one question about each entry: what does this do, in a sentence, without looking it up?

    Checkpoint

    You should have a list where you can name the function of every item. Anything you can't explain is a finding on its own — it's loaded into every session and you don't know why.

    Exercise 2: Subtract the baseline from the tool

    12 min

    Why this matters

    The delta is the decision. Everything else in this workshop refines it.

    The structure

    Pull the tool's component list from its reference documentation rather than its README — the first enumerates, the second sells. Many projects publish something like docs/reference/features.md:

    curl -fsSL https://raw.githubusercontent.com/<owner>/<repo>/<branch>/docs/reference/features.md
    

    Then walk it and mark each item one of three ways:

    The three buckets

    Every component lands in exactly one

    Bucket Meaning What it does to the decision
    Native Your setup or your agent already does this Removes it from the gain column entirely
    New Genuinely absent from your baseline This is the delta. The only column that argues for installing
    Blocked New, but unusable for a reason unrelated to quality Wrong platform, broken upstream, needs a subscription you don't hold, requires patching something you don't control

    Blocked is the bucket people collapse into the other two, and it distorts the answer in both directions. A component that is excellent and unusable is not a gain, and it is also not a criticism of the tool.

    Then run it backwards: what does your baseline do that the tool's list never mentions? That column is real and nobody publishes it for you.

    Your turn

    Produce the three-way split, then the reverse column. Count the New bucket.

    Checkpoint

    You should be able to name your covering item for every Native row. "I think I have something like that" doesn't count — if you can't name it, it belongs in New.

    If New is large, the tool is a genuine addition and the rest of this workshop is about risk. If New is one or two items, ask whether those items are available on their own before you adopt everything attached to them.

    Exercise 3: Query the issue tracker

    10 min

    Why this matters

    A project's documentation describes what it does when it works. Its issue tracker describes what it does on other people's machines. The second is written by users, and unlike testimonials it can't be curated without the absence being obvious.

    This is also the fastest read of a project's health that exists, and it's entirely mechanical.

    The structure

    Three queries. Volume, recency, and your own failure modes.

    # how much is open, and how fast is it arriving
    gh api "search/issues?q=repo:<owner>/<repo>+is:issue+is:open&per_page=1" --jq '.total_count'
    gh api "search/issues?q=repo:<owner>/<repo>+is:issue+created:>=<30-days-ago>&per_page=1" --jq '.total_count'
    
    # what users argue about most
    gh api "search/issues?q=repo:<owner>/<repo>+is:issue&sort=comments&order=desc&per_page=25" \
      --jq '.items[] | "\(.comments)c \(.state) #\(.number) \(.title)"'
    

    Then search the tracker for the things that would specifically hurt you — your platform, your host tool's version, your cost sensitivity, your workflow.

    Read for patterns rather than counts. A high open count on a popular project mostly means it's popular. What matters is shape:

    • Does it break when its host releases a new version? Search the host's version numbers.
    • Do its own updates break things? Search auto-update and after updating.
    • Are the most-discussed threads features or the same bug recurring?

    Your turn

    Run the three queries, then two searches specific to your situation. Write down the pattern in one sentence.

    Checkpoint

    You should have one sentence of the form: "The recurring failure mode is _, and it would/wouldn't hit me because _."

    If your searches return nothing, say that plainly rather than treating silence as a clean bill of health. A young project has a quiet tracker for reasons that have nothing to do with quality.

    Exercise 4: Confirm the exit before the entrance

    8 min

    Why this matters

    "It's cheap to try" is a claim about uninstalling, not installing. Every installer is one command. The exit is the part nobody tests, and it's the part that decides whether trying costs you an afternoon or a weekend.

    The structure

    Read the install guide's own account of what lands on disk, and check each location against your machine before running anything:

    ls -1 ~/.local/bin                       # names it might shadow
    grep -nE '^\[' <the-config-file-it-edits>  # sections it might rewrite
    

    Prefixed names and new named sections are additive and reversible. Bare generic names and rewrites of sections you already use are not.

    Then do the thing almost nobody does: search the issue tracker for the uninstall itself.

    gh api "search/issues?q=repo:<owner>/<repo>+is:issue+uninstall+OR+cleanup+OR+remove" \
      --jq '.items[] | "\(.state) \(.created_at[0:10]) #\(.number) \(.title)"'
    

    Open reports that removal leaves things behind are worth more than the entire installation guide. They tell you the true cost of being wrong.

    Your turn

    Write a one-line verdict for each: name collisions, config writes, and whether uninstall is reported to work.

    Checkpoint

    You should be able to say, without having installed anything: which files it creates, which existing file it modifies, whether any name shadows one of yours, and whether other people have successfully removed it.

    If the install guide won't tell you enough to answer those, that absence is your finding. Installers that won't say what they write are the ones most worth not running.

    What You Ran

    Four checks, none of which required installing the thing:

    • A baseline you can read, which turns every future evaluation into a lookup
    • A three-bucket subtraction — native, new, blocked — run in both directions
    • Three mechanical queries against the writing the vendor doesn't control
    • An exit check, before the entrance

    The output is a number: how many components this tool would actually add to your setup. That number is the decision, and it is different for every person who asks you whether the tool is good.

    Where This Is Heading

    Exercise 1 is the one that hurts, and not because it's difficult.

    Most people cannot list what their agent loads at startup. The list has grown by accretion — a hook added during one bad afternoon, a rule added after one bad review, three skills installed and never invoked since. Every one of them still loads.

    Which means the honest version of "should I install this" usually surfaces a second question underneath it, and it's the less comfortable one: what is already in here that I would not install today?

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:19

    ↗

    I used to learn like this. Buy the cheap Udemy course, get three hours in, realize it was recorded against a version of the framework that no longer exists. Open twelve Stack Overflow tabs. Read the official docs, get stuck on step four, ping a coworker who is also busy. Then...

    I used to learn like this. Buy the cheap Udemy course, get three hours in, realize it was recorded against a version of the framework that no longer exists. Open twelve Stack Overflow tabs. Read the official docs, get stuck on step four, ping a coworker who is also busy. Then I would keep repeating this pattern until I gave up.

    Learning a new framework in 2026 is easier than that. However it got easier in a different place than I expected.

    Recently I was at Chain React. It's a React Native conference and for my talk I decided to really deep into the framework and learn it. I started doing the same thing I always did, reading the docs, buying courses etc. Then I realized I could use AI to speed up my learning. This is what I want to teach you today, with a few additional tips along the way.

    Want to watch instead, check out my full video on the subject!

    (Full disclosure before I start naming tools: I'm a Developer Advocate at AWS, and Kiro is an AWS product. It's the agentic harness I use daily. Everything here works the same in Claude Code, Cursor, or whatever you already have open.)

    Start with a goal, not a curriculum

    I don't start by learning a framework. I start with something that I want to build for fun.

    thinking slide

    When I was learning React Native, it was a workout tracker. I wanted to learn a program called 75 Hard, which is 75 days of hitting the same set of daily goals including a diet, an outdoor workout, and a second workout. I wanted to check those off on my phone. That was the goal.

    Making sure you have a goal is really important. A todo list app teaches you nothing, because it means almost nothing. A real app forces decisions you have to make, like does it work offline, where does the data live, what happens when permissions get denied. Pick something with a little resistance in it, ideally tied to a hobby so you'll still care as your learning progression continues.

    Decide how much you actually need to know

    In other words, how much of it do I need to actually learn to achieve my goal?

    In today's age, you need to learn the primitives. The basics, and some architecture of how it all works. You do not need to memorize API signatures anymore.

    Let's imagine you are learning React. If you're using useState everywhere, understand what it does and why re-renders happen. Whether you can recall the exact argument order from memory is irrelevant, because the model will write it. Same with optional chaining, same with whatever config format the build tool wants this year. Learn the basics of the framework or library you are learning. Let the agent handle the syntax.

    Ask three models what they'd build today

    Before I learn anything, I want to know what the ecosystem actually looks like right now. So I ask several models the same broad question and compare.

    When I was learning React Native, I ran the same prompt through Gemini, Claude, and GPT. Something like this:

    Build a mobile application for iOS.
    

    That comes back with Swift, which tells me something. Then:

    Build a cross-platform iOS and Android application.
    

    Now I get Flutter or React Native. I'm not asking for code here. I'm reading the consensus from the agent to find out which frameworks and libraries it's recommending, which tells me whether the thing I'm about to learn is the thing I should be learning.

    When the models disagree, that's when I dig in further to find out why. I'll usually google around a bit to see if I'm on the right path.

    Get a personalized learning path

    Finally, I work on a personalized learning path, based on what I know already. Here is an example:

    Build me a curriculum to learn React Native. Assume I already
    have basic knowledge of React, HTML, CSS, and JavaScript.
    

    The second sentence is where I brought in my own personalization. I know web development, so I wanted to make sure my learning path is tailored to me. Also, I called out React Native, as per the last section, I learned it's the most popular and makes the most sense for me.

    Because I told it I already knew web basics, it skips React fundamentals and goes straight to what's different: native components instead of the DOM, StyleSheet instead of the CSS cascade, navigation as a stack instead of URLs, you get the idea. I then get topics with a suggested time frame, and I can work through them at my own pace.

    Heads up: model selection matters here more than almost anywhere else. I ran this against Sonnet 4 and Opus 5. Sonnet 4 handed me what was essentially 2023 React Native. Opus 5 gave me current information and a noticeably better curriculum. Every model has a different training cutoff, and a stale curriculum is worse than no curriculum, because you don't know which parts are wrong.

    If you're stuck on an older or a local model, run an adversarial review. Have the cheap model draft the learning path, then hand it to a current model and ask what's out of date. Going past two or three models surfaces a surprising amount, and it's also a good idea when you are trying to save tokens.

    Also make sure you use some of the tools your harness gives you. Most harnesses have search built in, plus MCP servers and skills. If you're learning React, install the current React skills before you generate anything. Scaffolding tools help here too: create-next-app now drops an AGENTS.md in your project, and that file is a better starting point for your curriculum than the model's memory.

    Build it: I do, we do, you do

    A learning path is a good start. It's still reading. To move to actual understanding I used an old teaching framework from 1983.

    It's called gradual release of responsibility, from a paper by Pearson and Gallagher, and it sits on top of Vygotsky's zone of proximal development. The sequence is I do, we do, you do. The teacher demonstrates, then you work together, then you work alone.

    To use this framework today, the the AI becomes the "I." It does, you watch. Then you work together with the AI, and finally you do it yourself. It's really helped me learn. Let's start with ** I do **

    I do: let it build, then read it

    Let the agent build the whole thing. Just create a simple prompt, and let it do it's thing. Don't type anything. Read the output, look at the file structure, see how it wired things together.

    You are not learning yet, and that's fine. This phase is the right one because it gives you an idea what is possible. If you like you could look over the code that was written, but it's not the best way to learn.

    We do: spec-driven development

    This is where most of the learning actually happened for me.

    Spec-driven development means writing structured specifications first, so the agent can build and verify against them. In practice I ask for a spec instead of an app:

    Create a spec that helps build a retro workout planner app.
    

    wedo graphic

    What comes back is a design document, and the design document is the cheat sheet. Mine told me to use Expo Router 57, expo-sqlite, an image picker, and notifications. That's a map of the architecture and the current library choices for a framework I didn't know yet. When I was working like this, I would constantly ask clarifying questions and ask why certain decisions were made. I would even often ask for changes.

    Then it generates requirements, usually as user stories. I skip this one when I'm learning. It's more useful when you're shipping to other people.

    The implementation plan is the part you need to pay attention to. It's a task list. Instead of letting the agent execute it, I work through the tasks myself, with the design doc open. When I get stuck, I say so:

    I tried to install NativeWind 5 and I don't understand what to do here. Can you help?
    

    implementation plan

    Then I get help, and I might even read the official documentation anyways. That's the "we do" phase working exactly as designed. I have enough context to attempt it and a patient buddy for the gaps. It's almost like rubber ducking, but with something that can respond.

    Keep in mind, if you write a spec and then let the agent implement all of it, you haven't learned much. You can now explain what your app does and still not explain how the framework does it. So I settled on a rule. Spec the what, hand-write the how, at least once per concept. Write the spec for the app, but make sure your writing things yourself (with help if needed).

    You do: turn it off

    Close the tab. Build something small with no assistance.

    Don't skip this step. This is the best way to check your recall and that you really understand everything. I wouldn't write the whole thing from scratch, mind you, but a few pieces just to make sure I understand the underlying concepts.

    Two more patterns worth stealing

    Let the AI quiz you. Take the learning path you already generated and ask for a quiz on it, then go back and forth. It's a quick way to find out which parts you only think you know.

    The stronger version of this flips the direction. Don't ask the model to explain hooks to you. Explain hooks to the model and ask it to grade you. "Here's my understanding of the New Architecture, what did I get wrong?" Use text-to-speach if you can, to make this even quicker.

    The other pattern is learning in public, which I was doing long before agents existed. Post what you're learning, on YouTube or Bluesky or a blog. Writing it down for someone else is what exposes the parts you weren't sure on. This post exists because I told a conference I'd stand up and explain React Native, and that deadline taught me more than any course.

    Where I've landed

    Start with a goal you care about. Use your usual harness on a current model. Have it build you a plan, with search and MCP servers turned on so it isn't working from memory. Learn the primitives and the architecture, skip memorizing the APIs. Then try out something like: having the AI build it, build alongside it, then build alone.

    AI didn't replace the learning. It replaced the searching. All those hours of hunting for the right Stack Overflow answer are gone, and I don't miss them. But the part where you sit with something confusing until it stops being confusing? That still has to happen. There's just no tab for it.

    How do you learn a new framework these days? Let me know in the comments if you do it differently. Until next time.

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:15

    ↗

    Chegamos ao grande final da nossa jornada! Recapitulando o que construímos até aqui: Criamos um pipeline que testa e compila nosso Frontend (Angular) e Backend (.NET) em paralelo. Tiramos a automação da nuvem paga e colocamos em um Self-Hosted Runner gratuito. Automatizamos a...

    Chegamos ao grande final da nossa jornada! Recapitulando o que construímos até aqui:

    1. Criamos um pipeline que testa e compila nosso Frontend (Angular) e Backend (.NET) em paralelo.
    2. Tiramos a automação da nuvem paga e colocamos em um Self-Hosted Runner gratuito.
    3. Automatizamos a criação de Tags e do Changelog.

    Neste ponto, você já tem um processo melhor do que muitas empresas grandes por aí. Mas, se você observar os logs de execução, vai notar um gargalo frustrante: a instalação das dependências.

    A cada push, seu robô perde minutos preciosos baixando a mesma pasta node_modules (o famoso buraco negro do universo) e os mesmos pacotes NuGet do zero. Hoje, vamos resolver isso e, de quebra, blindar a sua branch main contra códigos quebrados.

    1. A Mágica do Cache

    Em CI/CD, "Cache" significa guardar um backup das suas dependências da execução anterior. Se o seu arquivo package-lock.json ou .csproj não mudou, o GitHub Actions simplesmente restaura a pasta do cache em questão de segundos, pulando totalmente a etapa de download.

    Otimizando o Frontend (Angular / Node)

    Se você está usando a versão mais recente da action setup-node, o cache já vem embutido! Só precisamos ativá-lo e dizer onde está o nosso arquivo de lock, já que nosso projeto está dentro da pasta /frontend.

    Vá no seu .yml da Parte 1 e atualize a etapa do Node.js:

          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
              cache: 'npm' # Ativa o cache mágico!
              cache-dependency-path: './frontend/package-lock.json' # Aponta para a subpasta
    
    

    Pronto. Só de adicionar essas duas linhas, o tempo do seu npm ci vai cair drasticamente.

    Otimizando o Backend (.NET / NuGet)

    Para o C#, precisamos usar a action oficial de cache (actions/cache). Ela exige três coisas: o que salvar (caminho), como nomear o backup (key) e de onde tentar recuperar se não achar o exato (restore-keys).

    Adicione este bloco logo ANTES da etapa de dotnet restore:

          - name: Cache NuGet Packages
            uses: actions/cache@v4
            with:
              path: ~/.nuget/packages
              # Cria uma chave única baseada no SO e nos arquivos de projeto
              key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.lock.json') }}
              restore-keys: |
                ${{ runner.os }}-nuget-
    
    

    Na próxima vez que o pipeline rodar, a primeira execução ainda fará o download, mas salvará o cache no final. Nas execuções seguintes, você verá a magia da restauração rápida.

    2. Testando Multiversos com Matrix Builds

    E se você estiver construindo uma biblioteca ou ferramenta que precisa funcionar em múltiplas versões do Node (ex: 18 e 20) ou em vários Sistemas Operacionais simultaneamente?

    Você não precisa copiar e colar o seu código YAML. Basta usar a estratégia de Matrix.

    jobs:
      build-frontend:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            node-version: [18.x, 20.x, 22.x] # O GitHub vai rodar este Job 3 vezes simultaneamente!
    
        steps:
          - uses: actions/checkout@v4
          - name: Use Node.js ${{ matrix.node-version }}
            uses: actions/setup-node@v4
            with:
              node-version: ${{ matrix.node-version }}
    
    

    Com a matrix, o GitHub Actions cria caminhos paralelos automaticamente. Se uma versão falhar e as outras passarem, você sabe exatamente onde está o problema de compatibilidade.

    3. O Leão de Chácara: Branch Protection Rules

    De que adianta ter um pipeline que roda testes incríveis se um desenvolvedor apressado pode simplesmente clicar em "Merge Pull Request" ignorando que tudo ficou vermelho?

    A automação só é útil se for obrigatória. Para isso, usamos as Regras de Proteção de Branch.

    1. Vá na aba Settings do seu repositório no GitHub.
    2. Na barra lateral, clique em Branches.
    3. Clique em Add branch ruleset (ou edite a regra da sua branch main).
    4. Marque a opção Require a pull request before merging.
    5. O MAIS IMPORTANTE: Marque a opção Require status checks to pass before merging.
    6. Na barra de pesquisa que aparecer, digite os nomes exatos dos seus Jobs (ex: Build Angular App e Build & Test .NET API) e adicione-os como obrigatórios.

    O Resultado: A partir de agora, o botão verde de Merge no GitHub ficará bloqueado. Ele só será liberado se o nosso GitHub Actions rodar, compilar o Angular, passar nos testes do .NET e retornar a luz verde.

    Conclusão da Série

    Parabéns! Se você aplicou os conceitos desta série de 4 partes, você evoluiu de "alguém que arrasta arquivos para o FTP" para um administrador de infraestrutura ágil, segura e profissional.

    Dominar pipelines, gestão de secrets, runners locais e versionamento semântico são habilidades que diferenciam desenvolvedores de alto nível no mercado. O GitHub Actions é uma ferramenta incrivelmente poderosa que vai muito além de apenas subir código.

    Como ficou a sua esteira de CI/CD? Qual foi o tempo que você conseguiu economizar usando o sistema de Caches? Compartilhe o link do seu repositório (ou os perrengues que passou configurando) aqui nos comentários!

  • DEV Community dev.to community dev-to software-dev technology 2026-08-03 17:15

    ↗

    Se você acompanhou as partes anteriores desta série, seu projeto Full-Stack (Angular + .NET) já está sendo testado e compilado automaticamente em um Self-Hosted Runner gratuito. A infraestrutura está perfeita. Mas e a organização do código? Imagine a seguinte situação: você...

    Se você acompanhou as partes anteriores desta série, seu projeto Full-Stack (Angular + .NET) já está sendo testado e compilado automaticamente em um Self-Hosted Runner gratuito. A infraestrutura está perfeita.

    Mas e a organização do código?

    Imagine a seguinte situação: você trabalha com mais três desenvolvedores. Vocês abrem PRs, fazem merge na branch main e o deploy acontece. Chega sexta-feira e o cliente pergunta: "O que exatamente entrou na versão 1.2.0 que acabou de ir pro ar?"

    Se você responde revirando um histórico de commits cheio de "fix bug", "ajuste no layout", ou "wip", nós temos um problema.

    Nesta Parte 3, vamos transformar seu repositório bagunçado em uma máquina de versionamento profissional. Vamos padronizar os commits, gerar o CHANGELOG.md magicamente e usar o GitHub Actions para criar Releases visuais.

    Passo 1: O Fim do "Commit Bagunça"

    A automação de versões depende de previsibilidade. O robô não sabe ler mentes, mas ele sabe ler padrões. É aqui que entra o Conventional Commits.

    Em vez de escrever o que vier à cabeça, você passa a iniciar seus commits com prefixos específicos:

    • feat: adiciona dark mode no dashboard (Uma nova funcionalidade - vai gerar uma nova minor version ex: 1.1.0 -> 1.2.0)
    • fix: corrige erro no grid do canvas (Uma correção de bug - vai gerar um patch ex: 1.2.0 -> 1.2.1)
    • chore: atualiza dependências do Angular (Tarefas de manutenção que não afetam o usuário final)

    Se você tem dificuldade de lembrar as regras, recomendo usar a biblioteca Commitizen. Ela transforma o comando git commit em um questionário interativo no seu terminal, forçando o padrão correto.

    Passo 2: Gerando a Versão Magicamente

    Agora que seus commits estão organizados no projeto, vamos usar uma ferramenta maravilhosa chamada commit-and-tag-version (um fork moderno do antigo standard-version).

    Como nosso frontend é em Angular, podemos instalar isso facilmente na pasta do projeto usando o NPM:

    cd frontend
    npm i -D commit-and-tag-version
    

    Agora, abra o seu package.json e adicione este script:

    "scripts": {
      "release": "commit-and-tag-version"
    }
    

    O que esse comando faz? Quando você rodar npm run release, a ferramenta vai:

    1. Ler todo o seu histórico de commits desde a última versão.
    2. Descobrir automaticamente qual é a próxima versão (baseado nos seus feat: e fix:).
    3. Atualizar o número da versão dentro do package.json.
    4. Criar ou atualizar um arquivo CHANGELOG.md com um resumo lindíssimo de tudo que foi feito.
    5. Fazer um commit automático com esses arquivos.
    6. Criar uma Git Tag (ex: v1.2.0).

    Passo 3: O Workflow de Release no GitHub

    O nosso repositório local já está tagueado e o changelog foi gerado. Agora precisamos que o GitHub Actions perceba isso e crie uma aba de "Release" oficial lá no site do GitHub, para que qualquer pessoa possa baixar os binários ou ver as notas de atualização.

    Vamos criar um novo arquivo de workflow. Crie .github/workflows/release.yml:

    name: Generate GitHub Release
    
    # Esse robô NÃO roda no push convencional. Ele só acorda quando uma Tag é enviada.
    on:
      push:
        tags:
          - 'v*' # Aciona quando a tag começa com "v", ex: v1.0.0
    
    jobs:
      create-release:
        name: Create Official Release
        runs-on: ubuntu-latest
    
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
            with:
              fetch-depth: 0 # Essencial: baixa o histórico completo para ler as tags anteriores
    
          - name: Create GitHub Release
            # Uma action super popular para gerar a interface visual de Release no GitHub
            uses: softprops/action-gh-release@v2
            with:
              generate_release_notes: true # O próprio GitHub ajuda a agrupar as PRs na tela
              # files: se quiser, pode anexar um .zip do backend compilado aqui!
    

    O Fluxo de Trabalho Completo (Dia a Dia)

    Parece muita coisa, mas olha como a sua vida e da sua equipe fica simples no dia a dia:

    1. Você trabalha no seu código normalmente.
    2. Faz o commit usando o padrão: git commit -m "feat: integra API C# com o kanjidex"
    3. Quando a equipe decide que é hora de ir para produção, você roda: npm run release
    4. A ferramenta processa tudo, cria a tag e o CHANGELOG.md localmente.
    5. Você envia tudo para o GitHub com um comando especial que empurra as tags junto com o código: git push --follow-tags

    Pronto! O seu workflow principal da Parte 1 vai fazer o deploy da aplicação, e o nosso novo workflow da Parte 3 vai criar uma Release oficial no repositório. Documentação em dia sem você precisar escrever uma única linha de relatório.

    Resumo e Próximos Passos

    Temos integração contínua (CI), entrega contínua (CD), servidores gratuitos e versionamento semântico automatizado. É uma arquitetura de DevOps invejável!

    Contudo, conforme o repositório fica gigante, as execuções começam a demorar. Ficar baixando a pasta node_modules e os pacotes NuGet do zero a cada execução não faz o menor sentido e atrasa a esteira.

    Na Parte 4 (O Grande Final), vamos turbinar a performance do nosso pipeline implementando Caches inteligentes e explorando as Matrix Builds para testar múltiplos ambientes de uma só vez.

  • Loading more…
Maibook — your private personalized AI community
  • rcanand.com
  • mlaillc.com
  • @rcanand (X)
  • LinkedIn
  • Feedback
  • Credits