a shippers field note

the dead
basics

you read code all day. this is the grammar underneath it.

you stop and read most of what the agents write, and you ship it, and it works. what you never did was the first-year lecture. this is that lecture, in the order it matters, with your own code as the examples. nine strata, one interview at the bottom.
start at the machine

a script is a text file a program reads top to bottom. everything else is detail.

that sentence is the whole of stratum one. the rest of this note is what the detail looks like in python, javascript and typescript, how the web stack sits on top, what the shell and git actually do, what a container is and why kubernetes exists, and how to say all of it out loud to someone who is deciding whether to hire you.

01
the machine · how code actually runs
scripts, interpreters,
compilers

source code is text. a program has to turn it into instructions the cpu runs. an interpreter does that line by line while it runs (python, javascript). a compiler does it all up front and hands you a binary (go, rust, c). typescript is a third thing: it compiles to javascript, which is then interpreted. the distinction matters for one reason in practice: with an interpreter, a typo on line 200 only explodes when line 200 runs.

every program is a process: it gets memory, three streams (stdin in, stdout out, stderr for complaints), arguments, environment variables, and it ends with an exit code. zero means fine. anything else means not fine. that number is how one program tells another whether it worked, and it is the whole basis of ci pipelines and cron jobs.

interpretercompilerprocessstdin / stdout / stderrexit codeargvenv varsPATHshebang
#!/usr/bin/env python3          ← shebang: "use whichever python3 is on PATH"
import sys, os                  ← modules: code someone else wrote, pulled in by name

name = sys.argv[1] if len(sys.argv) > 1 else "world"   ← argv[0] is the script itself
mode = os.environ.get("MODE", "dev")                      ← env var with a default
print(f"hello {name} ({mode})")                          ← writes to stdout
sys.exit(0 if name != "nobody" else 1)                   ← the exit code

# three ways to run the same file:
#   python3 hello.py jamal        → interpreter reads the file
#   chmod +x hello.py && ./hello.py jamal   → shell reads the shebang, finds python3
#   MODE=prod ./hello.py jamal && echo "ok"  → env var set for one run; echo only fires if exit was 0
where it lives in your stack

build_cvs.py is exactly this. one text file. the top is data (your CV as python lists and tuples), the bottom is two functions that walk that data and draw it, once with reportlab into a pdf, once with python-docx into a word file. python3 build_cvs.py starts the interpreter, it reads top to bottom, hits the last two lines, calls both functions, exits 0. nothing is compiled, nothing is installed into the system. the script is the program.

say it → "python is interpreted, so errors surface at runtime; that is why we lean on tests and types to catch them earlier."
02
the first language · the one that reads like english
python

python's whole design is that indentation is the syntax. a block is whatever is indented under the line that opened it. there are no braces to close. the four things you use constantly are lists (ordered), dicts (key to value), functions (named blocks with inputs and a return), and loops over either. everything is an object, including functions, which is why you can pass them around.

two habits mark someone who actually writes it: comprehensions instead of building lists in loops, and a virtual environment per project so dependencies do not bleed between them (python3 -m venv .venv, then pip install inside it).

list / dict / tuple / setdef · returnfor · incomprehensionNone · truthinessclass · selfimport · venv · pipf-stringstry / except
def in_ring(lon, lat, ring):                    ← a function: name, inputs, one job
    """ray-cast point-in-polygon. crossings odd → inside."""
    inside = False
    j = len(ring) - 1                           ← len() works on any sequence
    for i in range(len(ring)):                  ← loop by index because we need i and j
        xi, yi = ring[i][0], ring[i][1]           ← unpacking: two names from two values
        xj, yj = ring[j][0], ring[j][1]
        if (yi > lat) != (yj > lat) and lon < (xj - xi) * (lat - yi) / (yj - yi) + xi:
            inside = not inside                 ← flip on every edge crossing
        j = i
    return inside

