you read code all day. this is the grammar underneath it.
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.
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.
#!/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
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.
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).
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")
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.
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".
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); }); });
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.
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.
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
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.
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.
// 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 }
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.
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.
# 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.
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.
"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.
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
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.
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.
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;
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.
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.
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.
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 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.
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.
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.
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 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.
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.
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".
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.
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.