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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ code/include
*/__pycache__
.vercel
__pycache__/

# Playwright MCP scratch output
.playwright-mcp/
13 changes: 13 additions & 0 deletions ontology/dprod/dprod-ontology.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dct:description a owl:AnnotationProperty .
dct:contributor a owl:AnnotationProperty .
dct:license a owl:AnnotationProperty .
dct:modified a owl:AnnotationProperty .
dct:issued a owl:AnnotationProperty .

dprod:
a owl:Ontology ;
Expand All @@ -28,6 +29,18 @@ dprod:
"The ontology utilizes the notion of Datasets as defined from DCAT vocabulary."@en ;
rdfs:comment "DPROD is an OWL ontology designed to facilitate interoperability between data product descriptors."@en ;
dct:license <https://creativecommons.org/licenses/by/4.0/> ;
# The publication date of this version of the specification, and the single
# source of truth for the date in the generated spec's header. Authored on
# purpose: deriving it from the last commit let maintenance changes — a URL
# retirement, a typo fix — silently republish the standard (issue #253).
# Bump it when a version is issued, in step with owl:versionIRI.
#
# Once a version is published by OMG, its OMG document number is
# authoritative for this date: ptc/YY-MM-NN encodes the month. DPROD 1.0
# beta 1 is ptc/25-02-01, hence February 2025 in the frozen archive. The
# value below is for the working draft, which has no OMG document number
# yet, so it tracks owl:versionIRI instead.
dct:issued "2026-06-01"^^xsd:date ;
dct:modified "2026-08-04"^^xsd:date ;
dct:publisher <https://www.omg.org/> ;
dct:contributor <https://www.linkedin.com/in/matthiasautrata> ;
Expand Down
4 changes: 4 additions & 0 deletions respec/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
latestVersion: "https://www.omg.org/spec/DPROD/dprod/",
edDraftURI: "https://ekgf.org/spec/{{ branch }}/",
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
// browser (issue #253).
publishDate: "{{ publish_date }}",
editors: [
{
name: "Tony Seale, Chair",
Expand Down
14 changes: 14 additions & 0 deletions site/public/spec/archive/1.0/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
// group: "Semantic Data Products Working Group",
latestVersion: "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
// commit touches the branch. Without it ReSpec would stamp the day the
// page is viewed, since it runs in the reader's browser (issue #253).
//
// The OMG document number is authoritative for the date of a published
// DPROD specification. This is ptc/25-02-01, whose YY-MM-NN form gives
// February 2025, and the document's own cover agrees ("Date: February
// 2025").
//
// The OMG catalog page instead records "Publication Date: January 2025".
// It is not followed: the document number wins. Neither source states a
// day, so the 01 is a convention rather than a fact.
publishDate: "2025-02-01",
editors: [
{
name: "Tony Seale, Chair",
Expand Down
30 changes: 29 additions & 1 deletion spec-generator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ def detect_branch() -> str:
return "main"


def detect_publish_date(g) -> str:
"""The publication date of this version, from `dct:issued` on the ontology.

ReSpec runs in the reader's browser, and with no `publishDate` it defaults
to the day the page is *viewed*. Every build therefore claimed to have been
published today — including the frozen 1.0 archive, whose header changed
date every morning (issue #253).

The date is read from a triple rather than derived from git history. A
commit date would mean any maintenance change — retiring a URL, fixing a
typo — silently republished the standard. Issuing a version is a decision,
so it is recorded as one.
"""
issued = g.value(URIRef(ontology_namespace_iri), DCTERMS.issued)
if issued is None:
raise RuntimeError(
f"<{ontology_namespace_iri}> has no dct:issued. The spec's "
"publication date is authored in dprod-ontology.ttl; add the "
"triple rather than letting ReSpec fall back to today's date."
)
return str(issued)


# Define a function to add classes and properties to the context
def add_to_context(g, node_shape_iri, node_shapes: dict):
if not is_node_shape(g, node_shape_iri): # we're at this point only interested in the NodeShapes
Expand Down Expand Up @@ -100,7 +123,12 @@ def main():

# generate_class_and_property_pages(classes)

generate_spec_page({'classes': classes, 'examples': examples, 'branch': detect_branch()})
generate_spec_page({
'classes': classes,
'examples': examples,
'branch': detect_branch(),
'publish_date': detect_publish_date(g),
})

g_ontology = load_dprod_ontology()
g_shapes = load_dprod_shapes()
Expand Down
76 changes: 76 additions & 0 deletions tests/test_spec_publish_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""The spec's publication date must be authored, never inferred.

ReSpec runs in the reader's browser. With no `publishDate` in its config it
stamps the day the page is *viewed*, so every DPROD document — including the
frozen 1.0 archive — reported that it had been published today, and the date
changed every morning (issue #253).

Deriving the date from git history does not fix it either: a maintenance
commit that retires a URL or fixes a typo would silently republish the
standard. The date is therefore a triple, `dct:issued`, and these tests keep
it that way.
"""

from __future__ import annotations

import re
import unittest
from pathlib import Path

from rdflib import DCTERMS, Graph, URIRef

REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
ONTOLOGY_FILE = REPOSITORY_ROOT / "ontology" / "dprod" / "dprod-ontology.ttl"
SPEC_TEMPLATE = REPOSITORY_ROOT / "respec" / "template.html"
ARCHIVE_SPEC = (
REPOSITORY_ROOT / "site" / "public" / "spec" / "archive" / "1.0" / "index.html"
)
ONTOLOGY_IRI = URIRef("https://www.omg.org/spec/DPROD/dprod/")
ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


class SpecPublishDateTest(unittest.TestCase):
def test_ontology_carries_exactly_one_issued_date(self) -> None:
graph = Graph()
graph.parse(ONTOLOGY_FILE, format="ttl")

issued = list(graph.objects(ONTOLOGY_IRI, DCTERMS.issued))

self.assertEqual(
1,
len(issued),
"The DPROD ontology must carry exactly one dct:issued; it is the "
"single source of the spec's publication date.",
)
self.assertRegex(str(issued[0]), ISO_DATE)

def test_template_pins_the_publish_date(self) -> None:
template = SPEC_TEMPLATE.read_text(encoding="utf-8")

self.assertIn(
'publishDate: "{{ publish_date }}"',
template,
"respec/template.html must pin publishDate. Without it ReSpec "
"falls back to the date the page is viewed.",
)

def test_frozen_archive_pins_a_literal_publish_date(self) -> None:
"""The 1.0 archive is immutable, so its date is a literal, not a build value."""
archive = ARCHIVE_SPEC.read_text(encoding="utf-8")

match = re.search(r'publishDate:\s*"(\d{4}-\d{2}-\d{2})"', archive)

self.assertIsNotNone(
match,
"The frozen 1.0 archive must pin its own publishDate, otherwise an "
"immutable standard reports a new publication date every day.",
)
self.assertNotIn(
"{{",
match.group(0),
"The archive is a static snapshot and must not be templated.",
)


if __name__ == "__main__":
unittest.main()