OPDA Knowledge base
Physical — deployment

Round-trip MVP CI gate

Round-trip MVP CI gate

The round-trip gate is the MVP gate for the OPDA ontology-implementation programme per ODR-0010 §Q7 and ODR-0003 §“Programme retirement criterion” condition (i). It guarantees that BASPI5 instance data validates against the BASPI5 overlay profile + foundation + module shape graphs without false positives or false negatives across the 15 diagnostic exemplars. Three layers run in two GitHub Actions workflows:

  • ci-profile-contract — three-rule interface contract enforcement (defined in .github/workflows/ontology-byte-identity.yml).
  • tests/baspi5_round_trip/ — Python-driven round-trip equivalence + traceability + exemplar regression (defined in .github/workflows/baspi5-round-trip.yml).
  • Expected-report byte-identity — regenerated <stem>-expected-report.ttl matches committed (defined in .github/workflows/baspi5-round-trip.yml).

What the gate enforces

Per ADR-0013 + ADR-0014:

  1. Three-rule interface contract (ci-profile-contract):

    • sh:in semantics — overlay enum subsets are subset-of-foundation.
    • sh:Violation floor — overlay severity ≥ foundation severity.
    • No-identity-override — overlays MUST NOT redefine foundation identity-key shapes (Cat 3 NoIdentityOverride meta-shape catches this).
  2. Round-trip equivalence (test_round_trip.py): JSON ⇄ Turtle ⇄ JSON-LD ⇄ Turtle round-trips on the synthetic BASPI5 sample preserve every dct:source URI, every opda: predicate, every value (no lossy conversion).

  3. dct:source traceability (test_traceability.py): Every emitted shape resolves its dct:source to a form-question IRI under https://www.basp.uk/forms/baspi5#…; every IRI matches a baspi5Ref value in source/03-standards/schemas/src/schemas/v3/overlays/baspi5.json (per ADR-0014 G19 closure).

  4. Exemplar regression (test_exemplar_regression.py): Per-exemplar pyshacl validation report matches <stem>-expected-report.ttl exactly across all 15 exemplars (registered-freehold-house, chain-of-transactions, etc.).

  5. Expected-report byte-identity: opda-gen emit-exemplar-reports regenerates all 15 expected-report TTLs byte-for-byte; CI fails if any expected-report drifts (catches silent pyshacl-version drift or shape-semantics change).

Commands

# Interface contract (3 rules)
opda-gen ci-profile-contract --ontology-dir source/03-standards/ontology
# Expected: "profile contract CI: PASS (all 3 rules)"

# Per-exemplar validation (single exemplar)
opda-gen validate-exemplar \
  source/03-standards/ontology/exemplars/registered-freehold-house.ttl
# Exit 0 if report matches expected; exit 1 otherwise.

