Marks I
This commit is contained in:
parent
1274b36d42
commit
7f7c029601
|
|
@ -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 130–280 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 (0–100) | confidence axis length |
|
||||
| `importance` | yes (1–5) | importance axis length |
|
||||
| `evidence` | yes (1–5) | 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 0–1 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 0–100; 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,
|
||||
1–1.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 8–10 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 §§2–9 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.
|
||||
83
WRITING.md
83
WRITING.md
|
|
@ -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 # 0–100 integer (%)
|
||||
confidence: 72 # 0–100 integer (%); also accepts the sentinel `proved` / `proven` for formal mathematical results
|
||||
importance: 3 # 1–5 integer (rendered as filled/empty dots ●●●○○)
|
||||
evidence: 2 # 1–5 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
|
||||
|
|
|
|||
|
|
@ -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,22 +496,36 @@ 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)
|
||||
case lookupStringList "confidence-history" meta of
|
||||
Nothing -> fail "no confidence history"
|
||||
Just xs -> case lastTwo xs of
|
||||
Nothing -> fail "no confidence history"
|
||||
Just (prevS, curS) ->
|
||||
let prev = readMaybe prevS :: Maybe Int
|
||||
cur = readMaybe curS :: Maybe Int
|
||||
in case (prev, cur) of
|
||||
(Just p, Just c)
|
||||
| c - p > trendThreshold -> return "\x2191" -- ↑
|
||||
| p - c > trendThreshold -> return "\x2193" -- ↓
|
||||
| otherwise -> return "\x2192" -- →
|
||||
_ -> return "\x2192"
|
||||
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"
|
||||
Just (prevS, curS) ->
|
||||
let prev = readMaybe prevS :: Maybe Int
|
||||
cur = readMaybe curS :: Maybe Int
|
||||
in case (prev, cur) of
|
||||
(Just p, Just c)
|
||||
| c - p > trendThreshold -> return "\x2191" -- ↑
|
||||
| p - c > trendThreshold -> return "\x2193" -- ↓
|
||||
| otherwise -> return "\x2192" -- →
|
||||
_ -> return "\x2192"
|
||||
where
|
||||
trendThreshold :: Int
|
||||
trendThreshold = 5
|
||||
|
|
@ -534,11 +559,21 @@ confidenceTrendField = field "confidence-trend" $ \item -> do
|
|||
--
|
||||
-- Formula: raw = conf/100 · 0.6 + (ev − 1)/4 · 0.4 (0–1)
|
||||
-- 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
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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 -- ^ 1–5 ordinal.
|
||||
, epEvidence :: Maybe Int -- ^ 1–5 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 0–100 (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=\""Fira Sans", 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>"
|
||||
]
|
||||
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ executable site
|
|||
SimilarLinks
|
||||
Compilers
|
||||
Contexts
|
||||
Marks
|
||||
Patterns
|
||||
Photography
|
||||
Stats
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -1,8 +1,21 @@
|
|||
<main id="markdownBody" data-pagefind-body>
|
||||
<h1 class="page-title">$title$</h1>
|
||||
$if(date)$
|
||||
<p class="post-date"><time class="date-hover" datetime="$date-iso$" data-date-start="$date-iso$">$date$</time></p>
|
||||
$endif$
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -9,8 +9,22 @@
|
|||
</nav>
|
||||
</aside>
|
||||
<main id="markdownBody" data-pagefind-body$if(no-collapse)$ data-no-collapse$endif$>
|
||||
<h1 class="page-title">$title$</h1>
|
||||
$partial("templates/partials/metadata.html")$
|
||||
<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>
|
||||
$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>
|
||||
|
|
|
|||
|
|
@ -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$
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
@ -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$>
|
||||
<h1 class="page-title">$title$</h1>
|
||||
$partial("templates/partials/metadata.html")$
|
||||
<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>
|
||||
$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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue