High-Value If, Low-Value Foreach: Why Agents Trade in Judgment Structures, Not Models

High-Value If, Low-Value Foreach

Why agents trade in judgment structures, not models

Why model placement, not model frequency, determines whether agents become real products

This is the first in a series on the engineering of Agent Runtimes. It argues that the 2026 problem for agent builders is not intelligence — it is where intelligence is placed. The full series builds the discipline from high-value ifs to value functions, schemas, workflows, and the if/foreach ledger.

Abstract

You are not selling model access. You are selling reusable judgment.

Most agent products fail not because the model is too weak, but because the model is in the wrong place. They put a frontier model inside the foreach loop — re-reading every page, re-planning every step, re-generating every action — then collapse under cost, latency, and reliability failures the moment they leave the demo environment.

The discipline this essay proposes: place the stochastic model at high-value ifs — branches with high uncertainty, dense semantics, and high stakes — and hand the deterministic foreach to programs, workflows, and tools. Once the model makes a useful judgment, the system must absorb it into schemas, workflows, audit logs, skills, and evaluation sets. The goal is not more model calls. The goal is fewer unnecessary ones over time.

Frontier models open paths. Deterministic systems pave roads. Everything else is engineering.

1. The 2026 Problem for Agent Builders

Most agent demos — built by what I’ll call agent builders: the teams designing model routing, workflows, schemas, permissions, approvals, and audit logs — fail for the same reason: they put the model in the wrong place.

They ask the model to read every page, re-understand every DOM, re-plan every step, and generate every action. The demo looks impressive. The product collapses under cost pressure, latency, reliability failures, and lack of reuse.

The first commercial problem of agents is not intelligence. It is where intelligence is placed. Where should the stochastic model appear inside a deterministic system?

Over the past two years, from the Chat Completions explosion to the emergence of Computer Use compatibility layers, the industry has hit a structural wall: there is no mechanism to crystallize stochastic judgment into deterministic assets. Every model call is still a one-time expense. When the task ends, the exploration vanishes with it.

In 2026, the model layer is moving from chat-completion interfaces toward agent-building primitives — Responses API, Structured Outputs, Computer Use, Agents SDK, Sandbox Agents. Frontier inference costs are dropping into a range where deep semantic analysis finally pencils out. Local runtime ecosystems are becoming mature enough to orchestrate complex tasks. And the old internet spent three decades compressing reality into low-resolution proxies — degrees, ratings, sales volume, clicks, rankings, and other sortable fields — which are now exposing systemic weakness under information degradation and Goodhart’s Law.

Agent builders need a complete methodology spanning high-value ifs, value functions, and engineering implementation. The question reduces to one thing:

How can a single model judgment become a durable system asset?

Builders won’t win by out-scaling model providers. They’ll win by owning the judgment structures, workflows, and responsibility boundaries that models serve.

2. Model Placement, Not Model Frequency

In agent systems, model placement matters more than model frequency.

The do-everything agent that wows demo audiences dies in production from cost blowups and reliability collapse. The answer lies not in parameter count or prompt sophistication, but in the most fundamental engineering discipline: where the model sits in the architecture.

Luo Fuli, who led DeepSeek-V2 research before joining Xiaomi’s MiMo team in late 2025, framed it in a recent interview as a product of three factors: effectiveness × cost × efficiency. The framing matters because it forces agent design back from the “fully autonomous” myth to the engineering reality of the runtime. None of the three is optional. Effectiveness without cost control cannot become a commercial product. Low cost without trustworthy results creates no willingness to pay. Efficiency that only runs deterministic flows is not an agent — it is RPA with a wrapper.

Translating this into concrete architectural decisions gives us the first engineering discipline of the agent era.

What Models Should and Should Not Do

The past two years of Agent Runtime practice — the execution layer that manages state, permissions, workflows, tool calls, model routing, audit logs, and human approval — point repeatedly to one division of labor:

The Agent Runtime should place the model at high-uncertainty, semantically dense, high-stakes decision branches. It should hand repeatable, well-scoped, verifiable, low-risk execution to programs, workflows, and tool layers.

Low-value foreach: what it looks like

Foreach is batchable, repetitive, procedural, well-scoped work:

  • Batch-scraping web pages
  • Batch-organizing files
  • Batch-exporting conversation logs
  • Batch-converting Markdown to other formats
  • Batch-executing known browser flows
  • Batch-calling API endpoints
  • Batch-structuring content into uniform formats
  • Batch-classifying items in a low-risk first pass

These tasks are often packaged as “AI automation,” but structurally they belong to programs, scripts, CLI, SQL, queue systems, rule engines, Playwright, MCP tools, or deterministic pipelines.

On tasks that can be deterministically described, programs are typically orders of magnitude cheaper, faster, more stable, and easier to audit and reproduce than LLM calls.

If a task can be described as:

for item in items:
    do_something(item)

the model should not be the primary executor of that loop.

High-value if: the model’s anchor point

I call these branches high-value ifs: decision points with high uncertainty, semantic density, and high stakes. They cannot be reliably captured by a few fields, nor safely decided by low-trust rules.

Not every if is high-value. file_exists(path) is an if — but it is a state check. user.is_logged_in is an if — but it is a boolean lookup. inventory > 0 is an if — but it is a numeric comparison. These are low-value state judgments better handled by programs, databases, caches, rule engines, or workflows.

High-value ifs in an Agent Runtime are judgment nodes that require the model to synthesize context, objectives, risk, constraints, and user intent across multiple possibilities, then choose:

  • Intent judgment: What does the user actually want?
  • Information sufficiency: Is the current context enough to proceed?
  • Cost-benefit judgment: Is this task worth continuing?
  • Tool selection: Which tool best fits this step?
  • Quality judgment: Is this result trustworthy?
  • Signal-noise judgment: Which parts are noise?
  • Information value: What should enter long-term memory?
  • Risk classification: Which operations need human confirmation?
  • Pattern recognition: Which processes can be hardened into workflows?
  • Error attribution: Is the failure a page change, permission issue, model misunderstanding, or unclear user intent?

None of these can be solved by a simple foreach. These decision branches are where the LLM should appear in an agent system.

A high-value if is not completed by the LLM alone. It is completed by two layers:

Layer 1 — The LLM operates in high-dimensional semantic space: understanding, extracting, attributing, comparing, generating candidate judgments, and handling uncertainty.

Layer 2 — Schema, enums, booleans, workflows, rules, and human approval gates constrain the model’s output into auditable, reproducible, accountable final decisions.

Without the LLM, high-value ifs regress to field filtering. Without the schema, high-value ifs become unauditable free-form generation. Both layers are indispensable.

If and Foreach Are Nested, Not Exclusive

High-value ifs and low-value foreach loops rarely appear in isolation. They interleave constantly.

The user says: “Organize this week’s important emails and classify them by project.” Structurally, this is a foreach:

for email in this_week:
    classify(email)

But expand one layer, and every step contains ifs:

for email in this_week:
    if is_spam(email):
        skip(email)

    if is_newsletter(email):
        archive(email)

    if is_work_related(email):
        project = infer_project(email)

        if project.confidence > 0.85:
            classify_into_project(email, project)
        else:
            ask_user_or_create_candidate_project(email)

        if contains_deadline(email):
            date = extract_date(email)

            if date.confidence > 0.9:
                create_task(email, date)
            else:
                mark_for_review(email)

If and foreach nest inside each other. This nesting does not imply every step should call an LLM. The primary engineering job of the Agent Runtime is to precisely decompose this mixed structure:

  • Which foreach loops can run through deterministic pipelines with no LLM at all.
  • Which ifs require a frontier model, because they are high-value judgment nodes.
  • Which ifs can be resolved by historical workflows, because the pattern has been absorbed.
  • Which ifs can be handled by a lightweight model plus rule engine, because the risk is low.
  • Which ifs require human approval, because the action is irreversible.
  • Which exceptions need to re-invoke a frontier model, because existing workflows have broken.

The key thesis compresses to one line:

The key to agent engineering is model placement.

3. Why Agents Fail When the Model Becomes the Executor

Many agent products fail because they treat the LLM as a universal executor rather than a critical judgment engine. They make the model re-read the page, re-understand the DOM, re-plan the path, and re-generate the action at every step. This looks powerful in demos. Once deployed, it hits five structural failures simultaneously.

Failure 1: Uncontrollable cost

Every step calls a frontier model and recomputes the full context. A task advertised as “fully automated” rapidly accumulates model cost, latency cost, and failure-recovery cost.

Failure 2: Uncontrollable reliability

An LLM is a stochastic system. Every re-decision adds another stochastic failure point. A 10-step task at 95% per-step success has ~60% overall success. A 20-step task drops to ~36%.

The 95%^n figure is an independence approximation. Real agent steps are not independent: page changes, permission expirations, context pollution, and upstream errors increase correlation, making actual failure modes more complex. The problem is not that the model is “slightly off.” It is the physics of error accumulation in a long-sequence autoregressive decision process.

Failure 3: Uncontrollable state pollution

As the LLM ingests execution results across a long loop, the context gets progressively dirtier. Early system prompts, task objectives, risk boundaries, and output constraints get diluted by page noise, failed attempts, and intermediate results. Lost-in-the-middle (U-shaped attention decay in long contexts, Liu et al., 2023) amplifies state pollution. Handing foreach to programs means every necessary model call happens inside a shorter, cleaner context window.

Failure 4: Throughput-latency mismatch

Foreach means high throughput demand. If means high latency tolerance. Batch-processing thousands of items requires throughput, concurrency, retries, and monitoring. LLMs’ autoregressive generation makes them inherently unsuited to high-throughput execution. A model can be slow and expensive at a few critical nodes, but not at every iteration of a pipeline.

Failure 5: Non-reusable results

Every task starts from scratch. After the task ends, the system retains nothing. The same user issuing the same task triggers the same cost and probability gamble from zero.

These five failures explain why many Computer Use and Browser Agent products stall: they put the LLM inside the foreach, then try to brute-force their way out by reaching for stronger models. The root cause is failing to draw the physical boundary between stochastic judgment and deterministic execution.

The model contributes semantic judgment at the high-value if. The system crystallizes that judgment into structure. The foreach then reuses it at low cost.

4. Explore, Prune, Harden, Evolve

Expanding “model placement over model frequency” into a product architecture yields four phases.

Phase 1: Explore

The user initiates a new type of task for the first time. The system recognizes an unseen scenario: no reusable workflow, no cache hit, no known fallback.

This is the moment to call the frontier model.

Its job goes beyond “completing this task.” It explores the real structure of the task in high-dimensional semantic space:

  • What is the user’s true intent — does it match the literal request?
  • Where are the critical judgment nodes?
  • Which steps are deterministic, which require judgment?
  • What are the success conditions for the critical path?
  • Where should the system fall back on failure?
  • Which steps involve irreversible actions requiring human approval?

The frontier model’s output should be a structured decision graph: explicit schema, explicit step sequence, explicit branching conditions, explicit success signals, explicit fallbacks.

This call is expensive because it bears the cost of task-structure discovery, risk-boundary identification, and the foundation for all subsequent low-cost reuse.

Phase 2: Prune

The decision graph from Phase 1 is a first draft — “theoretically completable.” Real environments expose stability gaps: pages redesign, networks fluctuate, user inputs are ambiguous, third-party APIs fail intermittently.

The key action is testing and pruning with real data:

  • Which branch conditions have never actually been triggered.
  • Which “requires human review” nodes can actually be automated.
  • Which nominally uncertain steps always take the same branch in practice.
  • Which fallback paths are more stable than the primary path.

Pruning compresses the “redundant decision graph” into a “minimal necessary decision graph validated by real data.” This phase often does not need a frontier model. Mid-tier models, real-data replays, rule-based statistics, and manual review are usually sufficient.

Phase 3: Harden

The pruned decision graph gets hardened into an executable structured workflow. A real industrial Agent Runtime stores DAG-level (directed acyclic graph) or FSM-level (finite state machine) state-transition logic — “under what conditions to transition from state A to state B,” not “do step one then step two.”

A simplified example:

{
  "workflow": "export_bank_statement",
  "site": "example_bank.com",
  "intent": "download monthly statement as PDF",
  "preconditions": [
    "user_logged_in",
    "two_factor_completed"
  ],
  "steps": [
    {
      "type": "navigate",
      "target": "/account/statements"
    },
    {
      "type": "click",
      "selector_hint": "button containing 'Statements'"
    },
    {
      "type": "select",
      "field": "month",
      "value_from_user": "target_month"
    },
    {
      "type": "click",
      "selector_hint": "download_pdf_button"
    }
  ],
  "approval_required": false,
  "success_signal": "file_downloaded_with_pdf_extension",
  "fallback": [
    "ask_user_to_confirm_page",
    "rerun_page_exploration"
  ],
  "max_retries": 2,
  "confidence": 0.78
}

Key properties of this workflow:

  • Structured and machine-parseable — not a natural-language wish.
  • Explicitly declares uncertainty — selector_hint instead of pretending there is always a stable selector.
  • Explicit success signal — not a false completion.
  • Fallback paths — knows where to retreat on failure.
  • Retry limits — prevents infinite loops.
  • Confidence score — knows how reliable it currently is.

