Skip to content
Open
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ An adapter is "OLF-conformant" iff it passes the suite. That is the only gate.
The suite, not the corpus, not any single adapter, is the artifact other
projects can adopt even without using our archive.

The suite is runnable and dependency-free (Python standard library). Run it on a
document, or check identifier stability across two regenerations:

```bash
python -m conformance.akn4olf_conformance path/to/document.akn.xml
python -m conformance.akn4olf_conformance run1.akn.xml --compare run2.akn.xml
```

See [`conformance/README.md`](./conformance/README.md) for each criterion, what
is mechanically enforced versus advisory, and the bundled conformant /
non-conformant samples.

---

## How a jurisdiction joins
Expand Down
2 changes: 2 additions & 0 deletions conformance/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.py[cod]
198 changes: 198 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# AKN4OLF conformance suite

The heart of the project, and the only gate. This directory is the **runnable**
reference implementation of the six conformance criteria the [spec README](../README.md)
defines. An adapter's output is "OLF-conformant" **iff** it passes this suite —
independent of who wrote the adapter. The suite, not any single adapter or the
archive corpus, is the artifact other projects can adopt.

It is pure standard library (`xml.etree.ElementTree`): no third-party
dependencies, so any adapter author can run it.

```
conformance/
├── akn4olf_conformance.py library + CLI implementing criteria 1–6
├── __init__.py public API: check(), stability(), report types
├── selftest.py tiny self-test over the bundled samples
├── README.md this file
└── samples/
├── conformant_monovigente.akn.xml real Normattiva export (single-version)
├── conformant_multivigente.akn.xml synthetic multi-version document
└── nonconformant.akn.xml deliberately broken (fails criterion 3)
```

## How to run

```bash
# Check one document (criteria 1–5), per-criterion table + final verdict:
python -m conformance.akn4olf_conformance path/to/document.akn.xml

# Add the criterion-6 stability check against a second regeneration:
python -m conformance.akn4olf_conformance doc_run1.akn.xml --compare doc_run2.akn.xml

# Run the suite's own self-test over the bundled samples:
python -m conformance.selftest
```

As a library:

```python
from conformance import check, stability

report = check("document.akn.xml") # criteria 1–5 on one document
print(report.verdict) # "CONFORMANT" / "NON-CONFORMANT"
for c in report.criteria:
print(c.number, c.status.value, c.reason)

result = stability("run1.akn.xml", "run2.akn.xml") # criterion 6
print(result.status.value) # "PASS" / "FAIL"
```

The CLI exits `0` when the document is CONFORMANT (and, with `--compare`,
stability PASSes) and `1` otherwise, so it drops into CI directly.

A document is **CONFORMANT** iff no criterion FAILs. A `NOT-APPLICABLE`
criterion is surfaced but **does not gate** — it never makes a document
non-conformant.

## The six criteria

Each criterion returns **PASS**, **FAIL**, or **NOT-APPLICABLE** with a
human-readable reason. We implement them *honestly* against what is mechanically
checkable, and label the boundary between what is enforced and what is advisory.

### 1. Schema-valid Akoma Ntoso in a recognized profile

Mechanically enforced (structural):

- the document is well-formed XML;
- the root is `{akn}akomaNtoso` with the AKN namespace declared;
- exactly one recognized AKN document element (`act` / `bill` / `doc` / `judgment`
/ …) sits directly under the root (the "recognized profile");
- that element carries an `{akn}meta` block;
- the OLF `<proprietary source="#openlawsfoundation">` agent IDREF **resolves** to
a declared `<TLCOrganization eId="openlawsfoundation">` (a dangling IDREF is not
schema-valid AKN);
- the OLF namespace is declared where OLF metadata is used.

**Honest limitation — no official AKN XSD is bundled.** The OASIS Akoma Ntoso 3.0
XSD is not redistributed in this repository, so by default criterion 1 performs
the thorough *structural* validation above and labels itself **"structural, not
XSD"** in its reason text. If you make an XSD available offline — via the
`AKN4OLF_XSD` environment variable or a `conformance/schema/akn.xsd` file, with
`lxml` installed — criterion 1 additionally validates against it and marks the
result `[XSD]`. An available XSD that the document fails is a hard FAIL.

### 2. Required AKN4OLF lifecycle events present and well-formed

Mechanically enforced:

- an `<olf:lifecycle>` element exists;
- `@inForceFrom` is present and a valid ISO `YYYY-MM-DD` date;
- any `@inForceTo` present is a valid ISO date;
- every `<olf:event>` has a `@type` from the fixed vocabulary
**{enacted, published, commenced, amended, repealed}** and a valid ISO `@date`;
- a `commenced` event, when present, agrees with `@inForceFrom`.

**Which events are mandatory — stated interpretation.** The spec README says
AKN4OLF "fixes the required event vocabulary and which ones are mandatory" but
does not enumerate the mandatory set in prose. We adopt the minimal, defensible
interpretation: the validity interval itself is mandatory, so a document **must**
carry `inForceFrom` and a matching **`commenced`** event (entry into force is the
one lifecycle fact that, per the core principle, "exists everywhere"). Every
other event present is validated for well-formedness but is not required. This
interpretation lives in `MANDATORY_LIFECYCLE_EVENTS` in the code and is named in
the criterion's reason text, so it is auditable and easy to tighten later.

### 3. Every `<ref>` resolves to an OLF id or is explicitly `unresolved`

Mechanically enforced: every `{akn}ref` carries **either** a well-formed
`{olf}target` OLF id (`olf:<jur>/<type>/<year>/<number>[/<eId>]`) **or**
`{olf}resolution="unresolved"`. The reason reports the counts (resolved /
unresolved / non-conformant). A document with zero refs trivially passes.