owners = {}                                     ← a dict, filled as we go
for v in vignettes:                             ← vignettes is a list of dicts from json
    near = {f["properties"]["id"]                 ← a set comprehension: build the set in one expression
            for f in feats
            if in_ring(v["lon"], v["lat"], f["geometry"]["coordinates"][0])}
    owners[v["id"]] = near or {nearest(feats, v)}  ← "or": empty set is falsy, so fall back
print(f"{len(owners)} vignettes mapped")
where it lives in your stack

this is the headless check that proved the hover-reveal mapping before it shipped: 12 vignettes, 835 polygons, one dict of sets. ava's backend is 10,700 lines of the same grammar in fastapi and langgraph. when you read those files, this is all that is happening: functions calling functions, dicts moving between them.

say it → "i reach for python for backends, data work and agents; fastapi for apis, pydantic for validated shapes at the boundary."
03
the browser's language · and node's
javascript &
the event loop

javascript runs in two places: the browser (where it drives the page) and node (where it is a normal server language). the grammar is c-shaped, braces and semicolons, const for things that do not get reassigned, let for things that do. functions are values; arrow functions (x) => x * 2 are the short form you see everywhere.

the one idea that separates people who know javascript from people who copy it is the event loop. there is one thread. anything slow (a network call, a timer, a file read) is handed off, and the loop keeps running. when the slow thing finishes, its callback is queued. async / await is sugar over that: await pauses this function only, not the whole program. a promise is just "a value that will exist later".

const / letarrow functionsobjects · arrayspromiseasync / awaitevent loopsetTimeout · rAFimport / exportnpm · package.jsonthe DOM
async function loadVignettes() {                      ← async: this function can await
  const res = await fetch("/data/vignettes.json");      ← hands off to the network, this function pauses
  if (!res.ok) throw new Error(`http ${res.status}`);   ← 404, 500: ok is false, so bail loudly
  return res.json();                                  ← also a promise; caller awaits it
}

const vignettes = await loadVignettes();
const byId = Object.fromEntries(vignettes.map(v => [v.id, v]));   ← array → object in one line

canvas.addEventListener("pointermove", (e) => {        ← the DOM: react to the user
  requestAnimationFrame(() => {                        ← "do this on the next painted frame"
    const hit = pick(e.clientX, e.clientY);            ← raycast; expensive, so once per frame max
    setHover(hit ? hit.id : null);
  });
});
where it lives in your stack

today's bug was the event loop in the wild: hover in peoples of africa sits inside requestAnimationFrame, and a backgrounded tab stops painting frames. so hover never fired while you were on another tab, and my probe hung waiting for a callback that was never going to be queued. that is not a bug in the app. it is the loop doing exactly what it does.

say it → "javascript is single-threaded with an event loop; i/o is non-blocking, so you await it instead of waiting on it."
04
the contract · javascript with shapes
typescript

typescript is javascript plus a description of the shape of every value. an interface is a named shape. a union (string | null) is "one of these". a generic (Map<string, Set<string>>) is a shape with a hole you fill in. the compiler, tsc, checks that shapes line up, then throws the types away and emits plain javascript. types exist at build time only; at runtime there is nothing there.

the payoff is at boundaries: a function that says it takes a VignetteDef cannot be handed a random object without the compiler complaining. the limit is that tsc checks shapes, not meaning.

interface · typestring | nullgenericsnarrowingstricttsc --noEmit.d.tssatisfies · as
export interface VignetteDef {                 ← a named shape
  id: string; lon: number; lat: number;
  needs: "land" | "water";                       ← a union of two literal strings
}

export function vignetteRegionMap(
  peoples: PeopleFeature[],                     ← array of a shape
  vignettes: VignetteDef[],
): Map<string, Set<string>> { /* … */ }         ← generic: map of id → set of ids

// the consumer, before the fix. `owners` is a Set<string>, hoverId is string | null.
const owners = regionOf.get(v.id);
if (owners !== hoverId) return null;            ← a Set is never === a string. always true. nothing ever rendered.

