diff --git a/respec/template.html b/respec/template.html index e347c87..a8f3124 100644 --- a/respec/template.html +++ b/respec/template.html @@ -11,8 +11,17 @@ const respecConfig = { // Working Groups ids at https://respec.org/w3c/groups/ // group: "Semantic Data Products Working Group", - latestVersion: "https://www.omg.org/spec/DPROD/dprod/", - edDraftURI: "https://ekgf.org/spec/{{ branch }}/", + // The OMG catalog entry, which always resolves to the newest published + // version. Not the ontology namespace IRI (.../DPROD/dprod/): that + // identifies the vocabulary, not a document, and 404s (issue #255). + latestVersion: "https://www.omg.org/spec/DPROD/", + // The one canonical continuously-updated draft, whichever branch this + // build came from. A branch preview points readers at mainline; its own + // URL is thisVersion below. + edDraftURI: "https://ekgf.org/dprod/spec/develop/", + // This build. The slug is the branch with slashes replaced by dashes, + // and the /dprod basePath is required — without it the URL 404s. + thisVersion: "https://ekgf.org/dprod/spec/{{ branch_slug }}/", specStatus: "base", // Pinned to the branch's last commit by the spec generator. Without it // ReSpec uses the date the page is viewed, since it runs in the reader's @@ -148,7 +157,10 @@ }, dprod: { title: "Data Product Ontology", - href: "https://www.omg.org/spec/DPROD/dprod/", + // The OMG catalog entry, which resolves. The vocabulary namespace + // IRI (.../DPROD/dprod/) identifies the terms rather than a + // document, and does not currently dereference (issue #255). + href: "https://www.omg.org/spec/DPROD/", authors: [ "Tony Seale", "Natasa Varytimou", @@ -393,7 +405,7 @@

For a complete list of trademarks, - see: https://www.omg.org/legal/tm_list.htm. + see: https://www.omg.org/legal/tm_list.htm. All other products or company names mentioned are used for identification purposes only and may be trademarks of their respective owners.

@@ -457,7 +469,7 @@

Data Product Ontology (DPROD)

Scope

- The Data Product (DPROD) specification is a + The Data Product (DPROD) specification is a profile of the Data Catalog (DCAT) Vocabulary, designed to describe Data Products. This document defines the schema and provides examples of its use. @@ -470,7 +482,7 @@

Scope

and enables federated search across multiple sites using a uniform query mechanism and structure.

- The namespace for DPROD terms is https://www.omg.org/spec/DPROD/dprod/ + The namespace for DPROD terms is https://www.omg.org/spec/DPROD/dprod/

The suggested prefix for the DPROD namespace is dprod @@ -484,7 +496,7 @@

Scope

  • Harmonize Data Schemas: Using shared schemas helps unify different data formats. - For instance, the DPROD specification provides a + For instance, the DPROD specification provides a common set of rules for defining a Data Product. Users of DPROD can extend this schema as needed. diff --git a/site/public/spec/archive/1.0/index.html b/site/public/spec/archive/1.0/index.html index bf7ba89..b13d178 100644 --- a/site/public/spec/archive/1.0/index.html +++ b/site/public/spec/archive/1.0/index.html @@ -11,7 +11,15 @@ const respecConfig = { // Working Groups ids at https://respec.org/w3c/groups/ // group: "Semantic Data Products Working Group", - latestVersion: "https://ekgf.org/dprod/spec/main/", + // The OMG catalog entry, so a reader of this frozen 1.0 can reach the + // newest published version. It previously pointed at this document + // itself, which is what "this version" means, not "latest". + latestVersion: "https://www.omg.org/spec/DPROD/", + // Set explicitly. When edDraftURI is absent ReSpec infers it from the + // github config as https://ekgf.github.io/dprod/ — GitHub Pages, retired + // by #235, so every reader of the frozen standard met a 404 (issue #255). + edDraftURI: "https://ekgf.org/dprod/spec/develop/", + thisVersion: "https://ekgf.org/dprod/spec/main/", specStatus: "base", // Frozen: DPROD 1.0 beta 1. Hard-coded rather than derived, because this // document is immutable and its date must not move when a maintenance @@ -389,7 +397,7 @@

    For a complete list of trademarks, - see: https://www.omg.org/legal/tm_list.htm. + see: https://www.omg.org/legal/tm_list.htm. All other products or company names mentioned are used for identification purposes only and may be trademarks of their respective owners.

    diff --git a/spec-generator/main.py b/spec-generator/main.py index abd2993..3fe7800 100644 --- a/spec-generator/main.py +++ b/spec-generator/main.py @@ -30,6 +30,16 @@ def detect_branch() -> str: return "main" +def branch_slug(branch: str) -> str: + """The branch name as it appears in a /spec// URL. + + Slashes become dashes, matching site/src/lib/spec-versions.ts. The raw + branch name is still what ReSpec's `github` config needs for its commit + history link, so both are passed to the template. + """ + return branch.replace("/", "-") + + def detect_publish_date(g) -> str: """The publication date of this version, from `dct:issued` on the ontology. @@ -127,6 +137,7 @@ def main(): 'classes': classes, 'examples': examples, 'branch': detect_branch(), + 'branch_slug': branch_slug(detect_branch()), 'publish_date': detect_publish_date(g), }) diff --git a/tests/test_spec_header_urls.py b/tests/test_spec_header_urls.py new file mode 100644 index 0000000..3bc84a0 --- /dev/null +++ b/tests/test_spec_header_urls.py @@ -0,0 +1,107 @@ +"""The spec header must not advertise URLs that 404. + +Three separate dead links shipped at once (issue #255): + +* `edDraftURI` pointed at `https://ekgf.org/spec/{branch}/` — missing the + `/dprod` basePath, and with the branch name unslugified, so a branch + containing a slash could never resolve; +* `latestVersion` pointed at the *vocabulary namespace* IRI, which identifies + the terms rather than a document and does not dereference; +* the frozen 1.0 archive set no `edDraftURI` at all, so ReSpec inferred one + from the `github` config — the GitHub Pages site retired by #235. + +These are cheap to reintroduce and invisible without rendering the page in a +browser, since ReSpec builds the header client-side. +""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +SPEC_TEMPLATE = REPOSITORY_ROOT / "respec" / "template.html" +ARCHIVE_SPEC = ( + REPOSITORY_ROOT / "site" / "public" / "spec" / "archive" / "1.0" / "index.html" +) + +#: Hosts and paths known not to resolve. +RETIRED_URLS = ( + "ekgf.github.io/dprod", + "https://www.omg.org/spec/DPROD/dprod.ttl", +) +SPEC_BASE = "https://ekgf.org/dprod/spec/" + + +class SpecHeaderUrlTest(unittest.TestCase): + @staticmethod + def without_comments(content: str) -> str: + """Strip comments: a URL explained in one is not a URL that is linked.""" + content = re.sub(r"", "", content, flags=re.DOTALL) + return "\n".join( + line for line in content.splitlines() if not line.lstrip().startswith("//") + ) + + def documents(self) -> list[tuple[str, str]]: + return [ + ( + path.relative_to(REPOSITORY_ROOT).as_posix(), + self.without_comments(path.read_text("utf-8")), + ) + for path in (SPEC_TEMPLATE, ARCHIVE_SPEC) + ] + + def test_no_retired_urls_are_linked(self) -> None: + for name, content in self.documents(): + for retired in RETIRED_URLS: + with self.subTest(document=name, url=retired): + self.assertNotIn( + retired, + content, + f"{name} references {retired}, which does not resolve.", + ) + + def test_both_documents_set_an_editors_draft(self) -> None: + """Unset, ReSpec infers the retired GitHub Pages URL from `github`.""" + for name, content in self.documents(): + with self.subTest(document=name): + self.assertIn( + f'edDraftURI: "{SPEC_BASE}develop/"', + content, + f"{name} must set edDraftURI explicitly to the canonical " + f"develop draft.", + ) + + def test_latest_version_is_the_omg_catalog(self) -> None: + for name, content in self.documents(): + with self.subTest(document=name): + self.assertIn( + 'latestVersion: "https://www.omg.org/spec/DPROD/"', + content, + f"{name}: latestVersion must be the OMG catalog entry, which " + f"always resolves to the newest published version.", + ) + + def test_this_version_carries_the_basepath_and_a_slug(self) -> None: + template = SPEC_TEMPLATE.read_text(encoding="utf-8") + self.assertIn( + f'thisVersion: "{SPEC_BASE}{{{{ branch_slug }}}}/"', + template, + "thisVersion must use the /dprod basePath and the slugified branch; " + "a raw branch name containing a slash does not resolve.", + ) + + archive = ARCHIVE_SPEC.read_text(encoding="utf-8") + self.assertIn(f'thisVersion: "{SPEC_BASE}main/"', archive) + + def test_no_href_swallows_a_trailing_full_stop(self) -> None: + """`href="....htm."` — the sentence's full stop inside the URL.""" + for name, content in self.documents(): + with self.subTest(document=name): + offenders = re.findall(r'href="[^"]*\.(?:htm|html|org|com)\."', content) + self.assertEqual([], offenders, f"{name}: {offenders}") + + +if __name__ == "__main__": + unittest.main()