From LLM output to stable DAG, there must be an alignment layer. Real hardening is not saving model-generated JSON directly into production. It means decomposing successful trajectories into nodes, actions, preconditions, state transitions, failure branches, and success signals, then validating through replay, unit tests, shadow testing, and evals. Any action hardened into a workflow must first pass deterministic shadow testing or evaluation-set replay. The confidence score is computed, not self-reported by the model.

The second time, the tenth time, the hundredth time the user says “export my statement,” the system does not re-invoke the frontier model. It runs the workflow deterministically, supplementing judgment only at local uncertainty nodes. Marginal cost approaches zero.

Phase 4: Evolve

Workflows are not one-time artifacts. Pages redesign, DOMs change, CAPTCHAs appear, A/B tests show different interfaces, sessions expire, permissions shift. All of these can break an existing workflow.

The system should not immediately invoke a frontier model for full re-exploration. It should follow an escalation gradient:

  1. Try existing fallback paths.
  2. If fallbacks also fail, trigger a limited-scope LLM re-judgment.
  3. If the failure mode is known, update the corresponding workflow branch directly.
  4. If the failure mode is novel, escalate to the frontier model for re-exploration.

The key variable across all four phases is confidence. A workflow is a set of judgment assets with confidence scores, not rigid rules. Each successful execution raises confidence. Each failure lowers it. When confidence drops below a threshold, the system re-invokes model exploration.

Combining the four phases:

The model opens the road. The system paves it.

The cost curve shifts from “linear growth per model call” to “growth per new scenario and exception.” The former gets more expensive over time. The latter gets cheaper.

The asset is not the answer

The asset is not that the model answered once. The asset is the absorbed judgment.

If the model’s output cannot be captured by the system, even an excellent answer is just an expensive one-time consumption. If the model’s judgment can be absorbed, even an expensive call is an asset investment — it becomes next time’s rule, workflow, skill, or memory, lowering the cost of the next hundred tasks.

  • A correct if becomes a rule.
  • A stable branch becomes a workflow.
  • A recurring judgment pattern becomes a skill.
  • A persistently useful user preference becomes memory.

The dividing line between chat products and Agent Runtimes:

Chat products optimize single-answer quality. Agent Runtimes optimize the conversion of judgment into reusable system assets.

The effectiveness × cost × efficiency formula translates into precise engineering:

  • Effectiveness: determined by the frontier model’s high-dimensional semantic judgment quality during first exploration.
  • Cost: determined by whether the system can absorb that judgment into a workflow.
  • Efficiency: determined by the stability of downstream foreach execution.

These three are multiplicative, not additive. Great effectiveness with uncontrolled cost cannot become commercially viable. Low cost with untrustworthy results creates no willingness to pay. High efficiency that only runs deterministic flows is just RPA.

The engineering discipline:

  • Put high-value ifs on the frontier model — protect effectiveness.
  • Absorb successful ifs into workflows — protect cost.
  • Put foreach on deterministic execution — protect efficiency.

All three are necessary.

5. A Schema Is a Boundary, Not a Form

The four-phase cycle leaves one critical question: what exactly is an if node inside a workflow?

Many builders, the first time they see a JSON workflow or structured schema, instinctively read it as “field filtering from traditional software engineering”:

if degree == "Bachelor's" and years_of_experience >= 5:
    pass
else:
    reject()

This reading immediately degrades the agent into “SQL queries wearing an AI costume.”

A mature agent if node is a boundary condition of a high-dimensional judgment space actively defined by the builder — not field filtering.

A schema is a directional constraint on LLM compute. An LLM without schema constraints is a high-dimensional hallucination field. A well-designed schema projects a precise decision mold into the LLM’s latent space. The builder designs that mold.

If a content-filtering agent’s output enum contains only LIKE, DISLIKE, SKIP, it structurally cannot express “this article creates cognitive friction, but it is important.”

If a research agent’s output enum contains only USEFUL and USELESS, it structurally cannot express “this material is not directly usable, but it provides a critical counter-example.”

If a recruiting agent’s rejection reasons include EDUCATION_BACKGROUND_TOO_LOW, the agent structurally inherits degree-based elimination. If that enum value is removed, the agent structurally cannot use “degree too low” as a final rejection reason.

The boundary of the enum is the boundary of the agent’s thought.

From field filtering to high-dimensional optimization

In a recruiting scenario, a recruiting agent’s schema should not default to a field filter of “degree, years of experience, tech-stack keywords.” It should be a judgment space with constraints, dynamic weights, and evidence chains.

Context includes: industry profile, real job requirements, reporting line, compensation budget, team structure, and candidate status. Dynamic weights can include: degree, big-company experience, project depth, technical capability, chaos tolerance, and output quality under extreme pressure. Hard constraints include: salary must not exceed budget ceiling, availability must not be later than project start date. Output structure includes: interview recommendation, rejection reason, hidden-potential flag, human-review escalation, and evidence chain.

The critical point is the rejection-reason enum. It may include: core skill gap, excessive job-hopping, budget overrun, availability mismatch, cultural fit risk. But it must not include EDUCATION_BACKGROUND_TOO_LOW.

First, the agent can treat education as a dynamic signal but cannot use it as a final rejection conclusion. Education may carry weight in stable organizations and approach zero in an early-stage startup. It is a variable, not a verdict.

Second, compensation budget is a hard constraint, not a dynamic weight. Hard constraints cannot be compensated by other dimensions. Even if a candidate is outstanding, the agent cannot bypass the budget limit — it must escalate to human review.

Third, the agent performs constrained high-dimensional optimization, not field matching. The LLM compresses the semantic world into optimization parameters — extracting dynamic weights w_i, candidate feature scores Feature_i, evidence chains, constraints, and uncertainty. Ranking, filtering, combinatorial selection, and budget-constraint solving are handled by deterministic algorithms.

The system does not solve:

if degree_ok and years_ok:
    interview()

It solves:

Find candidate c that maximizes Σ(w_i * Feature_i)

Subject to:
  Salary <= Budget
  Availability <= ProjectStartDate
  Irreversible risks escalate to human review
  Key conclusions require evidence_chain

The dynamic weights w_i are the critical lever. Education should not become an automatic elimination criterion — not because a prompt says ignore_degree = True, but because the schema forces the agent into a larger context for trade-off analysis: company stage, real role needs, team gap, budget rigidity, and whether the candidate’s strengths compensate for weaknesses.

The shift from field filtering to scenario-level judgment depends on structural constraints, not prompt instructions.

For 25 to 30 years, platforms across recruiting, e-commerce, content, healthcare, and investment have relied on low-resolution proxies — degrees, sales volume, ratings, rankings — not because they preferred these fields, but because they lacked scalable high-dimensional judgment capability.

A schema is the builder’s boundary definition of a high-dimensional judgment space. It is not a field table.

6. Every High-Value If Encodes a Value Function

The preceding sections solve “who judges” and “when to judge.” They do not yet solve “on what basis.”

If the builder only says “hand this to the frontier model,” a gap remains: what ordering does the model’s output obey? What matters more? What cannot be breached? When should the system stop and hand responsibility back to a human?

Every high-value if needs an explicit value function behind it. If you do not define the value function explicitly, the model’s default training biases, the platform’s default commercial interests, or random noise will define it for you.

By “value function,” I do not mean a narrow reinforcement-learning reward function. I mean an operational decision structure: objectives, dynamic weights, hard constraints, risk thresholds, rejection rules, human-approval conditions, and evidence requirements.

The if is the entry point of the value function

Every high-value if is a moment when the product makes a trade-off among multiple values: what matters more, what can be sacrificed, what must stop, what is worth recommending, what must be rejected.

In the old software era, value functions were hardcoded by PMs, algorithm engineers, and platform commercial objectives. Recommendation systems decided dwell-time weight, completion-rate weight, click-through-rate weight. Recruiting platforms decided how degree, company background, tenure, and keywords entered ranking. Users entered the system, provided data and clicks; rules were defined by the system.

The power shift in the agent era: from “system defines rules, user provides data” to “users express value-function parameters, preferences, constraints, and risk boundaries in natural language, and the agent compiles them into executable if/foreach structures that the runtime can run.”

  • A user says “screen suppliers, but prioritize eliminating delivery risk over low price” — injecting a procurement value function.
  • A user says “screen candidates, don’t use education as a rejection reason, focus on real output in chaotic environments” — rewriting the recruiting agent’s judgment boundary.
  • A user says “review this contract; the deal can move slowly, but liability transfer and data-usage boundaries need to be conservative” — defining the contract agent’s risk function.

The high-value if is the entry point through which the user’s value function enters the Runtime. The agent’s job is to compile these natural-language value orderings into schema, weights, hard constraints, human-review rules, audit fields, workflow branches, and executable tool calls. Natural language is not the endpoint. The Runtime is the endpoint. Expression is not the asset. A compiled, executed, and reviewable value function is the asset.

The intent compiler is not a shallow metaphor for “translating user speech into code.” What it truly compiles is value functions: pressing vague, implicit, trade-off-laden user intent into structures the system can execute, audit, roll back, and reuse.

No high-value if is neutral

There is no such thing as a neutral high-value if. What appears to be a “neutral tool” often simply hides its default value function.

Internet platforms make daily ifs. Who ranks first in search? What does the feed show next? Which product appears in e-commerce search? Which candidate gets exposure on the recruiting platform? These ifs look like algorithmic ranking. They are actually value ranking. The most common default value function for internet platforms has been maximizing dwell time, click-through rate, conversion rate, ad revenue, and GMV (gross merchandise volume).

Current mainstream LLMs have ifs too. When the user asks a question — give a complete answer, or admit uncertainty? When the user’s plan has problems — agree, or point it out? When the user is emotional — empathize, or encourage calm? These are not tone choices. They are value choices.

Mainstream models have established Provider-level Alignment: general behavioral norms, safety rules, preference alignment. But Provider-level Alignment cannot replace Builder-level, Domain-level, or User-level value functions. The model company defines the lowest-common-denominator behavioral boundary. Industrial agents need concrete value orderings for specific scenarios.

High-value ifs often have a trolley-problem structure: different value frameworks can be internally coherent while producing different decisions. The issue is not that the model is not smart enough. The issue is that the decision itself has no value-neutral answer.

Factual boundaries come before value functions. Factual errors are still errors. Violations of law are still violations. Safety incidents are still incidents. Facts, law, safety, and the responsibility floor cannot be rewritten by value functions. High-value ifs enter the choice space only above these floors.

A stronger model cannot define the value function for the builder

A common approach is a consultant-executor architecture: a cheaper model handles execution, consulting a stronger model at critical points. This reduces cost and improves task completion. But it solves the capability-and-cost problem, not the value-function-source problem.

The stronger model’s judgment still comes from the provider’s blended alignment and default value function. It does not automatically know what your product stands for, what your user’s authorization boundaries are, what your responsibility structure looks like, or which bottom lines you would rather lose short-term conversion to defend.

The builder’s responsibility is not to find a stronger model to make value judgments. It is to actively define: in this specific scenario, what value function governs the high-value ifs — and then ensure that regardless of which model tier is called, this value function is caught by schema, workflow, audit log, and human-approval boundaries.

Engineering solutions can optimize capability. Engineering solutions cannot assume the builder’s value-function definition authority.

An industrial agent must be delegable, auditable, reproducible, and accountable over time. This is why the LLM cannot be the final decision structure. It handles ambiguity; the runtime collapses ambiguity into schema, workflow, approval gates, and audit logs.

The engineering triad: schema, workflow, audit log

The builder cannot write a value function into an agent with slogans. Three instruments are needed:

Schema is the shape of the value function. It specifies which signals are usable, which are reference-only, which constraints are hard, which conclusions are allowed, and which judgments must escalate to human review.

Workflow is the path of the value function. If the product values long-term interest, the workflow must read a long time window. If the product opposes preference flattery, the workflow must surface opposing perspectives. If the product respects factual boundaries, the workflow must perform cross-source verification and explicitly label uncertainty. The LLM cannot skip risk enumeration because the user is in a hurry, omit alternatives because the answer sounds fluent, or pretend information is sufficient when it is not.

Audit log is the evidence of the value function. It records authorization scope, input data, model version, schema version, workflow path, human-review nodes, final action, and post-hoc remediation. An agent without audit logs cannot enter high-trust workflows: errors cannot be located, located errors cannot be explained, unexplained errors cannot be fixed, and unfixed errors cannot sustain long-term responsibility.

Schema gives the value function structure. Workflow makes it reproducible. Audit log makes it accountable.

The Value Function Spec

Every high-value if can be written as a one-page Value Function Spec: primary objective, hard boundaries, what may be sacrificed, what is never sacrificed, who receives escalation on conflict, and which output-schema fields express evidence, gaps, counter-examples, and rejection reasons. Without this spec, “intelligent judgment” is just model default bias.

