INHERIT v2 ε.ι Layer 2 architectural pattern: the LinkML YAML schema is the canonical metadata source-of-truth. Everything downstream is derived; metadata access flows from YAML, not from generated code.

Why: S3 spike 2026-05-02 verified that gen-typescript + gen-json-schema drop annotations entirely (LinkML 1.10 deliberate design — see feedback_linkml_annotation_surface_separation). gen-pydantic preserves via linkml_meta + Field.json_schema_extra. Catala scope-binding works end-to-end via YAML-direct shim. The architecturally-correct answer is not to fight LinkML’s separation — it’s to respect it and route metadata access via the appropriate surface.

Operational pattern for ε.ι Layer 2 (and any INHERIT v2 phase-gated / OntoUML-stereotyped class):

  1. YAML schema is canonical: author OntoUML stereotypes + phase metadata as annotations: blocks at the LinkML YAML level. Use annotations: (LinkML community’s de-facto pattern), not extensions: (extensions: is for typed schema-extensions; same generator behaviour but signals different intent).
  2. Python runtime via Pydantic: emit Pydantic with ~/tools/inherit-spike-env/bin/gen-pydantic --meta full schema.yaml > module.py 2> module.py.err. Access annotations via MyClass.linkml_meta.root['annotations'] (class-level) + MyClass.model_fields[<slot>].json_schema_extra['linkml_meta']['annotations'] (attribute-level). Note that gen-pydantic emits files with hyphens (schema.py) but Python imports need underscores — workaround: cp schema.py schema_module.py or rename.
  3. Catala scope-binding via YAML-direct shim: build-pipeline shim reads YAML via yaml.safe_load, filters classes by phase annotations, emits per-phase .catala_en files. Reference shim at /tmp/spike-s3-ontouml/scope_binding_shim.py (38 lines) + emitter at /tmp/spike-s3-ontouml/scope_binding_emitter.py (58 lines). Validation: ~/tools/inherit-spike-bin/catala typecheck --no-stdlib emitted-phase-X.catala_en for spike-level scope validation; production v2 build pipeline uses clerk start for full Stdlib_en resolution.
  4. TS + JSON-Schema are data-validation surfaces only: emit them with gen-typescript schema.yaml > schema.ts + gen-json-schema schema.yaml > schema.schema.json. They do NOT carry OntoUML metadata. If browser-side or wire-format consumers need metadata, emit a sidecar *.metadata.json (Year-2+ uplift; ~10-line Python build script that parses YAML + projects annotations into a flat metadata map).
  5. Frontmatter pin-drift hook validates annotations: block schema: extend ~/testatetech/scripts/check-frontmatter-pins.py to enforce inherit:phase_activation_status{active, deferred, retired} + inherit:phase_activation_target_phase matches valid phase string. (richard-task #204 logged 2026-05-02.)

How to apply:

  • For Phase-1 build pipeline scaffolding (code-inherit-v2/ Tier-3 standard): canonical pattern for any class needing OntoUML/inherit:phase metadata. Apply to all 9 i-ζ classes (per ε.ε lock) + future Phase-1.5 deferred classes.
  • For ε.ι A-131 amendment authoring (Phase E Task 13 lock-time): name this pattern explicitly. Cost-row at lock: ~£0.5-1K incremental tooling for the YAML-direct shim + sidecar JSON helper (vs S2 fallback’s manual hand-curation, this is automated build-pipeline tooling).
  • For Catala scope authoring across modules (Wills, Probate, Trusts, etc.): per-module Catala scope files emitted from per-phase filtered LinkML YAML. Wills.Bequest scopes filter by attestation phase; Probate.ExecutedEstate scopes filter by registration phase; Trusts.Trust scopes filter by trust-type-active phase; Pension scope omitted Phase-1 + included Phase-1.5+ per PensionAsset deferred-activation pattern.
  • For partner-firm REVIEW (Layer 4 of ε.ι universal-production-pipeline): partner-document includes the LinkML YAML fragment + the Pydantic class signature + the Catala scope file emitted at the partner’s target phase. This makes the phase-gating concrete + reviewable.
  • For browser-side TS clients (Phase-1.5+ uplift candidate; not Phase-1 critical path): emit sidecar *.metadata.json; inherit-pilot.metadata.json consumed by build-time TS module that re-exports phase-gating helpers.

Tooling pin (verified 2026-05-02 at S3 spike):

  • LinkML 1.10.0 → gen-pydantic --meta full (NOT --include-annotations=True — that flag does not exist; older docs/plans had wrong flag name)
  • Catala 1.1.0 → typecheck --no-stdlib for spike-level validation; production via clerk start
  • node 24.14.1 --experimental-strip-types --check for TS syntactic validation without tsc

Don’t:

  • Don’t try to “fix” the TS + JSON-Schema annotation-drop via custom Jinja templates or LinkML-version chasing (LinkML 1.11.0-rc1 not validated; not a Phase-1 priority).
  • Don’t read gen-json-schema output for metadata access — it has only description: + standard JSON Schema fields. YAML or Pydantic instead.
  • Don’t trust gen-pydantic --meta auto (default) to behave identically to --meta full across all schema shapes — test with --meta full explicitly when annotations are load-bearing.
  • Don’t run catala typecheck without --no-stdlib flag in spike contexts — fails on missing Stdlib_en. Production needs clerk start.

Source: T-spike-eps-iota-S3-ontouml-linkml-2026-05-02.md §3 (working configuration for ε.ι Layer 2 lock-time framing); arch-state v3.19 §11 S3 row + Changelog v3.19 row; plan v1.4 §1.8 + §2 Task 3 (Steps 3 + 7 corrected to use --meta full + --no-stdlib). PensionAsset (richard-task #213 deferred Year-2+) used as canonical phase-stereotype pilot class.

Related memories:

  • feedback_linkml_annotation_surface_separation — the LinkML 1.10 design choice that motivates this architectural pattern
  • feedback_kill_condition_strict_vs_spirit_reading_via_outcome_MITIGATED — how S3 outcome was scored MITIGATED rather than KILL despite strict-clause met
  • feedback_universal_production_pipeline_sequence — Layer 4 of ε.ι; this YAML-as-canonical pattern is Layer 2 (ontological-grounding tooling)
  • feedback_bold_front_loaded_synthesis_preference — the cost-row reasoning that ~£0.5-1K incremental tooling is rounding-error vs acquirer-narrative-value