// after. membership, not equality.
const shown = owners != null && hoverId != null && owners.has(hoverId);   ← narrowing: inside this expression hoverId is a string
where it lives in your stack

that exact bug shipped for six minutes this afternoon. tsc was green because comparing a Set to a string is legal; it is just never true. the types told me the shapes were fine. they could not tell me the logic was wrong. that is the honest ceiling of a type system, and it is why the test that caught it was a human reading the diff, not the compiler.

say it → "types are documentation the compiler enforces. they catch shape errors at build time, not logic errors, so i still test behaviour."
05
the wire · request, response, render
http, apis &
the front end

the entire web is one conversation shape: a client sends a request (a verb, a path, headers, maybe a body), a server sends a response (a status code, headers, a body). GET reads, POST creates, PUT/PATCH change, DELETE removes. 2xx worked, 3xx go elsewhere, 4xx you got it wrong, 5xx we got it wrong. json is the body format everyone agreed on.

a front end framework like react is a way of saying "the page is a function of state". a component is a function that returns markup. state is data that, when it changes, makes the component re-render. an effect is "when this changes, go do something outside the render", like fetching. next.js wraps react with a server: some components render on the server, some in the browser, and a route handler is just an api endpoint living in the same repo.

GET / POST / PUT / DELETEstatus codesheaders · cookiesjsonauth · jwt · sessionsCORScomponent · state · effectSSR vs clientroute handler
// server: a next.js route handler. this file IS the endpoint.
export async function GET() {
  const ok = await db.ping();
  return Response.json({ ok, at: Date.now() }, { status: ok ? 200 : 503 });   ← 503: "we are not fine"
}

// browser: a react component. state in, markup out.
function Health() {
  const [status, setStatus] = useState<"…" | "ok" | "down">("…");   ← state: changing it re-renders
  useEffect(() => {                                     ← effect: runs after render, once ([])
    fetch("/api/health").then(r => setStatus(r.ok ? "ok" : "down"));
  }, []);
  return <span data-status={status}>{status}</span>;     ← jsx: markup as a return value
}
where it lives in your stack

every strydeos surface is this. portal.strydeos.com is react rendered by next.js; the mcp at /api/mcp is a route handler; evidence's /api/evidence streams a response body. and the status codes are not academic: the shippers studio site returned 503 usage_exceeded on 5 aug with a perfectly good deploy, because 5xx means the server side is the problem, and that day the server side was netlify's billing.

say it → "http is stateless request/response; auth rides in a header or cookie; the ui is a function of state, and the server owns the truth."
06
the hands · the tools that touch the machine
the shell & git

the shell is a language too. its nouns are files and processes, its verbs are commands, and its grammar is the pipe: | sends one command's stdout into the next one's stdin. && means "only if the last one exited 0". > writes output to a file. that is 80% of what any script you have read is doing.

git is a database of snapshots. a commit is a snapshot plus a message plus a pointer to its parent. a branch is a movable name for a commit. merge joins two lines of history; rebase replays yours on top of theirs; revert adds a new commit that undoes an old one (safe); reset --hard throws work away (not safe). the remote is just another copy; push and pull sync them.

| · && · >grep · sed · awk$? exit codechmod · PATHcron · launchdcommit · branchmerge · rebaserevert vs resetPR · CI
# pipe: list files, keep the ones that look like a CV, show the newest
ls -lt ~/Downloads | grep -iE 'cv|resume' | head -3

# chain: only deploy if typecheck AND build both exited 0
npx tsc --noEmit && npm run build && npx wrangler pages deploy dist

# surgical undo: bring ONE file back from an old commit, keep everything else
git checkout a8a7c09 -- src/ui/Chrome.tsx           ← the attribution rollback
git commit -m "Roll back on-page model attribution"   ← history moves forward, nothing rewritten

# the one you do not run on a tree holding someone else's work
git reset --hard origin/main                          ← uncommitted changes are gone. no undo.
where it lives in your stack

