Skip to content

ID-132 SPEC — G-IRI-PROJECTION: JSON-LD @context / IRI projection over the OKF ontology layer ({132.43}/{132.44}, bl-457)

ID-132 SPEC — G-IRI-PROJECTION JSON-LD @context / IRI projection

Section titled “ID-132 SPEC — G-IRI-PROJECTION JSON-LD @context / IRI projection”

Kind: TECH-lite implementation slice. Numbered, testable IRI-N invariants + one-to-one Proposed-changes mapping + a 2-Subtask decomposition ({132.43}, {132.44}) the Orchestrator can dispatch. It specifies the additive JSON-LD @context / IRI projection layer over the already-landed DR-027 base-ontology seam and the {132.34} client-overlay compose path; it does not rewrite the authoring format (markdown + plain YAML frontmatter stays). Authored: 16/07/2026 · Task Planner (fresh dispatch, bl-457 owner-ratified design). Owner-ratified frame (bl-457, 2026-07-15 — BINDING, do not re-litigate): “DO the JSON-LD @context/IRI projection — additive layer, NOT a rewrite. Authoring stays markdown + plain YAML frontmatter; the producer bundle-writer + frontmatter emitter gain a deterministic IRI-minting projection step. Ship the @context SELF-CONTAINED/inline in the bundle (portable, client-owned, no phone-home) over hosted. [[wikilink]] emission was considered and REJECTED. The base+overlay split extends to NAMESPACES: base terms → platform-owned stable namespace (…/ontology/base#term); client/overlay terms → a per-client namespace (…/ontology/client/#term), NEVER minted under base. Mint rule is deterministic + automatic (IRI = namespace(scope) + ’#’ + slug(term)). Promotion path overlay→base re-mints under base + keeps the old client IRI as a sameAs/equivalentClass alias. Base IRIs are versionLESS/stable; an incompatible meaning change = a NEW IRI + deprecate, never a mutated IRI. Published IRIs are citation-pin-class irreversible → the namespace authority + versioning governance MUST be decided before the first client-bundle mint.” Governing rulings to respect (cite, not re-litigate): DR-027 (effective ontology ships in the bundle; base source-of-truth in the platform repo; overlay client-owned), DR-054 (client-CV-overlay contract — reserved additive JSON, fail-loud composition), DR-060 (producer memo delta-determinism contract — versions live in bundle metadata / log.md, not in the identity), DR-079 (OKF bundle doctrine — cross-bundle identity = these base-namespace IRIs). All four verified in-force in reference/decision-register.md on 16/07/2026 (grep hits: DR-079:48, DR-060:298, DR-054:386, DR-027:736).

Empirical-verification note. This slice introduces no new external-library symbol. The mint + projection path uses Python stdlib only — json (already imported, bundle_writer.py:151 region), re + unicodedata (slug normalisation), os.environ (client-id resolution, same pattern as the existing OKF_BUNDLE_DIR read at flow_def.py:202). The one external-library call in the seam, localfs.declare_file, is unchanged and already carries an EXECUTOR-VERIFY empirical finding against the real cocoindex==1.0.7 engine (bundle_writer.py:34-62 docstring). All other cited symbols are internal Canonical Platform surfaces (import-and-call check N/A per shared-discipline §Empirical verification — stdlib/internal only). No ABSENT/SIGNATURE_DRIFT/BEHAVIOUR_DRIFT risk applies.

Code-intelligence orientation (verbatim; repo:'canonical'). Per the Planner code-intel binding rule; tool outputs quoted verbatim and cross-checked against direct file reads on 16/07/2026 (the OVERLAY-CV.md line numbers below have since drifted — this slice orients on the CURRENT lines from direct reads, per grounding Part 3).

  • gitnexus_query({query:'ontology compose effective ontology bundle writer base overlay snapshot', repo:'canonical'})"processes": [], "process_symbols": []. definitions surfaced the exact seams this slice extends, verbatim: Method:…/producer/validator.py:EffectiveOntology.compose#1 (startLine 249, endLine 271), Method:…:EffectiveOntology.base_only#0 (238–246), Class:…:EffectiveOntology (226–271), Function:…/producer/bundle_writer.py:write_ontology_artefact (761–790), Function:…:write_bundle (820–957), Function:…:read_client_overlay (727–758), Function:…/producer/flow_def.py:run_producer_flow (329–494). No @context, jsonld, or IRI/iri symbol exists anywhere in the corpus — confirming net-new capability over an existing write seam.
  • gitnexus_context({name:'write_ontology_artefact', repo:'canonical'})"incoming": {"calls": [{"name": "write_bundle"}]}, "outgoing": {"calls": [{"name": "_base_ontology_snapshot"}]}, "processes": []. The sole caller is write_bundle; the sole callee is _base_ontology_snapshot. The new context artefact wires in at exactly one call site (write_bundle), a sibling of this existing ontology-artefact call.
  • gitnexus_context({name:'write_bundle', repo:'canonical'})"incoming": {"calls": [{"name": "run_producer_flow"}]}; outgoing includes EffectiveOntology.compose, read_client_overlay, write_ontology_artefact, append_log_entry (+ index/theme writers). So write_bundle already composes the EffectiveOntology (bundle_writer.py:880) and already writes the ontology artefact (:955) — the composed effective ontology this slice must project is already in hand at exactly the point the new context.jsonld write belongs.
  • ccc search fallback: not required — gitnexus + direct read both returned the real surface. No greenfield disclaimer applies.

Direct reads (cross-check) — the CURRENT seams this slice extends:

  • bundle_writer.py:660-677 _base_ontology_snapshot() — DR-027’s pinned base snapshot. Returns {"concept_types": sorted(ALLOWED_CONCEPT_TYPES), "entity_types": sorted(ALLOWED_ENTITY_TYPES), "relationship_types": sorted(ALLOWED_RELATIONSHIP_TYPES)}. All three CV dimensions (overlay-extensible). Base terms are already URL-safe [a-z0-9_] slugs (case_study, topic, …).

    STALE 2026-08-10 (id-427 {427.13}): this seam no longer exists. _base_ontology_snapshot and ALLOWED_CONCEPT_TYPES are both DELETED — the former by {427.11} (DR-027’s S546 amendment; ontology.json has no base key), the latter by {427.5} (DR-141). Left in place because this block is captioned as a point-in-time cross-check of “the CURRENT seams this slice extends”, and it correctly records what was current when the slice was designed.

  • validator.py:226-272 EffectiveOntology (frozen dataclass) + .compose(overlay) — OV-7 sorted-union base ∪ overlay per dimension. write_bundle holds the composed effective_ontology (bundle_writer.py:880). This is the exact object to project.
  • bundle_writer.py:762-791 write_ontology_artefact(bundle_dir, *, client_overlay=None) — serialises {"base": _base_ontology_snapshot(), "overlay": …} to ontology.json with json.dumps(payload, indent=2, sort_keys=True) (deterministic). The new context writer mirrors this serialisation contract exactly.
  • bundle_writer.py:174-189 _RESERVED_BUNDLE_FILENAMES — additive frozenset (index.md, log.md, ontology.json, README.md, CONFORMANCE.md, ontology-overlay.json). _existing_concept_paths (:799-818) diffs bundle .md files against this set; .json/.jsonld are already glob-excluded (rglob("*.md")), so a new context.jsonld never surfaces as a false RunSummary.removed — but is listed here for intent/parity with the other bundle-level artefacts.
  • bundle_writer.py:955 write_bundle tail: write_ontology_artefact(bundle_dir, client_overlay=overlay) then append_log_entry(...). The new write_context_artefact(...) call belongs immediately after the ontology write, before the log entry.
  • flow_def.py:198-216 _resolve_bundle_dir reads OKF_BUNDLE_DIR from os.environ. The client-id resolution mirrors this pattern (OKF_CLIENT_ID). flow_def.py:404-410 run_producer_flow calls write_bundle(resolved_bundle_dir, drafts, reference_drafts, theme_config=…, timestamp=…) — the one production call site that must pass the resolved client-id.
  • External §9 validator (okf_validate.py, scaccogatto plugin okf@0.3.4) walks bundle.rglob("*.md") ONLY (okf_validate.py:162); JSON/JSON-LD artefacts are invisible to it. Its only hard errors are §9.1 (parseable .md frontmatter) + §9.2 (non-empty type). Adding context.jsonld is therefore trivially §9-conformant — the checker never inspects it. The internal producer BI-13 gate (validator.check_concept) gates .md concepts against the effective ontology, also untouched by a JSON artefact.

Problem. ontology.json today ships a plain-JSON vocabulary (base/overlay term lists) with no IRIs and no @context — there is no stable, portable, machine-resolvable identity for a concept type or ontology term. DR-079 makes cross-bundle identity depend on base-namespace IRIs that do not yet exist; {132.39} (concept-graph UI) needs a concept-type → IRI projection to render; and the base+overlay composition ({132.34}, DR-027/054) already produces the exact EffectiveOntology object to project, but nothing mints IRIs from it. This slice adds the deterministic mint + a self-contained context.jsonld artefact — additive, gating nothing.

Design decisions (Planner rulings on the delegated mechanics)

Section titled “Design decisions (Planner rulings on the delegated mechanics)”

The owner delegated the projection mechanics to the Planner (“Exact projection mechanics: where @context lands … what gets projected … validator posture”). Rulings:

  1. @context lands in a NEW reserved bundle file context.jsonld (not embedded in ontology.json). Rationale: (a) idiomatic JSON-LD (a @context document is conventionally its own file — Vault-LD context.jsonld precedent); (b) insulates ontology.json’s existing base/overlay shape and every current consumer + the ontology-artefact tests from any change; (c) the external §9 validator ignores non-.md files, so a sibling JSON-LD file is zero-risk. It is shipped INLINE in the bundle (self-contained, no phone-home) per the ratified frame — “inline in the bundle” is satisfied by a bundle-resident file, not only by an in-ontology.json key.

  2. What gets projected: ontology TERMS only (all three CV dimensions), NOT per-concept frontmatter. The @context maps every effective-ontology term (base ∪ overlay across concept_types / entity_types / relationship_types) to its IRI. (Amended 2026-08-10, id-427 {427.13}: two dimensions, not three, and client-overlay terms only rather than base ∪ overlay — see IRI-5’s supersession note below for both steps and their grounds. The decision this paragraph actually makes — terms, not per-concept frontmatter — is UNAFFECTED and still holds.) Per-concept frontmatter gains no iri: key this wave. Rationale: (a) a per-concept key rewrites EVERY concept .md file → churns the {132.35} BI-18 delta-determinism re-proof this slice gates before; (b) it collides head-on with the bl-456/bl-477 frontmatter wave (shared frontmatter.py); (c) it is redundant — the concept’s type frontmatter value IS the join key, so {132.39} resolves concept-type → IRI purely from context.jsonld’s term→IRI map (IRI-11). The ratified “frontmatter emitter gains the projection step” is honoured by making the mint a SHARED pure module (iri_projection.py) importable by frontmatter.py later, without forcing per-concept churn now (deferral recorded as OQ-2).

  3. Client-id source = a new OKF_CLIENT_ID env var (mirrors OKF_BUNDLE_DIR), threaded as an explicit client_id: str | None = None kwarg on write_bundle (mirrors the existing client_ontology_overlay kwarg idiom). The producer carries no client identity today (verified: no client_id/client_slug/config surface in producer/), so this is net-new. Overlay-term IRIs are minted ONLY when OKF_CLIENT_ID is explicitly set; absent, context.jsonld ships base-only and overlay terms are recorded as un-projected (advisory, run NOT aborted — IRI-6). Rationale: published IRIs are irreversible, so we never mint an overlay IRI under a GUESSED/derived client-id that a later real client-id would contradict permanently.

  4. Namespace authority is a SINGLE config constant (IRI_BASE_NAMESPACE), its concrete value = OQ-1 (owner ratification). Code lands with a loud # OQ-1 UNRATIFIED PLACEHOLDER marker; the FIRST real client-bundle mint is gated on OQ-1. Recommendation in OQ-1 below.

Invariants (IRI-N — testable; the Checker verifies acceptance against these)

Section titled “Invariants (IRI-N — testable; the Checker verifies acceptance against these)”
  • IRI-1 — Deterministic mint. For a term T and scope S, iri(T,S) = namespace(S) + "#" + slug(T). A pure function of (T,S): identical inputs → byte-identical IRI on every run, with no clock/UUID/ordering nondeterminism.

  • IRI-2 — Base/overlay namespace split. Base terms mint under {IRI_BASE_NAMESPACE}/base; overlay terms under {IRI_BASE_NAMESPACE}/client/{client_id}. An overlay term is never minted under the base namespace, and a base term is never minted under a client namespace.

  • IRI-3 — Versionless / stable base IRIs. A base IRI carries no version segment (the IRI identifies the concept; version lives in bundle metadata / log.md per DR-060). The mint is meaning-stable: the projection never mutates an existing base IRI (an incompatible meaning change is a governance act producing a NEW IRI + deprecate — out of this slice’s runtime scope).

  • IRI-4 — Self-contained @context. Every bundle ships context.jsonld carrying the full term→IRI mapping inline; a consumer resolves any term without a network fetch.

  • IRI-5 — All three dimensions projected. concept_types, entity_types, relationship_types are each fully projected from the composed EffectiveOntology (base ∪ overlay); no dimension is dropped.

    SUPERSEDED 2026-08-10 (recorded by id-427 {427.13}). Both halves of this invariant have fallen, in two separate steps, and neither is recoverable by narrowing it:

    1. The concept_types dimension is gone (id-427 {427.5}, DR-141). EffectiveOntology carries two fields, not three. The classifier that made the projection meaningful was ALLOWED_CONCEPT_TYPES — the register that said which terms were base. With it gone there is no base/overlay distinction for that dimension, and minting an IRI for an organically-minted label would assert exactly the registration DR-027-as-amended retired. Carried as TQ-2 in specs/id-427-producer-inversion/TECH.md §6, ruled S546: no live requirement; defer, documenting the door.
    2. The BASE half of the remaining two is gone (id-427 {427.14}, executing TQ-1’s S546 ruling as amended S548). project_context projects client-overlay terms only; a bundle composing no overlay emits {"@context": {}}. The base registers became an EXCLUSION filter rather than a mint list.

    IRI-6’s “base-only” fallback goes with it — there is no base-only emission left for a missing client-id to fall back to.

    Not settled, and deliberately not settled here: TQ-1a is still OPEN (carried verbatim in tasks/id-427.md {427.14} and in §6 of id-427’s TECH). {427.14} shipped reading (b) — artefact always written, carrying only overlay terms — as a REVERSIBLE DEFAULT, not as an answer, because no requirement with a current source discriminates it from reading (a), the artefact not being written at all on a no-overlay run.

  • IRI-6 — Client-id gate on overlay minting (non-gating fallback). Overlay-term IRIs are minted iff an explicit client-id is present. Absent → context.jsonld is emitted base-only, overlay terms recorded in an advisory diagnostic (logged; not a fatal error); the producer run is not aborted.

  • IRI-7 — Slug determinism + collision posture. slug(T) is deterministic and idempotent on the existing snake_case base terms (case_studycase_study). A within-(scope,dimension) slug collision between two distinct source terms is resolved deterministically (sorted-order first-wins) and recorded as an advisory diagnostic — never gates a run.

  • IRI-8 — Promotion alias affordance. The projection structure can carry a sameAs/alias for a term whose IRI changed via overlay→base promotion, so already- published client IRIs never dangle. The promotion MECHANISM is out-of-scope this wave (no term promoted yet); the emission shape must simply be able to express the alias.

  • IRI-9 — Additive, non-gating validator posture. context.jsonld is additive: the external §9 validator (walks .md only) is unaffected; the internal BI-13 gate is untouched; no projection outcome ever aborts a bundle run.

  • IRI-10 — Single namespace-authority config point. The base namespace is exactly one constant; changing the authority is a one-line change. Its value is OQ-1 (unratified) and the first real client-bundle mint is gated on OQ-1 ratification.

  • IRI-11 — Concept-type → IRI resolution surface ({132.39}). A consumer resolves a concept’s type frontmatter value to its IRI purely via context.jsonld’s term→IRI map — no per-concept frontmatter change is required.

  • IRI-12 — BI-18 delta-determinism preserved. With unchanged inputs (same effective ontology, same client-id), context.jsonld is byte-identical run-to-run (sort_keys=True serialisation) → no spurious churn in the {132.35} BI-18 re-proof.

Proposed changes per invariant (one-to-one map)

Section titled “Proposed changes per invariant (one-to-one map)”
InvariantProposed changeLanding Subtask
IRI-1, IRI-2, IRI-3, IRI-7, IRI-8New pure module producer/iri_projection.py: IRI_BASE_NAMESPACE constant, _base_namespace()/_client_namespace(client_id), slug(term), mint_iri(term, *, scope), project_context(effective_ontology, *, client_id) -> dict (builds the @context dict + advisory diagnostics), deterministic collision guard. No I/O.{132.43}
IRI-4, IRI-5, IRI-6, IRI-9, IRI-10, IRI-12bundle_writer.py: CONTEXT_FILENAME = "context.jsonld" added to _RESERVED_BUNDLE_FILENAMES; new write_context_artefact(bundle_dir, effective_ontology, *, client_id) serialising project_context(...) with json.dumps(..., indent=2, sort_keys=True); write_bundle gains client_id kwarg and calls write_context_artefact(...) immediately after write_ontology_artefact (:955).{132.44}
IRI-6, IRI-10flow_def.py: resolve OKF_CLIENT_ID from os.environ (mirroring _resolve_bundle_dir) and pass to the one production write_bundle(...) call (:404-410).{132.44}
IRI-11No code change — verified by an acceptance test that reads context.jsonld and resolves a known concept type to its base IRI.{132.44}

context.jsonld shape (self-describing; full absolute IRIs so no JSON-LD processor is required by {132.39}, with prefix definitions for humans/expansion):

{
"@context": {
"base": "{IRI_BASE_NAMESPACE}/base#",
"client": "{IRI_BASE_NAMESPACE}/client/{client_id}#",
"case_study": "{IRI_BASE_NAMESPACE}/base#case_study",
"topic": "{IRI_BASE_NAMESPACE}/base#topic",
"<overlay_term>": "{IRI_BASE_NAMESPACE}/client/{client_id}#<slug(overlay_term)>"
}
}
  • client prefix + client/... term entries are omitted when no client-id is set (IRI-6). Keys are emitted sort_keys=True for byte-determinism (IRI-12).
  • Slug rule (IRI-7): slug(T) = NFKD-normalise → lowercase → replace every run of characters not in [a-z0-9_-] with a single - → collapse repeated - → strip leading/trailing -. This preserves existing snake_case base terms verbatim (they contain only [a-z_]) and normalises arbitrary runtime overlay strings.
  • Collision guard (IRI-7): iterate terms in sorted order per (scope,dimension); if two distinct source terms slug to the same fragment, keep the first and append the loser to a _projection_diagnostics.collisions list (logged at WARNING); do not raise.
  • Alias affordance (IRI-8): reserve an optional expanded term form ({"@id": "...", "sameAs": "..."}) the projector can emit later; not populated this wave.

Namespace authority — OQ-1 (owner ratification required)

Section titled “Namespace authority — OQ-1 (owner ratification required)”

The IRI is an identifier, not a runtime-resolved URL (self-contained @context, no phone-home), so permanence — not live resolution — is the governing property. Options weighed:

  • w3id.org (RECOMMENDED) — e.g. https://w3id.org/canonical/ontology (→ …/base#, …/client/<id>#). W3C Permanent Identifier CG redirection service; the de-facto standard for stable semantic-web IRIs; permanence is decoupled from aisolutionhub’s own DNS tenure (directly addresses “citation-pin-class irreversible”). Cost: a one-time w3id GitHub PR to register /canonical/, and confirming that path is free before the first mint.
  • aisolutionhub-owned purl-style domain — e.g. https://ontology.canonical.<tld> — full control, simplest, but IRI permanence rests entirely on the org perpetually holding the domain (a lapse dangles every published IRI). Viable fallback if w3id /canonical/ is taken or the org prefers self-hosting.
  • Public canonical GitHub repo raw URL — REJECTED: not a persistent-identifier authority (repo rename/move breaks every IRI); wrong tool for irreversible identifiers.
  • purl.org — viable but functionally dominated by w3id.org for this use.

Recommendation: IRI_BASE_NAMESPACE = "https://w3id.org/canonical/ontology". Code lands with this as a marked placeholder; the first real client-bundle mint is gated on owner ratification of OQ-1.

  • OQ-1 (namespace authority — BLOCKS first real mint, not implementation). Adopt https://w3id.org/canonical/ontology (w3id) as IRI_BASE_NAMESPACE? Recommend YES (permanence decoupled from org DNS; semantic-web standard). Fallback: aisolutionhub-owned purl-style domain. Implementation of {132.43}/{132.44} proceeds with a placeholder + loud marker; only the first published bundle’s mint is gated on this.
  • OQ-2 (per-concept frontmatter IRI — deferral). Confirm we do NOT add an iri: frontmatter key this wave. Recommend YES, defer (per-concept churn perturbs BI-18 + collides with the bl-456/bl-477 frontmatter wave; the context.jsonld term→IRI map already serves {132.39}). Revisit as a follow-on backlog item after the frontmatter wave settles, only if a per-file IRI proves needed.
  • OQ-3 (client-id source). Confirm OKF_CLIENT_ID env var (mirroring OKF_BUNDLE_DIR) as the client-id source, resolved in flow_def.py. Recommend YES (consistent with the existing env-driven bundle-dir pattern; no overlay-schema change — the OV-2 closed schema stays intact). Alternative (a bundle-root manifest field) is heavier and unneeded now.

File ownership + overlap report (for Orchestrator sequencing)

Section titled “File ownership + overlap report (for Orchestrator sequencing)”

{132.43} owns (zero overlap — safe to run in parallel with anything):

  • scripts/cocoindex_pipeline/producer/iri_projection.py (NEW)
  • scripts/tests/test_producer_iri_projection.py (NEW)

{132.44} owns:

  • scripts/cocoindex_pipeline/producer/bundle_writer.py (constants region :174-189; new write_context_artefact beside write_ontology_artefact :762-791; write_bundle signature + tail :820-957)
  • scripts/cocoindex_pipeline/producer/flow_def.py (OKF_CLIENT_ID resolution + the one write_bundle(...) call :404-410)
  • scripts/tests/test_producer_bundle_writer.py (additive tests)
  • scripts/tests/test_producer_flow_def.py (additive tests)

Overlap vs the bl-456/bl-477 frontmatter wave (SHARED files flagged):

  • bundle_writer.py is the one shared file. The frontmatter wave touches frontmatter.py
    • bundle_writer.py’s per-concept declare_concept/render_concept_frontmatter call path (:339-353 region). {132.44} touches DIFFERENT regions (reserved-file constants :174-189, the ontology/context-artefact writers :762-791, and write_bundle’s tail :955) — but it is the same file, so a same-wave parallel dispatch risks a merge conflict at _RESERVED_BUNDLE_FILENAMES (both waves may add reserved filenames) and at write_bundle. Recommendation: serialise the bundle_writer.py edits — dispatch {132.44} and the frontmatter wave’s bundle_writer.py slice in different waves (cherry-pick order does not matter provided they are not concurrent). {132.43} (new file) has no overlap and may run concurrently with either.
  • frontmatter.py is NOT touched by this slice (Design decision 2) — the primary de-risking of the overlap. flow_def.py is not expected to be a frontmatter-wave file (low risk).
  • Sequencing gate (bl-457): this slice gates BEFORE {132.35} BI-18 re-proof (in_progress). IRI-12 keeps context.jsonld delta-deterministic so it does not perturb that re-proof.

Subtask decomposition (2 Subtasks — records returned to the Orchestrator)

Section titled “Subtask decomposition (2 Subtasks — records returned to the Orchestrator)”
  • {132.43} — pure mint module iri_projection.py + unit tests. No I/O, no bundle_writer change. Depends on nothing.
  • {132.44}context.jsonld emission in bundle_writer.py + OKF_CLIENT_ID threading in flow_def.py + reserved-file + integration/determinism tests. Depends on {132.43}.

Sibling-only dependency ({132.44} → {132.43}, both under Task 132) — no cross-Task dep, no escalation. Estimated total effort ~2–3h (mint module small; integration + determinism + §9-still-passes tests the bulk). Both are additive; neither regenerates Supabase types.