From ad10ca1a225e5e1d579882f2c61a15d5fcb368eb Mon Sep 17 00:00:00 2001 From: HackingGate Date: Sun, 1 Nov 2026 01:50:39 +0900 Subject: [PATCH 1/2] citations-linked holds ISO and IEEE citations to their catalogue hosts - ISO and ISO/IEC (`ISO `, optional `-` and `:`) link to a page under https://www.iso.org/standard/. - IEEE (`IEEE `, `IEEE Std `, optional `-`) link to a page under https://standards.ieee.org/ or https://ieeexplore.ieee.org/document/. - Neither catalogue numbers its pages by the standard (ISO 8601-1:2019 is /standard/70907.html), so the link is held to the host and path and no further. A link template takes `**` for the rest of the path, and the suggested link shows it as an ellipsis. The last of the three plan items. Closes #438 Claude-Session: https://claude.ai/code/session_01EycJ7jceyKGYYpSgtty4NQ --- docs/REFERENCE.md | 9 ++++-- policy/base/published-text.toml | 16 +++++------ src/guard/citations.rs | 51 +++++++++++++++++++++++++++++---- tests/shim_cli.rs | 20 +++++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 426c8aa..e8ac48d 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -598,7 +598,7 @@ it refuses** so the name predicts the rule list: | `unowned-push` | a push to an owner this repository has not named — **installs `pre-push`**, and refuses to run until the repository says who it is | | `private-names` | a private organization or repository named in a commit message, a staged diff, or the tracked tree of a **public** repository — **installs five stages**, and refuses to run until the repository says whether it is published | | `stale-visibility` | a policy declaring `private` over a repository the forge serves as **public** — **installs `pre-push` and `manual`**, and reaches the network. Refuses that one direction only; it can never confirm privacy | -| `published-text` | host identity, refused markers and private names in the text a command is about to publish — a pull-request body, an issue title, a branch name in a push — and, as `unowned-forge-target`, a `gh` invocation bound for a repository this workspace does not own, which refuses to run until the repository says who it is, and, as `no-empty-merge-subject`, a merge commit subject that is blank once the forge's ` (#N)` is taken off, at the shim, at the harness hook and over a message given to `--text` — see [a merge with no subject](#a-merge-with-no-subject) — and, as `ruling-citations-resolve`, an `Answered:` line citing a catalog line that does not resolve at the uphold revision the repository pins — see [a ruling's citations](#a-rulings-citations) — and, as `citations-linked`, a citation of an RFC, an Internet-Draft, a BCP or STD, a CVE, a CWE, a GHSA, a DOI, an arXiv id, a WCAG success criterion, a PEP or a Unicode report that does not link to what it cites — see [citations of standards and identifiers](#citations-of-standards-and-identifiers). **Installs no git hook**: its rules run at the shim seam (`gh`, `git push`), and it refuses to load until the repository has declared the `[[shim]]` tables itself — see [the set's command ceiling](#what-a-bundled-set-may-install) | +| `published-text` | host identity, refused markers and private names in the text a command is about to publish — a pull-request body, an issue title, a branch name in a push — and, as `unowned-forge-target`, a `gh` invocation bound for a repository this workspace does not own, which refuses to run until the repository says who it is, and, as `no-empty-merge-subject`, a merge commit subject that is blank once the forge's ` (#N)` is taken off, at the shim, at the harness hook and over a message given to `--text` — see [a merge with no subject](#a-merge-with-no-subject) — and, as `ruling-citations-resolve`, an `Answered:` line citing a catalog line that does not resolve at the uphold revision the repository pins — see [a ruling's citations](#a-rulings-citations) — and, as `citations-linked`, a citation of an RFC, an Internet-Draft, a BCP or STD, a CVE, a CWE, a GHSA, a DOI, an arXiv id, a WCAG success criterion, a PEP, a Unicode report, an ISO or an IEEE standard that does not link to what it cites — see [citations of standards and identifiers](#citations-of-standards-and-identifiers). **Installs no git hook**: its rules run at the shim seam (`gh`, `git push`), and it refuses to load until the repository has declared the `[[shim]]` tables itself — see [the set's command ceiling](#what-a-bundled-set-may-install) | | `prose-shapes` | four sentence shapes that carry nothing — a sentence announcing what the next one will say, a clause behind a dash restating the one in front of it, a hedge admitting no uncertainty, an objection nobody raised being answered. **Installs no git hook**: its rules run in `uphold scan` and at the shim seam (`gh`, `git push`), and like `published-text` it refuses to load until the repository has declared the `[[shim]]` tables itself. `UPHOLD_ALLOW=` is the waiver for the sentence a rule is wrong about | | `narrated-history` | a document's sentence narrating how something changed, in Markdown outside `adr` directories; the near misses it lets through are listed in the set's header. **Installs no git hook and no command**: it runs in `uphold scan` only, because a commit message and a pull-request body are where a change is narrated. A baseline entry excuses the one sentence it is wrong about | @@ -2868,6 +2868,8 @@ the strongest check its links allow: | WCAG 2.1, 2.2 | `WCAG SC ..`, or `success criterion` | `https://www.w3.org/TR/WCAG/`, or the dated `https://www.w3.org/TR//REC-WCAG-/` | begins `#x---` | | PEP | `PEP `, optionally followed by `§` or `Section ` | `https://peps.python.org/pep-/` | any, when a section is cited | | Unicode | `UAX #`, `UTS #`, with the same locators | `https://www.unicode.org/reports/tr/`, or a dated `…/tr/tr-.html` | any, when a section is cited | +| ISO, ISO/IEC | `ISO `, `ISO/IEC `, with optional `-` and `:` | any page under `https://www.iso.org/standard/` | — | +| IEEE | `IEEE `, `IEEE Std `, with optional `-` | any page under `https://standards.ieee.org/` or `https://ieeexplore.ieee.org/document/` | — | A section cited is matched by its anchor as far as the number gives it, so no link is fetched and no forge is asked. An RFC or draft section's anchor is the @@ -2879,7 +2881,10 @@ PEP's or a Unicode report's anchors are names, so a cited section asks only that the link name one. The version a WCAG citation states must be the one linked; an undated link to it is accepted, as an undated PEP or Unicode link is. WCAG 2.0 is not in the table: its pages name a success criterion's anchor -without its number. A section not cited leaves the fragment free. An arXiv citation that states a version must link to that version; one +without its number. An ISO or IEEE link is held to the catalogue's host and +path and no further: the catalogue numbers its pages itself (ISO 8601-1:2019 +is `/standard/70907.html`), so which page belongs to which standard cannot be +told from the citation. A section not cited leaves the fragment free. An arXiv citation that states a version must link to that version; one that does not may link to any. `https://www.rfc-editor.org/rfc/rfc`, with no `.html`, is not an RFC link here: it answers with a redirect to the RFC's info page, where a fragment names nothing. diff --git a/policy/base/published-text.toml b/policy/base/published-text.toml index 17538d3..5657527 100644 --- a/policy/base/published-text.toml +++ b/policy/base/published-text.toml @@ -173,14 +173,14 @@ command.scope = "always" # A citation of a numbered standard or an identifier -- an RFC, an # Internet-Draft, a BCP or STD, a CVE, a CWE, a GHSA, a DOI, an arXiv id, a -# WCAG success criterion, a PEP, a Unicode report -- links to what it cites, -# and a section cited links to that section as far as the family's anchors -# can be told from the citation. Read from a closed table of families compiled into the binary, so -# a family added is a row and not a policy regular expression; no link is -# fetched, because the section anchors follow from the number. `gh` alone, as -# above: a pull-request or issue body is where a citation is published. -# `command.scope = "always"`: a citation in a private repository's body -# misleads its reader as much as one in a public one. +# WCAG success criterion, a PEP, a Unicode report, an ISO or IEEE standard -- +# links to what it cites, and a section cited links to that section as far as +# the family's anchors can be told from the citation. Read from a closed table +# of families compiled into the binary, so a family added is a row and not a +# policy regular expression; no link is fetched, because the section anchors +# follow from the number. `gh` alone, as above: a pull-request or issue body is +# where a citation is published. `command.scope = "always"`: a citation in a +# private repository's body misleads its reader as much as one in a public one. [rule.citations-linked] builtin = "citations-linked" command.before = ["gh"] diff --git a/src/guard/citations.rs b/src/guard/citations.rs index 916f738..242ade4 100644 --- a/src/guard/citations.rs +++ b/src/guard/citations.rs @@ -44,7 +44,8 @@ use crate::guard::Refusal; /// is how far a family's links let a check go. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Locate { - /// Nothing past the document: a record, whose id names one page. + /// Nothing past the document: a record, whose id names one page, or a + /// catalogue, whose page cannot be told from the citation at all. Nothing, /// `#section-` or `#appendix-`, or a paragraph of it, from the /// number: the RFC series' anchors. @@ -89,7 +90,9 @@ struct Family { pattern: &'static str, spelling: Spelling, /// The family's links to document ``, matched against the whole URL - /// with its fragment taken off. `*` is one path segment. + /// with its fragment taken off. `*` is one path segment and `**` the + /// rest of the path; a template with no `` is a catalogue host, + /// where no page can be told from the citation. links: &'static [&'static str], /// Whether `` compares without regard to case, as a DOI does. id_any_case: bool, @@ -249,6 +252,33 @@ const FAMILIES: &[Family] = &[ id_any_case: false, id_suffix: "", }, + // A catalogue row. An ISO catalogue page is numbered by the catalogue, + // not by the standard (ISO 8601-1:2019 is /standard/70907.html), and the + // text is sold, so the link is held to the catalogue's host and no + // further. + Family { + name: "ISO", + locate: Locate::Nothing, + spelling: Spelling::AsWritten, + pattern: r"\bISO(?:/IEC)?[ \u{a0}]*(?P[1-9][0-9]{1,5}(?:-[0-9]+)*)(?::[0-9]{4})?\b", + links: &["https://www.iso.org/standard/**"], + id_any_case: false, + id_suffix: "", + }, + // The same for IEEE: its standards pages and Xplore documents are + // numbered by the site. + Family { + name: "IEEE", + locate: Locate::Nothing, + spelling: Spelling::AsWritten, + pattern: r"\bIEEE(?:[ \u{a0}]+Std)?[ \u{a0}]*(?P[1-9][0-9]{2,4}(?:\.[0-9]+)*[a-z]?)(?:-[0-9]{4})?\b", + links: &[ + "https://standards.ieee.org/**", + "https://ieeexplore.ieee.org/document/**", + ], + id_any_case: false, + id_suffix: "", + }, ]; fn compiled() -> &'static [Regex] { @@ -401,9 +431,9 @@ fn judge(citation: &Citation) -> Option { .any(|template| links_to(template, &citation.id, family, document)) { return Some(format!( - "links to {link}, which is not a {} link to {}; link it to {}", - family.name, + "links to {link}, which is not a link to {} ({}); link it to {}", citation.written, + family.name, wanted(family, citation) )); } @@ -456,7 +486,12 @@ fn wanted(family: &Family, citation: &Citation) -> String { family .links .iter() - .map(|link| format!("{}{anchor}", link.replace("", &linked_id))) + .map(|link| { + format!( + "{}{anchor}", + link.replace("", &linked_id).replace("**", "…") + ) + }) .collect::>() .join(" or ") } @@ -475,7 +510,11 @@ fn links_to(template: &str, id: &str, family: &Family, document: &str) -> bool { } pattern.push_str(family.id_suffix); } - pattern.push_str(®ex::escape(part).replace(r"\*", "[^/?#]+")); + pattern.push_str( + ®ex::escape(part) + .replace(r"\*\*", "[^?#]*") + .replace(r"\*", "[^/?#]+"), + ); } pattern.push('$'); Regex::new(&pattern).is_ok_and(|compiled| compiled.is_match(document)) diff --git a/tests/shim_cli.rs b/tests/shim_cli.rs index 7c39021..8ec71dc 100644 --- a/tests/shim_cli.rs +++ b/tests/shim_cli.rs @@ -9705,6 +9705,26 @@ const CITATION_ROWS: &[(&str, &str, &[&str])] = &[ "https://doi.org/10.1002/spe.4380120706", &["https://doi.org/10.1002/spe.4380120707"], ), + ( + "ISO 8601-1:2019", + "https://www.iso.org/standard/70907.html", + &["https://www.iso.org/obp/ui/#iso:std:iso:8601:-1:ed-1:v1:en"], + ), + ( + "ISO/IEC 27001", + "https://www.iso.org/standard/27001", + &["https://en.wikipedia.org/wiki/ISO/IEC_27001"], + ), + ( + "IEEE 754-2019", + "https://standards.ieee.org/ieee/754/6210/", + &["https://en.wikipedia.org/wiki/IEEE_754"], + ), + ( + "IEEE Std 802.3", + "https://ieeexplore.ieee.org/document/9844436", + &["https://ieeexplore.ieee.org/xpl/conhome/1000/all-proceedings"], + ), ( "arXiv:1706.03762", "https://arxiv.org/abs/1706.03762v7", From ba1aea25cbae98f8b877382243583b07760d29ce Mon Sep 17 00:00:00 2001 From: HackingGate Date: Sun, 1 Nov 2026 02:12:20 +0900 Subject: [PATCH 2/2] citations-linked reads IEEE 802.11ax whole and fills ** before the id From the review on #474: - The IEEE id takes any run of letters after its number, so `IEEE Std 802.11ax-2021` is one citation, not `IEEE Std 802`. - A suggested link replaces the template's `**` before the id goes in, so an id is never rewritten. Refs #438 Claude-Session: https://claude.ai/code/session_01EycJ7jceyKGYYpSgtty4NQ --- src/guard/citations.rs | 14 ++++++++++++-- tests/shim_cli.rs | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/guard/citations.rs b/src/guard/citations.rs index 242ade4..4d6d21f 100644 --- a/src/guard/citations.rs +++ b/src/guard/citations.rs @@ -271,7 +271,7 @@ const FAMILIES: &[Family] = &[ name: "IEEE", locate: Locate::Nothing, spelling: Spelling::AsWritten, - pattern: r"\bIEEE(?:[ \u{a0}]+Std)?[ \u{a0}]*(?P[1-9][0-9]{2,4}(?:\.[0-9]+)*[a-z]?)(?:-[0-9]{4})?\b", + pattern: r"\bIEEE(?:[ \u{a0}]+Std)?[ \u{a0}]*(?P[1-9][0-9]{2,4}(?:\.[0-9]+)*[a-z]*)(?:-[0-9]{4})?\b", links: &[ "https://standards.ieee.org/**", "https://ieeexplore.ieee.org/document/**", @@ -489,7 +489,7 @@ fn wanted(family: &Family, citation: &Citation) -> String { .map(|link| { format!( "{}{anchor}", - link.replace("", &linked_id).replace("**", "…") + link.replace("**", "…").replace("", &linked_id) ) }) .collect::>() @@ -715,6 +715,16 @@ mod tests { assert!(wanted(family, &citation).starts_with("https://arxiv.org/abs/1706.03762v7")); } + #[test] + fn a_dotted_ieee_number_with_letters_and_a_year_is_read_whole() { + assert_eq!( + read("IEEE Std 802.11ax-2021 says so.") + .first() + .map(|citation| citation.0.as_str()), + Some("IEEE Std 802.11ax-2021") + ); + } + #[test] fn a_paragraph_of_the_cited_section_is_in_it() { assert!(in_section("section-10.2.3-1", "section-10.2.3")); diff --git a/tests/shim_cli.rs b/tests/shim_cli.rs index 8ec71dc..161217d 100644 --- a/tests/shim_cli.rs +++ b/tests/shim_cli.rs @@ -9720,6 +9720,11 @@ const CITATION_ROWS: &[(&str, &str, &[&str])] = &[ "https://standards.ieee.org/ieee/754/6210/", &["https://en.wikipedia.org/wiki/IEEE_754"], ), + ( + "IEEE Std 802.11ax-2021", + "https://standards.ieee.org/ieee/802.11ax/7180/", + &["https://en.wikipedia.org/wiki/IEEE_802.11ax"], + ), ( "IEEE Std 802.3", "https://ieeexplore.ieee.org/document/9844436",