Taking supplier screening as an example:

  1. Metadata: version, domain, responsible party, floor constraints. The system serves buyer procurement, not supplier exposure. Compliance, no fabricated evidence, and no hidden paid ranking are floor constraints.

  2. Objectives and dynamic weights: the primary objective is minimizing total procurement risk, not finding the cheapest supplier. Delivery reliability, quality-evidence strength, total cost of ownership, and compliance cleanliness outrank the quote. Low price is a weak signal. Buyer long-term health holds veto power.

  3. Prohibited decision factors: supplier paid exposure, platform ranking, and sales-pitch quality cannot serve as recommendation or elimination criteria.

  4. Hard constraints and escalation: high compliance risk requires human review or rejection. Missing core evidence requires supplementary materials. Amounts above the approval threshold enter Human-in-the-loop approval. Hard constraints cannot be compensated by other strengths.

  5. Output structure: finite decision enum, confidence score, evidence chain, counter-evidence, missing-information type, risk flags, next-step recommendations, and human-review flag. missing_information_type and counter_evidence must be in the output structure.

  6. Workflow and audit: the workflow enforces evidence collection, material normalization, dynamic-weight scoring, hard-constraint checking, counter-evidence generation, human-review routing, and audit writing. The audit log records value-function version, input source, model version, workflow version, overridden recommendations, missing-information types, confidence scores, and human-review records.

The engineering chain:

Thought → Schema → Workflow → Audit Log.

Thought that does not enter schema has no structure. Schema that does not enter workflow cannot be reproduced. Workflow that does not enter audit log cannot be held accountable.

A value function is not philosophical decoration. It is the judgment structure of the Agent Runtime.

Restraint as engineering discipline

A high-trust agent is defined less by what it can do than by what it refuses to do: actions it can automate but should not, revenue it can capture but should not, preferences it can flatter but should not, and ambiguity it can hide behind but should not.

Can act, but does not: capability restraint enters the workflow. A recruiting agent can auto-decide whether a candidate enters the final round, but senior positions should escalate to human review. An investment agent can auto-generate buy/sell recommendations, but decisions exceeding a percentage of user assets need human approval. A legal agent can assess contract risk, but major legal liability requires a professional’s signature. The implementation is physically inserting Human-in-the-loop approval nodes into the workflow. When contract amounts exceed the threshold, user asset exposure exceeds risk budget, or an action creates an external commitment, the process pauses and requests human approval — and this node cannot be bypassed by the model, user emotion, or commercial metrics.

Can profit, but does not: metrics restraint enters the schema’s dynamic weights. The product could add more aggressive recommendations, stronger emotional hooks, harder-to-cancel flows — all boosting short-term metrics. But a high-trust agent cannot turn user authorization into growth fuel. The implementation gives user_long_term_health, trust_preservation, and downside_risk weight or veto power that overrides short-term GMV in the schema. Once addiction risk, hidden costs, or irreversible financial loss triggers, the system stops the conversion path.

Can flatter, but does not: emotional restraint enters the workflow branch. The user wants their startup direction affirmed — the agent still points out major risks. The user wants to make an impulsive investment — the agent still opposes. The user is emotionally agitated and wants to decide immediately — the agent triggers a cooling-off period. Implementation means writing emotional-polarization detection into workflow branches. When panic, euphoria, or anger is detected and the current decision is irreversible, the system enters a cooling period, generates counter-arguments, and requires delayed confirmation.

Can obscure, but does not: responsibility restraint enters the audit log. Vague answers are safest: “both sides have a point,” “it depends,” “this is complex, consider everything.” These rarely err and rarely help. An industrial agent should give clear judgment when evidence is sufficient, explicitly acknowledge uncertainty when evidence is lacking, and explicitly escalate to human review when responsibility exceeds its boundary. Implementation means making “I don’t know” leave computational evidence: the system records decision state, missing-information type, confidence score, evidence-gap reason, and next-step action.

Restraint that stays in text gets swallowed by growth pressure. Restraint that enters schema, workflow, and audit log becomes systemic discipline.

7. The If/Foreach Ledger

Model placement ultimately needs to materialize as an if/foreach ledger.

Every agent MVP needs to maintain this ledger. It records at least eight categories:

  • Which nodes are high-value ifs, and why they cannot be handled by fields, rules, or low-trust models.
  • Which nodes are low-value ifs, and why they can be handed to programs, rules, small models, or caches.
  • Which steps are foreach, and whether they can be batched, parallelized, retried, replayed, and monitored.
  • Which ifs have already been absorbed by historical workflows and do not need re-exploration each time.
  • Which nodes need a frontier model to open the road, and which only need a mid-tier model to assist.
  • Which nodes need human approval, because the action is irreversible or responsibility cannot be automatically transferred.
  • Which successful judgments can be encoded as workflows, skills, memory, or evaluation samples.
  • Which downgrade conditions have been met, allowing expensive model calls to be moved off the critical path.

The ledger’s purpose is not documentation theater. It forces the builder to ask before every model call: is this truly incompressible uncertainty, or has the system lazily wrapped repetitive execution in an “intelligence” label?

How the ledger matures

The first time, the ledger records where the task genuinely requires judgment.

The second time, it records which judgments can be absorbed by workflows.

The third time, it records which processes have migrated from model calls to deterministic execution.

By the tenth time, the ledger starts exposing product boundaries: which judgments still need humans, which can be entrusted to models, and which actions should never be automated.

A mature Agent Runtime is not one that calls the strongest model everywhere. It is one whose ledger gets thicker over time: fewer unnecessary model calls, more hardened workflows, clearer approval gates, and a better understanding of which decisions should never be automated.

Once model placement is ledgered, value functions, low-resolution proxies, and responsibility redistribution all become engineerable problems.

Summary

The agent shift is not only a model shift. It redistributes computation, responsibility, and asset ownership.

The core concepts of this essay:

  • High-value if / low-value foreach: the model appears at judgment branches, not inside execution loops.
  • Model placement over model frequency: the key question is not how many times the model is called, but where it is placed.
  • The model opens the road, the system paves it: explore → prune → harden → evolve.
  • A schema is a boundary, not a form: the boundary of the enum is the boundary of the agent’s thought.
  • Every high-value if encodes a value function: no high-value if is neutral; someone always defines what matters more.
  • Schema / workflow / audit log: thought must enter schema to have structure, enter workflow to be reproducible, enter audit log to be accountable.
  • The if/foreach ledger: the engineering artifact that turns abstract discipline into daily practice.

An agent is, at its core, an intent compiler. The old internet compressed human intent into low-resolution SQL through forms and clicks. In the agent era, users express intent in natural language and ambiguous context. The agent’s entire job is to first identify where incompressible uncertainty exists, then compile the clarified portions into a tightly-fitted, responsibility-bounded foreach automation pipeline. The agent absorbs uncertainty at compile time. The system sprints on deterministic rails at runtime.

In the long run, whoever defines the high-value ifs controls the decision layer. Whoever controls the decision layer builds a practical form of digital sovereignty — not through owning the model, but through owning the judgment structure that the model serves.

The builder’s position comes down to this: in every specific scenario, write a clear, explainable, reproducible, and accountable value function into every high-value if.

I Built a Football Tournament Simulator with Vue 3 ⚽

I Built a Football Tournament Simulator with Vue 3 ⚽

I recently built a browser-based football tournament simulator using Vue 3 and TypeScript.

What started as a very small bracket app slowly turned into a much bigger project with features like:

  • Tournament brackets
  • Match simulation
  • Penalty shootouts
  • Team power system
  • Seasons
  • Seeded draws
  • Match history
  • Responsive design
  • Import/export system

You can create your own tournaments, simulate matches, and watch the bracket progress until a champion is crowned.

Why I Built It

I enjoy football management and simulation games, so I wanted to build something similar in the browser while also improving my Vue and TypeScript skills.

At first, the app was extremely simple:

  • create teams
  • simulate matches
  • show a bracket

But after adding more systems and improving the simulation logic, the project became much more fun to work on.

One of the Most Fun Parts

The most interesting part was definitely the match engine.

I tried to make matches feel:

  • random enough to stay exciting
  • but realistic enough so stronger teams usually perform better

Later, I also added penalty shootouts for knockout matches.

Small changes in the algorithm completely changed how the tournaments felt.

Tech Stack

  • Vue 3
  • TypeScript
  • Vite
  • Pinia
  • Vue Router

Features Added Over Time

Some of my favorite additions were:

  • Fullscreen bracket mode
  • Team form/history pages
  • Responsive layout support
  • Dark mode improvements
  • Team-colored confetti 🎉
  • Dataset import/export
  • Sample tournament datasets

I also refactored many parts into composables to keep the project cleaner as it grew.

Links

GitHub Repository:
GitHub Repo

Live Demo:
Vercel Demo

Final Thoughts

This project started as a tiny MVP and became one of my favorite frontend projects so far.

Building simulation systems is surprisingly addictive because every small tweak changes the entire experience.

I still plan to add more features in the future like:

  • group stages
  • league systems
  • player stats
  • better AI simulation

Thanks for reading ⚽

Shopify ShieldScan

Your Shopify store isn’t just a storefront.

For many enterprises, it’s a revenue system — connected to payments, customers, and brand trust.

But here’s what we keep seeing: the platform is solid. The apps and themes around it often aren’t.

Old permissions. Forgotten scripts. Keys in theme files. “It was installed by the agency two years ago.”

That’s why i built ShieldScan — security auditing built for Shopify, not generic website scanners.

Scan in minutes. Get an A–F grade. Know what to fix before customers or attackers do.

Curious — who owns “security” for your Shopify stack today? IT, e‑commerce, or… nobody?

shopify #store #security #privacy

https://apps.shopify.com/shieldscan

Running Gemma 4 Inside a Docker Container with GPU Passthrough

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

Here’s a situation every ML team eventually hits.

Your intern gets Gemma 4 running locally. Beautiful. Fast. Impresses the standup. Then they leave for the semester. Three weeks later, nobody can reproduce the setup. The model was in a path that no longer exists. The Python version was wrong. The CUDA libraries conflicted with PyTorch. Someone pip installed something globally and broke everything.

This is why Docker exists. And yet, every Gemma 4 setup guide I’ve found uses either the Ollama GUI, LM Studio’s drag-and-drop interface, or bare ollama run commands that vanish when the terminal closes. None of them answer the actual question a dev team has:

How do we version-control this, ship it to staging, and have it work the same way every time?

This guide answers that. By the end, you’ll have:

  • A Dockerfile your team can pin, review, and modify in a PR
  • A docker-compose.yml that brings up the full stack with one command
  • A running Gemma 4 endpoint that speaks OpenAI’s API format — meaning zero changes to any code that already calls GPT-4o

Let’s build it.

Why Docker + GPU Is Annoying (and How It Actually Works)

Before the files, a quick mental model — because this is where most people get confused.

CUDA is not inside your container. The NVIDIA driver lives on the host machine. The container runtime (Docker) needs a way to reach through the container wall and talk to those drivers. That bridge is the NVIDIA Container Toolkit.

Here’s the flow:

Your Container
    └── CUDA libraries (inside the image)
          └── NVIDIA Container Runtime  ← the bridge
                └── Host NVIDIA Driver  ← never inside the container
                      └── Physical GPU

This means:

  1. You do NOT need to install CUDA on your host. Only the NVIDIA driver.
  2. You do NOT bundle the driver inside your Docker image. Only CUDA libraries.
  3. The container toolkit version on your host must be compatible with your driver version.

A common gotcha: if your host driver is old (e.g., 470.x), it cannot run CUDA 12.x containers. The CUDA version inside your image must be less than or equal to what your driver supports. Check first:

nvidia-smi
# Look for: "CUDA Version: XX.X" in the top-right corner
# That's the maximum CUDA your driver supports

Prerequisites Checklist

Before touching any Docker file, verify these four things on your host machine:

# 1. Confirm NVIDIA GPU is visible
lspci | grep -i nvidia

# 2. Check driver is installed and working
nvidia-smi

# 3. Check Docker version (need 19.03+ for native --gpus support)
docker --version

# 4. Check NVIDIA Container Toolkit is installed
nvidia-ctk --version

If step 4 fails, install the toolkit:

# Ubuntu/Debian
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | 
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | 
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | 
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit

# Wire it into Docker
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

# Test — you should see nvidia-smi output from inside a container
docker run --rm --gpus all nvidia/cuda:12.3.0-base-ubuntu22.04 nvidia-smi

If that last command shows your GPU, you’re ready. If it errors, stop here and fix the driver — no amount of clever Dockerfiles will help.

The Setup We’re Building

We’re going to run three containers in a single Compose stack:

Container What it does Port
gemma4 Ollama serving Gemma 4 (with GPU) 11434 (internal)
litellm OpenAI-compatible proxy in front of Ollama 8000 (external)
model-puller One-shot container that pulls the model on first boot —

The LiteLLM proxy is the key piece most guides skip. It translates OpenAI API calls (/v1/chat/completions) into Ollama’s native format, adds API key auth, rate limiting, and request logging — all without touching your application code.

Project Structure

Create this directory layout before writing any files:

