diff --git a/bench/site_facts.py b/bench/site_facts.py index d1cb32b..5bbed0b 100755 --- a/bench/site_facts.py +++ b/bench/site_facts.py @@ -242,6 +242,65 @@ def bench_modules(readme: str, lines: int) -> int: return modules +def context_scope() -> dict: + """What the context-pack figures were measured on. + + THE BUG THIS EXISTS TO PREVENT. The /agents page set the bench's + 30-module project beside the pack size and the token saving, as the + project they were measured on. `bench/context_pack.py` measures neither + there. The pack is measured against the import closure of each sample in + its `TARGETS`, and the token runs ground one task on one of those samples. + So the scope is read out of the script that takes the measurement, the way + the module count is read out of the generator. + + `TARGETS` is a literal, read with `ast` rather than imported, so nothing + in that script runs here. The token task's sample is the entry + `rounds_arm` passes to `pack_for`. It has to be exactly one entry, and + one of the targets, or the two arms are no longer measured on the samples + the page names. + """ + import ast + + script = "bench/context_pack.py" + tree = ast.parse((REPO / script).read_text(encoding="utf-8")) + targets = None + tasks: set[str] = set() + for node in tree.body: + if isinstance(node, ast.Assign) and any( + isinstance(name, ast.Name) and name.id == "TARGETS" for name in node.targets + ): + targets = ast.literal_eval(node.value) + if isinstance(node, ast.FunctionDef) and node.name == "rounds_arm": + tasks |= { + call.args[0].value + for call in ast.walk(node) + if isinstance(call, ast.Call) + and isinstance(call.func, ast.Name) + and call.func.id == "pack_for" + and call.args + and isinstance(call.args[0], ast.Constant) + } + + if not isinstance(targets, dict) or not targets: + raise SystemExit( + f"bench/site_facts.py: `{script}` no longer declares its samples " + f"as a literal `TARGETS`, so the site cannot say what the pack " + f"was measured on." + ) + if len(tasks) != 1 or not tasks <= targets.keys(): + raise SystemExit( + f"bench/site_facts.py: `{script}`'s rounds arm grounds " + f"{sorted(tasks) or 'no literal entry'} with `pack_for`, which is " + f"not one of its targets. The site names the sample the token " + f"runs were measured on, so it has to be exactly one of them." + ) + + return { + "targets": [{"entry": entry, "imports": list(closure)} for entry, closure in targets.items()], + "task": tasks.pop(), + } + + # (key, pattern, group names). A miss is an error, never a default. RECORDED_PATTERNS: list[tuple[str, str, tuple[str, ...]]] = [ ("machine", r"\(([^()]*Ryzen[^()]*)\)", ("name",)), @@ -498,6 +557,7 @@ def build() -> tuple[dict, list[str]]: # README.md names no machine for either, because neither is a # duration, so the stamp says so rather than borrowing one. "context_pack": { + **context_scope(), "size_low": int(raw["context_pack"]["size_low"]), "size_high": int(raw["context_pack"]["size_high"]), "tokens_low": int(raw["context_tokens"]["low"]), diff --git a/doc/ART-DIRECTION.md b/doc/ART-DIRECTION.md index 2c3c0d2..5e22527 100644 --- a/doc/ART-DIRECTION.md +++ b/doc/ART-DIRECTION.md @@ -16,18 +16,19 @@ and to stay honest under the reader who checks: every figure on a page is either counted from the tree or stamped with the machine that measured it, and `scripts/no-transcription.mjs` fails the build if one is typed by hand. -The look is deliberately unremarkable. White ground, a sans-serif, a mono for -anything a machine printed, one accent, rounded cards, and nothing else. The -mascot supplies all the character the page needs; a second ornament would -compete with it. +The look is a dark page with warm greys, a wide sans for the author, a mono +for anything a machine printed, one red accent and a green and an amber that +report how a run ended. Rounded cards on hairlines carry the evidence. The +mascot supplies the character, and the type does the rest: the headings are +set a little wide and tight, so they carry weight without going to black. ## The mascot `src/assets/mascot.png`, cut out of its background so it sits on the page -ground in either scheme. It appears in four places and no more: large in the -hero, small beside the wordmark in the header, at the foot of every page, and -as the favicon, cropped to the face. It is -never used as a bullet, a spinner, a badge or a reaction. The `i` on each +ground in either scheme. It appears beside the wordmark in the header and at +the foot of every page, small in front of the eyebrow that opens each section +page, and as the favicon, cropped to the face. It is never used as a bullet in +a list, a spinner or a reaction. The `i` on each cheek is the tittle, the dot that in Turkish orthography separates `i` from dotless `ı`; the same dot terminates every section rule on the site. @@ -36,8 +37,8 @@ drawing is 1200px across; the header mark is 28px. Served out of `public/` it was the same file every time, so a reader paid the hero's weight to draw a mark the width of a word, four times a page. Through `astro:assets` each call site asks for the size it draws at, `densities={[1, 2]}` covers a retina screen, and -the build emits exactly those. The hero keeps `fetchpriority="high"` because it -is the largest thing painted above the fold; every other placement is lazy. +the build emits exactly those. The header mark and the eyebrow are eager, +because they are above the fold; the footer's is lazy. ## Palette @@ -46,106 +47,100 @@ are used in is computed by `scripts/contrast.mjs` on each build and the build fails under the floor the pair's role requires, so no ratio is written down here or there: the file that once recorded five of them had four wrong. +The values are the redesign's, and where one of them fell under its floor it +was moved the least distance that clears it, by the gate's own arithmetic: +`--mute` in both schemes, `--boundary` in both, and `--brand-dark`, whose white +button label sat under 4.5. Separators and soft grounds were alphas in the +redesign; here they are the solid colour each composites to over the page, +because an alpha is invisible to the gate. + +**Dark is the default on a screen.** It is the scheme the page is designed in, +and the light one is the reader's choice, remembered. + | Token | Value | Where it is used | |---|---|---| | `--paper` | `#ffffff` | Page ground. | -| `--raise` | `#f7f7f8` | Alternating bands, cards, code blocks, the footer. | -| `--ink` | `#1c1c1e` | Text and headings. | -| `--graphite` | `#4d4f53` | Secondary prose, captions, console output. | -| `--mute` | `#676a6f` | Labels, ticks, elision marks. | -| `--hairline` | `#e3e4e8` | Every separator between two areas of the page. | -| `--track` | `#eceef1` | The unfilled part of a gauge. | -| `--boundary` | `#85888f` | The edge of a control, and nothing else. | -| `--brand` | `#d1333d` | The mascot's red. Buttons, figures, the tittle. | -| `--brand-deep` | `#ad1f29` | Links and any brand text that has to pass AA on white. | -| `--brand-soft` | `#fdeeee` | The tinted ground a caveat sits on. | -| `--glove` | `#ecb643` | The mascot's yellow. Reserved; the mascot carries it. | -| `--signal-deep` | `#8a151d` | The darkest cut of the accent, for a loss set small. | +| `--raise` | `#f6f5f3` | The footer, a console's ground, inline code, a hovered row. | +| `--card` | `#ffffff` | Cards, steps, the tour's groups. | +| `--code` | `#f8f7f5` | A listing's ground. | +| `--ink` | `#0e0c0b` | Text and headings. | +| `--graphite` | `#4a4540` | Secondary prose, leads, captions. | +| `--mute` | `#746d67` | Labels, line numbers, the bars of code frames. | +| `--hairline` | `#e8e7e6` | Every separator between two areas of the page. | +| `--boundary` | `#8d8c8b` | The edge of a control, and nothing else. | +| `--brand` | `#e5203a` | The mascot's red. The primary button, a selected step's mark. | +| `--brand-deep` | `#c8102e` | Links, eyebrows, keywords, and any red set as text. | +| `--brand-soft` | `#fdeff1` | The ground of a red chip or an errored line. | +| `--on-brand` | `#ffffff` | The label on a red button. | +| `--ok` | `#167d3b` | A run that came back clean, strings in a listing, iyi's bar. | +| `--warn` | `#9a5b00` | A run with something to read, a recorded-only step, an edit notice. | + +And the dark scheme, declared under `-dark` names beside the light one and +switched on by aliasing. `--on-brand` is white in both and has no dark name. + +| Token | Value | +|---|---| +| `--paper-dark` | `#0a0908` | +| `--raise-dark` | `#100e0c` | +| `--card-dark` | `#14110f` | +| `--code-dark` | `#0e0c0b` | +| `--ink-dark` | `#f4f1ee` | +| `--graphite-dark` | `#a49d96` | +| `--mute-dark` | `#837d76` | +| `--hairline-dark` | `#1e1b19` | +| `--boundary-dark` | `#66625f` | +| `--brand-dark` | `#df3228` | +| `--brand-deep-dark` | `#ff6157` | +| `--brand-soft-dark` | `#210e0c` | +| `--ok-dark` | `#5fd98a` | +| `--warn-dark` | `#f5b451` | `--signal` is an alias of `--brand-deep`, because the measurement components -address the accent by that name and a loss and a link get the same colour: the -site has one accent, taken from the mascot, and spends it on what a reader can -act on or should notice. +address the accent by that name: a loss and a link get the same colour. `--hairline` and `--boundary` are not interchangeable and the split is the point. A hairline separates two areas of the page, which WCAG 1.4.11 exempts, and it is drawn faint on purpose. A boundary is the edge of a thing you can press, which 1.4.11 does not exempt, and every control on the site draws with -it: the copy button, the ghost CTA, the declaration toggle, the chart's replay -and the menu button. They used to draw with the hairline, which put every -control's edge far under the floor. +it. -### The dark scheme +### The two schemes -This document used to say there was one scheme, because a second palette would -be a second design to keep honest. That objection is answered rather than -abandoned: `scripts/contrast.mjs` computes every declared pair on every build -and fails under the floor, and the dark pairs are in its list, so the second -palette is kept honest by the same machinery as the first. A dark scheme that -was not gated would be exactly the thing the refusal was about. +Three rules hold them together. Every colour is a literal declared once in the +base `:root`, light under its own name and dark under a `-dark` name, and the +scheme switch only aliases them, so no component knows which scheme it is in +and the gate can see both palettes. The switch is `@media screen`, and the +`screen` is load bearing: a print is always the light palette, so printing a +lesson does not empty a toner cartridge into the page. -| Token | Value | Note | -|---|---|---| -| `--paper-dark` | `#141418` | Ground. Not an inverted white, which reads as a smudge. | -| `--raise-dark` | `#1d1d23` | Bands, cards, code blocks. | -| `--ink-dark` | `#f0f0f2` | Text and headings. | -| `--graphite-dark` | `#c3c5cb` | Secondary prose, captions, console output. | -| `--mute-dark` | `#9ba0a8` | Labels, ticks, elision marks. | -| `--hairline-dark` | `#33343c` | Separators. | -| `--track-dark` | `#2b2c33` | Gauge track. | -| `--boundary-dark` | `#6b6d76` | Control edges. | -| `--brand-dark` | `#f4636d` | The accent, lifted so it carries against a dark ground. | -| `--brand-deep-dark` | `#ff8f96` | The cut that passes AA as running text. | -| `--brand-soft-dark` | `#2b171a` | The tinted ground a caveat sits on. | -| `--glove-dark` | `#f0c25e` | Reserved, as in the light scheme. | -| `--signal-deep-dark` | `#ffb3b8` | A loss set small. | - -Three rules hold it together. Every colour is a literal declared once in the -base `:root`, and both scheme switches only alias them, so no component knows -which scheme it is in and the gate can see both palettes. The default switch -is `@media screen and (prefers-color-scheme: dark)`, and the `screen` is load -bearing: a print query matches a reader's colour preference too, and without it -printing a lesson under a dark preference would empty a toner cartridge into -the page. - -And the machine's answer can be overruled. The masthead carries one control, -drawn with the mark of what the press will do, that writes `data-theme` onto -`` and remembers it. It is `display: none` until a script claims the -masthead, the same handshake the menu button uses, so nothing that cannot -work is ever offered: with scripts blocked the page is still whatever the -machine prefers, which is what the media query decided before any of this. -That is why the alias list is written twice in `src/styles/tokens.css`, once -behind the preference and once behind the attribute, and the preference block -carries `:not([data-theme="light"])` so the override can turn it off. - -The mark is geometry in the markup and not a character. The first draft used -the moon and sun codepoints, both outside the latin and latin-ext subsets the -site ships, so they fell back to whatever the reader's machine had; the second -spelled the press as the mono words `dark` and `light`, which read as a label -rather than a control and sat badly beside the menu button on a phone. Both -are inline SVG now, hairline strokes in `currentColor` at the weight of the -rules around them, and an emoji is still never an icon here. The switch and -the menu button are one square each, the same box and the same edge, 36 to a -side on a pointer and 44 on a thumb, so the pair reads as a pair. - -The mascot is cut out of its background and the code is set in weight rather -than hue, which is why one drawing and one highlighting scheme serve both -grounds. +And the reader can turn it off. The masthead carries one control that writes +`data-theme="light"` onto `` and remembers it; an inline script in the +head restores that choice before the first paint, so a reader who chose light +never sees a flash of the dark page. With scripts blocked the page is dark, +which is what the stylesheet draws with nothing set. + +The mark is geometry in the markup and not a character, inline SVG in +`currentColor`, and it names what the press will do: a dark page offers the +sun. ## Type Two families. **The sans is the author, the mono is the machine.** Anything a command printed is set in mono, without exception. -**Figtree** (OFL, variable) for prose and display. A friendly geometric sans -with enough weight range to carry an 800 headline and a 400 paragraph, and it -ships `latin-ext`, which is a requirement rather than a nicety: the project's -name is Turkish and `ı İ ş ğ ç ö ü` must render. +**Mona Sans** (OFL, variable in weight and width) for prose and display. The +headings and the large figures are set at 106% to 108% of its width with +negative tracking, which is what gives them their weight without going to +black. That is why the width-axis file ships rather than the lighter +weight-only one: without it those headings would render at 100%, and quietly +be a different typeface. It ships `latin-ext`, which is a requirement rather +than a nicety: the project's name is Turkish and `ı İ ş ğ ç ö ü` must render. -**IBM Plex Mono** (OFL) for code, commands, output, labels and the evidence -rail. Ligatures are disabled (`font-variant-ligatures: none`), because a site -about a language's syntax must not silently redraw that syntax. Its true italic -is what comments use. +**JetBrains Mono** (OFL, variable) for code, commands, output, labels and the +bars of code frames. Ligatures are disabled (`font-variant-ligatures: none`), +because a site about a language's syntax must not silently redraw that syntax. +Comments are set in a muted colour rather than in italic, so no italic face +ships. Both are vendored through `@fontsource`, never fetched from a CDN. A site arguing that a program should link only what it uses does not pull webfonts @@ -155,46 +150,44 @@ off a third party. packages and writes `src/generated/fonts.css`, which is the only font import in `src/layouts/Page.astro`. It keeps `woff2` and drops the legacy `woff` that sits beside every `@fontsource` face, and it keeps `latin` and `latin-ext` and drops -`cyrillic`, `cyrillic-ext` and `vietnamese`. Importing the packages' own -stylesheets shipped all of it: two thirds of the font payload was alphabets no -page here contains and a format no browser capable of rendering this site would -request. The `unicode-range` of each face is read out of the package and never -authored, because typing it here would be a transcription of a machine value -that goes stale silently, which is the defect this whole publication is -arranged around. - -Four mono faces ship, and they are the four the stylesheets ask for: 400, 400 -italic, 500 and 600. There is deliberately no 700. `code.css` used to ask for -one for `.tok-rule`, the site's single typographic emphasis, and no 700 face -was ever loaded, so that emphasis was either a synthetic smear or a silent -match to the 600 depending on the reader's browser. It asks for the 600 it -gets. Figtree is variable, so 650 and 800 are real interpolations. +every other alphabet. The `unicode-range` of each face is read out of the +package and never authored, because typing it here would be a transcription of +a machine value that goes stale silently. Two faces are preloaded, the latin sans and the latin mono, because every page -needs both before it can draw a word and they are otherwise discovered a round -trip late. The rest are left to be discovered: preloading a face a page does -not use is the waste this section is about. +needs both before it can draw a word. The `latin-ext` files are left to be +discovered: a page with no Turkish letter on it never asks for them. -Numerals are tabular everywhere so figures in a column compare by eye. The -scale is fluid, ratio 1.25, `--step--2` through `--step-5`. +Numerals are tabular everywhere so figures in a column compare by eye. Body +copy is fixed at 16px; the headings are fluid between a phone and the page +width, `--step-1` through `--step-4`. ## Layout Two containers, declared once in `src/styles/base.css`. -`.wrap` is the page: `min(72rem, 100% - 2 * edge)`, centred. The header, the -footer and every home page section use it, and the home page alternates white -and `--raise` bands across the full width with a hairline between them. +`.wrap` is the page: `min(70rem, 100% - 2 * edge)`, centred, with a 20px edge. +The header and the footer line up with it. `.spread` is the reading page: a `68ch` measure with a `21ch` rail beside it, as named grid lines, so a component can opt into `main`, `rail`, `wide` or `full` without knowing the page. Below 62rem the rail collapses under the -measure. The rail is where provenance lives, at a fixed position, never behind -a hover: a fact whose source you have to go looking for is being presented as -trust rather than as evidence. - -Cards have an 8px radius, 14px on the home page grid. Hairlines separate; -there are no shadows. +measure. The rail is where provenance lives on a lesson, at a fixed position, +never behind a hover. + +A section page is a hero and then sections, each a heading block beside its +evidence in a two-column `.split` that stacks on a phone. The shared pieces +are in `src/styles/site.css`: the type classes, `.sec`, `.split`, `.grid4`, +`.card`, `.code`, `.tabs`, `.chip`, `.tbl`. Anything one page draws stays in +that page's scoped style. + +Cards have a 16px radius, code frames 14px. Hairlines separate. There are two +kinds of lift and no more: a selected tab sits on a one-pixel shadow, and the +two replay panels, the rebuild and the agent race, are lit instruments with a +gradient edge and a drop shadow. A hatch, drawn as a repeating gradient, marks +what a figure leaves out: the part of Crystal's square iyi's hello does not +need, and a module whose body a build skipped. +The masthead is translucent and blurs what scrolls under it. ## Code display @@ -204,7 +197,10 @@ highlighter, into `records/highlight.json`, so what counts as a token is answered by the lexer and never by a grammar this site maintains. The site adds one class after the fact: `.tok-rule`, weight 600, on the keywords that are iyi's and not Crystal's (`module`, `import`, `using`, `pub`, `trait`, `impl`, -`type`, `abstract`). Everything else is weight, not hue. `forall` and `derive` +`type`, `abstract`). Three hues and no more: keywords in `--brand-deep`, +strings in `--ok`, comments in `--mute`, and the rest is ink. The rules keep +their extra weight on top of the hue, so the delta still reads in a printout +or to a reader who cannot tell red from green. `forall` and `derive` are contextual in iyi's parser and reach the page as bare text; emphasising them is a change to the compiler's highlighter, not to this site. @@ -213,7 +209,7 @@ source block shows its path in a header. **Three block types, because they are three kinds of evidence.** A `.source` block is code you could write. A `.console` block is a recording of a real -run: terminal chrome, a mute `$` prompt, and output that is never highlighted +run: a caption bar, a mute `$` prompt, and output that is never highlighted because nothing highlighted it in the terminal. A `.diagnostic` block is a compiler error, caret line preserved exactly, with the rule it cites pulled into a footer that names the SPEC.md section and line the premise is stated @@ -229,9 +225,10 @@ counted from the tree, identical on every machine, and rendered flat and inline by `Measure`, which throws if the key is absent from `facts.json`. A **recorded** number is a second, a byte, a millisecond: a machine, not a -language. It is rendered only by `Stamped`, whose left edge is a ruler of ticks -so the eye reads "this came off an instrument" before it reads any text. The -headline of the card is the ratio, because a ratio survives the machine; the +language. It is rendered only by `Stamped`, a card whose bar reads +`measured · ` and what it is a measurement of, so the eye reads "this came off +an instrument" before it reads the figure. The headline of the card is the +ratio, set at display size, because a ratio survives the machine; the absolute sits in the body beside its spread. The card does not print the box it came off. It did, as a labelled `MACHINE` @@ -259,18 +256,15 @@ a control that cannot do anything is not offered. ## Motion -The site declares motion in four places and no more: the CTA's background -transition, the hamburger's bars, and the chart's two keyframe animations. - -Every one of them is switched off under `@media (prefers-reduced-motion: -reduce)`, in CSS, next to the rule it disables. Not in a script. The preference -was honoured only by a `matchMedia` check inside the chart's client script, -which meant it was honoured only for readers who run scripts, and there was no -`prefers-reduced-motion` query in any stylesheet at all. +Every transition and animation on the site is switched off under +`@media (prefers-reduced-motion: reduce)`, in CSS, by one rule in +`src/styles/base.css`. Not in a script: a preference that only a running +script respects is not respected. Nothing is hidden to satisfy the preference. Reduced motion shows the settled state rather than a faster animation, because a shortened animation would be a -false reading, and the settled state is what the page draws anyway. +false reading, and the settled state is what the page draws anyway. The /agents +race, under the preference, is drawn finished and does not loop. ## Reachability @@ -323,11 +317,19 @@ because the bars are the measurement. ## Imagery -The mascot, and the artifact. There is no photography, no stock, no gradient -and no decorative abstraction. Beyond the mascot, every visual is generated -from something the repository produces: duration figures from the measurement -record, mass figures at true area, and structure diagrams set typographically -in mono with hairline edges. An emoji is never an icon. +The mascot, and the artifact. There is no photography, no stock and no +decorative abstraction. Beyond the mascot, every visual is generated from +something the repository produces: duration figures from the measurement +record, mass figures at true area, a recorded session rendered line by line. + +Two visuals are illustrative, and each page that shows one says so under it. +The home page's rebuild names its modules and its edit for the sake of the +picture; the two durations it runs for, the module count and the ratio are the +measured ones. The agent race, on /agents and in the home page's agents +section, generates its scrolling module bodies from a fixed seed in the +browser; the figures it lands on are read out of `facts.json`, and so is the +line naming the samples they were measured on. Both are in the built HTML +finished, and the script replays them. ## Rendered proof @@ -345,13 +347,15 @@ Captured from the running site in a browser. Recorded because each is a way this specific site could fail. -- No gradient, no glassmorphism, no floating orbs, no icon strip. +- No gradient and no shadow beyond the ones named under Layout. No floating + orbs, no icon strip. - No emoji used as an icon, and no mascot used as one. -- No em-dash and no en-dash in authored copy. A verbatim record, compiler +- No em-dash in authored copy, and an en-dash only between the two ends of a + numeric range set as a figure, `31–43%`. A verbatim record, compiler output, README quotation or sample source, is reproduced exactly, dashes included, because editing a recording to satisfy a house style turns it into a paraphrase. -- No hue in syntax highlighting. +- No hue in syntax highlighting beyond the three named under Code display. - No recorded number outside a measurement card. - No hand-transcribed number anywhere. See `doc/STACK.md`. - No time estimate in any copy, including anything implying how long learning @@ -360,9 +364,12 @@ Recorded because each is a way this specific site could fail. modules; it does not compile what a visitor types, and the page says so. - No figure that only exists once a script has run. Every measurement is drawn by the stylesheet from data in the built HTML, and a script may animate it, - never reveal it. -- No control offered where it cannot act. A menu button and a replay button - appear only once a script has claimed them. + never reveal it. The rebuild, the agent race and the recorded loop are all + in the built HTML, finished; the script replays the first two and folds the + third into tabs. +- No control offered where it cannot act. The menu button, the scheme switch, + every replay button, the loop's tabs and its reader switch appear only once + a script has claimed them. - No colour outside `src/styles/tokens.css`. Both schemes are literals in the base `:root`; the dark scheme only aliases them. A `` and the web manifest take a literal and no variable, so they read the token diff --git a/doc/PLAYGROUND-SERVICE.md b/doc/PLAYGROUND-SERVICE.md index f464696..5d52587 100644 --- a/doc/PLAYGROUND-SERVICE.md +++ b/doc/PLAYGROUND-SERVICE.md @@ -118,10 +118,10 @@ output and compiler diagnostics arriving in the output pane; and a sentence in the output pane on load saying what happens to what you type. **Take the structure and the interaction model. Do not take the look.** This -site has an art direction, `doc/ART-DIRECTION.md`, and it binds: paper -ground, two families and no third, weight-only syntax emphasis with no hue, the -signal colour reserved for losses and caveats, hairlines rather than shadows, -and no sans-serif anywhere. A playground that arrived in another site's visual +site has an art direction, `doc/ART-DIRECTION.md`, and it binds: the page +ground, two families and no third, three hues in syntax and no more, the +accent reserved for what a reader can act on and for losses, hairlines rather +than shadows. A playground that arrived in another site's visual language would be the first page here that did not belong to the publication. ``` diff --git a/doc/screenshots/40-home.png b/doc/screenshots/40-home.png index 1e5ef2f..ee316fc 100644 Binary files a/doc/screenshots/40-home.png and b/doc/screenshots/40-home.png differ diff --git a/doc/screenshots/41-why.png b/doc/screenshots/41-why.png index 16b03ed..bc57ce5 100644 Binary files a/doc/screenshots/41-why.png and b/doc/screenshots/41-why.png differ diff --git a/doc/screenshots/42-learn.png b/doc/screenshots/42-learn.png index bef92fb..f626949 100644 Binary files a/doc/screenshots/42-learn.png and b/doc/screenshots/42-learn.png differ diff --git a/doc/screenshots/43-sample-run.png b/doc/screenshots/43-sample-run.png index dd9db98..03fc133 100644 Binary files a/doc/screenshots/43-sample-run.png and b/doc/screenshots/43-sample-run.png differ diff --git a/doc/screenshots/45-tour.png b/doc/screenshots/45-tour.png index e16d8ef..ba4e38d 100644 Binary files a/doc/screenshots/45-tour.png and b/doc/screenshots/45-tour.png differ diff --git a/package-lock.json b/package-lock.json index c8cc801..bfbc5b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,8 @@ "dependencies": { "@astrojs/mdx": "^7.0.7", "@astrojs/sitemap": "^3.7.4", - "@fontsource-variable/figtree": "^5.3.0", - "@fontsource/ibm-plex-mono": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/mona-sans": "^5.3.0", "astro": "^7.2.4" }, "devDependencies": { @@ -132,9 +132,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -151,9 +148,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -170,9 +164,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -189,9 +180,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -516,9 +504,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -532,9 +517,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -548,9 +530,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -564,9 +543,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1199,19 +1175,19 @@ "node": ">=18" } }, - "node_modules/@fontsource-variable/figtree": { + "node_modules/@fontsource-variable/jetbrains-mono": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource-variable/figtree/-/figtree-5.3.0.tgz", - "integrity": "sha512-VRVodD7OiG7apQwE8/2fMjaqpY/a0qRJqL5HtYj8CKIVps+9amaFcdfKmDIf7iqYQx6iPLlArVXAfKeIZmVcNQ==", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@fontsource/ibm-plex-mono": { + "node_modules/@fontsource-variable/mona-sans": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", - "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", + "resolved": "https://registry.npmjs.org/@fontsource-variable/mona-sans/-/mona-sans-5.3.0.tgz", + "integrity": "sha512-PkxRB3KI4z9Gjpk7vVqgO3GMSyQuOIIj4TaRDbvbcDj8SzWwI38jvLTQxJM/cTRorhcT7h8qjcllR+qjRtM/Fg==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -1329,9 +1305,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1348,9 +1321,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1367,9 +1337,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1386,9 +1353,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1405,9 +1369,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1424,9 +1385,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1443,9 +1401,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1462,9 +1417,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1481,9 +1433,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1506,9 +1455,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1531,9 +1477,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1556,9 +1499,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1581,9 +1521,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1606,9 +1543,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1631,9 +1565,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1656,9 +1587,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1948,9 +1876,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1967,9 +1892,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1986,9 +1908,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2005,9 +1924,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2024,9 +1940,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2043,9 +1956,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4108,9 +4018,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4131,9 +4038,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4154,9 +4058,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4177,9 +4078,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/package.json b/package.json index 8111933..1cddfa8 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,8 @@ "dependencies": { "@astrojs/mdx": "^7.0.7", "@astrojs/sitemap": "^3.7.4", - "@fontsource-variable/figtree": "^5.3.0", - "@fontsource/ibm-plex-mono": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/mona-sans": "^5.3.0", "astro": "^7.2.4" }, "devDependencies": { diff --git a/scripts/budget.json b/scripts/budget.json index 70bf9e0..03a6197 100644 --- a/scripts/budget.json +++ b/scripts/budget.json @@ -20,23 +20,23 @@ ], "grain": 16384, "routes": [ - { "match": "/project/security/", "ceiling": 180224 }, - { "match": "/project/", "ceiling": 180224 }, - { "match": "/project/contributing/", "ceiling": 180224 }, - { "match": "/project/notice/", "ceiling": 180224 }, - { "match": "/project/conduct/", "ceiling": 180224 }, - { "match": "/targets/", "ceiling": 196608 }, - { "match": "/download/", "ceiling": 196608 }, - { "match": "/agents/", "ceiling": 212992 }, - { "match": "/", "ceiling": 278528 }, - { "match": "/404.html", "ceiling": 212992 }, + { "match": "/project/security/", "ceiling": 212992 }, + { "match": "/project/", "ceiling": 212992 }, + { "match": "/project/contributing/", "ceiling": 212992 }, + { "match": "/project/notice/", "ceiling": 212992 }, + { "match": "/project/conduct/", "ceiling": 212992 }, + { "match": "/targets/", "ceiling": 229376 }, + { "match": "/download/", "ceiling": 229376 }, + { "match": "/agents/", "ceiling": 262144 }, + { "match": "/", "ceiling": 294912 }, + { "match": "/404.html", "ceiling": 245760 }, { "match": "/why/", "ceiling": 229376 }, - { "match": "/install/", "ceiling": 196608 }, - { "match": "/learn/", "ceiling": 212992 }, - { "match": "/learn/*/", "ceiling": 212992 }, - { "match": "/playground/", "ceiling": 196608 }, - { "match": "/playground/iyi/*/", "ceiling": 229376 }, + { "match": "/install/", "ceiling": 229376 }, + { "match": "/learn/", "ceiling": 245760 }, + { "match": "/learn/*/", "ceiling": 245760 }, + { "match": "/playground/", "ceiling": 229376 }, + { "match": "/playground/iyi/*/", "ceiling": 262144 }, { "match": "/playground/*/", "ceiling": 294912 } ], - "assets": 589824 + "assets": 638976 } diff --git a/scripts/budget.mjs b/scripts/budget.mjs index c7e7e96..38f2aff 100755 --- a/scripts/budget.mjs +++ b/scripts/budget.mjs @@ -61,7 +61,7 @@ import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); const site = resolve(here, ".."); @@ -90,7 +90,7 @@ const files = (dir) => * SITE_BASE set, a page's `href` carries that prefix and joining it onto * `dist/` blind would find nothing, weigh nothing, and pass. A URL this script * built itself from a path on disk has no prefix, hence the conditional. */ -const base = (await import(resolve(site, "astro.config.mjs"))).default.base.replace(/\/*$/, "/"); +const base = (await import(pathToFileURL(resolve(site, "astro.config.mjs")).href)).default.base.replace(/\/*$/, "/"); const asset = (url) => { const path = url.replace(/[?#].*$/, ""); @@ -199,8 +199,13 @@ function weigh(file) { for (const [, set] of html.matchAll(/\bsrcset="([^"]+)"/g)) { for (const candidate of set.split(",")) pictures.add(candidate.trim().split(/\s+/)[0]); } - for (const [, url] of html.matchAll(/]*\brel="preload"[^>]*\bhref="([^"]+)"/g)) { - pictures.add(url); + /* A preloaded image is a picture. A preloaded font is not: it is one of the + * faces the font column below already counts, and adding it here as well + * counted every preloaded face twice. */ + for (const [tag] of html.matchAll(/]*\brel="preload"[^>]*>/g)) { + if (/\bas="font"/.test(tag)) continue; + const href = /\bhref="([^"]+)"/.exec(tag); + if (href) pictures.add(href[1]); } const points = codepoints(html); diff --git a/scripts/contrast.mjs b/scripts/contrast.mjs index 48199df..d62e4c2 100755 --- a/scripts/contrast.mjs +++ b/scripts/contrast.mjs @@ -81,7 +81,7 @@ const value = (name) => { /* ------------------------------------------------------------------------ */ /** The grounds prose sits on, in the light scheme. */ -const GROUNDS = ["paper", "raise", "brand-soft"]; +const GROUNDS = ["paper", "raise", "card", "code", "brand-soft"]; /** * And in the dark one. `src/styles/tokens.css` declares the dark palette under @@ -92,7 +92,7 @@ const GROUNDS = ["paper", "raise", "brand-soft"]; * means two sets of rows, which is the cost of the second scheme being * checkable rather than asserted. */ -const GROUNDS_DARK = ["paper-dark", "raise-dark", "brand-soft-dark"]; +const GROUNDS_DARK = ["paper-dark", "raise-dark", "card-dark", "code-dark", "brand-soft-dark"]; /** * The pairs, each with the floor its role requires. @@ -102,8 +102,11 @@ const GROUNDS_DARK = ["paper-dark", "raise-dark", "brand-soft-dark"]; * at `--step-1` and heavier or larger, so they are large text: 3.0. `--brand` * is only ever used that way; `--brand-deep` exists for the sizes that are * not, which is why the two are separate tokens at all. - * `reverse` is paper on a filled ground: 4.5, because the CTA's label is 17px - * at 600 and that is not large text. + * `reverse` is a label on a filled ground: 4.5, because a button's label is + * 15px at 600 and that is not large text. The red button's label is + * `--on-brand`, white in both schemes; the ink button's is the page ground. + * `status` is the green and the amber a run reports in, as text on every + * ground and on the soft ground of its own chip: 4.5, they are set small. * `boundary` is a focus ring or any non-text control edge: 1.4.11, 3.0. */ const PAIRS = [ @@ -116,12 +119,15 @@ const PAIRS = [ })), ), ...GROUNDS.map((ground) => ({ ink: "brand", ground, role: "display", floor: 3 })), - ...["brand", "brand-deep", "ink"].map((ground) => ({ + { ink: "on-brand", ground: "brand", role: "reverse", floor: 4.5 }, + ...["brand-deep", "ink"].map((ground) => ({ ink: "paper", ground, role: "reverse", floor: 4.5, })), + ...[...GROUNDS, "ok-soft"].map((ground) => ({ ink: "ok", ground, role: "status", floor: 4.5 })), + ...[...GROUNDS, "warn-soft"].map((ground) => ({ ink: "warn", ground, role: "status", floor: 4.5 })), ...["paper", "raise"].map((ground) => ({ ink: "brand", ground, @@ -137,12 +143,15 @@ const PAIRS = [ })), ), ...GROUNDS_DARK.map((ground) => ({ ink: "brand-dark", ground, role: "display", floor: 3 })), - ...["brand-dark", "brand-deep-dark", "ink-dark"].map((ground) => ({ + { ink: "on-brand", ground: "brand-dark", role: "reverse", floor: 4.5 }, + ...["brand-deep-dark", "ink-dark"].map((ground) => ({ ink: "paper-dark", ground, role: "reverse", floor: 4.5, })), + ...[...GROUNDS_DARK, "ok-soft-dark"].map((ground) => ({ ink: "ok-dark", ground, role: "status", floor: 4.5 })), + ...[...GROUNDS_DARK, "warn-soft-dark"].map((ground) => ({ ink: "warn-dark", ground, role: "status", floor: 4.5 })), ...["paper-dark", "raise-dark"].map((ground) => ({ ink: "brand-dark", ground, diff --git a/scripts/fonts.mjs b/scripts/fonts.mjs index 206a739..373824a 100755 --- a/scripts/fonts.mjs +++ b/scripts/fonts.mjs @@ -11,7 +11,7 @@ // cannot read it also cannot read the `@media (prefers-color-scheme)` and // the custom properties this site is built out of. Those files were half // the font payload and zero of the font traffic. -// * `@fontsource/ibm-plex-mono/400.css` and its siblings pull the cyrillic, +// * A `@fontsource` stylesheet and its siblings pull the cyrillic, // cyrillic-ext and vietnamese subsets in with the latin ones. The site is // in English with Turkish proper nouns. Those subsets can never match a // codepoint on any page here. @@ -48,24 +48,22 @@ const require = createRequire(import.meta.url); /** * The faces the stylesheets ask for, and nothing else. * - * The mono list is the set of weights `src/styles` and the components actually - * declare: 400 body code, 400 italic for comments (IBM Plex Mono was chosen - * over the ligature monos for its true italic), 500 for a Crystal keyword and - * the small-caps labels, 600 for a measured value and for `.tok-rule`, the one - * emphasis this site draws. There is deliberately no 700: `code.css` used to - * ask for one, no 700 face was ever loaded, and a weight nothing can render is - * either a synthetic smear or a silent fall back to 600. It now asks for the - * 600 it gets. + * Both families are variable, so one file per subset covers every weight the + * stylesheets name and the in-between weights the headings use (650, 750) are + * real interpolations rather than matches. * - * Figtree is variable, so one file per subset covers 300 through 900 and the - * 650 and 800 the headings use are real interpolations rather than matches. + * Mona Sans ships with its width axis because the display type asks for it: + * the headings and the large figures are set at `font-stretch: 106%` to + * `108%`. The width file is the heavier one, and it is the one the design + * draws with; a `wght`-only file would render those headings at 100% and + * quietly be a different typeface. + * + * JetBrains Mono is upright only. Comments are set in a muted colour rather + * than in italic, so no italic face is asked for and none ships. */ const FACES = [ - { pkg: "@fontsource-variable/figtree", sheet: "wght.css" }, - { pkg: "@fontsource/ibm-plex-mono", sheet: "400.css" }, - { pkg: "@fontsource/ibm-plex-mono", sheet: "400-italic.css" }, - { pkg: "@fontsource/ibm-plex-mono", sheet: "500.css" }, - { pkg: "@fontsource/ibm-plex-mono", sheet: "600.css" }, + { pkg: "@fontsource-variable/mona-sans", sheet: "wdth.css" }, + { pkg: "@fontsource-variable/jetbrains-mono", sheet: "wght.css" }, ]; /** diff --git a/scripts/links.mjs b/scripts/links.mjs index a61cc56..7d76f2e 100755 --- a/scripts/links.mjs +++ b/scripts/links.mjs @@ -47,7 +47,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs"; import { basename, dirname, join, posix, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); const site = resolve(here, ".."); @@ -61,7 +61,7 @@ const dist = resolve(site, process.argv[2] ?? "dist"); /* The serving rules are the config's, not this file's. Repeating "the origin * is iyi.dev and the base is /" here would be a second place for them to * disagree, and the config is the one the build obeys. */ -const config = (await import(resolve(site, "astro.config.mjs"))).default; +const config = (await import(pathToFileURL(resolve(site, "astro.config.mjs")).href)).default; const origin = new URL(config.site).origin; const base = config.base.endsWith("/") ? config.base : `${config.base}/`; diff --git a/src/assets/mascot.png b/src/assets/mascot.png index 8572c95..9de2631 100644 Binary files a/src/assets/mascot.png and b/src/assets/mascot.png differ diff --git a/src/components/AgentRace.astro b/src/components/AgentRace.astro new file mode 100644 index 0000000..248a623 --- /dev/null +++ b/src/components/AgentRace.astro @@ -0,0 +1,405 @@ +--- +/** + * The agent race: the same four verbs in two lanes, one grounded in the + * sources and one in `iyi mod context`'s pack, landing on the measured pack + * size and token saving. + * + * The figures are the measured ones, from facts.json, and so is the scope + * line: the samples `bench/context_pack.py` measures, which + * `bench/site_facts.py` reads out of that script. The code scrolling through + * the lanes is illustrative, and the page says so under the panel. + * + * SETTLED IS THE DEFAULT. Both lanes are built finished, every step ticked, + * the result in the foot, so with scripts off this is the result. + * `src/lib/agent-race.ts` takes it back to the start and replays it once the + * reader has scrolled and has it in view, on /agents as anywhere else: never + * at load, where it played out while the reader was still on the heading. + * + * Each step is a link to that step of the recorded loop on /agents, so it + * goes somewhere with scripts off too. + */ +import facts from "../generated/facts.json"; + +interface Props { + /** Where the recorded loop is: `#loop` on /agents, a full path elsewhere. */ + loop: string; + /** The link in the foot that leads to it. */ + hint: string; +} + +const { loop, hint } = Astro.props; +const pack = facts.recorded.context_pack; +const measuredOn = pack.targets.map((target) => target.entry.split("/").at(-1)).join(", "); +--- + +
+ + Same fix. {pack.tokens_low}–{pack.tokens_high}% fewer prompt tokens,{" "} + for no fewer rounds. + + {hint} +
+Same four verbs in both lanes. Watch what each one has to read.
+
$
{row.label}
- - Bars are the fastest of {sessions.length} published sessions. The hairline - behind each one runs to that session's slowest reading, { - rows.find((r) => r.key === "crystal")!.slow.toFixed(2) - } s at the far end. The ratio held from {lo.toFixed(2)}x to {hi.toFixed(2)}x - while the seconds moved, which is why the ratio is the claim. -
` open tag.
* Every sample begins with a comment, so this costs nothing today; it is here
@@ -120,182 +121,183 @@ const section = tourSections.find((s) => s.steps.some((step) => step.id === samp
const step = at >= 0 ? `${at + 1} of ${tourSamples.length}` : null;
---
-
-
-
-
+
+
+
+
+
Playground
- {section && `, ${section.title}`}{step && `, ${step}`}
+ {section && ` · ${section.title}`}{step && ` · ${step}`}
- {sample.set === "tour" ? sample.id : sample.path}
- {blurb}.
+
+ {
+ sample.set === "tour"
+ ? sample.id
+ : sample.path.split("/").map((part, i, all) => (
+ <>{part}{i < all.length - 1 && <>/ >}>
+ ))
+ }
+
+
-