Engineering Journal

Notes on product engineering, cloud systems, data, and applied AI, including the decisions behind the software we build.

Recent articles

Model routing for instructor briefings

Applied AI Model routing Small models JEV verifier Zero Data Retention .NET

An instructor may review dozens of actions in one course. A short briefing can help them find the useful question in each one, but it has to be precise about time. A plan describes an intention. A turn preview estimates what may happen. A completed turn records what did happen. Mixing those states produces a confident sentence that can mislead a class discussion.

Start with the action's own facts

The instructor digest begins with the same action data shown on the page: the selected issue, cost, saved audience list, its filters, and any linked analysis. For a planned action, it receives the team's objective and expected outcome. Recorded reach and effect enter the briefing only after the action runs. Forecast alternatives carry their own label so an estimate cannot quietly become a result.

One action, two states

Before a turn, the note can say which issue and list a team chose and ask what evidence supports that choice. After the turn, it can discuss recorded reach and ask the team to compare its expectation with the outcome. A forecast of another audience stays a forecast in both cases.

Check the text before an instructor sees it

The generator writes a short verdict and one coaching question in the page language. Deterministic checks reject invented numbers, unsupported comparisons, the wrong language, and claims of results for actions that have not run. They also keep an automated action from being described as a choice the students made.

JEV adds a separate review against labelled facts. It checks for contradictions, claims with no support in those facts, and forecasts phrased as recorded outcomes. If a draft fails, the system tries once more with the failure explained. A localized summary built from the action data is available when generated text still fails the checks or the model call fails.

Choose a model for the job

Turn previews, briefing drafts, and verification have different inputs and costs. Smaller, lower-cost models can handle narrow tasks; work that needs broader reasoning can use a more capable model. The checks apply to the finished text regardless of which model produced it. Those checks remain in place as model selection changes with cost and capability.

Keep the data boundary clear

Model-provider requests run under Zero Data Retention (ZDR) terms. Providers do not retain prompts or responses. The boundary applies to model requests. The simulation still keeps the course records needed to show the team's analysis, plan, action, and result to its instructor. The briefing is a starting point for review, with the underlying work available for the instructor to inspect.

All journal articles

Teaching through a business simulation

Business computing CSV analysis Simulation design Instructor tools Accessibility Arabic RTL Guided video

Spreadsheet skills are easy to practice in isolation. The harder classroom task is using an incomplete dataset to justify a decision, committing to that decision, and explaining the result afterward. MISsimulation gives teams a repeatable setting for that full cycle. Its fictional election campaign supplies the scenario for business computing work with data, budgets, plans, and outcomes.

Begin with imperfect information

Students work with CSV files that cover different parts of the simulated population. A survey offers a sample of issue preferences; registration and household data provide broader records for finding people a team can reach. Students can use a spreadsheet to compare interest by area, filter a group, and join records into a target list. The survey sample leaves gaps, so students must decide what their analysis supports and where they are making an assumption.

A classroom exercise

A team finds strong interest in an issue among surveyed residents in one area. It checks how that group maps to the broader household file, builds a list of addresses, and explains why those homes are worth the cost of an action. The spreadsheet work now has a decision attached to it.

Commit to a plan, then inspect the turn

The team uploads its list, chooses an issue, and submits an action. The product checks the uploaded file and shows the action cost. When strategic intent is enabled, students also record an objective, their targeting strategy, and an expected outcome. These statements give the instructor something concrete to compare with the results after the turn runs.

After the turn, teams can inspect what happened and revise their next move. Instructors can follow linked analysis, saved lists, plans, actions, and outcomes. The coaching view surfaces cases such as an action without supporting analysis or analysis that never informed an action. Those are useful prompts for a discussion about the quality of the reasoning, whatever the final standing.

Make the workflow usable

Guidance appears where students need it: videos are available near data work, action upload, and the turn schedule. Captions and guided audio offer more than one way to follow a lesson. The interface also includes keyboard access, screen-reader support, and right-to-left Arabic layouts.

Students can show the data they used, the list they built, the choice they made, and what they changed after seeing the outcome. That gives instructors a basis for assessing the process as well as the result.

All journal articles
All journal articles

From Uniformity to Diversity: Engineering Richer Procedural Worlds

MISsimulation TypeScript Cloudflare Workers Gaussian Distributions Procedural Generation Data Modeling

MISsimulation is a business computing simulation set in the context of a fictional election campaign. Its scenario generator creates the data teams use to practice analysis and decision-making. The data was valid, but each generated island felt much the same: similar resident groups, political tendencies, and geographic patterns.

The campaign is the setting; the engineering challenge was to make each scenario distinct while keeping its data coherent and reproducible for teaching and comparison.

The Tech Stack and Starting Point

