Overlay-Profile Emitter Generalisation and Full Rollout
Amendment — Council Session 022 (2026-05-30; 6–0; council-ratified — greenfield, no WG). A convention-review council confirmed this
ProfileSpec/_build_profilerefactor is idiomatic — it is exactly a DCTAP→SHACL step (the data-dictionary per-overlay table IS a DC Tabular Application Profile). Two revisions: (a) drop therequiresProfileSpec field +opda:requiresemission — redundant (a SHACL processor enumerates required terms fromsh:path/sh:minCount); (b) per governance directive (2026-05-30): no profile-object / PROF layer — the SHACL overlay IS the form (ODR-0010, unchanged). Dropopda:overlaysContextand theCONTEXT_OF→context-concept plan (theprofiles.py:250bug is moot); the form↔base link is the shapes’sh:targetClass, and the form↔community link is one standarddct:subject/dct:publishertriple on the form graph. Noprof:Profile, noprof:isProfileOf, no spike;opda:ValidationContextstays as ODR-0010 defines it. The SHACL shapes, the full-profile rollout, and the byte-identity discipline are unchanged. See session-022 §Governance directive.
Implementation status — 2026-05-30: PARTIALLY EXECUTED (sound slice landed; full-fidelity rollout coupled to ADR-0028). Delivered and green (full
opda-gensuite + byte-identity + three-graph + profile-contract CI):
- S022 correctness on
baspi5.ttl— droppedopda:requires+ the mis-targetedopda:overlaysContext(work-item 2); added the onedct:subject → opda:EstateAgencyContextcommunity triple;OVERLAY_COMMUNITYmap for all 31 in-scope overlays. baspi5’sopda:ValidationContextretained per ODR-0010 (keptprofileURI/sourcedFrom/formVersion).- Generic builder (work-item 1, partial) —
ProfileSpec+_build_profile(spec)added: emits the sharedowl:Ontologyheader + thedct:subjectcommunity tag, delegating SHACL shapes to an optionalshape_builder. The header/community scaffolding is now generalized and tested once.- All 31 profiles emitted (work-item 3) —
PROFILE_FILENAMESextended to the 31 in-scope overlays (15 active main + 16 NTS2 extensions); the 3 legacy editions (baspi4/nts/ntsl) asserted absent. Coverage + community-tag tests added and green.Two honest gaps (status — both resolved/ruled by the Session 034 amendment below: gap (2) is DONE — baspi5 routed through
_build_profile, byte-identical, commit8753784; gap (1) is no longer ADR-0028-coupled — the descriptive TBox is built (254 emittedopda:predicates; Category-G 239/239; the monetary walk executed), so S034 rules it complete by eager-on-bindable enumeration). As originally written, both were coupled to ADR-0028 (deferred):
- The 30 non-BASPI5 profiles are THIN (header +
dct:subjectcommunity only). Their leaves have no term-grainopda:property paths to constrain viash:pathuntil ADR-0028’s descriptive-layer walk lands — and that walk was deferred to a curated WG pass (see ADR-0028 implementation note). Each thin profile’sdct:description+ comment header states this inline.- baspi5 retains its bespoke
_build_baspi5_profile(not yet routed through_build_profile). Data-fying its ~30 shapes into ashape_builderis an output-neutral refactor (baspi5.ttl stays byte-identical) whose payoff — uniform spec-driven shapes — only matters once the other forms have terms to constrain. Deferred with gap (1); the byte-parity regression gate is satisfied trivially (baspi5’s builder is untouched).Net: the form→community map (ODR-0020 Rule 6 half) is now machine-readable for all 31 forms; the per-leaf constraint half awaits ADR-0028. Status stays
proposed.
gap-1 EXECUTED 2026-06-01 (Council Session 034 — eager-on-bindable enumeration). Both gaps are now closed: gap-2 (baspi5 →
_build_profile, byte-identical) landed at8753784; gap-1 is executed by the S034 bind-only-what-exists resolver (tools/opda-gen/src/opda_gen/inputs/leaf_resolver.py). 28 forms enumerated (12 ref-carrying mains + 16 NTS2 extensions); 224 bindable leaves enumerated, 1095 GAPped (by reason: no-predicate / no-domain / multi-domain / ref-collision / collider-ambiguous), each form carrying an emitted per-form gap register (dct:descriptionon the form header — S034 Q4, literal). oc1/llc1 stay THIN (ODR-0008d authority-retrieved register extracts; held per S034 Q2). baspi5.ttl/oc1.ttl/llc1.ttl are byte-identical (untouched). The seven S034 as-built fixes landed: (1)OPDAinline; (2)ntsReffor the 16 extensions; (3) G3 forms-authority generalised to the overlay$idauthority (https://trust.propdata.org.uk/.../overlays/); (4) intrinsic path-aware collider guard (details/price…); (5) three-branch resolver tests (single-domain → bind, zero/multi-domain → GAP) green before wiring; (6) the enumerator wired via a factory-boundshape_builder; (7) a GAPped leaf emits NOsh:path+ NOdct:sourceso the hard G3 gate stays green (252→746 addressable form leaves, 0 unaddressable, 0 doubly-bound). Each property shape’sdct:sourceis the JSON-pointer schema-leaf-path anchor (<$id>#/path/to/field, ODR-0022 §Rules.2 G2 S034-amended). Enumeration does NOT discharge G3’s worked-query limb (per-consumer; Davis withdrawal condition). All six CI gates + the fullopda-genpytest suite green.
Amendment — Council Session 023 (2026-05-30; re-scoped by ODR-0022; council-ratified — greenfield, no WG). ODR-0022 makes the thin profiles’ constraint half the carrier of round-trip coverage, so it constrains how the profiles are completed once the (re-scoped) ADR-0028 walk lands. The per-form overlay profiles — currently emitted thin (header +
dct:subjectcommunity only, gap (1) above) — MUST enumerate each form’s leaves in theirsh:path/sh:minCount/dct:sourceshapes. This per-leaf enumeration is what discharges ODR-0022 gate G3 (coverage-by-test): a BASPI5 round-trip passes on the collapsed TBox only because the profile, not the base TBox, carries the descriptivesh:minCountcardinality (the base TBox carries zero descriptivesh:minCount, per ODR-0008 §Q7a). Each shape’sdct:sourceMUST point at the schema-leaf-path (ODR-0022 G2), not the deciding ODR. Additionally, the emitter targets broaden alongside the re-scoped ADR-0028 walk: the Category C status-enum SKOS schemes (~54) and the Category DFixtureItemScheme(ODR-0011) and the Category ESearchResult/RiskAssessmentclass become emitter targets, so per-formsh:inrestrictions over the C/D schemes and the E class hang off these profiles. Cite ODR-0022 §Rules.2 (G2/G3) + §Consequences.
Context and Problem Statement
ODR-0010 models each PDTF form as a SHACL overlay profile — an opda:ValidationContext carrying opda:overlaysContext + per-leaf opda:requires, with per-form sh:minCount/sh:in constraint variation. ODR-0020’s term→context derivation reads overlaysContext + requires off these profiles. Verified state exposes two blockers:
- Only one profile is emitted, of 34.
source/03-standards/ontology/profiles/holds exactlybaspi5.ttl. PDTF v3 ships 34 overlay files (per/modelling/overlays): 18 main inv3/overlays/*.json—baspi5,baspi4†,nts2,nts†,ntsl2,ntsl†,piq,ta6,ta7,ta10,lpe1,fme1,con29R,con29DW,llc1,oc1,rds,sr24(† = legacy: superseded but still shipped for backward compat) — plus 16 extension overlays inv3/overlays/extensions/*.json(the NTS2 fragmentsas dr er fd hi hs jk la ma mc oa oc sb sf sl tf, each adding one NTS2 topic to annts2023 base for staged migration; adopting all 16 ≡nts2). So 1 of 34 is captured in SHACL. Of the 34, 31 are in scope (15 active main + 16 extensions); the 3 legacy editions (baspi4,nts,ntsl) are out of scope — OPDA validates current-edition data only. (combined.json/skeleton.jsonat the v3 root are pre-merged/starter artefacts, not overlays.) - No generic composer. Council Session 021 (Cagle & Knublauch, verified) found
profiles.py’s_build_baspi5_profile()is ~420 lines of hand-coded, BASPI5-specific shape construction, andemit_profileraisesNotImplementedErrorfor any other overlay. “The other ~17 profiles” is not a config change — each is currently a bespoke 400-line builder. Authoring 17 more by hand is the anti-pattern.
Plus the profiles.py:250 mis-target (ADR-0026 work-item 3): opda:overlaysContext points at a profile-LAYER IRI, not a context concept. The directing governance ruled one-go, full coverage, no staging — so this ADR delivers the refactor and all profiles in the same delivery, not a demand-pulled trickle.
Decision Drivers
- Constraint-reuse over hand-authoring — compose profiles from a declarative spec + shared shape library (TopBraid/TopQuadrant discipline, Knublauch), not N bespoke builders.
- Behaviour-preserving refactor — the generalisation MUST emit
baspi5.ttlbyte-for-byte identical; that is the regression gate proving it is safe. - ODR-0010 canonical mapping —
required[]→sh:minCount; enum-subset→mergedsh:in(build-step replacement, not stacking);oneOf→sh:xone;baspi5Ref/leaf-ref→dct:source; UI→DASH; per ODR-0010 Rules 1–5. - Context wiring — every profile’s
overlaysContext→ its industry…Contextconcept viaCONTEXT_OF(ODR-0020 Rule 6). - One delivery, full coverage — all profiles this delivery; the byte-identity baseline re-pinned once.
Considered Options
- Option A — generic
_build_profile(spec: ProfileSpec)+ author all specs (CHOSEN). Generalise the BASPI5 builder so per-form variation is input data; populate oneProfileSpecper overlay by walking its overlay JSON; wireCONTEXT_OF. - Option B — hand-code each profile builder (extend the status quo). Rejected: ~17 × 400-line bespoke builders is unmaintainable and the smell S021 named; constraint logic is identical across forms — it belongs in data, not code.
- Option C — emit profiles incrementally / demand-pulled (Davis’s watching-brief). Rejected by the S021 governance directive (one-go, full coverage).
Decision Outcome
Chosen option: Option A. Work items (all in the single delivery, alongside ADR-0026 + ADR-0028):
-
Extract
_build_profile(spec: ProfileSpec) -> Graph. Generalise_build_baspi5_profile()so per-form variation is aProfileSpec(dataclass parsed from the overlay JSON), with fields mapping to ODR-0010 Rules:ProfileSpecfieldODR-0010 Rule SHACL output required: [path,…]Rule 1 sh:property [ sh:path … ; sh:minCount 1 ]enum_subset: {path: [members]}Rule 2 single merged sh:in (…)(build-step replacement; never twosh:inon one path)oneOf: {discriminator, branches}Rule 3 sh:xone (…)withsh:qualifiedValueShapeleaf_refs: {path: anchor}Rule 4 dct:source <…/forms/<form>#<anchor>>ui: {path: {viewer,editor,order,group}}Rule 5 dash:viewer/dash:editor/sh:order/sh:groupcommunityODR-0020 / governance directive one dct:subject→ its context concept, on the form graph’sowl:Ontologyheader (noopda:requires/opda:overlaysContext— shapes enumerate required terms;sh:targetClassgives the base) -
Overlay→community map → one
dct:subjecttriple per form (governance directive — notopda:overlaysContext/PROF).OVERLAY_COMMUNITY = { baspi5/nts2/ntsl2 + all 16 extensions → EstateAgency; ta6/ta7/ta10/lpe1 → Conveyancing; fme1 → MortgageLending; piq → Surveying; rds/oc1/llc1/con29R/con29DW/sr24 → PropertyDataServices }(legacybaspi4/nts/ntslskipped — out of scope; Property Tech owns no overlay — base only). Emit<formGraph> dct:subject <communityConcept>on each form’sowl:Ontologyheader. Theprofiles.py:250defect is moot (the predicate is gone). -
Author profile specs for the full inventory (34 overlays) — no profile left unwritten. One
ProfileSpecper overlay JSON, emitting that overlay’s SHACL shapes (sh:minCount/sh:in/sh:xone+dash:+ per-leafdct:source) + itsdct:subjectcommunity triple — no wrapper node. Scope, in two tranches:- 15 active main (
v3/overlays/*.json) in priority order —ta6 → piq → fme1 → rds → lpe1 → ta7 → ta10 → oc1 → llc1 → con29R → con29DW → sr24 → nts2 → ntsl2(baspi5 already done). The 3 legacy editions (baspi4,nts,ntsl) are OUT OF SCOPE — skipped (no SHACL profile; OPDA validates current-edition data only). Re-open only if backward-compat validation of a superseded edition is ever required. - 16 extension (
v3/overlays/extensions/*.json): each NTS2 fragment (as dr er fd hi hs jk la ma mc oa oc sb sf sl tf) is a first-class overlay → its ownProfileSpec/profile, all in Estate Agency (supports staged NTS 2023→NTS2 migration — a real use case per/modelling/overlays).
- 15 active main (
-
Contract + determinism gates. The three-rule interface contract (ODR-0010 §Q8 / ODR-0013) green on every profile; the enum-union test (ODR-0008 §Q7a: union of per-profile
sh:inmembers == the SKOS scheme’s concept set); byte-identity re-pinned once for the delivery.
Consequences
- Good, because it completes the term→context usage map — every form contributes its
overlaysContext+requires, so the dormantopda:servesContextderivation becomes rich and correct across all six contexts. - Good, because per-form variation lives in declarative
ProfileSpecdata, not 400-line builders — adding or revising a form is a spec edit, and the constraint logic is tested once. - Good, because
piq+ta6co-requiringfloodRiskproduces the first real bucket-B spanning (twoservesContextedges);oc1/llc1/con29exercise bucket-C (consumesFromHMLR/LA), validating the placement method end-to-end. - Bad, because this is the long pole of the delivery — the refactor + 30 specs (14 active main [15 − baspi5, already done] + 16 extensions; the 3 legacy editions are out of scope); mitigated by the spec being mechanical (the overlay JSON carries the columns) and by the byte-identity regression gate on
baspi5. The 16 extensions are small (one topic each). Target profile set: 31 (15 active main + 16 extensions). - Neutral, because
opda:servesContextCONSTRUCT activation vs the active validation set stays governed by ODR-0019 Rule 8 — emitting and running the derivation is in scope; lifting dormancy is a one-line ODR-0019 amendment if the WG chooses.
Confirmation
- Refactor regression gate:
baspi5.ttlemits byte-for-byte identical after the_build_profile(spec)refactor (proves behaviour-preservation); existingtest_profiles.py+profile_contract_test.pygreen. - Community-tag test: every emitted form graph (all 34, modulo the legacy scope decision) carries exactly one
dct:subject→ askos:Conceptinopda:BoundedContextScheme— noopda:overlaysContext/wrapper (governance directive). - Coverage test: a profile exists for every active overlay in
v3/overlays/*.json(15) and every fragment inv3/overlays/extensions/*.json(16) = 31 — reconciled against the/modelling/overlayscatalogue; the 3 legacy editions (baspi4/nts/ntsl) are explicitly excluded (asserted absent, so they can’t be silently re-added or silently forgotten). - Three-rule contract (ODR-0010 §Q8 / ODR-0013): green on every profile.
- Enum-union test (ODR-0008 §Q7a): for each spanning leaf, union of per-profile
sh:inmembers == the SKOS scheme’sskos:Conceptset. - Byte-identity CI (ADR-0007 §6a): second regeneration identical across the full profile set; baseline re-pinned once.
- Derivation richness: with all profiles emitted, the (dormant)
servesContextCONSTRUCT yields edges for all six contexts; the cross-check shape (ADR-0026 amendment) fires onlysh:Warnings, neversh:Violation.
More Information
- Realises: ODR-0010 (overlay-profile mechanism — full rollout) + ODR-0020 Rule 6 (the
overlaysContext→context wiring andCONTEXT_OFmap). - Council provenance: ODR session-021 (Cagle & Knublauch’s
profiles.pyfinding +ProfileSpecrecommendation; build order ta6 → piq → fme1 → rds → lpe1/ta7/ta10 → oc1/llc1/con29; governance one-go directive). - Generator framework: ADR-0007 (determinism); ADR-0013 / ODR-0013 (severity + the three-rule contract); ADR-0026 (
CONTEXT_OF+profiles.py:250fix, shared). - Co-delivered with: ADR-0028 (supplies the term-grain
opda:properties the profilesrequire). - Files touched:
tools/opda-gen/src/opda_gen/emitters/profiles.py(theProfileSpec/_build_profilerefactor,OVERLAY_COMMUNITYmap);PROFILE_FILENAMESextended to the 31 in-scope overlays (15 active mainv3/overlays/*.json+ 16 extensionv3/overlays/extensions/*.json; the 3 legacy editions excluded);source/03-standards/ontology/profiles/*.ttl(all profiles, regenerated — incl. anextensions/or flat layout for the 16 fragments);tests/test_profiles.py+tests/baspi5_round_trip/(regression + per-form contract + the coverage test). - Inventory source of truth:
/modelling/overlays(src/pages/modelling/overlays.astro) — 34 overlay files (18 main + 16 extension), active/legacy status, and the overlay→community map. The plan reconciles against it.
Comments
Loading comments…
Sign in to post a comment