Crosswalk-Concept LinkML Schema¶
Status: Schema designed; all 17
reviewedconcepts migrated Markdown → YAML and validated; the YAML → Markdown renderer is now trusted and run for real (--write);docs/crosswalks/concepts/*.mdis a generated artifact. TSV/TTL generation now also reads from YAML and writes the nesteddata/crosswalks/<slug>/<slug>.{tsv,ttl}layout. See 2026-07-05 (second pass) addendum below. Date: 2026-07-05 Related:docs/adr/ADR-0024-linkml-schema-validated-data-as-ssot-for-crosswalk-concepts.md,docs/INFORMATION_ARCHITECTURE.md
Purpose¶
ADR-0024 decided that a LinkML schema-validated YAML record becomes the SSOT for a crosswalk concept, with Markdown/TSV/TTL as generated projections, but explicitly deferred both the schema's field-by-field design and the 17-concept migration to implementation. This document records that design and the completed migration.
Schema Location¶
data/crosswalks/schema/crosswalk-concept.yaml — a single LinkML schema modeling the full nine-section structure docs/INFORMATION_ARCHITECTURE.md defines for a concept crosswalk (Identity/Editorial Status through Review History and Future Work). One CrosswalkConcept root class holds eight repeatable-row classes (LabelEntry, SourceDefinition, Correspondence, ComparisonNote, MappingAssertion, UncertaintyItem, ReferenceCitation, ReviewEvent), one per Markdown table.
Per ADR-0024 point 1, all long-form fields (scope_note, text, note, rationale, description, etc.) remain arbitrary-length free-text strings — the schema validates structure and controlled vocabulary, not prose length or content.
Migration Tool¶
scripts/crosswalk-md-to-yaml.py is a one-off migration script, not a continuously-run generator (unlike scripts/crosswalk-to-sssom-tsv.py, which ADR-0023 intends to keep re-running). It parses each docs/crosswalks/concepts/<slug>.md and writes data/crosswalks/<slug>/<slug>.yaml. Run once per concept to produce the initial YAML; edit the YAML directly afterward. Re-running it against a concept whose YAML has since diverged from its Markdown would silently discard the YAML-only edits — --check guards against exactly that (content diff, not just file-presence) for whichever concepts haven't yet had their Markdown retired.
python3 scripts/crosswalk-md-to-yaml.py # migrate all 17
python3 scripts/crosswalk-md-to-yaml.py object time # migrate specific slugs
python3 scripts/crosswalk-md-to-yaml.py --check # exit non-zero on any diff
Controlled Vocabulary Reused, Not Reinvented¶
Every enum's permissible values are the literal strings docs/INFORMATION_ARCHITECTURE.md and its amending ADRs already define, not a new naming convention:
EditorialStatus— the six values fromdocs/INFORMATION_ARCHITECTURE.md's Editorial Status Values table.Dimension—technical/philosophical/both(ADR-0010).ClaimType— used by both Source Definitions and Semantic Comparison Notes (see "A Real Finding" below for why these were merged into one five-value enum rather than IA's originally documented two-value/four-value split).RelationCategory— the nine categories fromdocs/methodology/crosswalk-methodology.md.ConfidenceLevel— the six-value scale fromADR-0009/ADR-0013(highthroughunknown, including the two sourcing-shift intermediate values). The numeric export projection (ADR-0014) is deliberately not a schema field — it stays a generation-time computation, never hand-authored, consistent withADR-0014's "contributors never pick a number by hand" rule.MappingStatus—candidate/reviewed/rejected(concept-levelEditorialStatusis separate from a single mapping's own status).UncertaintyType— the four values fromdocs/INFORMATION_ARCHITECTURE.mdsection 7.
Enum permissible-value names intentionally keep spaces and hyphens (e.g. "direct quotation", "close-match") rather than following LinkML's own snake_case/PascalCase naming-convention lint suggestions — linkml-lint flags this (standard_naming warnings, not errors) but round-trip fidelity to the exact strings already used across 17 Markdown files was judged more valuable than lint-style compliance. This is a deliberate choice, not an oversight.
One convention change from the Markdown status quo: Uncertainty items no longer encode "resolved" via inline ~~strikethrough~~/prose in the Type cell (e.g. ~~uncertainty~~ **resolved 2026-07-01**). The schema adds explicit resolved (boolean) and resolved_date fields instead.
Validation Toolchain¶
LinkML, PyYAML, and RDFLib (pinned versions verified working as of 2026-07-05) are tracked in scripts/requirements.txt — install into a virtualenv, not system Python (this project's own "no build system, package manager... in the conventional sense" framing per CLAUDE.md still holds for the repository as a whole; this covers only the scripts under scripts/):
python3 -m venv /tmp/xwkont-scripts-venv && source /tmp/xwkont-scripts-venv/bin/activate
pip install -r scripts/requirements.txt
# Schema self-check (structural lint; standard_naming warnings on the
# deliberately-literal enum values above are expected and not defects):
linkml-lint data/crosswalks/schema/crosswalk-concept.yaml
# Validate a concept instance against the schema:
linkml-validate -s data/crosswalks/schema/crosswalk-concept.yaml data/crosswalks/<slug>/<slug>.yaml
gen-json-schema data/crosswalks/schema/crosswalk-concept.yaml also succeeds, confirming the schema is self-consistent LinkML (no undeclared slots, no broken class/slot/enum references).
Migration Results: All 17 Concepts¶
Every reviewed concept in docs/crosswalks/concepts/ (object first as a hand-authored test-fit, then all 17 via the script once it existed) now has a data/crosswalks/<slug>/<slug>.yaml record, and every one validates clean:
$ for f in data/crosswalks/*/*.yaml; do linkml-validate -s data/crosswalks/schema/crosswalk-concept.yaml "$f"; done
No issues found (x17)
Row counts were checked programmatically against each Markdown source for all nine sections (Labels, Source Definitions, Correspondences, Comparison Notes, Mapping Assertions, Uncertainties, References, Review History, Future Work) across all 17 concepts, not spot-checked — per this project's own recorded practice of catching numeric-consistency errors by counting rather than inspection (TODO.md's large-enumeration-document lesson). Every count matches exactly; zero discrepancies found.
docs/crosswalks/concepts/*.md files themselves were not touched by this migration — Markdown remains the human-facing published artifact (see Deferred).
A Real Finding: Corpus Practice Already Diverges From docs/INFORMATION_ARCHITECTURE.md¶
Writing a mechanical parser against all 17 concepts (rather than hand-transcribing one, as the initial object-only pass did) surfaced drift a single-concept test-fit couldn't have found:
- Claim types in Source Definitions. IA restricts this section to
direct quotation/paraphraseonly. Actual practice across the corpus useseditorial observation(7 rows),inference(1 row), and a fifth value not documented anywhere in IA at all,non-equivalence(39 rows) — most commonly to record a verified absence of a source-ontology category (e.g. "full-text search... returned zero matches"). That's 47 of 212 Source Definitions rows (~22%) outside IA's stated two-value restriction. Resolved the same wayADR-0013resolved an analogous confidence-vocabulary drift: the schema'sClaimTypeenum now has five values, shared by both Source Definitions and Semantic Comparison Notes, rather than enforcing a restriction actual practice already exceeded at scale. This is a finding for a maintainer decision (whether to amenddocs/INFORMATION_ARCHITECTURE.mditself), not one this schema/migration makes unilaterally. - Multi-reference cells.
docs/adr/ADR-0009and IA describe aReference/referencefield as if single-valued. In practice, 10 Correspondence rows and 7 Source Definitions rows cite two or threexwkont:ref:*records in one cell (e.g. a primary paper plus its OWL translation). The schema'sreferenceslot is multivalued to match. - Strikethrough re-classification markup. Three Semantic Comparison Notes rows (
event.mdnote-003,process.mdnote-002,quality.mdnote-002) use~~inference~~ **checked and unconfirmed, 2026-07-01**-style markup to record that a claim's original classification was later revisited. The schema'sclaim_typefield holds the unwrapped base value; a new optionalclaim_type_annotationfield holds the revisiting note verbatim. Two Semantic Comparison Notes rows do the same forconfidence(e.g.high (verified directly against each source's own text)); a matchingconfidence_annotationfield was added. - One non-
xwkont:ref:*provenance citation.continuant-occurrent.md's References list citesADR-0015directly (the scope-expansion decision driving that crosswalk's own content buildout) alongside its normalxwkont:ref:*entries. Thereference_idpattern now accepts both forms. - One free-text placeholder in
supporting_references.proposition-content.mdnote-003 cites*(cross-cutting across this crosswalk and the other 8 0.2.0-batch concepts)*instead of a specific reference, for a genuinely cross-cutting batch-level observation.supporting_referencesis no longer pattern-constrained for this reason.
None of these were silently normalized away or dropped — each is preserved in the migrated YAML and documented here and inline in the schema's own field descriptions, so a maintainer can decide whether docs/INFORMATION_ARCHITECTURE.md itself should be amended to match observed practice (the ADR-0013 precedent) or whether some of this drift should instead be corrected in the source Markdown.
Findings (Schema Fit)¶
- The schema holds all 17 concepts' content with zero information loss once the drift above was accommodated — every row, every free-text field, and every controlled-vocabulary value (including the five real deviations found) had a schema slot to land in.
- The nine-section Markdown structure and the nine-class LinkML structure remain a clean 1:1 correspondence at the table level; the divergences found were all within individual cell values, not structural.
- The
resolved/resolved_date(Uncertainty),claim_type_annotation/confidence_annotation(Comparison Notes) fields are genuine structural improvements found during migration, not just transcription artifacts: each replaces prose-embedded markup with a real, independently-queryable field.
Dry-Run Results: YAML → Markdown¶
scripts/crosswalk-yaml-to-md.py renders data/crosswalks/<slug>/<slug>.yaml back to Markdown and, by default (no --write flag), only diffs the result against the existing docs/crosswalks/concepts/<slug>.md — it has never overwritten any Markdown file. Building this renderer and diffing it against all 17 concepts (not just eyeballing one) is what actually validated the migration, catching real bugs a Markdown → YAML-only pass had no way to surface:
python3 scripts/crosswalk-yaml-to-md.py --summary # one line per concept: IDENTICAL or DIFF (+n/-n)
python3 scripts/crosswalk-yaml-to-md.py <slug> # full diff for one concept
python3 scripts/crosswalk-yaml-to-md.py --write <slug> # actually overwrite -- not run in this pass
Four real bugs found and fixed, not just cosmetic mismatches:
- Silent content loss: 12 of 17 concepts have a prose sentence trailing the Correspondences table (e.g.
abstract-concrete.md: "No UFO or TUpper correspondence is recorded — see Source Definitions and Uncertainty."), which the original migration script never parsed at all — it only read the table, so this sentence existed in the Markdown but nowhere in the YAML. Fixed with a newcorrespondences_notefield, migration script updated to capture it, all 17 re-migrated and re-validated. - Data corruption, not just loss:
strip_cell()blindly stripped the first and last backtick off any cell that happened to start and end with one, which corrupts a cell containing multiple separately-backtick-wrapped tokens (e.g.`https://w3id.org/gfo/base/Property`, `Property_value`, `Value_space`inquality.md's GFO correspondence) into a mangled string with stray backticks in the middle. Affected 9 files'source_identifier_or_iricells. Fixed by only stripping when a cell has exactly one backtick pair; the renderer now also re-wraps bare single-token identifiers on the way back out, so both directions agree. - Double-nested backticks on render: the renderer's
join_refs()unconditionally backtick-wrapped every item in a reference list, including the rare prose-placeholder fallback item that already contains its own backticks (proposition-content.mdnote-003's*(cross-cutting ...0.2.0-batch ...)*), producing a mangled double-backtick render. Fixed by only wrapping items that are actually a cleanxwkont:ref:*/ADR-NNNNid. - Fabricated markup for a bare
resolvedcell: two Uncertainty rows insituation-state-of-affairs.md(uncertainty-002/003) have a Type cell that is literally just the wordresolved, with no strikethrough/bold markup and no recoverable originaluncertainty_type. The migration was storing this as a fakeresolution_note, which made the renderer reproduce a fuller~~uncertainty~~ **resolved**form the original never had. Fixed by leavingresolution_noteunset for this case, so the renderer reproduces the bare word.
Residual, documented, not fixed (all confirmed to be connector prose or formatting variants, not lost claims/evidence/mappings):
- Two Review History header variants exist in the corpus (
Review Event/Participant(s)— used by the original 8 concepts — vs.Review ID/Reviewer— used by the 90.2.0-batch concepts andTEMPLATE.md); the renderer always emits the latter. Same for two placeholder-token styles for "no review ID yet" (*(unreviewed)*vs.—) — renderer always emits—. Column labels and placeholder tokens only; no data affected. This is now the only remaining difference in 16 of the 17 concepts. - 6 rows across the corpus have short connector prose mixed into an otherwise-comma-separated reference list (e.g.
`xwkont:ref:a`, confirmed against `xwkont:ref:b` directly, or(all verified)trailing a list). The migration extracts thexwkont:ref:*ids correctly but drops the connector phrase. Judged not worth a fifth annotation field for 6 rows of non-substantive text. - One file (
continuant-occurrent.md) has an intro sentence before its Mapping Assertions table; one file (quality.md) has a trailing bare paragraph (not a-bullet) in Future Work. Both single-occurrence formatting quirks, not repeated patterns.
Net result after all four fixes: aggregate diff across all 17 concepts dropped from >600 changed lines to 97 (mostly the header/placeholder variant above, repeated per file) — on files totaling roughly 2,000 Markdown lines combined. Every one of the 17 YAML records still validates clean against the schema after these fixes.
Deferred (Not Done by This Pass)¶
- Amending
docs/INFORMATION_ARCHITECTURE.mdto formally document theClaimTypefive-value vocabulary, multi-reference cells, or the annotation-markup convention — left as a maintainer decision per "A Real Finding" above, not made by this pass.
Addendum, 2026-07-05 (second pass): Both Previously-Deferred Items Now Done¶
The maintainer confirmed both items deferred above (trusting the Markdown renderer to write, and switching TSV/TTL generation to the YAML source) should happen now rather than stay deferred further.
Markdown renderer trusted and run for real. python3 scripts/crosswalk-yaml-to-md.py --write was run for all 17 concepts. Since the previous pass's fixes had already brought every concept to IDENTICAL against the renderer's dry-run output (re-confirmed with --summary immediately before writing), the aggregate diff from this write was 0 lines across all 17 files — git diff docs/crosswalks/concepts/ was empty after the write. docs/crosswalks/concepts/*.md is now a real generated artifact, not hand-maintained; the residual Review History header/placeholder cosmetics and connector-prose gaps documented above under Dry-Run Results had already been absorbed into the committed Markdown by the time this pass ran, so there was nothing left to change.
TSV/TTL generation switched to YAML, nested layout. scripts/crosswalk-to-sssom-tsv.py now parses data/crosswalks/<slug>/<slug>.yaml directly (via PyYAML, no LinkML runtime dependency) instead of the Markdown Mapping Assertions table, and writes data/crosswalks/<slug>/<slug>.tsv (nested) instead of the flat data/crosswalks/<slug>.tsv. scripts/sssom-tsv-to-ttl.py likewise now reads/writes the nested data/crosswalks/<slug>/<slug>.{tsv,ttl} paths; its TSV-to-RDF logic is unchanged. Both scripts' docstrings now cite ADR-0024 (YAML SSOT) rather than ADR-0023 alone. The old flat data/crosswalks/<slug>.tsv/.ttl files were removed (git rm) and regenerated fresh in the nested layout.
All 17 concepts regenerated cleanly with both scripts (--check passes on both afterward, confirming idempotency). Diffing each new nested TSV against its prior flat version found exactly one systematic difference, confined to the xwkont_provenance column: the old Markdown-table parser's strip_cell() only stripped a leading/trailing backtick pair from the whole joined cell, leaving stray internal backticks in any cell citing more than one reference (e.g. xwkont:ref:bfo-2020`, `xwkont:ref:dolce-wonderweb-d18, cf. Dry-Run Results bug #2 above, which fixed the analogous bug in the Markdown renderer but the flat TSV script was never patched to match). Reading each reference id directly out of the YAML's provenance list and joining with ", " produces the clean form (xwkont:ref:bfo-2020, xwkont:ref:dolce-wonderweb-d18) with no stray backticks. Verified column-by-column across all 17 files: columns 1–10 are byte-identical between old flat and new nested TSVs; column 11 differs only by this backtick artifact (confirmed by stripping backticks from the old value and diffing against the new — zero remaining mismatches). All 17 TTL files are byte-identical old-vs-new (the Turtle builder never touches the provenance column), so only their file location changed.
No LinkML-vs-Markdown-vs-old-TSV data discrepancy requiring a judgment call was found beyond the backtick artifact above, which is a bug-fix (same underlying data, corrected serialization), not a genuine content conflict.
New runtime dependency note: PyYAML. scripts/crosswalk-yaml-to-md.py (prior pass) and scripts/crosswalk-to-sssom-tsv.py (this pass) both import yaml directly — this is a lighter dependency than the full LinkML toolchain (no schema validation happens in either script, just structured reads), but it is a real dependency beyond the RDFLib-only baseline docs/publication/validation-commands.md currently documents. Confirmed: PyYAML is not present in this machine's system Python (blocked from installing via pip by Homebrew's PEP 668 externally-managed-environment guard); verifying these scripts required building a throwaway venv, the same pattern this document already recommends for LinkML itself.
Addendum, 2026-07-05 (third pass): dependency-pinning file added¶
scripts/requirements.txt now tracks linkml==1.11.1, PyYAML==6.0.3, and rdflib==7.6.0 — the exact versions verified working in this pass's throwaway venv. This closes both previously-deferred dependency items (the LinkML pinning file and the smaller PyYAML note above) with a single file, per ADR-0024's own trade-off note that the project's "no build system" framing "will need updating... not glossed over." The pinning is scoped to scripts/ tooling only — docs/publication/validation-commands.md's repository-wide validation commands still assume only RDFLib, which remains true (its own commands never import linkml or yaml).