gemma4-stack/
├── .env                    # Secrets and config — never commit this
├── .env.example            # Template for new team members
├── docker-compose.yml      # The full stack
├── Dockerfile.ollama       # Custom Ollama image with health checks
├── litellm/
│   └── config.yaml         # LiteLLM model routing config
├── scripts/
│   └── pull-model.sh       # Idempotent model pull script
└── tests/
    └── smoke_test.py       # Verify the stack works end-to-end

Step 1: The .env File

# .env — copy from .env.example and fill in your values
# !! Never commit this file — add .env to .gitignore

# Which Gemma 4 variant to run
# Options: gemma4:e2b | gemma4:e4b | gemma4:26b | gemma4:31b
GEMMA_MODEL=gemma4:26b

# API key for LiteLLM (clients use this to authenticate)
# Generate a random one: openssl rand -hex 32
LITELLM_MASTER_KEY=sk-your-secret-key-here

# How long Ollama keeps the model in VRAM after the last request
# Increase this if you have spare VRAM and want faster cold starts
OLLAMA_KEEP_ALIVE=10m

# Max parallel inference requests (tune to your VRAM)
OLLAMA_NUM_PARALLEL=2

# HuggingFace token — needed only if you pull gated models
HF_TOKEN=
# .env.example — commit this
GEMMA_MODEL=gemma4:26b
LITELLM_MASTER_KEY=sk-CHANGE-ME
OLLAMA_KEEP_ALIVE=10m
OLLAMA_NUM_PARALLEL=2
HF_TOKEN=

Step 2: The Dockerfile

This is where we diverge from every “just use the ollama image” guide. Our custom image adds:

  • A proper health check endpoint (so Compose knows when Ollama is actually ready)
  • A non-root user (security baseline for any production-adjacent setup)
  • Pinned Ollama version (reproducibility)
# Dockerfile.ollama
# Pin to a specific Ollama release — update deliberately, not accidentally
FROM ollama/ollama:0.9.3

# Metadata
LABEL maintainer="[email protected]"
LABEL description="Ollama serving Gemma 4 with GPU passthrough"
LABEL gemma.version="gemma4"

# GPU access configuration
# NVIDIA_VISIBLE_DEVICES and NVIDIA_DRIVER_CAPABILITIES tell the
# container runtime which GPU capabilities to expose.
# "compute" = CUDA, "utility" = nvidia-smi
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility

# Ollama configuration
ENV OLLAMA_HOST=0.0.0.0
ENV OLLAMA_ORIGINS=*

# Keep the model warm in VRAM (overridable via docker-compose env)
ENV OLLAMA_KEEP_ALIVE=10m

# Number of parallel inference slots
# Each slot uses roughly model_size / 2 additional VRAM
ENV OLLAMA_NUM_PARALLEL=2

# Flash attention — reduces memory for long contexts
ENV OLLAMA_FLASH_ATTENTION=1

# Model storage location inside the container
# We'll mount a volume here to persist across container restarts
ENV OLLAMA_MODELS=/models

# Create the models directory and set ownership
RUN mkdir -p /models && chmod 755 /models

# Expose Ollama's API port
EXPOSE 11434

# Health check: ask Ollama if it's ready
# --start-period gives it 60s to load before health checks count
HEALTHCHECK --interval=15s --timeout=10s --retries=5 --start-period=60s 
  CMD curl -sf http://localhost:11434/api/tags || exit 1

# Default command — starts Ollama server
CMD ["ollama", "serve"]

Why pin ollama:0.9.3 and not ollama:latest?

Because latest on a production service means “surprise breaking changes on every docker pull.” Pin it. Update it intentionally via PR. When a new Ollama release drops and the team wants to update, the diff is one line and the change is reviewable.

Step 3: The Model Pull Script

This script runs once on startup and is idempotent — running it twice does nothing harmful:

#!/usr/bin/env bash
# scripts/pull-model.sh
# Pulls the Gemma 4 model if not already present
# Safe to run multiple times — checks before pulling

set -euo pipefail

OLLAMA_HOST="${OLLAMA_HOST:-http://gemma4:11434}"
MODEL="${GEMMA_MODEL:-gemma4:26b}"

echo "==> Waiting for Ollama to be ready at ${OLLAMA_HOST}..."

# Wait up to 5 minutes for Ollama to respond
for i in $(seq 1 60); do
  if curl -sf "${OLLAMA_HOST}/api/tags" > /dev/null 2>&1; then
    echo "==> Ollama is up."
    break
  fi
  echo "    Attempt ${i}/60 — retrying in 5s..."
  sleep 5
done

# Check if model already exists
if curl -sf "${OLLAMA_HOST}/api/tags" | grep -q ""${MODEL}""; then
  echo "==> Model '${MODEL}' already present. Skipping pull."
  exit 0
fi

echo "==> Pulling model: ${MODEL}"
echo "    This can take 5–20 minutes depending on your connection."
echo "    The 26B model is ~15GB. Get a coffee."

curl -sf -X POST "${OLLAMA_HOST}/api/pull" 
  -H "Content-Type: application/json" 
  -d "{"name": "${MODEL}", "stream": false}"

echo "==> Pull complete. Model '${MODEL}' is ready."
chmod +x scripts/pull-model.sh

Step 4: LiteLLM Config

LiteLLM is the OpenAI-compatible gateway. This config tells it that “gemma-4” (what your app calls) maps to “ollama_chat/gemma4:26b” (what Ollama actually serves):

# litellm/config.yaml
model_list:
  # Gemma 4 26B MoE — best balance of speed and quality
  - model_name: gemma-4
    litellm_params:
      model: ollama_chat/gemma4:26b
      api_base: http://gemma4:11434

  # Gemma 4 E4B — fast, low VRAM, good for quick tasks
  - model_name: gemma-4-fast
    litellm_params:
      model: ollama_chat/gemma4:e4b
      api_base: http://gemma4:11434

  # Gemma 4 31B Dense — maximum quality, needs 24GB+ VRAM
  - model_name: gemma-4-max
    litellm_params:
      model: ollama_chat/gemma4:31b
      api_base: http://gemma4:11434

litellm_settings:
  # Log all requests (set to false in prod if you handle this elsewhere)
  set_verbose: false

  # Drop unsupported params instead of erroring
  # Gemma doesn't support every OpenAI parameter (e.g., logprobs)
  drop_params: true

  # Request timeout in seconds
  request_timeout: 120

general_settings:
  # Master key for authentication
  # Clients send: Authorization: Bearer sk-your-secret-key-here
  master_key: "os.environ/LITELLM_MASTER_KEY"

Step 5: The docker-compose.yml

This is the centrepiece. Read it carefully — every decision has a comment explaining why:

# docker-compose.yml
# Gemma 4 production-grade local inference stack
# Usage: docker compose up -d
# First run: docker compose up -d && docker compose logs -f model-puller

name: gemma4-stack

services:

  # ─────────────────────────────────────────────
  # OLLAMA: The inference engine with GPU access
  # ─────────────────────────────────────────────
  gemma4:
    build:
      context: .
      dockerfile: Dockerfile.ollama
    container_name: gemma4-ollama

    # GPU passthrough — this is the magic line
    # "count: all" exposes every GPU on the host
    # Change to "count: 1" or "device_ids: ['0']" to pin a specific GPU
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

    # Model storage: named volume persists across container recreations
    # Without this, you re-download 15GB every time you update the image
    volumes:
      - gemma4-models:/models

    environment:
      OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-10m}
      OLLAMA_NUM_PARALLEL: ${OLLAMA_NUM_PARALLEL:-2}
      OLLAMA_FLASH_ATTENTION: "1"
      OLLAMA_MODELS: /models

    # Do NOT expose port 11434 to the host
    # All external traffic goes through LiteLLM on port 8000
    # This prevents clients from bypassing auth
    expose:
      - "11434"

    restart: unless-stopped

    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:11434/api/tags"]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 60s

  # ─────────────────────────────────────────────
  # MODEL PULLER: Downloads Gemma 4 on first boot
  # Exits cleanly after pulling — not a long-running service
  # ─────────────────────────────────────────────
  model-puller:
    image: curlimages/curl:latest
    container_name: gemma4-model-puller

    volumes:
      - ./scripts/pull-model.sh:/pull-model.sh:ro

    environment:
      OLLAMA_HOST: http://gemma4:11434
      GEMMA_MODEL: ${GEMMA_MODEL:-gemma4:26b}

    entrypoint: ["/bin/sh", "/pull-model.sh"]

    depends_on:
      gemma4:
        condition: service_healthy

    # This container should exit 0 after pulling the model
    restart: "no"

  # ─────────────────────────────────────────────
  # LITELLM: OpenAI-compatible API gateway
  # Your apps connect to THIS, not to Ollama directly
  # ─────────────────────────────────────────────
  litellm:
    image: ghcr.io/berriai/litellm:main-stable
    container_name: gemma4-litellm

    volumes:
      - ./litellm/config.yaml:/app/config.yaml:ro

    environment:
      LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}

    command: ["--config", "/app/config.yaml", "--port", "8000"]

    ports:
      # THIS is the only port exposed to your host machine
      # http://localhost:8000/v1 — drop-in replacement for OpenAI
      - "8000:8000"

    depends_on:
      gemma4:
        condition: service_healthy

    restart: unless-stopped

    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
      interval: 20s
      timeout: 10s
      retries: 3

# ─────────────────────────────────────────────
# VOLUMES
# ─────────────────────────────────────────────
volumes:
  gemma4-models:
    driver: local
    # Uncomment to bind to a specific host path instead:
    # driver_opts:
    #   type: none
    #   o: bind
    #   device: /data/gemma4-models

networks:
  default:
    name: gemma4-net

Step 6: Boot It Up

# Clone or create the project directory
cd gemma4-stack

# Copy the env template and fill in your master key
cp .env.example .env
# Edit .env — at minimum, set LITELLM_MASTER_KEY

# Build the Ollama image and start the stack
docker compose up -d

# Watch the model download (first run only — this takes a while)
docker compose logs -f model-puller

# Once model-puller exits, check everything is healthy
docker compose ps

Expected output from docker compose ps once everything is running:

NAME                   IMAGE                          STATUS
gemma4-ollama          gemma4-stack-gemma4            Up (healthy)
gemma4-litellm         ghcr.io/berriai/litellm:...   Up (healthy)
gemma4-model-puller    curlimages/curl:latest         Exited (0)

The model-puller showing Exited (0) is correct — it finished its job and stopped cleanly.

Step 7: Test the OpenAI-Compatible Endpoint

Your stack is now running a drop-in OpenAI replacement at http://localhost:8000/v1. Test it:

# Check available models
curl http://localhost:8000/v1/models 
  -H "Authorization: Bearer ${LITELLM_MASTER_KEY}"

# Send a chat completion — same format as OpenAI
curl http://localhost:8000/v1/chat/completions 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer ${LITELLM_MASTER_KEY}" 
  -d '{
    "model": "gemma-4",
    "messages": [
      {
        "role": "system",
        "content": "You are a senior backend engineer doing code review."
      },
      {
        "role": "user",
        "content": "Review this Python function and identify any issues:nndef get_user(id):n    return db.query(f"SELECT * FROM users WHERE id={id}")"
      }
    ],
    "temperature": 0.3,
    "max_tokens": 512
  }'

Expected response — Gemma 4 should immediately flag the SQL injection:

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "gemma-4",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "Critical issue: SQL injection vulnerability on line 2. The `id` parameter is interpolated directly into the query string with an f-string. An attacker could pass `id = '1 OR 1=1 --'` and retrieve all users. Fix: use parameterized queries:nn```

pythonndef get_user(user_id: int):n    return db.query(n        "SELECT * FROM users WHERE id = %s",n        (user_id,)n    )n