The generator runs on Cloudflare Workers using TypeScript. The core engine uses a seeded random approach with Gaussian distributions for demographic modeling. We output both CSV files and MySQL dumps containing everything from voter registration to social media networks.

The generator already used deterministic seeding, so the same seed produced the same output. It could build populations of more than 5,000 residents in memory within the request budget.

The problem was that we were using the same four demographic cluster templates for every single island. It worked, but it was predictable.

Problem 1: The Apolitical Rate Was Hardcoded

Originally, every island had roughly the same percentage of politically disengaged residents because we calculated it the same way every time. This created unrealistic uniformity across different "worlds" that should have had distinct political cultures.

The fix: We implemented a weighted random apolitical target using a power curve:

const apoliticalTarget = _.clamp(
  0.1 + Math.pow(Math.max(randUnit(880), 0), 1.4) * 0.4,
  0.1,
  0.5
);

This samples a target between 10% and 50%. Raising the random draw to 1.4 favors values toward the lower end while leaving room for higher targets. Because the draw is seeded, a given island keeps the same target when regenerated.

But here's the tricky part: we couldn't just set a global variable and walk away. We needed to recalculate the "quiet majority" cluster's population share to hit that target while keeping all other demographic clusters intact:

const apoliticalOther = otherClusters.reduce((sum, cluster) => {
  return sum + cluster.share * cluster.apoliticalProbability;
}, 0);

const quietProb = Math.max(0.05, quietCluster.apoliticalProbability);
let desiredShare = (this.targetApoliticalRate - apoliticalOther) / quietProb;
desiredShare = _.clamp(desiredShare, 0.05, 0.8);

This dynamically adjusts cluster shares to achieve the target rate. Some islands end up with highly engaged populations, others are dominated by politically indifferent residents.

Problem 2: Template Fatigue

Nine demographic archetypes lived in our codebase, but we only ever used the same four (plus the quiet majority baseline). Every island got the exact same mix.

The technical constraint: we needed deterministic generation for testing. If you generate an island with seed 12345 today, it needs to be identical tomorrow.

The solution: Keep all nine templates but randomly select four based on the island's seed:

const optionalSelection = optionalTemplates
  .map((template, index) => ({ template, score: randUnit(500 + index) }))
  .sort((a, b) => a.score - b.score)
  .slice(0, optionalSlots)
  .map((entry) => entry.template);

Each template gets a seeded random score, we sort by that score, and take the top four. Same seed = same selection every time. Different seed = different cluster composition.

This means one island might combine "young progressives" with "rural conservatives" and "wealthy moderates," while another gets "retirees," "working-class liberals," and "suburban independents." The nine-choose-four selection gives us 126 possible template mixes before the apolitical target and geographic weights vary.

Problem 3: Geographic Distribution Was Too Uniform

Our area assignment code gave every geographic area roughly equal population. Real places don't work that way. City centers concentrate population. Rural areas spread thin. Some suburbs boom while others stagnate.

The original code multiplied cluster affinities by 1.0 for baseline, with minor adjustments. Too conservative.

The fix: We added a seeded area diversity bias with base weights between 0.65 and 2.0:

this.config.areas.forEach((area, index) => {
  let base = 0.65 + localRand(index * 19 + 1) * 1.35;

  // Urban preference adjustment
  if (['Central', 'Downtown', 'City Center'].includes(area)) {
    base *= 0.9 + this.config.urbanRuralSkew * 0.5;
  }

  bias[area] = base;
});

Then during resident assignment, we apply this bias on top of cluster affinities:

if (this.areaDiversityBias) {
  areas.forEach((area, index) => {
    const modifier = this.areaDiversityBias![area];
    if (modifier !== undefined) {
      const jitter = 0.9 + (index % 5) * 0.02;
      areaScores[area] *= modifier * jitter;
    }
  });
}

The same area can now receive a different share of residents under different seeds, while each island remains reproducible.

Bonus: Performance Instrumentation

We also switched from Date.now() to performance.now() for timing metrics:

const getTimestamp: () => number = (() => {
  try {
    if (typeof globalThis !== 'undefined') {
      const perf = (globalThis as any).performance;
      if (perf && typeof perf.now === 'function') {
        return () => perf.now();
      }
    }
  } catch (error) {
    // fallback
  }
  return () => Date.now();
})();

This gives the timing breakdown finer resolution when profiling generation work inside a Worker request.

The Results

Before these changes, the main sources of repetition were:

Now the seed controls three separate choices:

Reusing a seed under the same generator version recreates the same scenario. Changing it can change the resident mix and geographic distribution while preserving the structure of the files students analyze.

Those changes give instructors varied datasets for the same analysis tasks. The reproducible seed lets a team revisit a scenario and lets us diagnose a generation issue without losing the conditions that produced it.