npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@overpunch/cedars-engine

v8.0.0

Published

Compute a font's **CEDARS+** typographic profile — **C**ontrast, **E**nergy, **D**etails, **A**xis, **R**hythm, **S**tructure — directly from its outline geometry. Framework-agnostic; runs in the browser and in Node. The profile is modelled (since v3) as

Readme

@overpunch/cedars-engine

Compute a font's CEDARS+ typographic profile — Contrast, Energy, Details, Axis, Rhythm, Structure — directly from its outline geometry. Framework-agnostic; runs in the browser and in Node. The profile is modelled (since v3) as continuous scales + a stress angle + categorical facet classifiers.

v6 in one line: Rhythm now means what CEDARS means by it. The spacing-regularity scale was called pattern and the stem-width scale was called rhythm, which pointed the letter R at the wrong measurement. Regularity is now rhythm; width is now width, under the +. No geometry changed and no landmark moved — see The six letters, and the plus and The two-version discipline.

v5 in one line: scores are not clamped to 0–100. Each scale's 0 and 100 are two named landmarks — reference archetypes, like the 0 °C and 100 °C of water — so a hairline Didone honestly scores 167 and an ultra-condensed face −139. See Landmarks, not limits.

The measurements themselves (redesigned in v4 for accuracy) are unchanged in v5 and v6:

  • Contrast — perpendicular-corrected p95/p5 ring thickness around the de-slanted bowl (no more phantom stress on squarish monolines).
  • Energy — springing suddenness + springing height of the stem→arch transition, empirically calibrated on real fonts (the old counter-ratio was weight-noisy and direction-ambiguous; the refutation history is documented in src/metrics/energy.ts).
  • Width — de-slanted stem-centerline distance ÷ x-height (centerlines, not outer edges, so weight doesn't read as width). Signed about a normal-proportion zero.
  • Rhythm — evenness of the sidebearing budget within structural classes (round letters are designed tighter than straight ones — that's rhythm working, not irregularity); monospace → n/a, not a nonsense score.
  • Aperture — terminal-endpoint distance ÷ counter height; a genuinely closed c reads 0, not unavailable.
  • Axis — closed-form second-harmonic fit of the whole ring-thickness series (sub-degree, vs the old 2° argmin grid), with a graded stressConfidence.

CEDARS+ is I Love Typography's classification system. CEDARS™ is ILT's, and the name is used here with their permission — this package is an independent implementation that reads a profile from outline geometry and does not reproduce ILT's own scores or data. It is the shared engine behind the CEDARS Analyzer app and the @overpunch/sanity-font-manager auto-tagging step.

Install

npm install @overpunch/cedars-engine

opentype.js is bundled as a runtime dependency and installs automatically.

Three kinds of quantity, not six scales

CEDARS descriptors are not all sliders. From I Love Typography's own taxonomy they split into three kinds, and the engine (since v3) models each honestly instead of forcing everything onto a 0–100 scale:

  • Continuous scales (landmark-anchored, unbounded): Contrast, Energy, Width (proportion — signed), Rhythm (spacing regularity), Aperture (open ↔ closed).
  • An angle: Axis (stress orientation), as an axial angle mod 180° with an explicit monoline / "no stress" state — a monoline face has no stress direction, so it never collapses onto "angle 0" the way v2's signed magnitude did.
  • Categorical facets (soft classifiers, each a probability distribution): Loops (7 named bowl shapes), Terminals, Transitions, Fill, Construction. A triangle no longer reads as "near" a rectangle on a 1-D fill ratio.

The six letters, and the plus

CEDARS is six letters plus room beyond them. This engine measures some of that geometrically, and which measurement serves which letter is exported as data rather than left to prose, so a consumer can present a profile under the letters a type designer already knows:

import { CEDARS_LETTERS, CEDARS_PLUS_SCALES } from "@overpunch/cedars-engine";

| Letter | Scales | Facets | Other | |---|---|---|---| | C Contrast | contrast | — | — | | E Energy | energy | — | — | | D Details | — | terminals, transitions, fill | — | | A Axis | — | — | the stress angle (neither scale nor facet) | | R Rhythm | rhythm | — | — | | S Structure | aperture | loops, construction | — | | + | width | — | plus.xHeightRatio, plus.capHeightRatio, plus.isMonospaced |

Why Width sits under the plus. Proportion is not one of the six letters. Through v5 the engine called it rhythm anyway, which left CEDARS' R pointing at stem width while the regularity scale it should have named was filed under pattern — a letter CEDARS does not have. The collision was real enough to leak: sanity-font-manager carried if (key === 'rhythm') return 'Width' to correct the label at the display layer. v6 fixes the names instead of the labels.

Two assignments in the table are readings rather than quotations, and are the first things to revisit if ILT's own grouping differs — both are flagged as such in src/types.ts:

  • fill under Details. It is ILT's own cedarsFill, so it belongs to one of the six; Details is the closest of them.
  • aperture under Structure. ILT's Structure covers "open vs closed", which is exactly what aperture reads.

One open question: the source comment in src/metrics/rhythm.ts records that ILT's own field name for this scale is cedarsPattern. CEDARS has no P, so that is presumably an internal name for R — but it is worth confirming, because if ILT really does call regularity "Pattern" then the letter map above is still right and only the key name is arguable.

Landmarks, not limits

A CEDARS score is not a percentage and not a percentile. Every scale is defined by two named semantic landmarks — describable reference archetypes — and the map between them is linear (or log-linear) and unclamped in both directions:

| Scale | Raw quantity | Score 0 landmark | Score 100 landmark | Map | |---|---|---|---|---| | Contrast | thick/thin ratio r | monoline floor, r = 1.0 | Didone archetype, r = 6.0 | 100·ln(r)/ln 6 | | Energy | secantDeg + 80·(springHeight/xHeight) | quarter-round shoulder, 45° + 0.5·xH = 85 | broad-nib branch, 80° + 0.85·xH = 148 | (raw − 85)·100/63 | | Width | width ratio w | centre, text-normal n, w = 2/3 | ±100 at ×1.5 / ÷1.5 (w = 1.0 / 4/9) | 100·ln(w/(2/3))/ln 1.5 | | Rhythm | coefficient of variation cv | deviation equals budget, cv = 1 | mechanical evenness, cv = 0 | 100·(1 − cv) | | Aperture | openness ratio a | sealed, a = 0 (physical floor) | full-counter, a = 1.0 | 100·a |

Three consequences worth internalising:

  1. A landmark is a reference point, never a maximum. Scoring past 0 or 100 is a correct reading, not an error. Do not clamp, range-validate, or store these as 0..100.
  2. New fonts can never move existing scores. v4's anchors were corpus percentiles (p5/p95 of a 110-font sample), so every re-sweep silently re-scored the entire corpus and no two vintages were comparable. Landmarks are defined without reference to any sample.
  3. Width is signed. 0 means normal proportion, negative is condensed, positive extended — a real zero, which the v4 scale (0 = "the narrowest thing in our corpus") never had.

Labels follow the same logic. Contrast and Width are raw-keyed (thresholds on the measured quantity, so they survive a landmark re-tune); Energy, Rhythm and Aperture are score-banded, with the overflow regions named rather than folded into the end band — inert / explosive for Energy, arrhythmic for Rhythm, wide open for Aperture, and the symmetric very condensed / condensed / normal / extended / very extended for signed Width.

Impossible situations, honestly scored

The scales that used to saturate now say what they measured. Real Fontsource faces:

| Face | Score | What v4 reported | |---|---|---| | Playfair Display 900 | Contrast 113 (r = 7.5) | 100 — tied with every other high-contrast face | | Hairline Didones (r = 12–25) | Contrast 139–180 | 100 | | Oswald 400 | Width −129 (w = 0.39) | 0 — "as condensed as anything can be" | | Ultra-condensed (w = 0.38) | Width −139 | 0 | | Courier Prime | Aperture 107 (a = 1.07) | ~82, and its rhythm is correctly n/a | | Libre Bodoni 400 | Energy −1 — gentler than a quarter-round shoulder | 0 | | Sealed c | Aperture exactly 0 — the one genuine floor | 0 |

Naming: one label set, from one place

Through v7 the package produced labels twice: each metric computed its own inline ternary, and the naming layer banded the raw quantities separately. Two vocabularies for one profile, free to drift — and three of the metric versions banded on a score rather than on a measurement.

Since v8 there is one source. profile.labels comes from REFERENCE_BANDS via labelFor, so the cut points are exported, stated in their own units and versioned (REFERENCE_BANDS_VERSION). The collapse was verified label-for-label against v7 across 2,049 Google Fonts families: identical on every measurable scale, no label changed anywhere. One behaviour did change, intentionally — an unmeasurable scale now has "" rather than a word, because the bottom band would assert a reading that was never taken.

describe(profile) returns the same names with their provenance attached:

import { describe, REFERENCE_BANDS, REFERENCE_CORPUS, nameFor, familyReading } from "@overpunch/cedars-engine";

describe(profile).contrast;  // { name: "medium", value: 2.1, quantity: "contrastRatio",
                             //   unit: "thick:thin ratio", confidence: "anchored" }

Use profile.labels when you just need the word. Use describe when a reader may need to disagree with where a boundary sits, when you are storing a name and want it traceable to the cuts that produced it, or when you want to tune the boundaries for your own catalogue — labelFor takes an overriding band set, and the labels move with it.

What the cuts are, and what they are not

| Scale | Cuts on | v3 cuts | Derived from | |---|---|---|---| | Contrast | contrastRatio | 1.15 / 1.6 / 2.5 / 4 | typographic ratios | | Energy | energyRaw = secantDeg + 80 x springHeight/xHeight | 85 / 100.75 / 116.5 / 132.25 / 148 | the two archetypes, quartered | | Width | widthRatio | 0.444 / 0.544 / 0.817 / 1.0 | 2/3 landmark, stepped by √1.5 then 1.5 | | Rhythm | rhythmCv | 0.34 / 0.67 / 1 | cv = 0 and cv = 1 landmarks, thirded | | Aperture | opennessRatio | 0.33 / 0.66 / 1.0 | sealed = 0 and full counter = 1, thirded |

Every cut is landmark-derived, and deliberately not a percentile. v2 fitted them on corpus quantiles and that was wrong for labels: Google Fonts text faces cluster so tightly on width that the middle three-fifths spans only 0.606–0.703, so a quintile cut called a face 1.6% off the 2/3 landmark "extended". That is a statement about the sample, not about the face — and letting the sample move the names is exactly what v5 ended for scores. REFERENCE_CORPUS.setsReferenceCuts is false to record this.

The outermost bands are the overflow names the metrics always had: inert/explosive past the two energy archetypes, arrhythmic past cv = 1 where the spacing deviation exceeds the budget it varies around, wide open past a full counter, and very condensed/very extended past 1.5x either side of normal proportion.

rhythmCv runs opposite to the score: RHYTHM.cvAtZero is 1 and cvAtHundred is 0, so a low cv is an even face and the band names read even → arrhythmic as cv rises.

The corpus, and what it is for

REFERENCE_CORPUS records a measured corpus — one upright per family across 2,049 Google Fonts families, 1,116 of them text faces (Sans Serif, Serif, Monospace) — and states that it does not set the reference cuts. It is there for the relative path: corpusBands() and positionIn(), which answer "where does this sit in my library" and are explicitly not comparable across libraries. corpusBands() passes every reference scale through untouched; re-fitting is opt-in via refitAnchored, and a corpus-fitted set is marked corpus-fitted precisely so it cannot be mistaken for an absolute reading.

Two things the corpus did establish. The text faces' median widthRatio is 0.661, within 1% of the independently chosen v5 landmark for a text-normal n (2/3) — the archetype was right. And measurability varies sharply by kind: the energy arch reads on 92% of text faces against 52% of display and script, which is why a display-heavy corpus cannot define "normal" even if you wanted it to.

Family rollups carry their own agreement

familyReading(styles) gives one name per scale across a family, with the share of styles that agreed. The number is not decoration — measured across one foundry's 452 styles, styles within a family agreed on rhythm 80% of the time and on aperture only 53%, a coin flip. A family-level name with no agreement beside it asserts more than its styles support, so callers get the figure and can decline to render a weak one.

Raws: why a recalibration is now cheap

profile.raw persists the raw measured quantities behind the scores. A score is (raw quantity) × (landmark map); storing only the score fuses the two, so any future recalibration means re-parsing every font file — the cost that keeps calibration from ever happening. With raws stored, moving a landmark is a pure re-map: read raw → apply new map → write score.

profile.raw.contrast; // { ratio }
profile.raw.energy;   // { secantDeg, springHeightRatio }
profile.raw.width;    // { widthRatio }
profile.raw.rhythm;   // { pooledSd, meanBudget }
profile.raw.aperture; // { opennessRatio }
profile.raw.axis;     // { angleDeg, stressConfidence } — always present

Two rules, both enforced by tests:

  • Store components, not composites, wherever the composite bakes in a tunable constant. Energy's raw folds in springHeightWeight and Rhythm's cv folds in meanFloor, so those two scales store their pieces — otherwise retuning either constant would demand a reseed.
  • A scale's key is present if and only if that scale is available. An unavailable metric's diagnostics hold placeholders, and persisting a placeholder as a "raw measurement" is exactly the lie this structure exists to prevent. (axis is always present; its angleDeg: null for a monoline face is the measurement.)

Nothing here is newly measured — every value was already in profile.diagnostics. This is persistence, not measurement.

The two-version discipline

The package carries two versions because a profile is the product of two separable things:

| | Means | A change requires | |---|---|---| | ENGINE_VERSION | the measurement code — what geometry comes out of the outlines, i.e. the values in profile.raw | a full reseed: re-parse every font file | | TAXONOMY_VERSION | the landmark map — how raws become scores: landmark constants, scale vocabulary, vector layout | a pure re-map from stored raws; no font files touched |

Bump the one that actually changed. Bumping ENGINE_VERSION for a landmark tweak forces a reseed nobody needed; failing to bump TAXONOMY_VERSION for one leaves incomparable scores in the same column.

v6 bumps TAXONOMY_VERSION to "6" and leaves ENGINE_VERSION at "5.0.0". No geometry changed, so nothing needs re-parsing; no landmark moved, so nothing needs re-mapping either. What changed is the scale vocabulary, which this stamp explicitly covers, and the migration is the cheapest of the three — a key rename in stored rows:

| v5 key | v6 key | Values | |---|---|---| | pattern | rhythm | identical | | rhythm | width | identical |

It still earns the bump, for the reason the stamp exists: a consumer reading scores.rhythm across the boundary gets stem width from a v5 row and spacing regularity from a v6 row, with no error and no clue that anything moved. Rename the keys in any stored corpus once; the recall vector itself is untouched (see below), so vectors do not need rebuilding.

ENGINE_VERSION and package.json's version are deliberately not in lockstep when a release changes only packaging or naming: v5.0.1 and v6.0.0 both report ENGINE_VERSION "5.0.0" because they measure identically.

Similarity with unbounded scores

Unbounded scores need a bounded similarity coordinate, so each score passes through one shared squash g(u) over u = (score − lo)/(hi − lo) (windows: [0,100], or [−100,100] for signed Width):

g(u) = u                            0 ≤ u ≤ 1     identity — in-window values untouched
g(u) = 1 + 0.25·tanh(4·(u − 1))     u > 1
g(u) = −0.25·tanh(−4·u)             u < 0

It is value- and slope-continuous at both edges, strictly monotone, and bounded to (−0.25, 1.25) — so one overflowing dimension can't swamp a composite, while 167 and 250 remain different coordinates. Inside the window it is bit-identical to v4's score/100, so all existing in-window similarity behaviour is preserved exactly.

Use the exported transform — never re-derive a /100, which is no longer correct:

import { scoreToRecallDim, scoreToGowerValue, SCALE_WINDOWS } from "@overpunch/cedars-engine";

profile.vector and profileToGower both call it; there is no inline /100 left in the engine. Unavailable scales still impute 0.5 — and for signed Width that imputation is score 0, normal proportion, which is the correct agnostic prior. gowerDistance is unchanged (its per-dim distance is |g(a) − g(b)|, with g pre-applied), but its documented range is corrected: [0, 1.5], exceeding 1 only when overflowing scores are involved.

v6 does not move a vector dimension. profile.vector is built as CEDARS_SCALES.map(…), so dimension order follows that array — and the rename kept every slot bound to the measurement it already held. Slot 2 is still signed stem width (now named width) and slot 3 is still spacing regularity (now named rhythm). Had the two swapped positions instead, every stored vector would have remained numerically identical while silently meaning something else.

Measured accuracy

The v4 test for every scale was the same: different weights of the same design should score the same. Measured cross-weight spread within families (roman pool), v3 → v4:

| Scale | v3 spread | v4 spread | |-------|----------:|----------:| | Energy | 69.9 | 16.4 | | Width | 34.1 | 5.8 | | Rhythm | 60.6 | 8.8 | | Aperture | 68.7 | 15.7 |

The recovered stress angle is accurate to ±1.1° (closed-form harmonic fit; the old estimator was an argmin over a 2° grid).

Availability contract. An unavailable scale's stored score is a placeholder 0 — not a low reading — and an unavailable facet's dist is a uniform placeholder, so unavailable dimensions stay out of similarity honestly: profileToGower omits them entirely (gowerDistance renormalizes over the dims present, as it already does for the null monoline angle), and the ANN recall vector imputes the neutral 0.5 so recall doesn't drag unmeasurable fonts to an extreme. Since v5, profile.raw omits an unavailable scale's key entirely, for the same reason.

Usage

import { analyzeFont, ENGINE_VERSION, CEDARS_SCALES, CEDARS_FACETS } from "@overpunch/cedars-engine";

// buffer: ArrayBuffer / Uint8Array (browser File) or a Node Buffer.
const profile = analyzeFont(buffer);

profile.scores;          // { contrast, energy, width, rhythm, aperture } — UNBOUNDED landmark
                         // scores, rounded. NOT 0..100: 113 and −129 are real readings. Never clamp.
profile.raw;             // the raw measured quantities behind those scores — persist these
profile.labels;          // human-readable per scale, e.g. { contrast: "extreme", width: "very condensed" }
profile.availability;    // per scale: false = not measurable (0 is a placeholder, not a low reading)
profile.axis;            // { hasStress, angleDeg (0..180 | null), label } — the stress ANGLE
profile.facets;          // per facet: { top, confidence, dist } — soft categorical classification
profile.vector;          // RECALL vector: [scoreToRecallDim(scale, score) …, sin2θ·conf, cos2θ·conf]
profile.plus;            // { xHeightRatio, capHeightRatio, isMonospaced }
profile.diagnostics;     // typed raw measurements per scale + angle
profile.facetDiagnostics;// typed raw features behind each facet classifier

analyzeFont throws a FontParseError for unsupported/corrupt input — WOFF2 is not supported (opentype.js 1.x has no Brotli decoder); convert to OTF/TTF/WOFF first. Wrap the call in try/catch.

Also exported: parseFont(buffer) → FontContext, analyzeProfile(ctx), profileToGower(profile) (omits unavailable dims — see the availability contract above), the score→similarity transform scoreToRecallDim / scoreToGowerValue / squash / SCALE_WINDOWS / UNAVAILABLE_IMPUTATION / isCedarsScale, similarity helpers euclidean / cosineDistance / nearest and the mixed-type gowerDistance / rerankByGower / DEFAULT_GOWER_WEIGHTS, the vocabularies CEDARS_SCALES / CEDARS_FACETS / FACET_CLASSES / CEDARS_LETTERS / CEDARS_PLUS_SCALES, ENGINE_VERSION + TAXONOMY_VERSION (stamp corpus rows to detect drift), the legacy CEDARS_AXES / CEDARS_DIMENSIONS (for reading pre-3.0 rows), and all types.

What each axis measures

The Measurement column is what geometry is extracted (ENGINE_VERSION); how it becomes a score is the landmark table above (TAXONOMY_VERSION).

| Axis | Kind | Glyph | Measurement | |------|------|-------|------------------| | Contrast | scale | o | p95/p5 of perpendicular-corrected stroke thickness around the de-slanted bowl ring | | Energy | scale | n (h m agreement checks) | springing suddenness + springing height of the stem→arch transition — how high and how suddenly the arch darts off the stem | | Width | scale | n | de-slanted stem-centerline distance ÷ x-height (proportion, not advance — spacing is Rhythm). Signed: 0 = normal proportion | | Rhythm | scale | many | within-structural-class evenness of the per-glyph sidebearing budget; monospace → n/a | | Aperture | scale | c e | terminal-endpoint distance ÷ counter height (open humanist ↔ closed grotesque; closed = 0) | | Axis | angle | o | closed-form second-harmonic fit of the de-slanted ring thickness series, with graded stressConfidence; monoline ⇒ hasStress:false | | Loops | facet | o | 7-shape classifier over [squareness n, contrast-corrected aspect ratio, centroid taper] of the counter | | Terminals | facet | l (c) | stem-top width-profile classifier → blunt/angled/flared/rounded/tapered/ball; a radial curved-terminal probe on c catches ball terminals when the stem carries a serif | | Transitions | facet | n | stem↔arch junction span ÷ x-height (stem-relative leaked weight) + a real disconnectivity test → blended/tapered/abrupt/disconnected | | Fill | facet | o | ink fraction across the left band (outer edge → counter edge) + counter topology → solid/hollow/inline/texture/ornament — a carved inline channel is actually detectable now | | Construction | facet | a g | counter topology → double-story / single-story / mixed: a double-story a with a single-story g (most mainstream sans) lands decisively on mixed instead of the old deterministic 47/47 tie |

The bowl is de-slanted by the font's italic angle before measuring Contrast, Axis, and Loops, so instances of the same design don't drift across upright/italic, and the class/self-ratio metrics stay stable across weights. (The declared italic angle is also verified geometrically against the measured stem slant — diagnostics.axis.slantUsedDeg records what was actually removed.)

Axis diagnostics: named angles, one deprecation trap

AxisDiagnostics exposes the stress geometry under honest names: axialAngleDeg / thinDiameterAngleDeg (axial, [0,180), 90 = vertical), thinDeviationFromVerticalDeg / thickDiameterAngleDeg (signed, (−90,90]), their original-frame variants thinDiameterAngleOriginalDeg / thickDiameterAngleOriginalDeg (mapped back into the slanted glyph frame, for drawing overlays), and stressConfidence (0..1, scales the sin2θ/cos2θ embedding continuously toward the monoline point).

Trap: the legacy stressAngleDeg field is deprecated — it is the signed deviation of the thin diameter from vertical and differs from axis.angleDeg by 90°. Use thinDeviationFromVerticalDeg (same value, honest name) or the diameter fields. And never read any angle field without checking hasStress / stressConfidence first: when monoline, every angle holds a placeholder.

Calibration & roadmap

Calibration lives in src/constants.ts (landmarks, categorical prototypes). Every landmark is a named archetype stated in its comment, and the v4 "anchor sweep" roadmap item is deleted, not deferred: re-deriving anchors from corpus percentiles is exactly the practice v5 exists to end. What replaces it:

  • Landmark review (TODO(landmark-review) in the source) — argue about whether the named archetype is the right archetype (is a broad-nib branch really 80° at 0.85·xH?), which is a typographic question with a checkable answer, unlike "what did our sample happen to contain". Any outcome is a re-map from stored raws, not a reseed.
  • Human-ratings calibration — re-ground the landmarks on human consensus via isotonic regression. A calibration game at cedarstype.com/calibrate feeds it; that site is not public yet (it currently returns 401), so treat the link as forthcoming rather than live. Note that human ratings are collected on a bounded slider while scores are unbounded, so the fit must be to the squashed coordinate, not the raw score.
  • Transitions weight stability — x-height normalization removed the first-order weight leak (Light read "blended" while Black read "abrupt"); verify the residual across full weight spans.
  • Letter-map review — confirm fill under Details and aperture under Structure against ILT's own grouping, and settle whether their cedarsPattern is an internal name for R.
  • Known Energy limitation: "suddenness" saturates on square-cornered constructions, so squared-shoulder display designs (Eurostile-likes) read energetic; re-examine at the landmark review.

Development

npm run build      # tsup → dist (ESM + CJS + types + sourcemaps)
npm test           # vitest — 146 tests across 11 files: geometry, similarity, profile invariants,
                   # per-metric accuracy regressions, and the landmark/squash/raws contracts
npm run typecheck

test/landmark.test.ts is the guard rail for everything above: it pins each map at its landmarks, pins the overflow and monotonicity beyond them, pins the squash's slope continuity and its bit-identical in-window behaviour, and round-trips profile.raw back to profile.scores — that round-trip is the contract that makes a future re-map safe. If you change a landmark, expect it to fail, and update it with a recomputed expectation rather than a loosened one.

Bump ENGINE_VERSION when the measurements change (consumers must re-seed) and TAXONOMY_VERSION when the landmark map or the scale vocabulary changes (consumers re-map, or rename keys) — see the two-version discipline above.

License

MIT © Overpunch

CEDARS™ is a trademark of I Love Typography Ltd, used with permission. This package is an independent implementation and is not affiliated with or endorsed by I Love Typography.