every ship command this month was a && chain, and every "it deployed but the site is broken" was a chain that should have stopped and did not (the pipe-tail lesson: | tail swallows the exit code of the thing before it). the reset --hard rule is in your memory for a reason: it once nearly ate work that was not yours.

say it → "revert for shared history, reset only for my own local branch. ci is just the && chain run by a robot on every push."
07
the box · ship the machine with the code
docker &
kubernetes

"works on my machine" is the oldest bug. a container fixes it by shipping the machine: your code plus its exact runtime and libraries, as an image, run as an isolated process on any host. it is not a virtual machine; it shares the host kernel, which is why it starts in milliseconds. a Dockerfile is the recipe, each line a cached layer. a registry is where images live.

kubernetes is what you need when you have many containers and want them kept alive, scaled, and reachable without a human. the vocabulary: a pod runs one or more containers; a deployment says "keep n copies of this pod running and roll out changes gradually"; a service gives pods a stable address; an ingress routes outside traffic in; configmaps and secrets are environment; a node is a machine; the cluster is all of them. you do not run it for a one-person studio. you understand it because the job ads say so and because cloud run is kubernetes with the cluster hidden.

image · layer · registryDockerfileport · envpoddeployment · replicasservice · ingressconfigmap · secretrolling updateHPA autoscalingcloud run
FROM python:3.12-slim                   ← start from an image someone maintains
WORKDIR /app
COPY requirements.txt .                 ← copy deps first so this layer caches
RUN pip install --no-cache-dir -r requirements.txt
COPY . .                                ← now the code; changes here do not re-run pip
ENV PORT=8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]   ← the process the container runs

# deployment.yaml · "keep three of these alive, roll them one at a time"
apiVersion: apps/v1
kind: Deployment
metadata: { name: ava }
spec:
  replicas: 3                           ← three pods; one dies, kubernetes starts another
  selector: { matchLabels: { app: ava } }
  template:
    metadata: { labels: { app: ava } }
    spec:
      containers:
      - name: ava
        image: europe-docker.pkg.dev/stryde/ava:1.4.2   ← the image from the registry, pinned
        ports: [{ containerPort: 8080 }]
        envFrom: [{ secretRef: { name: ava-secrets } }]  ← secrets as env, never in the image
        resources: { limits: { memory: "512Mi", cpu: "500m" } }   ← the bill, in advance
where it lives in your stack

ava already runs in a container on cloud run. the Dockerfile above is its shape. what cloud run does for you is the deployment, the service, the ingress and the autoscaling, with no cluster to babysit. the "deploy drift" incident in the data-integrity notes was a kubernetes-class problem: the image running was not the code in main. same failure, managed platform.

say it → "i ship containers to cloud run; i know the kubernetes objects it abstracts, and i would reach for a cluster when i need custom networking or gpu scheduling, not before."
08
the floor · where the truth is kept
databases,
secrets, pipelines

a relational database is tables with rows, and sql is how you ask questions of them. SELECT picks columns, WHERE filters rows, JOIN stitches tables on a shared key, and an index is what makes WHERE fast on a big table. postgres is the serious one; sqlite is the same idea in a single file, which is exactly why evidence uses it per tenant. a migration is a versioned sql change so every environment's schema moves in lockstep.

row level security is the database refusing to show a row unless a policy says the caller may see it. it is the difference between "the app filters by clinic" and "the database will not hand you another clinic's rows even if the app has a bug". secrets live in an injector like doppler and arrive as env vars; they never live in the repo. ci/cd is the && chain from stratum six, run automatically on every push, ending in a deploy.

SELECT · WHERE · JOINindexmigrationpostgres · sqliteRLS policyvector columnenv · dopplerci/cddns · cdn · pages
alter table public.players enable row level security;   ← from now on, no policy = no rows

create policy "members read their club"
  on public.players for select
  using ( private.is_club_member(club_id) );           ← the policy is a boolean sql expression per row

grant execute on function private.is_club_member(uuid)
  to anon, authenticated, service_role;                ← roles: who is asking. these are not secrets.

-- the question the app asks, and what the database actually answers
select p.name, s.rpe, s.minutes
from players p
join session_loads s on s.player_id = p.id            ← join on the shared key
where s.logged_on > now() - interval '28 days'        ← the ACWR window; an index on logged_on makes this cheap
order by s.logged_on desc;
where it lives in your stack

this migration is in the injury time repo you made public this afternoon, and those grant lines were the only thing the secret scan flagged, because "service_role" looks like a credential and is not. knowing the difference between a role name and a key is stratum eight. strydeos's £3.36m revenue bug was a stratum-eight bug too: a query reading the wrong window.

say it → "the database is the last line of defence: rls for tenant isolation, migrations for schema, an index for every hot where clause."
09
the room · saying it out loud
the interview

the interview questions are the strata above, asked sideways. the method for every one of them is the same: name the layer, state the tradeoff, give the receipt. you have more receipts than most candidates have years. the vocabulary below is what you were missing; the answers are already yours.

big-Ohash map vs arrayconcurrency vs parallelismidempotencycaching · invalidationtesting pyramidlatency vs throughputeventual consistency
what happens when you type a url and press enter?

dns turns the name into an address. tcp connects, tls encrypts. the browser sends a GET; a server (or a cdn edge) answers with html; the browser parses it, fetches css and js, builds the dom, runs the js, paints. the cloudflare pages sites you ship are the "cdn edge answers" case: no server runs at all.

explain big-O like i am not an engineer.

it is how the cost grows as the input grows. looking something up in an array by scanning is O(n): double the list, double the time. a hash map (a dict) is O(1): the same cost at any size. sorting is O(n log n). the point-in-polygon check was O(vignettes × polygons × vertices), which is fine at 12 × 835 and would not be fine at 12,000.

concurrency versus parallelism?

concurrency is dealing with many things at once; parallelism is doing many things at once. javascript is concurrent on one thread via the event loop. python's multiprocessing is parallel. the ava war room fleet is parallel: separate processes, separate cpus.

what does idempotent mean and why does it matter?

calling it twice has the same effect as once. it matters because networks retry. a "book appointment" tool that is not idempotent double-books on a retry. ava's booking path carries a key so the second call is a no-op.

how do you know a change did not make things worse?

the eval runs on every change and blocks the deploy below a threshold. 300+ simulated callers, 95% floor, any p0 stops the ship. and the scorecard prints the failing number, because a gate that only shows green is not a gate.

how would you isolate tenants?

at the database, not the app: rls in postgres, or one sqlite file per tenant as evidence does. the app filtering by clinic id is a convenience; the database refusing the row is the guarantee.

when would you use kubernetes?

when i need custom networking, gpu scheduling, or many services with different scaling curves. for a handful of http services, cloud run or vercel give me the deployment, service and autoscaler with no cluster to run. i know the objects; i choose not to pay for them yet.

tell me about a bug you shipped.

this afternoon. i changed a map's value from a string to a set and left a consumer comparing with ===. typescript passed because the comparison is legal, just never true. it was live for six minutes; a diff read caught it, not the compiler. the fix was one line; the lesson is that types check shapes, tests check behaviour, and you need both.

where it lives in your stack

every answer above is anchored to something you shipped. that is the whole trick. the candidate who says "i understand the event loop" loses to the candidate who says "the event loop is why my hover probe hung in a background tab on tuesday".

say it → layer, tradeoff, receipt. every time.
you already run this stack. now you can name it.

nothing in this note is new to your hands. a script is a text file read top to bottom. types are shapes. the loop is one thread. a container is the machine shipped with the code. the database is the last line of defence. the lecture you skipped was mostly vocabulary for things you were already doing.

the one move

before each interview, pick three strata and say each "say it" line out loud with its receipt. not memorised, just said. the words will stop feeling borrowed after the third time.

build it. ship it. move on.
shippers.
every example above is live code from the shippers repos, 24 aug 2026.
field notes: the production stack · agent / harness engineering · shippers-tt