Metadata-Version: 2.4
Name: iirds
Version: 0.5.0
Summary: Validate, lint, pack and serve iiRDS packages from the command line; read and write them from Python. Offline, graph-based. Held in stewardship for the iiRDS ecosystem.
Author-email: Wooyong Lee <zero8004paz@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/dev365code/iirds-validate
Project-URL: Source, https://github.com/dev365code/iirds-validate
Project-URL: Issues, https://github.com/dev365code/iirds-validate/issues
Project-URL: Changelog, https://github.com/dev365code/iirds-validate/blob/main/CHANGELOG.md
Keywords: iirds,tekom,rdf,validation,technical-documentation,intelligent-information,vdi2770
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Text Processing :: Markup :: XML
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
License-File: THIRD_PARTY.md
Requires-Dist: rdflib>=6
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff==0.16.3; extra == "dev"
Provides-Extra: shacl
Requires-Dist: pyshacl==0.40.*; extra == "shacl"
Dynamic: license-file

```text
    _ _ ____  ____  _____
   (_|_) __ \/ __ \/ ___/                 ___    __     __
  / / / /_/ / / / /\__ \       _  _____ _/ (_)__/ /__ _/ /____
 / / / _, _/ /_/ /___/ /      | |/ / _ `/ / / _  / _ `/ __/ -_)
/_/_/_/ |_/_____//____/       |___/\_,_/_/_/\_,_/\_,_/\__/\__/
```

<div align="center">

*conformance and interoperability checking for iiRDS packages — offline*

