Skip to content

Add the first cut of the LinkML schema for one MHPKG slice - #54

Merged
jh-RLI merged 4 commits into
productionfrom
linkml-first-cut
Aug 13, 2026
Merged

jh-RLI merged 4 commits into
productionfrom
linkml-first-cut

Conversation

@jh-RLI

@jh-RLI jh-RLI commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary of the discussion

Covers a municipal heat plan, its target scenario and one final energy consumption value: deep enough to test the LinkML-authors-SHACL decision against real MHPO and OEO terms, small enough to finish.

The decision holds. gen-shacl puts class_uri straight into sh:targetClass, so the generated shapes constrain MHPO and OEO IRIs directly rather than a LinkML-shaped shadow of them. MHPO's role reification did not bite here, because OEO's quantity value already reifies the same way LinkML would — but that is a deferral, not a clearance: the areas-and-roles subtree is three quarters of MHPO and this slice does not enter it.

Enums are the pattern to reuse. OEO models units, carriers and sectors as classes while a statistic is about the carrier type, so pointing at a class IRI is unavoidable punning. An enum whose every meaning: is an OEO IRI generates sh:in, a real value-set constraint instead of sh:nodeKind sh:IRI. oekg/shapes/oekg_shapes.ttl already does this by hand for ~120 IRIs.

Four things that will shape later tickets:

  • The IRI policy cannot be generated. SHACL constrains a node's own IRI with sh:pattern at node level and LinkML cannot emit that; a type-level pattern is dropped (silently with uri: set, otherwise logged as ERROR and still exit 0), and on an identifier slot it lands on an invented property that no triple uses, so it can never fire. Hence the hand-written companion file — the first artifact here that is not generated.
  • Enum membership cannot be checked by anything in this repository. OEO's energy carrier has four asserted subclasses, all abstract; natural gas qualifies only by inference. Confirming membership needs a reasoner and the toolchain deliberately has none.
  • MH-02 §6 and MHPO collide over the municipality. The policy reuses municipality/AGS_06611000 for the commissioning body, but the only class for it is municipality area — a spatial region cannot commission a plan, and the two IRI shapes cannot both be satisfied. Needs a human.
  • pyshacl accepts repeated -s and silently uses only the last one. The first validation run reported Conforms: True against both shape files while consulting neither, and still passed with a required label deleted. validate.py merges the shapes into one graph for that reason.

Also recorded: closed shapes reject provenance triples; German labels must be plain literals because @de is rdf:langString and fails sh:datatype xsd:string; and Turtle cannot abbreviate these IRIs at all, since a prefixed name may not contain an unescaped slash.

Five term requests are left as commented blocks at the position they would occupy, so the holes are visible in the artifact. No term is minted here and gen-owl is never run.

Verified with linkml 1.11.1, pyshacl 0.40.1, rdflib 7.6.0 against MHPO pin 34776b6 and OEO 2.13.0. validate.py asserts all 14 negative-control violations are caught and exits 1 when the IRI shapes are removed.

Type of change (CHANGELOG.md)