```nnAlso: rename the parameter from `id` to `user_id` — `id` shadows a Python built-in."
    },
    "finish_reason": "stop"
  }]
}

Step 8: Drop It Into Your Existing Code

Here’s the part that makes this setup valuable to a team. If you already have code calling OpenAI, you change two lines:

Python (openai SDK)

# Before — calling OpenAI
from openai import OpenAI

client = OpenAI(api_key="sk-your-openai-key")

# After — calling your local Gemma 4 stack
from openai import OpenAI
import os

client = OpenAI(
    base_url="http://localhost:8000/v1",  # ← changed
    api_key=os.environ["LITELLM_MASTER_KEY"],  # ← changed
)

# Everything below is identical — no other changes needed
response = client.chat.completions.create(
    model="gemma-4",  # or "gemma-4-fast" for E4B, "gemma-4-max" for 31B
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain what a Docker volume is in one sentence."},
    ],
    temperature=0.7,
    max_tokens=200,
)

print(response.choices[0].message.content)

Node.js / TypeScript

import OpenAI from "openai";

// Same two-line swap
const client = new OpenAI({
  baseURL: "http://localhost:8000/v1",
  apiKey: process.env.LITELLM_MASTER_KEY,
});

const response = await client.chat.completions.create({
  model: "gemma-4",
  messages: [
    { role: "user", content: "Write a TypeScript type for a paginated API response." }
  ],
});

console.log(response.choices[0].message.content);

LangChain

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="your-litellm-master-key",
    model="gemma-4",
    temperature=0,
)

response = llm.invoke("What are the SOLID principles? Give me one sentence per principle.")
print(response.content)

Step 9: The Smoke Test

Add this to your CI pipeline or run it after deployment to verify the stack is working:

# tests/smoke_test.py
"""
Smoke test for the Gemma 4 Docker stack.
Run with: python tests/smoke_test.py
Exit 0 = healthy. Exit 1 = something's broken.
"""

import os
import sys
import time
from openai import OpenAI

BASE_URL = os.environ.get("GEMMA_BASE_URL", "http://localhost:8000/v1")
API_KEY = os.environ.get("LITELLM_MASTER_KEY", "sk-test")
MODEL = os.environ.get("GEMMA_MODEL_ALIAS", "gemma-4")

def check(name: str, condition: bool, detail: str = ""):
    if condition:
        print(f"  ✓ {name}")
    else:
        print(f"  ✗ {name}{': ' + detail if detail else ''}")
        sys.exit(1)

print(f"nSmoke test — {BASE_URL}n")

client = OpenAI(base_url=BASE_URL, api_key=API_KEY)

# Test 1: Models endpoint
models = client.models.list()
model_ids = [m.id for m in models.data]
check("Models endpoint", len(model_ids) > 0, f"got: {model_ids}")
check(f"Model '{MODEL}' available", MODEL in model_ids)

# Test 2: Basic chat completion
start = time.time()
resp = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "Reply with exactly the word PONG and nothing else."}],
    max_tokens=10,
    temperature=0,
)
elapsed = time.time() - start

reply = resp.choices[0].message.content.strip().upper()
check("Chat completion returns", "PONG" in reply, f"got: {reply!r}")
check("Response time < 30s", elapsed < 30, f"took {elapsed:.1f}s")

# Test 3: Streaming
chunks = []
for chunk in client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": "Count to 3, one number per line."}],
    max_tokens=30,
    stream=True,
):
    if chunk.choices[0].delta.content:
        chunks.append(chunk.choices[0].delta.content)

check("Streaming works", len(chunks) > 0, f"got {len(chunks)} chunks")

print(f"nAll checks passed. Stack is healthy.n")

Useful Day-to-Day Commands

# Start the stack
docker compose up -d

# Tail logs from all containers
docker compose logs -f

# Tail logs from one container
docker compose logs -f gemma4

# Check GPU utilization inside the container
docker exec gemma4-ollama nvidia-smi

# List loaded models and their VRAM usage
docker exec gemma4-ollama ollama ps

# Pull a different model (e.g., try 31B if you have the VRAM)
docker exec gemma4-ollama ollama pull gemma4:31b

# Hard restart (keeps model volume intact)
docker compose restart gemma4

# Full teardown — preserves the model volume
docker compose down

# NUCLEAR: full teardown including the 15GB model volume
# Only do this if you want to re-download everything
docker compose down -v

# Update to a newer Ollama version (edit Dockerfile.ollama first)
docker compose build --no-cache gemma4
docker compose up -d gemma4

# Check container resource usage
docker stats gemma4-ollama

Hardware Decision Guide

Not sure which Gemma 4 variant to put in your .env? Here’s the honest breakdown:

Your hardware Set GEMMA_MODEL to Why
RTX 3090/4090 (24 GB VRAM) gemma4:31b Full quality, fits comfortably
RTX 3080/4080 (12–16 GB VRAM) gemma4:26b MoE activates only 3.8B — fits and runs fast
RTX 3060/4060 (8 GB VRAM) gemma4:e4b Edge model, good quality, 3.5 GB VRAM
Any GPU with 6 GB VRAM gemma4:e2b Lightweight, still genuinely useful
CPU only (no GPU) gemma4:e2b Slow but functional; ~3–5 tok/s on modern CPU
2× GPUs (any combo) gemma4:31b + tensor parallel See multi-GPU note below

For multi-GPU setups, add to your Ollama environment:

environment:
  OLLAMA_GPU_LAYERS: 999      # Offload all layers to GPU
  OLLAMA_SCHED_SPREAD: "1"    # Spread across multiple GPUs

Common Errors and Fixes

nvidia-smi: command not found inside container
Your NVIDIA Container Toolkit isn’t wired into Docker. Re-run sudo nvidia-ctk runtime configure --runtime=docker and restart Docker.

CUDA error: no kernel image is available for execution on the device
Your GPU is too old for the CUDA version in the Ollama image. Check nvidia-smi for your driver version and use an older Ollama base image with a compatible CUDA tag.

LiteLLM returns APIConnectionError
The litellm container can’t reach gemma4. Check they’re on the same network: docker network inspect gemma4-net. The service name gemma4 in the LiteLLM config must match the Compose service name exactly.

docker compose up says GPU not found on Linux
The Docker daemon didn’t get restarted after toolkit installation. Run sudo systemctl restart docker.

Model-puller keeps restarting instead of exiting
Ollama isn’t healthy yet. Increase start_period in the Ollama healthcheck from 60s to 120s — large models take longer to initialize on slow disks.

What This Unlocks for Your Team

Once this stack is running, here’s what your team gets for free:

Reproducibility. git clone + cp .env.example .env + docker compose up -d = working Gemma 4 in under 20 minutes for any new team member. No “it works on my machine.”

Version control. Every change to the model, Ollama version, or config goes through a PR. Your ML setup is as auditable as your application code.

OpenAI drop-in. Any code that calls OpenAI works immediately. Switch between gemma-4 (local) and gpt-4o (cloud) by changing one environment variable. Useful for cost comparisons and offline development.

Privacy by default. No token leaves the machine. Customer data, proprietary code, internal documents — all processed locally with no third-party API calls.

Zero cloud dependency. The stack works on a laptop, a workstation, an on-prem server, or an air-gapped environment. The only network requirement is the initial model download.

References

I vibe-coded a Postgres workspace because pgAdmin and DBeaver don’t fit how I work

Heads up before anything else: this is built with Claude Code. I don’t have time to write a Postgres admin tool from scratch, and the existing ones
just annoy me. I don’t want another desktop app to install on every machine. I don’t want to hand my prod creds to some SaaS. I want something I
can drop into my docker-compose, point at my dev DB, and forget exists.

So I made dbil. One container, around 20MB, React UI baked in. Add it as a service, slap a dbil.enable: “true” label on your Postgres container,

and it picks the connection up on its own. That’s the whole setup.

What it does — real ER diagram pulled from pg_catalog, no manual dragging boxes around. SQL editor with autocomplete. Paginated row browsing. Live
metrics for TPS, cache hit ratio, replication lag, slow queries, lock chains, unused indexes. The stuff you’d actually look at.

Safety is tied to a connection tag. Tag something production and DDL is just refused. DELETE or UPDATE without a WHERE gets blocked. Spot a session
blocking five others? Hit Kill and dbil sends pg_terminate_backend. No second-guessing.

State file is encrypted at the app layer — AES-256-GCM, per field, per row. Master key can come from KMS, OS keychain, a mounted secret file, an

env var, or a TTY prompt. Audit log is forward-chained SHA-256, so if someone tampers with a row directly the chain breaks and the verifier flags
it. Multi-arch images signed with cosign keyless OIDC, every release ships an SPDX SBOM.

Repo: https://github.com/unkabas/dbil

Not pretending this is enterprise-grade. It’s a personal tool that turned into something I want to keep building, and I’d rather do it in the open
than sit on it. Try it, break it, star it if it’s useful. Open issues with whatever feels off — that’s the only way it gets better.

Front Controller: The Pattern That Unifies Your Web Application’s Entry Point

Part of the series: Enterprise Application Architecture Patterns — Martin Fowler

The Problem You’ve Probably Already Faced

You’re building an API. You have /users, /products, /orders. Every endpoint needs to verify the token, log the request, handle unexpected errors. And before you know it, you end up with this:

# users.py
def handle(request):
    log(request)                  # again
    if not check_auth(request):   # again
        return 401
    try:
        # actual logic
    except Exception:
        return 500                # again
# products.py
def handle(request):
    log(request)                  # again
    if not check_auth(request):   # again
        return 401
    try:
        # actual logic
    except Exception:
        return 500                # again

Duplicated code everywhere. If tomorrow you change how authentication works, you touch 10 files. If you add rate limiting, another 10.

The Front Controller pattern solves exactly this.

What Is the Front Controller?

Fowler’s definition:

“A controller that handles all requests for a Web site.”

In practical terms: a single entry point that intercepts all HTTP requests, executes common logic (auth, logging, error handling), and then delegates to the appropriate handler.

This isn’t a new or exotic idea. When you use Django, FastAPI, Laravel, or Spring MVC, you’re already using a Front Controller. What this article does is show you what’s underneath, by implementing it from scratch.

Pattern Structure

The pattern has three pieces:

HTTP Request
      │
      ▼
┌─────────────────────┐
│   Front Controller  │  ← Single entry point
│  - Logging          │    Executes cross-cutting logic
│  - Authentication   │
│  - Error Handling   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│     Dispatcher      │  ← Maps URL → Handler
└──────────┬──────────┘
           │
    ┌──────┴───────┐
    ▼              ▼
[UsersHandler] [ProductsHandler] ...  ← Business logic only

Front Controller: Receives everything. Knows nothing about business, only about infrastructure.

Dispatcher: Knows which handler corresponds to which URL. Nothing more.

Handlers: Only know their own resource. They know nothing about auth or logging.

Python Implementation (No Frameworks)

The example uses only Python’s standard library (http.server, json, logging). No Django, no Flask, no FastAPI. This way the pattern is fully exposed — no magic.

It simulates a store API with three resources: users, products, and orders.

Project Structure

front_controller/
├── main.py
├── front_controller.py
├── dispatcher.py
└── handlers/
    ├── __init__.py
    ├── base_handler.py
    ├── users_handler.py
    ├── products_handler.py
    └── orders_handler.py

1. The Front Controller — the central piece

# front_controller.py
import logging
from http.server import BaseHTTPRequestHandler
from dispatcher import Dispatcher

VALID_TOKEN = "Bearer secret-token-123"

class FrontController(BaseHTTPRequestHandler):
    dispatcher = Dispatcher()

    def do_GET(self):    self._handle_request("GET")
    def do_POST(self):   self._handle_request("POST")
    def do_PUT(self):    self._handle_request("PUT")
    def do_DELETE(self): self._handle_request("DELETE")

    def _handle_request(self, method):
        self._log_request(method)

        if not self._authenticate():
            return

        try:
            self.dispatcher.dispatch(self, method, self.path)
        except Exception as e:
            logging.error(f"Unhandled exception: {e}")
            self._send_json(500, {"error": "Internal Server Error"})

    def _log_request(self, method):
        logging.info(f"[{method}] {self.path}")

    def _authenticate(self):
        token = self.headers.get("Authorization", "")
        if token != VALID_TOKEN:
            self._send_json(401, {"error": "Unauthorized"})
            return False
        return True

    def _send_json(self, status, data):
        import json
        body = json.dumps(data).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format, *args):
        pass  # Silences BaseHTTPRequestHandler's default log

Notice what this file does and what it doesn’t do:

  • ✅ Logs every request
  • ✅ Validates the token
  • ✅ Catches any unhandled exception
  • ✅ Delegates to the dispatcher
  • ❌ Knows nothing about users, products, or orders

2. The Dispatcher — the routing map

# dispatcher.py
from handlers.users_handler import UsersHandler
from handlers.products_handler import ProductsHandler
from handlers.orders_handler import OrdersHandler

class Dispatcher:
    def __init__(self):
        self.routes = {
            "/users":    UsersHandler(),
            "/products": ProductsHandler(),
            "/orders":   OrdersHandler(),
        }

    def dispatch(self, controller, method, path):
        handler = self.routes.get(path)
        if handler:
            handler.handle(controller, method)
        else:
            controller._send_json(404, {"error": f"Path '{path}' not found"})

Simple and direct. A dictionary mapping paths to handlers. If the path doesn’t exist, it responds with 404. The dispatcher knows nothing about HTTP details or business logic.

3. The BaseHandler — shared behavior

# handlers/base_handler.py
import json

class BaseHandler:
    def handle(self, controller, method):
        if method == "GET":
            self.get(controller)
        elif method == "POST":
            self.post(controller)
        else:
            self.send_error(controller, 405, "Method Not Allowed")

    def get(self, controller):
        self.send_error(controller, 405, "Method Not Allowed")

    def post(self, controller):
        self.send_error(controller, 405, "Method Not Allowed")

    def send_json(self, controller, status, data):
        body = json.dumps(data).encode()
        controller.send_response(status)
        controller.send_header("Content-Type", "application/json")
        controller.end_headers()
        controller.wfile.write(body)

    def send_error(self, controller, status, message):
        self.send_json(controller, status, {"error": message})

4. The Handlers — business logic only

# handlers/users_handler.py
from handlers.base_handler import BaseHandler

class UsersHandler(BaseHandler):
    def get(self, controller):
        self.send_json(controller, 200, {
            "users": [
                {"id": 1, "name": "Alice"},
                {"id": 2, "name": "Bob"},
                {"id": 3, "name": "Charlie"},
            ]
        })

    def post(self, controller):
        self.send_json(controller, 201, {
            "message": "User created successfully",
            "resource": "/users"
        })
# handlers/products_handler.py
from handlers.base_handler import BaseHandler

class ProductsHandler(BaseHandler):
    def get(self, controller):
        self.send_json(controller, 200, {
            "products": [
                {"id": 1, "name": "Laptop",   "price": 999},
                {"id": 2, "name": "Mouse",    "price": 29},
                {"id": 3, "name": "Keyboard", "price": 79},
            ]
        })

    def post(self, controller):
        self.send_json(controller, 201, {
            "message": "Product created successfully",
            "resource": "/products"
        })
# handlers/orders_handler.py
from handlers.base_handler import BaseHandler

class OrdersHandler(BaseHandler):
    def get(self, controller):
        self.send_json(controller, 200, {
            "orders": [
                {"id": 1, "user_id": 1, "total": 1028},
                {"id": 2, "user_id": 2, "total": 79},
                {"id": 3, "user_id": 3, "total": 29},
            ]
        })

    def post(self, controller):
        self.send_json(controller, 201, {
            "message": "Order created successfully",
            "resource": "/orders"
        })

5. The Entry Point

# main.py
import logging
from http.server import HTTPServer
from front_controller import FrontController

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)

if __name__ == "__main__":
    server = HTTPServer(("localhost", 8000), FrontController)
    logging.info("Server running on http://localhost:8000")
    server.serve_forever()

How to Test It

Start the server:

python main.py

Successful GET — list of users:

curl -i -H "Authorization: Bearer secret-token-123" http://localhost:8000/users

Successful POST — create a product:

curl -i -X POST -H "Authorization: Bearer secret-token-123" http://localhost:8000/products

No token — 401 Unauthorized:

curl -i http://localhost:8000/users

Non-existent route — 404 Not Found:

curl -i -H "Authorization: Bearer secret-token-123" http://localhost:8000/invalid-path

Unsupported method — 405 Method Not Allowed:

curl -i -X PUT -H "Authorization: Bearer secret-token-123" http://localhost:8000/users

The Full Flow Visualized

curl GET /users  (with valid token)
         │
         ▼
  FrontController.do_GET()
         │
         ├── _log_request()     → INFO [GET] /users
         ├── _authenticate()    → token OK ✓
         └── dispatcher.dispatch()
                    │
                    ▼
             routes["/users"] → UsersHandler
                    │
                    ▼
             UsersHandler.get()
                    │
                    ▼
         HTTP 200 {"users": [...]}


curl GET /users  (no token)
         │
         ▼
  FrontController.do_GET()
         │
         ├── _log_request()     → INFO [GET] /users
         └── _authenticate()    → invalid token ✗
                    │
                    ▼
         HTTP 401 {"error": "Unauthorized"}
         (never reaches the dispatcher)

What This Teaches You About Frameworks

When you use FastAPI:

app = FastAPI()

@app.middleware("http")
async def log_requests(request, call_next):
    # This is the Front Controller
    ...

@app.get("/users")
def get_users():
    # This is the Handler
    ...

The FastAPI app is the Front Controller. The middleware is the cross-cutting logic. The @app.get() decorators register handlers in the dispatcher. Now you know exactly what’s going on under the hood.

When to Use It (and When Not To)

Use it when:

  • You have cross-cutting logic (auth, logging, rate limiting) that applies to all routes
  • You want a single place to change global behavior
  • You’re building a framework, an API Gateway, or a custom router

Don’t force it when:

  • Your app has 2 or 3 endpoints and won’t grow
  • You’re already using a framework that implements it (use it, don’t reinvent it)

Relationship With Other Fowler Patterns

Pattern Relationship
Page Controller The opposite: each page handles its own requests. Simpler, but doesn’t scale.
Application Controller A natural complement: handles screen navigation and application flow, not HTTP.
Dispatcher View / Service to Worker Variants of the same concept with different distribution of responsibilities.

Conclusion

Front Controller is one of those patterns that, once you understand it, you start seeing everywhere. Spring’s DispatcherServlet, Flask’s app, Express’s Router — they’re all Front Controllers.

The value isn’t in implementing it from scratch in production (that’s what frameworks are for). The value is understanding the why behind its design: centralize the cross-cutting concerns, free the business logic.

When your authentication code lives in one single place, when adding new middleware means changing one line, when your handlers are so clean they only speak business — that’s when the pattern is doing its job.

Full code available on GitHub: https://github.com/KrCrimson/front_controller.git

Beyong New and Delete: from auto_ptr to unique_ptr

This guide walks through a legacy pattern built around std::auto_ptr, something still found in many older C++ codebases. The pattern usually involves a helper like cdup that deep‑copies an object and returns it as an auto_ptr, and classes that store these copies and access them through get() checks and raw pointer calls. It worked in its time, but it no longer fits modern C++ and creates unnecessary ownership confusion.

Here, we break down how the pattern works, why it causes problems, and how to refactor it step by step using std::unique_ptr. The goal is not just to replace one smart pointer with another, but to make ownership explicit, improve readability, and align the code with modern C++ practices.

The Legacy Pattern

A typical example looks like this: a cdup utility that deep‑copies an object into an auto_ptr, and a class that stores those copies and uses get() for access and null checks.

namespace sys_Util {
    template <typename T>
    inline std::auto_ptr<T> cdup(T* t) {
        return t ? std::auto_ptr<T>(new T(*t))
                 : std::auto_ptr<T>(0);
    }
}

class WidgetOwner {
    std::auto_ptr<Widget> m_widget;
public:
    explicit WidgetOwner(Widget* w)
        : m_widget(sys_Util::cdup(w)) {}

    void process() {
        if (m_widget.get()) {
            m_widget.get()->doWork();
        }
    }

    Widget* getWidget() { return m_widget.get(); }
};

At first glance, this seems harmless. But once you look closer, the cracks show.

Evaluation of the Pattern

The pattern deep‑copies the object, wraps it in auto_ptr, and uses defensive get() checks before access.

The Problems of the Pattern

  1. auto_ptr is deprecated and removed

    It was deprecated in C++11 and removed entirely in C++17, so the code simply won’t compile on modern standards.

  2. Broken copy semantics

    Copying an auto_ptr silently transfers ownership, leaving the source null. This is one of the most dangerous behaviors in old C++ codebases.

std::auto_ptr<Widget> a(new Widget);
std::auto_ptr<Widget> b = a;  // a becomes null! (silent move)
  1. Confusing ownership
    Passing a raw pointer to the constructor looks like ownership transfer, but the class actually makes a deep copy. The caller must still delete the original pointer, which is easy to forget.
WidgetOwner owner(raw);  // Does it take ownership? No, it copies.
delete raw;              // Must remember to delete (if not leaked)
  1. Clunky access syntax
    m_widget.get()->method() is verbose and error‑prone compared to modern pointer semantics.
m_widget.get()->doWork();     // Too verbose
(*m_widget.get()).property;   // Ugly
  1. Awkward null checks

    Every access requires if (ptr.get()), which is harder to read than necessary.

  2. Raw pointer exposure

    Returning T* invites accidental deletion or dangling references.

Widget* getWidget() { return m_widget.get(); }
// Caller might delete it, or hold it past owner lifetime
  1. Exception safety issues
    new T(*t) can leak if the copy constructor throws an exception.
new T(*t)  // If T(const T&) throws, memory leaks

Thread Safety of the Pattern

No synchronization, no atomicity — not safe for concurrent access. Copy operation on auto_ptr are not atomic.

Memory Safety of the Pattern

The pattern itself doesn’t leak, but raw pointer containers or circular references with auto_ptr can cause trouble.

Refactoring: Moving to unique_ptr

We keep the original behavior: the pointer may be null, and deep copies must still work. But we want clearer ownership and modern syntax.

Step 1: Replace auto_ptr with unique_ptr

std::unique_ptr<Widget> m_widget;

This immediately removes silent moves and clarifies ownership.

Step 2: Remove the cdup Utility

Instead of a custom deep‑copy helper, use std::make_unique directly:

auto copy = original
    ? std::make_unique<Widget>(*original)
    : nullptr;

This eliminates an entire utility and makes intent clearer .

Step 3: Fix the Constructor

The old constructor took a raw pointer and deep‑copied it, which was ambiguous.

We replace it with two explicit options: copy or move (take ownership).

explicit WidgetOwner(const Widget& w)
    : m_widget(std::make_unique<Widget>(w)) {}

explicit WidgetOwner(std::unique_ptr<Widget> w)
    : m_widget(std::move(w)) {}

Null handling becomes explicit and predictable.

Step 4: Add Natural Access Operators

To avoid get() everywhere, we add operator->, operator*, and operator bool.

Widget* operator->() { return m_widget.get(); }
Widget& operator*()  { return *m_widget; }
explicit operator bool() const { return m_widget != nullptr; }

This makes usage clean and modern:

if (m_widget) {
    m_widget->doWork();
}

Step 5: Improve Null Check Syntax

Instead of:

if (m_widget.get())

We now simply write:

if (m_widget)

Or use a guard clause:

if (!m_widget) return;

Step 6: Fix Raw Pointer Exposure

Returning raw pointers is risky. We replace it with safer alternatives:

Widget& getWidget() {
    if (!m_widget) throw std::runtime_error("Widget is null");
    return *m_widget;
}

Or, if needed:

Widget* getWidget() { return m_widget.get(); }  // Document: can be null

Complete Refactored Example

Before: Legacy Code

// sys_util.h
namespace sys_Util {
    template<typename T>
    inline std::auto_ptr<T> cdup(T* t) {
        return t ? std::auto_ptr<T>(new T(*t)) : std::auto_ptr<T>(0);
    }
}

// database_handler.h
class DatabaseHandler {
    std::auto_ptr<Database> m_db;
    std::auto_ptr<Logger> m_logger;

public:
    DatabaseHandler(Database* db, Logger* logger) 
        : m_db(sys_Util::cdup(db))
        , m_logger(sys_Util::cdup(logger)) {}

    void query(const std::string& sql) {
        if (m_db.get()) {
            m_db.get()->execute(sql);
        }
        if (m_logger.get()) {
            m_logger.get()->log(sql);
        }
    }

    Database* getDatabase() { return m_db.get(); }
    Logger* getLogger() { return m_logger.get(); }
};

// main.cpp
Database* db = new Database("localhost:5432");
Logger* logger = new Logger("app.log");
DatabaseHandler handler(db, logger);
handler.query("SELECT * FROM users");
delete db;      // Confusing - does handler own this?
delete logger;  // Did we just double delete?

After: Modern Code

// sys_util.h - DELETED (no longer needed)

// database_handler.h
class DatabaseHandler {
    std::unique_ptr<Database> m_db;
    std::unique_ptr<Logger> m_logger;

public:
    // Clear: we make copies
    explicit DatabaseHandler(const Database& db, const Logger& logger) 
        : m_db(std::make_unique<Database>(db))
        , m_logger(std::make_unique<Logger>(logger)) {}

    // Clear: we take ownership (handles null case)
    explicit DatabaseHandler(std::unique_ptr<Database> db, std::unique_ptr<Logger> logger) 
        : m_db(std::move(db))
        , m_logger(std::move(logger)) {}

    void query(const std::string& sql) {
        if (m_db) {          // Clean bool check
            m_db->execute(sql);  // Natural syntax
        }
        if (m_logger) {
            m_logger->log(sql);
        }
    }

    // Safe access with reference
    Database& getDatabase() {
        if (!m_db) throw std::runtime_error("Database not initialized");
        return *m_db;
    }

    // Or optional access
    Database* getDatabaseOrNull() { return m_db.get(); }

    // Natural operators for direct use
    explicit operator bool() const { return m_db && m_logger; }
    Database* operator->() { return m_db.get(); }
};

// main.cpp - clear ownership
Database db("localhost:5432");     // Stack object
Logger logger("app.log");           // Stack object
DatabaseHandler handler(db, logger); // Clear: we copy
handler.query("SELECT * FROM users");
// No confusion - everything automatically cleaned up

// Handling null case
std::unique_ptr<Database> maybe_db = getDatabaseOrNull();  // Could be null
std::unique_ptr<Logger> maybe_logger = getLoggerOrNull();
DatabaseHandler nullable_handler(std::move(maybe_db), std::move(maybe_logger));
if (nullable_handler) {
    nullable_handler.query("SELECT * FROM users");
}

Migration Summary Table

A concise comparison of old vs. new patterns can be found here.
| Aspect | Before (auto_ptr) | After (unique_ptr) |
|——–|——————–|———————|
| Smart pointer | std::auto_ptr<T> | std::unique_ptr<T> |
| Copy utility | sys_Util::cdup(t) | std::make_unique<T>(*t) |
| Null check | if (ptr.get()) | if (ptr) |
| Member access | ptr.get()->method() | ptr->method() |
| Dereference | *ptr.get() | *ptr |
| Constructor | explicit A(T* t) | explicit A(const T& t) or explicit A(std::unique_ptr<T> t) |
| Exposure | T* get() (dangerous) | T& get() (safe) or T* get() (documented) |
| Bool check | Manual get() | explicit operator bool() |

Migration Checklist

A practical step‑by‑step list covering mechanical replacement, operator additions, interface cleanup, and correctness verification.

Phase 1: Mechanical Replacement

  • [ ] Replace std::auto_ptr<T> with std::unique_ptr<T>
  • [ ] Replace cdup(ptr) with ptr ? std::make_unique<T>(*ptr) : nullptr
  • [ ] Change raw pointer constructor parameters to const T& or std::unique_ptr<T>
  • [ ] Delete the cdup utility function

Phase 2: Add Convenience Operators

  • [ ] Add operator->() and operator*()
  • [ ] Add explicit operator bool() for null checks
  • [ ] Keep get() temporarily for compatibility

Phase 3: Clean Up Access Patterns

  • [ ] Replace ptr.get()->method() with ptr->method()
  • [ ] Replace if (ptr.get()) with if (ptr)
  • [ ] Replace *ptr.get() with *ptr

Phase 4: Improve Interfaces

  • [ ] Change T* get() to T& get() with null check (if never null)
  • [ ] Or keep T* get() but document null possibility
  • [ ] Consider encapsulation instead of exposing pointers

Phase 5: Verify Correctness

  • [ ] Ensure all null handling preserved
  • [ ] Run tests to verify behavior unchanged
  • [ ] Compile with C++17 or later to confirm no auto_ptr usage

Common Pitfalls and Solutions

Covers the three main issues: missing null checks, losing null checks during migration, and constructors that can’t represent null.

Pitfall 1: Forgetting Null Checks

// Wrong - if m_widget is null, this crashes
m_widget->doWork();

// Right - check first
if (m_widget) m_widget->doWork();

Pitfall 2: Losing the Null Check During Migration

// Before - had check
if (m_widget.get()) { ... }

// After - still need check, just cleaner
if (m_widget) { ... }  // Not removed!

Pitfall 3: Not Handling Null in Constructor

// Wrong - assumes non-null
explicit WidgetOwner(const Widget& w) : m_widget(std::make_unique<Widget>(w)) {}
// Can't represent null owner

// Right - handle null case
explicit WidgetOwner(std::unique_ptr<Widget> w) : m_widget(std::move(w)) {}
WidgetOwner null_owner(nullptr);  // Works

Conclusion

The migration from auto_ptr to unique_ptr is more than a mechanical replacement—it’s an opportunity to:

  1. Clarify ownership – unique_ptr makes exclusive ownership explicit
  2. Improve syntax – Operators provide natural pointer semantics
  3. Enhance safety – No silent moves, clear null handling
  4. Modernize code – Comply with C++11/14/17 standards

The pattern preserves the original behavior (deep copy with optional null) while making the code cleaner, safer, and more maintainable. Every auto_ptr replaced is a step toward modern C++.

Consolidating Power Automate Flows to a Single Connection Reference with PAC CLI

A friendly, end-to-end walkthrough for Power Platform admins and developers

If you’ve inherited (or grown) a Power Platform solution where every flow has its own connection reference for the same connector, you already know the pain: every new environment means re-mapping a dozen references, every connection rotation cascades into a dozen failures, and “who owns what” becomes a Tuesday-morning archaeology project.

The cleaner pattern is to consolidate to a single connection reference per connector and have every flow in the solution point at it. That way, when you swap the underlying connection, you swap it once.

This post walks through the full loop using PAC CLI: installing the tooling in VS Code, authenticating to your dev environment, cloning the solution, editing the flow JSON to point at one shared connection reference, and importing the result back without your flows turning themselves off in the process. There’s a small but important warning at the end about who owns what during import — please don’t skip it.

Before and after: five flows each with their own connection reference, vs. five flows pointing at a single shared connection reference

What you’ll need

  • Visual Studio Code installed
  • Maker access to a development environment with the solution you want to refactor
  • (Optional but recommended) A service principal you can use as the importer — covered in the appendix

Step 1 — Install PAC CLI in VS Code

The fastest way to get PAC CLI working is to install the Power Platform Tools extension. It bundles the CLI and wires it up to your integrated terminal automatically.

  1. Open VS Code.
  2. Open the Extensions view (Ctrl+Shift+X).
  3. Search for Power Platform Tools (publisher: microsoft-IsvExpTools).
  4. Click Install.
  5. Reload VS Code if prompted.

Power Platform Tools extension card in the VS Code Extensions view, with the Install button highlighted

Open a new PowerShell terminal in VS Code (Ctrl+`) and confirm everything’s wired up:

