Repository navigation
obo: land ontology-onboarding — edges, registry, spine, rdf beside rf2 - #263
Merged
Merged
Conversation
…e CSR The bake persisted the per-predicate degree histogram in the edge block (`row_ptr`) but not the targets (`col_idx`), so a baked row could describe its own local shape and still not be traversed. This adds the targets. They ride the free value lanes in the one form the substrate has: a lane is `classid(4) + 12 content bytes`, and the classid says how to read its own 12. An edge lane reads its content as the G2 `4x24` carving — four literal links — and carries the *target ontology's own row classid*, so a link is exactly the `(classid, identity)` pair a key carries. The same binary search that resolves a key resolves a link; nothing new is invented to point at a node. Layout: the 480-byte value slab is 30 lanes. Lane 6 is the EntityType tenant; lanes 0..6 stay reserved (RESERVE, DON'T RECLAIM); edges take lanes 7..30 = 92 link slots. Targets are emitted ascending by (predicate, target namespace, numeric), and a lane never mixes target ontologies — its classid names exactly one, so a namespace change starts a new lane even mid-predicate. Unused slots are 0, which is unambiguous because no OBO CURIE numeric is 0. The predicate is not stored on the lane. A reader recovers it from the degree histogram, consuming `degree(p)` links per predicate in ascending order — which makes the already-persisted `row_ptr` load-bearing instead of decorative, and leaves a row that is a complete CSR adjacency row on its own. `EdgeLanes` is the read side: it borrows the row and decodes a u24 with a shift. No allocation, no gathered window, no owned copy of anything the lane already holds. Measured before the layout constant was chosen (`probe_edge_capacity`, real sources): 186 029 triples over 68 789 subjects, max out-degree 62, max lane demand for a single row 16 against a 23-lane budget. Both packing assumptions verified on the same data — CURIE numeric 0 never occurs (so the pad is safe) and every numeric fits 24 bits. The packer reports an overflow in `PackStats` rather than truncating; `bake` accumulates it so a driver can refuse to ship an artifact that is missing edges. Verified end-to-end (`verify_edges`): links read back out of the row bytes match the SPO triple table as a multiset, per ontology — RO 3 links / is_a 2, UBERON 71 844 / 27 170, HP 50 284 / 31 284, PATO 3 389 / 2 909, MONDO 60 509 / 46 448; 186 029 packed, 0 dropped, 0 rows overflowed. Those are two different code paths over the same input. The is_a totals also match the degree bytes in the already-baked slabs, so the packing agrees with the artifact it extends. RO is small enough to check by hand and is printed in full: RO:0002532 --Other--> RO:0002533, RO:0002533 and RO:0002534 --is_a--> RO:0002532, and RO:0002577 correctly carries no link at all because its only parent (BFO:0000040) is outside the five core namespaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
The crate doc said "Where this runs: BEFORE the bake, never after". That is backwards and was ruled so: the joins are pre-bake, but ELK is not a join — it observes the baked result and reports what follows from it. The bake is its input, not its output. This corrects the doc, the Cargo description, and the three consequences that were derived from the wrong premise. The consequence that needed real rework was "types are legal here, nothing this crate defines survives the bake". Post-bake that reads as a contradiction, and the honest form is narrower: `ClassAddr` is not schema, it is the (classid, identity) pair a baked key already carries, decoded from its position — the same shape `ogar_obo::edges::Link` reads out of a lane. It declares nothing and outlives nothing. `Subsumption` stays a distinct type for the one reason it was ever worth one: so a caller cannot pass a part_of edge to a reasoner that has no role composition. The other two consequences survive unchanged and one gets stronger: post-bake, "no file, no CURIE, no label" is not a discipline the crate has to keep — baked rows carry no CURIE and no label to reach for. Adds the adapter this rests on, `ogar_obo::edges::subsumptions`: the is_a-only projection over baked rows, yielding (subclass, superclass) as the address pairs the keys and lanes already hold. The is_a filter is the safety, not a convenience — walking subsumption together with part_of derives false ancestors, and filtering in the projection means a caller cannot forget to. Also adds `unpack_key` (a row's own address, zero value decode) and `edges::subject`. `elk_over_bake` runs the whole path on the real slab and cross-checks it against a representation it does not share. Reading is_a out of the lanes yields 107 813 asserted subsumptions — the per-ontology degree totals summed — and closing them yields 738 651 transitive ancestors and 0 equivalence cycles. The bake's own `reason::saturate` reports 738 651 subsumption pairs and 0 cycles from the out-of-line triple table. Two reasoners over two representations, identical to the unit. Root counts agree too: RO has exactly 2 of its 4 rows with a parent, which are RO:0002533 and RO:0002534. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
… ends Written down because the reasoning is not recoverable from the code: why the G2 4x24 carving carries the targets rather than the cheaper 6x(u8:u8) rails (a rail ref is basin-local and the bake has no basin — HEEL/HIP/TWIG are zero on all 68,797 rows while identities run past 8.9 million, so a one-byte ref has nothing to mean), and why identity does the opposite and rides the rail (the same 24 bits as an X:Y pair with headroom, which a flat u24 cannot be because it has no axis). Records the measurements the layout constant was chosen FROM, not after: max lane demand 16 against a 23-lane budget, and both packing assumptions checked on the real sources — CURIE numeric 0 never occurs, so the pad is safe, and every numeric fits 24 bits. Lane demand rather than out-degree is the binding constraint, because the (predicate, namespace) grouping fragments lanes. Names the three verification paths and what makes them independent, including the one that matters most: closing the lanes gives 738,651 transitive ancestors and 0 cycles, and the bake's own saturation computes the same two numbers from the out-of-line triple table — different representation, different code. Separates ogar_obo::Predicate from the ogar-ro crate explicitly, since the names invite conflation: the first is the byte palette carried on baked edges (the THINK arm, what the graph asserts), the second is a callable vocabulary at concept 0x0306 whose operands resolve against a basin-local codebook (the DO arm). They meet at the ontology, not in the bytes. Open ends are stated as open: the rails have no scheduled trigger, W-RO-5 is undecided by both sides, and degree saturation at 255 is untested because the measured maximum is 62 — if a source ever crosses it the histogram clamps while the lanes still hold the targets, and the two halves would disagree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
The plan said to reach for TypedGraph::traverse / hdr_bfs. Reading them, both take a concrete &GrBMatrix rather than a trait, and GrBMatrix's CsrStorage holds values: Vec<BitVec> at 16384 bits — roughly 2 KiB PER EDGE. Materialising the 107,813 is_a edges that way costs on the order of 200 MB to hold an adjacency the rows already contain, which is the opposite of a large hydrated table with a small resident set. traverse() also computes matrix.mxm(matrix), which is one hop of the SQUARE, not a walk from a source. So the plan was wrong on two counts and is corrected here: the lens IS the traversal, not an adapter into blasgraph. ogar-obo::spine — SpineLens<'a>, one borrowed field, in the house style of the workspace's other lenses. resolve() is a binary search over (classid, num), sound because the bake emits rows in that order: family holds the high half of the numeric, so ordering by (classid, family, identity) IS ordering by (classid, num). Verified over the shipped bake — 68,797 rows, zero inversions. is_sorted() exists because an unsorted slice does not panic, it silently finds nothing, so the precondition is checkable at the boundary where rows arrive. ogar-elk::lens — Parents (one visitor method, so a hop allocates nothing) and LensClosure. A blanket impl over closures keeps the seam free of a dependency edge in either direction: the crate owning the byte layout hands over a closure capturing its own view, and neither crate names the other's types. NO HASHING on this path. Dedup is an ordered insert, membership is a binary search. Not a micro-optimisation: addresses arrive already ordered from the bake, hashing discards that ordering, and the ordering has to be rebuilt before two ancestries can be intersected. Keeping it makes meet() a single merge pass and makes two runs produce identical output — the property a layer emitting facts has to have. Removing the hash sets took the full sweep from 7.8x the indexed walk to 4.8x. meet() is the Schnittpunkt: an x-hop cross-ontology lookup asks where reachable sets MEET, and meeting is an intersection over sorted addresses — no vectors, no per-edge payload, nothing serialized. The lens never caches. A transitive closure recomputes equal, which under the zero-copy law makes it a projection, and a projection is never stored however much work produced it. Asking twice walks twice, deliberately. Falsifier (examples/elk_lens_vs_closure, every class, no sampling): 68,786 classes compared, 0 ancestor sets differing, 738,651 transitive ancestors and 0 equivalence cycles from both the lens and the indexed closure. Cost is printed whether or not it flatters the lens: 0 build vs 19 ms, 4.8x on a full sweep, and the lens holds a slice where the index holds a map over 107,813 edges. KNOWN LIMIT, measured not assumed: meet() between a MONDO and an HPO class returns 0, because is_a never crosses ontologies. Cross-ontology Schnittpunkte need the cross-predicates (has_phenotype / has_anatomy / has_quality), which are present in the lanes — 78,216 of the 186,029 links — but which the closure deliberately does not walk, since mixing part_of into subsumption derives false ancestors. A typed cross-predicate walk is the next piece, not this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
meet over is_a alone can never cross an ontology — subsumption stays inside the ontology that declares it. Crossing needs a ROLE (has_phenotype into HPO, has_anatomy into Uberon) plus R-exists generalization of the FILLER up the grounding spine. The two spines are different and swapping them is the classic error, so both are explicit parameters with no default that could silently be wrong: grounding_parents_of is is_a UNION part_of (what a filler generalizes up); parents_of stays is_a alone (what a subject subsumes through). Generalizing a subject through part_of is unsound — A part_of B, B subclass C does not give A subclass C — while generalizing a filler through it is the standard inherit-grounding deduction. TWO CORRECTIONS THIS COMMIT RECORDS, both caught by checks rather than by care. 1. The falsifier was originally aimed at reason::saturate's existential_inferred and is NOT any more. Measured: the combined is_a UNION part_of spine carries 2,277 back edges even though is_a alone has zero, and ancestor_closure SKIPS a back edge when it meets one. Its memo is therefore an incomplete closure whose value depends on DFS start order — the same code over the same triples in two orders produced 9,522,084 and 9,437,429. A number that moves cannot falsify anything, so the check is now against an independent local computation that shares no code with fillers_closed: 64 seeds, 0 divergences, and all 64 widened (so the rule is not inert). 2. The first meet was sound and vacuous. 120 of 120 sampled disease pairs "shared a phenotype" and the shared address was always HP:0000001 — the HPO root — because every phenotype generalizes to "All". A result that fires on every input carries as much information as one that never fires. most_specific narrows a meet to members that are not an ancestor of another member, and the sampled pairs then split 81 sharing something specific vs 39 sharing only the root. It is deliberately NOT folded into meet_via: the raw meet is the sound object and narrowing is the caller's presentation choice. Scale is seeded, never global. A query starts from facts that already exist — an observed lab code, a dispensed drug, a coded diagnosis — which is a handful of addresses. An earlier draft summed R-exists over all 67,745 existential edges to chase an aggregate; that is a theory-of-everything computation nobody asked for and it is gone. Controls that must keep holding: is_a finds 0 cross-ontology meets, and the narrowed meet must produce BOTH specific and root-only pairs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
The bake reaches its namespaces through the `Namespace` enum, which spreads
one namespace's facts across four `match` bodies plus an index array
(`prefix` / `from_prefix` / `concept_id` / `namespace`). A sixth namespace
means editing five sites with hand-written literals — the shape that makes a
bake un-extendable without a code change per ontology.
`registry::NsRegistry` carries the same facts as one ordered table of
`NsSpec { prefix, concept_id }` rows. A driver that wants a different set of
namespaces passes a different table; it does not touch this crate's types.
`NsRegistry::parse` is the general form of `TermId::parse`, which is this
against `OBO_CORE`.
The row INDEX is the `TermId::ns` ordinal, and `bake` sorts ids by
`(ns, num)` — so table order determines the baked row order and therefore the
artifact digest. `OBO_CORE` is a second copy of the enum's facts and is only
safe to hold because `obo_core_matches_the_shipped_enum` fails the moment the
two disagree on order, prefix, or concept id. That is the drift guard, not a
formality: a reorder silently re-bakes every downstream slab.
`classify` deliberately stays on the enum — a `(subject_ns, object_ns)` pair
maps to an RO predicate, which is semantics between two named ontologies, not
a per-namespace attribute. A namespace outside the core classifies as
`Predicate::Other`, correct for upper-level spine ontologies whose edges are
`is_a` skeletons with no cross-ontology predicate to name.
Falsifiers, not assertions implied by the code:
- the mirror test fails on any reorder/renumber/rename;
- registry parse must agree with the shipped parse on all six accepts AND all
five rejects (prefix outside table, >24-bit, malformed, no colon, wrong
case) — otherwise routing a bake through the table would change which terms
are tiled;
- the extended-table test asserts the shipped core CANNOT parse `BFO:0000002`,
so a table that was silently ignored would fail rather than pass;
- ordinals are table-relative — MONDO is row 1 in the extended table and row 0
in the core — pinning that a TermId is only meaningful WITH its registry.
No shipped digest moves: nothing in the bake path consults the registry yet.
34 tests pass; clippy -D warnings clean.
… numeric
The workspace already parses RDF — lance-graph-ontology's OwlHydrator reads
Turtle and RDF/XML through oxttl/oxrdfxml. I proposed building one before
checking, was corrected, and read it. It cannot serve as a bake front-end, for
two reasons found in `hydrate_many`:
1. It DROPS the triple after interning. `OntologySlot` is
`{ entity_count, iri_to_id }` and nothing else — no subClassOf survives
hydration. `ContextBundle::edge_types` is a whitelist of predicate IRIs a
traversal may follow, not stored edges.
2. Its identity is an intern COUNTER, so it depends on file order. A bake
keyed on it would not be reproducible.
`rdf` is the opposite of both, and is NOT a second parser — oxttl/oxrdfxml do
the reading, pinned to the same versions lance-graph-ontology pins, behind an
off-by-default `rdf` feature so the crate keeps its ogar-vocab-only profile.
Identity comes from the term: `…/obo/HP_0000118` -> `HP:0000118` -> 118, which
is the numeric pack_key writes on the family:identity rail. The extraction map
IS the NsRegistry — every OBO PURL is `<base><PREFIX>_<NNNNNNN>`, so a new
ontology is a table row, never a code path.
Predicates are read from `owl:onProperty` against OBO_RESTRICTION_PREDICATES,
not guessed from a namespace pair. parse_obo guesses deliberately, because the
`.obo` relationship label is unreliable; RDF names the property, so the guess
is unnecessary. Consequence stated in the docs: an RDF bake and a .obo bake of
the same ontology may type the same edge differently. The shipped five-namespace
bake was produced from .obo and is NOT re-baked here.
The table carries only the three CURIEs the crate already documents on
Predicate (BFO:0000050, RO:0002200, RO:0004026). HasAnatomy/HasQuality are
documented as harvested from logical definitions with no named property, so
there is no CURIE to add that would not be invented — those land on Other.
Documented limits, each with an assertion rather than a hope: restrictions
nested in an owl:intersectionOf collection are not walked; equivalentClass to
a NAMED class is skipped (equivalence is not subsumption — recording it as
is_a would invent a parent); a term becomes a row only on `a owl:Class`.
Falsifiers:
- order-independence: the same graph with stanzas reversed yields identical
ids and edges, and the test asserts the reordered bytes actually differ. An
intern-counter identity fails this.
- an EMPTY predicate table must classify everything as Other, proving the
table is read rather than the mapping baked into the function.
- a unionOf blank node must contribute NO edge — the documented limit asserted,
not hoped.
- a malformed document must error, so a parse failure cannot look like an
empty ontology.
- the registry is genuinely consulted: the same IRI resolves under one table
and not another.
Also adds META_STUDY_SPINE (8 rows, 0x0306..0x030D) as config, and
examples/probe_spine.rs, which measures the shipped path over real documents
instead of asserting it works. Measured, union of all eight:
ns classes is_a rel max num id==0 obsolete
BFO 35 137 0 182 0 0
COB 26 30 16 1300 0 9
IAO 216 373 76 8000020 0 22
OBI 4317 5928 1443 9999994 0 108
OBCS 370 374 58 375 0 0
SEPIO 62 46 16 435 1 15
ECO 2299 3780 3 9000001 1 85
FBbi 662 575 49 100028 1 107
TOTAL 7987 11243 1661
1634 of 1661 typed edges land on Predicate::Other — reported, not guessed.
Two corrections this probe forced, recorded rather than quietly fixed:
- I earlier described the spine's bake order as "derived, not chosen,"
implying a DAG. It is cyclic: OBI<->IAO (394/10), OBI<->COB (236/3),
OBI<->OBCS (58/70). Nothing depends on acyclicity — bake takes the union and
sorts by (ns, num) — so the doc now says order is a convention that fixes
row order and therefore the digest, not a topological sort.
- The class counts supersede an earlier throwaway-script probe (OBI 4286->4317,
IAO 210->216, SEPIO 57->62, COB 23->26, ECO 2303->2299, total 7946->7987).
These come from the shipped path.
The id==0 measurement corroborates the earlier finding exactly — SEPIO, ECO
and FBbi each carry a real term at numeric 0, which edges.rs uses as the lane
pad meaning "lane spent, advance". An edge into such a term truncates a walk
silently, so META_STUDY_SPINE is documented as NOT bakeable for those three
until the +1 bias lands in edges.rs. That fix moves every shipped slab digest
and forces a re-bake of all five core tables, so it stays an operator call and
is not a side effect of adding a table.
42 tests pass; clippy -D warnings clean with and without the feature.
The lane pad was all-zero bytes, and the encoding relied on an assertion to
keep that unambiguous: "no OBO CURIE numeric is 0". That held for the five
baked namespaces and is false in general — probe_spine measured a real term at
numeric 0 in three further namespaces, one of them a namespace root.
The failure shape was the bad one. Removing the bias and running the new test
shows it directly:
left: [] right: [0, 118]
Not [118]. BOTH links vanish. A zero slot does not drop one edge, it ends the
lane, so every target after it in that lane is lost too — and the artifact
still validates, so nothing anywhere reports it. A traversal simply returns
less than the graph contains.
Fix: store the numeric biased by one (encode_numeric/decode_numeric). Stored 0
means pad by construction rather than by a property of the input.
MAX_LINK_NUMERIC is 2^24-2 = 16_777_214 against a measured maximum of
9_999_994. The one unrepresentable numeric is reported as dropped, never
wrapped — with a test, because a guard that cannot be shown to fire is the
defect one level up.
Re-baked the core, as the gate required. Measured, not assumed:
pre-A1 c88b44ea85a11bcf1e820b2aa03000fed157e39760ab3e5b83d40697a9f30bfb
post-A1 424e81cce7050b593fbd61c3507530b3b80c5471d6f7180e5e834165b69b0ae6
186,567 of 35,224,064 bytes differ (0.53%), 68,789 of 68,797 rows
(the 8 unchanged rows are the ones with no out-edges)
Same graph, different bytes: rows 68,797, triples 186,029, links packed
186,029, dropped 0, overflowed 0, is_a cycles 0, dangling 0, saturation
738,651 subsumption pairs — every one identical to the shipped bake.
verify_edges re-checks every lane against the SPO triple table and the is_a
degree totals are unchanged (RO 2 / UBERON 27,170 / HP 31,284 / PATO 2,909 /
MONDO 46,448).
bake_obo did not print links_dropped / rows_overflowed at all. They were
computed and thrown away, so a run could look clean while the artifact was
missing edges — the same silent shape A1 is about, one level up. Now printed,
and asserted rather than warned about.
A1 existed to unblock the spine, so this finishes the job rather than stopping
at the bias. `bake` resolved classids through the hardcoded Namespace enum,
whose ordinal array panics for any ordinal >= 5, so no table but OBO_CORE
could bake. Added `bake_with(nodes, app_prefix, reg)`; `bake` is that against
OBO_CORE. Verified digest-neutral — the core re-bakes to 424e81cc unchanged.
A target whose namespace is absent from the registry is reported as dropped,
never guessed at.
examples/bake_spine.rs bakes all eight spine namespaces through the table with
no per-ontology code:
rows 7,641 · triples 12,885 · packed 12,885 · dropped 0 · overflowed 0
cycles 0 · dangling 0
BFO 35 · COB 17 · IAO 194 · OBI 4,209 · OBCS 370 · SEPIO 47 · ECO 2,214 · FBbi 555
sha256 437ea33aa7dda76bb37f13fd14849e8a339d71e354d7d90cea595b80875396c5
and asserts the gate directly: 10 edges point at a numeric-0 term, and 10 of
10 are read back out of the baked lanes. Before the bias each of those 10
truncated its lane.
1,628 of 12,885 typed edges land on Predicate::Other — no CURIE row matched
their owl:onProperty. Reported, not guessed.
Also declares required-features for the two rdf examples so
`clippy --all-targets` on the default feature set skips them rather than
failing.
46 tests pass; clippy -D warnings clean with and without the feature.
Found while auditing something else. I minted META_STUDY_SPINE at
0x0306..=0x030D yesterday. `ogar_ro::RELATION_BODY_CONCEPT_ID` is 0x0306 —
live, and already consumed by medcare-cohorts.
That is not a near-miss. A baked BFO row and an ogar-ro relation-body row
would carry the SAME classid, and nothing downstream could tell them apart.
The spine bake I shipped had 35 BFO rows sitting on that address.
The fix is not +1. `0x0300..=0x030F` is now the reserved core band — the five
OBO-core namespaces, ogar-ro, and room for nine more — and the spine starts at
the next clean boundary, 0x0310..=0x0317. Shifting by one would have parked
BFO at 0x0307, immediately adjacent to a live allocation, which is how this
happens again.
Two guards, because one cannot cover it. ogar-obo cannot see ogar-ro (the
dependency runs ogar-ro -> ogar-obo), so:
- ogar-obo asserts the BAND: no spine row inside 0x0300..=0x030F, no core
row outside it, no id claimed twice across both tables, and every id still
inside the 0x03 Ontology domain.
- ogar-ro asserts the CONSTANT: no ogar-obo namespace claims
RELATION_BODY_CONCEPT_ID — plus an anti-vacuity test that the tables it
checks are actually populated, since two empty tables would pass while
proving nothing.
Both verified to fire: restoring 0x0306 fails with
BFO at 0x0306 sits in the reserved core band (0x0300..=0x030F) — that is
where ogar-ro's 0x0306 lives
Re-baked the spine. Classids are now 0x0310_0000..0x0317_0000, digest moves
437ea33a -> e1aa94d23bd3b2ffbf901591bcafb158dd625f4744a291026677349ad15d524f.
Everything else is unchanged: 7,641 rows, 12,885 triples, 0 dropped, 0
overflowed, and the A1 proof still holds — 10 numeric-0 links present, 10 of
10 read back from the lanes.
The core bake (424e81cc) is untouched: OBO_CORE's ids did not move.
Worth naming why this was invisible: the collision lived in two repos' worth
of hex literals with no test between them. It was found by hand-enumerating
the 0x03 domain during an unrelated audit, which is not a process that scales
— hence a guard on each side rather than a note.
…he union S3 holds the bake both ways: one union `obo-core.soa` (what bakes.tsv pins) and five per-namespace slabs. The splitter that made the second was not in the repo, so re-syncing S3 after the A1 re-bake meant reconstructing it — which is the same class of problem as a pin table living inside a shell script. Splits `bake_with`'s output by namespace ordinal, asserting the ship gate (links_dropped == 0, rows_overflowed == 0) before writing anything. Row counts and byte sizes reproduce the slabs already on S3 exactly, so the only difference against the previous upload is the corrected bytes. Each slab verifies independently downstream: cross-slab references (HP->UBERON in a HP-only slab) are counted separately from dangling ones, so a legitimate partition does not read as a broken artifact.
…nding failed META_STUDY_SPINE was moved to 0x0310..=0x0317 this morning after colliding with ogar_ro's 0x0306. That block collided with FOUR consumer allocations: 0x0311 COB vs OPS 0x0313 OBI vs a product lane 0x0315 SEPIO vs the UMLS CUI convergence lane 0x0317 FBbi vs Orphanet The second attempt failed for a reason worth naming rather than patching: the 0x03 domain's occupancy was enumerated IN THIS REPO ONLY. A private consumer holds an odd-stride run (0x0307, 09, 0B, 0D, 0F, 11, 13, 15, 17, 19, 1B, 1D) that no test here can see, and it grows into odd slots — so a nearby block collides eventually rather than immediately, which is the worse failure because it lands long after the decision. Moved to 0x0340..=0x0347, clear of both the core band and that run's growth. But the spacing is a MITIGATION, not the guard, and the band test here is weaker than it reads: it proves this table is self-consistent and clear of upstream neighbours, which is exactly what it did while colliding with four ids it had no way to enumerate. The doc now says that outright so nobody reads it as coverage it does not have. The real guard cannot live here. It has to see both sides, so it lives in the consumer (medcare-cohorts::classid_census), which enumerates its own allocations and checks them against ogar-obo's tables. Run against the 0x0310 block it reports all four collisions by name. Spine digest moves e1aa94d2 -> a2205fb7255eb2801d0056944b364172d2be529ffb0ac6874ad5510bb13c3f06. Rows, triples and the A1 proof are unchanged: 7,641 rows, 12,885 triples, 0 dropped, 10 of 10 numeric-0 links read back. The core bake is untouched. Also fixed a test fixture the move caught: `a_different_table_...` built its EXTENDED table from the spine's literal, so the table moved while its assertion literal did not.
The bake reported `dangling: 0` for an artifact a reader finds 55 dangling edges in. Both were right about what they measured, and the stat was measuring the wrong thing. `dangling` counted edges whose object is absent from the node MAP. An obsolete term is IN the map — `parse_obo` / `parse_rdf` keep it, and `bake` deliberately does not tile it — so an edge into one passed the check while pointing at a row that does not exist. Found by running the edge-resolution hook against the spine I had just published. An edge into an obsolete term is itself obsolete, so it is now dropped rather than emitted, and counted honestly: `dangling` means "object gets no ROW", whether absent from the sources or present-but-obsolete. Digest-neutral for the `.obo` core — no edge there targets an untiled row, so `bake_obo` reproduces 424e81cc exactly. It is the RDF path, where `owl:deprecated` classes are still referenced by live ones, that this corrects: the spine drops 12,885 -> 12,830 triples, exactly the 55, and its digest moves a2205fb7 -> 4c41538151b344e31db70d674697f01580e1c2b755bc12c796f6ec755d8f138f. The two per-edge `dangling += 1` counters are now unreachable (the filter runs first), so they became debug_asserts rather than dead arithmetic that would quietly disagree with the filter if either changed.
…icates Found by the edge-agreement check: the artifact's degree histogram totals 107,813 is_a slots while carrying 92,685 DISTINCT (subject, parent) pairs. The gap is 15,128 duplicate is_a slots across 10,589 rows. Cause: a driver that unions several sources concatenates parent lists. `bake_obo` merges five .obo files with `extend`, so a term appearing in more than one gets its parents twice. `rdf::parse_rdf` already dedups its own output; doing it in `bake_with` covers every driver instead of every driver having to remember. Across all predicates the effect is larger than the is_a-only measurement suggested: triples drop 186,029 -> 152,073, so 33,956 edges (18.2%) were duplicates. `rel` carries them too, which the is_a-only probe could not see. Semantically this changed nothing — `A is_a B` asserted twice is `A is_a B`, so no answer was ever wrong. What it cost: lane budget, and an over-reporting degree histogram, which is exactly what `resident_out_degree` reads. A row claiming 62 out-edges could have had 56. `BakeStats.duplicates` reports what was collapsed rather than silently shrinking the edge count. NOT re-published. This moves the core digest 424e81cc -> 75ec52a8 and changes numbers that appear in the roadmap, the pipeline spec and the boot receipt (186,029 triples is quoted in several places). That is a deliberate call, not a side effect of a lint fix, and today has already spent three re-bakes. The code is correct from here; the shipped artifact carries the duplicates and is not wrong because of them.
…rdf land beside rf2 Resolves the module-list conflict in ogar-obo/src/lib.rs (both sides added modules: main brought rf2, the branch brought registry + spine — union, alphabetized). The ogar-ro side auto-merged: the concept-id collision guard (the classid-guard mirror half) now sits beside the 0xA0..0xA2 mints that landed on main after the branch diverged. Verified: cargo test -p ogar-obo -p ogar-ro green (55 tests), full workspace builds. Unblocks the MedCare-rs edge-agreement CI hooks, which consume ogar_obo::edges::EdgeLanes — stranded against this branch since 2026-08-08. Co-Authored-By: Claude <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_ed1c21c1-83af-47e6-bc87-ee1d7db2c4d0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges the stranded
claude/ontology-onboardingbranch (13 commits:edges.rslane packing,registry.rsNsRegistry + band guard,spine.rs, RDF front-end, dedup/dangling fixes) intomain, which had moved 4 commits ahead (#261 RO mints, #262 RF2 reader).Conflict resolution:
ogar-obo/src/lib.rsmodule list — both sides added modules (main:rf2; branch:registry,spine) — union, alphabetized.ogar-ro/src/lib.rsauto-merged: the concept-id collision guard (the classid-guard mirror half) now sits beside the0xA0..0xA2mints.Verified:
cargo test -p ogar-obo -p ogar-rogreen (55 tests); full workspace builds.Downstream: unblocks the MedCare-rs edge-agreement CI hooks (
ogar_obo::edges::EdgeLanes), stranded against this branch since 2026-08-08.Generated by Claude Code