From 9e0ca5732286e4e1b0fb84b23a13ad28f6587b4a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 21:19:47 +0000 Subject: [PATCH 1/9] draft: mark ruff-csharp-do-arm-handover RESOLVED (ruff PR #59 merged) --- .claude/draft/ruff-csharp-do-arm-handover.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.claude/draft/ruff-csharp-do-arm-handover.md b/.claude/draft/ruff-csharp-do-arm-handover.md index 367650b..0fd7bf6 100644 --- a/.claude/draft/ruff-csharp-do-arm-handover.md +++ b/.claude/draft/ruff-csharp-do-arm-handover.md @@ -1,5 +1,19 @@ # DRAFT / HANDOVER — close out the C# (Roslyn) DO-arm harvest for MedCare +> **RESOLVED — 2026-07-07.** The ready-to-apply patch this handover +> pointed at landed as [AdaWorldAPI/ruff#59](https://github.com/AdaWorldAPI/ruff/pull/59) +> and merged (`5c6c0fcf9ca4a7ac3a7efb924b00285456b5cf51`). The ruff-write +> 403 was proxy-specific, not an org/token permission gate — direct +> `git`/REST against `github.com` with the proxy env unset worked (see +> `tesseract-rs/CLAUDE.md` § GitHub access matrix). Loader fixture + +> codebook-DTO check are in on `main` now. Still open: the `dotnet` run +> against a real C# corpus to de-draft `EmitBodyArm` (no `dotnet` in the +> sandbox that closed this out). Full record: +> `MedCare-rs/.claude/handovers/ruff-csharp-arm/RESOLVED.md`. + +--- + + > **Why this lives in OGAR, not ruff:** ruff writes are currently > **403-blocked** for this session (org-level — the Claude GitHub App needs > (re)connecting for `AdaWorldAPI`, and/or `ruff` re-added to its scope; From b799198f33fe0481cdf11d42c71e05620f1df777 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 22:08:52 +0000 Subject: [PATCH 2/9] ogar-vocab: healthcare capability table (parity-plan P3) + registry wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-authored, harvest-informed healthcare action table on the six minted 0x09XX Health concepts (patient/diagnosis/lab_value/medication/treatment/ vital_sign). Registers in capability_registry::domain_tables() through the generic entries_from_actions derive, so resolve_hotplug turns green for the Health classids that previously banged NoCapabilitiesFor. Why hand-authored, not the plan's mechanical Loop A: the 2026-07-07 harvest over the consumer C# tree falsified the auto-derive premise. The consumer is a WinForms monolith — no AR-shaped class per domain concept; the domain surface lives as verb_concept methods on one monolithic DAL class. A class-keyed lift has nothing to key on, so — exactly like ocr_actions.rs's "no ActiveRecord source" rationale — the table is declared by hand, with the capability split following the measured DAL method inventory. The concrete method-name evidence stays in the consumer repo, never committed here. visit (0x0906) deliberately excluded: zero harvest evidence (operator- confirmed). Six subjects, not the port's seven; a visit_is_minted_but_ deliberately_tableless pin keeps the exclusion visible. - healthcare_actions.rs: 20 name-only ActionDefs + HEALTHCARE_ACTION_NAMES / HEALTHCARE_SUBJECT_CLASSIDS / HEALTHCARE_EXPECTED_EXECUTORS=["medcare-rs"]; 9 module tests (subject-domain fuse, subject-const parity, visit exclusion, name-only, well-formed identity). - capability_registry.rs: healthcare_entries() DomainTable + healthcare_hotplug_resolves_vocab_and_actions probe; the each_drift_arm NoCapabilitiesFor probe moved 0x0901 -> visit 0x0906 (patient now has caps). - plan doc: P3/P4 shipped + the Loop-A-falsified correction. 131 ogar-vocab tests green; fmt + clippy -D warnings clean; PII word-boundary scan clean; no private-corpus identifiers in committed artifacts. --- crates/ogar-vocab/src/capability_registry.rs | 61 +++- crates/ogar-vocab/src/healthcare_actions.rs | 321 ++++++++++++++++++ crates/ogar-vocab/src/lib.rs | 4 + .../MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md | 22 +- 4 files changed, 398 insertions(+), 10 deletions(-) create mode 100644 crates/ogar-vocab/src/healthcare_actions.rs diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index c6dc315..2d120d3 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -170,11 +170,18 @@ pub struct DomainTable { /// Every registered authoritative domain table. Append-only: a new domain /// (thinking styles, …) adds one entry and is immediately resolvable. pub fn domain_tables() -> Vec { - vec![DomainTable { - domain: "ocr", - expected_executors: crate::ocr_actions::OCR_EXPECTED_EXECUTORS, - entries: ocr_entries, - }] + vec![ + DomainTable { + domain: "ocr", + expected_executors: crate::ocr_actions::OCR_EXPECTED_EXECUTORS, + entries: ocr_entries, + }, + DomainTable { + domain: "healthcare", + expected_executors: crate::healthcare_actions::HEALTHCARE_EXPECTED_EXECUTORS, + entries: healthcare_entries, + }, + ] } /// Derive a domain's `(capability, subject classid)` join rows GENERICALLY @@ -226,6 +233,13 @@ fn ocr_entries() -> Vec<(String, u16)> { entries_from_actions(&defs) } +/// Healthcare domain rows ([`crate::healthcare_actions`], the medcare-rs +/// table — parity-plan P3), derived through the same generic +/// [`entries_from_actions`] path as OCR. +fn healthcare_entries() -> Vec<(String, u16)> { + entries_from_actions(&crate::healthcare_actions::healthcare_actions()) +} + /// A green hot-plug resolution: the `(concept, classid)` vocab rows and the /// sorted capability names for exactly the hot-plugged ids. pub type HotplugActivation = (Vec<(&'static str, u16)>, Vec); @@ -326,16 +340,47 @@ mod hotplug_tests { assert_eq!(caps.len(), 8); } + /// The parity-plan P3 probe: `resolve_hotplug("medcare-rs", + /// HEALTHCARE_SUBJECT_CLASSIDS, covered)` GREEN (KILL condition was + /// `NoCapabilitiesFor`/`Uncovered`). The Health classids that banged + /// `NoCapabilitiesFor` before this table landed now resolve. + #[test] + fn healthcare_hotplug_resolves_vocab_and_actions() { + let (concepts, caps) = resolve_hotplug( + "medcare-rs", + crate::healthcare_actions::HEALTHCARE_SUBJECT_CLASSIDS, + crate::healthcare_actions::HEALTHCARE_ACTION_NAMES, + ) + .expect("healthcare hot-plug drifted from the authoritative table"); + assert_eq!(concepts.len(), 6, "six subjects (visit excluded)"); + assert!(concepts.contains(&("patient", 0x0901))); + assert!(concepts.contains(&("vital_sign", 0x0907))); + assert_eq!(caps.len(), 20); + // The wrong consumer against the same ids bangs UnexpectedConsumer. + assert!(matches!( + resolve_hotplug( + "tesseract-ogar", + crate::healthcare_actions::HEALTHCARE_SUBJECT_CLASSIDS, + crate::healthcare_actions::HEALTHCARE_ACTION_NAMES, + ), + Err(HotplugDrift::UnexpectedConsumer(_)) + )); + } + #[test] fn each_drift_arm_bangs() { assert!(matches!( resolve_hotplug("tesseract-ogar", &[0xFFFE], &[]), Err(HotplugDrift::UnknownClassid(0xFFFE)) )); - // patient (0x0901) is minted but has no action table yet. + // visit (0x0906) is minted but deliberately table-less — the one + // Health entity the healthcare table excluded for zero harvest + // evidence (`healthcare_actions` module doc). It replaced patient + // (0x0901) as this arm's probe when the healthcare table landed + // and patient gained capabilities. assert!(matches!( - resolve_hotplug("tesseract-ogar", &[0x0901], &[]), - Err(HotplugDrift::NoCapabilitiesFor(0x0901)) + resolve_hotplug("tesseract-ogar", &[crate::class_ids::VISIT], &[]), + Err(HotplugDrift::NoCapabilitiesFor(0x0906)) )); assert!(matches!( resolve_hotplug("stranger", OCR_IDS, OCR_COVERED), diff --git a/crates/ogar-vocab/src/healthcare_actions.rs b/crates/ogar-vocab/src/healthcare_actions.rs new file mode 100644 index 0000000..9669e0f --- /dev/null +++ b/crates/ogar-vocab/src/healthcare_actions.rs @@ -0,0 +1,321 @@ +//! Healthcare capability surface — the **medcare-rs authoritative action +//! table** (`MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md` P3). +//! +//! This module declares the healthcare capabilities the medcare-rs +//! consumer executes, one [`ActionDef`] per capability, each targeting a +//! minted `0x09XX` Health concept ([`crate::class_ids`]) as its +//! `object_class`. It registers in +//! [`crate::capability_registry::domain_tables`] through the generic +//! [`crate::capability_registry::entries_from_actions`] derive — the +//! "config becomes data" seam — so `resolve_hotplug` turns green for the +//! Health classids that previously banged `NoCapabilitiesFor` (the +//! deficiency the parity plan §0 names). +//! +//! # Why hand-authored (harvest-INFORMED, not harvest-DERIVED) +//! +//! The plan's Loop A hoped to auto-derive this table mechanically +//! (Roslyn harvest → `reassemble` → `lift_actions` → +//! `entries_from_actions`). The real corpus falsified that premise +//! (2026-07-07 harvest over the consumer C# tree): the application is a +//! WinForms monolith — **no AR-shaped class per domain concept exists**. +//! The domain concepts (`patient`, `diagnosis`, `lab_value`, …) never +//! appear as classes; the domain surface lives as `verb_concept` methods +//! on one monolithic DAL class plus WinForms UserControls. A class-keyed +//! lift has nothing to key on, so — exactly like [`crate::ocr_actions`]'s +//! "no ActiveRecord source" rationale — the table is declared by hand. +//! +//! The harvest still did its job as **evidence**: each concept's presence +//! and its per-concept capability split (CRUD-shaped: add / get / list / +//! delete arms) follow the measured method inventory in the consumer's DAL +//! (roughly a dozen methods per concept for the busiest, single digits for +//! the rest). The concrete method-name evidence stays in the consumer repo +//! (the harvest artifact is never committed here); a re-harvest that +//! surfaces a new method family is the trigger to extend this table — +//! harvest-informed, with the authorship step explicit instead of +//! pretending to be mechanical. +//! +//! # `visit` (0x0906) is deliberately absent +//! +//! The harvest found **zero** methods touching a visit/encounter concept +//! anywhere in the DAL or UI layer (operator-confirmed exclusion, +//! 2026-07-07). Declaring a capability with no implementation evidence +//! would be fabrication; the concept stays minted but table-less until a +//! consumer actually grows a visit surface. Consequence: +//! [`HEALTHCARE_SUBJECT_CLASSIDS`] has 6 entries, not the port's 7 — and +//! `resolve_hotplug` on `VISIT` still bangs `NoCapabilitiesFor`, which is +//! the honest signal. +//! +//! # Name-only ActionDefs (empty effect facts) — deliberate +//! +//! `reads`/`writes` are left empty: the DO-arm harvest facts on the DAL +//! methods are *internal* effects (SQL-buffer scratch fields), not +//! domain-field effects — copying them here would be noise, and inventing +//! domain-level effect facts would be fabrication. Per +//! [`crate::capability_registry::entries_from_actions`]'s contract +//! (and its `entries_from_actions_ignores_effect_facts` test), the +//! hot-plug join keys on capability name + subject classid alone, so +//! name-only defs are fully valid rows. Effect-fact enrichment is the +//! plan's P6, orthogonal to this table. + +use crate::{ActionDef, ActionSubject, KausalSpec}; + +/// Every healthcare capability name, in table order — the `const`-evaluable +/// fingerprint of [`healthcare_actions`] (same role as +/// [`crate::ocr_actions::OCR_ACTION_NAMES`]). A consumer that wants to +/// assert "I handle every healthcare capability" matches this slice +/// exhaustively without constructing the full table. +pub const HEALTHCARE_ACTION_NAMES: &[&str] = &[ + // patient (0x0901) + "register_patient", + "get_patient_record", + "list_patients", + "update_patient_access", + // diagnosis (0x0902) + "add_diagnosis", + "get_diagnosis", + "list_diagnoses", + "delete_diagnosis", + // lab_value (0x0903) + "add_lab_result", + "get_lab_result", + "list_lab_results", + // medication (0x0904) + "add_medication", + "get_medication", + "list_medications", + // treatment (0x0905) + "add_treatment", + "get_treatment", + "list_treatments", + // vital_sign (0x0907) + "add_vital_sign", + "list_vital_signs", + "delete_vital_signs", +]; + +const _: () = assert!(HEALTHCARE_ACTION_NAMES.len() == 20); + +/// Build one [`ActionDef`] for a healthcare capability. `subject_concept` +/// MUST be a name minted in [`crate::class_ids::ALL`] under the `0x09XX` +/// (Health) domain — enforced by this module's tests, same fuse shape as +/// `ocr_actions::ocr_action_def`. +fn healthcare_action_def(capability: &'static str, subject_concept: &'static str) -> ActionDef { + let object_class = format!("ogit-health/{subject_concept}"); + let identity = format!("{object_class}::action_def::{capability}"); + ActionDef { + identity, + predicate: capability.to_owned(), + object_class, + default_subject: ActionSubject::User, + // Every capability here is invoked by an authenticated caller + // through the medcare-rs axum route surface — an external cause + // (HTTP request) with no OGAR-side precondition to guard on, + // matching `KausalSpec::External`'s doc. + kausal: Some(KausalSpec::External), + ..ActionDef::default() + } +} + +/// The medcare-rs healthcare capability surface — the **authoritative OGAR +/// action table** for the clinical / patient / care domain. One +/// [`ActionDef`] per capability, in [`HEALTHCARE_ACTION_NAMES`] order. +/// +/// The per-concept capability split is CRUD-shaped and follows the +/// consumer DAL's measured method inventory (2026-07-07 harvest over the +/// consumer C# tree; the concrete method-name evidence stays in the +/// consumer repo, never committed here). +/// +/// | subject concept | capabilities | +/// |---|---| +/// | `patient` (`0x0901`) | `register_patient` / `get_patient_record` / `list_patients` / `update_patient_access` | +/// | `diagnosis` (`0x0902`) | `add_diagnosis` / `get_diagnosis` / `list_diagnoses` / `delete_diagnosis` | +/// | `lab_value` (`0x0903`) | `add_lab_result` / `get_lab_result` / `list_lab_results` | +/// | `medication` (`0x0904`) | `add_medication` / `get_medication` / `list_medications` | +/// | `treatment` (`0x0905`) | `add_treatment` / `get_treatment` / `list_treatments` | +/// | `vital_sign` (`0x0907`) | `add_vital_sign` / `list_vital_signs` / `delete_vital_signs` | +#[must_use] +pub fn healthcare_actions() -> Vec { + const SUBJECT_OF: &[(&str, &str)] = &[ + ("register_patient", "patient"), + ("get_patient_record", "patient"), + ("list_patients", "patient"), + ("update_patient_access", "patient"), + ("add_diagnosis", "diagnosis"), + ("get_diagnosis", "diagnosis"), + ("list_diagnoses", "diagnosis"), + ("delete_diagnosis", "diagnosis"), + ("add_lab_result", "lab_value"), + ("get_lab_result", "lab_value"), + ("list_lab_results", "lab_value"), + ("add_medication", "medication"), + ("get_medication", "medication"), + ("list_medications", "medication"), + ("add_treatment", "treatment"), + ("get_treatment", "treatment"), + ("list_treatments", "treatment"), + ("add_vital_sign", "vital_sign"), + ("list_vital_signs", "vital_sign"), + ("delete_vital_signs", "vital_sign"), + ]; + SUBJECT_OF + .iter() + .map(|&(capability, subject)| healthcare_action_def(capability, subject)) + .collect() +} + +/// The executors the authority EXPECTS to register against this table. +/// medcare-rs is the Rust consumer (P4 of the parity plan wires its +/// `HOT_PLUG` const + activation test against exactly this list). +pub const HEALTHCARE_EXPECTED_EXECUTORS: &[&str] = &["medcare-rs"]; + +/// The distinct subject classids this table binds (canon-high concept +/// ids). Six of the seven `HealthcarePort` entities — `visit` (0x0906) is +/// deliberately absent (zero harvest evidence; see the module doc). A +/// registering consumer must activate exactly this set. +pub const HEALTHCARE_SUBJECT_CLASSIDS: &[u16] = &[ + crate::class_ids::PATIENT, + crate::class_ids::DIAGNOSIS, + crate::class_ids::LAB_VALUE, + crate::class_ids::MEDICATION, + crate::class_ids::TREATMENT, + crate::class_ids::VITAL_SIGN, +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::{canonical_concept_domain, class_ids}; + use std::collections::BTreeSet; + + fn subject_concept_of(def: &ActionDef) -> &str { + def.object_class + .strip_prefix("ogit-health/") + .expect("every healthcare ActionDef.object_class starts with `ogit-health/`") + } + + #[test] + fn table_length_matches_const_name_fingerprint() { + let actions = healthcare_actions(); + assert_eq!(actions.len(), HEALTHCARE_ACTION_NAMES.len()); + for (def, name) in actions.iter().zip(HEALTHCARE_ACTION_NAMES) { + assert_eq!(&def.predicate, name); + } + } + + #[test] + fn capability_names_are_unique() { + let actions = healthcare_actions(); + let names: BTreeSet<&str> = actions.iter().map(|d| d.predicate.as_str()).collect(); + assert_eq!( + names.len(), + actions.len(), + "duplicate capability name in HEALTHCARE_ACTION_NAMES / healthcare_actions()" + ); + } + + /// Every action's subject resolves to a minted `0x09XX` concept in + /// [`class_ids::ALL`] — the fuse that catches a renamed or unminted + /// concept before a consumer trusts this table. + #[test] + fn subjects_resolve_to_minted_0x09_health_concepts() { + for def in healthcare_actions() { + let concept = subject_concept_of(&def); + let entry = class_ids::ALL + .iter() + .find(|(name, _)| *name == concept) + .unwrap_or_else(|| { + panic!( + "{}: subject concept `{concept}` is not in class_ids::ALL", + def.predicate + ) + }); + assert_eq!( + canonical_concept_domain(entry.1), + crate::ConceptDomain::Health, + "{}: subject concept `{concept}` (0x{:04X}) is not in the Health domain", + def.predicate, + entry.1 + ); + } + } + + /// The distinct subject set of the table equals + /// [`HEALTHCARE_SUBJECT_CLASSIDS`] — no phantom subject const, no + /// subject the const forgot. + #[test] + fn subject_classids_const_matches_table() { + let mut from_table: Vec = healthcare_actions() + .iter() + .map(|def| { + crate::canonical_concept_id(subject_concept_of(def)) + .expect("minted (previous test)") + }) + .collect(); + from_table.sort_unstable(); + from_table.dedup(); + let mut from_const = HEALTHCARE_SUBJECT_CLASSIDS.to_vec(); + from_const.sort_unstable(); + assert_eq!(from_table, from_const); + } + + /// `visit` (0x0906) is minted in the codebook but deliberately absent + /// from this table (zero harvest evidence — module doc). This pin + /// keeps the exclusion a visible decision instead of silent drift: if + /// a visit capability ever lands, this test fails and the author + /// updates both the table and the exclusion note. + #[test] + fn visit_is_minted_but_deliberately_tableless() { + assert!( + class_ids::ALL + .iter() + .any(|&(name, id)| name == "visit" && id == class_ids::VISIT), + "visit must stay minted in the codebook" + ); + assert!( + !HEALTHCARE_SUBJECT_CLASSIDS.contains(&class_ids::VISIT), + "visit gained a table row — update the module-doc exclusion note" + ); + for def in healthcare_actions() { + assert_ne!( + subject_concept_of(&def), + "visit", + "{}: visit capability landed without updating the exclusion note", + def.predicate + ); + } + } + + #[test] + fn every_action_declares_external_kausal_and_user_subject() { + for def in healthcare_actions() { + assert_eq!(def.kausal, Some(KausalSpec::External)); + assert_eq!(def.default_subject, ActionSubject::User); + } + } + + /// Name-only by design (module doc): the hot-plug join must not + /// depend on effect facts, and this table doesn't fabricate any. + #[test] + fn actions_are_name_only() { + for def in healthcare_actions() { + assert!(def.reads.is_empty(), "{}: unexpected reads", def.predicate); + assert!( + def.writes.is_empty(), + "{}: unexpected writes", + def.predicate + ); + } + } + + #[test] + fn identity_and_object_class_are_well_formed() { + for def in healthcare_actions() { + assert!(def.identity.starts_with("ogit-health/")); + assert!(def.object_class.starts_with("ogit-health/")); + assert!( + def.identity + .ends_with(&format!("::action_def::{}", def.predicate)) + ); + } + } +} diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 4a09a18..7f96c62 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -42,6 +42,10 @@ pub mod recipe; /// `render_tsv` / `render_hocr` / `render_searchable_pdf`, each targeting a /// minted `0x08XX` [`class_ids`] concept. pub mod capability_registry; +/// Healthcare capability surface — the medcare-rs authoritative action +/// table (parity-plan P3; hand-authored, harvest-informed — see the +/// module doc for why the mechanical lift was falsified by the corpus). +pub mod healthcare_actions; pub mod ocr_actions; /// Source language hint — discriminates the producer for traceability diff --git a/docs/integration/MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md b/docs/integration/MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md index f8116a6..966c4fc 100644 --- a/docs/integration/MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md +++ b/docs/integration/MEDCARE-CSHARP-PARITY-INTEGRATION-PLAN.md @@ -1,7 +1,25 @@ # MedCare ⇄ OGAR C# Parity — Future Integration Plan -> **Status:** PROPOSAL (2026-07-07). Not shipped; gated on operator -> green-light per OGAR probe-first discipline (`docs/INTEGRATION-TEST-PLAN.md` +> **Status:** P3 + P4 SHIPPED (2026-07-07, this branch); P1/P2/P5/P6 +> still gated on operator. `healthcare_actions.rs` (P3) + `medcare-rbac` +> `hotplug.rs` (P4) are green (`resolve_hotplug("medcare-rs", …)` resolves +> the 6 Health classids; probe `healthcare_hotplug_resolves_vocab_and_actions`). +> +> **Loop A premise correction (2026-07-07 harvest).** The plan's Loop A +> hoped to auto-derive the table mechanically (Roslyn harvest → +> `reassemble` → `lift_actions` → `entries_from_actions`). The real corpus +> **falsified that**: MedCare is a WinForms monolith — no AR-shaped class +> per domain concept exists (the concepts never appear as classes; the +> domain surface is `verb_concept` methods on one monolithic DAL class). +> So P3 is **hand-authored, harvest-INFORMED** (the capability split +> follows the measured DAL method inventory, evidence kept in the consumer +> repo), exactly like +> `ocr_actions.rs`'s "no ActiveRecord source" rationale — NOT the +> mechanical lift. `visit` (0x0906) excluded: zero harvest evidence +> (operator-confirmed). Six subjects, not seven. +> +> PROPOSAL for the remaining phases; gated on operator green-light per +> OGAR probe-first discipline (`docs/INTEGRATION-TEST-PLAN.md` > — no integration brick lands before its probe is green). > > **READ WITH:** `.claude/knowledge/hotplug-consumer-migration.md` From 183d6de58c999d3c6888f9ba1110b23836a4677a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 22:23:35 +0000 Subject: [PATCH 3/9] ogar-vocab: pin codebook-DTO bijectivity for the healthcare table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards the Boundary-4 failure mode (a green codebook-DTO check that does NOT predict nonzero registration because the check's case-folding concept_key normalization and the registration derive's exact-match canonical_concept_id disagree). codebook_dto_derive_is_bijective asserts three properties over healthcare_actions(): 1. forward total + nonzero — entries_from_actions yields a nonzero id for every row (no classid-0 sentinel escapes, the exact Boundary-4 symptom); 2. inverse round-trip — each derived id reverses via canonical_concept_name to exactly its object_class segment (concept->id->concept bijection); 3. concept_key fixed-point — every segment is already canonical snake_case (no uppercase, no ':' namespace), the condition under which the check's concept_key is the identity, so case-folding check and exact-match derive resolve this table's names identically. ogar-vocab has no ruff_spo_triplet dep, so concept_key is asserted via its fixed-point condition rather than called directly. 14 healthcare tests green. --- crates/ogar-vocab/src/healthcare_actions.rs | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/ogar-vocab/src/healthcare_actions.rs b/crates/ogar-vocab/src/healthcare_actions.rs index 9669e0f..464a567 100644 --- a/crates/ogar-vocab/src/healthcare_actions.rs +++ b/crates/ogar-vocab/src/healthcare_actions.rs @@ -285,6 +285,68 @@ mod tests { } } + /// **Bijectivity through the codebook-DTO derive** (guards the + /// Boundary-4 failure mode: a green codebook check that does NOT + /// predict nonzero registration because three concept-normalizations + /// disagree). + /// + /// Three properties, together proving concept↔classid is a clean + /// bijection for this table AND that the codebook-DTO *check* + /// (`ruff_spo_triplet::concept_key`, case-folding) cannot diverge from + /// the registration *derive* (`entries_from_actions` → + /// `canonical_concept_id`, exact-match): + /// + /// 1. **Forward total + nonzero** — every derived row resolves to a + /// nonzero id (no `0` default-class sentinel escapes: the exact + /// Boundary-4 symptom where `entries_from_actions` silently yields + /// `("cap", 0)` for a name it can't exact-match). + /// 2. **Inverse round-trip** — each distinct derived id reverses via + /// `canonical_concept_name` back to exactly the `object_class` + /// segment that produced it, so concept→id→concept is injective + /// both ways over the table's concepts. + /// 3. **`concept_key` fixed-point** — every `object_class` segment is + /// already canonical `snake_case` (equals its own ASCII-lowercase, + /// carries no `:` namespace), which is precisely the condition + /// under which the codebook-DTO check's `concept_key` normalization + /// is the identity. So the case-folding check and the exact-match + /// derive resolve this table's names identically — a green check + /// here provably predicts the same nonzero registration. + #[test] + fn codebook_dto_derive_is_bijective() { + let defs = healthcare_actions(); + let rows = crate::capability_registry::entries_from_actions(&defs); + + // (1) forward total + nonzero — no classid-0 sentinel escapes. + for (cap, id) in &rows { + assert_ne!( + *id, 0, + "`{cap}` derived classid 0 — the Boundary-4 exact-match miss" + ); + } + + for def in &defs { + let seg = subject_concept_of(def); + + // (3) concept_key fixed-point: canonical snake_case, no ':'. + assert!( + !seg.contains(':') && seg.chars().all(|c| !c.is_ascii_uppercase()), + "`{seg}` is not a concept_key fixed-point — the check's \ + case-fold could diverge from the exact-match derive" + ); + + // (1)+(2) forward exact-match to a nonzero id, then inverse + // round-trips to the same segment (bijection). + let id = crate::canonical_concept_id(seg) + .unwrap_or_else(|| panic!("`{seg}` must forward-resolve")); + assert_ne!(id, 0); + assert_eq!( + crate::canonical_concept_name(id), + Some(seg), + "id 0x{id:04X} must reverse to exactly `{seg}`" + ); + } + } + #[test] fn every_action_declares_external_kausal_and_user_subject() { for def in healthcare_actions() { From ed109240476f0a18fd391aeeadde4c139420aa60 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 20:13:31 +0000 Subject: [PATCH 4/9] =?UTF-8?q?ogar-vocab:=20residual-aware=20capability?= =?UTF-8?q?=20derive=20=E2=80=94=20the=20slag=20ledger=20(doctrine=20Phase?= =?UTF-8?q?=206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit entries_from_actions_with_residuals: resolved rows carry their nonzero classid; an unresolvable concept lands as an UnmintedRow (capability, object_class, concept) on the slag ledger instead of degrading to the id-0 sentinel. The residual is not waste — it is the empirical boundary of the current codebook/convention; each recurring row names the next config fact (a mint, an alias row, or a convention fix). "Study the slag, teach the furnace." entries_from_actions is now the LEGACY id-0-sentinel projection of the same single-pass ordered derive (I-LEGACY-API-FEATURE-GATED: the existing name keeps byte-identical semantics — pinned by the new interleaved-order parity test; new semantics got a new name). Healthcare table through the residual derive: zero slag (mirrors the codebook_dto_derive_is_bijective pin). 135 ogar-vocab tests green; clippy -D warnings clean. --- crates/ogar-vocab/src/capability_registry.rs | 161 ++++++++++++++++++- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index 2d120d3..a82a960 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -184,6 +184,79 @@ pub fn domain_tables() -> Vec { ] } +/// One slag-ledger row: an ActionDef whose subject concept the codebook +/// could not mint. NOT waste — the empirical boundary of the current +/// codebook/convention: each recurring residual names the next config fact +/// (a mint, an alias row, or a convention fix). (MedCare transcode +/// doctrine: "study the slag, teach the furnace".) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnmintedRow { + /// The capability name (ActionDef.predicate). + pub capability: String, + /// The full object_class the def carried. + pub object_class: String, + /// The concept segment that failed to resolve (last '/'-segment). + pub concept: String, +} + +/// One row of the single-pass action walk shared by +/// [`entries_from_actions`] and [`entries_from_actions_with_residuals`] — +/// private so both public functions project the SAME walk instead of +/// re-deriving it and risking drift between them. +enum ActionRow { + /// A resolved `(capability, subject classid)` join row. + Resolved(String, u16), + /// A row whose subject concept the codebook could not mint. + Unminted(UnmintedRow), +} + +/// Walk `actions` exactly once, in order, resolving each subject classid +/// from [`crate::ActionDef::object_class`] (`/` — the +/// concept is the last `/`-segment, so any prefix works: `ogit-ocr/…`, +/// `medcare:…/…`, …) via [`crate::canonical_concept_id`]. Effect-fact +/// fields on the `ActionDef` (`reads`/`writes`/`calls`/`raises`) are +/// irrelevant to the join — the hot-plug keys on the capability NAME +/// (`predicate`) and the subject classid alone, so a name-only `ActionDef` +/// (e.g. a C#-Roslyn lift before the harvester's method-body walk lands) +/// derives a valid row. +fn derive_action_rows(actions: &[crate::ActionDef]) -> Vec { + actions + .iter() + .map(|def| { + let concept = def.object_class.rsplit('/').next().unwrap_or_default(); + match crate::canonical_concept_id(concept) { + Some(id) => ActionRow::Resolved(def.predicate.clone(), id), + None => ActionRow::Unminted(UnmintedRow { + capability: def.predicate.clone(), + object_class: def.object_class.clone(), + concept: concept.to_string(), + }), + } + }) + .collect() +} + +/// Residual-aware twin of [`entries_from_actions`]: resolved rows carry +/// their nonzero classid; unresolvable concepts land in the slag ledger +/// instead of degrading to the id-0 sentinel. New consumers (the +/// transcode-doctrine Phase 6 exam) use THIS; the id-0 sentinel path +/// below stays for existing fuse consumers (I-LEGACY-API-FEATURE-GATED: +/// same name keeps same semantics; new semantics get a new name). +#[must_use] +pub fn entries_from_actions_with_residuals( + actions: &[crate::ActionDef], +) -> (Vec<(String, u16)>, Vec) { + let mut resolved = Vec::new(); + let mut residuals = Vec::new(); + for row in derive_action_rows(actions) { + match row { + ActionRow::Resolved(capability, id) => resolved.push((capability, id)), + ActionRow::Unminted(residual) => residuals.push(residual), + } + } + (resolved, residuals) +} + /// Derive a domain's `(capability, subject classid)` join rows GENERICALLY /// from any `[ActionDef]` — the output of `ogar-from-ruff::lift_actions` /// for ANY language frontend (Ruby/Rails, Python/Odoo, C#/Roslyn, @@ -210,14 +283,23 @@ pub fn domain_tables() -> Vec { /// name-only `ActionDef` (e.g. a C#-Roslyn lift before the harvester's /// method-body walk lands) derives a valid row; the effect facts are /// enrichment for projection/kausal/RBAC, not join inputs. +/// +/// **LEGACY id-0-sentinel projection.** This function is now the id-0 +/// projection of [`entries_from_actions_with_residuals`]: same single +/// walk ([`derive_action_rows`]), same original order, but an unminted +/// concept folds to `(capability, 0)` here instead of surfacing as an +/// [`UnmintedRow`] in a slag ledger. Existing fuse consumers +/// (`resolve_hotplug` / `verify_registration`) key on the id-0 sentinel +/// and keep calling this function; new consumers wanting the residual +/// ledger (no unwrap-or-default concept-id fallback — doctrine Phase 6) +/// should call `entries_from_actions_with_residuals` directly. #[must_use] pub fn entries_from_actions(actions: &[crate::ActionDef]) -> Vec<(String, u16)> { - actions - .iter() - .map(|def| { - let concept = def.object_class.rsplit('/').next().unwrap_or_default(); - let id = crate::canonical_concept_id(concept).unwrap_or_default(); - (def.predicate.clone(), id) + derive_action_rows(actions) + .into_iter() + .map(|row| match row { + ActionRow::Resolved(capability, id) => (capability, id), + ActionRow::Unminted(residual) => (residual.capability, 0), }) .collect() } @@ -447,6 +529,73 @@ mod hotplug_tests { assert_eq!(entries_from_actions(&[a]), vec![("bar".to_string(), 0)]); } + /// The residual-aware derive: minted rows resolve to nonzero classids + /// (no id-0 escapes into the resolved vec), in original order; + /// unminted concepts land in the slag ledger carrying enough to name + /// the next config fact — never silently dropped, never degraded to + /// a fallback id. + #[test] + fn entries_from_actions_with_residuals_splits_minted_and_slag() { + use crate::ActionDef; + let minted_a = ActionDef::new("a", "recognize_line", "ogit-ocr/textline"); + let unminted = ActionDef::new("z", "bar", "any:ns/totally_not_a_concept"); + let minted_b = ActionDef::new("b", "render_hocr", "ogit-ocr/ocr_renderer"); + let (resolved, residuals) = + entries_from_actions_with_residuals(&[minted_a, unminted, minted_b]); + + assert_eq!( + resolved, + vec![ + ("recognize_line".to_string(), crate::class_ids::TEXTLINE), + ("render_hocr".to_string(), crate::class_ids::OCR_RENDERER), + ] + ); + assert!( + resolved.iter().all(|&(_, id)| id != 0), + "no id-0 escapes into resolved rows" + ); + + assert_eq!(residuals.len(), 1); + assert_eq!(residuals[0].capability, "bar"); + assert_eq!(residuals[0].object_class, "any:ns/totally_not_a_concept"); + assert_eq!(residuals[0].concept, "totally_not_a_concept"); + } + + /// Parity pin: `entries_from_actions` is exactly the interleaved + /// (resolved ++ residual-as-0) projection of the residual-aware + /// derive, in the ORIGINAL action order — the legacy id-0 sentinel + /// behavior is unchanged by the residual-aware rewrite. + #[test] + fn entries_from_actions_matches_residual_derive_interleaved() { + use crate::ActionDef; + let minted_a = ActionDef::new("a", "recognize_line", "ogit-ocr/textline"); + let unminted = ActionDef::new("z", "bar", "any:ns/totally_not_a_concept"); + let minted_b = ActionDef::new("b", "render_hocr", "ogit-ocr/ocr_renderer"); + let actions = [minted_a, unminted, minted_b]; + + let expected = vec![ + ("recognize_line".to_string(), crate::class_ids::TEXTLINE), + ("bar".to_string(), 0), + ("render_hocr".to_string(), crate::class_ids::OCR_RENDERER), + ]; + assert_eq!(entries_from_actions(&actions), expected); + } + + /// The healthcare table's bijectivity pin (module doc, + /// `codebook_dto_derive_is_bijective`), mirrored through the + /// residual-aware derive: zero slag rows — every subject concept is + /// minted, so nothing degrades and nothing lands in `UnmintedRow`. + #[test] + fn healthcare_entries_with_residuals_has_zero_slag() { + let (resolved, residuals) = + entries_from_actions_with_residuals(&crate::healthcare_actions::healthcare_actions()); + assert!(residuals.is_empty(), "unexpected slag: {residuals:?}"); + assert_eq!( + resolved.len(), + crate::healthcare_actions::HEALTHCARE_ACTION_NAMES.len() + ); + } + /// The hand-authored OCR table, routed through the generic derive, is /// byte-for-byte what it was before the refactor — the "config becomes /// data" seam subsumes the bespoke path with zero behavior change. From b0ac061ac8dadafca49275534ef6bbb25a359996 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 07:42:12 +0000 Subject: [PATCH 5/9] =?UTF-8?q?ogar-from-ruff:=20lift=5Factions=5Fwith=5Fc?= =?UTF-8?q?onvention=20=E2=80=94=20Phase=201=20rekey=20wired=20into=20the?= =?UTF-8?q?=20lift=20(doctrine=20Phase=206=20green)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The production half of the CONCEPT = CLASS fix: lift_actions keys every ActionDef.object_class on the owning class — correct for AR-shaped corpora, the give-up point for God-object DALs. lift_actions_with_convention runs ruff_spo_triplet::rekey_model (ruff #72) over the model with the corpus-owner convention and re-subjects each ActionDef to its inferred concept (concept_key-fixed-point, so the codebook exact-match resolves — Boundary-4); unsplit methods keep the class subject and land on the slag ledger via entries_from_actions_with_residuals, never silent id 0. Effect/kausal enrichment delegates to lift_actions, so the two paths cannot drift. The OGAR-side furnace exam (god_object_rekey_lift_derives_nonzero_concepts_ with_slag) pins the doctrine's Phase 6 success criteria on a neutral God-object fixture: split methods nonzero, unsplittable method a NAMED residual. Run against the real corpus through the full chain (harvest -> reassemble -> rekey -> lift -> entries_with_residuals): all six healthcare concepts derived nonzero (23/4/20/11/10/11 capabilities), 2668 residuals ledgered, zero id-0 escapes. Builds against ruff_spo_triplet post-#72 main (Cargo.lock is gitignored here; the branch=main git dep re-resolves on fresh checkouts). 78 + 135 tests green; clippy -D warnings clean. Known ledgered finding: the derive is domain-blind — one real-corpus residue cross-matched an OCR-domain mint; a domain-gate convention row is the named next config fact. --- crates/ogar-from-ruff/src/lib.rs | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/crates/ogar-from-ruff/src/lib.rs b/crates/ogar-from-ruff/src/lib.rs index e941b5e..2ab6e63 100644 --- a/crates/ogar-from-ruff/src/lib.rs +++ b/crates/ogar-from-ruff/src/lib.rs @@ -635,6 +635,53 @@ pub fn lift_actions(model: &Model) -> Vec { .collect() } +/// Re-keyed DO-arm lift for a God-object model (MedCare transcode doctrine +/// Phase 1 wired into the lift — the CONCEPT = CLASS fix, production side). +/// +/// [`lift_actions`] keys every [`ActionDef::object_class`] on the OWNING +/// CLASS name — correct for AR-shaped corpora (one class per concept), and +/// exactly the give-up point for a God-object DAL, where hundreds of +/// `verb_concept` methods share one class and +/// `entries_from_actions` resolves classid 0 for every row. This variant +/// runs `ruff_spo_triplet::concept_split::rekey_model` over the model with +/// the corpus-owner convention (data-as-config) and re-subjects each +/// ActionDef: +/// +/// - a method that SPLITS gets `object_class = ` (already +/// `concept_key`-fixed-point, so the codebook exact-match resolves it — +/// Boundary-4) and `predicate = _` is NOT +/// invented: the predicate stays the source method name (capability +/// identity is the name; renaming is a registration concern); +/// - a method that refuses to split keeps the class subject and lands on +/// the caller's slag ledger via +/// `ogar_vocab::capability_registry::entries_from_actions_with_residuals` +/// (never silent id 0). +/// +/// The effect/kausal enrichment is IDENTICAL to [`lift_actions`] — this +/// wrapper only re-keys the subject, so the two paths cannot drift. +#[must_use] +pub fn lift_actions_with_convention( + model: &Model, + conv: &ruff_spo_triplet::ConceptConvention, +) -> Vec { + let outcome = ruff_spo_triplet::rekey_model(model, conv); + let concept_of: HashMap<&str, &str> = outcome + .keyed + .iter() + .map(|(name, split)| (name.as_str(), split.concept.as_str())) + .collect(); + lift_actions(model) + .into_iter() + .map(|mut a| { + if let Some(concept) = concept_of.get(a.predicate.as_str()) { + a.object_class = (*concept).to_string(); + a.identity = format!("{}::action_def::{}", concept, a.predicate); + } + a + }) + .collect() +} + // ───────────────────────────── associations ────────────────────────────── /// Lift one [`AssocDecl`] to an OGAR [`Association`]. @@ -2223,4 +2270,74 @@ mod tests { assert_eq!(hook.writes, vec!["closed_on".to_string()]); assert_eq!(hook.reads, vec!["updated_on".to_string()]); } + /// **The OGAR-side furnace exam** (MedCare transcode doctrine Phase 6): + /// a God-object model — one class, `verb_concept` methods — flows + /// through `rekey_model → lift_actions_with_convention → + /// entries_from_actions_with_residuals` and re-derives its concepts + /// with NONZERO classids, while the unsplittable method lands on the + /// slag ledger instead of degrading to the id-0 sentinel. Green means + /// a hand-authored domain table for such a corpus is only an + /// unautomated config read. (The ruff-side twin — real-corpus + /// harvest → reassemble → rekey → codebook check — lives in + /// `ruff_spo_triplet`'s `rekey_exam` example; this is the OGAR half + /// the doctrine's success criteria name: "no unwrap_or_default + /// concept-id fallback; every unresolved row becomes a residual DTO".) + #[test] + fn god_object_rekey_lift_derives_nonzero_concepts_with_slag() { + use ruff_spo_triplet::ConceptConvention; + + // A God-object DAL: every concept lives in the method name. The + // concept names are public codebook mints (`patient` 0x0901, + // `diagnosis` 0x0902); the method/class shapes are neutral. + let mut m = Model::new("Database"); + for name in ["add_iv_patient", "get_list_patient", "add_iv_diagnosis"] { + m.functions.push(Function { + name: name.to_string(), + ..Default::default() + }); + } + // The slag: no verb prefix — refuses to split, keeps the class key. + m.functions.push(Function { + name: "InitializeComponent".to_string(), + ..Default::default() + }); + + let conv = ConceptConvention { + verbs: vec![ + ("add".into(), "create".into()), + ("get".into(), "read".into()), + ], + scopes: vec!["iv".into(), "list".into()], + concept_aliases: vec![], + }; + + let defs = lift_actions_with_convention(&m, &conv); + assert_eq!(defs.len(), 4); + let (rows, residuals) = + ogar_vocab::capability_registry::entries_from_actions_with_residuals(&defs); + + // Split methods resolve their concept NONZERO — never id 0. + assert_eq!(rows.len(), 3); + for (cap, id) in &rows { + assert_ne!(*id, 0, "`{cap}` must resolve nonzero"); + } + assert!( + rows.iter() + .any(|(c, id)| c == "add_iv_patient" && *id == 0x0901) + ); + assert!( + rows.iter() + .any(|(c, id)| c == "get_list_patient" && *id == 0x0901) + ); + assert!( + rows.iter() + .any(|(c, id)| c == "add_iv_diagnosis" && *id == 0x0902) + ); + + // The unsplittable method is SLAG — a named residual carrying the + // class subject, not a silent zero. + assert_eq!(residuals.len(), 1); + assert_eq!(residuals[0].capability, "InitializeComponent"); + assert_eq!(residuals[0].concept, "Database"); + } } From 2c8836f6716b1523a88a89d0f95404d5a2f79976 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 12:11:29 +0000 Subject: [PATCH 6/9] ogar-vocab: mint 4 harvest-derived Health concepts (0x0908..0x090B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 furnace-exam mints through the engineer's gate, hardened by a staged 5+3 council (5 savants -> consolidate -> 3 brutal reviewers). All four are grounded in the transcode furnace exam's slag ledger (concept-shaped unbound residues on the real corpus): anamnesis (0x0908, English synonym medical_history), investigation (0x0909), examination (0x090A, parallel grounding OGIT Healthcare:Assessment — see the assessment-drift note in ports.rs), practitioner (0x090B, the clinical actor; NOT staff/license admin, NOT HR employment). Deliberately name-level (council anti-fabrication pin): each builder carries exactly one structural attribute + the patient edge where the record lives in the patient file; clinical schemas arrive from the harvest via the production lift, never by hand. A new test pins the attribute counts. No OGIT entity yet -> no port alias; ports.rs pin stays 7 with the drift note. COUNT_FUSE 84->88 (lance-graph mirror lands paired). concepts_in_domain(Health) 7->11 across doctest + domain-loop + enumeration tests; ogar-class-view registers the 4 builders (empty-ObjectView guard extended to all 11). Pre-existing, unrelated, unchanged: ogar-adapter-csharp parity test (executor-list drift) and ogar-from-elixir unused-import warnings. Co-Authored-By: Claude --- crates/ogar-class-view/src/lib.rs | 23 +++- crates/ogar-vocab/src/lib.rs | 188 +++++++++++++++++++++++++++--- crates/ogar-vocab/src/ports.rs | 9 +- 3 files changed, 202 insertions(+), 18 deletions(-) diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index d122093..245db73 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -69,6 +69,7 @@ use ogar_vocab::{ // 0x0CXX — automation (HIRO MARS CMDB + DO-arm actuators) action_applicability, action_handler, + anamnesis, anatomical_structure, auth_ory_keto, auth_store, @@ -85,10 +86,12 @@ use ogar_vocab::{ commercial_line_item, currency_policy, diagnosis, + examination, hr_department, hr_employee, hr_employment_contract, hr_job, + investigation, joint, knowledge_item, lab_value, @@ -114,6 +117,7 @@ use ogar_vocab::{ page_layout, patient, payment_record, + practitioner, pricelist, pricelist_rule, priority, @@ -210,7 +214,7 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("page_layout", page_layout()), ("page_image", page_image()), ("ocr_renderer", ocr_renderer()), - // ── 0x09XX — health (OGIT Healthcare) ── + // ── 0x09XX — health (7 OGIT Healthcare + 4 harvest-derived mints) ── ("patient", patient()), ("diagnosis", diagnosis()), ("lab_value", lab_value()), @@ -218,6 +222,10 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("treatment", treatment()), ("visit", visit()), ("vital_sign", vital_sign()), + ("anamnesis", anamnesis()), + ("investigation", investigation()), + ("examination", examination()), + ("practitioner", practitioner()), // ── 0x0AXX — anatomy (FMA reference kinds) ── ("anatomical_structure", anatomical_structure()), ("skeleton", skeleton()), @@ -596,9 +604,12 @@ mod tests { #[test] fn health_concepts_are_registered_with_their_fields() { - // The 7 OGIT Healthcare concepts resolve to registry entries — - // this is what makes `every_codebook_id_appears_in_class_ids_all` - // green for the 0x09XX block. + // All 11 Health concepts (7 OGIT + 4 harvest-derived mints) + // resolve to registry entries — this is what makes + // `every_codebook_id_appears_in_class_ids_all` green for the + // 0x09XX block. The `!fields.is_empty()` assertion is the + // empty-ObjectView guard: even the name-level mints must carry + // at least one field. let v = OgarClassView::new(); for concept in [ "patient", @@ -608,6 +619,10 @@ mod tests { "treatment", "visit", "vital_sign", + "anamnesis", + "investigation", + "examination", + "practitioner", ] { let id = canonical_concept_id(concept).unwrap(); let view = v diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 7f96c62..890059d 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1098,7 +1098,7 @@ impl Class { /// 0x06XX unassigned /// 0x07XX reserved: OSINT /// 0x08XX OCR (container kinds: unicharset/recoder/charset) -/// 0x09XX Health (clinical / patient / care; 7 OGIT entities) +/// 0x09XX Health (clinical / patient / care; 7 OGIT entities + 4 harvest-derived mints) /// 0x0AXX Anatomy (FMA reference ontology; bones/skeleton) /// 0x0BXX Auth (IAM; the AuthStore class family) /// 0x0CXX Automation (HIRO IT-automation: MARS CMDB + actuators) @@ -1226,14 +1226,19 @@ const CODEBOOK: &[(&str, u16)] = &[ ("page_image", 0x0808), ("ocr_renderer", 0x0809), // ── 0x09XX — Health domain (clinical / patient / care) ── - // medcare-rs Healthcare-namespace promotion (Northstar T9). The 7 - // entities the OGIT `NTO/Healthcare/entities/` TTL ships, projected - // onto canonical Health ids so `ports::HealthcarePort` resolves them + // 11 concepts, two provenance classes. 0x0901..0x0907: the 7 entities + // the OGIT `NTO/Healthcare/entities/` TTL ships (medcare-rs + // Healthcare-namespace promotion, Northstar T9), projected onto + // canonical Health ids so `ports::HealthcarePort` resolves them // through the `UnifiedBridge` codebook path (the same way OpenProject // `WorkPackage` and Redmine `Issue` resolve through their ports). - // Single-tenant today — no cross-curator convergence yet — but the - // ids are minted into the shared codebook so a future second clinical - // curator (FMA / SNOMED import) converges here rather than re-mints. + // 0x0908..0x090B: harvest-derived mints — surfaced by the transcode + // furnace exam's slag ledger (concept-shaped unbound residues), minted + // through the engineer's gate; no OGIT TTL entity yet, so they carry + // no port alias until one exists. Single-tenant today — no + // cross-curator convergence yet — but the ids are minted into the + // shared codebook so a future second clinical curator (FMA / SNOMED + // import) converges here rather than re-mints. ("patient", 0x0901), ("diagnosis", 0x0902), ("lab_value", 0x0903), @@ -1241,6 +1246,10 @@ const CODEBOOK: &[(&str, u16)] = &[ ("treatment", 0x0905), ("visit", 0x0906), ("vital_sign", 0x0907), + ("anamnesis", 0x0908), + ("investigation", 0x0909), + ("examination", 0x090A), + ("practitioner", 0x090B), // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── // The public anatomical reference frame consumed by the splat-native // ultrasound arc (`docs/SPLAT-NATIVE-CUSTOMER.md` §6 litmus) and the FMA @@ -1449,7 +1458,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { /// .map(|(name, _id)| name) /// .collect(); /// assert!(health.contains(&"patient")); -/// assert_eq!(health.len(), 7); // the 7 OGIT Healthcare entities +/// assert_eq!(health.len(), 11); // 7 OGIT Healthcare entities + 4 harvest-derived mints /// ``` pub fn concepts_in_domain(domain: ConceptDomain) -> impl Iterator { CODEBOOK @@ -1743,6 +1752,30 @@ pub mod class_ids { /// `vital_sign` (`0x0907`) — a measured vital. OGIT /// `Healthcare:VitalSign`. pub const VITAL_SIGN: u16 = 0x0907; + /// `anamnesis` (`0x0908`) — a patient's recorded history-taking + /// (the discoverable English synonym is `medical_history`; SNOMED + /// "history taking"). Harvest-derived mint (furnace-exam slag, + /// round 2); no OGIT entity — no port alias until one exists. + pub const ANAMNESIS: u16 = 0x0908; + /// `investigation` (`0x0909`) — an ordered clinical workup / + /// investigation record (FHIR ServiceRequest/DiagnosticReport- + /// adjacent). Harvest-derived mint (furnace-exam slag, round 2); + /// no OGIT entity — no port alias until one exists. + pub const INVESTIGATION: u16 = 0x0909; + /// `examination` (`0x090A`) — a performed examination record, + /// distinct from `investigation` (performed exam vs ordered + /// workup). Parallel grounding: OGIT `Healthcare:Assessment` + /// (upstream 2026-07-06, not yet lifted into this codebook — see + /// the assessment-drift follow-up before minting `assessment`). + /// Harvest-derived mint (furnace-exam slag, round 2). + pub const EXAMINATION: u16 = 0x090A; + /// `practitioner` (`0x090B`) — the clinical actor: the physician + /// selected on clinical screens (FHIR `Practitioner`). Explicitly + /// NOT staff-account / license administration — that is an admin + /// concern, not the care-delivery role — and NOT employment + /// (`0x0DXX` HR owns the employment axis). Harvest-derived mint + /// (furnace-exam slag, round 2); no OGIT entity — no port alias. + pub const PRACTITIONER: u16 = 0x090B; // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── @@ -1932,6 +1965,10 @@ pub mod class_ids { ("treatment", TREATMENT), ("visit", VISIT), ("vital_sign", VITAL_SIGN), + ("anamnesis", ANAMNESIS), + ("investigation", INVESTIGATION), + ("examination", EXAMINATION), + ("practitioner", PRACTITIONER), // 0x0AXX — anatomy (FMA reference ontology) ("anatomical_structure", ANATOMICAL_STRUCTURE), ("skeleton", SKELETON), @@ -2032,7 +2069,7 @@ pub mod class_ids { // lance-graph mirror is rebuilt against it. assert_eq!( ALL.len(), - 84, + 88, "class_ids::ALL count changed — update this pin AND the \ lance-graph mirror COUNT_FUSE (crates/lance-graph-ogar/src/lib.rs) \ in the same PR", @@ -2864,8 +2901,8 @@ pub fn all_promoted_classes() -> Vec { page_layout(), page_image(), ocr_renderer(), - // 0x09XX — health arm (7 OGIT Healthcare concepts), in - // class_ids::ALL order. + // 0x09XX — health arm (7 OGIT Healthcare concepts + 4 + // harvest-derived mints), in class_ids::ALL order. patient(), diagnosis(), lab_value(), @@ -2873,6 +2910,10 @@ pub fn all_promoted_classes() -> Vec { treatment(), visit(), vital_sign(), + anamnesis(), + investigation(), + examination(), + practitioner(), // 0x0AXX — anatomy arm (FMA reference kinds), in class_ids::ALL order. anatomical_structure(), skeleton(), @@ -4165,6 +4206,83 @@ pub fn vital_sign() -> Class { c } +// The four 0x0908..0x090B builders below are harvest-derived mints +// (furnace-exam slag, round 2) with NO OGIT TTL entity behind them. Per +// the round-2 council ruling they are deliberately NAME-LEVEL: canonical +// concept + the harvest-evident minimum (one structural attribute, plus +// the patient edge where the record demonstrably lives in the patient +// file). Clinical attribute schemas are NOT fabricated here — they +// arrive from the harvest via the production lift when the evidence +// exists, never by hand. A test pins these attribute counts. + +/// Anamnesis — a patient's recorded history-taking (`0x0908`; the +/// discoverable English synonym is `medical_history`; SNOMED "history +/// taking"). Harvest-derived mint — no OGIT entity, name-level schema. +#[must_use] +pub fn anamnesis() -> Class { + let mut c = Class::new("Anamnesis"); + c.language = Language::Unknown; + c.canonical_concept = Some("anamnesis".to_string()); + c.description = + Some("A patient's recorded medical history (history-taking record)".to_string()); + c.associations = vec![family_edge("patient", "Patient")]; + let mut recorded_at = Attribute::new("recorded_at"); + recorded_at.type_name = Some("datetime".to_string()); + c.attributes = vec![recorded_at]; + c +} + +/// Investigation — an ordered clinical workup / investigation record +/// (`0x0909`; FHIR ServiceRequest/DiagnosticReport-adjacent). +/// Harvest-derived mint — no OGIT entity, name-level schema. +#[must_use] +pub fn investigation() -> Class { + let mut c = Class::new("Investigation"); + c.language = Language::Unknown; + c.canonical_concept = Some("investigation".to_string()); + c.description = Some("An ordered clinical workup / investigation record".to_string()); + c.associations = vec![family_edge("patient", "Patient")]; + let mut recorded_at = Attribute::new("recorded_at"); + recorded_at.type_name = Some("datetime".to_string()); + c.attributes = vec![recorded_at]; + c +} + +/// Examination — a performed examination record (`0x090A`), distinct +/// from [`investigation`] (performed exam vs ordered workup). Parallel +/// grounding: OGIT `Healthcare:Assessment` (upstream 2026-07-06, not +/// yet lifted into this codebook). Harvest-derived mint, name-level. +#[must_use] +pub fn examination() -> Class { + let mut c = Class::new("Examination"); + c.language = Language::Unknown; + c.canonical_concept = Some("examination".to_string()); + c.description = Some("A performed examination record".to_string()); + c.associations = vec![family_edge("patient", "Patient")]; + let mut performed_at = Attribute::new("performed_at"); + performed_at.type_name = Some("datetime".to_string()); + c.attributes = vec![performed_at]; + c +} + +/// Practitioner — the clinical actor (`0x090B`; FHIR `Practitioner`): +/// the physician selected on clinical screens. Explicitly NOT +/// staff-account / license administration (an admin concern, not the +/// care-delivery role) and NOT employment (`0x0DXX` HR owns that axis). +/// Harvest-derived mint — no OGIT entity, name-level schema. No patient +/// edge: the practitioner is the actor, not a patient-file record. +#[must_use] +pub fn practitioner() -> Class { + let mut c = Class::new("Practitioner"); + c.language = Language::Unknown; + c.canonical_concept = Some("practitioner".to_string()); + c.description = Some("The clinical actor delivering care".to_string()); + let mut name = Attribute::new("name"); + name.type_name = Some("string".to_string()); + c.attributes = vec![name]; + c +} + // ── 0x0BXX — Auth domain builders (the AuthStore class family, keystone §7) ── /// The `auth_store` (`0x0B01`) base class of the AuthStore family — the @@ -5239,6 +5357,10 @@ mod tests { "treatment", "visit", "vital_sign", + "anamnesis", + "investigation", + "examination", + "practitioner", ] { let id = canonical_concept_id(health_concept) .unwrap_or_else(|| panic!("{health_concept} missing from codebook")); @@ -5314,6 +5436,10 @@ mod tests { (treatment, "treatment", 0x0905), (visit, "visit", 0x0906), (vital_sign, "vital_sign", 0x0907), + (anamnesis, "anamnesis", 0x0908), + (investigation, "investigation", 0x0909), + (examination, "examination", 0x090A), + (practitioner, "practitioner", 0x090B), ] { let c = builder(); assert_eq!(c.canonical_concept.as_deref(), Some(concept)); @@ -5322,6 +5448,38 @@ mod tests { } } + #[test] + fn harvest_derived_health_mints_stay_name_level() { + // Round-2 council pin (anti-fabrication): the four 0x0908..0x090B + // builders are harvest-derived with NO OGIT entity behind them, so + // their schemas stay at the harvest-evident minimum — exactly ONE + // structural attribute each, a patient edge only where the record + // demonstrably lives in the patient file, and zero fabricated + // clinical attributes. Clinical schemas arrive from the harvest + // via the production lift, never by hand. Growing one of these + // sets is a deliberate act: bring the harvest evidence and update + // this pin in the same change. + for (c, expected_edges) in [ + (anamnesis(), 1usize), + (investigation(), 1), + (examination(), 1), + (practitioner(), 0), + ] { + assert_eq!( + c.attributes.len(), + 1, + "{}: harvest-derived mint grew past its name-level schema", + c.name, + ); + assert_eq!( + c.associations.len(), + expected_edges, + "{}: unexpected family-edge set for a name-level mint", + c.name, + ); + } + } + #[test] fn diagnosis_is_the_rich_worked_example() { // The 0x0902 worked example: full typed-attribute schema + two @@ -5363,6 +5521,10 @@ mod tests { "treatment", "visit", "vital_sign", + "anamnesis", + "investigation", + "examination", + "practitioner", ], "Health domain set drift — re-sync the consumer coverage gate", ); @@ -5370,8 +5532,8 @@ mod tests { for (_, id) in concepts_in_domain(ConceptDomain::Health) { assert_eq!(canonical_concept_domain(id), ConceptDomain::Health); } - // Counts line up with the codebook blocks. - assert_eq!(concepts_in_domain(ConceptDomain::Health).count(), 7); + // Counts line up with the codebook blocks (7 OGIT + 4 harvest mints). + assert_eq!(concepts_in_domain(ConceptDomain::Health).count(), 11); // 0x08XX OCR: the nine container KINDS (unicharset/recoder/charset/ // network_layer + the PDF→text plan mints textline/blob/page_layout/ // page_image/ocr_renderer). Content (the 112 unichars, code tables, diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 7c5b22d..932a7cd 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -593,7 +593,14 @@ mod tests { #[test] fn healthcare_alias_count_matches_ogit_entities() { // Patient / Diagnosis / LabValue / Medication / Treatment / Visit - // / VitalSign — the 7 OGIT `NTO/Healthcare/entities/` classes. + // / VitalSign — the 7 OGIT `NTO/Healthcare/entities/` classes this + // port was lifted from. Deliberately NOT the Health-domain codebook + // cardinality (11): the 0x0908..0x090B harvest-derived mints carry + // no OGIT entity and therefore no port alias. NOTE (assessment + // drift, filed follow-up): upstream OGIT now ships an 8th entity + // (`Healthcare:Assessment`, 2026-07-06) that was never lifted — + // when it is, this pin moves 7→8 and `examination` (0x090A) must + // NOT be double-minted as `assessment`. assert_eq!( HealthcarePort::aliases().len(), 7, From 13e1b0fa3c5ad7ca14c38540680a48bf719fea24 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 16:32:43 +0000 Subject: [PATCH 7/9] =?UTF-8?q?ogar-vocab:=20mint=20external=5Fpractice=20?= =?UTF-8?q?(0x090C)=20=E2=80=94=20three-axis-witnessed=20Health=20concept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4b furnace mint, hardened by a staged 5+3 council AND an operator-directed three-axis grounding gate (method + storage + Klickwege structure): external_practice is witnessed by a DAL address-form method family, a storage projection, and — decisively — a dedicated navigation home reachable in the nav plane, so it grounds [G] rather than method-only. FHIR Organization; name-level builder, no patient edge (an org, not a patient-file record); anti-fabrication pin extended (external_practice, 0 edges). COUNT_FUSE 88->89 (lance-graph mirror paired). concepts_in_domain(Health) 11->12 across doctest + domain-loop + enumeration + tuple tests; ogar-class-view registers the builder. ports.rs alias-count note explains why 0x090C stays alias-less (mechanical sql_mirror stub, no curated OGIT entity). The round's second candidate (a permission catalog) was REFUSED a mint by the same gate: storage + method witnesses but ZERO Klickwege home (embedded combo, never a destination) => it is a lookup surface, not a concept — no codebook slot consumed. The three-axis gate is recorded in the consumer transcode doctrine. Co-Authored-By: Claude --- crates/ogar-class-view/src/lib.rs | 5 +++- crates/ogar-vocab/src/lib.rs | 49 ++++++++++++++++++++++++++----- crates/ogar-vocab/src/ports.rs | 8 +++-- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 245db73..aa4789b 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -87,6 +87,7 @@ use ogar_vocab::{ currency_policy, diagnosis, examination, + external_practice, hr_department, hr_employee, hr_employment_contract, @@ -226,6 +227,7 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("investigation", investigation()), ("examination", examination()), ("practitioner", practitioner()), + ("external_practice", external_practice()), // ── 0x0AXX — anatomy (FMA reference kinds) ── ("anatomical_structure", anatomical_structure()), ("skeleton", skeleton()), @@ -604,7 +606,7 @@ mod tests { #[test] fn health_concepts_are_registered_with_their_fields() { - // All 11 Health concepts (7 OGIT + 4 harvest-derived mints) + // All 12 Health concepts (7 OGIT + 5 harvest-derived mints) // resolve to registry entries — this is what makes // `every_codebook_id_appears_in_class_ids_all` green for the // 0x09XX block. The `!fields.is_empty()` assertion is the @@ -623,6 +625,7 @@ mod tests { "investigation", "examination", "practitioner", + "external_practice", ] { let id = canonical_concept_id(concept).unwrap(); let view = v diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index 890059d..d8143a7 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1098,7 +1098,7 @@ impl Class { /// 0x06XX unassigned /// 0x07XX reserved: OSINT /// 0x08XX OCR (container kinds: unicharset/recoder/charset) -/// 0x09XX Health (clinical / patient / care; 7 OGIT entities + 4 harvest-derived mints) +/// 0x09XX Health (clinical / patient / care; 7 OGIT entities + 5 harvest-derived mints) /// 0x0AXX Anatomy (FMA reference ontology; bones/skeleton) /// 0x0BXX Auth (IAM; the AuthStore class family) /// 0x0CXX Automation (HIRO IT-automation: MARS CMDB + actuators) @@ -1226,13 +1226,13 @@ const CODEBOOK: &[(&str, u16)] = &[ ("page_image", 0x0808), ("ocr_renderer", 0x0809), // ── 0x09XX — Health domain (clinical / patient / care) ── - // 11 concepts, two provenance classes. 0x0901..0x0907: the 7 entities + // 12 concepts, two provenance classes. 0x0901..0x0907: the 7 entities // the OGIT `NTO/Healthcare/entities/` TTL ships (medcare-rs // Healthcare-namespace promotion, Northstar T9), projected onto // canonical Health ids so `ports::HealthcarePort` resolves them // through the `UnifiedBridge` codebook path (the same way OpenProject // `WorkPackage` and Redmine `Issue` resolve through their ports). - // 0x0908..0x090B: harvest-derived mints — surfaced by the transcode + // 0x0908..0x090C: harvest-derived mints — surfaced by the transcode // furnace exam's slag ledger (concept-shaped unbound residues), minted // through the engineer's gate; no OGIT TTL entity yet, so they carry // no port alias until one exists. Single-tenant today — no @@ -1250,6 +1250,7 @@ const CODEBOOK: &[(&str, u16)] = &[ ("investigation", 0x0909), ("examination", 0x090A), ("practitioner", 0x090B), + ("external_practice", 0x090C), // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── // The public anatomical reference frame consumed by the splat-native // ultrasound arc (`docs/SPLAT-NATIVE-CUSTOMER.md` §6 litmus) and the FMA @@ -1458,7 +1459,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { /// .map(|(name, _id)| name) /// .collect(); /// assert!(health.contains(&"patient")); -/// assert_eq!(health.len(), 11); // 7 OGIT Healthcare entities + 4 harvest-derived mints +/// assert_eq!(health.len(), 12); // 7 OGIT Healthcare entities + 5 harvest-derived mints /// ``` pub fn concepts_in_domain(domain: ConceptDomain) -> impl Iterator { CODEBOOK @@ -1776,6 +1777,16 @@ pub mod class_ids { /// (`0x0DXX` HR owns the employment axis). Harvest-derived mint /// (furnace-exam slag, round 2); no OGIT entity — no port alias. pub const PRACTITIONER: u16 = 0x090B; + /// `external_practice` (`0x090C`) — a referral-partner organization: + /// an external clinic recorded for inter-clinic correspondence + /// (name, postal address, contact channel). FHIR `Organization`. NOT + /// a patient-file record — it is an org, not care data — so it + /// carries no patient family edge. Harvest-derived mint (furnace-exam + /// slag, round 4); three-axis-witnessed (method family + storage + /// projection + a dedicated navigation home), so grounded [G] rather + /// than method-only; no clean OGIT entity — no port alias until one + /// exists. + pub const EXTERNAL_PRACTICE: u16 = 0x090C; // ── 0x0AXX — Anatomy domain (FMA reference ontology) ── @@ -1969,6 +1980,7 @@ pub mod class_ids { ("investigation", INVESTIGATION), ("examination", EXAMINATION), ("practitioner", PRACTITIONER), + ("external_practice", EXTERNAL_PRACTICE), // 0x0AXX — anatomy (FMA reference ontology) ("anatomical_structure", ANATOMICAL_STRUCTURE), ("skeleton", SKELETON), @@ -2069,7 +2081,7 @@ pub mod class_ids { // lance-graph mirror is rebuilt against it. assert_eq!( ALL.len(), - 88, + 89, "class_ids::ALL count changed — update this pin AND the \ lance-graph mirror COUNT_FUSE (crates/lance-graph-ogar/src/lib.rs) \ in the same PR", @@ -2901,7 +2913,7 @@ pub fn all_promoted_classes() -> Vec { page_layout(), page_image(), ocr_renderer(), - // 0x09XX — health arm (7 OGIT Healthcare concepts + 4 + // 0x09XX — health arm (7 OGIT Healthcare concepts + 5 // harvest-derived mints), in class_ids::ALL order. patient(), diagnosis(), @@ -2914,6 +2926,7 @@ pub fn all_promoted_classes() -> Vec { investigation(), examination(), practitioner(), + external_practice(), // 0x0AXX — anatomy arm (FMA reference kinds), in class_ids::ALL order. anatomical_structure(), skeleton(), @@ -4283,6 +4296,22 @@ pub fn practitioner() -> Class { c } +/// ExternalPractice — a referral-partner organization (`0x090C`; FHIR +/// `Organization`): an external clinic recorded for inter-clinic +/// correspondence. Harvest-derived mint, name-level schema. NO patient +/// edge — it is an organization, not a patient-file record. +#[must_use] +pub fn external_practice() -> Class { + let mut c = Class::new("ExternalPractice"); + c.language = Language::Unknown; + c.canonical_concept = Some("external_practice".to_string()); + c.description = Some("A referral-partner organization".to_string()); + let mut name = Attribute::new("name"); + name.type_name = Some("string".to_string()); + c.attributes = vec![name]; + c +} + // ── 0x0BXX — Auth domain builders (the AuthStore class family, keystone §7) ── /// The `auth_store` (`0x0B01`) base class of the AuthStore family — the @@ -5361,6 +5390,7 @@ mod tests { "investigation", "examination", "practitioner", + "external_practice", ] { let id = canonical_concept_id(health_concept) .unwrap_or_else(|| panic!("{health_concept} missing from codebook")); @@ -5440,6 +5470,7 @@ mod tests { (investigation, "investigation", 0x0909), (examination, "examination", 0x090A), (practitioner, "practitioner", 0x090B), + (external_practice, "external_practice", 0x090C), ] { let c = builder(); assert_eq!(c.canonical_concept.as_deref(), Some(concept)); @@ -5464,6 +5495,7 @@ mod tests { (investigation(), 1), (examination(), 1), (practitioner(), 0), + (external_practice(), 0), ] { assert_eq!( c.attributes.len(), @@ -5525,6 +5557,7 @@ mod tests { "investigation", "examination", "practitioner", + "external_practice", ], "Health domain set drift — re-sync the consumer coverage gate", ); @@ -5532,8 +5565,8 @@ mod tests { for (_, id) in concepts_in_domain(ConceptDomain::Health) { assert_eq!(canonical_concept_domain(id), ConceptDomain::Health); } - // Counts line up with the codebook blocks (7 OGIT + 4 harvest mints). - assert_eq!(concepts_in_domain(ConceptDomain::Health).count(), 11); + // Counts line up with the codebook blocks (7 OGIT + 5 harvest mints). + assert_eq!(concepts_in_domain(ConceptDomain::Health).count(), 12); // 0x08XX OCR: the nine container KINDS (unicharset/recoder/charset/ // network_layer + the PDF→text plan mints textline/blob/page_layout/ // page_image/ocr_renderer). Content (the 112 unichars, code tables, diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index 932a7cd..129a037 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -595,8 +595,12 @@ mod tests { // Patient / Diagnosis / LabValue / Medication / Treatment / Visit // / VitalSign — the 7 OGIT `NTO/Healthcare/entities/` classes this // port was lifted from. Deliberately NOT the Health-domain codebook - // cardinality (11): the 0x0908..0x090B harvest-derived mints carry - // no OGIT entity and therefore no port alias. NOTE (assessment + // cardinality (12): the 0x0908..0x090C harvest-derived mints carry + // no OGIT entity and therefore no port alias. (0x090C + // external_practice has a mechanical sql_mirror TTL stub but no + // curated OGIT NTO/Healthcare entity — the port mirrors the curated + // entity set, so it stays alias-less like its siblings.) NOTE + // (assessment // drift, filed follow-up): upstream OGIT now ships an 8th entity // (`Healthcare:Assessment`, 2026-07-06) that was never lifted — // when it is, this pin moves 7→8 and `examination` (0x090A) must From bcee3da165a03f52147ccddeb099011f95b60466 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 16:46:34 +0000 Subject: [PATCH 8/9] =?UTF-8?q?ogar-emitter:=20emit=5Fdo=5Fadapters=20?= =?UTF-8?q?=E2=80=94=20ActionDef=20->=20DO-arm=20adapter=20codegen,=20Klic?= =?UTF-8?q?kwege-gated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DO-arm codegen back-end of the OGAR compiler (core-first: the Core emits adapters). emit_do_adapters(classes, nav_witnessed) renders a deterministic Rust module of classid-keyed adapter scaffolds FROM harvested ActionDef signatures (writes/reads per predicate), gated on Klickwege structure-parity witnesses: a class whose canonical concept is NOT surfaced by any navigation screen is emitted with a marker so the parity gap is visible in the generated code, never silent. Fully deterministic (BTree ordering, no timestamp/id) so the emitted adapter is diffable as a golden artifact. Bodies are stubs — thinking lives in lance-graph, not in generated consumer code. Corpus-agnostic: input is typed structs, neutral Invoice/Cipher tests only (5, pinned output + shuffle-determinism + witness gate + snake/collision + stub-body). Co-Authored-By: Claude --- crates/ogar-emitter/src/do_adapter.rs | 347 ++++++++++++++++++++++++++ crates/ogar-emitter/src/lib.rs | 3 + 2 files changed, 350 insertions(+) create mode 100644 crates/ogar-emitter/src/do_adapter.rs diff --git a/crates/ogar-emitter/src/do_adapter.rs b/crates/ogar-emitter/src/do_adapter.rs new file mode 100644 index 0000000..36e3201 --- /dev/null +++ b/crates/ogar-emitter/src/do_adapter.rs @@ -0,0 +1,347 @@ +//! DO-arm adapter codegen — deterministic Rust scaffolds emitted from +//! harvested `ActionDef` signatures, gated on Klickwege structure-parity +//! witnesses (see [`emit_do_adapters`]). +//! +//! Corpus-agnostic by construction: this module only consumes typed +//! [`AdapterClass`] / [`AdapterAction`] structs the caller builds from +//! the real `ogar_vocab::ActionDef` harvest — no corpus data lives here. +//! Output is fully deterministic (stable sorts on every input `Vec`, +//! `BTreeSet`-tracked name disambiguation) so it is diffable as a golden +//! artifact across regenerations of the same harvest. + +use std::collections::BTreeSet; + +/// One class to emit an adapter for, built from the ActionDef harvest. +#[derive(Debug, Clone)] +pub struct AdapterClass { + /// PascalCase class name (the ActionDef `object_class`). + pub class_name: String, + /// Resolved canonical concept snake_case name, or empty if unresolved. + pub concept: String, + /// The class's DO-arm actions, in harvest order. + pub actions: Vec, +} + +/// One action = one ActionDef signature (writes/reads only; kausal/body +/// are out of scope — signature-only harvest). +#[derive(Debug, Clone)] +pub struct AdapterAction { + /// The predicate (method name). + pub predicate: String, + /// Write-target strings (e.g. "Ns:Class.Field"), harvest order. + pub writes: Vec, + /// Read-source strings, harvest order. + pub reads: Vec, +} + +/// Emit a deterministic Rust module of classid-keyed DO-arm adapter +/// scaffolds. `nav_witnessed` is the set of canonical concept names the +/// Klickwege navigation plane surfaces (structure-parity witnesses): a +/// class whose `concept` is present is STRUCTURE-WITNESSED; one whose +/// concept is absent (or empty) is emitted with a +/// `// STRUCTURE-UNWITNESSED` marker so the parity gap is visible in the +/// generated code, never silent. Fully deterministic (BTree ordering, +/// stable across runs) so the output is diffable as a golden artifact. +#[must_use] +pub fn emit_do_adapters(classes: &[AdapterClass], nav_witnessed: &BTreeSet) -> String { + let mut sorted: Vec<&AdapterClass> = classes.iter().collect(); + sorted.sort_by(|a, b| a.class_name.cmp(&b.class_name)); + + let mut out = String::new(); + out.push_str("//! Generated DO-arm adapters — DO NOT EDIT.\n"); + out.push_str("//! Emitted from the ActionDef harvest; gated on Klickwege structure parity.\n"); + out.push('\n'); + + let mut witnessed_count = 0usize; + let mut unwitnessed_count = 0usize; + + for class in &sorted { + let is_witnessed = !class.concept.is_empty() && nav_witnessed.contains(&class.concept); + if is_witnessed { + witnessed_count += 1; + out.push_str("// [structure: witnessed]\n"); + } else { + unwitnessed_count += 1; + let concept_display = if class.concept.is_empty() { + "?" + } else { + class.concept.as_str() + }; + out.push_str(&format!( + "// [structure: UNWITNESSED — no Klickwege screen surfaces \"{concept_display}\"]\n" + )); + } + + out.push_str(&format!("pub struct {}Adapter;\n", class.class_name)); + out.push('\n'); + out.push_str(&format!("impl {}Adapter {{\n", class.class_name)); + + let mut actions: Vec<&AdapterAction> = class.actions.iter().collect(); + actions.sort_by(|a, b| a.predicate.cmp(&b.predicate)); + + let mut used_fn_names: BTreeSet = BTreeSet::new(); + let mut action_blocks: Vec = Vec::with_capacity(actions.len()); + for action in &actions { + let base = to_snake_case(&action.predicate); + let fn_name = unique_fn_name(&base, &mut used_fn_names); + + let mut writes: Vec<&str> = action.writes.iter().map(String::as_str).collect(); + writes.sort_unstable(); + let mut reads: Vec<&str> = action.reads.iter().map(String::as_str).collect(); + reads.sort_unstable(); + + let mut block = String::new(); + block.push_str(&format!( + " /// `{}` — generated from ActionDef signature.\n", + action.predicate + )); + block.push_str(&format!( + " /// writes ({}): {}\n", + writes.len(), + name_preview(&writes) + )); + block.push_str(&format!( + " /// reads ({}): {}\n", + reads.len(), + name_preview(&reads) + )); + block.push_str(&format!(" pub fn {fn_name}(&self) {{\n")); + block.push_str( + " /* generated from ActionDef signature; body is upstream (thinking lives in lance-graph) */\n", + ); + block.push_str(" unimplemented!()\n"); + block.push_str(" }\n"); + action_blocks.push(block); + } + out.push_str(&action_blocks.join("\n")); + out.push_str("}\n"); + out.push('\n'); + } + + let total = sorted.len(); + out.push_str(&format!( + "// summary: total={total} witnessed={witnessed_count} unwitnessed={unwitnessed_count}\n" + )); + + out +} + +/// First-few-names preview of a sorted name slice: up to 3 names, with a +/// trailing `", ..."` when more were dropped. `"(none)"` when empty. +fn name_preview(names: &[&str]) -> String { + if names.is_empty() { + return "(none)".to_string(); + } + let take = names.len().min(3); + let mut s = names[..take].join(", "); + if names.len() > take { + s.push_str(", ..."); + } + s +} + +/// Convert a PascalCase / mixedCase / already-snake_case predicate name +/// into a Rust-identifier-safe snake_case fragment. Idempotent on input +/// that is already snake_case. Non-identifier characters are dropped +/// rather than transcribed, since predicate names are expected to be +/// identifier-shaped already. +fn to_snake_case(input: &str) -> String { + let chars: Vec = input.chars().collect(); + let mut out = String::with_capacity(input.len() + 4); + for (i, &c) in chars.iter().enumerate() { + if c == '_' || c == '-' || c == ' ' { + if !out.is_empty() && !out.ends_with('_') { + out.push('_'); + } + continue; + } + if c.is_ascii_uppercase() { + let prev = if i > 0 { Some(chars[i - 1]) } else { None }; + let next = chars.get(i + 1).copied(); + let boundary = match prev { + Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit() => true, + Some(p) if p.is_ascii_uppercase() => next.is_some_and(|n| n.is_ascii_lowercase()), + _ => false, + }; + if boundary && !out.is_empty() && !out.ends_with('_') { + out.push('_'); + } + out.push(c.to_ascii_lowercase()); + } else if c.is_ascii_alphanumeric() { + out.push(c.to_ascii_lowercase()); + } + } + let collapsed: String = out + .split('_') + .filter(|s| !s.is_empty()) + .collect::>() + .join("_"); + if collapsed.is_empty() { + "unnamed".to_string() + } else { + collapsed + } +} + +/// Deterministically disambiguate a snake_case function name against the +/// names already used within the same `impl` block: the first occurrence +/// wins the bare name; every subsequent collision gets `_2`, `_3`, …. +fn unique_fn_name(base: &str, used: &mut BTreeSet) -> String { + if used.insert(base.to_string()) { + return base.to_string(); + } + let mut n = 2usize; + loop { + let candidate = format!("{base}_{n}"); + if used.insert(candidate.clone()) { + return candidate; + } + n += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn invoice_class() -> AdapterClass { + AdapterClass { + class_name: "Invoice".to_string(), + concept: "invoice".to_string(), + actions: vec![ + AdapterAction { + predicate: "PostInvoice".to_string(), + writes: vec![ + "Invoice.state".to_string(), + "Invoice.posted_at".to_string(), + ], + reads: vec!["Invoice.lines".to_string()], + }, + AdapterAction { + predicate: "cancel_invoice".to_string(), + writes: vec![], + reads: vec!["Invoice.state".to_string()], + }, + ], + } + } + + fn cipher_panel_class() -> AdapterClass { + AdapterClass { + class_name: "CipherPanel".to_string(), + concept: String::new(), + actions: vec![AdapterAction { + predicate: "Reset".to_string(), + writes: vec!["CipherPanel.key".to_string()], + reads: vec![], + }], + } + } + + fn witness_set() -> BTreeSet { + let mut s = BTreeSet::new(); + s.insert("invoice".to_string()); + s + } + + #[test] + fn deterministic_output_is_pinned() { + let classes = vec![invoice_class(), cipher_panel_class()]; + let out = emit_do_adapters(&classes, &witness_set()); + let expected = r#"//! Generated DO-arm adapters — DO NOT EDIT. +//! Emitted from the ActionDef harvest; gated on Klickwege structure parity. + +// [structure: UNWITNESSED — no Klickwege screen surfaces "?"] +pub struct CipherPanelAdapter; + +impl CipherPanelAdapter { + /// `Reset` — generated from ActionDef signature. + /// writes (1): CipherPanel.key + /// reads (0): (none) + pub fn reset(&self) { + /* generated from ActionDef signature; body is upstream (thinking lives in lance-graph) */ + unimplemented!() + } +} + +// [structure: witnessed] +pub struct InvoiceAdapter; + +impl InvoiceAdapter { + /// `PostInvoice` — generated from ActionDef signature. + /// writes (2): Invoice.posted_at, Invoice.state + /// reads (1): Invoice.lines + pub fn post_invoice(&self) { + /* generated from ActionDef signature; body is upstream (thinking lives in lance-graph) */ + unimplemented!() + } + + /// `cancel_invoice` — generated from ActionDef signature. + /// writes (0): (none) + /// reads (1): Invoice.state + pub fn cancel_invoice(&self) { + /* generated from ActionDef signature; body is upstream (thinking lives in lance-graph) */ + unimplemented!() + } +} + +// summary: total=2 witnessed=1 unwitnessed=1 +"#; + assert_eq!(out, expected); + } + + #[test] + fn shuffled_input_is_still_deterministic() { + let classes_forward = vec![invoice_class(), cipher_panel_class()]; + let classes_reversed = vec![cipher_panel_class(), invoice_class()]; + let witness = witness_set(); + let out_forward = emit_do_adapters(&classes_forward, &witness); + let out_reversed = emit_do_adapters(&classes_reversed, &witness); + assert_eq!(out_forward, out_reversed); + } + + #[test] + fn witness_gate_marks_witnessed_and_unwitnessed_classes() { + let classes = vec![invoice_class(), cipher_panel_class()]; + let out = emit_do_adapters(&classes, &witness_set()); + assert!(out.contains("// [structure: witnessed]\npub struct InvoiceAdapter;")); + assert!(out.contains( + "// [structure: UNWITNESSED — no Klickwege screen surfaces \"?\"]\npub struct CipherPanelAdapter;" + )); + } + + #[test] + fn snake_case_collisions_get_distinct_suffixes() { + let class = AdapterClass { + class_name: "Cipher".to_string(), + concept: "cipher".to_string(), + actions: vec![ + AdapterAction { + predicate: "GetX".to_string(), + writes: vec![], + reads: vec![], + }, + AdapterAction { + predicate: "get_x".to_string(), + writes: vec![], + reads: vec![], + }, + ], + }; + let mut witness = BTreeSet::new(); + witness.insert("cipher".to_string()); + let classes = vec![class]; + let out = emit_do_adapters(&classes, &witness); + assert!(out.contains("pub fn get_x(&self)")); + assert!(out.contains("pub fn get_x_2(&self)")); + assert!(!out.contains("pub fn get_x_2_2(&self)")); + } + + #[test] + fn generated_body_is_unimplemented_with_upstream_note() { + let classes = vec![invoice_class()]; + let out = emit_do_adapters(&classes, &witness_set()); + assert!(out.contains("unimplemented!()")); + assert!(out.contains("thinking lives in lance-graph")); + } +} diff --git a/crates/ogar-emitter/src/lib.rs b/crates/ogar-emitter/src/lib.rs index 03dd0f1..5be279a 100644 --- a/crates/ogar-emitter/src/lib.rs +++ b/crates/ogar-emitter/src/lib.rs @@ -39,6 +39,9 @@ use ogar_vocab::{ Validation, }; +mod do_adapter; +pub use do_adapter::{AdapterAction, AdapterClass, emit_do_adapters}; + /// A subject-predicate-object triple in the OGAR / OGIT prefix-radix /// namespace. /// From 59b4ac52c801caef3dbec5747f183a90baac373f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 18:14:58 +0000 Subject: [PATCH 9/9] adapter parity: use table-less `visit` for the NoCapabilitiesFor probe (healthcare table fallout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dump-parity probe for the NoCapabilitiesFor drift arm used `patient` as a 'classid with no capabilities' example. That held while the OCR table was the only DomainTable, but this branch adds the healthcare capability table — which gives `patient` capabilities, so the executor guard (tableContributes && !expected) now fires FIRST and throws UnexpectedConsumer(tesseract-ogar), uncaught by the catch(NoCapabilitiesFor) → the C# Dump() crashed and the Rust ground-truth mismatched. Switch the probe to `visit` (0x0906) — the deliberately table-less Health concept (excluded from HEALTHCARE_SUBJECT_CLASSIDS), so it has no OCR and no healthcare capabilities and reliably yields NoCapabilitiesFor. This is exactly the classid capability_registry.rs:464 already uses for the same probe. Fixed in lockstep across all three emission sites (Rust ground-truth generator + C# template + Python template) so the dumps still agree. C# dotnet parity 2/2 green. Co-Authored-By: Claude --- crates/ogar-adapter-csharp/src/lib.rs | 5 ++++- crates/ogar-adapter-python/src/lib.rs | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/ogar-adapter-csharp/src/lib.rs b/crates/ogar-adapter-csharp/src/lib.rs index 03a2630..2791fd6 100644 --- a/crates/ogar-adapter-csharp/src/lib.rs +++ b/crates/ogar-adapter-csharp/src/lib.rs @@ -522,7 +522,10 @@ public static class OgarCapabilitySurface try { - ResolveHotplug("tesseract-ogar", new[] { ClassIds["patient"] }, Array.Empty()); + // `visit` (0x0906): the table-less Health concept — the stable + // "no capabilities" probe now that `patient` carries the + // healthcare table (whose executor guard would fire first). + ResolveHotplug("tesseract-ogar", new[] { ClassIds["visit"] }, Array.Empty()); throw new Exception("expected NoCapabilitiesFor"); } catch (NoCapabilitiesFor e) { lines.Add($"NoCapabilitiesFor={e.ClassId:X4}"); } diff --git a/crates/ogar-adapter-python/src/lib.rs b/crates/ogar-adapter-python/src/lib.rs index e160066..f104d24 100644 --- a/crates/ogar-adapter-python/src/lib.rs +++ b/crates/ogar-adapter-python/src/lib.rs @@ -489,7 +489,10 @@ def dump() -> str: lines.append(f"UnknownClassid={e.classid:04X}") try: - resolve_hotplug("tesseract-ogar", [CLASS_IDS["patient"]], []) + # `visit` (0x0906): the table-less Health concept — the stable + # "no capabilities" probe now that `patient` carries the healthcare + # table (whose executor guard would fire first). + resolve_hotplug("tesseract-ogar", [CLASS_IDS["visit"]], []) raise AssertionError("expected NoCapabilitiesFor") except NoCapabilitiesFor as e: lines.append(f"NoCapabilitiesFor={e.classid:04X}") @@ -625,7 +628,13 @@ pub mod ground_truth { "UnknownClassid={}\n", expect_drift_hex(r1, "UnknownClassid") )); - let r2 = resolve_hotplug("tesseract-ogar", &[class_ids::PATIENT], &[]); + // `visit` (0x0906) is the deliberately table-less Health concept + // (excluded from HEALTHCARE_SUBJECT_CLASSIDS) — no OCR and no + // healthcare capabilities, so it is the stable "no capabilities" + // probe. `patient` USED to work here but now carries the + // healthcare table, so its executor guard fires before the + // NoCapabilitiesFor check (matches capability_registry's own probe). + let r2 = resolve_hotplug("tesseract-ogar", &[class_ids::VISIT], &[]); out.push_str(&format!( "NoCapabilitiesFor={}\n", expect_drift_hex(r2, "NoCapabilitiesFor")