What is URL-specific in the board's machinery, and what a general version takesSUPPORTING MATERIAL
REFERENCE SHELF
Your guided curriculum
SUPPORTING MATERIALGUIDED READING

What is URL-specific in the board's machinery, and what a general version takes

This is the part that decides hours against days. The board was built for one problem, and the coupling is in code, not in content files: a second problem's board.json, tests.json and narration.json cannot change any of what follows. Line numbers are at origin/main 000ed34 (board 3.10). The runner helper (~/scratch/j2s-runner) and the words helper (~/scratch/j2s-words) are changing flow-run.js, resources-sim.js, scenes.js, voice.js, the tests and the narration right now: re-grep after they merge. Two surveys of the code fed this file; every identifier quoted here was checked against the code, but read the function before you change it.

Already general: reuse as is

  • The drawing board: catalog.json (no URL words), rules.js, model.js, layout.js, geometry.js, copies.js, sketch.js, boxes.js; Break it (sim.js, resilience.js, worded for the whiteboard's "member"); the checks arrows, reachesApi, spread, readsSkipDb in checks.js; Haiku drawing (drawer.js).
  • Content wiring: site/reader.py mount_designboard mounts any page with a <page>.board.json (the exercise's heading must equal an h2; <page>.tests.json is required when api is true; <page>.narration.json is optional). site/narration.py and site/tools/narrate.py take every *.narration.json; give each problem its own prefix, because stray clips are found per prefix folder.
  • The interviewer server: site/interviewer/server.py loads every <problem>.interview.json by glob, layered over general.json; test_server.py already loads a second problem (a chat stub). Exceptions: evaluate.py (L111, L119) and real_check.py (L41-53) are hard-wired to url-shortener; general.json's cues for traffic ("visits|redirects") and the hot key ("single link|one link|campaign|super ?bowl|...") and the example in jev_request (L218, "how many links a day, and do they expire?") are URL-worded.
  • Engines with no URL in them: the flow model and editor mechanics (trees, forks, answers, then-steps, ids, undo, the design half's placement and arrows); the simulation pop-up (speeds, flowLight, spotsOf, hopsOf, wiresOf, the pulse, the steps list); the voice player (Voice, holdStep, introduce in voice.js); the heavy test's physics per kind of part where it does not touch the request columns (lambda, cpu, autoscale, load balancers, gateways, pass-throughs, objects, inMemory, retries, queueStep), wiring, the cost meters, Sonnet's number filter (numbersIn, keepGiven, validateReport), applyChange and resimulate, the view's animation.

1. The runner's world: resources-sim.js and flow-run.js (the biggest piece of the API half)

What is URL-only today:

  • What a request is, by its method: flow-run.js ctxOf (POST and PUT create a link, DELETE takes it down, GET resolves it and every GET makes a click), PURPOSE, shape ({code}), whoOf (a GET is Leo), signedIn (Maya and Sam are signed in, Leo is not), decide (found, not-found, blocked, created, conflict, done).
  • One key per request and one record shape: q.ctx.code (the path's last segment, or the body's alias); a record is {code, target, expires, owner}. makeWorld keys every store and cache by that one code.
  • The scenario's events: resources-sim.js EVENTS = cache-drops, pause-invalidation, slow-clicks; parse refuses anything else, plus the clicks expectation.
  • The checks: flow-run.js check decides expiry (against rec.expires), owner, signed-in, valid input (an http(s) target), the deny list (a snapshot every seconds old, read from a store the takedown marked) and the rate limit (nobody in the page's scenes is over it).
  • Clicks: clickStep (which GET step is the click), CLICKY (resources-sim.js L10), count, post of {type: "click"}, the tallies, the "Clicks counted" frames, workerName.
  • Takedowns: makeWorld takeDown (a store keeps the code, blocked; a cache loses its copy or holds it blocked; nothing while invalidation is paused).
  • Every sentence: PAGE (deny list 5 s, p99 100 ms, expiry 30 days), WANTS, FOR, TOLD, TELLS, titleOf, shouldOf ("poster link", "campaign"), PEOPLE, NAMES, castOf, describe, describeCheck, factOf, stuckSays, answerSays, momentOf, explain and each why* (whyStillRedirects, whyDeleted, whyNotFound, whyRefused, whyBothOwn, whySlow, whyClicks), HINTS and FLOW_HINTS.

In flow-run.js that is lines 136-230, 338-373, 394-447 and 522-805, about 470 of its 805 lines. The engine around it is general: begin, stepOnce, visit, thenStep, reach, stuck, deliverUpTo, the loop where calls at one moment race, inClockOrder, the result's shape. In resources-sim.js only findEndpoint, parse's frame, groups, secs and clock are general.

A general version: the engine stays; a problem module supplies the world. Each request kind (method and path pattern): what it is for, who makes it, what it carries, and under which key or keys it is stored. Then the outcomes and the answer each should get, the scenario events and what each does to the world, the checks it offers and how each decides, the cast, and the sentence builders with the hint table. Not sure: whether the sentences can become data. The why sentences read the whole walk log, so they probably stay code, one module per problem, keeping today's function names (titleOf, shouldOf, momentOf, explain). Proof it worked: the URL problem's verdicts, first wrong lines, frames and lessons unchanged on every flow the tests already use. That is how 2.x moved to flows: test/flow-convert.test.mjs ran 89 specs through both runners, verdict for verdict.

2. The step vocabulary: what the API can say

What is URL-only today, shared by every problem (no board.json field changes it):

  • flow.js: CHECKS (L9: expiry, owner, deny-list, signed-in, rate-limit, valid-input, a fixed list); CHECK_WAYS; USES, needsTo, mayTo (only the deny list names a resource and takes every); nodeWords, WAY_WORDS; marksBlocked (L226-227: any DELETE that writes "marks it blocked"); flowLines (a GET's send to a queue is a click, L326); the click helpers clickQueues, strandedClicks, firehoseFor, CLICKS_KEPT, strandedAdvice, strandedStep (L270-309).
  • api.js: STATUS_WORDS (L129-130: 409 "already taken", 403 "not yours", 410 "gone"); specLines' takedown sentence; throughFirehose (a click or count written to S3 goes through Firehose).
  • flow-view.js: VERBS, SPLIT_ASKS ("Expired or not?", "On the deny list or not?"), MEANT (a conditional write only in a database), WRITE_MODES (the label Say it hears is "Save in X if the name is free"), eventOnly, SAID_STATUS, LOOSE_STATUS, STEPPING; options (L447-486: "Mark it blocked in X" in a DELETE, "Check the expiry" always offered, "Check the deny list in X"); reach's sentences ("Needs a database or a cache that keeps your links").
  • flow-writer.js: the system prompt says its examples are about other systems (L55), but it speaks of redirects, "the name is free" and blocklists (L81-101), and flowTool describes every as the deny list's.
  • Say it: say.js SAID_AS; server.py GLOSSES (L485-501) are global, describe URL steps, and must match the editor's labels exactly; the threshold SURE = 0.9 was measured on URL menus (say.eval.json).
  • flow-convert.js: moves 2.x saves into flows, with the URL's meanings. A new problem has no 2.x saves: leave it.

A general version: the checks a problem offers become data per problem, each with its ways, its words, its parameters (the deny list's every is one) and whether it reads a resource. Also per problem: the write modes, what a DELETE means, and the event noun ("clicks" today). The writer's prompt gets examples truly from other systems. GLOSSES follow the labels. A changed label means measuring Say it again with evaluate_say.py before trusting SURE; that spends a little on Bruno's TypeSafe key, so run it once.

3. The simulations' words, and the voice

  • scenes.js: ORDER (L35, the URL's test ids; a tests file's own order array already wins: use it), NUDGES (L39-65, questions about Leo, Maya, launch and links, keyed by hint pattern), toldOf and wentWrong (L142-171: what a status tells a person about a link), MACHINE, ink by cast order. The rest of the pop-up is general.
  • voice.js: REQUEST (POST create, GET open, DELETE takedown, PUT change), toldOf (201 ready, 409 taken, 410 gone, 404 no-such-link...), and the line id of each frame rebuilt from the runner's English sentence by regular expressions: stepOf (" counts the click: ", " marks .+ blocked$", " takes the click;"...), checkOf, eventLine ("Clicks counted by", "Invalidation is paused", "slows down").
  • The catalog: problems/url-shortener.narration.json, 88 lines with the url prefix.

A general version: the runner puts a situation id on each frame, and the voice reads it rather than rebuilding it from words. Until then, any rewording of the runner's sentences can silence Katie without an error: test/voice.test.mjs (every frame of the 98 test APIs finds its line or is a listed silent one) is the guard, and it must stay green after every wording change.

4. The heavy test: heavy.js and its family

What is URL-only today:

  • The request mix is the data layout. PROBLEMS (L140-180) has one entry; each level's at(t) returns {R, hot, create}: redirects a second, the hot link's share, creates. The Walk constructor (L520-565) lays out fixed columns for exactly that model: hot and tail redirects and creates (H, T, C); lookups with and without a click update to follow (RH, RT, RHU, RTU, UH, UT); new links (W); click events and the counts a worker writes (EV, EVH, KH, KT, KC); per arrow, redirects and click counts (RED, CLK). Every handler's signature carries those columns.
  • How a request walks: plan (code looks a link up), answer (cache first, store on a miss, creates to a store, the click inline when there is no queue), specOf and follow (only GET /code, POST /links and the EVENTs are followed; a deny-list check is recognised by name), onCdn (a redirect can be cached at the edge; a POST always passes), fork (p99 is the redirects' only).
  • The event path: clicks, clicksSpec, handOff, counts, K.clickBatch, the hot link's count on its own DynamoDB item (dynamo) or row (rows), CLICK_CAUSES, facts.clicksLost, facts.clicksUncounted.
  • The verdict: K.sloP99Ms (100) and K.sloOk (99.9%) count redirects only; the same two numbers are typed again in heavy-view.js (L223, L629, L1044-1069) and heavy-report.js (L330, L1121).
  • Cost: heavy-cost.js PRICES.bytes ("What one redirect weighs"), DEFAULTS.eventBytes (a click), compareRuns' "Cost per million redirects answered".
  • Words and tables: heavy-fixes.js (FAMILY, TITLES, RULES: "look each link up", a CloudFront TTL bounded by the page's 5-second block), heavy-improve.js TABLE (DynamoDB "keyed on the short code", "clicks are events", page quotes), heavy-explain.js (BY_PART, BY_KIND, SWAP, ADD, SET), heavy-report.js (BRIEFS["url-shortener"], ASSUMPTIONS, HOW_THE_BOARD_READS), heavy-view.js (CAUSE, captions, chart titles), board.js (LEVEL_HINT L97-101, and the level ids fixed to low, medium, high at L1776).
  • Fixtures: test/heavy-designs.mjs (13 shortener boards), test/candidate-design.mjs, the digests in test/fixtures/heavy-base-digests.json, and site/tools/heavy-check.mjs calling heavyRun(..., "url-shortener", ...).

The survey's estimate: about a third of heavy.js is shortener-only (about 750 lines), a third is general AWS physics written against the shortener's columns (about 700), and a third is general as it is.

A general version: a request table per problem. Each request kind gives the method and path it follows in the API, whether it reads, writes or emits an event, its share of the level's rate, its hot-key share, whether a CDN may answer it, its SLO, its bytes and its CPU time. The Walk runs over an array of request kinds instead of named columns. Events are named per problem (an event emitted by kind Y on outcome Z, and whether its consumer adds up per key or writes each one). One place holds the SLOs, and everything reads them there. The words move to the problem. The fix, next-step and explain tables keep their structure (keyed on causes and kinds of part), with the words and page quotes moved out.

Also not sure: that every problem has three levels. The research that chose the URL problem said the ticket problem's "LOW and MEDIUM have no natural story"; let a problem name its own levels.

This is the largest single job, and the one I am least sure of the shape of: a refactor of Walk, or a second walk beside it. Give it a contract-first round of its own. Prove it the way the concept rounds did: the old and new heavyRun side by side over every saved design at every level, and heavy-base-digests.json unchanged (C2 showed 45 runs identical that way).

5. Problem words living in code, and checks wired to one page

  • checks.js namesInDb (L440-456) and clicksOffPath (L458-471): board.json can override their title and ask but not their verdict sentences ("A database owns every short name", "Counting a click never slows a redirect"). Their logic is general ("the database decides uniqueness", "events off the request path").
  • board.js TTLS (L94) and the CloudFront select (L840) say "How long CloudFront keeps a redirect" on every board.
  • flow-panel.js heavyHtml (L727-732) makes a button only of the literal words "Heavy test" in the tests file's heavy line.
  • The browser checks hard-code the URL page and its files: flow-check.mjs (L22, L25), sim-check.mjs (L23, L918), voice-check.mjs (L23-25), interviewer-check.mjs (L18, L26, L540), heavy-check.mjs (many calls), designboard-check.mjs (L276-282, L1138), side-panel-check.mjs (L12). Their fixtures are URL specs (api-specs.mjs, flow-specs.mjs, heavy-designs.mjs). A general version takes the problem as an environment variable, and each problem brings its fixtures.

Found while surveying (not fixed: not this helper's files)

  • heavy-improve.js L199: the "front" next step prints fmt(K.gatewayRate). K.gatewayRate has been Infinity since board 3.4, and fmt(Infinity) gives "Infinity" (checked in node), so a design with API Gateway in front of one Lambda can read "API Gateway takes at most Infinity requests a second". Not checked on the page.
  • heavy-report.js ASSUMPTIONS still gives Sonnet API Gateway's 10,000 a second, Lambda's 1,000 concurrent and a table that "stops at 40,000"; since board 3.4 the model assumes all three raised.
  • heavy.js PROBLEMS, High: creates are 40 + 40 * rest, 80 a second against 79,800 redirects at the peak (about 1,000 to 1), while the comment above it and BRIEFS say one new link per 100 redirects.

The order to make it general

  1. Wait for the runner and words rounds to merge. They change the runner's sentences, the takedown test and the kinds.
  2. The runner's world into a problem module, with the voice keyed by situation ids and the scenes' order, nudges and person-sentences from the problem. Proof: the URL problem's verdicts, frames, lessons and lines unchanged.
  3. The vocabulary as data (checks, ways, write modes, DELETE's meaning, the event noun), GLOSSES following the labels, Say it measured again.
  4. The heavy test's request table, its own round, proved by digests.
  5. Problem words out of code, and the checks given the problem as a parameter.

For size: board 3.0 replaced the API model, its runner, its editor, its writer and the pop-up's middle in one contract-first round of four helpers, about two and a quarter hours from the helpers' start (22:10 on 27 Sep) to the last merge (00:08); steps 1 and 2 are of that order. Step 3 is the unknown. After all four, a new problem is content plus, at most, a few new verbs, checks or scenario events.

If a problem has to ship before the machinery is general, fork deliberately: a copy of the runner's world for that problem, said in the commit and the README, never a quiet if (problem === ...) inside shared code.