Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<rule-id>` 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 |

Expand Down Expand Up @@ -2868,6 +2868,8 @@ the strongest check its links allow:
| WCAG 2.1, 2.2 | `WCAG <v> SC <a>.<b>.<c>`, or `success criterion` | `https://www.w3.org/TR/WCAG<vv>/`, or the dated `https://www.w3.org/TR/<yyyy>/REC-WCAG<vv>-<date>/` | begins `#x<a>-<b>-<c>-` |
| PEP | `PEP <n>`, optionally followed by `§<name>` or `Section <name>` | `https://peps.python.org/pep-<nnnn>/` | any, when a section is cited |
| Unicode | `UAX #<n>`, `UTS #<n>`, with the same locators | `https://www.unicode.org/reports/tr<n>/`, or a dated `…/tr<n>/tr<n>-<rev>.html` | any, when a section is cited |
| ISO, ISO/IEC | `ISO <n>`, `ISO/IEC <n>`, with optional `-<part>` and `:<year>` | any page under `https://www.iso.org/standard/` | — |
| IEEE | `IEEE <n>`, `IEEE Std <n>`, with optional `-<year>` | 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
Expand All @@ -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<n>`, 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.
Expand Down
16 changes: 8 additions & 8 deletions policy/base/published-text.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
61 changes: 55 additions & 6 deletions src/guard/citations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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-<s>` or `#appendix-<X>`, or a paragraph of it, from the
/// number: the RFC series' anchors.
Expand Down Expand Up @@ -89,7 +90,9 @@ struct Family {
pattern: &'static str,
spelling: Spelling,
/// The family's links to document `<id>`, 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 `<id>` is a catalogue host,
/// where no page can be told from the citation.
links: &'static [&'static str],
/// Whether `<id>` compares without regard to case, as a DOI does.
id_any_case: bool,
Expand Down Expand Up @@ -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<id>[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<id>[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] {
Expand Down Expand Up @@ -401,9 +431,9 @@ fn judge(citation: &Citation) -> Option<String> {
.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)
));
}
Expand Down Expand Up @@ -456,7 +486,12 @@ fn wanted(family: &Family, citation: &Citation) -> String {
family
.links
.iter()
.map(|link| format!("{}{anchor}", link.replace("<id>", &linked_id)))
.map(|link| {
format!(
"{}{anchor}",
link.replace("**", "…").replace("<id>", &linked_id)
)
})
.collect::<Vec<_>>()
.join(" or ")
}
Expand All @@ -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(&regex::escape(part).replace(r"\*", "[^/?#]+"));
pattern.push_str(
&regex::escape(part)
.replace(r"\*\*", "[^?#]*")
.replace(r"\*", "[^/?#]+"),
);
}
pattern.push('$');
Regex::new(&pattern).is_ok_and(|compiled| compiled.is_match(document))
Expand Down Expand Up @@ -676,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"));
Expand Down
25 changes: 25 additions & 0 deletions tests/shim_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9705,6 +9705,31 @@ 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.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",
&["https://ieeexplore.ieee.org/xpl/conhome/1000/all-proceedings"],
),
(
"arXiv:1706.03762",
"https://arxiv.org/abs/1706.03762v7",
Expand Down
Loading