pac

You should see the PAC CLI banner with a version number:

Microsoft PowerPlatform CLI
Version: 1.x.x

If pac isn’t recognized in a plain Windows Command Prompt or system PowerShell, that’s expected — by default the extension only enables PAC inside the VS Code integrated terminal. If you want it everywhere, install the .NET Tool version or the Windows MSI alongside it. For this walkthrough, the VS Code terminal is perfectly fine.

Step 2 — Authenticate to your dev environment

PAC CLI uses authentication profiles. Each profile is a stored connection to a specific tenant and, optionally, a specific environment. Most teams end up with a profile per environment per customer.

Create a profile pointed at your dev environment:

pac auth create --name "Contoso-Dev" --environment "https://contoso-dev.crm.dynamics.com"

A browser window opens; sign in with your maker account. You can substitute the environment URL for an environment ID, a unique name, or even a partial name — PAC will resolve it.

Confirm the profile is active:

pac auth list

Output looks something like:

Index Active Kind      Name         Friendly Name   Url                                       User
[1]   *      UNIVERSAL Contoso-Dev  Contoso Dev     https://contoso-dev.crm.dynamics.com/     [email protected]

If you already have multiple profiles and need to switch, use pac auth select --index <n>. A quick pac auth who will tell you exactly which environment is currently in play — run it before any destructive command, just to be safe.

