Decisions, and what each one cost
Short records of the tradeoffs behind the work on this site. Each one states the context, the decision and the consequence, and links to the artifact where it shows up.
| Record | Decision | Artifact |
|---|---|---|
| DR-001 | The spam classifier fails open | Platform |
| DR-002 | Content lives in files, not a database | Platform |
| DR-003 | Rate limits are counted in memory | Platform |
| DR-004 | One source for every outcome number | Platform |
| DR-005 | Generate model output offline and commit it | MOS Translator |
| DR-006 | Salary data falls back in tiers and labels its source | MOS Translator |
| DR-007 | Standard library and keyless data only | Job Kit |
| DR-008 | Live data or stop | Job Kit, Hashflag Skills |
| DR-009 | Scripts compute, the model grades with a quote | Job Kit, Hashflag Skills |
| DR-010 | Two composite scores with declared weights | Job Kit |
| DR-011 | Solutions are gated behind effort | J0DI3 |
| DR-012 | Every AI call is mocked in tests | J0DI3 |
DR-001: The spam classifier fails open
Context. The vetswhocode.io contact form receives inquiries from veterans, employers and donors alongside SEO and link-building pitches. A model can tell them apart. A model can also be unavailable or wrong.
Decision. A message is dropped only when the classifier returns spam with confidence of 0.8 or higher. A missing key, an API error, an empty response or unparseable output all deliver the message.
Consequence. Some spam reaches Slack. No outage or model mistake short of a confident wrong answer can discard a real message. Platform deep dive.
DR-002: Content lives in files, not a database
Context. The site’s content is blog posts, press entries, team and program data. None of it needs per-user state.
Decision. Store content as Markdown, MDX, JSON and TypeScript in the repository.
Consequence. Every content change is reviewed, versioned and reversible, and there is no database to operate. Publishing needs a pull request, and features with per-user state do not fit. The learning management system moved to its own application in September 2026. Platform deep dive.
DR-003: Rate limits are counted in memory
Context. Public form endpoints need abuse protection. The limits are small: 3 applications per 10 minutes, 5 contact messages per minute, per client IP.
Decision. A sliding-window limiter with counters in process memory, with a separate bucket per endpoint.
Consequence. No extra service to run. Counts are exact only on a single instance, which the documentation states. The limiter’s interface is written so a shared store can replace the map without touching call sites. Planned for v2.
DR-004: One source for every outcome number
Context. Placement rate, alumni earnings and veterans trained appear on about ten pages. Numbers that are typed in more than one place drift apart.
Decision. Define each statistic once, with its source and the date it was confirmed. A test pins the values and scans the source tree for stray copies.
Consequence. A number changes in one file or the build fails. Fields that are not confirmed are left unset instead of guessed. Platform deep dive.
DR-005: Generate model output offline and commit it
Context. Career guides for more than four thousand job codes need model-written content. A nonprofit cannot pay for a model call on every page view.
Decision. Run generation as offline scripts, validate each object against a schema, commit the JSON and build static pages from it.
Consequence. Readers get no model latency, there is no per-view cost and a model outage cannot break a page. Content is only as fresh as the last run. MOS Translator deep dive.
DR-006: Salary data falls back in tiers and labels its source
Context. Lightcast salary data needs credentials, and every credential in the project is optional so that contributors can run it without any.
Decision. Try Lightcast, then Census ACS, then a curated table. Every record carries a dataSource field.
Consequence. The pipeline always completes, and a reader can see whether a figure is live or curated. Curated figures can age. MOS Translator deep dive.
DR-007: Standard library and keyless data only
Context. The job kit is used by workshop attendees and veterans on whatever laptop they have. Its method document sets the bar: every learner has to be able to rerun every number for free.
Decision. Python standard library only. Public data sources that need no account: O*NET files, BLS tables and the BLS public API, public job board APIs and the unauthenticated GitHub API.
Consequence. Nothing to install and nothing to pay for. Commercial labor data is out of reach, and the public APIs have small quotas. Job Kit deep dive.
DR-008: Live data or stop
Context. A score against an out-of-date market looks exactly like a current one.
Decision. When a source is unreachable the job kit prints STOPPED: and exits. Hashflag Skills refuses postings older than 14 days and O*NET data older than 30.
Consequence. Users sometimes get no result and have to re-fetch. They never get a stale number presented as current. Job Kit deep dive.
DR-009: Scripts compute, the model grades with a quote
Context. In the first eval round, grading sessions credited résumés with skills they never named, and scores moved by up to 5.1 points between sessions.
Decision. All arithmetic is done by scripts. The model only decides whether a line is evidence for a skill, and it must return the quote. The quote has to name the skill, and scripts reject a quote that is not in the text.
Consequence. By round three the spread was 1.2 points or less in every lane. A person who describes real work loosely scores lower until they name the skill. Hashflag Skills deep dive.
DR-010: Two composite scores with declared weights
Context. An applicant tracking system and a hiring engineer read a candidate differently. One number would hide that.
Decision. Report a machine read (résumé 40, LinkedIn 35, GitHub 15, portfolio 10) and a human read (portfolio 35, GitHub 30, résumé 20, LinkedIn 15), with a résumé gate at 70. Label the weights as a design judgment and make each one a command-line option.
Consequence. The user sees which audience they are weak for. The weights are not research, and the documentation says so. Job Kit deep dive.
DR-011: Solutions are gated behind effort
Context. A learner who is handed the answer does not build the skill an interview will test.
Decision. Hints unlock after a first attempt. A full solution unlocks only after two or more hints or three or more failed attempts. Coaching does not start until an attempt is recorded. The rules live in the endpoints.
Consequence. The assistant is less convenient than a general chatbot, on purpose. The gate holds whatever the model would have said. J0DI3 deep dive.
DR-012: Every AI call is mocked in tests
Context. A test suite that calls a model is slow, costs money and fails for reasons unrelated to the change.
Decision. Mock all model calls. Test the code around the model: gating, budgets, breakers, safety checks and persistence.
Consequence. More than 670 tests run in about eight seconds. Answer quality is not measured by this suite and needs its own eval. Planned for v2.