1.1.2 — one sentence, in all five places a reader meets it

1.1.1 changed the description in Cargo.toml and in the npm package and
missed pyproject.toml, which is the one maturin publishes. PyPI took the
old summary and neither registry lets that be edited afterwards, so the
correction needs a version of its own.

The two READMEs had drifted further than the description fields. A
registry page leads with the README rather than the summary, and npm's
opened by saying the package needs no network without ever saying it
covers the Byzantine Catholic churches — which is most of what the
sentence was changed to convey.

Five files now carry it, and a test reads all five and names the ones
that disagree. It was written against the mistake above and fails on it.
This commit is contained in:
funkpower
2026-09-21 07:46:58 -04:00
parent dde50192c7
commit 93b597302a
7 changed files with 123 additions and 6 deletions
+11
View File
@@ -4,6 +4,17 @@ Notable changes, in the terms [docs/versioning.md](docs/versioning.md) sets out.
**Calendar** entries change what a day reports, whatever the version number
does.
## 1.1.2
### Fixed
- The published description says what the package covers, and says the same
thing in both registries. PyPI takes its summary from `pyproject.toml`, which
1.1.1 did not update, so it went out with the old one-line text while npm got
the new one. The two READMEs had drifted apart as well: npm's opened without
mentioning the Byzantine Catholic churches at all. All four now carry one
sentence, and a test fails if any of them moves without the others.
## 1.1.1
### Fixed
Generated
+1 -1
View File
@@ -404,7 +404,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "typikon"
version = "1.1.1"
version = "1.1.2"
dependencies = [
"clap",
"js-sys",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "typikon"
version = "1.1.1"
version = "1.1.2"
edition = "2021"
rust-version = "1.85"
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings."
+2 -2
View File
@@ -1,8 +1,8 @@
# typikon
The Orthodox liturgical day: feasts, seasons and the fast, computed rather
than fetched. The Byzantine Catholic churches are here too, on their own
calendars and, where the law has been sourced, under their own particular law.
than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic
churches, across the Julian, Revised Julian and Gregorian reckonings.
Everything derives from the paschalion and the fixed calendar, so it needs no
network, cannot go stale, and answers for any year you ask about. No API key,
+4 -1
View File
@@ -1,7 +1,10 @@
# typikon
The Orthodox liturgical day: feasts, seasons and the fast, computed rather
than fetched. No network, no data files, no service to be up.
than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic
churches, across the Julian, Revised Julian and Gregorian reckonings.
No network, no data files, no service to be up.
```sh
npm install typikon
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "typikon"
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched"
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched. Twenty Orthodox jurisdictions and four Byzantine Catholic churches, across the Julian, Revised Julian and Gregorian reckonings."
readme = "README.md"
license = "GPL-2.0-only"
license-files = ["LICENSE"]
+103
View File
@@ -0,0 +1,103 @@
//! The packaging metadata, which lives in three files and is published to two
//! registries.
//!
//! `Cargo.toml` describes the crate, `pyproject.toml` is what maturin puts on
//! PyPI, and `npm/package.json.in` is what npm shows. Nothing makes them agree
//! and nothing notices when they stop: a description edited in one of the three
//! reaches one registry and not the other, and the wrong one is then frozen
//! into a release, because neither registry lets published metadata be edited.
//! That has happened once already.
//!
//! The two READMEs open with the same sentence, because that paragraph is the
//! first thing shown on both registry pages and is what most people will read
//! instead of the description field.
use std::fs;
/// The value of a `key = "…"` line, from the first section that has one.
fn toml_string(path: &str, key: &str) -> String {
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}"));
for line in text.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix(key) else { continue };
let Some(rest) = rest.trim_start().strip_prefix('=') else { continue };
let rest = rest.trim();
if let Some(v) = rest.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
return v.to_string();
}
}
panic!("{path}: no `{key} = \"…\"` line");
}
/// The value of a `"key": "…"` line. Enough for a hand-written file with no
/// escapes in it, and the test below fails loudly rather than quietly if that
/// ever stops being true.
fn json_string(path: &str, key: &str) -> String {
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}"));
let needle = format!("\"{key}\":");
for line in text.lines() {
let Some(rest) = line.trim().strip_prefix(&needle) else { continue };
let rest = rest.trim().trim_end_matches(',').trim();
if let Some(v) = rest.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
assert!(!v.contains('\\'), "{path}: {key} has escapes this reader cannot handle");
return v.to_string();
}
}
panic!("{path}: no `\"{key}\": \"…\"` line");
}
#[test]
fn the_three_descriptions_are_the_same_sentence() {
let cargo = toml_string("Cargo.toml", "description");
let pyproject = toml_string("pyproject.toml", "description");
let npm = json_string("npm/package.json.in", "description");
assert_eq!(
cargo, pyproject,
"Cargo.toml and pyproject.toml disagree; pyproject.toml is the one PyPI shows"
);
assert_eq!(
cargo, npm,
"Cargo.toml and npm/package.json.in disagree; the latter is the one npm shows"
);
}
#[test]
fn the_description_says_what_the_crate_is_called() {
// Cheap guard against a description edited into something that no longer
// describes this package at all.
let d = toml_string("Cargo.toml", "description");
assert!(d.len() > 40, "description is too short to be one: {d:?}");
assert!(!d.ends_with(' '), "description has a trailing space");
assert!(d.contains("Orthodox"), "description no longer mentions Orthodox: {d:?}");
}
/// The opening paragraph of a README, unwrapped to one line.
fn lede(path: &str) -> String {
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("{path}: {e}"));
let body = text
.split("\n\n")
.find(|p| !p.trim().is_empty() && !p.trim_start().starts_with('#'))
.unwrap_or_else(|| panic!("{path}: no opening paragraph"));
body.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn both_readmes_open_with_the_description() {
// A registry page leads with the README, not with the description field,
// so the sentence a reader actually meets is this one. npm's README is a
// separate file from the crate's and had gone its own way: it described
// the package without mentioning that it covers Byzantine Catholic
// churches at all.
let description = toml_string("Cargo.toml", "description");
assert_eq!(
lede("README.md"),
description,
"README.md does not open with the published description"
);
assert_eq!(
lede("npm/README.md"),
description,
"npm/README.md does not open with the published description"
);
}