Compare commits

...

2 Commits

Author SHA1 Message Date
Levi Neuwirth 36748573cd auto: 2026-05-09T01:05:52Z [skip ci] 2026-05-08 21:05:52 -04:00
Levi Neuwirth 7f7c029601 Marks I 2026-05-07 23:51:14 -04:00
37 changed files with 3264 additions and 50 deletions

959
MARKS.md Normal file
View File

@ -0,0 +1,959 @@
# Frontmatter Marks: Specification
A two-part visual identity for essay and research frontmatter, designed
to extend (not replace) the existing epistemic profile system.
The mark system has two pieces:
1. **Monogram** — a hand-authored SVG glyph per piece, abstracted from
the work's central concept. The author's statement of *what* the
piece is about.
2. **Epistemic figure** — a build-time SVG generated deterministically
from existing frontmatter fields. The site's statement of *where
the piece stands*.
The two are paired in the frontmatter: monogram on the left, title and
abstract in the middle, epistemic figure on the right. Either can be
present alone; both can be absent. When a field that drives the
epistemic figure is missing, the figure is omitted entirely rather
than rendered with empty axes.
This document specifies the authoring interface, the field schema
(extending the existing one in `WRITING.md`), the visual contract,
the build-time rendering pipeline, and the migration plan.
It is written to slot in alongside `WRITING.md` as a sibling reference,
and to live as a section in the colophon once shipped.
---
## 1. Scope and non-goals
### In scope
- A monogram convention (location, dimensions, line-weight, palette)
that any author or generator can produce SVGs for.
- Two new optional frontmatter fields (`peer-status`, `result-shape`)
that surface useful information for both essays and formal research,
with a colophon-glossed interpretation that adapts to genre.
- One narrow exception (`confidence: proved`) that lets formal proofs
honestly opt out of a numeric credence without forcing false precision.
- A new Pandoc filter (`Filters/Mark.hs`) that emits the epistemic
figure SVG inline in the essay header at build time.
- Template changes in `templates/essay.html` and `templates/blog-post.html`
to provide three-column frontmatter slots (monogram | title | figure).
- A `make audit-marks` build target and an addition to `/build/`
surfacing which essays are missing one or both marks.
### Out of scope
- A separate "research badge" figure type. The single radial figure
handles both essays and formal research; see §3.4.
- A unified mode-switched figure with axes that change meaning based on
a `claim-mode` flag. Visual grammar should be unambiguous; one figure
type, stable axis semantics. See §11 for the rejected alternatives.
- Per-portal iconographic systems (the Approach 5 idea from earlier
exploration). Not ruled out for the future, but not specified here.
- Author UI for generating monograms. Authors may use any tool —
hand-drawn, prompt-driven, traced from references — provided the
output meets §2.
---
## 2. The monogram
### 2.1 Authoring contract
A monogram is a single SVG file at:
content/essays/{slug}/mark.svg ← directory-form essays
content/essays/{slug}.mark.svg ← flat-file essays
content/blog/{slug}.mark.svg
content/poetry/{slug}.mark.svg
content/fiction/{slug}.mark.svg
content/music/{slug}/mark.svg
content/{slug}.mark.svg ← standalone pages
content/drafts/essays/{slug}.mark.svg ← drafts
The build picks up the file by the same slug-resolution rules already
used for score fragments and page-local JS. No frontmatter key is
required to opt in; the file's presence is the opt-in. To opt out
(suppress an inherited or stale mark), delete the file.
### 2.2 Visual contract
Monograms must satisfy the following constraints. These are enforced
by `tools/audit-marks.py` and by `make audit-marks` (§9), not at
build-fail level — violations warn but do not break the build.
| # | Constraint | Rationale |
|---|---|---|
| M1 | `viewBox="0 0 280 280"` (or proportional, square) | Renders at 130280 px equally. |
| M2 | All strokes use `stroke="currentColor"`; all fills use `fill="none"` except small filled point-marks which use `fill="currentColor"` | Inverts cleanly under Light, Dark, Cappuccino without per-theme assets. The score-fragment filter already does this substitution; monograms must be authored this way from the start. |
| M3 | Outer roundel: `<circle cx="140" cy="140" r="128" stroke-width="0.6"/>` | The unifying frame. Without it, marks read as illustrations, not as a system. |
| M4 | Stroke widths within {0.3, 0.5, 0.6, 0.8, 1.0, 1.2, 1.4} | Limits visual rhythm to a small palette. |
| M5 | `stroke-linecap="round"` and `stroke-linejoin="round"` everywhere | Spectral-compatible terminals. |
| M6 | No `<text>`, no `<image>`, no gradients, no filters, no embedded fonts, no rasters | Letterforms and color belong to the page, not the mark. Note: `<title>` and `<desc>` for accessibility are required (§2.3), not forbidden. |
| M7 | No XML prologue, no `<?xml` declaration, no DOCTYPE | The file is inlined; a prologue would land mid-body. |
| M8 | File size ≤ 8 KiB | A working corpus of 200 marks at this ceiling is 1.6 MiB total; larger is overkill for a 280-px frontispiece. |
| M9 | Validates as well-formed XML (round-trips through `xmllint --noout`) | Inlining a malformed SVG breaks the surrounding page. |
A reference monogram template lives at `static/templates/mark-template.svg`
and is the recommended starting point for hand-authoring.
### 2.3 Accessibility
Each monogram must include a `<title>` element as the first child of
`<svg>` and may include a `<desc>`:
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-title">
<title id="mark-title">Half-buried column on a low desert horizon</title>
<desc>A frontispiece mark for the essay "Ozymandias: A Static Site Framework".</desc>
...
</svg>
The author writes the visible-content description in `<title>`. The
`role="img"` and `aria-labelledby` attributes are required by M9.
Screen readers announce the title; the desc is supplementary.
### 2.4 Inlining and theming
Monograms are inlined into the page HTML at build time by the same
mechanism `Filters/Score.hs` uses for score fragments. The build step:
1. Reads the SVG file.
2. Strips any `width=` and `height=` attributes from the `<svg>` root
(presentation is controlled by CSS).
3. Replaces any `fill="#000000"`, `fill="black"`, `stroke="#000000"`,
`stroke="black"` with `currentColor` (defensive: lets authors
produce SVGs from generators that hardcode black without breaking
the contract).
4. Wraps the SVG in `<figure class="frontmatter-mark frontmatter-mark--monogram">…</figure>`.
The CSS rule `.frontmatter-mark svg { color: var(--text); }` propagates
the page color to the SVG strokes. No theme-switching JS is required.
### 2.5 Print and reduce-motion
Monograms render in print (they are inert SVG; there is nothing to
suppress). They are unaffected by `prefers-reduced-motion` because
they do not animate. The Display panel's "Focus Mode" hides them via
`.focus-mode .frontmatter-mark { display: none; }`.
---
## 3. The epistemic figure
### 3.1 Visibility rule
The epistemic figure is rendered for a piece if, and only if, the
existing visibility rule for the epistemic block is met:
> The epistemic figure renders when `status:` is set in frontmatter.
This matches the existing rule in `WRITING.md` ("The epistemic footer
section appears when `status` is set"). No new gating field is
introduced. A piece without `status:` gets no figure — the same as it
gets no epistemic block today.
This is deliberate. A figure showing five missing axes and one filled
axis would look like a build bug, not a deliberate position. Either
the piece has taken a position (and therefore renders the figure), or
it has not (and therefore renders only the monogram). The audit job
in §9 lists pieces in `research/` or with `peer-status:` that lack
`status:`, so the absence is visible without being silently broken.
### 3.2 Inputs
The figure consumes only fields already in the schema (per `WRITING.md`),
plus the two new optional fields specified in §4:
| Field | Existing? | Maps to |
|---|---|---|
| `confidence` | yes (0100) | confidence axis length |
| `importance` | yes (15) | importance axis length |
| `evidence` | yes (15) | evidence axis length |
| `scope` | yes (5-step ordinal) | scope axis length |
| `novelty` | yes (4-step ordinal) | novelty axis length |
| `practicality` | yes (5-step ordinal) | practicality axis length |
| *(stability)* | auto from git | outer-ring tick count |
| *(trust score)* | auto from formula | center number |
| `peer-status` | **new** (§4.1) | outer-ring tick *style* |
| `result-shape` | **new** (§4.2) | center glyph beside trust |
| `confidence: proved` | **new exception** (§4.3) | confidence axis renders as proof-cap |
Ordinal-to-numeric mapping is exactly what `Contexts.hs` already does
for the dot-rendering of these fields:
| `scope` value | Numeric |
|---|---|
| `personal` | 1 |
| `local` | 2 |
| `average` | 3 |
| `broad` | 4 |
| `civilizational` | 5 |
| `novelty` value | Numeric |
|---|---|
| `conventional` | 1 |
| `moderate` | 2 |
| `idiosyncratic` | 3 |
| `innovative` | 4 |
(Note: `novelty` is a 4-step scale in the existing schema, not 5. The
figure normalizes to a 01 axis length the same way the dots do —
`(value - 1) / (max - 1)` — so a 4-step scale renders one step shorter
at maximum than a 5-step scale. This is honest and matches existing
behavior; do not silently widen the scale to 5.)
| `practicality` value | Numeric |
|---|---|
| `abstract` | 1 |
| `low` | 2 |
| `moderate` | 3 |
| `high` | 4 |
| `exceptional` | 5 |
The `confidence` axis is 0100; it normalizes as `confidence / 100`.
### 3.3 Geometry
The figure is a 200×200 SVG rendered at frontmatter scale (170 px on
desktop, 130 px on mobile). It consists of:
- An outer roundel (two thin concentric circles, `r=88` and `r=90`,
both `stroke-width="0.5"`).
- Six radial axes at 60° spacing, in this fixed clockwise order
starting from 12 o'clock:
1. confidence (top, 0°)
2. novelty (60°)
3. practicality (120°)
4. scope (180°, bottom)
5. evidence (240°)
6. importance (300°)
Axis stroke `0.3`, opacity `0.55`. Visible at all times whether or
not the corresponding field is set; a missing field renders the
axis without a polygon vertex (see below).
- Four inner concentric guide circles at `0.2, 0.4, 0.6, 0.8` of the
axis radius. Stroke `0.25`, opacity `0.4`.
- A polygon connecting the field values along their axes.
Stroke `1.1`, fill `currentColor` at `fill-opacity="0.08"`. The
polygon is closed only if all six fields are present; otherwise it
is rendered as an open polyline through the present vertices in
axis order, and missing axes contribute no vertex (the line jumps
the missing axis). This is the only mode where partial fields are
rendered; in practice §3.1 ensures all six are present whenever the
figure renders at all.
- Vertex point marks at each present field's position
(`r=2`, `fill="currentColor"`).
- The center number: trust score, in Spectral 500 weight, font-size 16,
centered on the geometric center.
- Below the trust number, in 5 pt Fira Sans with letter-spacing 0.18em,
the literal text `TRUST`.
- Stability ticks on the outer ring at 12 o'clock (see §3.5).
- A result-shape glyph immediately to the right of the trust number
(rendered only when `result-shape:` is set; see §4.2).
The figure deliberately omits the confidence-trend arrow; the trend is
rendered inline in the compact epistemic strip instead (see §3.4). The
figure carries only the geometry, the trust score, and the result-shape
glyph.
A reference renderer in pure SVG, with annotated coordinates, is
checked in at `static/templates/epistemic-figure-reference.svg` for
visual regression testing.
### 3.4 Confidence trend
The trend arrow is rendered inline in the compact epistemic strip,
immediately after the confidence percentage (e.g. `conf 80%↑`). It
indicates the direction of the *last* step in `confidence-history`:
| Last step | Glyph |
|---|---|
| Increase (∆ > 5) | ↑ |
| Decrease (∆ < 5) | ↓ |
| Equal (within ±5) | → |
When `confidence-history` is absent or has fewer than two entries, the
arrow is not drawn. The arrow uses the same parsing and ±5 threshold
the existing epistemic block uses; no new heuristic is introduced.
The arrow lives in the strip rather than on the figure for two reasons:
the figure stays visually clean, and the arrow sits next to the value
it modifies. This is a deliberate departure from earlier drafts that
placed the arrow at the confidence vertex.
### 3.5 Stability ticks
The existing `Stability.hs` heuristic produces one of five labels:
`volatile`, `revising`, `fairly stable`, `stable`, `established`.
These map to outer-ring ticks at 12 o'clock:
| Stability | Tick count | Tick positions (visible / dim) |
|---|---|---|
| volatile | 1 | center only |
| revising | 2 | center + left |
| fairly stable | 3 | center + left + right |
| stable | 4 | center + left + right + far-left |
| established | 5 | all five |
Ticks are short radial line segments just outside the outer roundel,
11.5 px in length, `stroke-width="1"`. Inactive ticks are drawn at
`opacity="0.4"` so the full set is always visible; this gives the
reader a constant five-step scale to anchor against.
The manual override mechanism (`IGNORE.txt`) documented in WRITING.md
applies unchanged: a path listed there pins stability for one build
and is cleared by `make build`. The figure honors the override.
### 3.6 Visibility under reduce-motion and print
The figure does not animate, so reduce-motion has no effect.
In print, the figure renders at fixed size and inverts to black-on-white
via the existing `@media print` rules in `static/css/print.css`. No
new print rules are required.
### 3.7 Theming
The figure uses `currentColor` exclusively. The `<text>` elements
explicitly set `fill="currentColor" stroke="none"` to prevent text
nodes from inheriting the strokes used for geometry.
### 3.8 Tooltip and link
The figure is wrapped in `<a href="#epistemic">…</a>`. Hovering it
triggers the existing epistemic-jump-link popup (per WRITING.md:
"Epistemic jump link (`#epistemic`) — Clone of the full epistemic
profile"). Clicking jumps to the epistemic block at the page footer.
This means the figure does double duty: it is a glance-readable
summary at the top, and a clickable handle for the full block at the
bottom. The popup logic is already implemented; this is a free pickup.
---
## 4. New optional frontmatter fields
Two new fields and one new value of an existing field. All optional;
all backward-compatible; existing essays continue to render
identically until the author opts in.
### 4.1 `peer-status`
Captures the *external* review state of a piece, distinct from
`status` (which captures the author's internal position).
| Value | Meaning |
|---|---|
| `unreviewed` | No external review has taken place. Default if omitted. |
| `under-review` | Currently in submission or peer review. |
| `peer-reviewed` | Has been peer-reviewed (e.g. preprint with referee reports addressed) but not yet formally published. |
| `published` | Appeared in a peer-reviewed venue. Treat as canonical. |
| `retracted` | Formally retracted. Renders with a strikethrough on the field name in the epistemic block. |
This is genre-agnostic. An essay can be `under-review` at a magazine;
a paper can be `peer-reviewed` at a journal. The vocabulary doesn't
change.
#### Visual encoding
`peer-status` modulates the *style* of the stability ticks (§3.5),
not their count. Stability and peer-status are factored: stability
remains git-derived and counts ticks; peer-status changes how those
ticks are drawn:
| `peer-status` | Tick style |
|---|---|
| `unreviewed` (default) | Plain solid ticks. |
| `under-review` | Solid ticks with a small unfilled circle (`r=1`) just outside the outermost tick — "in flight" mark. |
| `peer-reviewed` | Solid ticks with a single horizontal bar above the outer roundel arc. |
| `published` | Solid ticks bracketed by two short vertical marks at ±15° on the outer roundel — a printer's bracket. |
| `retracted` | Solid ticks struck through with a horizontal line `stroke-width="1.5"` across the tick group. |
The reading order on the figure thus becomes: outer ring (stability +
peer-status) communicates *external* standing; inner shape
(polygon + trust + result-shape) communicates *internal* claim.
Cleanly factored.
#### Compact-row rendering
In addition to modulating the figure, `peer-status` adds a compact
chip to the existing epistemic-block primary row, alongside `status`
and the trust chip:
88% trust · Durable · under review · 80% confidence · ●●●○○ importance · …
Rendered for any non-`unreviewed` value. The label uses the
hyphen-stripped form (`under review`, `peer-reviewed`, `published`,
`retracted`).
### 4.2 `result-shape`
Captures the *shape* of the piece's central claim. This is missing
from the current vocabulary and surfaces information that's currently
buried in the abstract.
| Value | Meaning | Center glyph |
|---|---|---|
| `positive` | Argues for or proves something works. | `+` |
| `negative` | Argues against or proves a barrier. | `` |
| `mixed` | Both positive and negative results coexist (e.g. *Branch-Based Local Capture*'s "double pincer"). | `±` |
| `comparative` | Compares two or more approaches. | `` |
| `descriptive` | Describes a system, observation, or position without arguing for or against. | `□` |
The glyph appears immediately to the right of the trust score, in
Spectral, font-size 16, vertically centered on the trust number. When
omitted, no glyph is drawn (the trust number sits alone).
#### Compact-row rendering
`result-shape` adds nothing to the compact row. The character is small
enough that the figure carries it without competing with the chip
sequence. If omitted, the figure simply renders the trust number alone.
### 4.3 `confidence: proved` (and `proven`)
Formal mathematical results don't have credences in the same sense
that essays do. A theorem with a complete proof has confidence
~100 modulo soundness, but writing `confidence: 95` invites a
false-precision reading. The colophon's commitment to honest
epistemic accounting requires a way to opt out.
The exception:
confidence: proved
(or the equivalent `proven` — both forms accepted) does three things:
1. The trust score is computed as `100 × 0.6 + ((evidence-1)/4) × 100 × 0.4`,
i.e. as if `confidence` were 100. Evidence still varies; trust is
not pinned to 100.
2. The confidence axis on the figure is drawn full-length, with a
small distinct cap at the vertex: a 3×3-px filled square (instead
of the usual 2-px circle). The square is the visual marker that
reads "this is not a credence, it is a proof-completeness flag."
3. The compact row renders `proved confidence` instead of the
`XX% confidence` form.
`confidence-history` is incompatible with `confidence: proved`. If
both are set, the build emits a warning and `confidence-history` is
ignored (a proof either is or is not; tracking history of a
binary-after-the-fact value is incoherent).
This is the *only* genre-specific carve-out in the schema. All other
fields read across genres without modification, with the colophon
gloss in §6 explaining the cross-genre interpretation.
### 4.4 `subtitle`
Captures a short secondary title shown below the main `title` in the
center column of the new three-column header (§7.2). The field is
optional and free-form.
subtitle: "A Static Site Framework"
When omitted, no subtitle line is rendered and the byline collapses
upward against the title. The subtitle is *not* an abstract; abstracts
remain in the existing `abstract:` field and render below the byline.
Subtitles do not feed the epistemic figure or any audit metric; they
are purely a presentation field. They render in print and do not
participate in any focus-mode hiding.
---
## 5. Frontmatter layout
The combined frontmatter for an essay using all features:
---
title: "The Title"
subtitle: "An Optional Secondary Line"
date: 2026-05-07
abstract: >
One-paragraph description.
tags:
- research/mathematics
# existing epistemic
status: "Durable"
confidence: 80
importance: 3
evidence: 5
scope: average
novelty: moderate
practicality: moderate
confidence-history: [60, 70, 80]
# new
peer-status: under-review
result-shape: mixed
---
For a formal-mathematics piece using `confidence: proved`:
---
title: "Branch-Based Local Capture in Tree-Ball Geometry"
status: "Durable"
confidence: proved
importance: 3
evidence: 5
scope: average
novelty: idiosyncratic
practicality: low
peer-status: under-review
result-shape: mixed
---
The monogram lives outside frontmatter, in
`content/essays/branch-based-local-capture-in-tree-balls/mark.svg`.
---
## 6. Colophon gloss
The colophon's *Living Documents* section is updated to add a
paragraph documenting genre-specific reading of the existing
fields. Proposed text (to be inserted before the field list):
> The epistemic vocabulary above is genre-general but reads
> differently across genres. For a personal essay, `confidence`
> reflects credence in a thesis — "I might change my mind." For an
> empirical research paper, it reflects expected generalization —
> "this would replicate." For formal mathematics, it reflects
> credence in proof correctness, with a special value `proved`
> available for theorems with complete proofs (where any numeric
> value would be false precision). `evidence` reads analogously: the
> strength of arguments and supporting writing in essays, the
> empirical base in research, the structure of the proof in
> mathematics. The fields are the same; the interpretive frame
> shifts with the work.
Two new field rows are appended to the existing field list:
> **Peer status** — the external review state, distinct from
> `status` (which is the author's internal position). Values:
> *unreviewed* (default), *under review*, *peer reviewed*,
> *published*, *retracted*. This information modulates the outer
> ring of the epistemic figure; a *retracted* piece is also rendered
> with the field name struck through.
>
> **Result shape** — the shape of the central claim: *positive*
> (argues something works), *negative* (argues something does not),
> *mixed* (both, as in a double-pincer barrier paper), *comparative*
> (compares approaches), or *descriptive* (describes without arguing
> for or against). Encoded as a small glyph beside the trust score
> on the epistemic figure.
---
## 7. Pandoc filter and template integration
### 7.1 New Haskell module
A new module `build/Filters/Mark.hs` exports two functions:
-- | Render the monogram inline. Reads from disk; substitutes
-- black/#000000 fills and strokes with currentColor; strips
-- width/height attributes from <svg>; wraps in <figure>.
-- Returns an empty document fragment if the file is absent.
renderMonogram :: FilePath -> Compiler Html
-- | Build the epistemic figure SVG from a Context.
-- Reads exactly the fields listed in §3.2.
-- Returns Nothing when `status` is absent.
renderEpistemicFigure :: EpistemicFields -> Maybe Html
`EpistemicFields` is a small record type that reuses the parsing
logic already in `Contexts.hs` for the existing block. The figure
generator is a pure function from this record to SVG markup.
The filter is wired into `build/Compilers.hs` as the last step of
the AST transformation, so it runs after the existing image,
sidenote, and citation passes. It produces no AST mutation; instead,
the rendered SVG is added to the page Context as two new fields:
monogramSvg -- inline SVG or empty
epistemicSvg -- inline SVG or empty
These are referenced in templates as `$monogramSvg$` and
`$epistemicSvg$`.
### 7.2 Template change
The current `templates/partials/metadata.html` carries everything
between the title and the cursive-L divider: tags, keywords, abstract,
byline, affiliation, the compact epistemic strip, and the page-nav
links. To make room for the three-column header without losing the
ordering or the divider, the partial is split in two:
- `templates/partials/metadata-header.html` — byline, abstract, and
the compact epistemic strip. Renders inside the center column of
the new header.
- `templates/partials/metadata-tail.html` — tags, keywords,
affiliation, page-nav, in that exact order (matching the current
`metadata.html` rendering order). Renders as a row beneath the
three-column header, above the cursive-L divider.
`templates/essay.html` and `templates/blog-post.html` then become:
<header class="essay-frontmatter">
<div class="frontmatter-mark frontmatter-mark--monogram">$monogramSvg$</div>
<div class="frontmatter-title">
<h1 class="page-title">$title$</h1>
$if(subtitle)$<p class="essay-subtitle">$subtitle$</p>$endif$
$partial("templates/partials/metadata-header.html")$
</div>
<div class="frontmatter-mark frontmatter-mark--epistemic">
<a href="#epistemic" aria-label="Jump to epistemic profile">$epistemicSvg$</a>
</div>
</header>
$partial("templates/partials/metadata-tail.html")$
<div class="content-divider" aria-hidden="true">
<a href="/new.html" class="content-divider-logo" aria-label="New"></a>
</div>
The cursive-L `content-divider-logo` is preserved exactly as it is
today; nothing about the frontmatter↔body separator changes. Only the
material *above* the divider is reorganized.
When either SVG is empty, the corresponding column collapses (CSS
grid, `auto` sizing). When both are absent, the header degrades to a
single-column layout that visually matches the existing one
(title + subtitle + metadata-header), so existing pages render
identically until they opt in.
`reading.html` (poetry/fiction) does NOT receive the figure column,
since these content types omit the epistemic block by design. They
do receive the monogram column when a `mark.svg` is present, plus the
`subtitle` field if set.
`pageCtx` (standalone pages) receives neither column but does honor
`subtitle` if set.
### 7.3 CSS
A new file `static/css/marks.css` defines the grid layout, the
collapse behavior, the print rules, the focus-mode hiding, and the
two `.frontmatter-mark` modifiers. It loads with the rest of the
stylesheet bundle; no new HTTP request.
The breakpoint at which the figure column drops below the title
(rather than sitting beside it) is the existing narrow-screen
breakpoint where sidenotes collapse to footnotes. A reader on
mobile sees: monogram → title → figure, stacked.
---
## 8. Build behavior
### 8.1 Determinism
Both monograms and epistemic figures must be deterministic at build
time. The monogram is just a file read; the epistemic figure is a
pure function of frontmatter and `git log --follow`. Two consecutive
builds of the same content tree must produce byte-identical SVGs.
This is enforced by:
- No timestamps in generated SVGs.
- No floating-point coordinates beyond two decimal places.
- Stable ordering of attributes (alphabetical) and elements
(declaration order).
- No build-time UUIDs or random IDs (use deterministic IDs derived
from slug, e.g. `id="mark-title-{slug}"`).
This matters for the GPG signing pipeline (`make sign`): a
non-deterministic SVG would invalidate page signatures across
otherwise-identical builds.
### 8.2 Performance
Reading 200 small SVG files at build is negligible. Rendering 200
epistemic figures is a few hundred lines of string concatenation each
and well within the existing build budget. No new build step is
needed; the work happens inside `Compilers.hs` alongside existing
filter passes.
The Hakyll dependency tracking already keys on frontmatter changes
via the existing essay context. Adding `monogramSvg` and
`epistemicSvg` to the same context propagates dependency tracking
for free: editing a frontmatter field invalidates the page; replacing
a `mark.svg` invalidates only that page's dependencies (Hakyll's
file-watch already tracks `content/**`).
### 8.3 Failure modes
| Condition | Build behavior |
|---|---|
| `mark.svg` absent | Monogram column collapses; no warning. |
| `mark.svg` malformed XML | Warn; render the slot empty; do not fail the build. |
| `mark.svg` exceeds 8 KiB | Warn; render anyway. |
| `mark.svg` violates §2.2 contract | Warn (with specific violation); render anyway. |
| `status:` absent | Epistemic column collapses; no warning. |
| `status:` set, `confidence` missing | Render figure; confidence axis has no vertex point. |
| `peer-status:` invalid value | Warn; treat as `unreviewed`. |
| `result-shape:` invalid value | Warn; render figure without center glyph. |
| `confidence: proved` and `confidence-history:` both set | Warn; ignore `confidence-history`. |
Warnings go to stderr during `make build`. They are captured and
surfaced on `/build/` (§9).
### 8.4 Backwards compatibility
Every existing essay must render identically after this change is
deployed, until and unless the author edits the file to add a
`mark.svg` or new frontmatter fields. The new template grid must
collapse to the existing single-column layout when both
`$monogramSvg$` and `$epistemicSvg$` are empty; CSS feature-tests
for grid fallback are not needed because the existing template uses
flexbox/block already.
A pre-merge regression test runs `make build` on a snapshot of
`content/` from before the change and diffs `_site/` against a
known-good snapshot. The only allowed diffs are template-driven
whitespace.
---
## 9. Audit and telemetry
### 9.1 `make audit-marks`
A new build target lists pieces missing one or both marks. Output
columns: path, has-monogram?, has-epistemic-figure?, suggested
action.
$ make audit-marks
content/essays/ozymandias.md ✓ ✓
content/essays/branch-based-...md ✗ ✗ add mark.svg, set status:
content/essays/beyond-comorbidity-... ✗ ✓ add mark.svg
...
Implementation: `tools/audit-marks.py` walks `content/**/*.md`,
parses YAML frontmatter, checks for the corresponding `mark.svg`,
checks whether `status:` is set, and emits the table.
The script also emits two summary metrics: corpus monogram coverage
percentage and corpus epistemic-figure coverage percentage.
### 9.2 `/build/` integration
The existing build telemetry page already includes "epistemic
coverage" per the WRITING.md auto-generated-pages list. Two new
sub-sections are added to that page:
- **Monogram coverage**: count and percentage of essays/blog/poetry/
fiction/music with `mark.svg` present, broken down by portal.
- **Epistemic-figure coverage**: count and percentage of pieces
with `status:` set and a renderable figure, broken down by portal.
The same Stats.hs module that produces existing coverage figures
extends to compute these. No new external dependencies.
### 9.3 Linting hook
A pre-commit hook (`tools/hooks/pre-commit-marks.sh`) runs
`make audit-marks` and warns on any new `.md` file under
`content/essays/` or `content/research/` (effectively, anything
tagged `research/*` or in those directories) added without a
`mark.svg` or with `status:` unset. Warning only; does not block
the commit. Authors who genuinely want to publish without marks
can ignore the warning.
---
## 10. Migration
### Phase 1 — Wire the system, no content (1 build)
- Land `Filters/Mark.hs`, the template changes, and `static/css/marks.css`.
- Land `tools/audit-marks.py`.
- Land the two new schema fields (`peer-status`, `result-shape`)
and the `confidence: proved` exception in `Contexts.hs` and
`Stability.hs`.
- Update `WRITING.md` with the new fields and the `mark.svg`
convention.
- Update the colophon with the §6 gloss.
- Build. Every existing page renders identically (§8.4).
### Phase 2 — Reference monograms (1 week of evenings)
- Author monograms for the 810 most-trafficked pieces (likely:
*Colophon*, *Memento Mori*, *Ozymandias*, *Beyond Comorbidity Indices*,
*Branch-Based Local Capture*, the *Music* index, the *Library* portal
landing, and a poetry collection landing).
- Author the reference monogram template at
`static/templates/mark-template.svg`.
- Validate them against §2.2 with `make audit-marks`.
### Phase 3 — Backfill epistemic fields (incremental)
- For each piece in `research/` and `nonfiction/`, decide whether to
add `status:`, `peer-status:`, `result-shape:`. The audit script
surfaces the candidates.
- Specifically: *Branch-Based Local Capture* gets `status: Durable`,
`confidence: proved`, `evidence: 5`, `peer-status: unreviewed`,
`result-shape: mixed`. *Beyond Comorbidity Indices* gets
`peer-status: under-review` and `result-shape: comparative`
added.
### Phase 4 — Iterate
- Once 30+ marks exist, review the corpus as a system. Tighten
§2.2 constraints if cross-mark consistency is weaker than expected.
Loosen if the constraints are pinching authorship.
- Decide whether portal-level base monograms (the
Approach 5 idea) are worth adding as a third tier.
---
## 11. Rejected alternatives
These were considered and not adopted; recording them so future
revisions don't relitigate.
- **Two figure types (essay vs. research badge).** The existing
fields handle both genres when read with appropriate gloss
(§6). Two figures would create a visual fork that costs more
than it pays. *Beyond Comorbidity Indices* is the proof point:
formal research already renders cleanly with the existing
vocabulary.
- **Mode-switched figure with axes that change meaning per genre.**
Visual grammar should be unambiguous. A single radial figure
where the axes mean different things depending on a frontmatter
flag would require footnotes to read. Genre-gloss in the colophon
handles the same need without ambiguity.
- **Auto-derived monograms from semantic-search embeddings.** Tried
in spec drafting. The result is generic and lacks the editorial
statement that a hand-authored monogram makes. Authors may use
AI-assist tools to generate monograms (against §2.2 contract),
but the system does not derive them automatically.
- **Ghosted axes for missing fields.** Tested visually. Reads as a
bug, not a deliberate position. Better to suppress the figure
entirely (§3.1) and surface the absence in `/build/` (§9).
- **A separate `claim-mode: formal | empirical | essay` field.**
Solved the wrong problem. The fields don't need a mode flag; they
need a gloss. The two new fields (`peer-status`, `result-shape`)
plus the `confidence: proved` exception cover the genre-specific
needs surfaced in audit.
- **Folding `peer-status` into `status`.** Tempting but wrong.
`status` is the author's position ("I expect this to hold up").
`peer-status` is the world's position ("the field has confirmed
it"). A piece can be `Durable` and `unreviewed` simultaneously
(the author believes it; the world hasn't checked yet). Keeping
them factored preserves that distinction.
---
## 12. Open questions for review
1. **Monogram filename convention.** Spec proposes `mark.svg` (in
directory-form) and `{slug}.mark.svg` (flat-form). Alternative:
always require directory-form for any piece that wants a
monogram, simplifying the resolver. Cost: forces directory-form
migration on currently-flat essays. Recommend keeping both
forms; the resolver is small and the migration cost is real.
2. **Should `peer-status: retracted` survive `make build`?**
Currently spec'd as a normal field with a strikethrough.
Alternative: `make build` refuses to publish pieces marked
`retracted` and instead generates a tombstone page at the
original URL. Probably overkill for the personal-site context;
leaving as a normal field with visual indicator. Worth flagging.
3. **Should the figure's confidence trend arrow distinguish
"stable" (∆ ≤ 2) from "unchanged" (∆ = 0)?** Currently treats
them the same as `→`. The existing trend arrow in the epistemic
block does too. No need to diverge.
4. **Per-portal monogram defaults.** Should an absent `mark.svg`
fall back to a portal-level base monogram (e.g. all `research/`
pieces show a default research mark)? Spec says no — absence
is meaningful and surfaces in `/build/`. The visual specimen
sheets in the earlier exploration suggested portal-level
iconography is interesting; defer to a future spec.
5. **Naming.** The pair of glyphs is currently called "monogram"
and "epistemic figure." Considered alternatives: "device"
(printer's-mark lineage) and "figure" (Tufte lineage), or
"mark" and "badge" (more colloquial). Spec uses
"monogram + epistemic figure" because it most accurately
describes what each thing *is*. Open to naming bikeshed.
---
## 13. Files touched
A complete list of files this spec creates or modifies, for tracking
PR scope:
**New:**
- `build/Filters/Mark.hs`
- `tools/audit-marks.py`
- `tools/hooks/pre-commit-marks.sh`
- `static/css/marks.css`
- `static/templates/mark-template.svg`
- `static/templates/epistemic-figure-reference.svg`
- `MARKS.md` (this file, after merge)
**Modified:**
- `build/Compilers.hs` (expose new context fields where essay /
blog / reading / page contexts are assembled)
- `build/Contexts.hs` (parse `subtitle`, `peer-status`,
`result-shape`, `confidence: proved`; produce `monogramSvg` and
`epistemicSvg` context fields; render the inline trend arrow
inside the compact-row `confidence` chip)
- `build/Stability.hs` (consume `peer-status` for tick styling
if rendering moves out of pure SVG generator)
- `build/Stats.hs` (monogram + epistemic-figure coverage on
`/build/`)
- `templates/essay.html`
- `templates/blog-post.html`
- `templates/reading.html` (monogram column + `subtitle`; no figure)
- `templates/partials/metadata.html` (split into the two new
partials below; this file becomes a thin shim or is removed)
- `templates/partials/metadata-header.html` (new — center-column
metadata: byline, abstract, compact epistemic strip)
- `templates/partials/metadata-tail.html` (new — row beneath the
three-column header: tags, keywords, affiliation, page-nav, in
that order)
- `Makefile` (`audit-marks` target)
- `WRITING.md` (new fields including `subtitle`; monogram convention)
- `content/colophon.md` (genre gloss)
**Per-essay (Phase 2+):**
- `content/essays/{slug}/mark.svg` × N (hand-authored monograms)
- frontmatter edits to add `peer-status:`, `result-shape:`,
`confidence: proved` where applicable.
---
## 14. Future work (out of scope for the initial rollout)
These extensions are explicitly deferred. They are recorded here so
that the structural decisions in §§29 do not foreclose them.
- **Monogram in hyperlink popup previews.** The existing on-hover
page-preview popup (which already renders title and abstract for
internal links) should display the monogram alongside the title
when one exists. The popup is the smallest place a reader meets a
page; the monogram earns its keep there.
- **Monogram in `/library/` and `/new/` feed listings.** Both the
library portal and the recent-changes feed render lists of pages.
Once monogram coverage is non-trivial (Phase 2 ships ≥10), each
list item should render the monogram as a small inline glyph
beside the title.
- **Portal-level base monograms.** Deferred per §12.4, but the
correct natural place to introduce them is once the popup and
feed-listing wiring is in place — base monograms compensate for
list rows where the per-page monogram is absent.
These items are scoped as a follow-up PR (informally "PR 4") after
the audit-tool PR ships and after at least 10 hand-authored monograms
exist to test the popup/listing rendering against real content.

View File

@ -61,6 +61,7 @@ custom template variables.
```yaml
---
title: "The Title of the Essay"
subtitle: "An Optional Secondary Line" # optional; rendered below the title in the frontmatter header
date: 2026-03-15 # required; used for ordering, feed, and display
abstract: > # optional; shown in the metadata block and link previews
A one-paragraph description of the piece.
@ -87,7 +88,7 @@ js: scripts/my-widget.js # optional; per-page JS file (see Page scripts)
# Epistemic profile — all optional; the entire section is hidden unless `status` is set
status: "Working model" # Draft | Working model | Durable | Refined | Superseded | Deprecated
confidence: 72 # 0100 integer (%)
confidence: 72 # 0100 integer (%); also accepts the sentinel `proved` / `proven` for formal mathematical results
importance: 3 # 15 integer (rendered as filled/empty dots ●●●○○)
evidence: 2 # 15 integer (same)
scope: average # personal | local | average | broad | civilizational
@ -97,6 +98,8 @@ confidence-history: # list of integers; trend arrow derived from last two
- 55
- 63
- 72
peer-status: under-review # optional; unreviewed (default) | under-review | peer-reviewed | published | retracted
result-shape: mixed # optional; positive | negative | mixed | comparative | descriptive
# Version history — optional; falls back to git log, then to date frontmatter
history:
@ -809,6 +812,84 @@ commits or age <730 days → *stable*; otherwise → *established*.
To pin a manual stability label for one build, add the file path to `IGNORE.txt`
(one path per line). The file is cleared automatically by `make build`.
### `peer-status`, `result-shape`, and `confidence: proved`
Three optional extensions to the epistemic vocabulary, introduced
alongside the epistemic figure (`MARKS.md` for the full spec).
**`peer-status`** — the *external* review state, factored out of `status`
(which captures the author's internal position). Values:
| Value | Meaning |
|-------|---------|
| `unreviewed` | Default. No external review has taken place. |
| `under-review` | In submission or peer review. |
| `peer-reviewed` | Reviewed but not yet formally published. |
| `published` | Appeared in a peer-reviewed venue. |
| `retracted` | Formally retracted; renders with a strikethrough. |
A non-`unreviewed` value adds a chip (`under review`, `peer-reviewed`, …)
to the compact epistemic strip and modulates the outer-ring tick *style*
on the epistemic figure.
**`result-shape`** — the shape of the central claim:
| Value | Meaning |
|-------|---------|
| `positive` | Argues for / proves something works. |
| `negative` | Argues against / proves a barrier. |
| `mixed` | Both, e.g. a double-pincer barrier paper. |
| `comparative` | Compares two or more approaches. |
| `descriptive` | Describes without arguing for or against. |
Renders as a small glyph (`+`, ``, `±`, ``, `□`) beside the trust score
on the epistemic figure. Adds nothing to the compact row.
**`confidence: proved`** (or `proven`) — the formal-proof carve-out. A
theorem with a complete proof has confidence ≈ 100 modulo soundness, and
writing `confidence: 95` invites a false-precision reading. The sentinel:
* Substitutes 100 in the trust-score formula (evidence still varies, so
a complete proof with thin supporting apparatus does not pin trust to
100).
* Renders the compact-row chip as `proved confidence` instead of
`XX% confidence`.
* Draws the confidence axis on the epistemic figure full-length, with a
3×3 px filled square at the vertex (the "proof cap" marker) instead
of the usual 2 px circle.
* Suppresses the inline trend arrow. Setting both
`confidence: proved` and `confidence-history` emits a build warning
and the history is ignored.
This is the only genre-specific carve-out in the schema. All other
fields read across genres without modification.
### Frontmatter marks (monogram + epistemic figure)
Each piece may carry a hand-authored monogram — a small SVG glyph
abstracted from its central concept — that renders in the left column of
the frontmatter header. Drop a `mark.svg` next to the source file; the
build picks it up automatically. No frontmatter key is required to opt
in.
| Source path | Mark path |
|-------------|-----------|
| `content/essays/foo.md` | `content/essays/foo.mark.svg` |
| `content/essays/foo/index.md` | `content/essays/foo/mark.svg` |
| `content/blog/post.md` | `content/blog/post.mark.svg` |
| `content/{slug}.md` | `content/{slug}.mark.svg` |
The right column of the header carries the **epistemic figure** — a
build-time SVG generated deterministically from the existing epistemic
fields. It renders only when `status:` is set, matching the existing
visibility rule for the epistemic block. See `MARKS.md` for the full
specification (visual contract, accessibility, geometry).
A reference monogram template lives at
`static/templates/mark-template.svg`. Authors hand-rolling a monogram
should copy and adapt it; the file documents the §2.2 visual contract
(currentColor strokes, no embedded text, ≤ 8 KiB, etc.).
---
## Version history

View File

@ -29,6 +29,7 @@ import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Key as AK
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Vector as V
import Data.Char (toLower)
import Data.List (intercalate, isPrefixOf, sortBy)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Ord (comparing)
@ -38,6 +39,7 @@ import Data.Time.Clock (UTCTime, getCurrentTime, utctDay)
import Data.Time.Format (formatTime, defaultTimeLocale, parseTimeM)
import System.Directory (doesFileExist)
import System.FilePath (takeDirectory, takeFileName, (</>))
import System.IO (hPutStrLn, stderr)
import Text.Read (readMaybe)
import qualified Data.Text as T
import qualified Data.Yaml as Y
@ -46,6 +48,7 @@ import Text.Pandoc.Options (WriterOptions(..), HTMLMathMethod(..))
import Hakyll hiding (trim)
import Backlinks (backlinksField)
import Dingbat (dingbatField)
import Marks (monogramSvgField, epistemicSvgField)
import SimilarLinks (similarLinksField)
import Stability (stabilityField, lastReviewedField, lastReviewedIsoField,
versionHistoryField,
@ -54,6 +57,13 @@ import Stability (stabilityField, lastReviewedField, lastReviewedIsoField,
versionHistoryRangeEndField, versionHistoryCommitsField)
import Utils (authorSlugify, authorNameOf, trim)
-- | Returns 'True' when the @confidence:@ frontmatter value is the
-- "proved" / "proven" sentinel — the §4.3 carve-out for formal proofs
-- that opt out of a numeric credence. Case-insensitive.
isProvedConfidence :: Maybe String -> Bool
isProvedConfidence (Just s) = map toLower (trim s) `elem` ["proved", "proven"]
isProvedConfidence _ = False
-- ---------------------------------------------------------------------------
-- Affiliation field
-- ---------------------------------------------------------------------------
@ -426,6 +436,7 @@ siteCtx =
<> descriptionField
<> summaryField
<> dingbatField
<> monogramSvgField
<> defaultContext
-- ---------------------------------------------------------------------------
@ -485,10 +496,24 @@ dotsField ctxKey metaKey = field ctxKey $ \item -> do
--
-- The arrow flips when the absolute change crosses 'trendThreshold'
-- (currently 5 percentage points). Smaller swings count as flat.
--
-- When @confidence: proved@ (or @proven@) is in effect, the arrow is
-- suppressed: a proof either holds or it does not, so tracking trend on
-- a binary-after-the-fact value is incoherent (MARKS.md §4.3). If the
-- frontmatter sets both @confidence: proved@ and @confidence-history:@
-- the build emits a warning and the history is ignored.
confidenceTrendField :: Context String
confidenceTrendField = field "confidence-trend" $ \item -> do
meta <- getMetadata (itemIdentifier item)
if isProvedConfidence (lookupString "confidence" meta)
then do
case lookupStringList "confidence-history" meta of
Just _ -> unsafeCompiler $ hPutStrLn stderr $
"[Marks] " ++ toFilePath (itemIdentifier item) ++
": confidence: proved is incompatible with confidence-history; ignoring history"
Nothing -> return ()
fail "confidence is proved; trend suppressed"
else case lookupStringList "confidence-history" meta of
Nothing -> fail "no confidence history"
Just xs -> case lastTwo xs of
Nothing -> fail "no confidence history"
@ -534,11 +559,21 @@ confidenceTrendField = field "confidence-trend" $ \item -> do
--
-- Formula: raw = conf/100 · 0.6 + (ev 1)/4 · 0.4 (01)
-- score = clamp₀₋₁₀₀(round(raw · 100))
--
-- The @confidence: proved@ (or @proven@) sentinel — see MARKS.md §4.3 —
-- substitutes @conf = 100@ in the formula. Evidence still varies, so
-- trust is not pinned to 100; a complete proof with weak supporting
-- apparatus (evidence=1) lands at 60, the same as a numeric
-- confidence=100, evidence=1 entry would.
overallScoreField :: Context String
overallScoreField = field "overall-score" $ \item -> do
meta <- getMetadata (itemIdentifier item)
let readInt s = readMaybe s :: Maybe Int
case ( readInt =<< lookupString "confidence" meta
confRaw = lookupString "confidence" meta
confInt = if isProvedConfidence confRaw
then Just 100
else readInt =<< confRaw
case ( confInt
, readInt =<< lookupString "evidence" meta
) of
(Just conf, Just ev) ->
@ -549,16 +584,106 @@ overallScoreField = field "overall-score" $ \item -> do
in return (show score)
_ -> fail "overall-score: confidence or evidence not set"
-- | @$confidence$@: numeric override that suppresses the @proved@ /
-- @proven@ sentinel. When the frontmatter value is parseable as an
-- integer this returns its 'show' form; otherwise 'noResult' so the
-- template's @$if(confidence)$@ guard collapses cleanly. The sentinel
-- case is surfaced via 'confidenceProvedField' instead.
--
-- Composed before 'defaultContext' so this override wins; without it
-- @$confidence$@ would render the literal string @"proved"@ and the
-- template's @$confidence$% confidence@ would print @"proved%
-- confidence"@.
confidenceField :: Context String
confidenceField = field "confidence" $ \item -> do
meta <- getMetadata (itemIdentifier item)
case lookupString "confidence" meta of
Nothing -> noResult "no confidence"
Just s -> case readMaybe (trim s) :: Maybe Int of
Just n -> return (show n)
Nothing -> noResult "confidence not numeric"
-- | @$confidence-proved$@: present (renders as @"true"@) when
-- @confidence:@ is the @proved@ / @proven@ sentinel; 'noResult'
-- otherwise. Templates branch on this to render @"proved confidence"@
-- in place of the @"XX% confidence"@ chip.
confidenceProvedField :: Context String
confidenceProvedField = field "confidence-proved" $ \item -> do
meta <- getMetadata (itemIdentifier item)
if isProvedConfidence (lookupString "confidence" meta)
then return "true"
else noResult "confidence is not proved"
-- | @$peer-status$@: validated raw value (slug form) from the
-- @peer-status:@ frontmatter. Used by the template as a class-attribute
-- modifier (@ep-peer-status--retracted@ etc.). Invalid values warn and
-- degrade to 'noResult', so a typo doesn't render an unstyled chip.
-- Absent and @unreviewed@ both produce 'noResult' — the chip is the
-- exception, not the default.
peerStatusField :: Context String
peerStatusField = field "peer-status" $ \item -> do
meta <- getMetadata (itemIdentifier item)
case lookupString "peer-status" meta of
Nothing -> noResult "no peer-status"
Just raw ->
let s = map toLower (trim raw)
in if s `elem` knownPeerStatuses
then if s == "unreviewed"
then noResult "peer-status is unreviewed (default)"
else return s
else do
unsafeCompiler $ hPutStrLn stderr $
"[Marks] " ++ toFilePath (itemIdentifier item) ++
": invalid peer-status value \"" ++ raw ++
"\"; treating as unreviewed"
noResult "invalid peer-status"
where
knownPeerStatuses = ["unreviewed", "under-review", "peer-reviewed",
"published", "retracted"]
-- | @$peer-status-display$@: human-readable form of the @peer-status@
-- value, suitable for the compact-row chip text. Per MARKS.md §4.1 the
-- display strings are:
--
-- * @under-review@ → @"under review"@ (hyphen → space)
-- * @peer-reviewed@ → @"peer-reviewed"@ (kept as-is)
-- * @published@ → @"published"@
-- * @retracted@ → @"retracted"@
--
-- 'noResult' for absent, invalid, or @unreviewed@ values, mirroring
-- 'peerStatusField'.
peerStatusDisplayField :: Context String
peerStatusDisplayField = field "peer-status-display" $ \item -> do
meta <- getMetadata (itemIdentifier item)
case lookupString "peer-status" meta of
Nothing -> noResult "no peer-status"
Just raw ->
case lookup (map toLower (trim raw)) displayMap of
Just disp -> return disp
Nothing -> noResult "no display form (absent / unreviewed / invalid)"
where
displayMap =
[ ("under-review", "under review")
, ("peer-reviewed", "peer-reviewed")
, ("published", "published")
, ("retracted", "retracted")
]
-- | All epistemic context fields composed.
epistemicCtx :: Context String
epistemicCtx =
dotsField "importance-dots" "importance"
<> dotsField "evidence-dots" "evidence"
<> confidenceField
<> confidenceProvedField
<> peerStatusField
<> peerStatusDisplayField
<> overallScoreField
<> confidenceTrendField
<> stabilityField
<> lastReviewedField
<> lastReviewedIsoField
<> epistemicSvgField
-- ---------------------------------------------------------------------------
-- Essay context
@ -742,6 +867,23 @@ postCtx =
<> dateField "date" "%-d %B %Y"
<> dateField "date-iso" "%Y-%m-%d"
<> constField "math" "true"
-- Blog posts can opt in to the epistemic figure / chips by setting
-- the relevant frontmatter fields. The Marks module's epistemic SVG
-- field returns 'noResult' when @status:@ is absent, so unstatused
-- posts render unchanged. The dot / strip fields below mirror the
-- essay context so a status-bearing post gets the same chips.
<> dotsField "importance-dots" "importance"
<> dotsField "evidence-dots" "evidence"
<> confidenceField
<> confidenceProvedField
<> peerStatusField
<> peerStatusDisplayField
<> overallScoreField
<> confidenceTrendField
<> stabilityField
<> lastReviewedField
<> lastReviewedIsoField
<> epistemicSvgField
<> siteCtx
-- ---------------------------------------------------------------------------

562
build/Marks.hs Normal file
View File

@ -0,0 +1,562 @@
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Frontmatter marks: the monogram (a hand-authored SVG per piece) and
-- the epistemic figure (a build-time SVG generated from frontmatter).
-- See MARKS.md for the full specification.
--
-- Two Hakyll context fields are exported:
--
-- * @$monogramSvg$@ — the inlined monogram for the current item, or
-- 'noResult' when no co-located @mark.svg@ exists.
-- * @$epistemicSvg$@ — the generated epistemic figure, or 'noResult'
-- when the item has no @status:@ frontmatter
-- (MARKS.md §3.1).
--
-- Both fields are deterministic: byte-identical inputs produce
-- byte-identical SVGs, so the GPG signing pipeline is undisturbed.
module Marks
( monogramSvgField
, epistemicSvgField
) where
import Control.Exception (IOException, try)
import Data.Char (toLower)
import Data.Maybe (catMaybes, isJust)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Numeric (showFFloat)
import System.Directory (doesFileExist)
import System.FilePath (takeBaseName, takeDirectory,
takeFileName, (</>))
import System.IO (hPutStrLn, stderr)
import Text.Read (readMaybe)
import Hakyll
import Stability (resolveStability)
-- ---------------------------------------------------------------------------
-- Monogram path resolution
-- ---------------------------------------------------------------------------
-- | Candidate monogram paths for a given source path. The build picks the
-- first one that exists on disk. This dual-form resolver matches the
-- site's mixed flat / directory essay convention:
--
-- > content/essays/foo.md → content/essays/foo.mark.svg
-- > content/essays/foo/index.md → content/essays/foo/mark.svg
monogramCandidates :: FilePath -> [FilePath]
monogramCandidates fp =
let dir = takeDirectory fp
fname = takeFileName fp
in if fname == "index.md"
then [dir </> "mark.svg"]
else [dir </> takeBaseName fp ++ ".mark.svg"]
-- | Return the first candidate path that exists on disk, or 'Nothing'.
resolveMonogramPath :: Item a -> Compiler (Maybe FilePath)
resolveMonogramPath item =
unsafeCompiler $ firstExisting (monogramCandidates fp)
where
fp = toFilePath (itemIdentifier item)
firstExisting [] = return Nothing
firstExisting (p:ps) = do
e <- doesFileExist p
if e then return (Just p) else firstExisting ps
-- ---------------------------------------------------------------------------
-- Monogram inlining
-- ---------------------------------------------------------------------------
-- | @$monogramSvg$@. Reads the resolved @mark.svg@, normalizes black
-- fills/strokes to @currentColor@ (defensive — authors using AI-assist
-- tools may produce hardcoded blacks; the contract still holds), strips
-- the @width@/@height@ presentation attributes from the root @<svg>@,
-- and wraps the result in @<figure class="frontmatter-mark
-- frontmatter-mark--monogram">@. Returns 'noResult' when no candidate
-- exists; warns and returns 'noResult' on read failure.
monogramSvgField :: Context String
monogramSvgField = field "monogramSvg" $ \item -> do
mPath <- resolveMonogramPath item
case mPath of
Nothing -> noResult "no mark.svg"
Just path -> do
result <- unsafeCompiler $ try (TIO.readFile path)
:: Compiler (Either IOException T.Text)
case result of
Left e -> do
unsafeCompiler $ hPutStrLn stderr $
"[Marks] " ++ toFilePath (itemIdentifier item) ++
": failed to read " ++ path ++ ": " ++ show e
noResult "monogram read failed"
Right svg -> return $ T.unpack $ wrapMonogram (processSvg svg)
-- | Wrap inlined monogram SVG in its outer figure element.
wrapMonogram :: T.Text -> T.Text
wrapMonogram svg = T.concat
[ "<figure class=\"frontmatter-mark frontmatter-mark--monogram\">"
, svg
, "</figure>"
]
-- | Replace hardcoded black fills/strokes with @currentColor@ and strip
-- the root @<svg>@'s @width@/@height@ attributes (presentation lives
-- in CSS via the @.frontmatter-mark svg@ selector). Mirrors the color
-- substitution in 'Filters.Score.processColors' so the two SVG
-- inliners agree on the contract.
processSvg :: T.Text -> T.Text
processSvg = stripRootDims . normalizeColors
-- | The same chain 'Filters.Score' applies, kept in sync deliberately.
-- 6-digit patterns first so the 3-digit replacement doesn't match
-- the prefix of a 6-digit value.
normalizeColors :: T.Text -> T.Text
normalizeColors
= T.replace "fill=\"#000\"" "fill=\"currentColor\""
. T.replace "fill=\"black\"" "fill=\"currentColor\""
. T.replace "stroke=\"#000\"" "stroke=\"currentColor\""
. T.replace "stroke=\"black\"" "stroke=\"currentColor\""
. T.replace "fill:#000" "fill:currentColor"
. T.replace "fill:black" "fill:currentColor"
. T.replace "stroke:#000" "stroke:currentColor"
. T.replace "stroke:black" "stroke:currentColor"
. T.replace "fill=\"#000000\"" "fill=\"currentColor\""
. T.replace "stroke=\"#000000\"" "stroke=\"currentColor\""
. T.replace "fill:#000000" "fill:currentColor"
. T.replace "stroke:#000000" "stroke:currentColor"
-- | Remove @width="..."@ and @height="..."@ from the root @<svg>@.
-- The substitution is conservative: it walks once and only touches
-- the first occurrence of each attribute (the root tag in a
-- well-formed monogram).
stripRootDims :: T.Text -> T.Text
stripRootDims = stripFirst "width" . stripFirst "height"
where
stripFirst attr txt =
case T.breakOn (T.pack (" " ++ attr ++ "=\"")) txt of
(before, after)
| T.null after -> txt
| otherwise ->
-- Drop ` attr="..."` including its closing quote.
let restAfterEq = T.drop (T.length (T.pack (" " ++ attr ++ "=\""))) after
in case T.breakOn "\"" restAfterEq of
(_, rest) | T.null rest -> txt
| otherwise -> before <> T.drop 1 rest
-- ---------------------------------------------------------------------------
-- Epistemic figure: data extraction
-- ---------------------------------------------------------------------------
-- | Captures the frontmatter inputs the figure consumes. Constructed
-- once per item by 'readEpistemicData', then handed to the pure
-- geometry below. Keeps the I/O step (metadata + git) separate from
-- the SVG-string formatter, so the formatter is testable in isolation
-- without mocking Hakyll.
data EpistemicData = EpistemicData
{ epConfidence :: Maybe Int -- ^ Numeric confidence; 'Nothing' if proved/absent/unparseable.
, epConfidenceProved :: Bool -- ^ True when @confidence: proved@ / @proven@.
, epImportance :: Maybe Int -- ^ 15 ordinal.
, epEvidence :: Maybe Int -- ^ 15 ordinal.
, epScope :: Maybe String -- ^ Validated scope value.
, epNovelty :: Maybe String -- ^ Validated novelty value.
, epPracticality :: Maybe String -- ^ Validated practicality value.
, epPeerStatus :: Maybe String -- ^ Validated peer-status slug ('Nothing' when absent / unreviewed / invalid).
, epResultShape :: Maybe String -- ^ Validated result-shape value.
, epStability :: String -- ^ Always one of the five stability labels.
, epTrust :: Int -- ^ Trust score 0100 (60/40 weighted; @proved@ substitutes 100 for confidence).
}
-- | Read the figure inputs from a Hakyll item's metadata + git history.
readEpistemicData :: Item a -> Compiler EpistemicData
readEpistemicData item = do
meta <- getMetadata (itemIdentifier item)
stab <- resolveStability item
let confRaw = lookupString "confidence" meta
proved = isProvedConfidenceM confRaw
confInt = if proved then Just 100 else readMaybe . trimS =<< confRaw
confNumeric = if proved then Nothing else confInt
importance = readMaybe . trimS =<< lookupString "importance" meta
evidence = readMaybe . trimS =<< lookupString "evidence" meta
scope = validate scopeValues =<< lookupString "scope" meta
novelty = validate noveltyValues =<< lookupString "novelty" meta
practical = validate practicalityValues =<< lookupString "practicality" meta
peer = validatePeerStatus =<< lookupString "peer-status" meta
resultShape = validate resultShapeValues =<< lookupString "result-shape" meta
trust = computeTrust confInt evidence
return EpistemicData
{ epConfidence = confNumeric
, epConfidenceProved = proved
, epImportance = importance
, epEvidence = evidence
, epScope = scope
, epNovelty = novelty
, epPracticality = practical
, epPeerStatus = peer
, epResultShape = resultShape
, epStability = stab
, epTrust = trust
}
where
trimS = trim'
-- | Trust score: the same 60/40 weighted composite of confidence and
-- evidence used by 'Contexts.overallScoreField'. Returns 0 when either
-- input is missing — which is fine for the figure (the polygon and
-- trust label simply collapse to the bare frame).
computeTrust :: Maybe Int -> Maybe Int -> Int
computeTrust (Just c) (Just e) =
let raw :: Double
raw = fromIntegral c / 100.0 * 0.6 + fromIntegral (e - 1) / 4.0 * 0.4
in max 0 (min 100 (round (raw * 100.0)))
computeTrust _ _ = 0
-- | Same predicate as 'Contexts.isProvedConfidence' — local copy to keep
-- the module's dependency graph light (Marks → Stability only). The
-- two are tested against the same vocabulary; if either drifts the
-- build still warns via the schema validators in Contexts.hs.
isProvedConfidenceM :: Maybe String -> Bool
isProvedConfidenceM (Just s) = map toLower (trim' s) `elem` ["proved", "proven"]
isProvedConfidenceM _ = False
trim' :: String -> String
trim' = f . f
where f = reverse . dropWhile (`elem` (" \t\n\r" :: String))
-- | Validate a value against an enum list. Returns the lowercase form
-- on hit, 'Nothing' otherwise (no warning here — Contexts.hs's parsers
-- already warn on invalid frontmatter; the figure simply degrades).
validate :: [String] -> String -> Maybe String
validate vs raw =
let s = map toLower (trim' raw)
in if s `elem` vs then Just s else Nothing
-- | Peer-status validator: matches @peerStatusField@ in Contexts.hs but
-- maps @unreviewed@ to 'Nothing' so the figure's outer ring stays
-- neutral by default.
validatePeerStatus :: String -> Maybe String
validatePeerStatus raw =
let s = map toLower (trim' raw)
in if s `elem` ["under-review", "peer-reviewed", "published", "retracted"]
then Just s
else Nothing -- includes "unreviewed" and any unknown value
scopeValues, noveltyValues, practicalityValues, resultShapeValues :: [String]
scopeValues = ["personal", "local", "average", "broad", "civilizational"]
noveltyValues = ["conventional", "moderate", "idiosyncratic", "innovative"]
practicalityValues = ["abstract", "low", "moderate", "high", "exceptional"]
resultShapeValues = ["positive", "negative", "mixed", "comparative", "descriptive"]
-- | Map an ordinal value to its numeric rank (1-based).
ordinalRank :: [String] -> String -> Maybe Int
ordinalRank vs s = lookup s (zip vs [1..])
-- ---------------------------------------------------------------------------
-- Epistemic figure: geometry
-- ---------------------------------------------------------------------------
-- | Centre of the figure (viewBox coordinates).
fxCenter, fyCenter :: Double
fxCenter = 100
fyCenter = 100
-- | Inner / outer radii of the roundel and the polygon's full extent.
fxOuter, fxOuterPlus, fxAxisFull :: Double
fxOuter = 88 -- inner roundel circle
fxOuterPlus = 90 -- outer roundel circle
fxAxisFull = 80 -- full axis length (polygon vertex when value = 1.0)
-- | Six axis angles, clockwise from 12 o'clock, in degrees.
-- Index → field: 0 confidence, 1 novelty, 2 practicality,
-- 3 scope, 4 evidence, 5 importance.
axisAngles :: [Double]
axisAngles = [0, 60, 120, 180, 240, 300]
-- | Convert a (clockwise-angle-from-12-o'clock, distance-from-centre) pair
-- to absolute viewBox coordinates.
polar :: Double -> Double -> (Double, Double)
polar angleDeg dist =
let theta = (angleDeg - 90) * pi / 180
in (fxCenter + dist * cos theta, fyCenter + dist * sin theta)
-- | Axis index → normalized [0,1] value, or 'Nothing' when the
-- underlying frontmatter field is absent / unparseable.
axisValue :: EpistemicData -> Int -> Maybe Double
axisValue d i = case i of
0 -> if epConfidenceProved d
then Just 1.0
else fmap (\c -> fromIntegral c / 100.0) (epConfidence d)
1 -> normalizeOrdinal noveltyValues 4 (epNovelty d)
2 -> normalizeOrdinal practicalityValues 5 (epPracticality d)
3 -> normalizeOrdinal scopeValues 5 (epScope d)
4 -> normalizeIntScale 5 (epEvidence d)
5 -> normalizeIntScale 5 (epImportance d)
_ -> Nothing
-- | Map a 1..n ordinal-name value to a [0,1] value via @(rank-1)/(n-1)@.
normalizeOrdinal :: [String] -> Int -> Maybe String -> Maybe Double
normalizeOrdinal vs n (Just s) = do
r <- ordinalRank vs s
return $ fromIntegral (r - 1) / fromIntegral (n - 1)
normalizeOrdinal _ _ Nothing = Nothing
-- | Map a 1..n integer to [0,1] via @(v-1)/(n-1)@.
normalizeIntScale :: Int -> Maybe Int -> Maybe Double
normalizeIntScale n (Just v) = Just $ fromIntegral (v - 1) / fromIntegral (n - 1)
normalizeIntScale _ Nothing = Nothing
-- ---------------------------------------------------------------------------
-- Epistemic figure: SVG rendering
-- ---------------------------------------------------------------------------
-- | Format a Double with two decimal places. Determinism (§8.1) requires
-- no platform-dependent floating-point formatting.
ff :: Double -> T.Text
ff x = T.pack (showFFloat (Just 2) x "")
-- | Format a "x,y" coordinate pair.
xy :: Double -> Double -> T.Text
xy x y = ff x <> T.singleton ',' <> ff y
-- | Render the full epistemic figure SVG.
renderEpistemicFigure :: EpistemicData -> T.Text
renderEpistemicFigure d = T.concat
[ "<svg xmlns=\"http://www.w3.org/2000/svg\""
, " viewBox=\"0 0 200 200\""
, " role=\"img\""
, " aria-label=\"Epistemic figure: trust ", T.pack (show (epTrust d))
, ", stability ", T.pack (epStability d), "\">"
, renderRoundel
, renderGuides
, renderAxes
, renderPolygon d
, renderVertexMarks d
, renderTicks (epStability d) (epPeerStatus d)
, renderTrustLabel (epTrust d)
, renderResultShape (epResultShape d) (epTrust d)
, "</svg>"
]
-- | Two thin concentric circles forming the outer roundel.
renderRoundel :: T.Text
renderRoundel = T.concat
[ "<circle cx=\"", ff fxCenter, "\" cy=\"", ff fyCenter
, "\" r=\"", ff fxOuter
, "\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"0.5\" opacity=\"0.7\"/>"
, "<circle cx=\"", ff fxCenter, "\" cy=\"", ff fyCenter
, "\" r=\"", ff fxOuterPlus
, "\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"0.5\" opacity=\"0.7\"/>"
]
-- | Four concentric guide circles at 0.2 R, 0.4 R, 0.6 R, 0.8 R.
renderGuides :: T.Text
renderGuides = T.concat $ map oneGuide [0.2, 0.4, 0.6, 0.8 :: Double]
where
oneGuide t = T.concat
[ "<circle cx=\"", ff fxCenter, "\" cy=\"", ff fyCenter
, "\" r=\"", ff (fxAxisFull * t)
, "\" fill=\"none\" stroke=\"currentColor\""
, " stroke-width=\"0.25\" opacity=\"0.4\"/>"
]
-- | Six radial axes from centre to the inner roundel.
renderAxes :: T.Text
renderAxes = T.concat $ map oneAxis axisAngles
where
oneAxis a =
let (x, y) = polar a fxAxisFull
in T.concat
[ "<line x1=\"", ff fxCenter, "\" y1=\"", ff fyCenter
, "\" x2=\"", ff x, "\" y2=\"", ff y
, "\" stroke=\"currentColor\" stroke-width=\"0.3\" opacity=\"0.55\"/>"
]
-- | Polygon connecting the present field values along their axes.
-- When all six axes have a value the polygon is closed; otherwise
-- it's an open polyline through the present vertices in axis order.
renderPolygon :: EpistemicData -> T.Text
renderPolygon d =
let pairs = [ (i, axisValue d i) | i <- [0..5] ]
verts = [ polar a (fxAxisFull * v)
| (i, Just v) <- pairs
, let a = axisAngles !! i ]
in case verts of
[] -> ""
_ ->
let pointsTxt = T.intercalate " " [ xy x y | (x, y) <- verts ]
allPresent = all (isJust . snd) pairs
tag = if allPresent then "polygon" else "polyline"
fillAttr = if allPresent
then " fill=\"currentColor\" fill-opacity=\"0.08\""
else " fill=\"none\""
in T.concat
[ "<", tag
, " points=\"", pointsTxt
, "\" stroke=\"currentColor\" stroke-width=\"1.1\""
, fillAttr
, " stroke-linejoin=\"round\" stroke-linecap=\"round\"/>"
]
-- | One vertex point per present axis. Confidence axis gets a 3×3 square
-- instead of a 2-px circle when @confidence: proved@ is in effect — the
-- "proof cap" marker (MARKS.md §4.3).
renderVertexMarks :: EpistemicData -> T.Text
renderVertexMarks d = T.concat $ catMaybes
[ vertexMark d i | i <- [0..5] ]
vertexMark :: EpistemicData -> Int -> Maybe T.Text
vertexMark d i = do
v <- axisValue d i
let (x, y) = polar (axisAngles !! i) (fxAxisFull * v)
squareCap = i == 0 && epConfidenceProved d
return $ if squareCap
then T.concat
[ "<rect x=\"", ff (x - 1.5), "\" y=\"", ff (y - 1.5)
, "\" width=\"3\" height=\"3\""
, " fill=\"currentColor\" stroke=\"none\"/>"
]
else T.concat
[ "<circle cx=\"", ff x, "\" cy=\"", ff y
, "\" r=\"2\" fill=\"currentColor\" stroke=\"none\"/>"
]
-- | Outer-ring stability ticks at the top of the figure. Always five
-- positions; inactive ticks render at opacity 0.4 so the full scale
-- stays visible. Peer-status modulates tick *style*; see
-- 'renderPeerStatusOverlay'.
renderTicks :: String -> Maybe String -> T.Text
renderTicks stability peerStatus =
let activeCount = case stability of
"volatile" -> 1
"revising" -> 2
"fairly stable" -> 3
"stable" -> 4
"established" -> 5
_ -> 1
tickAngles :: [Double]
tickAngles = [0, -15, 15, -30, 30]
tickOne :: Int -> Double -> T.Text
tickOne idx a =
let (x1, y1) = polar a fxOuterPlus
(x2, y2) = polar a (fxOuterPlus + 1.5)
op = if idx < activeCount then "1.0" else "0.4"
in T.concat
[ "<line x1=\"", ff x1, "\" y1=\"", ff y1
, "\" x2=\"", ff x2, "\" y2=\"", ff y2
, "\" stroke=\"currentColor\" stroke-width=\"1\""
, " stroke-linecap=\"round\" opacity=\"", op, "\"/>"
]
in T.concat (zipWith tickOne [0..] tickAngles)
<> renderPeerStatusOverlay peerStatus
-- | Per-peer-status decorations layered on top of the tick group.
-- Geometry per MARKS.md §4.1.
renderPeerStatusOverlay :: Maybe String -> T.Text
renderPeerStatusOverlay Nothing = ""
renderPeerStatusOverlay (Just "under-review") =
-- Small unfilled circle just outside the outermost tick, at the top.
let (x, y) = polar 0 (fxOuterPlus + 3.5)
in T.concat
[ "<circle cx=\"", ff x, "\" cy=\"", ff y
, "\" r=\"1\" fill=\"none\" stroke=\"currentColor\""
, " stroke-width=\"0.6\"/>"
]
renderPeerStatusOverlay (Just "peer-reviewed") =
-- Single horizontal bar above the outer roundel arc, centred on top.
T.concat
[ "<line x1=\"", ff (fxCenter - 6), "\" y1=\"", ff (fyCenter - fxOuterPlus - 3)
, "\" x2=\"", ff (fxCenter + 6), "\" y2=\"", ff (fyCenter - fxOuterPlus - 3)
, "\" stroke=\"currentColor\" stroke-width=\"0.7\""
, " stroke-linecap=\"round\"/>"
]
renderPeerStatusOverlay (Just "published") =
-- Printer's-bracket: short vertical marks at ±15° on the outer roundel.
let (lx1, ly1) = polar (-15) (fxOuterPlus + 1)
(lx2, ly2) = polar (-15) (fxOuterPlus + 4)
(rx1, ry1) = polar 15 (fxOuterPlus + 1)
(rx2, ry2) = polar 15 (fxOuterPlus + 4)
in T.concat
[ "<line x1=\"", ff lx1, "\" y1=\"", ff ly1
, "\" x2=\"", ff lx2, "\" y2=\"", ff ly2
, "\" stroke=\"currentColor\" stroke-width=\"0.8\""
, " stroke-linecap=\"round\"/>"
, "<line x1=\"", ff rx1, "\" y1=\"", ff ry1
, "\" x2=\"", ff rx2, "\" y2=\"", ff ry2
, "\" stroke=\"currentColor\" stroke-width=\"0.8\""
, " stroke-linecap=\"round\"/>"
]
renderPeerStatusOverlay (Just "retracted") =
-- Horizontal strikethrough across the tick group.
T.concat
[ "<line x1=\"", ff (fxCenter - 9), "\" y1=\"", ff (fyCenter - fxOuterPlus - 1)
, "\" x2=\"", ff (fxCenter + 9), "\" y2=\"", ff (fyCenter - fxOuterPlus - 1)
, "\" stroke=\"currentColor\" stroke-width=\"1.5\""
, " stroke-linecap=\"round\"/>"
]
renderPeerStatusOverlay (Just _) = ""
-- | Trust score (Spectral, 16 px) and the small "TRUST" label below it.
renderTrustLabel :: Int -> T.Text
renderTrustLabel score = T.concat
[ "<text x=\"", ff fxCenter, "\" y=\"", ff (fyCenter + 4)
, "\" text-anchor=\"middle\""
, " fill=\"currentColor\" stroke=\"none\""
, " font-family=\"Spectral, serif\" font-weight=\"500\" font-size=\"16\">"
, T.pack (show score)
, "</text>"
, "<text x=\"", ff fxCenter, "\" y=\"", ff (fyCenter + 14)
, "\" text-anchor=\"middle\""
, " fill=\"currentColor\" stroke=\"none\""
, " font-family=\"&quot;Fira Sans&quot;, sans-serif\""
, " font-size=\"5\" letter-spacing=\"0.18em\""
, " opacity=\"0.7\">TRUST</text>"
]
-- | Result-shape glyph immediately to the right of the trust score.
renderResultShape :: Maybe String -> Int -> T.Text
renderResultShape Nothing _ = ""
renderResultShape (Just shape) score =
let glyph = case shape of
"positive" -> "+"
"negative" -> "\x2212" -- minus sign (not hyphen-minus)
"mixed" -> "\x00B1" -- ±
"comparative" -> "\x223C" --
"descriptive" -> "\x25A1" -- □
_ -> ""
-- Offset proportional to the trust number's width (digits ≈ 8 px each).
digitCount = length (show score)
offset = fromIntegral digitCount * 4.5 + 3 :: Double
in if T.null (T.pack glyph)
then ""
else T.concat
[ "<text x=\"", ff (fxCenter + offset)
, "\" y=\"", ff (fyCenter + 4)
, "\" text-anchor=\"start\""
, " fill=\"currentColor\" stroke=\"none\""
, " font-family=\"Spectral, serif\" font-size=\"16\">"
, T.pack glyph
, "</text>"
]
-- ---------------------------------------------------------------------------
-- Field exports
-- ---------------------------------------------------------------------------
-- | @$epistemicSvg$@. Returns 'noResult' when @status:@ is absent
-- (matches the existing visibility rule for the epistemic block —
-- MARKS.md §3.1). Otherwise returns the inline SVG string ready for
-- template interpolation.
epistemicSvgField :: Context String
epistemicSvgField = field "epistemicSvg" $ \item -> do
meta <- getMetadata (itemIdentifier item)
case lookupString "status" meta of
Nothing -> noResult "no status; epistemic figure suppressed"
Just _ -> do
d <- readEpistemicData item
return $ T.unpack (wrapEpistemic (renderEpistemicFigure d))
wrapEpistemic :: T.Text -> T.Text
wrapEpistemic svg = T.concat
[ "<figure class=\"frontmatter-mark frontmatter-mark--epistemic\">"
, svg
, "</figure>"
]

View File

@ -17,6 +17,7 @@
-- every successful build, so pins are one-shot.
module Stability
( stabilityField
, resolveStability
, lastReviewedField
, lastReviewedIsoField
, versionHistoryField
@ -133,10 +134,15 @@ fmtIso s = case parseIso s of
-- Stability and last-reviewed context fields
-- ---------------------------------------------------------------------------
-- | Context field @$stability$@.
-- Always resolves to a label; prefers frontmatter when the file is pinned.
stabilityField :: Context String
stabilityField = field "stability" $ \item -> do
-- | Resolve the stability label for an item — frontmatter override
-- when the source path is pinned via @IGNORE.txt@, else the heuristic
-- run over @git log --follow@ on the source path.
--
-- Used by 'stabilityField' (which exposes the label as a context field)
-- and by Marks.hs (which feeds the label into the epistemic figure's
-- outer-ring tick count).
resolveStability :: Item a -> Compiler String
resolveStability item = do
let srcPath = toFilePath (itemIdentifier item)
meta <- getMetadata (itemIdentifier item)
unsafeCompiler $ do
@ -145,6 +151,11 @@ stabilityField = field "stability" $ \item -> do
then return $ fromMaybe "volatile" (lookupString "stability" meta)
else stabilityFromDates <$> gitDates srcPath
-- | Context field @$stability$@.
-- Always resolves to a label; prefers frontmatter when the file is pinned.
stabilityField :: Context String
stabilityField = field "stability" resolveStability
-- | Context field @$last-reviewed$@.
-- Returns the formatted date of the most-recent commit, or @noResult@ when
-- unavailable (making @$if(last-reviewed)$@ false in templates).

81
content/build.mark.svg Normal file
View File

@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-build">
<title id="mark-title-build">A vertical compilation pipeline rendered as a small DAG, with a clock-face fragment in the upper corner</title>
<desc>A frontispiece mark for the Build telemetry page. Three source nodes at top funnel through a filter stage, narrow to a single canonical AST, then expand to four output artifacts at the bottom. A small arc with a single tick mark in the upper-left of the inner field is a clock-face fragment, an unobtrusive nod to the build-timing aspect of the page.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M 50 70 A 10 10 0 0 1 60 60" stroke-width="0.7"/>
<line x1="50" y1="65" x2="52" y2="65" stroke-width="0.6"/>
<line x1="56" y1="60" x2="56" y2="62" stroke-width="0.6"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="92" y1="80" x2="120" y2="116" stroke-width="0.7"/>
<line x1="140" y1="80" x2="140" y2="116" stroke-width="0.7"/>
<line x1="188" y1="80" x2="160" y2="116" stroke-width="0.7"/>
<line x1="120" y1="124" x2="140" y2="148" stroke-width="0.7"/>
<line x1="140" y1="124" x2="140" y2="148" stroke-width="0.7"/>
<line x1="160" y1="124" x2="140" y2="148" stroke-width="0.7"/>
<line x1="140" y1="160" x2="140" y2="180" stroke-width="1.0"/>
<line x1="140" y1="190" x2="80" y2="222" stroke-width="0.6"/>
<line x1="140" y1="190" x2="115" y2="222" stroke-width="0.6"/>
<line x1="140" y1="190" x2="165" y2="222" stroke-width="0.6"/>
<line x1="140" y1="190" x2="200" y2="222" stroke-width="0.6"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="miter">
<rect x="84" y="72" width="16" height="14" stroke-width="0.9"/>
<rect x="132" y="72" width="16" height="14" stroke-width="0.9"/>
<rect x="180" y="72" width="16" height="14" stroke-width="0.9"/>
<line x1="87" y1="78" x2="97" y2="78" stroke-width="0.4" opacity="0.7"/>
<line x1="87" y1="82" x2="93" y2="82" stroke-width="0.4" opacity="0.7"/>
<line x1="135" y1="78" x2="145" y2="78" stroke-width="0.4" opacity="0.7"/>
<line x1="135" y1="82" x2="142" y2="82" stroke-width="0.4" opacity="0.7"/>
<line x1="183" y1="78" x2="193" y2="78" stroke-width="0.4" opacity="0.7"/>
<line x1="183" y1="82" x2="190" y2="82" stroke-width="0.4" opacity="0.7"/>
</g>
<g stroke="currentColor" fill="none" stroke-linejoin="miter" stroke-linecap="round">
<line x1="106" y1="120" x2="174" y2="120" stroke-width="0.4" opacity="0.55"/>
<line x1="120" y1="120" x2="120" y2="124" stroke-width="0.7"/>
<line x1="140" y1="120" x2="140" y2="124" stroke-width="0.7"/>
<line x1="160" y1="120" x2="160" y2="124" stroke-width="0.7"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="120" cy="120" r="1.6"/>
<circle cx="140" cy="120" r="1.6"/>
<circle cx="160" cy="120" r="1.6"/>
</g>
<circle cx="140" cy="154" r="6" stroke="currentColor" stroke-width="1.2" fill="none"/>
<circle cx="140" cy="154" r="2" fill="currentColor" stroke="none"/>
<g stroke="currentColor" fill="none" stroke-linejoin="miter" stroke-linecap="butt">
<rect x="72" y="222" width="16" height="14" stroke-width="0.7"/>
<rect x="107" y="222" width="16" height="14" stroke-width="0.7"/>
<rect x="157" y="222" width="16" height="14" stroke-width="0.7"/>
<rect x="192" y="222" width="16" height="14" stroke-width="0.7"/>
<line x1="80" y1="222" x2="80" y2="220" stroke-width="0.6"/>
<line x1="115" y1="222" x2="115" y2="220" stroke-width="0.6"/>
<line x1="165" y1="222" x2="165" y2="220" stroke-width="0.6"/>
<line x1="200" y1="222" x2="200" y2="220" stroke-width="0.6"/>
</g>
<g stroke="currentColor" fill="none" stroke-width="0.4" opacity="0.7" stroke-linecap="round">
<line x1="75" y1="227" x2="85" y2="227"/>
<line x1="75" y1="231" x2="83" y2="231"/>
<line x1="110" y1="227" x2="120" y2="227"/>
<line x1="110" y1="231" x2="118" y2="231"/>
<line x1="160" y1="227" x2="170" y2="227"/>
<line x1="160" y1="231" x2="168" y2="231"/>
<line x1="195" y1="227" x2="205" y2="227"/>
<line x1="195" y1="231" x2="203" y2="231"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

91
content/colophon.mark.svg Normal file
View File

@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-colophon">
<title id="mark-title-colophon">A printer's device — a small ordered tree of typesetting at center, ringed by gathering marks indicating the book is mid-set</title>
<desc>A frontispiece mark for the Colophon — drawn in the lineage of Aldine, Plantin, and Elzevir printer's marks. The central tree is the AST of one document being typeset; the outer ring of small marks indicates pages still being gathered, the document not yet bound.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<circle cx="140" cy="140" r="120" stroke="currentColor" stroke-width="0.4" fill="none" opacity="0.55"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="140" y1="78" x2="140" y2="98" stroke-width="1.2"/>
<line x1="140" y1="98" x2="108" y2="118" stroke-width="1.0"/>
<line x1="140" y1="98" x2="172" y2="118" stroke-width="1.0"/>
<line x1="108" y1="118" x2="92" y2="142" stroke-width="0.8"/>
<line x1="108" y1="118" x2="118" y2="148" stroke-width="0.8"/>
<line x1="172" y1="118" x2="162" y2="142" stroke-width="0.8"/>
<line x1="172" y1="118" x2="184" y2="148" stroke-width="0.8"/>
<line x1="92" y1="142" x2="84" y2="166" stroke-width="0.6"/>
<line x1="92" y1="142" x2="100" y2="170" stroke-width="0.6"/>
<line x1="118" y1="148" x2="112" y2="172" stroke-width="0.6"/>
<line x1="118" y1="148" x2="125" y2="174" stroke-width="0.6"/>
<line x1="162" y1="142" x2="156" y2="170" stroke-width="0.6"/>
<line x1="162" y1="142" x2="168" y2="172" stroke-width="0.6"/>
<line x1="184" y1="148" x2="178" y2="174" stroke-width="0.6"/>
<line x1="184" y1="148" x2="192" y2="166" stroke-width="0.6"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="140" cy="78" r="2.0"/>
<circle cx="140" cy="98" r="1.4"/>
<circle cx="108" cy="118" r="1.2"/>
<circle cx="172" cy="118" r="1.2"/>
<circle cx="92" cy="142" r="1.0"/>
<circle cx="118" cy="148" r="1.0"/>
<circle cx="162" cy="142" r="1.0"/>
<circle cx="184" cy="148" r="1.0"/>
<circle cx="84" cy="166" r="0.7"/>
<circle cx="100" cy="170" r="0.7"/>
<circle cx="112" cy="172" r="0.7"/>
<circle cx="125" cy="174" r="0.7"/>
<circle cx="156" cy="170" r="0.7"/>
<circle cx="168" cy="172" r="0.7"/>
<circle cx="178" cy="174" r="0.7"/>
<circle cx="192" cy="166" r="0.7"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.6">
<line x1="140" y1="190" x2="140" y2="208"/>
<line x1="135" y1="200" x2="145" y2="200"/>
<line x1="137" y1="204" x2="143" y2="204"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.7">
<line x1="140" y1="38" x2="140" y2="44"/>
<line x1="172" y1="44" x2="174" y2="50"/>
<line x1="200" y1="62" x2="204" y2="66"/>
<line x1="220" y1="92" x2="226" y2="94"/>
<line x1="234" y1="125" x2="240" y2="125"/>
<line x1="234" y1="155" x2="240" y2="155"/>
<line x1="226" y1="186" x2="220" y2="188"/>
<line x1="204" y1="214" x2="200" y2="218"/>
<line x1="174" y1="230" x2="172" y2="236"/>
<line x1="106" y1="230" x2="108" y2="236"/>
<line x1="76" y1="214" x2="80" y2="218"/>
<line x1="54" y1="186" x2="60" y2="188"/>
<line x1="40" y1="155" x2="46" y2="155"/>
<line x1="40" y1="125" x2="46" y2="125"/>
<line x1="60" y1="92" x2="54" y2="94"/>
<line x1="80" y1="62" x2="76" y2="66"/>
<line x1="108" y1="44" x2="106" y2="50"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="124" cy="225" r="1.2"/>
<circle cx="156" cy="225" r="1.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -127,7 +127,11 @@ to inspect it, fork it, or, more broadly, do whatever you please with it.
The dominant convention of academic and professional publication is that a document, once released, is finished. It carries an implicit claim: *this is what I think, full stop.*^[This is particularly problematic in academia, where there is a long tradition of researchers whose work was eventually disproven taking an extreme defensive stance, usually rooted in [confirmation bias](https://en.wikipedia.org/wiki/Confirmation_bias).] I find this convention dishonest in proportion to how seldom it is actually true. Thinking is continuous; positions shift; evidence accumulates; people change their minds and rarely say so in public. This site operates under a different premise, one that I strive to operate all of my life under.
Every essay and post on this site carries an **epistemic footer** — a structured block that reports my current relationship to the work. The footer only appears when a `status` field is set in the document's frontmatter; standalone pages and very short items omit it. The full set of fields:
Every essay and post on this site carries an **epistemic footer** — a structured block that reports my current relationship to the work. The footer only appears when a `status` field is set in the document's frontmatter; standalone pages and very short items omit it.
The vocabulary below is genre-general but reads differently across genres. For a personal essay, *confidence* reflects credence in a thesis — "I might change my mind." For an empirical research paper, it reflects expected generalization — "this would replicate." For formal mathematics, it reflects credence in proof correctness, with a special value `proved` available for theorems with complete proofs (where any numeric value would be false precision). *Evidence* reads analogously: the strength of arguments and supporting writing in essays, the empirical base in research, the structure of the proof in mathematics. The fields are the same; the interpretive frame shifts with the work.
The full set of fields:
- **Status** — a controlled vocabulary describing where the work stands: *Draft*, *Working model*, *Durable*, *Refined*, *Superseded*, or *Deprecated*. A document marked *Working model* is not just unfinished — it is a position I currently hold but would not stake much on. A document marked *Durable* is something I expect to hold up. *Superseded* means I wrote a better version; *Deprecated* means I no longer endorse it.
@ -141,6 +145,10 @@ Every essay and post on this site carries an **epistemic footer** — a structur
- **Scope**, **Novelty**, **Practicality** — orientation fields shown as their own rows in the epistemic footer alongside confidence, importance, and evidence. *Scope* ranges from *personal* to *civilizational*; *novelty* from *conventional* to *innovative*; *practicality* from *abstract* to *exceptional*. These are not ratings — they are orientations, and they intentionally do not feed the trust score.
- **Peer status** — the *external* review state, distinct from `status` (which is my internal position). Values: *unreviewed* (default), *under review*, *peer reviewed*, *published*, *retracted*. A piece can be *Durable* (I expect it to hold up) and *unreviewed* (the world hasn't checked yet) at the same time; the two axes are deliberately factored. A *retracted* piece renders with the field name struck through and the outer ring of the epistemic figure crossed out.
- **Result shape** — the shape of the central claim: *positive* (argues something works), *negative* (argues something does not), *mixed* (both, as in a double-pincer barrier paper), *comparative* (compares approaches), or *descriptive* (describes without arguing for or against). Encoded as a small glyph beside the trust score on the epistemic figure. Adds nothing to the compact row.
- **Stability** — auto-computed at every build from `git log --follow`. The heuristic: very new or barely-touched documents are *volatile*; actively-revised documents are *revising*; older documents with more commits settle into *fairly stable*, *stable*, or *established*. This requires no manual maintenance — the build reads the repository history and makes the inference.
The version history block, directly above the epistemic footer, uses a three-tier fallback: authored `history:` notes when they exist (written by me when the git log alone would not convey what changed), then the raw git log, then the `date:` frontmatter field as a creation record. `make build` auto-commits any changed content files before the Hakyll compilation runs, so the git log is always current.

View File

@ -23,6 +23,8 @@ evidence: 5
scope: average
novelty: moderate
practicality: moderate
peer-status: under-review
result-shape: comparative
bibliography: data/bci-paper.bib
repository: "https://git.levineuwirth.org/neuwirth/beyond_comorbidity_indices"
summary: |

View File

@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-comorbidity">
<title id="mark-title-comorbidity">A small grid of nodes resolving into a tighter ROC-style curve, with a baseline arc beneath</title>
<desc>A frontispiece mark for "Beyond Comorbidity Indices" — many diagnosis codes aggregated to a discriminating risk score.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g fill="currentColor" stroke="none">
<circle cx="65" cy="65" r="1.1"/>
<circle cx="80" cy="55" r="1.1"/>
<circle cx="95" cy="68" r="1.1"/>
<circle cx="105" cy="55" r="1.1"/>
<circle cx="65" cy="80" r="1.1"/>
<circle cx="78" cy="78" r="1.1"/>
<circle cx="92" cy="82" r="1.1"/>
<circle cx="108" cy="80" r="1.1"/>
<circle cx="68" cy="95" r="1.1"/>
<circle cx="82" cy="98" r="1.1"/>
<circle cx="98" cy="92" r="1.1"/>
<circle cx="110" cy="98" r="1.1"/>
<circle cx="62" cy="110" r="1.1"/>
<circle cx="78" cy="112" r="1.1"/>
<circle cx="92" cy="108" r="1.1"/>
<circle cx="105" cy="115" r="1.1"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.4" opacity="0.55">
<line x1="65" y1="65" x2="138" y2="142"/>
<line x1="80" y1="55" x2="138" y2="142"/>
<line x1="95" y1="68" x2="138" y2="142"/>
<line x1="105" y1="55" x2="138" y2="142"/>
<line x1="78" y1="78" x2="138" y2="142"/>
<line x1="92" y1="82" x2="138" y2="142"/>
<line x1="98" y1="92" x2="138" y2="142"/>
<line x1="82" y1="98" x2="138" y2="142"/>
<line x1="105" y1="115" x2="138" y2="142"/>
<line x1="92" y1="108" x2="138" y2="142"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="138" y1="225" x2="245" y2="225" stroke-width="0.8"/>
<line x1="138" y1="225" x2="138" y2="118" stroke-width="0.8"/>
<line x1="138" y1="225" x2="245" y2="118" stroke-width="0.4" stroke-dasharray="2 3"/>
<path d="M 138 225 Q 152 175 175 158 Q 200 142 230 128 L 245 122" stroke-width="1.6"/>
<path d="M 138 225 Q 165 200 195 185 Q 225 170 245 162" stroke-width="0.8" stroke-dasharray="3 3"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.5">
<line x1="138" y1="225" x2="138" y2="229"/>
<line x1="138" y1="225" x2="134" y2="225"/>
<line x1="245" y1="225" x2="245" y2="229"/>
<line x1="138" y1="118" x2="134" y2="118"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -14,6 +14,11 @@ affiliation:
bibliography: data/branch-capture-paper.bib
preprint: /papers/branch-capture-paper.pdf
no-collapse: true
status: "Durable"
confidence: proved
evidence: 5
peer-status: unreviewed
result-shape: mixed
further-reading:
- NowakowskiWinkler
- Quilliot

View File

@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-branch-based">
<title id="mark-title-branch-based">Two cones meeting at a vertex on a regular tree, with a depth-budget bracket beneath</title>
<desc>A frontispiece mark for "Branch-Based Local Capture in Tree-Ball Geometry: Sharp Positive and Negative Results".</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="140" y1="78" x2="140" y2="118" stroke-width="1.2"/>
<line x1="140" y1="118" x2="105" y2="148" stroke-width="1.0"/>
<line x1="140" y1="118" x2="175" y2="148" stroke-width="1.0"/>
<line x1="105" y1="148" x2="85" y2="178" stroke-width="0.8"/>
<line x1="105" y1="148" x2="125" y2="178" stroke-width="0.8"/>
<line x1="175" y1="148" x2="155" y2="178" stroke-width="0.8"/>
<line x1="175" y1="148" x2="195" y2="178" stroke-width="0.8"/>
<line x1="85" y1="178" x2="73" y2="200" stroke-width="0.5"/>
<line x1="85" y1="178" x2="97" y2="200" stroke-width="0.5"/>
<line x1="125" y1="178" x2="113" y2="200" stroke-width="0.5"/>
<line x1="125" y1="178" x2="137" y2="200" stroke-width="0.5"/>
<line x1="155" y1="178" x2="143" y2="200" stroke-width="0.5"/>
<line x1="155" y1="178" x2="167" y2="200" stroke-width="0.5"/>
<line x1="195" y1="178" x2="183" y2="200" stroke-width="0.5"/>
<line x1="195" y1="178" x2="207" y2="200" stroke-width="0.5"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="140" cy="78" r="2.4"/>
<circle cx="140" cy="118" r="1.8"/>
<circle cx="105" cy="148" r="1.6"/>
<circle cx="175" cy="148" r="1.6"/>
<circle cx="85" cy="178" r="1.2"/>
<circle cx="125" cy="178" r="1.2"/>
<circle cx="155" cy="178" r="1.2"/>
<circle cx="195" cy="178" r="1.2"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.6">
<path d="M 105 148 Q 140 168 175 148"/>
<path d="M 85 178 Q 195 215 195 178"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="58" y1="226" x2="222" y2="226" stroke-width="0.8"/>
<line x1="58" y1="226" x2="58" y2="220" stroke-width="0.8"/>
<line x1="222" y1="226" x2="222" y2="220" stroke-width="0.8"/>
<line x1="140" y1="226" x2="140" y2="232" stroke-width="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,90 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-title-levcs">
<title id="mark-title-levcs">A federated commit graph: multiple peer roots connected by signed edges, with no privileged origin.</title>
<desc>Frontmatter mark for the essay "LeVCS: A Distributed Version Control System".</desc>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<!-- Outer roundel -->
<circle cx="140" cy="140" r="128" stroke-width="0.6"/>
<!-- Three peer roots, equally weighted, at 90, 210, 330 degrees on r=78.
Each is the source of its own commit chain that intersects with the others.
Identity sigil (small filled square) inside each root marks
"identity in the protocol". -->
<!-- Root A: top -->
<circle cx="140" cy="62" r="9" stroke-width="1.2"/>
<rect x="136.5" y="58.5" width="7" height="7" fill="currentColor" stroke="none"/>
<!-- Root B: lower-left -->
<circle cx="72.4" cy="179" r="9" stroke-width="1.2"/>
<rect x="68.9" y="175.5" width="7" height="7" fill="currentColor" stroke="none"/>
<!-- Root C: lower-right -->
<circle cx="207.6" cy="179" r="9" stroke-width="1.2"/>
<rect x="204.1" y="175.5" width="7" height="7" fill="currentColor" stroke="none"/>
<!-- Commits along each chain. Each root spawns a chain of 3 commits walking
toward the center along its inward radial. Commits are small open circles. -->
<!-- Chain A: from top root walking down toward (140, 140). 3 commits. -->
<g stroke-width="0.9">
<line x1="140" y1="71" x2="140" y2="89"/>
<line x1="140" y1="98" x2="140" y2="116"/>
<line x1="140" y1="125" x2="140" y2="131"/>
</g>
<circle cx="140" cy="93.5" r="3.4" stroke-width="0.9"/>
<circle cx="140" cy="120.5" r="3.4" stroke-width="0.9"/>
<!-- Chain B: from lower-left root, walking toward center along the
radial. Direction vector = (1, -1)/sqrt(2). Step ~13.5 along the line.
B is at (72.4, 179); center at (140, 140); distance ~78. Step inward. -->
<g stroke-width="0.9">
<line x1="79.8" y1="174.4" x2="95.4" y2="165.5"/>
<line x1="103" y1="160.6" x2="118.6" y2="151.7"/>
<line x1="126.2" y1="146.8" x2="131.8" y2="143.6"/>
</g>
<circle cx="99.2" cy="163.05" r="3.4" stroke-width="0.9"/>
<circle cx="122.4" cy="149.25" r="3.4" stroke-width="0.9"/>
<!-- Chain C: from lower-right root, walking toward center.
C at (207.6, 179); direction = (-1, -1)/sqrt(2). -->
<g stroke-width="0.9">
<line x1="200.2" y1="174.4" x2="184.6" y2="165.5"/>
<line x1="177" y1="160.6" x2="161.4" y2="151.7"/>
<line x1="153.8" y1="146.8" x2="148.2" y2="143.6"/>
</g>
<circle cx="180.8" cy="163.05" r="3.4" stroke-width="0.9"/>
<circle cx="157.6" cy="149.25" r="3.4" stroke-width="0.9"/>
<!-- Confluence point: a small open square at the geometric center where the
three chains meet. This is the merge: cascading, format-aware, but does
not collapse history — the chains remain distinct above. -->
<rect x="135.5" y="135.5" width="9" height="9" stroke-width="1.2"/>
<!-- Cross-edges: peer-to-peer references between the three chains.
These are the federation edges — direct refs between commits on different
chains, not routed through any hub. Drawn as dashed thin lines. -->
<g stroke-width="0.5" stroke-dasharray="2 3" opacity="0.75">
<!-- A's first commit ↔ C's first commit -->
<path d="M 143 92 Q 168 110 178 161"/>
<!-- A's first commit ↔ B's first commit -->
<path d="M 137 92 Q 112 110 102 161"/>
<!-- B's second ↔ C's second -->
<path d="M 122 149 L 158 149" stroke-dasharray="2 3"/>
</g>
<!-- Signature ticks: short hash marks on each chain's middle-segment,
indicating signed history (BLAKE3). One small tick perpendicular to each
chain segment. -->
<g stroke-width="0.7">
<!-- Chain A tick: between commits 1 and 2 -->
<line x1="146" y1="107" x2="150" y2="107"/>
<!-- Chain B tick -->
<line x1="111.4" y1="160.0" x2="113.8" y2="156.8"/>
<!-- Chain C tick -->
<line x1="168.6" y1="160.0" x2="166.2" y2="156.8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,89 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-title-networking-stack">
<title id="mark-title-networking-stack">A four-layer protocol stack with addressing arrows passing vertically through, headers nested as concentric brackets at each layer.</title>
<desc>Frontmatter mark for the essay "Networking Stack from Scratch".</desc>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<!-- Outer roundel -->
<circle cx="140" cy="140" r="128" stroke-width="0.6"/>
<!-- Four protocol layers as horizontal bands, stacked.
Width 130, centered at x=140. Vertical band spacing: 30 px.
Layer order (top to bottom): Application, Transport, Internet, Link.
Visual encoding: stroke-style varies per layer to distinguish
without color and without text. -->
<!-- Layer 1: Application (e.g. SSH, DNS) — solid rule -->
<line x1="75" y1="80" x2="205" y2="80" stroke-width="1.2"/>
<line x1="75" y1="98" x2="205" y2="98" stroke-width="0.4"/>
<!-- Layer 2: Transport (TCP, UDP) — dashed rule -->
<line x1="75" y1="118" x2="205" y2="118" stroke-width="1.2" stroke-dasharray="6 3"/>
<line x1="75" y1="136" x2="205" y2="136" stroke-width="0.4"/>
<!-- Layer 3: Internet (IP, RIP) — densely-dashed rule -->
<line x1="75" y1="156" x2="205" y2="156" stroke-width="1.2" stroke-dasharray="2 2"/>
<line x1="75" y1="174" x2="205" y2="174" stroke-width="0.4"/>
<!-- Layer 4: Link — double rule -->
<line x1="75" y1="194" x2="205" y2="194" stroke-width="1.2"/>
<line x1="75" y1="198" x2="205" y2="198" stroke-width="0.6"/>
<line x1="75" y1="212" x2="205" y2="212" stroke-width="0.4"/>
<!-- Headers nesting: at each layer, a small bracket on the left margin
of the band indicates encapsulation. They get progressively wider
as we descend (each layer wraps the one above). -->
<g stroke-width="0.7">
<!-- Layer 1 bracket -->
<path d="M 88 84 L 84 84 L 84 94 L 88 94"/>
<!-- Layer 2 bracket -->
<path d="M 86 122 L 80 122 L 80 132 L 86 132"/>
<!-- Layer 3 bracket -->
<path d="M 84 160 L 76 160 L 76 170 L 84 170"/>
<!-- Layer 4 bracket -->
<path d="M 82 198 L 72 198 L 72 208 L 82 208"/>
</g>
<!-- Address arrows: a downward flow on the left, an upward flow on the right.
These are the data path — packet descending the stack on send,
ascending on receive. Each is interrupted at every layer with a
small horizontal tick (per-layer header attachment / strip). -->
<!-- Down arrow (left side): outbound -->
<g stroke-width="1.0">
<line x1="58" y1="62" x2="58" y2="226"/>
<!-- Arrowhead at bottom -->
<line x1="58" y1="226" x2="54" y2="220"/>
<line x1="58" y1="226" x2="62" y2="220"/>
<!-- Per-layer ticks crossing the arrow shaft -->
<line x1="54" y1="89" x2="62" y2="89"/>
<line x1="54" y1="127" x2="62" y2="127"/>
<line x1="54" y1="165" x2="62" y2="165"/>
<line x1="54" y1="203" x2="62" y2="203"/>
</g>
<!-- Up arrow (right side): inbound -->
<g stroke-width="1.0">
<line x1="222" y1="226" x2="222" y2="62"/>
<!-- Arrowhead at top -->
<line x1="222" y1="62" x2="218" y2="68"/>
<line x1="222" y1="62" x2="226" y2="68"/>
<!-- Per-layer ticks -->
<line x1="218" y1="89" x2="226" y2="89"/>
<line x1="218" y1="127" x2="226" y2="127"/>
<line x1="218" y1="165" x2="226" y2="165"/>
<line x1="218" y1="203" x2="226" y2="203"/>
</g>
<!-- Below the stack, two filled small circles connected by a horizontal line
indicating the wire / virtual machines. Two endpoints, one segment. -->
<g>
<line x1="100" y1="240" x2="180" y2="240" stroke-width="0.7"/>
<circle cx="100" cy="240" r="2.5" fill="currentColor" stroke="none"/>
<circle cx="180" cy="240" r="2.5" fill="currentColor" stroke="none"/>
<!-- A tiny squiggle between them indicating signal in transit -->
<path d="M 130 240 q 5 -3 10 0 t 10 0" stroke-width="0.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,106 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-title-neuropose">
<title id="mark-title-neuropose">An articulated kinematic figure with a small dendritic node at the head — pose tracked through the body, signal originating from the brain.</title>
<desc>Frontmatter mark for the essay "NeuroPose".</desc>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<!-- Outer roundel -->
<circle cx="140" cy="140" r="128" stroke-width="0.6"/>
<!-- Kinematic skeleton: head, spine, two arms, two legs.
Drawn in a slight asymmetric stance to suggest motion-tracking
rather than a static anatomical diagram.
Joint coordinates (the markers tracked):
head (140, 78)
neck (140, 102)
shoulder L (122, 110)
shoulder R (158, 110)
elbow L (108, 134)
elbow R (172, 132)
wrist L (96, 158)
wrist R (180, 156)
hip L (130, 158)
hip R (150, 158)
knee L (124, 192)
knee R (158, 190)
ankle L (118, 222)
ankle R (164, 224)
-->
<!-- Spine (neck → mid-hip) -->
<line x1="140" y1="102" x2="140" y2="158" stroke-width="1.2"/>
<!-- Shoulders crossbar -->
<line x1="122" y1="110" x2="158" y2="110" stroke-width="1.0"/>
<!-- Hips crossbar -->
<line x1="130" y1="158" x2="150" y2="158" stroke-width="1.0"/>
<!-- Left arm -->
<line x1="122" y1="110" x2="108" y2="134" stroke-width="1.0"/>
<line x1="108" y1="134" x2="96" y2="158" stroke-width="1.0"/>
<!-- Right arm: slightly forward stance for asymmetry -->
<line x1="158" y1="110" x2="172" y2="132" stroke-width="1.0"/>
<line x1="172" y1="132" x2="180" y2="156" stroke-width="1.0"/>
<!-- Left leg -->
<line x1="130" y1="158" x2="124" y2="192" stroke-width="1.0"/>
<line x1="124" y1="192" x2="118" y2="222" stroke-width="1.0"/>
<!-- Right leg: slight stride -->
<line x1="150" y1="158" x2="158" y2="190" stroke-width="1.0"/>
<line x1="158" y1="190" x2="164" y2="224" stroke-width="1.0"/>
<!-- Joint markers as small filled circles — these are the tracked points -->
<g fill="currentColor" stroke="none">
<circle cx="140" cy="102" r="1.6"/>
<circle cx="122" cy="110" r="1.6"/>
<circle cx="158" cy="110" r="1.6"/>
<circle cx="108" cy="134" r="1.6"/>
<circle cx="172" cy="132" r="1.6"/>
<circle cx="96" cy="158" r="1.6"/>
<circle cx="180" cy="156" r="1.6"/>
<circle cx="130" cy="158" r="1.6"/>
<circle cx="150" cy="158" r="1.6"/>
<circle cx="124" cy="192" r="1.6"/>
<circle cx="158" cy="190" r="1.6"/>
<circle cx="118" cy="222" r="1.6"/>
<circle cx="164" cy="224" r="1.6"/>
</g>
<!-- Head as the neural node: an open ring with a small dendritic spray
radiating upward, encoding "neuro" — origin of the motor signal.
Larger and more detailed than the joint markers to distinguish. -->
<circle cx="140" cy="78" r="9" stroke-width="1.0"/>
<!-- Dendrites: short branching strokes from the top half of the head -->
<g stroke-width="0.6">
<line x1="135" y1="71" x2="129" y2="62"/>
<line x1="129" y1="62" x2="124" y2="58"/>
<line x1="129" y1="62" x2="131" y2="55"/>
<line x1="140" y1="69" x2="140" y2="56"/>
<line x1="140" y1="56" x2="136" y2="50"/>
<line x1="140" y1="56" x2="144" y2="50"/>
<line x1="145" y1="71" x2="151" y2="62"/>
<line x1="151" y1="62" x2="156" y2="58"/>
<line x1="151" y1="62" x2="149" y2="55"/>
</g>
<!-- A central dot at the cortex — the source -->
<circle cx="140" cy="78" r="1.8" fill="currentColor" stroke="none"/>
<!-- Trajectory traces: thin dashed arcs at three of the limb endpoints,
indicating tracked motion across frames. Sparse — three is enough. -->
<g stroke-width="0.4" stroke-dasharray="1.5 2.5" opacity="0.7">
<!-- Right wrist arc -->
<path d="M 188 150 Q 192 156 180 156"/>
<!-- Left ankle arc -->
<path d="M 110 222 Q 114 226 118 222"/>
<!-- Right ankle arc -->
<path d="M 168 232 Q 165 226 164 224"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-ozymandias">
<title id="mark-title-ozymandias">A half-buried fluted column on a desert horizon, with a low sun-disk behind</title>
<desc>A frontispiece mark for "Ozymandias: A Static Site Framework".</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<circle cx="140" cy="155" r="34" stroke="currentColor" stroke-width="1.0" fill="none"/>
<line x1="32" y1="180" x2="248" y2="180" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M 110 100 L 168 96 L 172 110 L 106 114 Z" stroke-width="1.4"/>
<line x1="108" y1="107" x2="170" y2="103" stroke-width="1.4"/>
<line x1="118" y1="114" x2="120" y2="180" stroke-width="1.4"/>
<line x1="160" y1="110" x2="162" y2="180" stroke-width="1.4"/>
<line x1="128" y1="116" x2="130" y2="180" stroke-width="0.6"/>
<line x1="138" y1="115" x2="140" y2="180" stroke-width="0.6"/>
<line x1="148" y1="114" x2="150" y2="180" stroke-width="0.6"/>
</g>
<path d="M 32 180 Q 70 196 110 188 T 180 192 T 248 184" stroke="currentColor" stroke-width="0.8" fill="none" stroke-linecap="round"/>
<path d="M 50 180 Q 80 210 140 205 T 230 198" stroke="currentColor" stroke-width="0.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<g fill="currentColor" stroke="none">
<circle cx="80" cy="220" r="0.8"/>
<circle cx="200" cy="225" r="0.8"/>
<circle cx="155" cy="235" r="0.8"/>
<circle cx="100" cy="240" r="0.8"/>
<circle cx="180" cy="245" r="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-spec-dilemma">
<title id="mark-title-spec-dilemma">Many sparse arrows converging on a single dense focal point</title>
<desc>A frontispiece mark for "The Specification Dilemma" — divergent prompts collapsing to a homogeneous output.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.6">
<line x1="40" y1="60" x2="120" y2="135"/>
<line x1="65" y1="40" x2="125" y2="132"/>
<line x1="100" y1="35" x2="130" y2="130"/>
<line x1="140" y1="32" x2="140" y2="128"/>
<line x1="180" y1="35" x2="150" y2="130"/>
<line x1="215" y1="40" x2="155" y2="132"/>
<line x1="240" y1="60" x2="160" y2="135"/>
<line x1="35" y1="105" x2="120" y2="138"/>
<line x1="245" y1="105" x2="160" y2="138"/>
<line x1="55" y1="160" x2="125" y2="142"/>
<line x1="225" y1="160" x2="155" y2="142"/>
<line x1="80" y1="200" x2="130" y2="148"/>
<line x1="200" y1="200" x2="150" y2="148"/>
<line x1="115" y1="225" x2="135" y2="152"/>
<line x1="165" y1="225" x2="145" y2="152"/>
<line x1="140" y1="240" x2="140" y2="155"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="40" cy="60" r="0.9"/>
<circle cx="65" cy="40" r="0.9"/>
<circle cx="100" cy="35" r="0.9"/>
<circle cx="140" cy="32" r="0.9"/>
<circle cx="180" cy="35" r="0.9"/>
<circle cx="215" cy="40" r="0.9"/>
<circle cx="240" cy="60" r="0.9"/>
<circle cx="35" cy="105" r="0.9"/>
<circle cx="245" cy="105" r="0.9"/>
<circle cx="55" cy="160" r="0.9"/>
<circle cx="225" cy="160" r="0.9"/>
<circle cx="80" cy="200" r="0.9"/>
<circle cx="200" cy="200" r="0.9"/>
<circle cx="115" cy="225" r="0.9"/>
<circle cx="165" cy="225" r="0.9"/>
<circle cx="140" cy="240" r="0.9"/>
</g>
<g stroke="currentColor" fill="none">
<circle cx="140" cy="140" r="22" stroke-width="1.4"/>
<circle cx="140" cy="140" r="14" stroke-width="1.0"/>
<circle cx="140" cy="140" r="7" stroke-width="0.8"/>
</g>
<circle cx="140" cy="140" r="2.4" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,119 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-title-weenix">
<title id="mark-title-weenix">A process tree rooted at PID 1 living inside the kernel boundary, with hardware ticks outside — the roundel is the abstraction.</title>
<desc>Frontmatter mark for the essay "Weenix" — a Unix-like kernel from scratch.</desc>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<!-- Outer roundel: this IS the kernel boundary.
Slightly heavier weight than usual to mark its semantic role. -->
<circle cx="140" cy="140" r="128" stroke-width="1.1"/>
<!-- Inside: userspace process tree.
init (PID 1) sits just inside the top, descendants fan downward.
Process layout:
init (140, 60) depth 0
a (108, 100) depth 1
b (172, 100) depth 1
c ( 90, 142) depth 2
d (126, 142) depth 2
e (158, 142) depth 2
f (188, 142) depth 2
g (108, 184) depth 3
h (172, 184) depth 3
-->
<!-- Process boxes: small open rectangles. PID 1 is slightly larger. -->
<rect x="132" y="52" width="16" height="16" stroke-width="1.0"/>
<!-- Tiny "1" mark: a single short vertical inside the box, at offset, suggesting PID 1
without literal text. -->
<line x1="140" y1="56" x2="140" y2="64" stroke-width="0.7"/>
<rect x="101" y="93" width="14" height="14" stroke-width="0.9"/>
<rect x="165" y="93" width="14" height="14" stroke-width="0.9"/>
<rect x="84" y="135" width="12" height="12" stroke-width="0.8"/>
<rect x="120" y="135" width="12" height="12" stroke-width="0.8"/>
<rect x="152" y="135" width="12" height="12" stroke-width="0.8"/>
<rect x="182" y="135" width="12" height="12" stroke-width="0.8"/>
<rect x="102" y="177" width="12" height="12" stroke-width="0.8"/>
<rect x="166" y="177" width="12" height="12" stroke-width="0.8"/>
<!-- Fork edges: parent → child. Thin lines, drawn box-edge-to-box-edge. -->
<g stroke-width="0.7">
<!-- init → a -->
<line x1="138" y1="68" x2="110" y2="93"/>
<!-- init → b -->
<line x1="142" y1="68" x2="170" y2="93"/>
<!-- a → c -->
<line x1="105" y1="107" x2="92" y2="135"/>
<!-- a → d -->
<line x1="111" y1="107" x2="124" y2="135"/>
<!-- b → e -->
<line x1="169" y1="107" x2="156" y2="135"/>
<!-- b → f -->
<line x1="175" y1="107" x2="186" y2="135"/>
<!-- d → g -->
<line x1="124" y1="147" x2="110" y2="177"/>
<!-- e → h -->
<line x1="158" y1="147" x2="172" y2="177"/>
</g>
<!-- Syscall traces: a few thin arrows from process boxes to the kernel boundary,
showing requests crossing the abstraction. Kept sparse: 3 is enough.
Each is a short line ending in a small arrowhead just inside the roundel. -->
<!-- From box 'd' upward-left toward boundary -->
<g stroke-width="0.5" stroke-dasharray="1.5 2">
<line x1="123" y1="135" x2="80" y2="60"/>
</g>
<g stroke-width="0.6">
<line x1="80" y1="60" x2="86" y2="60"/>
<line x1="80" y1="60" x2="80" y2="66"/>
</g>
<!-- From box 'f' upward-right -->
<g stroke-width="0.5" stroke-dasharray="1.5 2">
<line x1="188" y1="138" x2="226" y2="100"/>
</g>
<g stroke-width="0.6">
<line x1="226" y1="100" x2="220" y2="100"/>
<line x1="226" y1="100" x2="226" y2="106"/>
</g>
<!-- From box 'g' downward-left -->
<g stroke-width="0.5" stroke-dasharray="1.5 2">
<line x1="108" y1="189" x2="68" y2="222"/>
</g>
<g stroke-width="0.6">
<line x1="68" y1="222" x2="74" y2="222"/>
<line x1="68" y1="222" x2="68" y2="216"/>
</g>
<!-- Outside the roundel: hardware. A sparse set of short radial tick marks
at compass positions, suggesting the device layer the kernel mediates. -->
<g stroke-width="0.7" opacity="0.85">
<!-- top-left cluster -->
<line x1="36" y1="64" x2="42" y2="68"/>
<line x1="32" y1="74" x2="38" y2="76"/>
<line x1="28" y1="86" x2="34" y2="86"/>
<!-- top-right cluster -->
<line x1="244" y1="64" x2="238" y2="68"/>
<line x1="248" y1="74" x2="242" y2="76"/>
<line x1="252" y1="86" x2="246" y2="86"/>
<!-- bottom-left cluster -->
<line x1="36" y1="216" x2="42" y2="212"/>
<line x1="32" y1="206" x2="38" y2="204"/>
<line x1="28" y1="194" x2="34" y2="194"/>
<!-- bottom-right cluster -->
<line x1="244" y1="216" x2="238" y2="212"/>
<line x1="248" y1="206" x2="242" y2="204"/>
<line x1="252" y1="194" x2="246" y2="194"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-simd-pqc">
<title id="mark-title-simd-pqc">A scalar lane stacked above eight parallel SIMD lanes, with a lattice of points beside</title>
<desc>A frontispiece mark for "Where Does SIMD Help Post-Quantum Cryptography?" — scalar versus vectorized arithmetic over an ML-KEM polynomial.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<rect x="50" y="68" width="180" height="14" stroke-width="1.0"/>
<line x1="62" y1="75" x2="218" y2="75" stroke-width="0.4" stroke-dasharray="3 3" opacity="0.6"/>
</g>
<circle cx="62" cy="75" r="1.4" fill="currentColor" stroke="none"/>
<g stroke="currentColor" fill="none">
<rect x="50" y="98" width="180" height="48" stroke-width="1.0"/>
</g>
<g stroke="currentColor" fill="none" stroke-width="0.5" opacity="0.7">
<line x1="72.5" y1="98" x2="72.5" y2="146"/>
<line x1="95" y1="98" x2="95" y2="146"/>
<line x1="117.5" y1="98" x2="117.5" y2="146"/>
<line x1="140" y1="98" x2="140" y2="146"/>
<line x1="162.5" y1="98" x2="162.5" y2="146"/>
<line x1="185" y1="98" x2="185" y2="146"/>
<line x1="207.5" y1="98" x2="207.5" y2="146"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="61.25" cy="122" r="1.4"/>
<circle cx="83.75" cy="122" r="1.4"/>
<circle cx="106.25" cy="122" r="1.4"/>
<circle cx="128.75" cy="122" r="1.4"/>
<circle cx="151.25" cy="122" r="1.4"/>
<circle cx="173.75" cy="122" r="1.4"/>
<circle cx="196.25" cy="122" r="1.4"/>
<circle cx="218.75" cy="122" r="1.4"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.8">
<path d="M 86 88 L 86 96"/>
<path d="M 83 93 L 86 96 L 89 93"/>
<path d="M 108 88 L 108 96"/>
<path d="M 105 93 L 108 96 L 111 93"/>
<path d="M 130 88 L 130 96"/>
<path d="M 127 93 L 130 96 L 133 93"/>
<path d="M 152 88 L 152 96"/>
<path d="M 149 93 L 152 96 L 155 93"/>
<path d="M 174 88 L 174 96"/>
<path d="M 171 93 L 174 96 L 177 93"/>
<path d="M 196 88 L 196 96"/>
<path d="M 193 93 L 196 96 L 199 93"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="62" cy="180" r="1.0"/>
<circle cx="86" cy="180" r="1.0"/>
<circle cx="110" cy="180" r="1.0"/>
<circle cx="134" cy="180" r="1.0"/>
<circle cx="158" cy="180" r="1.0"/>
<circle cx="182" cy="180" r="1.0"/>
<circle cx="206" cy="180" r="1.0"/>
<circle cx="62" cy="200" r="1.0"/>
<circle cx="86" cy="200" r="1.0"/>
<circle cx="110" cy="200" r="1.0"/>
<circle cx="134" cy="200" r="1.0"/>
<circle cx="158" cy="200" r="1.0"/>
<circle cx="182" cy="200" r="1.0"/>
<circle cx="206" cy="200" r="1.0"/>
<circle cx="62" cy="220" r="1.0"/>
<circle cx="86" cy="220" r="1.0"/>
<circle cx="110" cy="220" r="1.0"/>
<circle cx="134" cy="220" r="1.0"/>
<circle cx="158" cy="220" r="1.0"/>
<circle cx="182" cy="220" r="1.0"/>
<circle cx="206" cy="220" r="1.0"/>
</g>
<g stroke="currentColor" fill="none" stroke-width="0.4" opacity="0.5" stroke-linecap="round">
<line x1="62" y1="180" x2="206" y2="220"/>
<line x1="206" y1="180" x2="62" y2="220"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -18,5 +18,5 @@ This website is *not* an academic homepage, nor a blog, nor a portfolio — thou
</div>
<div class="hp-curiosity-row">
<a href="/memento-mori.html">Memento Mori</a><span class="hp-sep" aria-hidden="true">·</span><a href="/commonplace.html">Commonplace</a><span class="hp-sep" aria-hidden="true">·</span><a href="/colophon.html">Colophon</a><span class="hp-sep" aria-hidden="true">·</span><a href="/build/">Build</a><span class="hp-sep" aria-hidden="true">·</span><a href="#" data-random>Random</a>
<a href="/bibliography/">Bibliography</a><span class="hp-sep" aria-hidden="true">·</span><a href="/build/">Build</a><span class="hp-sep" aria-hidden="true">·</span><a href="/colophon.html">Colophon</a><span class="hp-sep" aria-hidden="true">·</span><a href="/commonplace.html">Commonplace</a><span class="hp-sep" aria-hidden="true">·</span><a href="/memento-mori.html">Memento Mori</a><span class="hp-sep" aria-hidden="true">·</span><a href="#" data-random>Random</a><span class="hp-sep" aria-hidden="true">·</span><a href="/stats/">Statistics</a>
</div>

62
content/library.mark.svg Normal file
View File

@ -0,0 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-library">
<title id="mark-title-library">An ex-libris bookplate — eight volumes of varied size on a shelf, with a single ribbon bookmark held between two of them</title>
<desc>A frontispiece mark for the Library — drawn in the ex-libris tradition. Eight vertical strokes, one per portal, of irregular height and weight; a small ribbon between two of them marks the reader's place.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<line x1="40" y1="200" x2="240" y2="200" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<g stroke="currentColor" fill="none" stroke-linecap="butt" stroke-linejoin="miter">
<line x1="58" y1="125" x2="58" y2="200" stroke-width="1.2"/>
<line x1="74" y1="142" x2="74" y2="200" stroke-width="0.8"/>
<line x1="86" y1="118" x2="86" y2="200" stroke-width="1.6"/>
<line x1="104" y1="135" x2="104" y2="200" stroke-width="1.0"/>
<line x1="120" y1="100" x2="120" y2="200" stroke-width="2.2"/>
<line x1="142" y1="128" x2="142" y2="200" stroke-width="1.2"/>
<line x1="158" y1="115" x2="158" y2="200" stroke-width="1.4"/>
<line x1="172" y1="138" x2="172" y2="200" stroke-width="0.6"/>
<line x1="186" y1="108" x2="186" y2="200" stroke-width="1.8"/>
<line x1="206" y1="130" x2="206" y2="200" stroke-width="1.0"/>
<line x1="222" y1="148" x2="222" y2="200" stroke-width="0.7"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="miter">
<line x1="55" y1="125" x2="61" y2="125" stroke-width="0.5"/>
<line x1="83" y1="118" x2="89" y2="118" stroke-width="0.5"/>
<line x1="116" y1="100" x2="124" y2="100" stroke-width="0.5"/>
<line x1="155" y1="115" x2="161" y2="115" stroke-width="0.5"/>
<line x1="183" y1="108" x2="189" y2="108" stroke-width="0.5"/>
</g>
<g stroke="currentColor" fill="none" stroke-linejoin="miter" stroke-linecap="butt">
<path d="M 130 100 L 130 222 L 134 216 L 138 222 L 138 100 Z" stroke-width="1.0"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.5" opacity="0.7">
<line x1="40" y1="206" x2="240" y2="206"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.6">
<line x1="60" y1="206" x2="60" y2="218"/>
<line x1="80" y1="206" x2="80" y2="222"/>
<line x1="100" y1="206" x2="100" y2="218"/>
<line x1="120" y1="206" x2="120" y2="225"/>
<line x1="140" y1="206" x2="140" y2="218"/>
<line x1="160" y1="206" x2="160" y2="222"/>
<line x1="180" y1="206" x2="180" y2="218"/>
<line x1="200" y1="206" x2="200" y2="225"/>
<line x1="220" y1="206" x2="220" y2="218"/>
</g>
<line x1="40" y1="226" x2="240" y2="226" stroke="currentColor" stroke-width="1.0" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

63
content/me/mark.svg Normal file
View File

@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-me">
<title id="mark-title-me">An invented constellation of eight stars of varied magnitude, lightly connected, over a low pre-dawn horizon, with a single brighter anchor-star slightly off the principal figure</title>
<desc>A frontispiece mark for the author's about page. The constellation is invented for this page: each star marks one of the page's named preoccupations (curiosity-and-creativity, aphantasia, music, mathematics, systems, language, running, present-moment), the lines mark which connect to which, and a ninth heavier point off the main figure stands for the one favorite the page admits — Karamazov. The horizon is low because the page locates the present moment in dawn over Rhode Island bays. No portrait, no initials, no logo: a person via their relations.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<line x1="36" y1="240" x2="244" y2="240" stroke="currentColor" stroke-width="0.5" stroke-linecap="round" opacity="0.55"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.4" opacity="0.5">
<line x1="140" y1="80" x2="100" y2="108"/>
<line x1="140" y1="80" x2="180" y2="108"/>
<line x1="100" y1="108" x2="78" y2="148"/>
<line x1="100" y1="108" x2="120" y2="160"/>
<line x1="180" y1="108" x2="200" y2="148"/>
<line x1="180" y1="108" x2="160" y2="160"/>
<line x1="120" y1="160" x2="160" y2="160"/>
<line x1="78" y1="148" x2="120" y2="160"/>
<line x1="200" y1="148" x2="160" y2="160"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="140" cy="80" r="2.6"/>
<circle cx="100" cy="108" r="1.8"/>
<circle cx="180" cy="108" r="1.8"/>
<circle cx="78" cy="148" r="1.6"/>
<circle cx="200" cy="148" r="1.6"/>
<circle cx="120" cy="160" r="1.4"/>
<circle cx="160" cy="160" r="1.4"/>
<circle cx="140" cy="200" r="1.2"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="140" cy="80" r="0.6"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="222" cy="92" r="2.0"/>
<circle cx="222" cy="92" r="3.4" opacity="0.18"/>
</g>
<g fill="currentColor" stroke="none">
<circle cx="56" cy="62" r="0.5"/>
<circle cx="84" cy="38" r="0.4"/>
<circle cx="148" cy="50" r="0.4"/>
<circle cx="208" cy="42" r="0.5"/>
<circle cx="244" cy="74" r="0.4"/>
<circle cx="50" cy="118" r="0.4"/>
<circle cx="240" cy="148" r="0.4"/>
<circle cx="42" cy="186" r="0.4"/>
<circle cx="246" cy="200" r="0.5"/>
<circle cx="68" cy="220" r="0.4"/>
<circle cx="220" cy="226" r="0.4"/>
</g>
<path d="M 36 240 Q 90 248 140 245 Q 190 242 244 240" stroke="currentColor" stroke-width="0.4" fill="none" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-memento-mori">
<title id="mark-title-memento-mori">A succession of waves making toward a pebbled shore beneath a low sun</title>
<desc>A frontispiece mark for "Memento Mori" — drawn from Sonnet 60: "Like as the waves make towards the pebbled shore, so do our minutes hasten to their end; each changing place with that which goes before, in sequent toil all forwards do contend."</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<circle cx="140" cy="74" r="22" stroke="currentColor" stroke-width="1.0" fill="none"/>
<line x1="48" y1="200" x2="232" y2="200" stroke="currentColor" stroke-width="1.0" stroke-linecap="round"/>
<g stroke="currentColor" fill="none" stroke-linecap="round">
<path d="M 48 196 Q 95 188 140 196 Q 185 204 232 196" stroke-width="0.4" opacity="0.35"/>
<path d="M 48 188 Q 95 178 140 188 Q 185 198 232 188" stroke-width="0.5" opacity="0.45"/>
<path d="M 48 178 Q 95 166 140 178 Q 185 190 232 178" stroke-width="0.7" opacity="0.6"/>
<path d="M 48 166 Q 95 152 140 166 Q 185 180 232 166" stroke-width="0.9" opacity="0.78"/>
<path d="M 48 152 Q 95 134 140 152 Q 185 170 232 152" stroke-width="1.2" opacity="1"/>
</g>
<circle cx="140" cy="152" r="1.4" fill="currentColor" stroke="none"/>
<g fill="currentColor" stroke="none">
<circle cx="62" cy="206" r="0.9"/>
<circle cx="78" cy="208" r="0.7"/>
<circle cx="92" cy="205" r="0.8"/>
<circle cx="108" cy="207" r="1.1"/>
<circle cx="122" cy="205" r="0.7"/>
<circle cx="135" cy="208" r="0.9"/>
<circle cx="148" cy="206" r="0.8"/>
<circle cx="162" cy="208" r="1.0"/>
<circle cx="176" cy="205" r="0.7"/>
<circle cx="190" cy="207" r="0.9"/>
<circle cx="204" cy="206" r="0.8"/>
<circle cx="218" cy="208" r="0.7"/>
<circle cx="70" cy="216" r="0.6"/>
<circle cx="100" cy="218" r="0.7"/>
<circle cx="128" cy="216" r="0.5"/>
<circle cx="156" cy="218" r="0.8"/>
<circle cx="182" cy="217" r="0.6"/>
<circle cx="210" cy="218" r="0.7"/>
<circle cx="84" cy="227" r="0.5"/>
<circle cx="118" cy="228" r="0.6"/>
<circle cx="148" cy="226" r="0.5"/>
<circle cx="178" cy="229" r="0.6"/>
<circle cx="208" cy="227" r="0.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

13
content/music/mark.svg Normal file
View File

@ -0,0 +1,13 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-music-title">
<title id="mark-music-title">Music: staff and treble curve</title>
<desc>Placeholder monogram for the Music index — a five-line staff crossed by a treble-clef-derived curve, abstracted to a single sweep through the staff lines.</desc>
<circle cx="140" cy="140" r="128" fill="none" stroke="currentColor" stroke-width="0.6"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.5">
<line x1="80" y1="110" x2="200" y2="110"/>
<line x1="80" y1="125" x2="200" y2="125"/>
<line x1="80" y1="140" x2="200" y2="140"/>
<line x1="80" y1="155" x2="200" y2="155"/>
<line x1="80" y1="170" x2="200" y2="170"/>
</g>
<path d="M 140 90 Q 165 125 145 145 Q 120 165 140 185 Q 160 205 130 200" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 981 B

View File

@ -0,0 +1,18 @@
---
title: "Selected Verse"
date: 2026-05-08
abstract: >
A placeholder poetry collection — replace this landing page with the
collection's actual title, abstract, and any prefatory note. Add
poems as `content/poetry/selected-verse/{slug}.md`; each compiles
with the standard poetry pipeline (hard line breaks, codex reading
mode). Rename the directory to retitle the collection.
---
This collection is a scaffold. Replace this body with a short
introduction describing the collection's organising principle — a
single author, a thematic arc, a translation project, a sequence
written in a particular season — or whatever frame matters to you.
Individual poems live in this directory and link from here
automatically through the site's backlink and tag systems.

View File

@ -0,0 +1,18 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-selectedverse-title">
<title id="mark-selectedverse-title">Selected Verse: open book</title>
<desc>Placeholder monogram for the Selected Verse poetry collection — an open book with a central spine and three lines of text on each page, abstracted from a printed octavo of poetry.</desc>
<circle cx="140" cy="140" r="128" fill="none" stroke="currentColor" stroke-width="0.6"/>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.8">
<line x1="140" y1="100" x2="140" y2="190"/>
<path d="M 140 100 Q 120 95 90 100 L 90 188 Q 120 185 140 190 Z"/>
<path d="M 140 100 Q 160 95 190 100 L 190 188 Q 160 185 140 190 Z"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="0.5">
<line x1="100" y1="120" x2="130" y2="120"/>
<line x1="100" y1="135" x2="130" y2="135"/>
<line x1="100" y1="150" x2="130" y2="150"/>
<line x1="150" y1="120" x2="180" y2="120"/>
<line x1="150" y1="135" x2="180" y2="135"/>
<line x1="150" y1="150" x2="180" y2="150"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

64
content/stats.mark.svg Normal file
View File

@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 280" role="img" aria-labelledby="mark-title-statistics">
<title id="mark-title-statistics">A small histogram of word-length buckets above a row of tag-frequency strokes, separated by a thin rule</title>
<desc>A frontispiece mark for the corpus statistics view of the build telemetry page. The upper register is a five-bucket histogram in the Tufte sparkbar tradition, encoding the word-length distribution. The lower register is a row of vertical strokes of varied length representing tag frequency across the corpus. A thin rule between the two separates them — the page reports many distributions, and the mark holds two of them side by side.</desc>
<circle cx="140" cy="140" r="128" stroke="currentColor" stroke-width="0.6" fill="none"/>
<g stroke="currentColor" fill="none" stroke-linecap="round">
<line x1="64" y1="140" x2="216" y2="140" stroke-width="0.4" opacity="0.55"/>
<line x1="64" y1="100" x2="64" y2="140" stroke-width="0.4" opacity="0.55"/>
</g>
<g stroke="currentColor" fill="none" stroke-linecap="butt" stroke-linejoin="miter">
<rect x="68" y="116" width="22" height="24" stroke-width="0.9" fill="none"/>
<line x1="68" y1="120" x2="90" y2="120" stroke-width="0.4" opacity="0.5"/>
<line x1="68" y1="128" x2="90" y2="128" stroke-width="0.4" opacity="0.5"/>
<line x1="68" y1="136" x2="90" y2="136" stroke-width="0.4" opacity="0.5"/>
<rect x="98" y="86" width="22" height="54" stroke-width="0.9" fill="none"/>
<rect x="128" y="74" width="22" height="66" stroke-width="0.9" fill="none"/>
<rect x="158" y="92" width="22" height="48" stroke-width="0.9" fill="none"/>
<rect x="188" y="110" width="22" height="30" stroke-width="0.9" fill="none"/>
<line x1="188" y1="116" x2="210" y2="116" stroke-width="0.4" opacity="0.5"/>
<line x1="188" y1="124" x2="210" y2="124" stroke-width="0.4" opacity="0.5"/>
</g>
<g stroke="currentColor" fill="none" stroke-width="0.4" stroke-linecap="round">
<line x1="79" y1="140" x2="79" y2="143"/>
<line x1="109" y1="140" x2="109" y2="143"/>
<line x1="139" y1="140" x2="139" y2="143"/>
<line x1="169" y1="140" x2="169" y2="143"/>
<line x1="199" y1="140" x2="199" y2="143"/>
</g>
<line x1="56" y1="160" x2="224" y2="160" stroke="currentColor" stroke-width="0.5" opacity="0.6" stroke-linecap="round"/>
<g stroke="currentColor" fill="none" stroke-linecap="butt" stroke-linejoin="miter">
<line x1="64" y1="178" x2="64" y2="218" stroke-width="1.4"/>
<line x1="76" y1="190" x2="76" y2="218" stroke-width="0.8"/>
<line x1="88" y1="172" x2="88" y2="218" stroke-width="1.6"/>
<line x1="100" y1="200" x2="100" y2="218" stroke-width="0.6"/>
<line x1="112" y1="184" x2="112" y2="218" stroke-width="1.0"/>
<line x1="124" y1="166" x2="124" y2="218" stroke-width="2.0"/>
<line x1="138" y1="194" x2="138" y2="218" stroke-width="0.7"/>
<line x1="150" y1="180" x2="150" y2="218" stroke-width="1.2"/>
<line x1="162" y1="188" x2="162" y2="218" stroke-width="0.9"/>
<line x1="174" y1="170" x2="174" y2="218" stroke-width="1.6"/>
<line x1="186" y1="198" x2="186" y2="218" stroke-width="0.7"/>
<line x1="198" y1="184" x2="198" y2="218" stroke-width="1.0"/>
<line x1="210" y1="206" x2="210" y2="218" stroke-width="0.5"/>
</g>
<line x1="56" y1="222" x2="224" y2="222" stroke="currentColor" stroke-width="0.7" stroke-linecap="round"/>
<g stroke="currentColor" fill="none" stroke-width="0.5" stroke-linecap="round" opacity="0.85">
<line x1="64" y1="222" x2="64" y2="227"/>
<line x1="100" y1="222" x2="100" y2="227"/>
<line x1="140" y1="222" x2="140" y2="227"/>
<line x1="180" y1="222" x2="180" y2="227"/>
<line x1="218" y1="222" x2="218" y2="227"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -22,6 +22,7 @@ executable site
SimilarLinks
Compilers
Contexts
Marks
Patterns
Photography
Stats

197
static/css/marks.css Normal file
View File

@ -0,0 +1,197 @@
/* ============================================================
FRONTMATTER MARKS monogram + epistemic figure
See MARKS.md for the full specification.
The header is a three-column grid sitting above the
cursive-L `content-divider`. Either or both mark columns
may be absent; the grid collapses cleanly because empty
columns size to zero (the conditional template guards
suppress the slot div entirely when its SVG is empty).
============================================================ */
/* Three-column grid:
[ monogram ] [ title block 1fr ] [ epistemic figure ]
The 1fr column absorbs all extra width. Mark slots are
sized to their content via grid auto-placement. When the
template guard suppresses one or both slot divs, the column
simply does not exist for layout purposes. */
.essay-frontmatter {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
column-gap: clamp(0.75rem, 2vw, 1.75rem);
row-gap: 0.75rem;
align-items: center;
margin-bottom: 1rem;
}
/* Reading variant (poetry / fiction) and blog variant share the
same grid; declared explicitly so future tweaks can diverge. */
.essay-frontmatter--reading,
.essay-frontmatter--blog {
grid-template-columns: auto minmax(0, 1fr) auto;
}
/* Title block stays in the centre; never shrinks below 0. */
.frontmatter-title {
min-width: 0;
}
/* Centre the title and metadata under the H1 matches the
visual rhythm of the reference mockup, where the byline,
abstract, and compact strip sit in a stacked column. */
.frontmatter-title > .page-title {
margin-bottom: 0.25rem;
}
/* Subtitle: a short secondary line, lighter than the H1, never
competing with it. Kept restrained so existing essays without
a subtitle render unchanged. */
.essay-subtitle {
font-family: var(--font-serif);
font-size: 1.05rem;
font-style: italic;
color: var(--text-muted);
margin: 0 0 0.75rem 0;
line-height: 1.35;
}
/* Mark slot: a column wrapper that hosts the monogram or the
epistemic figure. The slot itself is a plain block; the SVG
inside controls its dimensions. line-height: 0 suppresses the
baseline descender gap that would otherwise add a few pixels
below the figure. */
.frontmatter-mark-slot {
line-height: 0;
}
/* The mark itself: a <figure> wrapping the inlined SVG. Two
inherited styles need to be overridden:
`figure { margin: 2rem 0; max-width: 100% }` in images.css
(otherwise vertical margin pushes the mark off-grid).
`#markdownBody figure { background, padding, border,
border-radius, box-shadow }` in typography.css (the
card-style chrome that wraps inline body figures with
a subtle paper-edge effect not what we want for a
frontispiece glyph that should sit seamlessly on the
page background).
The `#markdownBody` prefix on the second rule has higher
specificity than a plain `.frontmatter-mark`, so we match
that prefix here too. */
.frontmatter-mark,
#markdownBody .frontmatter-mark {
margin: 0;
padding: 0;
width: 170px;
height: 170px;
max-width: none;
background: none;
border: none;
border-radius: 0;
box-shadow: none;
color: var(--text);
}
/* SVG fills its parent figure exactly. !important defeats the
global `img, video, svg { max-width: 100%; height: auto }` in
base.css for our specific case (which would otherwise leave the
figure as a tall, narrow strip when the column is grid-auto'd). */
.frontmatter-mark svg {
display: block;
width: 100%;
height: 100%;
color: inherit;
}
/* Epistemic figure inside an anchor: the anchor inherits color
from the slot (so currentColor in the SVG resolves to --text)
and stays inline-block so it doesn't pick up underline / link
chrome from the surrounding prose styles. */
.frontmatter-mark-slot--right > a {
display: inline-block;
color: inherit;
text-decoration: none;
line-height: 0;
}
/* ============================================================
COMPACT-STRIP CHIP ADDITIONS
============================================================ */
/* Inline trend arrow ( ) appended to the confidence percentage.
No leading separator sits flush against the value it modifies. */
.ep-trend {
display: inline-block;
margin-left: 0.15em;
font-family: var(--font-sans);
font-size: 0.7rem;
color: var(--text-muted);
vertical-align: 0.05em;
}
.ep-row .ep-trend::before { content: none; }
/* Peer-status chip same typography as .ep-status, distinct
data-term so future revisions can style it differently. */
.ep-peer-status {
font-family: var(--font-sans);
font-size: 0.72rem;
font-variant-caps: all-small-caps;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.ep-peer-status::before {
content: "·\00a0";
color: var(--border);
}
/* Retracted: strikethrough across the chip text the spec's
§4.1 visual marker for a retracted piece. */
.ep-peer-status--retracted {
text-decoration: line-through;
text-decoration-thickness: 1px;
}
/* Proved-confidence variant: same row typography, italic to
differentiate from a numeric XX% confidence chip. */
.ep-row--proved {
font-style: italic;
}
/* ============================================================
MOBILE / NARROW SCREENS
Stack the marks above and below the title at the same
breakpoint where the TOC sidebar collapses (900px), and
shrink the SVGs at the mobile breakpoint (680px) per
MARKS.md §3.3 (170px desktop 130px mobile).
============================================================ */
@media (max-width: 900px) {
.essay-frontmatter {
grid-template-columns: minmax(0, 1fr);
justify-items: center;
}
.frontmatter-mark-slot--left { order: 0; }
.frontmatter-title { order: 1; }
.frontmatter-mark-slot--right { order: 2; }
.frontmatter-title { text-align: center; }
}
@media (max-width: 680px) {
.frontmatter-mark {
width: 130px;
height: 130px;
}
}
/* ============================================================
FOCUS MODE
The Display panel's focus mode (toggled via the
data-focus-mode attribute on <html>, see settings.js)
suppresses marks alongside the TOC and other chrome.
============================================================ */
[data-focus-mode] .frontmatter-mark,
[data-focus-mode] .frontmatter-mark-slot {
display: none;
}
[data-focus-mode] .essay-frontmatter {
grid-template-columns: minmax(0, 1fr);
}

View File

@ -0,0 +1,42 @@
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Epistemic figure reference: skeleton (no field data)">
<!-- Reference skeleton for the epistemic figure generator
(build/Marks.hs). Renders the parts that are present whether
or not frontmatter fields are set: the outer roundel, the
four guide circles, the six radial axes, and the stability-
tick scale at 12 o'clock (all five ticks at "inactive"
opacity 0.4). The polygon, vertex marks, trust label, and
result-shape glyph are absent from this skeleton; they
appear only when frontmatter supplies values.
Used as a visual-regression baseline: compare against
generator output stripped of data-bearing elements. The
coordinate values mirror MARKS.md §3.3 exactly. -->
<!-- Outer roundel: r=88 inner, r=90 outer, stroke 0.5 -->
<circle cx="100.00" cy="100.00" r="88.00" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.7"/>
<circle cx="100.00" cy="100.00" r="90.00" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.7"/>
<!-- Inner guide circles at 0.2, 0.4, 0.6, 0.8 of axis radius (R=80) -->
<circle cx="100.00" cy="100.00" r="16.00" fill="none" stroke="currentColor" stroke-width="0.25" opacity="0.4"/>
<circle cx="100.00" cy="100.00" r="32.00" fill="none" stroke="currentColor" stroke-width="0.25" opacity="0.4"/>
<circle cx="100.00" cy="100.00" r="48.00" fill="none" stroke="currentColor" stroke-width="0.25" opacity="0.4"/>
<circle cx="100.00" cy="100.00" r="64.00" fill="none" stroke="currentColor" stroke-width="0.25" opacity="0.4"/>
<!-- Six radial axes: 0° confidence, 60° novelty, 120° practicality,
180° scope, 240° evidence, 300° importance -->
<line x1="100.00" y1="100.00" x2="100.00" y2="20.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<line x1="100.00" y1="100.00" x2="169.28" y2="60.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<line x1="100.00" y1="100.00" x2="169.28" y2="140.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<line x1="100.00" y1="100.00" x2="100.00" y2="180.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<line x1="100.00" y1="100.00" x2="30.72" y2="140.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<line x1="100.00" y1="100.00" x2="30.72" y2="60.00" stroke="currentColor" stroke-width="0.3" opacity="0.55"/>
<!-- Stability tick scale at 12 o'clock; all five at inactive
opacity 0.4 in this skeleton (no frontmatter → no live
stability label). -->
<line x1="100.00" y1="10.00" x2="100.00" y2="8.50" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
<line x1="76.71" y1="11.55" x2="75.32" y2="10.13" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
<line x1="123.29" y1="11.55" x2="124.68" y2="10.13" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
<line x1="55.00" y1="22.06" x2="53.70" y2="19.81" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
<line x1="145.00" y1="22.06" x2="146.30" y2="19.81" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,26 @@
<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="mark-template-title">
<title id="mark-template-title">Frontmatter monogram template</title>
<desc>A reference monogram demonstrating the §2.2 visual contract: outer roundel, currentColor strokes, no fills except small filled point-marks, round line caps. Authors copy this file to content/{section}/{slug}/mark.svg (or content/{section}/{slug}.mark.svg for flat-form pieces) and replace the inner geometry with a glyph abstracted from their work's central concept.</desc>
<!-- Outer roundel — required (§2.2 M3). Do not remove. -->
<circle cx="140" cy="140" r="128" fill="none" stroke="currentColor" stroke-width="0.6"/>
<!-- Inner geometry: replace this group with your monogram.
Constraints (§2.2):
stroke-width drawn from {0.3, 0.5, 0.6, 0.8, 1.0, 1.2, 1.4}
fill="none" everywhere except small filled point-marks
stroke-linecap="round", stroke-linejoin="round" everywhere
no <text>, <image>, gradients, filters, embedded fonts, rasters
file size ≤ 8 KiB
well-formed XML (round-trips through xmllint --noout) -->
<g stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<!-- Sample horizon line. -->
<line x1="60" y1="180" x2="220" y2="180" stroke-width="0.8"/>
<!-- Sample plinth — a low rectangular form on the horizon. -->
<rect x="120" y="160" width="40" height="20" stroke-width="0.6"/>
<!-- Sample column shaft. -->
<line x1="140" y1="160" x2="140" y2="100" stroke-width="1.0"/>
<!-- Sample capital. -->
<line x1="125" y1="100" x2="155" y2="100" stroke-width="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,8 +1,21 @@
<main id="markdownBody" data-pagefind-body>
<header class="essay-frontmatter essay-frontmatter--blog">
$if(monogramSvg)$
<div class="frontmatter-mark-slot frontmatter-mark-slot--left">$monogramSvg$</div>
$endif$
<div class="frontmatter-title">
<h1 class="page-title">$title$</h1>
$if(subtitle)$<p class="essay-subtitle">$subtitle$</p>$endif$
$if(date)$
<p class="post-date"><time class="date-hover" datetime="$date-iso$" data-date-start="$date-iso$">$date$</time></p>
$endif$
</div>
$if(epistemicSvg)$
<div class="frontmatter-mark-slot frontmatter-mark-slot--right">
<a href="#epistemic" aria-label="Jump to epistemic profile">$epistemicSvg$</a>
</div>
$endif$
</header>
$body$
$if(backlinks)$
<footer class="page-meta-footer">

View File

@ -9,8 +9,22 @@
</nav>
</aside>
<main id="markdownBody" data-pagefind-body$if(no-collapse)$ data-no-collapse$endif$>
<header class="essay-frontmatter">
$if(monogramSvg)$
<div class="frontmatter-mark-slot frontmatter-mark-slot--left">$monogramSvg$</div>
$endif$
<div class="frontmatter-title">
<h1 class="page-title">$title$</h1>
$partial("templates/partials/metadata.html")$
$if(subtitle)$<p class="essay-subtitle">$subtitle$</p>$endif$
$partial("templates/partials/metadata-header.html")$
</div>
$if(epistemicSvg)$
<div class="frontmatter-mark-slot frontmatter-mark-slot--right">
<a href="#epistemic" aria-label="Jump to epistemic profile">$epistemicSvg$</a>
</div>
$endif$
</header>
$partial("templates/partials/metadata-tail.html")$
$if(summary)$
<div class="essay-summary" data-pagefind-ignore="all">
<div class="essay-summary-label">Summary</div>

View File

@ -33,6 +33,7 @@ $if(description)$<meta name="twitter:description" content="$description$">$endif
<link rel="stylesheet" href="/css/selection-popup.css">
<link rel="stylesheet" href="/css/annotations.css">
<link rel="stylesheet" href="/css/images.css">
<link rel="stylesheet" href="/css/marks.css">
$if(home)$<link rel="stylesheet" href="/css/home.css">$endif$
$if(library)$<link rel="stylesheet" href="/css/library.css">$endif$
$if(library)$<link rel="stylesheet" href="/css/item-card.css">$endif$

View File

@ -0,0 +1,23 @@
<div class="metadata metadata-header">
<div class="meta-row meta-authors">
<span class="meta-label">by</span>$if(poet)$$poet$$else$$for(author-links)$<a href="$author-url$">$author-name$</a>$sep$, $endfor$$endif$
</div>
$if(abstract)$
<div class="meta-row meta-description">
$abstract$
</div>
$endif$
$if(status)$
<div class="meta-row meta-epistemic-strip" data-pagefind-ignore="all">
$if(overall-score)$<span class="ep-trust" data-ep-term="trust"><span class="ep-score">$overall-score$%</span> trust</span>$endif$
<span class="ep-status" data-ep-term="status">$status$</span>
$if(peer-status-display)$<span class="ep-peer-status ep-peer-status--$peer-status$" data-ep-term="peer-status">$peer-status-display$</span>$endif$
$if(confidence-proved)$<span class="ep-row ep-row--proved" data-ep-term="confidence">proved confidence</span>$else$$if(confidence)$<span class="ep-row" data-ep-term="confidence">$confidence$% confidence$if(confidence-trend)$<span class="ep-trend" aria-hidden="true">$confidence-trend$</span>$endif$</span>$endif$$endif$
$if(importance-dots)$<span class="ep-row" data-ep-term="importance"><span class="ep-dots">$importance-dots$</span> importance</span>$endif$
$if(evidence-dots)$<span class="ep-row" data-ep-term="evidence"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$
$if(scope)$<span class="ep-row" data-ep-term="scope">$scope$ scope</span>$endif$
$if(novelty)$<span class="ep-row" data-ep-term="novelty">$novelty$ novelty</span>$endif$
$if(practicality)$<span class="ep-row" data-ep-term="practicality">$practicality$ practicality</span>$endif$
</div>
$endif$
</div>

View File

@ -1,4 +1,4 @@
<div class="metadata">
<div class="metadata metadata-tail">
$if(essay-tags)$
<div class="meta-row meta-tags">
$for(essay-tags)$<a class="meta-tag" href="$tag-url$">$tag-name$</a>$endfor$
@ -9,31 +9,11 @@
$for(essay-keywords)$<a class="meta-keyword" href="$kw-url$">$kw-name$</a>$endfor$
</div>
$endif$
$if(abstract)$
<div class="meta-row meta-description">
$abstract$
</div>
$endif$
<div class="meta-row meta-authors">
<span class="meta-label">by</span>$if(poet)$$poet$$else$$for(author-links)$<a href="$author-url$">$author-name$</a>$sep$, $endfor$$endif$
</div>
$if(affiliation-links)$
<div class="meta-row meta-affiliation">
$for(affiliation-links)$$if(affiliation-url)$<a href="$affiliation-url$">$affiliation-name$</a>$else$$affiliation-name$$endif$$sep$ · $endfor$
</div>
$endif$
$if(status)$
<div class="meta-row meta-epistemic-strip" data-pagefind-ignore="all">
$if(overall-score)$<span class="ep-trust" data-ep-term="trust"><span class="ep-score">$overall-score$%</span> trust</span>$endif$
<span class="ep-status" data-ep-term="status">$status$</span>
$if(confidence)$<span class="ep-row" data-ep-term="confidence">$confidence$% confidence</span>$endif$
$if(importance-dots)$<span class="ep-row" data-ep-term="importance"><span class="ep-dots">$importance-dots$</span> importance</span>$endif$
$if(evidence-dots)$<span class="ep-row" data-ep-term="evidence"><span class="ep-dots">$evidence-dots$</span> evidence quality</span>$endif$
$if(scope)$<span class="ep-row" data-ep-term="scope">$scope$ scope</span>$endif$
$if(novelty)$<span class="ep-row" data-ep-term="novelty">$novelty$ novelty</span>$endif$
$if(practicality)$<span class="ep-row" data-ep-term="practicality">$practicality$ practicality</span>$endif$
</div>
$endif$
<nav class="meta-row meta-pagelinks" aria-label="Page sections">
$if(version-history-range)$
<a href="#version-history" class="date-hover"

View File

@ -1,7 +1,16 @@
<div id="reading-progress" aria-hidden="true"></div>
<main id="markdownBody" data-pagefind-body$if(no-collapse)$ data-no-collapse$endif$>
<header class="essay-frontmatter essay-frontmatter--reading">
$if(monogramSvg)$
<div class="frontmatter-mark-slot frontmatter-mark-slot--left">$monogramSvg$</div>
$endif$
<div class="frontmatter-title">
<h1 class="page-title">$title$</h1>
$partial("templates/partials/metadata.html")$
$if(subtitle)$<p class="essay-subtitle">$subtitle$</p>$endif$
$partial("templates/partials/metadata-header.html")$
</div>
</header>
$partial("templates/partials/metadata-tail.html")$
$if(summary)$
<div class="essay-summary" data-pagefind-ignore="all">
<div class="essay-summary-label">Summary</div>