Added

  • mhpkg/schema/: first cut of the MHPKG data shape — a LinkML schema for one slice, the SHACL
    generated from it, a hand-written companion enforcing the IRI policy, and a passing plus a
    deliberately failing example (#54)

Updated

  • Nothing. This PR only adds; pyproject.toml and uv.lock are deliberately untouched, since
    MH-04's schema and graph dependency groups already carried everything needed.

Removed

  • Nothing.

Workflow checklist

Automation

Closes #55

PR-Assignee

Reviewer

  • 🐙 Follow the Reviewer Guidelines
  • 🐙 Provided feedback and show sufficient appreciation for the work done

Covers a municipal heat plan, its target scenario and one final energy
consumption value: deep enough to test the LinkML-authors-SHACL decision
against real MHPO and OEO terms, small enough to finish.

The decision holds. gen-shacl puts class_uri straight into sh:targetClass,
so the generated shapes constrain MHPO and OEO IRIs directly rather than a
LinkML-shaped shadow of them. MHPO's role reification did not bite here,
because OEO's quantity value already reifies the same way LinkML would —
but that is a deferral, not a clearance: the areas-and-roles subtree is
three quarters of MHPO and this slice does not enter it.

Enums are the pattern to reuse. OEO models units, carriers and sectors as
classes while a statistic is about the carrier type, so pointing at a class
IRI is unavoidable punning. An enum whose every meaning: is an OEO IRI
generates sh:in, a real value-set constraint instead of sh:nodeKind sh:IRI.
oekg/shapes/oekg_shapes.ttl already does this by hand for ~120 IRIs.

Four things that will shape later tickets:

* The IRI policy cannot be generated. SHACL constrains a node's own IRI
  with sh:pattern at node level and LinkML cannot emit that; a type-level
  pattern is dropped (silently with uri: set, otherwise logged as ERROR and
  still exit 0), and on an identifier slot it lands on an invented property
  that no triple uses, so it can never fire. Hence the hand-written
  companion file — the first artifact here that is not generated.
* Enum membership cannot be checked by anything in this repository. OEO's
  energy carrier has four asserted subclasses, all abstract; natural gas
  qualifies only by inference. Confirming membership needs a reasoner and
  the toolchain deliberately has none.
* MH-02 §6 and MHPO collide over the municipality. The policy reuses
  municipality/AGS_06611000 for the commissioning body, but the only class
  for it is municipality area — a spatial region cannot commission a plan,
  and the two IRI shapes cannot both be satisfied. Needs a human.
* pyshacl accepts repeated -s and silently uses only the last one. The
  first validation run reported Conforms: True against both shape files
  while consulting neither, and still passed with a required label deleted.
  validate.py merges the shapes into one graph for that reason.

Also recorded: closed shapes reject provenance triples; German labels must
be plain literals because @de is rdf:langString and fails sh:datatype
xsd:string; and Turtle cannot abbreviate these IRIs at all, since a
prefixed name may not contain an unescaped slash.

Five term requests are left as commented blocks at the position they would
occupy, so the holes are visible in the artifact. No term is minted here
and gen-owl is never run.

Verified with linkml 1.11.1, pyshacl 0.40.1, rdflib 7.6.0 against MHPO pin
34776b6 and OEO 2.13.0. validate.py asserts all 14 negative-control
violations are caught and exits 1 when the IRI shapes are removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jh-RLI jh-RLI linked an issue Aug 13, 2026 that may be closed by this pull request
8 tasks
jh-RLI and others added 3 commits August 13, 2026 14:31
`gen-shacl` output is not byte-stable across runs. Every sh:property is a
blank node and rdflib mints fresh blank-node ids per process, so the
property blocks come out in a different order every time: three
consecutive runs of the same unchanged schema gave three different
SHA-256 sums, and PYTHONHASHSEED=0 does not help (three more runs, three
more sums). So this is blank-node id generation, not dict ordering.

That matters because the obvious drift check is a diff — it is what
mhpkg/mhpo/extract_terms.py --check does for terms.csv — and here it
would fail on every run while nothing was wrong. A check that cries wolf
every time is worse than no check, because people learn to ignore it.

check_generated.py compares the committed file to a fresh generation
graph-isomorphically via rdflib.compare.to_isomorphic, and confirms the
two are equal today at 206 triples. On failure it reports the concrete
constraint that differs rather than the raw set difference, which after
canonicalisation relabels every blank node and names nothing anyone can
act on: a planted `sh:minInclusive 1999` is now reported as exactly that.

It also fails when the generator logs an error while still succeeding.
gen-shacl drops a type-level pattern and exits 0, reporting `No URI for
type X` only through the logger, so the generator runs in-process here
and its log records are treated as part of the result. Verified with a
type-level pattern on a type an actual slot uses — the earlier attempt
put it on an unused type, which never gets resolved and so never logs.

Kept separate from validate.py on purpose: this needs the `schema` group
because it regenerates, while validate.py needs only `graph`. That split
is MH-04's and it exists because linkml is 88 of the 95 packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four files said things that stopped being true when mhpkg/schema/ landed.
The rule applied is the one MH-04 used: fix what your own change
falsified, and leave the rest to the ticket that owns it.

docs/mhpkg/index.md said "SHACL shapes: none yet" and described the model
as one Termboard draft. It also carried the provisional-name warning
telling readers not to bake `mhpkg` into IRIs or namespace prefixes —
which the committed schema now does, so a reader following the published
docs would have refused the namespace the code depends on. That is the
worst of these: not stale, but contradicted by shipped code.

docs/tech-stack.md said the schema does not exist yet and that gen-shacl
"has nothing to point at", and its workflow diagram still marked the model
source of truth UNDECIDED. The diagram now distinguishes what is built
from what is not, and records the two limits that constrain the
generated-artifacts question: the IRI policy cannot be generated, and
generated SHACL is not byte-stable so a drift check must compare graphs.

Its "what validates what" section needed care rather than deletion. "No
SHACL file here validates a live graph" is still true — the new shapes
validate example files, not the store — so it is kept and sharpened,
split into what was authored against a dump and what is now exercised on
every validate.py run. Added while checking: the old OEKG shapes cannot
match the live graph even in principle, because they use a hyphenated
http OEO namespace and property names OEO does not define. That namespace
redirects, but an IRI is identity rather than an address.

mhpkg/README.md claimed only one directory exists, and listed the model
source of truth and the Fuseki dataset as open questions. Both are now
decided; the internal layout is still open and stays listed.

CONTRIBUTING.md trap 4 carried the same superseded provisional-name
warning. The map had assigned that edit elsewhere, but leaving one of
three copies of a contradicted warning is worse than fixing all three,
and this change is what contradicts it. Rewritten in the style trap 5
already uses for a corrected claim, so anyone who read the old text sees
what changed.

Not touched: the graph/table boundary, still genuinely undecided, and the
absence of a SHACL CI job, which remains true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jh-RLI
jh-RLI merged commit fbc2d68 into production Aug 13, 2026
1 check passed
@jh-RLI
jh-RLI deleted the linkml-first-cut branch August 13, 2026 13:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MHPKG has no authored data shape yet

1 participant