[![CI](https://github.com/dev365code/iirds-validate/actions/workflows/ci.yml/badge.svg)](https://github.com/dev365code/iirds-validate/actions/workflows/ci.yml)
&nbsp;**Apache-2.0**&nbsp;·&nbsp;**Python 3.9–3.13**&nbsp;·&nbsp;**Linux · macOS · Windows**&nbsp;·&nbsp;**zero network, by design**

</div>

---

**A package can satisfy every rule in the [iiRDS](https://iirds.org)
specification and still be unreadable to whoever receives it.** This checks for
both, from the command line, on a machine with no internet connection, as a step
in a build.

> **At a glance** — 198 rules across five editions and three profiles · one
> pure-Python dependency (rdflib), zero for the single-file `.pyz` ·
> [what it checks](#what-it-checks) · [limits, measured](#limits-measured) ·
> [how it knows it is right](https://github.com/dev365code/iirds-validate/blob/main/docs/scope.md) · every number in this file is
> re-derivable from a committed tool, and the console sample below is
> generated by a test that fails the build when it goes stale.

```console
$ iirds manual.iirds
manual.iirds   iiRDS 1.3
  note: metadata read from META-INF/metadata.rdf

  ERROR M11       Rendition must have exactly one iirds:format
                      urn:example:manual has-rendition
                      0 found
                    → Give the Rendition exactly one iirds:format, holding the media type of the
                    → file it points at, for example application/xhtml+xml or application/pdf.
                    → Add one if there is none; remove the extras if there are several.
  WARN  L1        relation points at an IRI that is never described in this package
                      urn:example:event/al-204
                      referenced by Operating manual via relates-to-event
                    → Either describe the target in this package, or drop the reference. A
                    → relation pointing at an IRI nothing here mentions gives a consumer a name
                    → and no way to resolve it.

  FAIL  1 error(s), 1 warning(s), 0 informational
  177 rules checked, 21 not applicable to this version/variant (19 for iiRDS/H, 2 for other editions)
$ echo $?
1
```

**How to read a report.** `ERROR` is a specification violation and fails the
build (`exit 1`); `WARN` is this project's interoperability advice and does not,
unless you pass `-W`; the dimmed `note:` lines at the top are context about the
run, not findings. When one rule fires many times the report shows the count,
the first few subjects and the remedy once — `--format json` always carries
every finding. What caused other findings is printed first; what merely follows
from them, last.

The error is a specification violation. The warning is not — nothing in the
standard forbids pointing at an IRI you never describe, and a consumer reading
that package silently loses the data. It happens in one of tekom's own sample
packages.

---

## Start here

```sh
pip install iirds
iirds manual.iirds
```

One package, one runtime dependency (rdflib, pure Python): the checker, the
`iirds` command with `check`, `lint`, `pack` and `serve` under it, and the
`iirds` library for reading and writing packages from Python. Published to
PyPI via trusted publishing. `iirds-validate` and `iirdsv` still name the same
command, and `pip install iirds-validate` still resolves — to this. Tools that
install by executable, such as pipx and `uv tool`, want the name that has
one: `iirds`.

**Nothing to install:** copy one file in and run it — `python iirds.pyz dist/`.
Under a megabyte, contains rdflib and the iiRDS ontologies, compiles nothing, so
the same file runs on Linux, macOS and Windows. It is an ordinary zip: whoever
has to approve software entering the network can open it and read every line,
which is usually the hard part. Build it with `python tools/build_zipapp.py`;
for an air-gapped install see [docs/offline-install.md](https://github.com/dev365code/iirds-validate/blob/main/docs/offline-install.md).

**Then point it at something:**

```sh
iirds dist/manual.iirds      # a package
iirds build/manual/          # the same package before it was zipped
iirds dist/                  # every package under a directory
```

A path means "check it". No subcommand needed.

**Upgrading from `iirds-validate` 0.4.x?** `pip uninstall -y iirds-validate`
first, then `pip install -U iirds` — the `-U` because the `iirds` library
already there would satisfy a plain install. [CHANGELOG.md](https://github.com/dev365code/iirds-validate/blob/main/CHANGELOG.md) says
why the order matters.

---

## The commands

| | |
|---|---|
| `iirds <path>` | check **and** lint — what you want most of the time |
| `iirds check <path>` | **does it conform?** container, metadata graph, content |
| `iirds lint <path>` | **will anyone else be able to read it?** |
| `iirds check --fragment <file>` | a bare metadata snippet — spec example, editor's draft — with package-level rules suspended and named |
| `iirds pack <dir>` | write a directory as a conformant `.iirds`, then check that |
| `iirds rules` | every rule, one line each; `iirds rules M11` or `-v` adds versions, spec link, source and remedy |
| `iirds serve` | a drop page on this machine, for people who do not read terminals — same verdict, same process, nothing on a network |

### A drop page, for people who do not read terminals

```sh
iirds serve              # opens a browser at 127.0.0.1 on a free port
iirds serve --no-open    # prints the address instead
iirds serve --port 8791  # a port you choose
iirds serve --host ::1   # loopback by any other name; anything else is refused
```

Drag a `.iirds` file onto the page. The file is read by the process you just
started, on the machine you started it on. The page runs the same rules and
the same renderer in the same process as `iirds <path>`, so the findings and
their wording are the command line's — there is no second implementation of
the report to disagree with the first.

Three things differ, by construction rather than by accident: the page renders
into a string and so never carries the colour a run at a terminal does; where
a finding quotes the container's own path — C1 and S1 do, when the file cannot
be opened at all — it quotes the copy the handler made; and on Windows the
command line's line endings are the platform's while the page's are `\n`. Flags belong to
the command line: `-v`, `-q`, `-W`, `--format json`, `--fragment`, a directory,
several packages at once. What the page gives is the default run on one file.

The chrome is offered in English, German, Korean, Japanese and Chinese, and
follows your browser until you choose otherwise; light and dark follow the
system until you say otherwise. **The report itself is not translated** — it
is the command line's output, word for word, and every language says so.

It binds to the loopback interface and refuses any other address. This is a
window onto a local command, not a service: nothing listens beyond this
machine, nothing is uploaded, and the page ends when you stop the command. For
a network with nothing installed on it, the answer is still the single file —
see [`docs/offline-install.md`](https://github.com/dev365code/iirds-validate/blob/main/docs/offline-install.md).

### In a build

```sh
iirds check dist/ || exit 1            # fail the build on any error
iirds dist/ --format json > report.json
iirds check dist/ -W                   # warnings fail it too
iirds check dist/ -q                   # exit code only
```

Exit codes: `0` clean (warnings alone stay `0` unless `-W`), `1` errors, `2` could not run.

### From Python

```python
from iirds_validate import check, lint

report = check("manual.iirds")
for finding in report.findings:
    print(finding.id, finding.severity, finding.violation.message)
```

`report.as_dict()` is what `--format json` prints. Every finding carries
`source`, which is `catalogue` or `iirds-validate` — the name this project's own
rules have carried since the first release, kept so that stored reports stay
comparable — so a stored report stays unambiguous even if the catalogue later
mints an identifier this project already uses. The library that reads and
writes packages is described [below](#reading-and-writing-packages-from-python).

### Flags

| | |
|---|---|
| `--format json` | machine-readable; the banner never appears in it |
| `--iirds-version 1.2` | validate against a version other than the declared one |
| `-W` | warnings fail the run |
| `-q` | exit code only |
| `-v` | print the specification link behind each finding |

Two environment variables, both optional:

| | |
|---|---|
| `IIRDS_CONTENT_BUDGET` | the most a run will decompress in total, in bytes (default half a gigabyte). Per-entry limits bound each rendition; this bounds their sum, so an archive that compresses to nothing cannot make a run read as much as it declares. When it is reached, **S9** names the first rendition it stopped at and says the rest were not examined |
| `NO_COLOR` | no terminal colour, whatever the stream is |

---

## What makes it different

The [iiRDS Validation Tool](https://github.com/plusmeta/iirds-validation-tool)
by plusmeta is good and actively maintained, and its rule catalogue is the
foundation this project is built on — the rule identifiers here are deliberately
the same so results can be compared rule by rule. Use it to look at one package
by hand; that is what it is for. Everywhere the two disagree is written down,
with evidence, in [docs/divergences.md](https://github.com/dev365code/iirds-validate/blob/main/docs/divergences.md).

Four things here are different.

**It asks whether the package will work, not only whether it conforms.** Fifteen
interoperability rules, most with no counterpart in the specification, because
a conformant package can still be undeliverable (L2 and L9 do implement
sentences the standard states, and run under `check` accordingly):

| | |
|---|---|
| L1 | a relation points at an IRI the package never describes |
| L2 | `iirds:source` names a file that was not packed |
| L3 | a directory node unreachable from any root — invisible in every viewer |
| L4 | a cycle in the navigation structure |
| L5 | a proprietary class not linked to any iiRDS class |
| L6 | a metadata value with no label a consumer could display or match |
| L7 | an information unit with no title |
| L8 | references out to vocabularies an offline consumer cannot resolve |
| L9 | the RDF/XML and JSON-LD metadata describe different graphs |
| L10 | an abstract iiRDS class used to type an instance directly |
| L11 | content named `.xhtml` but declared as another media type, so nothing checked it |
| L12 | two entries differing only in case, so one is lost when the package is unpacked |
| L13 | a name in the iiRDS namespace that the standard does not define, with the term that was probably meant |
| L14 | a namespace one character from an iiRDS namespace, so that every name under it resolves to nothing |
| L15 | a name from a later edition of iiRDS than the package declares, so a consumer reading it as declared has no definition for it |

**It checks the content.** Appendix B states 25 absolute requirements about
iiRDS XHTML5 — no scripting, no forms, no `<svg>`, a fixed element list, a
hazard-statement vocabulary — and no tool checked any of them. Every rule in the
reference catalogue reads `META-INF/metadata.rdf` and never opens a content
file, so a package can pass every conformance check that exists while its
documents cannot be rendered.

**It reads the graph, not the document.** iiRDS metadata is RDF, and RDF/XML is
not a canonical way of writing it down. These are the same statement:

```xml
<iirds:Document rdf:about="urn:d1"/>

<rdf:Description rdf:about="urn:d1">
  <rdf:type rdf:resource="http://iirds.tekom.de/iirds#Document"/>
</rdf:Description>
```

A validator that walks the XML tree sees the shape its own generator emits and
silently reports a clean package for the others.
`tools/serialisation_equivalence.py` takes a real package, rewrites its metadata
four ways and checks the findings are identical. The same property is what makes
`META-INF/metadata.jsonld` work at all.

**It runs where the packages are.** Unattended, in CI, behind an air gap, from a
single file that needs no installation. Exit codes, JSON, a library API. That
the alternative validates client-side is true and is not the same as never
loading the page: a hosted application is fetched fresh every visit, and "open a
browser tab to an external domain and feed it engineering documentation" is not
a request that passes review at a manufacturer.

---

## What it checks

```console
$ iirds rules
container  19/19    the ZIP and its layout  +3 of its own
schema     135/135  the metadata graph  +6 of its own
system     3/3      the run itself  +7 of its own
content    -        iiRDS XHTML5 (Appendix B)  +10 of its own
lint       -        will a consumer be able to use it  +15 of its own
```

157 of 157 catalogued rules, plus 41 of this project's own.

| kind | catalogued | this project |
|---|---|---|
| container (C\*) | 19 / 19 | 3 |
| schema (M\*) | 135 / 135 | 6 |
| system (S\*) | 3 / 3 | 7 |
| content (B\*) | — | 10 |
| interoperability (L\*) | — | 15 |

Coverage of the catalogue is not coverage of the standard. The specification
states **314 absolute obligations**, counted by
[`tools/extract_requirements.py`](https://github.com/dev365code/iirds-validate/blob/main/tools/extract_requirements.py) and listed in
[docs/requirements.json](https://github.com/dev365code/iirds-validate/blob/main/docs/requirements.json) — 254 marked with an RFC 2119
keyword and 60 more stated as `0..1` in the property tables, which carry no
keyword at all and are obligations regardless. This README carried 254 from its first day
with nothing behind it; the figure was right about what it counted and counted
the wrong thing.

That is the denominator, not a score. One rule can cover several statements and
several rules one statement, and some requirements are not machine-checkable at
all. Mapping the 314 to rules is not done, so **this tool cannot tell you what
share of the standard it checks**, and "no findings" must not be read as
"conformant". `iirds rules -v` prints the specification link behind each rule. Three of
the 157 are aliases of rules with identical wording, one is a `MAY` with nothing
to violate, and two are conditions the runner reports rather than rules it
evaluates.

### Versions and profiles

iiRDS 1.0, 1.0.1, 1.1, 1.2 and 1.3, and the unrestricted, `A` and `H` profiles.
The axes are independent — a rule can be 1.3-only, iiRDS/H-only, or both — and
every combination is exercised by the suite.

An `iirds:iiRDSVersion` the standard never published is a finding, not something
quietly rounded to the newest version, and an `iirds:formatRestriction` matching
no profile is a finding rather than a way to switch both rule sets off at once.
Only the 1.3 ontology is bundled, so validating against an earlier version
borrows its class hierarchy; the report says so when it happens.

---

## The rules as SHACL — for everyone who is not running Python

[`shapes/`](https://github.com/dev365code/iirds-validate/tree/main/shapes) carries the language-neutral encoding: 142 SHACL shapes
generated from the same sources as the rules, written to SHACL Core and
SHACL-AF and tested on pySHACL 0.40, so a SHACL engine can check the graph
half of iiRDS conformance without this project's code. Every shape carries
the remedy text, severity, spec link and requirement id; every one is
differentially tested against the Python rules — fire-set equality over the
reference corpus, severity equality on every mutant and provocation fixture, and a
closing check that no shape sits the suite out — and [`shapes/MANIFEST.json`](https://github.com/dev365code/iirds-validate/blob/main/shapes/MANIFEST.json) accounts for
every rule without a shape, starting with the 40 that can never be one (ZIP
bytes have no graph), so nobody mistakes shapes for full conformance.
[`shapes/README.md`](https://github.com/dev365code/iirds-validate/blob/main/shapes/README.md) has the three conventions that matter
and the honest caveats.

## Limits, measured

"Can it handle large packages?" is three questions, because validation grows
along three independent axes — and only one of them costs anything:

| axis | scale tested | time | peak memory |
|---|---|---|---|
| **graph** — information units in the metadata | 50,000 topics (≈450k triples) | 15.6 s | ≈1 GB |
| | 20,000 topics | 5.8 s | ≈420 MB |
| **entries** — files in the archive | 70,000 entries | 0.7 s | — |
| **batch** — packages per invocation | 200 packages | 1.3 s | — |

Time is linear in graph size. Memory lives in the metadata graph alone —
roughly a hundred times the size of `metadata.rdf`, because rdflib holds it
in memory; content files are streamed one at a time and never held. Metadata
above 64 MiB is refused at that point rather than parsed, which also caps memory at a few GB
for the largest metadata the guard admits. Numbers from a laptop; re-derive
them on yours:

```sh
python tools/benchmark.py --full
```

## Directories, and packing one

A package spends most of its life as a directory, and checking it there finds a
defect in the thing you just made rather than in the artefact.

Five requirements are about the archive rather than the package — the `.iirds`
extension, `mimetype` first and stored uncompressed, no encryption, ZIP64 past
the limits — and cannot be assessed before there is one. The report says which,
rather than passing them in silence. `iirds pack` closes that:

```sh
iirds pack build/manual/ -o dist/manual.iirds
```

It writes the archive the way the specification requires, then validates what it
wrote. "First entry, stored uncompressed" is the requirement people get wrong
most often, and not through carelessness: `zip` manages it only with two
invocations and the right flags, most graphical tools cannot express it, and
`shutil.make_archive` gets it wrong every time. Packing the same directory twice
produces the same bytes, so "this archive came from that directory" is checkable
with `sha256` rather than taken on trust.

---

## Trusting the answer

Every defect this project has found in itself is recorded where it was fixed:
in the changelog entry, in the regression test that now covers it, and in the
commit both point at. That record is the argument for why there is twice as
much test and tool code here as validator.

[docs/scope.md](https://github.com/dev365code/iirds-validate/blob/main/docs/scope.md) is the map: what this is, what it deliberately is
not, where each thing lives, the four ways a validator can be wrong and which
instrument here finds which — and the list of what is still unresolved.


A validator's whole product is its verdict, and a wrong verdict is invisible
from the inside: it prints `PASS` and you learn nothing. So the evidence lives in
the repository.

- **Cross-validation, against a corpus that is in the repository.** The
  reference tool's own fixtures are vendored at the revision its rule catalogue
  came from, with a SHA-256 for each, so `tools/crossvalidate.py` and
  `tools/explain_silence.py` run offline and anyone can re-derive what is
  claimed below. Of the 103 rule/fixture pairs it says must fail, the expected
  rule fires here on 42; 34 more are cases where the reference does not report
  either, 11 are gated by version or variant, 9 are fixtures nobody can parse,
  3 are defects visible only in the XML tree — two serialisations of one
  graph, so there is nothing in the graph to report — and **4 are genuinely
  unresolved**.
  The full table, and why "65 of 66 fixtures produce some finding" is the
  flattering way to say this rather than the honest one, are in
  [docs/divergences.md](https://github.com/dev365code/iirds-validate/blob/main/docs/divergences.md).
- **Reports are ordered for a reader.** What caused the rest comes first, what
  merely follows from it comes last, severity in between. An archive zipped one
  directory too high used to open with three findings telling you to add files
  you already had; it now opens with the one saying your package is fine and
  merely misplaced.
- **Every finding says what to do about it.** All 198 rules carry one imperative
  sentence naming the change, and `tests/test_remediation.py` refuses a rule
  that does not. A validator that names a defect and not the remedy has told
  you that something is wrong and left you the specification to search, which
  is most of the work and all of the expertise.
- **Every rule has been watched fire.** The suite records which rule ids
  actually produce a finding, and 197 of the 198 have — the remaining one is a
  `MAY` with nothing to violate. It began at 63. A rule that fires nowhere is
  not known to work: S8 was exactly backwards from the day it was written, able to fire only on
  archives that were correct, and no test would have caught it because no test
  made it fire. Line coverage would not have helped; its body ran and returned
  the wrong answer.
- **Deterministic output**, byte-identical across `PYTHONHASHSEED` values, so two
  runs can be diffed.
- **No network, tested rather than asserted.** A JSON-LD `@context` may be a URL
  and the parser will dereference it, so remote contexts are refused — inside a
  plant network that is not only a broken promise but a supplier choosing which
  host a machine behind the firewall connects to.
- **Integrity.** The bundled ontologies are checked against recorded SHA-256
  digests; `python -m iirds_validate.ontology --verify` does it from the
  installed copy.
- **CI.** Python 3.9 to 3.13, Windows, rdflib at its 6.0.0 floor and at 7, the wheel installed into
  a clean environment, and the single-file form run with `python -S` so anything
  that works came out of the archive.

**What is not established.** The 41 rules this project invented have no second
implementation anywhere to be compared against. They have tests in both
directions, and those tests were checked by breaking each rule in turn, which is
weaker evidence than the catalogued rules have.
[docs/divergences.md](https://github.com/dev365code/iirds-validate/blob/main/docs/divergences.md) records where this project is
deliberately stricter than the reference and why. Anything derived from this
project's own reading rather than a literal `MUST` is a warning — with the
current exceptions named, not hidden, in that same document: L4, and the entry
condition that decides which files the Appendix B rules examine.

If it reports an error on a package you believe is conformant, that is the most
valuable bug report this project can receive. Please open an issue with the
package or a reduced case.

---

## Contributing

A rule is its implementation and two tests; the metadata comes from the
catalogue. See [CONTRIBUTING.md](https://github.com/dev365code/iirds-validate/blob/main/CONTRIBUTING.md) — including the DCO: every
commit carries a `Signed-off-by` line (`git commit -s`), which is a
certificate of origin, not a transfer of rights. Four rules of the road, each
of which exists because it was broken once:

1. Never spell an iiRDS term inline. Add it to `terms.py`, where a test confirms
   it exists in the ontology.
2. Ask the graph, not the document. A rule that behaves differently on JSON-LD
   is wrong.
3. Every rule needs a package that violates it and one that does not.
4. Do not edit `data/ontologies/`. Verbatim redistribution is a licence
   condition and the hashes are checked.

## Reading and writing packages from Python

The `iirds` library ships in the same distribution as the checker, and is the
container layer the checker is built on.

```python
import iirds

with iirds.open("machine-docs.iirds") as pkg:
    print(pkg.version, pkg.variant)      # "1.3" "unrestricted"
    graph = pkg.graph                    # rdflib.Graph of the package metadata
    data = pkg.read("content/topic1.xhtml")

iirds.pack("my-package-directory/")     # → my-package-directory.iirds,
                                         #   mimetype first and stored,
                                         #   byte-identical on every run
```

Deliberately small: open a container, get the metadata as an RDF graph, read
files, write a conformant container back. **`import iirds` does not validate**
— the checker is the `iirds` command and the `iirds_validate` package beside
it — and the library never imports the checker, so a tool built on it
inherits one dependency (rdflib) and no verdicts.

Two things the `pack()` half gets right that generic ZIP tooling gets wrong:
the `mimetype` entry is first and stored uncompressed, and packing the same
directory twice produces byte-identical output (honouring
`SOURCE_DATE_EPOCH`), so "this archive came from that directory" is checkable
with a hash instead of taken on trust.

### Queries

`Package.instances_of(cls)`, `Package.is_instance(node, cls)` and
`Package.label_of(node)` — the first and the last also at module level, taking
any rdflib graph — answer "what is in this package" with **section-7
semantics**: an instance of a class the package
itself declares beneath an iiRDS class *is* an instance of that class.

```python
from iirds import IIRDS
with iirds.open("docs.iirds") as pkg:
    for topic in pkg.instances_of(IIRDS["Topic"]):
        print(pkg.label_of(topic))
```

The closure walks only the package's own `rdfs:subClassOf` declarations —
no ontology is bundled, so `instances_of(IIRDS["InformationUnit"])`
returns only what the package declares beneath it. One rdflib trap worth
knowing: `Namespace` subclasses `str`, so `IIRDS.format` is `str.format`;
**always use bracket syntax** (`IIRDS["format"]`).

**Deliberately not here** (so nobody waits for it): per-class conveniences
(`topics()` is `instances_of(IIRDS["Topic"])` and 25 siblings would drift),
`typed_exactly` (one rdflib call on the public graph), a bundled ontology,
SPARQL wrappers, and anything that returns a verdict.

### Files behind renditions

`pkg.source_of(node)` resolves a Rendition's `iirds:source` to the entry
it names — the helper a naive implementation gets wrong. The resolution
matches the checker case for case: leading slashes stripped, `./`
and internal `../` collapsed, backslashes folded, the value
percent-decoded and its fragment and query cut, and a value still
carrying a colon — which §5.1.3 excludes from file names — treated as
naming nothing here. Reading the value as a URL follows §6.3, which
calls it one; Appendix A calls it a path, and that unsettled question and
what this reading costs are recorded in
[docs/divergences.md](https://github.com/dev365code/iirds-validate/blob/main/docs/divergences.md). What differs between the two projects is the
ending, not the reading: this refuses to resolve a path that escapes the
package, where a validator answers with nothing and reports it.
`pkg.open(node)` returns a readable stream over that entry (streaming —
a two-gigabyte PDF is read, not loaded), raising when the node names
nothing or names an absent entry. The stream borrows the package's open
ZIP handle, so consume it while the `Package` is still open rather than
after `close()`. Resolution never judges existence: that split keeps
"what does this rendition say" apart from "is this package whole", which
is the validator's question.

### Writing metadata

`write_metadata(graph, destination=None)` serialises a graph as
`metadata.rdf` — and self-verifies: the bytes are parsed back through
the same guarded reader every consumer uses and compared isomorphically
before being handed over, so "the validator can read what the SDK wrote"
is enforced at write time. Byte-stable across repeated writes of the
same Graph object, and no more: rdflib mints blank-node labels from a
process-global counter, so even identically-built graphs serialise
apart, and canonicalisation would be a different, heavier promise.
Composes with `pack()`: write the metadata into a directory, pack the
directory, open the result.

### Untrusted input

A package arrives from a supplier, so `open()` treats its metadata the way
the checker does — the checker imports these guards from here, so there is
one set of guards and one set of error strings:

- XML entity declarations are refused (a tame one is indistinguishable from
  the geometric kind until the parser is already inside it),
- metadata above 64 MiB uncompressed is refused *before* being read,
- a JSON-LD `@context` that names something to fetch is refused wherever it
  nests — a URL, an `@import`, a scoped context on a term, and equally a
  bare name, which the parser would otherwise resolve against whatever
  directory the tool was run from and read off your disk. Reading a package
  touches neither the network nor anything outside the container,
- a byte order mark decides the encoding, as XML says it should.

`META-INF/metadata.jsonld` is read and merged beside `metadata.rdf`
(isomorphic sources count once — blank nodes double under naive union).
`pkg.metadata_sources`, `pkg.metadata_graphs` and `pkg.parse_errors` say
what parsed, which document said what, and what was refused. `pkg.graph`
raises only when *nothing* parsed, because an empty graph is also what a
real, sparse package looks like.

### API stability

0.x: the surface will grow; what is published is intended not to break.
The API is small on purpose — additions are cheap, retractions are not.

## Stewardship

The `iirds` name on PyPI belongs to the standard's community more than to any
one project. **Should the iiRDS Consortium want this name for an official
SDK, it will be transferred on request** — until then it does real work
rather than squatting. `iirds-sdk` is an alias of this package and travels
under the same pledge.

This is an unofficial project, not affiliated with or endorsed by the iiRDS
Consortium or tekom Deutschland e.V. "iiRDS" is used descriptively, to name
the standard these functions read and write.

## Licence

Apache-2.0 — see [LICENSE](https://github.com/dev365code/iirds-validate/blob/main/LICENSE).

The bundled iiRDS ontologies are © tekom Deutschland e.V. / iiRDS Consortium
under **CC BY-ND 4.0** and are redistributed verbatim; the rule catalogue is
derived from plusmeta's MIT-licensed tool. CC BY-ND is not an OSI-approved
licence, so this distribution is not wholly open source even though the code is
— [docs/licensing.md](https://github.com/dev365code/iirds-validate/blob/main/docs/licensing.md) explains what that means for you and
what would fix it. Provenance in [NOTICE](https://github.com/dev365code/iirds-validate/blob/main/NOTICE) and
[THIRD_PARTY.md](https://github.com/dev365code/iirds-validate/blob/main/THIRD_PARTY.md).

Not affiliated with, endorsed by, or certified by the iiRDS Consortium, tekom
Deutschland e.V., plusmeta GmbH or Quanos Solutions GmbH. "iiRDS" is used
descriptively to name the standard this tool validates against.