### 4. Temporal validity is monotonic (no overlapping/contradictory intervals)

Two layers, and the distinction matters:

- the **act-level envelope** — the single `<olf:lifecycle>` interval — is checked
for internal consistency (`inForceFrom <= inForceTo`). It is the validity of
the act *as a whole* and is *expected* to span the per-version slices, so it is
**not** cross-checked for "overlap" against them (the envelope containing its
slices is correct, not a contradiction);
- the **per-version slices** — native `<temporalGroup>` intervals — are the
temporal versions of the act. They **must** be pairwise non-overlapping and
non-inverted (an overlap means two contradictory in-force texts for the same
instant), and each must lie within the act-level envelope.

In the common monovigente case there is one envelope interval and no slices, so
the check is trivially satisfied; the substance appears with multiple
temporalGroups. Open-ended intervals (no end) are treated as extending to
+infinity. If no dated interval is found at all, the criterion is
NOT-APPLICABLE.

### 5. As-enacted text reconstructible from a multi-version document

Only meaningful for a genuinely **multi-version** document — one carrying
multiple temporal versions of the same act, from which the original (as-enacted /
`ORIGINAL`) text can be reconstructed. Multi-version is detected when any of:

- the root act is declared `name="multivigente"`;
- native multi-version machinery is present (`<temporalData>` with more than one
`<temporalGroup>`, or multiple `<version>` markers);
- the main act carries more than one distinct `ita@<date>` expression date
(annexes excluded).

For a multi-version document we then require an identifiable as-enacted version
(an `ORIGINAL` marker, or the earliest expression date being addressable);
absence is a FAIL.

**Honest limitation.** For a single-version document (`monovigente` — a single
in-force snapshot, the common Normattiva export) there is no enacted-vs-current
history inside one file. The criterion returns **NOT-APPLICABLE** with a clear
note rather than a false PASS. This is expected and correct for monovigente
inputs.

### 6. OLF identifiers stable across regenerations (same source ⇒ same ids)

Inherently a **two-input** check: run it on two regenerations produced from the
same source. It asserts the two documents carry the identical

- act identity OLF id (`<olf:identity>/@olfId`),
- set of `{olf}target` reference OLF ids,
- set of structural `eId` element identifiers.

Volatile metadata (e.g. the provenance generation timestamp) is deliberately
ignored — only **identifiers** are compared, because that is what stability
means. Run via `--compare FILE2` on the CLI or `stability(doc_a, doc_b)` in code.
Run against a single document there is nothing to compare, so it is not part of
the single-document `check()`.

## Samples

- **`samples/conformant_monovigente.akn.xml`** — a **real** Normattiva export
(decreto-legge 19/2024) with the AKN4OLF metadata layer applied by the
reference `it` adapter. It is single-version, so it is **CONFORMANT** with
criterion 5 NOT-APPLICABLE — the honest outcome for a monovigente document.
- **`samples/conformant_multivigente.akn.xml`** — a small synthetic multi-version
document (two expression dates, two non-overlapping temporalGroups, an
`ORIGINAL` marker) that exercises criteria 4 and 5 as **PASS**.
- **`samples/nonconformant.akn.xml`** — structurally valid AKN but engineered to
**fail criterion 3** (a `<ref>` carrying neither `olf:target` nor
`olf:resolution="unresolved"`); the suite reports **NON-CONFORMANT** naming
criterion 3.

`python -m conformance.selftest` runs the suite over all three plus the
criterion-6 stability check (identical copy ⇒ PASS, id-mutated copy ⇒ FAIL) and
asserts the expected verdicts.

## Summary: enforced vs advisory

| # | Criterion | Mechanically enforced | Notable limitation |
|---|-----------|-----------------------|--------------------|
| 1 | Schema-valid AKN, recognized profile | structural (root, profile, meta, OLF agent IDREF resolves) | no official XSD bundled → structural unless `AKN4OLF_XSD` provided |
| 2 | Required lifecycle events well-formed | vocabulary, ISO dates, `inForceFrom` + `commenced` mandatory | mandatory set is our stated minimal interpretation |
| 3 | Every `<ref>` resolved-or-unresolved | full, with counts | — |
| 4 | Temporal validity monotonic | envelope consistency + non-overlapping version slices | NA if no dated interval |
| 5 | As-enacted text reconstructible | multi-version detection + as-enacted identifiable | NA for single-version (monovigente) |
| 6 | OLF ids stable across regenerations | identity + ref targets + eIds | needs **two** regenerations (`--compare`) |
41 changes: 41 additions & 0 deletions conformance/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""AKN4OLF conformance suite.

The suite (this package) defines what it means for an adapter's output to be
*correct*, independent of who wrote the adapter. An adapter is "OLF-conformant"
iff it passes the suite — that is the only gate.

Public API::

from conformance import check, stability, ConformanceReport
report = check("document.akn.xml") # criteria 1–5 on one document
result = stability(doc_a, doc_b) # criterion 6 across two regenerations

See ``conformance/README.md`` for the per-criterion definitions, what is
mechanically enforced versus advisory, and how to run the CLI.

Names are resolved lazily (via ``__getattr__``) so that running the module
directly with ``python -m conformance.akn4olf_conformance`` does not trigger an
eager re-import of the submodule through this package.
"""

from __future__ import annotations

__all__ = [
"AKN_NS",
"OLF_NS",
"ConformanceReport",
"CriterionResult",
"LIFECYCLE_EVENT_VOCAB",
"MANDATORY_LIFECYCLE_EVENTS",
"StabilityResult",
"Status",
"check",
"stability",
]


def __getattr__(name: str):
if name in __all__:
from . import akn4olf_conformance as _mod
return getattr(_mod, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Loading