Terminal output of pac auth list showing one active profile pointed at the dev environment

Tip — find your environment ID. Open the Power Platform admin center, pick your environment, and copy the Environment ID from the Details pane.

Step 3 — Clone the solution

pac solution clone is the right tool for this job. It pulls the solution down as a buildable project (with a cdsproj) and unpacks it into source files you can read, diff, and edit. Use clone (rather than export) when you want to change the contents of the solution, not just deploy it.

From the folder where you want the project to live:

pac solution clone --name ContosoIntegrations

--name is the solution unique name, not the display name. Find it under Solutions in the maker portal — it’s the column to the right of the display name.

After it runs, you’ll see a folder structure like this (the XML format that clone emits by default):

ContosoIntegrations/
├── ContosoIntegrations.cdsproj
└── src/
    ├── Other/
    │   ├── Customizations.xml      ← connection references live in here
    │   ├── Solution.xml
    │   └── Relationships.xml
    └── Workflows/
        ├── FlowA-1A2B3C4D....json  ← flow definition (this is what you edit)
        ├── FlowA-1A2B3C4D....xml
        ├── FlowB-5E6F7G8H....json
        └── FlowB-5E6F7G8H....xml

VS Code Explorer showing the cloned solution structure, with src/Other/Customizations.xml and src/Workflows highlighted

The two places that matter for this refactor are src/Other/Customizations.xml (where every connection reference component is defined, all in one file) and src/Workflows/ (one JSON file per flow, named <FlowName>-<GUID>.json, with a matching .xml workflow descriptor).