# Expected-report regeneration + byte-identity diff
opda-gen emit-exemplar-reports
git diff --exit-code source/03-standards/ontology/exemplars/*-expected-report.ttl

# Full three-layer harness
cd /Users/henrik/source/opda
/Users/henrik/source/opda/tools/opda-gen/.venv/bin/python -m pytest tests/baspi5_round_trip -v
# Expected tail: "27 passed"

Workflow definition — Layer 1 (round-trip, traceability, regression)

.github/workflows/baspi5-round-trip.yml job round-trip:

round-trip:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        submodules: recursive

    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'

    - name: Install opda-gen + harness deps
      working-directory: tools/opda-gen
      run: pip install -e .[dev]

    - name: Round-trip equivalence (Confirmation #3, #6)
      run: pytest tests/baspi5_round_trip/test_round_trip.py -v

    - name: dct:source traceability (Confirmation #5)
      run: pytest tests/baspi5_round_trip/test_traceability.py -v

    - name: Exemplar regression — combined (Confirmation #4)
      run: pytest tests/baspi5_round_trip/test_exemplar_regression.py -v

Workflow definition — Layer 2 (per-exemplar matrix)

The matrix runs each of the 15 exemplars in an isolated job so a single-exemplar failure surfaces as one row in the CI dashboard. fail-fast: false keeps all rows running independently:

exemplar-matrix:
  runs-on: ubuntu-latest
  strategy:
    fail-fast: false
    matrix:
      exemplar:
        - registered-freehold-house
        - unregistered-pre-first-registration-house
        - flat-with-split-uprn
        - flat-no-uprn-newly-converted
        - rural-plot-inspire-no-uprn
        - listed-building-divergent-addresses
        - person-with-name-change
        - organisation-with-merger
        - proprietorship-relator-multi-proprietor
        - simple-transaction-with-milestones
        - lease-extension-transaction
        - chain-of-transactions
        - claim-with-document-evidence
        - claim-with-electronic-record-evidence
        - claim-with-vouch-evidence
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'
    - name: Install opda-gen
      working-directory: tools/opda-gen
      run: pip install -e .[dev]
    - name: Validate exemplar via opda-gen validate-exemplar
      run: |
        opda-gen validate-exemplar \
          source/03-standards/ontology/exemplars/${{ matrix.exemplar }}.ttl

Workflow definition — Layer 3 (expected-report byte-identity)

expected-report-byte-identity:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'
    - name: Install opda-gen
      working-directory: tools/opda-gen
      run: pip install -e .[dev]
    - name: Regenerate expected reports
      run: opda-gen emit-exemplar-reports --ontology-dir /tmp/exemplar-roundtrip
    - name: Copy ontology fixtures
      run: |
        mkdir -p /tmp/exemplar-roundtrip/exemplars
        cp -r source/03-standards/ontology/*.ttl /tmp/exemplar-roundtrip/
        cp -r source/03-standards/ontology/exemplars/*.ttl /tmp/exemplar-roundtrip/exemplars/
        opda-gen emit-exemplar-reports --ontology-dir /tmp/exemplar-roundtrip
    - name: Compare regenerated vs committed expected-report.ttl
      run: |
        for f in source/03-standards/ontology/exemplars/*-expected-report.ttl; do
          stem=$(basename "$f")
          diff -q "$f" "/tmp/exemplar-roundtrip/exemplars/$stem"
        done

Expected output

When all three layers pass, the BASPI5 round-trip workflow shows:

Round-trip equivalence (Confirmation #3, #6)
  6 passed in 0.10s
dct:source traceability (Confirmation #5)
  3 passed in 0.05s
Exemplar regression — combined (Confirmation #4)
  18 passed in 0.55s
exemplar-matrix: 15 jobs passed
expected-report-byte-identity: passed

The local pytest tests/baspi5_round_trip -v tail reads:

======================= 27 passed, 719 warnings in 0.70s =======================

The warnings are rdflib’s pyparsing-deprecation noise; they do not gate the build.

Failure modes

Interface-contract violation:

PROFILE-CONTRACT VIOLATION: Baspi5_AddressShape carries identity-override on opda:Address (rule 3: no-identity-override)

A new overlay shape attempted to redefine a foundation identity key. Either move the constraint into a non-identity shape, or escalate to ODR amendment if the foundation key itself needs revising.

Round-trip failure:

test_round_trip.py::test_jsonld_roundtrip FAILED
  AssertionError: predicate opda:hasSpecialCategoryData lost during JSON→TTL→JSON-LD cycle

A predicate’s JSON-LD context entry drifted. Check content-negotiation/jsonld-context.md; the canonical context must carry a type-coercion or @id mapping for every predicate that appears in BASPI5 instance data.

Traceability failure:

test_traceability.py::test_all_dct_sources_resolve FAILED
  AssertionError: dct:source <https://www.basp.uk/forms/baspi5#A1.8.4.2> has no matching baspi5Ref

A shape’s dct:source form-question IRI doesn’t exist in the BASPI5 JSON schema. Realign to an actual baspi5Ref value (per the ADR-0014 G19 closure pattern), or update the JSON schema if the question genuinely exists in form version 5.0.3 but is missing from the schema.

Exemplar regression failure:

test_exemplar_regression.py::test_exemplar_validation_matches_expected_report[chain-of-transactions] FAILED
  AssertionError: actual report has 2 results; expected 1

Either the shape semantics changed (intentional → regenerate <stem>-expected-report.ttl via opda-gen emit-exemplar-reports) or pyshacl regressed (unintentional → pin pyshacl version, file upstream issue).

Expected-report byte-identity failure:

Files source/03-standards/ontology/exemplars/registered-freehold-house-expected-report.ttl and /tmp/exemplar-roundtrip/exemplars/registered-freehold-house-expected-report.ttl differ

The expected-report TTL serialiser became non-deterministic, or a fresh pyshacl run produced different output. Investigate via diff -u between committed and regenerated. If pyshacl drift, pin the version. If serialiser drift, check tools/opda-gen/src/opda_gen/emitters/exemplar_reports.py.

Source ADR + ODR

Comments

Loading comments…