A note on formats. If your team uses native Dataverse Git Integration — or a newer pac CLI configured to emit the YAML source-control format — you’ll see a different layout (solutions/<name>/solution.yml, separate connectionreferences/ and workflows/ folders at the repo root, etc.). The mechanics in this post are the same; you’d just edit connectionreferences/*.yml and workflows/<flow>/<flow>.json instead of Customizations.xml and Workflows/<flow>.json. For the rest of this walkthrough I’ll assume the default XML layout.

Step 4 — Update the flow JSON to use one connection reference

This is the core of the change. Today, each flow has its own connection reference logical name baked into its definition. We’re going to:

  1. Pick (or create) the single connection reference you want all flows to share.
  2. Edit each flow’s definition JSON so its connectionReferences block points at that shared logical name.

4a. Pick the canonical connection reference

Open src/Other/Customizations.xml and find the <connectionreferences> element. Each connection reference in the solution sits inside it as a <connectionreference> node. Pick the one you want all flows to point at — say the SharePoint one with logical name contoso_sharedsharepoint. It will look something like:

<connectionreferences>
  <connectionreference connectionreferencelogicalname="contoso_sharedsharepoint">
    <connectionreferencedisplayname>Shared SharePoint</connectionreferencedisplayname>
    <connectorid>/providers/Microsoft.PowerApps/apis/shared_sharepointonline</connectorid>
    <description>Single connection reference for all SharePoint actions</description>
  </connectionreference>
  <connectionreference connectionreferencelogicalname="contoso_flowa_sharepoint">
    <connectionreferencedisplayname>Flow A SharePoint</connectionreferencedisplayname>
    <connectorid>/providers/Microsoft.PowerApps/apis/shared_sharepointonline</connectorid>
  </connectionreference>
  <!-- ...the other per-flow connection references you're consolidating away... -->
</connectionreferences>

Note the connectionreferencelogicalname of the one you’re keeping — you’ll wire every flow to that value next.

4b. Update each flow’s JSON

Open one of the flow JSON files at src/Workflows/<FlowName>-<GUID>.json. You’re looking for the connectionReferences node near the top of the definition. Before consolidation it typically looks like this:

{
  "properties": {
    "connectionReferences": {
      "shared_sharepointonline": {
        "runtimeSource": "embedded",
        "connection": {
          "connectionReferenceLogicalName": "contoso_flowa_sharepoint"
        },
        "api": {
          "name": "shared_sharepointonline"
        }
      }
    },
    "definition": {
      "...": "..."
    }
  }
}

Change connectionReferenceLogicalName to your canonical reference:

{
  "properties": {
    "connectionReferences": {
      "shared_sharepointonline": {
        "runtimeSource": "embedded",
        "connection": {
          "connectionReferenceLogicalName": "contoso_sharedsharepoint"
        },
        "api": {
          "name": "shared_sharepointonline"
        }
      }
    },
    "definition": {
      "...": "..."
    }
  }
}

Repeat for every flow JSON in src/Workflows/. If you have a lot of flows, a careful find-and-replace across that folder works well — just make sure the key under connectionReferences (e.g., shared_sharepointonline) matches the connector you’re consolidating, and that you only replace the old logical names in connectionReferenceLogicalName, not the API name in the api.name field.

4c. Clean up the orphaned connection references

Once nothing in any flow JSON points at the per-flow references anymore, remove their <connectionreference> nodes from src/Other/Customizations.xml. While you’re in there, also remove their entries from the <RootComponents> block in src/Other/Solution.xml (look for type="29" entries with the schema names of the references you’re dropping — type 29 is connection references). Keeping only the canonical reference in both files is the whole point of the exercise.

Side-by-side diff showing the old per-flow connectionReferenceLogicalName replaced with the shared one

Step 5 — Pack and import the solution back

You have two reasonable patterns here. Both work; pick the one that matches how mature your ALM is.

Pattern A — Import the updated solution and let the maker portal prompt for connections

From inside the cloned project folder, build and import in one shot:

pac solution import --publish-changes

(With no --path, PAC builds the current cdsproj and imports the resulting .zip.) When the importing user has connections that match the connector(s) in your remaining connection reference, the import wizard auto-fills them. Otherwise you’ll be prompted to pick or create one.

Pattern B — Import with a deployment settings file (the ALM-friendly way)

This is the path you want for CI/CD or anything reproducible. Generate a deployment settings JSON from the built solution zip:

pac solution create-settings `
  --solution-zip .binDebugContosoIntegrations.zip `
  --settings-file .settingsdev.settings.json

You’ll get a JSON file like this, with one entry per connection reference and one per environment variable:

{
  "EnvironmentVariables": [],
  "ConnectionReferences": [
    {
      "LogicalName": "contoso_sharedsharepoint",
      "ConnectionId": "",
      "ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
    }
  ]
}

Grab the ConnectionId from the target environment — easiest path is make.powerapps.com → Connections, click the connection, and copy the GUID from the URL (the segment after /connections/<apiName>/). Paste it into the JSON:

{
  "EnvironmentVariables": [],
  "ConnectionReferences": [
    {
      "LogicalName": "contoso_sharedsharepoint",
      "ConnectionId": "9f66d1d455f3474ebf24e4fa2c04cea2",
      "ConnectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline"
    }
  ]
}

Then import using the settings file:

pac solution import `
  --path .binDebugContosoIntegrations.zip `
  --settings-file .settingsdev.settings.json `
  --publish-changes `
  --force-overwrite

Because the connection ID is wired in up front, the import doesn’t have to ask you anything interactively. This is the same shape of file Power Platform Build Tools expects in Azure DevOps pipelines, so you can re-use it.

Successful pac solution import using a deployment settings file, with all flows enabled

⚠️ Critical: who owns what when you import

Two side-by-side outcomes: the bad scenario where the importer has no access to the connection so every flow is imported off, and the good scenario where a service principal with shared connection access imports cleanly with all flows on

This is the part everyone learns the hard way exactly once. Skim it, please.

When a solution containing flows is imported, Power Automate validates that the importing user can use every connection referenced by every flow. If they can’t, the affected flows get imported in the off state and have to be turned on manually. With a large solution that you “just” reimported, that can mean every flow in the solution needs to be re-enabled by hand.

Two ways to avoid this:

Option 1 — The importer owns the connection. Whoever runs pac solution import should also be the owner of the connection that the (now single) connection reference points to. If you’re the person doing the import, make sure you created — or have been gifted ownership of — the connection in the target environment.

Option 2 — Import as a service principal that has the connection shared with it. This is the right answer for anything beyond a single-developer dev environment. The SPN owns the flows after import, doesn’t churn when humans leave or change roles, and pipelines can use it without storing user credentials. Share the connection with the SPN as Can use before you import. Setup is in the appendix.

Why “all flows turn off” happens. It’s the same ConnectionAuthorizationFailed failure mode you’d hit turning a flow on manually — Power Automate refuses to enable a flow when the activating identity doesn’t have rights to every connection it touches. The solution import is just the same check applied to N flows at once.

Troubleshooting

“pac is not recognized.”
You’re probably in a terminal outside VS Code. Either run the command in the VS Code integrated terminal (where the extension wires up the PATH) or install the .NET Tool so pac is available everywhere.

pac solution clone fails with “Solution not found”.
You’re using the display name. Switch to the unique name from the Solutions list. Also confirm pac auth who is pointed at the correct environment.

Import succeeds but the flows are turned off.
Whoever ran the import doesn’t have access to the connection. See the warning above. Either share the connection with that identity (as Can use) and turn the flows on, or re-run the import as a user/SPN that does have access. Once enabled by the connection owner, co-owners can toggle the flow on and off as needed.

“ConnectionAuthorizationFailed” when activating a flow.
Same root cause — the activating user is missing access to at least one connection. Either share all connections used by the flow with that user, or have the connection owner be the one to enable the flow.

The deployment settings file has empty ConnectionId values.
That’s normal — pac solution create-settings generates the file with empty slots. You fill them in per-environment. Keep one settings file per environment (dev.settings.json, test.settings.json, prod.settings.json) and check them into source control.

Pack fails after editing JSON.
Make sure the JSON is still valid (a stray comma is the usual culprit) and that you didn’t change any GUIDs, only the connectionReferenceLogicalName values. Run pac solution pack against the cloned folder before importing if you want to validate without round-tripping.

Flow shows “Invalid connection” after import.
The connection reference is pointing at a connection that’s in a bad state — usually expired credentials. Go to Connections in the maker portal, edit/refresh the underlying connection, and the reference will pick it back up.

Appendix — Setting up a service principal for imports

If you’re moving to pipeline-driven deployments, do this once and you’ll thank yourself.

A1. Register the application in Microsoft Entra

  1. Open the Microsoft Entra admin center.
  2. Go to Identity → Applications → App registrations → New registration.
  3. Give it a name like pp-deployer-spn. Single tenant is fine.
  4. After registration, note the Application (client) ID and Directory (tenant) ID.
  5. Under Certificates & secrets, create a new Client secret. Copy the value immediately — you can’t view it again.

Entra app registration overview with the Application (client) ID and Directory (tenant) ID called out for copying

A2. Create the application user in Dataverse

  1. Open the Power Platform admin center.
  2. Pick your target environment → Settings → Users + permissions → Application users.
  3. Click New app user, find the registration from A1, pick the business unit, and assign a security role with enough permissions for what your pipeline does (often System Customizer plus a custom “deployer” role).

A3. Authenticate PAC CLI as the SPN

pac auth create `
  --name "Contoso-Dev-SPN" `
  --environment "https://contoso-dev.crm.dynamics.com" `
  --applicationId "<APP-CLIENT-ID>" `
  --clientSecret "<CLIENT-SECRET>" `
  --tenant "<TENANT-ID>"

A4. Share the connection with the SPN

Back in the maker portal:

  1. Connections → click your shared connection (e.g., the SharePoint one).
  2. Share → search for the application user you created.
  3. Set permission to Can use → Save.

Repeat for every connection your single connection reference is going to be wired to.

Note. OAuth connections can only be explicitly shared with a user representing a service principal — not with security groups. If you have multiple SPNs (one per environment), share with each one explicitly.

A5. Import as the SPN

pac auth select to make the SPN profile active, then run your pac solution import as in Step 5. Because the SPN owns (or has been granted access to) the connection, the flows come in enabled and stay enabled across redeployments — no more “every flow is off” Monday mornings.

Wrapping up

A single shared connection reference is one of those changes that feels like a small refactor and pays you back every single deployment thereafter. The mechanical part — clone, edit JSON, import — is straightforward once you’ve done it once. The part that bites people is ownership: make sure the identity doing the import has access to the underlying connection, or you’ll spend the next hour turning flows back on by hand.

If you want to take the next step, wire the deployment settings JSON and the SPN into an Azure DevOps pipeline using the Power Platform Build Tools. At that point you’ve gone from “manual refactor” to “every environment is one click.”

Sources

Creating a super-efficient Valid Palindrome algorithm in C#

First, do you know what a Palindrome is? No? So, according to the first Google result: A palindrome is a word, phrase, number, or sequence that reads the same backward as forward. The term originates from the Greek word “palindromos”, meaning “running back again”.

Secondly, are you able to verify whether a string is a valid palindrome in an algorithm created with C#? This is the problem we’re going to solve today🫡.

The problem: Given a string s, return true if it is a palindrome, or false otherwise.

Example: Input: s = “Step on no pets” | Output: true
Explanation: “steponnopets” is a palindrome.

💡 Analyzing the problem

To determine if a string is a palindrome, the main idea is to check if it reads the same forward and backward.

However, real-world examples like “Step on no pets” usually have a catch: case sensitivity (capital ‘S’ vs lowercase ‘s’) and non-alphanumeric characters: spaces, punctuation, or symbols, etc… To solve this effectively, we can follow a two-step process:

  1. Clean the string
  2. Check for symmetry

The Step-by-Step Strategy

Step 1: Clean the Input

Before doing any comparisons, we need to normalize the string so that formatting doesn’t get in the way.

  1. Convert everything to lowercase so ‘S’ and ‘s’ match.

  2. Remove all non-alphanumeric characters (spaces, commas, periods, etc.). For your example, Original: “Step on no pets” –> Cleaned: “steponnopets”

Step 2: The Two-Pointer Approach

The most efficient way to check for a palindrome is to use two pointers (or indices): one starting at the very beginning (left) of the cleaned string, and one at the very end (right).

  1. Compare the character at the left pointer with the character at the right pointer.

  2. If they match, move the left pointer one step right, and the right pointer one step left.

  3. If they don’t match, you can stop immediately and return false, definitively it’s not a palindrome.

  4. Repeat this until the pointers meet or cross in the middle. If you make it to the middle without any mismatches, return true.

🛠️Hands on

In C# we can do the following:

public class Solution {
    public bool IsPalindrome(string s) {
        int left = 0;
        int right = s.Length - 1;

        while (left < right) {
            if (!char.IsLetterOrDigit(s[left])) {
                left++;
            }
            else if (!char.IsLetterOrDigit(s[right])) {
                right--;
            }
            else {
                if (char.ToLowerInvariant(s[left]) != char.ToLowerInvariant(s[right])) {
                    return false; 
                }
                left++;
                right--;
            }
        }

        return true;
    }
}

The solution: The Two-Pointer Approach

Instead of creating a brand new “cleaned” string (which takes up extra memory), this solution moves the pointers across the original string and simply skips over any spaces or punctuation on the fly. This optimizes our space complexity to O(1).

  1. First, we Initialize pointers at the beginning and end of the string. Then, while left is lass than right:

  2. Verify if left index is letter or digit, if it’s affirmative, move the left pointer right (left++).

  3. Verify if right index is letter or digit, if it’s affirmative, move the right pointer left (right++).

  4. If both are alphanumeric, compare them case-insensitively. If the left index is different from the right index, so, an incompatibility was found. It returns false.

  5. Left is incremented and right decremented.

  6. Finally, if all valid pairs matched, return true (valid palindrome)

1. ⏳ Time Complexity: O(n)

What it means: The time it takes to run the code grows linearly with the length of the string (n).

Why: In the worst-case scenario (when the string is a palindrome), the left and right pointers start at the edges and meet exactly in the middle. This means every character in the string is inspected exactly once.

Even though we skip non-alphanumeric characters, each character is still visited only a single time by one of the pointers.

2. 📦 Space Complexity: O(1)

What it means: The memory used by the algorithm is constant, meaning it stays the same no matter how massive the input string gets.

Why: Unlike the LINQ approach (or solutions in other languages that create a brand-new, filtered version of the string), this method processes the original string in place.

The only extra memory allocated during execution is for a couple of primitive integer variables (left and right) to track the array indices.

🌱 Wrap up

I hope it helps you understand more about Data Structure and how to solve the Valid Palindrome problem with an elegant and efficient way.

I Got Sick of My Claude Code UI Slop, So I Crystallized It into a Go CLI

Every time I asked Claude Code to “build me a landing page,” I got the same page back.

Centered hero. Inter font for everything. Maybe Space Grotesk if it felt artsy. A purple-to-blue gradient on the H1. A small pill badge floating above the headline (“New,” “Beta,” “Now with AI”). Three identical feature cards underneath, each with a Lucide icon on top, usually zap, shield-check, sparkles. A numbered 1-2-3 steps row. Glassmorphism on the nav. shadcn/ui everywhere. Dark mode with grey-on-darker-grey body text that fails contrast checks but “looks clean.”

After about the twentieth one I started seeing it in my sleep. So I stopped complaining and built a tool.

uislop: a Go CLI that scores how AI-generated a page looks

It takes a URL, fetches the HTML and linked JS, runs a pile of regexes against it, and prints a single number between 0 and 100.

$ uislop https://some-ai-startup.ai
  font:Inter            hits=12  weight=2  add=24
  color:purple          hits=8   weight=3  add=24
  color:gradient        hits=14  weight=2  add=28
  css:shadcn            hits=4   weight=4  add=16
  icon:svg_lucide       hits=11  weight=2  add=22
  layout:centered       hits=7   weight=2  add=14
  layout:allCaps        hits=9   weight=1  add=9
  ----
87

0 means “looks human-built or pre-2022.” 100 means “this was vibe-coded last Tuesday.”

Why Go? Same reason I defaulted my new projects to Go. One binary, runs anywhere, no virtualenv, no dependency drift. I can scp it to anything and grep the entire web.

The signals I crystallized

After eyeballing a few hundred AI startup landing pages, the patterns are weirdly consistent.

Fonts

  • Inter on literally everything
  • Space Grotesk when feeling fancy
  • Geist Sans / Geist Mono in the year-of-Vercel
  • Instrument Serif for one italic word in the H1 (“the only ~platform~ you need”)
  • CameraPlainVariable served from cdn.gpteng.co, basically a Lovable fingerprint

Colors

  • VibeCode purple (#8b5cf6, #7c3aed)
  • Gradients on the H1, the CTA, the icon backgrounds, the nav border, everywhere
  • Dark mode with bg-zinc-950
  • Body text at text-zinc-400 over bg-zinc-900, 3.8:1 contrast, fails WCAG, “looks clean”
  • Soft colored glows behind every primary button

Layout

  • Centered hero with max-w-3xl text-center
  • Tiny pill badge above the H1 (rounded-full bg-primary/10)
  • Three feature cards, grid-cols-3 gap-8, each with an icon on top
  • Numbered 01 - Sign up / 02 - Get started / 03 - Win steps
  • All-caps section labels with tracking-wider
  • Stat banner row (“10k+ users,” “99.9% uptime,” “$0 cost”)

The icons

Always Lucide. Always the same five: zap, shield-check, bot, sparkles, rocket. If a page has all five visible above the fold, it is a tell.

The CSS giveaways

  • shadcn primitives in the bundle
  • backdrop-blur-md bg-white/10 on the nav
  • Tailwind utility soup
  • cdn.gpteng.co in the asset URLs

What I learned shipping it

A few things surprised me.

  1. The signal is loud. I expected to need ML. Regex on the HTML and one pass of linked JS gets you to 90%+ accuracy on the obvious cases.
  2. Geist Sans skews the score harder than I thought. Half the false positives are real human-built sites that just happen to use Vercel’s font.
  3. The pattern travels. Once you have the score, you start seeing it everywhere. Half the recent YC batch lands in the 80s.
  4. The cure for being part of the problem is to look at your own page. Mine scored 72. I shipped the tool anyway.

What this is and is not

It is not a tool to shame anyone. Shipping anything is hard. If shadcn + Lucide + purple gradient is what gets you to revenue, ship it.

It is a forcing function. When the default output of every model converges on the same aesthetic, “looks like everyone else” becomes the new “looks unstyled.” The score is just a mirror.

One weird trick to drop your score: swap Inter for a real typeface, pick a color that is not on the violet wheel, and delete one feature card. Three minutes, ten points off.