<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Parthenon Blog</title>
        <link>https://parthenon.acumenus.net/docs/es/blog</link>
        <description>Parthenon Blog</description>
        <lastBuildDate>Thu, 31 Dec 2099 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>es</language>
        <item>
            <title><![CDATA[Introducing Parthenon: Transforming Healthcare with AI-Powered Outcomes Research]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon</guid>
            <pubDate>Thu, 31 Dec 2099 00:00:00 GMT</pubDate>
            <description><![CDATA[Pinned — The founding vision for Parthenon, a next-generation unified outcomes research platform built on OMOP CDM v5.4.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p><strong>Pinned Post</strong> | Originally published March 7, 2026</p>
</blockquote>
<p>Outcomes research has evolved alongside the broader arc of healthcare analytics infrastructure. Early siloed clinical systems produced fragmented administrative and claims data with limited analytic utility — adequate for billing, but structurally unsuitable for longitudinal cohort construction or comparative effectiveness work. The meaningful use era expanded the availability of structured clinical data, yet interoperability failures meant that patient journeys remained fractured across institutional boundaries, undermining the real-world evidence studies that outcomes researchers depend on. The shift to integrated analytics platforms — particularly the adoption of common data models like OMOP/OHDSI — marked a genuine inflection point: federated network studies, standardized phenotyping, and reproducible retrospective analyses became operationally feasible at scale. Now a fourth generation is taking shape, one in which AI-augmented clinical intelligence moves outcomes research from retrospective description toward prospective, near-real-time evidence generation — enabling dynamic cohort surveillance, treatment heterogeneity detection, and value-based care signal identification that was previously impractical outside of narrow clinical trial settings.</p>
<p>Parthenon is built for this fourth generation.</p>
<div style="border-radius:12px;overflow:hidden;margin-bottom:2rem"><img src="https://parthenon.acumenus.net/docs/img/parthenon-hero.jpg" alt="The Parthenon" style="width:100%;display:block"></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-we-built-this">Why We Built This<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#why-we-built-this" class="hash-link" aria-label="Enlace directo al Why We Built This" title="Enlace directo al Why We Built This">​</a></h2>
<p>The problems with traditional healthcare analytics infrastructure are well-documented but stubbornly persistent. Data fragmentation scatters patient information across EHR, laboratory, radiology, and claims platforms with inconsistent terminologies. Analytics teams are overwhelmed with routine reporting demands, leaving limited capacity for the strategic analysis that actually improves outcomes. And the insights that do emerge are retrospective — care gaps identified too late for optimal impact, interventions that are reactive rather than proactive.</p>
<p>The OHDSI community addressed part of this problem brilliantly. The OMOP Common Data Model standardizes clinical data across institutions. HADES packages encode decades of pharmacoepidemiology methodology. Atlas provides a visual interface for cohort building and analysis design. But the toolchain has grown to 15+ disconnected applications — Atlas, WebAPI, Achilles, DQD, CohortGenerator, CohortMethod, PatientLevelPrediction, and more — each with its own deployment, its own UI paradigm, and its own learning curve.</p>
<p>Parthenon replaces all of them with a single application.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-parthenon-does">What Parthenon Does<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#what-parthenon-does" class="hash-link" aria-label="Enlace directo al What Parthenon Does" title="Enlace directo al What Parthenon Does">​</a></h2>
<p>At its core, Parthenon is a unified outcomes research platform built on OMOP CDM v5.4. A researcher can move through the entire real-world evidence lifecycle without leaving the browser: explore vocabularies and build concept sets, construct patient cohorts with a visual builder, then run characterization, incidence rates, treatment pathways, population-level estimation, patient-level prediction, self-controlled case series, and evidence synthesis.</p>
<p>But Parthenon extends well beyond what Atlas ever offered.</p>
<p><strong>Genomics</strong> — Upload VCF files, annotate variants against ClinVar, browse mutations in an interactive variant browser, and convene virtual tumor boards with AI-assisted interpretation. This bridges the gap between population-level observational research and precision medicine.</p>
<p><strong>Medical Imaging</strong> — View DICOM studies with a built-in Cornerstone3D viewer, connect to PACS systems via WADO-RS, and incorporate imaging criteria directly into cohort definitions. Radiogenomics analysis becomes possible within the same platform where you run your epidemiological studies.</p>
<p><strong>Health Economics &amp; Outcomes Research</strong> — Model cost-effectiveness, identify care gaps across populations, and run economic analytics. The care gap module tracks screening compliance, flags missed interventions, and quantifies the financial impact of closing gaps at various capture rates.</p>
<p><strong>FHIR R4 Integration</strong> — Connect to EHR systems using SMART Backend Services for automated bulk export and incremental sync. Clinical data flows from production EHR systems into your OMOP CDM without manual ETL intervention.</p>
<p><strong>AI-Assisted Analysis</strong> — An integrated AI service powered by Ollama and MedGemma provides semantic concept search, natural-language cohort suggestions, clinical result interpretation, and genomic variant summarization. The AI doesn't replace the researcher — it reduces the time between question and insight.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-architecture">The Architecture<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#the-architecture" class="hash-link" aria-label="Enlace directo al The Architecture" title="Enlace directo al The Architecture">​</a></h2>
<p>Parthenon is a containerized multi-service application orchestrated with Docker Compose. The frontend is React 19 with TypeScript strict mode, Tailwind CSS, and Zustand for state management. The backend is Laravel 11 with PHP 8.4, using Sanctum authentication and Spatie role-based access control. A Python FastAPI service handles AI capabilities — MedGemma through Ollama, pgvector embeddings for semantic search. An R Plumber API executes HADES analyses — CohortMethod, PatientLevelPrediction, SelfControlledCaseSeries — against the CDM. PostgreSQL 16 stores both application data and the OMOP CDM across multiple schemas. Redis powers the job queue via Laravel Horizon. Solr provides full-text vocabulary search.</p>
<p>Eight Docker services, one <code>docker compose up -d</code> command. A Python installer walks you through configuration in nine phases — from preflight checks through admin account creation — with optional Eunomia demo data so you can start exploring immediately.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-ai-imperative">The AI Imperative<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#the-ai-imperative" class="hash-link" aria-label="Enlace directo al The AI Imperative" title="Enlace directo al The AI Imperative">​</a></h2>
<p>The PDF that inspired this platform — <em>Transforming Healthcare Delivery: Next-Generation Clinical Analytics Powered by Artificial Intelligence</em> — makes the business case quantitatively. Six in ten Americans live with chronic disease, driving $4.1 trillion in annual healthcare costs. Traditional monitoring of conditions like CKD achieves just 3% compliance across all seven recommended measures. AI-enhanced approaches have demonstrated 267% improvement in compliance, prevention of 15-20 dialysis cases per year, and $3-4 million in annual cost savings per 10,000 patients.</p>
<p>These aren't theoretical projections. They're the measurable outcomes that become possible when you combine standardized clinical data (OMOP CDM), validated analytical methods (HADES), and machine learning that identifies patterns humans can't see at scale.</p>
<p>Parthenon's care gap module, population risk scoring, and predictive analytics are designed to deliver exactly this kind of impact — clinical decision support that anticipates patient needs rather than simply responding to events.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="building-in-public">Building in Public<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#building-in-public" class="hash-link" aria-label="Enlace directo al Building in Public" title="Enlace directo al Building in Public">​</a></h2>
<p>This blog will serve as a daily development journal. Every day, we'll document what was built, what broke, what we learned, and what's next. The first technical post — about the <a href="https://parthenon.acumenus.net/docs/es/blog/ohdsi-hades-r-runtime-lessons">five bugs we had to fix</a> before HADES analyses would run in production — is already live. It's the kind of hard-won knowledge that doesn't appear in any documentation, and we think sharing it openly makes the entire OHDSI ecosystem stronger.</p>
<p>We're also automating this process. A Claude Code agent reviews the day's git history every night and generates a narrative dev log post — not just a commit list, but a story about what the code changes mean and why they matter.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/introducing-parthenon#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>The platform's roadmap follows a four-phase journey. The foundation phase establishes data integration and baseline analytics. Core analytics introduces care bundles for high-impact conditions. Advanced capabilities bring full population health management with HCC coding optimization and clinical decision support integration. The transformation phase enables value-based care analytics, precision medicine, and continuously learning systems.</p>
<p>We're deep in the foundation and core analytics phases right now, shipping features daily. Follow this blog to watch it happen.</p>
<hr>
<p><em>Parthenon is open-source and available at <a href="https://github.com/Acumenus-Data-Sciences/Parthenon" target="_blank" rel="noopener noreferrer">github.com/Acumenus-Data-Sciences/Parthenon</a>. Built by <a href="https://www.acumenus.io/" target="_blank" rel="noopener noreferrer">Acumenus Data Sciences</a>.</em></p>]]></content:encoded>
            <category>announcement</category>
            <category>vision</category>
            <category>architecture</category>
            <category>ai</category>
            <category>healthcare</category>
        </item>
        <item>
            <title><![CDATA[Parthenon v1.0.8: The Research Surface Grows Up]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up</guid>
            <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A deep look at the v1.0.8 milestone — a library-wide lifecycle state machine with a super-admin console, a server-side Publish module, the first two Claude Agent SDK copilots, Studies v2, and a full Hypertension v3 study run end-to-end on a 1M-patient OMOP CDM. More than 200 commits in 18 days.]]></description>
            <content:encoded><![CDATA[<p>Parthenon v1.0.8 is the release where the research surface grows up.</p>
<p>v1.0.7 was a platform release: the Community/Enterprise fork, AGPLv3,
extension points, and the deployment plumbing underneath everything. It
mattered, but it lived below the waterline. v1.0.8 comes back up to the
surface researchers actually touch — and it does three substantial things at
once. It gives every artifact in the library a real lifecycle. It turns a
finished study into a shareable, server-persisted manuscript. And it brings the
first two AI copilots into the workspace, behind a single switch an
administrator controls.</p>
<p>More than 200 commits landed in 18 days (May 10 to May 28). This post is the
engineering story behind them.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-theme-from-a-pile-of-artifacts-to-a-managed-library">The theme: from "a pile of artifacts" to "a managed library"<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-theme-from-a-pile-of-artifacts-to-a-managed-library" class="hash-link" aria-label="Enlace directo al The theme: from &quot;a pile of artifacts&quot; to &quot;a managed library&quot;" title="Enlace directo al The theme: from &quot;a pile of artifacts&quot; to &quot;a managed library&quot;">​</a></h2>
<p>If you have run an outcomes-research platform for any length of time, you know
the failure mode. Concept sets accumulate. Cohort definitions get cloned,
tweaked, abandoned. Half-finished analyses pile up next to the three that
actually went into a manuscript. Six months in, nobody can tell the canonical
hypertension cohort from the four experiments that preceded it, and the new
researcher who joins the project has no way to know which artifacts are safe to
build on.</p>
<p>That is not a UI problem. It is a missing concept. Artifacts had no <em>state</em>.</p>
<p>v1.0.8 introduces that state — a lifecycle — and then builds the user controls,
the admin console, the policies, the background jobs, and the API contract
around it. In parallel, it makes the <em>output</em> of research first-class with the
Publish module, and it brings AI assistance into the two places where it has
the highest leverage: designing a study and drafting a manuscript.</p>
<p>Three feature lines, one coherent direction: make the research surface
something a serious team can keep clean and keep moving.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="feature-line-1--library-lifecycle">Feature line 1 — Library Lifecycle<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#feature-line-1--library-lifecycle" class="hash-link" aria-label="Enlace directo al Feature line 1 — Library Lifecycle" title="Enlace directo al Feature line 1 — Library Lifecycle">​</a></h2>
<p>This is the spine of the release. Every library artifact — concept sets, cohort
definitions, and all eight analysis types — now carries an explicit lifecycle
state, with the full stack to manage it from a single researcher's list page up
to a super-admin governing the whole library across every user.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-state-machine">The state machine<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-state-machine" class="hash-link" aria-label="Enlace directo al The state machine" title="Enlace directo al The state machine">​</a></h3>
<p>The model is deliberately small. An artifact is in one of three working states:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">draft  ──promote──▶  active  ──archive──▶  archived</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   ▲                    ▲                      │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   └────────────────────┴──────restore─────────┘</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ul>
<li><strong>draft</strong> — a private working copy. Visible to its owner; hidden from everyone
else by a default query scope.</li>
<li><strong>active</strong> — broadly visible and reusable across studies. This is the
"canonical, build on me" state.</li>
<li><strong>archived</strong> — retired but recoverable. Hidden from the active library,
still fully accessible to admins, and one click away from being restored.</li>
</ul>
<p>The transitions are enforced in one place. A reusable Eloquent trait,
<code>HasLibraryLifecycle</code>, gives all nine models the same <code>promote</code>, <code>archive</code>, and
<code>restore_lifecycle</code> methods, the <code>status</code> cast, the <code>archived_at</code> /
<code>archived_by</code> / <code>promoted_at</code> columns, and the <code>active</code> / <code>draft</code> / <code>archived</code>
query scopes. The transitions are idempotent — archiving an already-archived
item is a no-op that returns the model unchanged, which is exactly what you want
when a bulk action sweeps a mixed selection.</p>
<p>A default global scope hides drafts from non-owners and hides archived items
from the active library, so the lifecycle is invisible until you go looking for
it. Nothing in the everyday research flow changed unless you wanted it to.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="authorization-without-ceremony">Authorization without ceremony<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#authorization-without-ceremony" class="hash-link" aria-label="Enlace directo al Authorization without ceremony" title="Enlace directo al Authorization without ceremony">​</a></h3>
<p>Lifecycle authorization lives in a single trait, <code>AuthorizesLibraryLifecycle</code>,
shared by the concept-set, cohort-definition, and analysis policies:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token keyword" style="color:hsl(286, 60%, 67%)">public</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">function</span><span class="token plain"> </span><span class="token function-definition function" style="color:hsl(207, 82%, 66%)">archive</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token class-name type-declaration" style="color:hsl(29, 54%, 61%)">User</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$user</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token class-name type-declaration" style="color:hsl(29, 54%, 61%)">Model</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$item</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token keyword return-type" style="color:hsl(286, 60%, 67%)">bool</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">return</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$this</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">isOwner</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$user</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$item</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">||</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$user</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">hasRole</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'super-admin'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Owners manage their own artifacts; super-admins manage everyone's. That single
<code>|| hasRole('super-admin')</code> clause is what makes the admin console possible
without a second, parallel set of endpoints — more on that below.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-api-contract">The API contract<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-api-contract" class="hash-link" aria-label="Enlace directo al The API contract" title="Enlace directo al The API contract">​</a></h3>
<p>The lifecycle is exposed as a small, uniform set of endpoints, generated per
entity behind the right permission gate:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /api/v1/{entity}/{id}/promote</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /api/v1/{entity}/{id}/archive</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /api/v1/{entity}/{id}/restore</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /api/v1/{entity}/bulk-archive     { ids: [...] }</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /api/v1/{entity}/bulk-restore     { ids: [...] }</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The bulk endpoints return a precise <code>{ done, skipped, missing }</code> triple, so the
UI can tell you "archived 9, skipped 2 (no permission), 1 not found" instead of
a vague success or failure.</p>
<p>One contract detail is worth calling out: attaching a <em>draft</em> artifact to a
study raises a <code>RequiresPromotionException</code>, which surfaces as an <strong>HTTP 409</strong>
with an auto-promote flow. You cannot accidentally build a study on a private
draft that nobody else can see — the system either promotes it for you or tells
you exactly why it stopped.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-user-facing-surface">The user-facing surface<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-user-facing-surface" class="hash-link" aria-label="Enlace directo al The user-facing surface" title="Enlace directo al The user-facing surface">​</a></h3>
<p>On the cohort-definitions, concept-sets, and analyses list pages, the lifecycle
shows up as a set of shared, reusable components built once and reused
everywhere:</p>
<ul>
<li>a color-coded <strong>status badge</strong> (green active, grey draft, amber archived),</li>
<li>a per-row <strong>action menu</strong> that offers only the transitions valid for the
current state,</li>
<li><strong>status tabs</strong> with live counts (Active / Drafts / Archived / All),</li>
<li>a <strong>bulk toolbar</strong> that slides in when rows are selected, and</li>
<li>a single <strong>confirm modal</strong> that handles both single-item and batch copy and
explains the consequence of each action before you commit it.</li>
</ul>
<p>Super-admins get one extra control on these pages: an <strong>All users</strong> toggle that
flips the list endpoint into <code>scope=all</code>, so an administrator can see and manage
artifacts across the entire user base, not just their own.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-admin-console">The admin console<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-admin-console" class="hash-link" aria-label="Enlace directo al The admin console" title="Enlace directo al The admin console">​</a></h3>
<p>The highest-leverage piece is the unified admin surface at <code>/admin/library</code>. It
is a single table that UNION-ALLs across all eleven lifecycle-managed tables,
so a super-admin can browse, filter, and act on every concept set, cohort
definition, and analysis in one place.</p>
<p>The console does the governance work that does not belong on a researcher's
page:</p>
<ul>
<li><strong>Hard delete</strong> with an attachment preflight — an artifact still referenced by
a study is blocked and the blocking studies are listed as deep links, so you
resolve the reference instead of orphaning it. Analyses are matched by their
fully-qualified class name, because that is how the polymorphic study
attachment actually stores them.</li>
<li><strong>Owner reassignment</strong> with a permission check and an audit row — you can hand
a departed researcher's artifacts to an active one, but only if the target
actually holds the relevant <code>.view</code> permission.</li>
<li>A <strong>nightly 30-day purge</strong> of soft-deleted items, and a one-time lifecycle
notice so existing users learn the new model exists.</li>
</ul>
<p>In this release the console also gained the lifecycle transitions themselves —
promote, archive, and restore, per-row and in bulk — brought up to the same
gold-standard UX as the researcher pages: pill tabs, live status counts, status
badges, sticky bulk toolbars, and modal confirmations in place of browser
<code>confirm()</code> dialogs.</p>
<p>The implementation detail that made this clean is worth a sentence, because it
is a pattern we will reuse. The admin console does <strong>not</strong> define its own
lifecycle endpoints. It reuses the exact per-entity routes the researcher pages
call, with one small piece of glue — a typed map from the console's snake_case
<code>item_type</code> to the kebab-case route slug:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Record</span><span class="token operator" style="color:hsl(207, 82%, 66%)">&lt;</span><span class="token plain">AdminLibraryItemType</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> LibraryEntity</span><span class="token operator" style="color:hsl(207, 82%, 66%)">&gt;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Because it is a <code>Record</code> over a closed union, the type checker guarantees all
eleven entities are mapped — a missing entry is a compile error, not a runtime
404. A heterogeneous bulk selection (concept sets plus cohorts plus analyses,
all checked at once) is grouped by entity and dispatched as one bulk call per
group, then the results are merged into a single toast. Zero new backend
surface area; the super-admin branch of the existing policy does the rest.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="cleanup-suggestions-and-the-eleventh-entity">Cleanup suggestions and the eleventh entity<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#cleanup-suggestions-and-the-eleventh-entity" class="hash-link" aria-label="Enlace directo al Cleanup suggestions and the eleventh entity" title="Enlace directo al Cleanup suggestions and the eleventh entity">​</a></h3>
<p>Two smaller pieces complete the line. A nightly job surfaces unused artifacts as
<strong>cleanup suggestions</strong> — a cache table, an API endpoint, and a banner that
nudges you toward the stale drafts worth archiving. And <strong>characterizations</strong>
were brought in as the eleventh lifecycle-managed entity: an additive migration
added the lifecycle columns, the model picked up the trait, and the frontend's
polymorphic analysis list enabled the controls automatically the moment the
entity map flipped its slug from <code>null</code> to <code>characterizations</code>.</p>
<p>That last detail is the whole point of building the lifecycle as shared
primitives: adding the eleventh entity to the user-facing UI required no UI
changes at all.</p>
<p>This line alone was <strong>33 <code>feat(library)</code> commits</strong>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="feature-line-2--the-publish-module">Feature line 2 — The Publish module<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#feature-line-2--the-publish-module" class="hash-link" aria-label="Enlace directo al Feature line 2 — The Publish module" title="Enlace directo al Feature line 2 — The Publish module">​</a></h2>
<p>Research that cannot be written up and shared is research that stays trapped in
the tool. The Publish module turns a finished study into a real manuscript
surface, persisted server-side so there is no "lost work" failure mode.</p>
<ul>
<li><strong>Server-side drafts.</strong> The publish page loads and saves through the API
rather than browser state, with a stable <code>documentHash</code> that makes autosave
deduplication reliable.</li>
<li><strong>Debounced autosave</strong> with retry and a <code>beforeunload</code> guard, surfaced
through a save-status indicator and a prompt-to-save modal on first edit. You
do not lose a paragraph because a tab closed.</li>
<li><strong>Snapshots.</strong> A snapshot service with create / list / revert endpoints under
optimistic locking, wired into create, revert, and panel UIs — version history
for a manuscript, not just an undo buffer.</li>
<li><strong>Study-scoped sharing.</strong> A <code>PublicationDraftPolicy</code>, per-draft visibility, a
visibility badge, a share dropdown, and a read-only wizard mode for viewer
collaborators via <code>Study::scopeAccessibleBy</code>. The people on a study can read
the draft; the wider world cannot, until you decide otherwise.</li>
<li>A <strong>publication library</strong> at <code>/publish/library</code> to find prior write-ups.</li>
</ul>
<p>This line was <strong>31 <code>feat(publish)</code> commits</strong> plus a dedicated <code>test(publish)</code>
series, and it shipped across two PR phases.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="feature-line-3--agentic-copilots-on-the-claude-agent-sdk">Feature line 3 — Agentic copilots on the Claude Agent SDK<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#feature-line-3--agentic-copilots-on-the-claude-agent-sdk" class="hash-link" aria-label="Enlace directo al Feature line 3 — Agentic copilots on the Claude Agent SDK" title="Enlace directo al Feature line 3 — Agentic copilots on the Claude Agent SDK">​</a></h2>
<p>v1.0.8 brings the first two AI copilots into the platform, built on the Claude
Agent SDK and engineered so an administrator can turn them off entirely.</p>
<ul>
<li><strong>Study Designer</strong> — a read-only assistant that helps design a study from
inside the Studies workspace.</li>
<li><strong>Publication assistant</strong> — a copilot for the Publish module that helps draft
a manuscript, shipped first as a read-only phase and then as a
write-with-approval phase.</li>
<li>A <strong>generalized agent core</strong> so both copilots share one engine across multiple
profiles, rather than each reimplementing orchestration.</li>
<li>A <strong>runtime AI Agents toggle</strong> — a single admin switch that gates both
copilots, replacing the earlier per-feature flag.</li>
</ul>
<p>The design principle here is the same one that runs through the whole release:
capability behind an explicit, auditable control. The copilots are <strong>off by
default</strong>. An administrator turns them on. There is no quiet AI in the loop that
an operator did not choose.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="studies-v2">Studies v2<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#studies-v2" class="hash-link" aria-label="Enlace directo al Studies v2" title="Enlace directo al Studies v2">​</a></h2>
<p>Underneath the new surfaces, the Studies engine took its v2 step:</p>
<ul>
<li><strong>Compiler Workbench v2</strong> was promoted to the default, with v1 fidelity
restored so nothing regressed in the switch.</li>
<li>A create-wizard shell with an eight-step stepper and a version popover wired
into the wizard footer.</li>
<li>A large post-flip audit closeout and <strong>204 new i18n keys</strong>, because a research
platform that only speaks English is not a serious international tool.</li>
<li><code>Study::scopeAccessibleBy</code> for collaborator lookups — the same scope the
Publish sharing model leans on.</li>
</ul>
<p><strong>11 <code>feat(studies)</code> commits</strong>, plus the workbench promotion.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-proof-hypertension-v3-end-to-end-on-a-1m-patient-cdm">The proof: Hypertension v3, end to end on a 1M-patient CDM<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-proof-hypertension-v3-end-to-end-on-a-1m-patient-cdm" class="hash-link" aria-label="Enlace directo al The proof: Hypertension v3, end to end on a 1M-patient CDM" title="Enlace directo al The proof: Hypertension v3, end to end on a 1M-patient CDM">​</a></h2>
<p>A platform feature is a hypothesis until a real study exercises it. So the
Hypertension v3 outcomes study was redesigned and run end-to-end on the Acumenus
OMOP CDM — roughly <strong>one million patients</strong> — as a working test of the new
surfaces.</p>
<ul>
<li>the v3 cohort was redesigned and the manuscript updated,</li>
<li><strong>12 OHDSI negative controls</strong> were added with empirical-null calibration,
the standard OHDSI method for quantifying residual systematic error, and</li>
<li>the whole thing ran end-to-end on the 1M-patient CDM.</li>
</ul>
<p>This is the difference between "we built a lifecycle and a publish module" and
"we used the lifecycle and the publish module to run and write up a real
study." The dog food is the validation.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="by-the-numbers">By the numbers<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#by-the-numbers" class="hash-link" aria-label="Enlace directo al By the numbers" title="Enlace directo al By the numbers">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Window           v1.0.7 → v1.0.8, May 10–28 (18 days)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Commits          200+</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">feat(library)    33    — lifecycle model, API, list UX, admin console, cleanup</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">feat(publish)    31    — drafts, autosave, snapshots, sharing, library</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">feat(studies)    11    — Compiler Workbench v2, wizard, audit closeout</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">docs             46    — devlogs, release notes, plans, this blog</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">agents                 — Study Designer, Publication assistant, shared core, toggle</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">i18n             204 new keys in Studies v2</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Three feature lines — Publish, Library Lifecycle, and Agentic Copilots — landed
together rather than in sequence, which is the harder way to ship but the right
one when the lines share models (<code>Study::scopeAccessibleBy</code> underpins both
Publish sharing and Studies v2) and surfaces (the Publication agent lives inside
the Publish module).</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="engineering-discipline">Engineering discipline<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#engineering-discipline" class="hash-link" aria-label="Enlace directo al Engineering discipline" title="Enlace directo al Engineering discipline">​</a></h2>
<p>The release was held to the same gates that guard every change. The pre-commit
hook runs Pint, PHPStan at level 8, TypeScript <code>tsc --noEmit</code>, ESLint at zero
warnings, and Vitest before any commit lands; CI re-runs the same checks plus
the stricter production Vite build. The lifecycle work in particular carries a
dedicated backend suite — per-entity lifecycle, bulk lifecycle, the
super-admin policy, the trait-applied check, the admin controller, and a
migration-columns test — alongside frontend tests for the list pages and the
admin console.</p>
<p>Two examples of the discipline in practice. The admin console's frontend-to-
backend slug map is verified byte-for-byte against the actual route slugs, so a
typo cannot ship a runtime 404. And the migrations are additive and idempotent —
the lifecycle columns were added without a destructive rebuild, applied through
a migrator role that has DDL rights the runtime role deliberately does not.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="upgrade-notes">Upgrade notes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#upgrade-notes" class="hash-link" aria-label="Enlace directo al Upgrade notes" title="Enlace directo al Upgrade notes">​</a></h2>
<p>For most environments <code>git pull &amp;&amp; ./deploy.sh</code> is enough. Two release-specific
steps:</p>
<ul>
<li>Run <code>./deploy.sh --db</code> to apply the idempotent lifecycle migrations, then
<code>php artisan library:backfill-lifecycle</code> once to set lifecycle state on
pre-existing rows.</li>
<li>The <strong>AI Agents</strong> (Study Designer and Publication assistant) are <strong>off by
default</strong>. Enable them from the admin AI Agents toggle when you are ready; the
legacy per-feature flag is no longer read.</li>
</ul>
<p>The nightly purge and cleanup-suggestion jobs schedule themselves. The full
per-PR changelog is in the
<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots">v1.0.8 release notes</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-v108-sets-up-next">What v1.0.8 sets up next<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#what-v108-sets-up-next" class="hash-link" aria-label="Enlace directo al What v1.0.8 sets up next" title="Enlace directo al What v1.0.8 sets up next">​</a></h2>
<p>The lifecycle is now the substrate for things that were awkward before it
existed: a true publication workflow where "active" means "citable," richer
cleanup analytics built on the suggestion cache, and agentic copilots that can
reason about artifact state ("this cohort is still a draft — promote it before
attaching it to the study?"). The Publish module is the natural home for the
manuscript end of that workflow, and the agent toggle is the governance model
for letting AI participate in it safely.</p>
<p>That is what makes v1.0.8 a milestone rather than a point release. It is not one
feature. It is a research surface that finally has the three things a serious
team needs: a way to keep the library clean, a way to publish what the research
produced, and assistance in the two hardest authoring tasks — all of it behind
controls an administrator owns.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-principle-going-forward">The principle going forward<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-research-surface-grows-up#the-principle-going-forward" class="hash-link" aria-label="Enlace directo al The principle going forward" title="Enlace directo al The principle going forward">​</a></h2>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Artifacts have state.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">State is enforced in one trait, authorized in one policy.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Admin reuses user endpoints; it does not fork them.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Capability lives behind explicit, auditable controls.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">A real study is the validation, not a checklist.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Parthenon v1.0.8 is live. Pull it, run <code>library:backfill-lifecycle</code>, and go
archive the six stale cohorts you already know are cluttering the list.</p>]]></content:encoded>
            <category>release</category>
            <category>milestone</category>
            <category>library-lifecycle</category>
            <category>publish</category>
            <category>agents</category>
            <category>studies</category>
            <category>ohdsi</category>
        </item>
        <item>
            <title><![CDATA[Parthenon v1.0.8 — Publish, Library Lifecycle, and Agentic Copilots]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots</guid>
            <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Three intertwined feature lines land together: the Publish module (server-side drafts, autosave, snapshots, study-scoped sharing), Library Lifecycle management with an admin console, and the first two Claude Agent SDK copilots — 205 commits in 18 days.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="v108--publish-library-lifecycle-and-agentic-copilots">v1.0.8 — Publish, Library Lifecycle, and Agentic Copilots<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#v108--publish-library-lifecycle-and-agentic-copilots" class="hash-link" aria-label="Enlace directo al v1.0.8 — Publish, Library Lifecycle, and Agentic Copilots" title="Enlace directo al v1.0.8 — Publish, Library Lifecycle, and Agentic Copilots">​</a></h2>
<p>After the v1.0.7 platform/architecture release (CE/EE fork, extension
points, AGPLv3), v1.0.8 returns to the research surface and lands three
intertwined feature lines at once: the <strong>Publish module</strong> for authoring and
sharing study write-ups, <strong>Library Lifecycle management</strong> that gives every
cohort, concept set, and analysis a draft → active → archived state
machine plus an admin console, and the first two
<strong>Claude Agent SDK copilots</strong> — a Study Designer and a Publication
assistant — gated behind a single runtime toggle.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="publish-module">Publish module<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#publish-module" class="hash-link" aria-label="Enlace directo al Publish module" title="Enlace directo al Publish module">​</a></h3>
<p>A full authoring surface for turning a study into a shareable write-up,
persisted server-side with no "lost work" failure modes:</p>
<ul>
<li><strong>Server-side drafts</strong> — <code>PublishPage</code> loads and saves drafts through the
API rather than browser state, with a stable <code>documentHash</code> for autosave
deduplication</li>
<li><strong>Debounced autosave</strong> with retry and a <code>beforeunload</code> guard, surfaced via
a <code>SaveStatusIndicator</code>, <code>SaveDraftButton</code>, and a <code>HybridPromptModal</code> that
prompts to save on first edit</li>
<li><strong>Snapshots</strong> — <code>PublicationSnapshotService</code> with create/list/revert
endpoints under optimistic locking, wired into <code>CreateSnapshotModal</code>,
<code>RevertSnapshotDialog</code>, and a <code>SnapshotsPanel</code></li>
<li><strong>Study-scoped sharing</strong> — <code>PublicationDraftPolicy</code>, per-draft <code>visibility</code>
and <code>updated_by_user_id</code>, a <code>VisibilityBadge</code>, a <code>ShareDropdown</code>, and a
read-only wizard mode for viewer collaborators (<code>Study::scopeAccessibleBy</code>)</li>
<li><strong>Publication library</strong> — <code>/publish/library</code> route + <code>PublicationLibraryPage</code></li>
</ul>
<p>Shipped across PR #339 (Phase 1) and PR #347.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="library-lifecycle-management">Library Lifecycle management<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#library-lifecycle-management" class="hash-link" aria-label="Enlace directo al Library Lifecycle management" title="Enlace directo al Library Lifecycle management">​</a></h3>
<p>Every library artifact — cohort definitions, concept sets, and the eight
analysis types — now carries a lifecycle state, with the plumbing to manage
it at both user and admin scale.</p>
<p><strong>Model + API (Phases A–B)</strong></p>
<ul>
<li><code>HasLibraryLifecycle</code> trait with <code>draft</code> / <code>active</code> / <code>archived</code>
transitions, reapplied to 9 models</li>
<li>Lifecycle columns on <code>concept_sets</code>, <code>cohort_definitions</code>, and 8 analyses
tables (folding in the prior <code>deprecated_at</code>)</li>
<li>Owner + super-admin lifecycle policies</li>
<li><code>promote</code> / <code>archive</code> / <code>restore</code> endpoints plus bulk-archive and
bulk-restore</li>
<li><code>RequiresPromotionException</code> → <strong>409 contract</strong>, with an auto-promote flow
when a draft artifact is attached to a study</li>
<li>Default scope hides drafts (for non-owners) and archived items</li>
</ul>
<p><strong>List-page UX (Phase B7–B9)</strong></p>
<ul>
<li>Status tabs with live counts on the cohort-definitions, concept-sets, and
analyses list pages</li>
<li>Super-admin <code>scope=all</code> on list endpoints with an <code>AllUsersToggle</code>
(D1–D2)</li>
</ul>
<p><strong>Admin console (Phase D3–D9)</strong></p>
<ul>
<li><code>/admin/library</code> unified index across all artifact types (D3)</li>
<li>Hard-delete with attachment preflight + audit (D4) — preflight matches
analyses by fully-qualified class name</li>
<li>Nightly 30-day purge of soft-deleted items (D5)</li>
<li>Owner reassignment with permission check + audit (D6)</li>
<li>Bulk delete, reassign, and trash on the admin page (D7)</li>
<li><code>library:backfill-lifecycle</code> command for existing rows (D8)</li>
<li>One-time lifecycle notice toast for end users (D9)</li>
</ul>
<p><strong>Cleanup suggestions (Phase C1–C3)</strong></p>
<ul>
<li>Nightly <code>SuggestLibraryCleanupJob</code> (C1), a cache table + model, an API
endpoint (C2), and a suggestions page + banner (C3) surfacing unused
artifacts</li>
</ul>
<p>33 <code>feat(library)</code> commits, landed across PR #339 and the D-phase series.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="claude-agent-sdk-copilots">Claude Agent SDK copilots<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#claude-agent-sdk-copilots" class="hash-link" aria-label="Enlace directo al Claude Agent SDK copilots" title="Enlace directo al Claude Agent SDK copilots">​</a></h3>
<p>The first two agentic copilots, built on the Claude Agent SDK and gated so
they can be turned off entirely:</p>
<ul>
<li><strong>Study Designer</strong> (PR #343) — a read-only slice (Phase 0+1) that assists
study design from inside the Studies workspace</li>
<li><strong>Publication agent</strong> (PR #347) — assists manuscript drafting in the
Publish module, shipped as a read-only Phase 1 and a write/approval
Phase 2</li>
<li><strong>Generalized agent core</strong> (PR #346) — the agent core was refactored for
multi-profile use so both copilots share one engine (Phase B)</li>
<li><strong>Runtime AI Agents toggle</strong> (PR #348) — a single admin switch that gates
both copilots, replacing the earlier <code>publish.agent</code> feature flag</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="studies-v2">Studies v2<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#studies-v2" class="hash-link" aria-label="Enlace directo al Studies v2" title="Enlace directo al Studies v2">​</a></h3>
<ul>
<li><strong>Compiler Workbench v2</strong> promoted to default, with v1 fidelity restored</li>
<li>Create wizard shell with an 8-step stepper (Phase 3) and a version popover
wired to the wizard footer</li>
<li>Post-flip audit closeout (H1–H5, M1–M19, L3–L4) plus 204 new i18n keys</li>
<li><code>Study::scopeAccessibleBy</code> for collaborator lookups</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="hypertension-v3-outcomes-study">Hypertension v3 outcomes study<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#hypertension-v3-outcomes-study" class="hash-link" aria-label="Enlace directo al Hypertension v3 outcomes study" title="Enlace directo al Hypertension v3 outcomes study">​</a></h3>
<p>The Hypertension v3 study was redesigned and run end-to-end on the Acumenus
OMOP CDM as a real-world exercise of the new surfaces:</p>
<ul>
<li>v3 cohort redesign + manuscript update</li>
<li>12 OHDSI negative controls with empirical-null calibration</li>
<li>End-to-end study run on the Acumenus OMOP CDM (1M patients)</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="dependencies">Dependencies<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#dependencies" class="hash-link" aria-label="Enlace directo al Dependencies" title="Enlace directo al Dependencies">​</a></h3>
<ul>
<li>Documented all directly-imported Python dependencies in the AI service
requirements</li>
<li><code>umap-learn</code> <code>&gt;=0.5.0</code> → <code>&gt;=0.5.12</code>; <code>python-multipart</code> <code>&gt;=0.0.27</code> →
<code>&gt;=0.0.29</code> (PRs #330, #331, #344)</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="upgrade-notes">Upgrade notes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#upgrade-notes" class="hash-link" aria-label="Enlace directo al Upgrade notes" title="Enlace directo al Upgrade notes">​</a></h3>
<ul>
<li><code>git pull &amp;&amp; ./deploy.sh</code> is sufficient for most environments. The
lifecycle columns are added by idempotent migrations; run
<code>./deploy.sh --db</code> to apply them.</li>
<li>Run <code>php artisan library:backfill-lifecycle</code> once to set lifecycle state
on pre-existing library rows.</li>
<li><strong>AI Agents</strong> (Study Designer + Publication assistant) are <strong>off by
default</strong> — enable them from the admin AI Agents toggle. The legacy
<code>publish.agent</code> flag is no longer read.</li>
<li>The nightly purge and cleanup-suggestion jobs are scheduled
automatically; no action required.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="by-the-numbers">By the numbers<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#by-the-numbers" class="hash-link" aria-label="Enlace directo al By the numbers" title="Enlace directo al By the numbers">​</a></h3>
<ul>
<li><strong>205 commits</strong> since v1.0.7 over 18 days</li>
<li><strong>33 <code>feat(library)</code></strong>, <strong>31 <code>feat(publish)</code></strong>, <strong>11 <code>feat(studies)</code></strong>,
plus the agent-core and copilot work</li>
<li><strong>3 feature lines</strong> landed together: Publish, Library Lifecycle, and
Agentic Copilots</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="contributors">Contributors<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-8-publish-library-lifecycle-agentic-copilots#contributors" class="hash-link" aria-label="Enlace directo al Contributors" title="Enlace directo al Contributors">​</a></h3>
<p>Claude Code + @sudoshi</p>]]></content:encoded>
            <category>release</category>
            <category>publish</category>
            <category>library-lifecycle</category>
            <category>agents</category>
            <category>studies</category>
        </item>
        <item>
            <title><![CDATA[From One-Shot Prompts to Autonomous Copilots: The Claude Agent SDK Comes to Parthenon]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[For a year, every AI feature in Parthenon spoke to a model the same way: build a prompt, send it, get one answer back. Abby answers a question. The publication writer drafts a paragraph. Useful — but fundamentally a vending machine. You put a prompt in, a completion comes out, and the model never gets to look around, use a tool, or change its mind.]]></description>
            <content:encoded><![CDATA[<p>For a year, every AI feature in Parthenon spoke to a model the same way: build a prompt, send it, get one answer back. Abby answers a question. The publication writer drafts a paragraph. Useful — but fundamentally a vending machine. You put a prompt in, a completion comes out, and the model never gets to <em>look around</em>, <em>use a tool</em>, or <em>change its mind</em>.</p>
<p>This milestone changes that. Parthenon now runs genuine <strong>agentic copilots</strong> — built on Anthropic's <strong>Claude Agent SDK</strong>, the same autonomous loop that powers Claude Code — inside two workflows: the <strong>Study Designer</strong> and the <strong>Publication assistant</strong>. The agent decides which tools to call, iterates (search → draft → validate → refine), keeps a session across turns, streams its work to the browser, and — critically — <em>asks for permission</em> before it writes anything. It ships on a reusable, profile-agnostic core, behind a super-admin runtime switch, with PHP still holding the pen on every database write.</p>
<p>This post is the full story: the architecture, the four pull requests that built it, the human-in-the-loop approval gate, and the engineering discipline (and bugs) along the way.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-an-agent-and-not-another-chatbot">Why an agent, and not another chatbot<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#why-an-agent-and-not-another-chatbot" class="hash-link" aria-label="Enlace directo al Why an agent, and not another chatbot" title="Enlace directo al Why an agent, and not another chatbot">​</a></h2>
<p>The distinction that matters is <strong>tools plus iteration</strong>. A one-shot call (<code>$this-&gt;llm-&gt;chat(...)</code>) is a single request/response with a hand-built prompt — no tools, no loop, no memory. It is perfect when a single structured answer suffices, and we keep it exactly where it belongs.</p>
<p>An <em>agent</em> is different. Given a goal, it reasons about which tool to call next, calls it, reads the result, and decides what to do with it — looping until the job is done. In the Study Designer, that looks like: <em>search the OMOP vocabulary → confirm the concept ids → draft a concept set → read the Compiler's readiness guidance → refine.</em> No human stitches those steps together; the model orchestrates them.</p>
<p>The Claude Agent SDK gives us that loop as a programmable Python library. Because the SDK shells out to the Claude Code CLI, it runs only in our Python service (<code>python-ai</code>), never in PHP or the browser.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-architecture-the-agent-orchestrates-php-still-owns-the-writes">The architecture: the agent orchestrates, PHP still owns the writes<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#the-architecture-the-agent-orchestrates-php-still-owns-the-writes" class="hash-link" aria-label="Enlace directo al The architecture: the agent orchestrates, PHP still owns the writes" title="Enlace directo al The architecture: the agent orchestrates, PHP still owns the writes">​</a></h2>
<p>The single most important design decision: <strong>the agent is an orchestration brain, and its tools are thin authenticated HTTP clients to existing Laravel endpoints.</strong> The agent never touches the database, the filesystem, or a shell. Every write still flows through Laravel — same validation, same RBAC, same audit trail.</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Browser (React copilot panel)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   │  ① POST start / message / approve         ④ Reverb events (WebSocket)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   ▼                                            ▲</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Laravel (Sanctum + RBAC)                        │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • mints a short-lived, RBAC-scoped token     │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • /broadcasting/auth (channel ownership)     │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • existing feature routes  ◄─────────────────┼── ③ tool callbacks (Bearer = scoped token)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   │  ② start/turn (internal HTTP)              │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   ▼                                            │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">python-ai (Claude Agent SDK)                    │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • runs one turn, streams events              │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • profile = system prompt + tool pack        │</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • in-process MCP tools → call Laravel ───────┘</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">   • publishes events → Reverb (Pusher HTTP) ───► ④</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>A few invariants hold this together:</p>
<ul>
<li><strong>PHP is the write authority.</strong> Agent tools are thin clients; all writes, validation, audit, and RBAC happen in Laravel.</li>
<li><strong><code>python-ai</code> is internal-only.</strong> The browser talks to Laravel; Laravel talks to <code>python-ai</code>; <code>python-ai</code> calls back to Laravel as the user. The browser never reaches the agent service directly.</li>
<li><strong>The agent acts <em>as</em> the user.</strong> Laravel mints a short-lived, RBAC-scoped Sanctum token per session; the agent's tool callbacks carry it. The agent can never exceed the user's permissions.</li>
<li><strong>Streaming is best-effort; Laravel is authoritative.</strong> Live tokens stream over Reverb (fail-open), but durable state — cost, tokens, session id, status — is persisted to a Laravel table, so a reconnecting client always reads the truth.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="a-clinical-grade-lockdown">A clinical-grade lockdown<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#a-clinical-grade-lockdown" class="hash-link" aria-label="Enlace directo al A clinical-grade lockdown" title="Enlace directo al A clinical-grade lockdown">​</a></h3>
<p>Healthcare data demands paranoia. The agent runs with <strong>every built-in tool removed</strong> (<code>tools=[]</code> strips Bash, Read, Edit, Write, Glob, Grep, WebSearch, WebFetch), no developer config bleed-in (<code>setting_sources=[]</code>), no stray MCP servers (<code>strict_mcp_config=True</code>), and a headless permission posture. The <em>only</em> capabilities it can reach are our own in-process tools, namespaced <code>mcp__parthenon__*</code>. It cannot read the filesystem, run a shell, or browse the web. It can search vocabulary, read guidance, and — with approval — stage a draft.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-journey-in-four-pull-requests">The journey, in four pull requests<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#the-journey-in-four-pull-requests" class="hash-link" aria-label="Enlace directo al The journey, in four pull requests" title="Enlace directo al The journey, in four pull requests">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="343--foundation-the-study-designer-copilot">#343 — Foundation: the Study Designer copilot<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#343--foundation-the-study-designer-copilot" class="hash-link" aria-label="Enlace directo al #343 — Foundation: the Study Designer copilot" title="Enlace directo al #343 — Foundation: the Study Designer copilot">​</a></h3>
<p>The first PR built the reusable core <em>and</em> the first profile: a read-only Study Designer assistant. The core pieces — a Reverb publisher, a turn-running service, an in-memory session registry with a per-session lock, and an "agent profile" (system prompt + model + effort) — were designed once to be shared.</p>
<p>It also surfaced eight real bugs that never show up in a happy-path demo: an Echo subscription that re-subscribed on every streamed event (and dropped tokens mid-turn), a React double-start that minted duplicate sessions, a leaked Sanctum token when the downstream call failed, a global semaphore where a per-session lock belonged, and a Zod schema that threw on a null token count. We caught and fixed every one. They are now a written "gotchas catalogue" so the next feature avoids them.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="346--generalizing-the-core-one-profile-becomes-many">#346 — Generalizing the core: one profile becomes many<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#346--generalizing-the-core-one-profile-becomes-many" class="hash-link" aria-label="Enlace directo al #346 — Generalizing the core: one profile becomes many" title="Enlace directo al #346 — Generalizing the core: one profile becomes many">​</a></h3>
<p>The Study Designer core was, understandably, <em>named</em> for Study Designer. Before adding a second profile we generalized it — non-destructively:</p>
<ul>
<li><code>StudyDesignToolContext</code> became a generic <code>AgentToolContext</code> (a scoped token plus a feature-specific bag of ids).</li>
<li>A <strong>tool-pack registry</strong> maps a profile name to its tool builder.</li>
<li>The feature-named router became a generic <code>/agent</code> router; Laravel now hands the agent its channel name and callback path, so the Python service carries <em>no</em> domain knowledge.</li>
<li>A single generic <code>agent_sessions</code> table — keyed by <code>(profile, subject_type, subject_id)</code> — replaced the per-feature table. The migration <strong>creates and copies; it never drops</strong> the old table. (Non-destructive by default is a house rule here.)</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="347--the-publication-assistant-grounding-then-writing">#347 — The Publication assistant: grounding, then writing<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#347--the-publication-assistant-grounding-then-writing" class="hash-link" aria-label="Enlace directo al #347 — The Publication assistant: grounding, then writing" title="Enlace directo al #347 — The Publication assistant: grounding, then writing">​</a></h3>
<p>The Publish page already generated narrative text with a one-shot call. The agentic version turns it into a true assistant: it pulls a study's real analyses, drafts each IMRAD manuscript section grounded <strong>only</strong> in those results (never inventing a statistic), and iterates on feedback.</p>
<p>Phase 1 shipped the read-only slice — research and draft, nothing saved. Phase 2 added the part that makes agents genuinely useful <em>and</em> genuinely dangerous: <strong>the ability to write</strong>, gated behind explicit human approval.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="348--a-runtime-switch-dark-launch-done-right">#348 — A runtime switch: dark launch done right<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#348--a-runtime-switch-dark-launch-done-right" class="hash-link" aria-label="Enlace directo al #348 — A runtime switch: dark launch done right" title="Enlace directo al #348 — A runtime switch: dark launch done right">​</a></h3>
<p>Finally, a single super-admin toggle on the AI Providers page — backed by a runtime feature flag, <code>ai.agents</code> — that enables or disables every copilot at once, instantly, with no redeploy. More on why that matters below.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-approval-gate-a-human-stays-in-the-loop">The approval gate: a human stays in the loop<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#the-approval-gate-a-human-stays-in-the-loop" class="hash-link" aria-label="Enlace directo al The approval gate: a human stays in the loop" title="Enlace directo al The approval gate: a human stays in the loop">​</a></h2>
<p>An agent that can call <code>update_draft</code> or <code>create_snapshot</code> unsupervised is a liability. So write tools are not auto-approved. They are deliberately <em>excluded</em> from the agent's allow-list, which routes them through the SDK's <code>can_use_tool</code> callback — our permission checkpoint.</p>
<p>When the agent wants a write, the flow is:</p>
<ol>
<li>The <code>can_use_tool</code> callback fires. Read tools (already allow-listed) sail through; unknown tools fail <strong>closed</strong>; a write tool is intercepted.</li>
<li>The service publishes an <code>agent.approval.request</code> event over Reverb and <strong>blocks on an <code>asyncio.Future</code></strong>, keyed by the tool-use id (with a timeout).</li>
<li>The copilot panel renders an <strong>Approve / Reject</strong> card describing exactly what the agent wants to do.</li>
<li>The author's decision posts to a Laravel endpoint that forwards it to <code>python-ai</code>, which resolves the future — <code>Allow</code> runs the write, <code>Reject</code> (or timeout) denies it.</li>
</ol>
<p>The result: the agent proposes, the human disposes, and the actual write still travels the proven Laravel draft/snapshot machinery.</p>
<p>We also closed a subtle authorization gap. The agent's scoped token is now <em>enforced</em> on the write routes via Sanctum <code>abilities:</code> middleware — so even though the agent runs as the user, it is constrained to exactly the write scope it was granted. (Regular users, whose tokens carry the wildcard ability, are unaffected.)</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="shipping-safely-decoupling-deploy-from-release">Shipping safely: decoupling deploy from release<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#shipping-safely-decoupling-deploy-from-release" class="hash-link" aria-label="Enlace directo al Shipping safely: decoupling deploy from release" title="Enlace directo al Shipping safely: decoupling deploy from release">​</a></h2>
<p>Here is a thing we believe in: <strong>deploying code and releasing a feature are two different events.</strong> A clinical platform should be able to merge, build, and ship an unfinished or externally-dependent feature to production without exposing it to a single user — and turn it on (or off) with one switch.</p>
<p>That is what the <code>ai.agents</code> flag buys us. It is resolved at runtime from a system setting, surfaced on the AI Providers admin page, and read by both copilots. With it off (the default), the copilots simply do not render — no sessions, no tokens, no surprises. A super-admin flips it on when the prerequisites are met, and can flip it off the instant anything looks wrong, with no redeploy and no revert. The toggle even reports whether an Anthropic provider is configured, so an admin knows <em>why</em> agents may be inactive.</p>
<p>The agentic copilots are live in production today — <strong>dark</strong>, behind that flag — while we complete final validation.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="engineering-rigor-and-a-few-scars">Engineering rigor (and a few scars)<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#engineering-rigor-and-a-few-scars" class="hash-link" aria-label="Enlace directo al Engineering rigor (and a few scars)" title="Enlace directo al Engineering rigor (and a few scars)">​</a></h2>
<p>Every layer was built test-first and gated by CI: Pest and PHPStan level 8 on the backend, <code>tsc</code> and a stricter <code>vite build</code> and Vitest on the frontend, <code>pytest</code> and <code>mypy</code> on the Python service. Each pull request was reviewed by an adversarial second pass whose only job was to find what the builder missed.</p>
<p>A few hard-won lessons we wrote down so we never relearn them:</p>
<ul>
<li><strong>The SDK is the source of truth, not the docs.</strong> We pinned against the <em>actually published</em> <code>claude-agent-sdk</code> and verified its permission types and option kwargs inside the container before relying on them.</li>
<li><strong><code>Sanctum::actingAs($user)</code> defaults to <em>empty</em> abilities, not the wildcard.</strong> A test that "proves" a real login token passes must request the wildcard explicitly — otherwise it proves the opposite of production behavior.</li>
<li><strong>Channel-authorization tests must not depend on the broadcast driver.</strong> The null test broadcaster doesn't enforce channel callbacks, so we assert the authorization predicate directly.</li>
<li><strong>Verify your branch before every commit.</strong> A concurrent process can move your working tree out from under you; when work looks "lost," the reflog and the commit object almost always have it.</li>
</ul>
<p>The whole pattern is now captured in an internal playbook so the <em>third</em> agentic feature is faster and safer than the second was.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's next<a href="https://parthenon.acumenus.net/docs/es/blog/claude-agent-sdk-agentic-copilots#whats-next" class="hash-link" aria-label="Enlace directo al What's next" title="Enlace directo al What's next">​</a></h2>
<p>The runtime is built, tested, merged, and deployed. The remaining path to a lit-up feature is short and operational: fund the model account, flip the toggle, and run a live end-to-end approval round-trip to validate the one interaction that only the real model can exercise. Beyond that, we have a tracked hardening list — session idle-eviction and token revocation on close, true admission control at the boundary, and PHI and load/cost reviews — none of it blocking, all of it on the board.</p>
<p>From a vending machine to a colleague that reasons, asks permission, and shows its work — grounded in your real OMOP data, and never allowed to write without a human nod. That is the kind of AI a clinical research platform can actually trust.</p>]]></content:encoded>
            <category>development</category>
            <category>ai</category>
            <category>agents</category>
            <category>claude</category>
            <category>studies</category>
            <category>publish</category>
            <category>architecture</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[Parthenon EE Kubernetes Foundation: A Second Deployment Lane]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Parthenon Enterprise now has a Kubernetes deployment foundation: a Helm chart, a Terraform existing-cluster module, explicit native-vs-Kubernetes boundaries, validation gates, and a critical path toward EKS, AKS, and GKE.]]></description>
            <content:encoded><![CDATA[<p>Parthenon Enterprise Edition has crossed an important infrastructure
milestone: it now has a real Kubernetes deployment foundation.</p>
<p>This is not a marketing placeholder, and it is not a pile of disconnected YAML.
The new work establishes a second, deliberately separate deployment lane beside
the host-native VPS and bare-metal installer track. The native path remains
focused on non-Docker Linux hosts. The Kubernetes path now has its own
architecture, Helm chart, Terraform module, existing-cluster example, validation
evidence, and todo trail toward cloud-provider orchestration on AWS, Azure, and
GCP.</p>
<p>The point of this milestone is simple: Parthenon EE can support two very
different enterprise realities without letting them blur into one fragile
installer.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-this-matters">Why this matters<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#why-this-matters" class="hash-link" aria-label="Enlace directo al Why this matters" title="Enlace directo al Why this matters">​</a></h2>
<p>Enterprise healthcare deployments rarely come in one shape.</p>
<p>Some teams want a single VPS or bare-metal host they can reason about directly:
system packages, systemd units, nginx, PHP-FPM, local PostgreSQL, Redis, Solr,
and a browser-reachable service. That is the native installer track. It matters
for hospitals, research groups, air-gapped environments, small teams, and
operators who need a direct Linux install without Docker or Kubernetes.</p>
<p>Other teams already run Kubernetes as their standard operating model. They
expect workloads to be packaged as containers, configured with Helm, deployed by
Terraform, connected to managed PostgreSQL and Redis, exposed by ingress, wired
to cloud secrets, and monitored through cluster-native controls. That is a
different product surface. It deserves a different deployment architecture.</p>
<p>The milestone this week is that Parthenon EE now has the beginning of that
second surface.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-architectural-decision">The architectural decision<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-architectural-decision" class="hash-link" aria-label="Enlace directo al The architectural decision" title="Enlace directo al The architectural decision">​</a></h2>
<p>The key decision was to keep the deployment lanes separate:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Native VPS / bare metal</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  Terraform -&gt; SSH -&gt; native installer contracts -&gt; Linux host</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Kubernetes</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  Terraform -&gt; Kubernetes API -&gt; Helm release -&gt; container workloads</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That split sounds obvious, but it is easy to get wrong. Terraform can be abused
into a second installer. Helm can be forced to run host-level bootstraps it was
never meant to own. A native installer can quietly grow Kubernetes assumptions.
All three paths lead to a support problem.</p>
<p>The architecture now draws a hard boundary.</p>
<p>The native track owns:</p>
<ul>
<li>operating-system capability detection</li>
<li>package-manager differences</li>
<li>filesystem layout</li>
<li>service users and groups</li>
<li>nginx and PHP-FPM host configuration</li>
<li>systemd units</li>
<li>local PostgreSQL, Redis, and Solr policy</li>
<li>host-native health, backup, and restore contracts</li>
<li>LAN and public browser exposure for VPS installs</li>
</ul>
<p>The Kubernetes track owns:</p>
<ul>
<li>Kubernetes namespace and workload manifests</li>
<li>packaged EE PHP and nginx images</li>
<li>Deployments, Services, Ingress, Jobs, CronJobs, PVCs, probes, labels, and
resource requests</li>
<li>external dependency wiring for PostgreSQL, Redis, and Solr</li>
<li>image pull secrets and runtime Secret references</li>
<li>Helm release lifecycle</li>
<li>future EKS, AKS, and GKE orchestration through shared Terraform modules</li>
</ul>
<p>That is the foundation for a maintainable enterprise platform: two deployment
lanes, one product, no accidental overlap.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-shipped">What shipped<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#what-shipped" class="hash-link" aria-label="Enlace directo al What shipped" title="Enlace directo al What shipped">​</a></h2>
<p>The first Kubernetes foundation landed as a focused EE branch with four main
parts.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">enterprise/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  helm/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    parthenon-ee/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      Chart.yaml</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      values.yaml</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      values.schema.json</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      templates/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  terraform/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    modules/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      kubernetes-helm-release/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    examples/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">      existing-kubernetes/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  docs/</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    kubernetes-helm-terraform-architecture.md</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    kubernetes-helm-terraform-todo.md</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The branch does not try to solve every cloud production question in one pass.
That restraint is intentional. The milestone is a foundation that can be
validated, reviewed, and extended.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-helm-chart">The Helm chart<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-helm-chart" class="hash-link" aria-label="Enlace directo al The Helm chart" title="Enlace directo al The Helm chart">​</a></h2>
<p>The chart is named <code>parthenon-ee</code>. It targets the packaged Enterprise images:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">ghcr.io/acumenus-data-sciences/parthenon-ee-php:&lt;tag&gt;</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">ghcr.io/acumenus-data-sciences/parthenon-ee-nginx:&lt;tag&gt;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The chart models the core application runtime:</p>
<ul>
<li>PHP-FPM application deployment</li>
<li>nginx/frontend/docs/OHIF deployment</li>
<li>Horizon worker deployment</li>
<li>Reverb websocket deployment</li>
<li>scheduler CronJob</li>
<li>migration Job</li>
<li>runtime ConfigMap</li>
<li>optional runtime Secret creation for local drills</li>
<li>PVCs for Laravel storage and bootstrap cache</li>
<li>optional Ingress</li>
<li>optional HPAs</li>
<li>resource requests and memory limits</li>
<li>readiness and liveness probes</li>
<li>image pull secret support</li>
</ul>
<p>The chart is not pretending that Kubernetes production is just Compose in a
different syntax. It uses a Kubernetes-specific nginx template so it does not
depend on Docker's embedded DNS resolver. It renders Kubernetes Services for
the app workloads. It separates static frontend/docs/OHIF serving from PHP-FPM,
while keeping the existing Parthenon routing model intact.</p>
<p>The first chart also records one explicit compatibility bridge:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token key atrule" style="color:hsl(29, 54%, 61%)">composeCompatibleServiceNames</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token boolean important" style="color:hsl(220, 14%, 71%);font-weight:bold">true</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The packaged nginx image historically expects upstream names such as <code>php</code> and
<code>reverb</code>. The chart therefore defaults to short service names within the target
namespace. That is acceptable for one Parthenon EE release per namespace and
gets us to a testable first deployment faster.</p>
<p>It is not the final word. The todo already tracks the production decision:
either prove release-scoped service names with a Kubernetes-native nginx
template, or document one release per namespace as the supported service-name
model.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="external-state-by-default">External state by default<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#external-state-by-default" class="hash-link" aria-label="Enlace directo al External state by default" title="Enlace directo al External state by default">​</a></h2>
<p>The chart deliberately does not bundle PostgreSQL, Redis, or Solr as the
production default.</p>
<p>That is one of the most important decisions in the milestone.</p>
<p>For serious enterprise Kubernetes deployments, stateful services should usually
be operated independently:</p>
<ul>
<li>PostgreSQL on RDS, Azure Database for PostgreSQL, Cloud SQL, or a dedicated
operator-managed database</li>
<li>Redis on ElastiCache, Azure Cache for Redis, Memorystore, or an equivalent
managed/cache service</li>
<li>Solr as a managed or separately operated search tier with persistent storage,
core bootstrap, backup, and performance tuning</li>
<li>object storage on S3, Azure Blob, GCS, or an S3-compatible service</li>
</ul>
<p>In-cluster PostgreSQL, Redis, and Solr can be useful for development and
staging. They should not become the default enterprise production story.</p>
<p>The chart values reflect that:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token key atrule" style="color:hsl(29, 54%, 61%)">database</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">host</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> postgres.example.invalid</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">port</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">5432</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">database</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> parthenon</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">username</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> parthenon</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">redis</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">host</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> redis.example.invalid</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">port</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">6379</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">solr</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">mode</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> external</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">url</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> http</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain">//solr.example.invalid</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain">8983/solr</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>This keeps the first chart honest. It deploys application workloads and expects
production state to be managed with production-grade tools.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-terraform-module">The Terraform module<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-terraform-module" class="hash-link" aria-label="Enlace directo al The Terraform module" title="Enlace directo al The Terraform module">​</a></h2>
<p>The Terraform foundation is a shared module named
<code>kubernetes-helm-release</code>.</p>
<p>Its job is intentionally narrow:</p>
<ul>
<li>create or target a namespace</li>
<li>assemble base Helm values</li>
<li>pass image tag, endpoint, ingress, Secret, and persistence settings</li>
<li>install or upgrade the Helm release</li>
<li>expose release status and application URL outputs</li>
</ul>
<p>It does not create a cluster. It does not create a database. It does not create
Redis or Solr. It does not generate application secrets. That work belongs to
provider-specific modules and secret-management tooling.</p>
<p>The module boundary looks like this:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">module "parthenon_ee" {</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  source = "../../modules/kubernetes-helm-release"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  release_name        = "parthenon-ee"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  namespace           = "parthenon-ee"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  image_tag           = "vEE-0.1.0"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  app_url             = "https://parthenon.example.com"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  runtime_secret_name = "parthenon-ee-runtime"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  database_host     = "postgres.example.internal"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  database_name     = "parthenon"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  database_username = "parthenon"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  redis_host = "redis.example.internal"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  solr_url   = "http://solr.example.internal:8983/solr"</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That is the contract future cloud modules should call after they provision
infrastructure.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="existing-cluster-first">Existing cluster first<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#existing-cluster-first" class="hash-link" aria-label="Enlace directo al Existing cluster first" title="Enlace directo al Existing cluster first">​</a></h2>
<p>The first example is intentionally <code>existing-kubernetes</code>, not <code>aws-eks</code>,
<code>azure-aks</code>, or <code>gcp-gke</code>.</p>
<p>That ordering is deliberate. Before we encode AWS, Azure, or GCP assumptions,
we need a provider-neutral deployment that proves the application shape:</p>
<ul>
<li>the chart renders</li>
<li>Terraform can install it</li>
<li>runtime Secrets are referenced correctly</li>
<li>image pull secrets work</li>
<li>migrations run</li>
<li>probes behave</li>
<li>ingress routes traffic</li>
<li>PHP, nginx, Horizon, scheduler, Reverb, Redis, PostgreSQL, and Solr all line
up</li>
</ul>
<p>The existing-cluster example also becomes the inner module that cloud-specific
orchestration wraps. EKS, AKS, and GKE should not each invent their own
Parthenon release logic. They should provision their cloud resources and then
call the shared Kubernetes Helm module.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="secrets-are-not-values">Secrets are not values<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#secrets-are-not-values" class="hash-link" aria-label="Enlace directo al Secrets are not values" title="Enlace directo al Secrets are not values">​</a></h2>
<p>The chart can create a Kubernetes Secret for local drills, but the default is
to reference an existing Secret:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token key atrule" style="color:hsl(29, 54%, 61%)">secrets</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">create</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token boolean important" style="color:hsl(220, 14%, 71%);font-weight:bold">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token key atrule" style="color:hsl(29, 54%, 61%)">existingSecret</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> parthenon</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">-</span><span class="token plain">ee</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">-</span><span class="token plain">runtime</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Expected keys include:</p>
<ul>
<li><code>APP_KEY</code></li>
<li><code>DB_PASSWORD</code></li>
<li><code>REDIS_PASSWORD</code></li>
<li><code>LICENSE_TOKEN</code></li>
<li><code>PARTHENON_INTERNAL_TOKEN</code></li>
<li><code>ORTHANC_AUTH_HEADER</code> when Orthanc proxying is enabled</li>
</ul>
<p>That choice is about production hygiene. Terraform state is not where
application secrets should live by default. The production path should use
External Secrets, Sealed Secrets, SOPS, a cloud secret sync, or a similarly
auditable secret-management system. Terraform can name the Secret. Kubernetes
can mount it. A dedicated secret tool should own its contents.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-release-image-policy">The release image policy<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-release-image-policy" class="hash-link" aria-label="Enlace directo al The release image policy" title="Enlace directo al The release image policy">​</a></h2>
<p>The chart defaults to <code>latest</code> only because a chart needs a development
default. Production deployments should pass an explicit EE release tag:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">vEE-0.1.0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That matters because EE release traceability is tied to two artifacts:</p>
<ul>
<li>the Enterprise release tag used for the packaged EE images</li>
<li>the Community Edition source and runtime image pin embedded in those images</li>
</ul>
<p>The Kubernetes deployment path should eventually verify the deployed image
digests against release provenance metadata. That is already tracked in the
todo. The principle is clear: a production cluster should not be a mystery
about what it is running.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="validation-completed">Validation completed<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#validation-completed" class="hash-link" aria-label="Enlace directo al Validation completed" title="Enlace directo al Validation completed">​</a></h2>
<p>This milestone was not committed as unchecked scaffolding. The branch was
validated locally before it was pushed.</p>
<p>The Helm chart passed:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">helm lint enterprise/helm/parthenon-ee</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">helm template parthenon-ee enterprise/helm/parthenon-ee </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--set</span><span class="token plain"> </span><span class="token assign-left variable" style="color:hsl(207, 82%, 66%)">ingress.enabled</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain">true </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--set</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'ingress.hosts[0].host=parthenon.example.com'</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--set</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'ingress.hosts[0].paths[0].path=/'</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--set</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'ingress.hosts[0].paths[0].pathType=Prefix'</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The chart lint passes with only Helm's non-blocking icon recommendation.</p>
<p>The Terraform module and example passed:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">terraform </span><span class="token function" style="color:hsl(207, 82%, 66%)">fmt</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-check</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-recursive</span><span class="token plain"> enterprise/terraform</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token builtin class-name" style="color:hsl(29, 54%, 61%)">cd</span><span class="token plain"> enterprise/terraform/modules/kubernetes-helm-release</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">terraform init </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-backend</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain">false</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">terraform validate</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token builtin class-name" style="color:hsl(29, 54%, 61%)">cd</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">..</span><span class="token plain">/</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">..</span><span class="token plain">/examples/existing-kubernetes</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">terraform init </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-backend</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain">false</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">terraform validate</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Additional checks passed:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">yamllint enterprise/helm/parthenon-ee/Chart.yaml </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  enterprise/helm/parthenon-ee/values.yaml</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">python3 </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-m</span><span class="token plain"> json.tool enterprise/helm/parthenon-ee/values.schema.json</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token function" style="color:hsl(207, 82%, 66%)">git</span><span class="token plain"> </span><span class="token function" style="color:hsl(207, 82%, 66%)">diff</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--check</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">./scripts/verify-no-ce-patches.sh </span><span class="token function" style="color:hsl(207, 82%, 66%)">pr</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">graphify update </span><span class="token builtin class-name" style="color:hsl(29, 54%, 61%)">.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Two small issues were caught and fixed during validation:</p>
<ul>
<li>The first Terraform module draft used the older Helm provider v2 nested
<code>set</code> block style. Local validation resolved Helm provider v3, where <code>set</code>
and <code>set_sensitive</code> are attributes. The module now pins and uses the v3 API.</li>
<li>Helm lint rejected a multi-PVC template separator even though the chart
rendered. Splitting storage and bootstrap-cache PVCs into separate template
files removed the ambiguity.</li>
</ul>
<p>Those are exactly the kind of defects a foundation branch should catch early,
before provider modules multiply the pattern.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-this-does-not-claim-yet">What this does not claim yet<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#what-this-does-not-claim-yet" class="hash-link" aria-label="Enlace directo al What this does not claim yet" title="Enlace directo al What this does not claim yet">​</a></h2>
<p>This is a milestone, not a production-complete declaration.</p>
<p>It does not yet claim:</p>
<ul>
<li>a successful live Kubernetes install</li>
<li>EKS, AKS, or GKE module support</li>
<li>managed PostgreSQL provisioning</li>
<li>managed Redis provisioning</li>
<li>managed Solr provisioning</li>
<li>external secret backend integration</li>
<li>ingress/TLS automation for every cloud</li>
<li>Kubernetes backup and restore coverage</li>
<li>production network policy</li>
<li>pod disruption budgets</li>
<li>cost sizing</li>
<li>full observability guidance</li>
</ul>
<p>Those are not omissions. They are the next phases, written down explicitly
instead of hidden in a conversation.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-critical-path-from-here">The critical path from here<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-critical-path-from-here" class="hash-link" aria-label="Enlace directo al The critical path from here" title="Enlace directo al The critical path from here">​</a></h2>
<p>The todo now tracks a clear sequence.</p>
<p>First, add CI gates:</p>
<ul>
<li>Helm lint</li>
<li>Helm render with production-like values</li>
<li>Terraform formatting</li>
<li>Terraform validation for both native and Kubernetes modules</li>
</ul>
<p>Second, run a disposable cluster drill:</p>
<ul>
<li>create namespace</li>
<li>create runtime Secret</li>
<li>create image pull Secret</li>
<li>connect to external PostgreSQL</li>
<li>connect to external Redis</li>
<li>connect to external Solr</li>
<li>apply the existing-cluster Terraform example</li>
<li>capture <code>kubectl get pods</code>, <code>kubectl get jobs</code>, describes, and targeted logs</li>
</ul>
<p>Third, smoke the app through the configured ingress:</p>
<ul>
<li><code>/api/health</code></li>
<li><code>/</code></li>
<li><code>/login</code></li>
<li><code>/docs/</code></li>
<li><code>/ohif/</code></li>
<li>Reverb websocket route <code>/app/</code></li>
<li>Horizon queue behavior</li>
<li>scheduler execution</li>
<li>Solr-backed vocabulary or search flow</li>
</ul>
<p>Fourth, add provider modules:</p>
<ul>
<li>AWS EKS</li>
<li>Azure AKS</li>
<li>GCP GKE</li>
<li>self-managed Kubernetes</li>
</ul>
<p>Each provider module should provision its cloud resources and then call the
same shared Helm release module. The cloud modules should not fork the app
deployment logic.</p>
<p>Fifth, close the operations story:</p>
<ul>
<li>backup scope</li>
<li>restore into a clean namespace</li>
<li>Helm upgrade and rollback</li>
<li>image provenance verification</li>
<li>secret backend examples</li>
<li>network policy</li>
<li>pod disruption budgets</li>
<li>observability</li>
<li>cloud cost and sizing guidance</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-this-is-a-real-platform-step">Why this is a real platform step<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#why-this-is-a-real-platform-step" class="hash-link" aria-label="Enlace directo al Why this is a real platform step" title="Enlace directo al Why this is a real platform step">​</a></h2>
<p>The milestone matters because it changes Parthenon EE from a project with one
primary server deployment model into a project with a clean deployment
portfolio.</p>
<p>The single-host path remains important. We have invested heavily in native
preflight, validation, package capabilities, Solr execution, Laravel bootstrap,
nginx exposure, systemd persistence, backup, restore, and Terraform orchestration
for VPS and bare metal. That work is still the right path for non-Docker hosts.</p>
<p>Kubernetes is a different operating model. It needs Helm charts, cloud
Terraform, external state, secret integration, ingress, registry policy,
cluster probes, autoscaling, and provider-specific infrastructure. It now has a
place to grow without distorting the native installer.</p>
<p>That is the achievement: not just "we added Helm," but "we created a deployment
architecture that can support both bare-metal/VPS installs and cloud-native
Kubernetes installs without confusing the responsibilities of either one."</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-principle-going-forward">The principle going forward<a href="https://parthenon.acumenus.net/docs/es/blog/ee-kubernetes-helm-terraform-foundation#the-principle-going-forward" class="hash-link" aria-label="Enlace directo al The principle going forward" title="Enlace directo al The principle going forward">​</a></h2>
<p>The rule is now clear:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Terraform provisions infrastructure.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Helm deploys Kubernetes workloads.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Native installer contracts configure Linux hosts.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Laravel remains the application authority.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Managed services own production state.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Secret systems own secrets.</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Validation evidence decides readiness.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That is the kind of boundary that makes future work faster. It lets one team
continue hardening the native installer while another builds EKS, AKS, and GKE
modules. It gives reviewers a concrete contract. It gives operators a clear
mental model. It gives the release process a place to attach provenance,
validation, and rollback evidence.</p>
<p>Parthenon EE now has the foundation for Kubernetes. The next milestone is to
prove it in a disposable cluster, then lift the same shared module into AWS,
Azure, and GCP.</p>]]></content:encoded>
            <category>development</category>
            <category>enterprise</category>
            <category>kubernetes</category>
            <category>helm</category>
            <category>terraform</category>
            <category>devops</category>
            <category>infrastructure</category>
        </item>
        <item>
            <title><![CDATA[Parthenon v1.0.7 — CE/EE Fork, Extension Points, AGPLv3]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3</guid>
            <pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Ground-up architectural release. Splits Parthenon into Community and Enterprise editions with 8 extension points, completes the AGPLv3 relicense, lands Harmonia AI concept-mapping, ships managed OHDSI Shiny + 4 industry templates, and closes 4 critical Sentinel security findings — 895 commits in 24 days.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="v107--ceee-fork-extension-points-agplv3">v1.0.7 — CE/EE Fork, Extension Points, AGPLv3<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#v107--ceee-fork-extension-points-agplv3" class="hash-link" aria-label="Enlace directo al v1.0.7 — CE/EE Fork, Extension Points, AGPLv3" title="Enlace directo al v1.0.7 — CE/EE Fork, Extension Points, AGPLv3">​</a></h2>
<p>v1.0.7 is the largest architectural release in the v1.0.x arc. Where v1.0.6
was a feature drop (FinnGen, SSO, light mode), v1.0.7 is the foundation
work that makes Parthenon a <em>platform</em> — a Community edition (AGPLv3) that
remains fully usable on its own and an Enterprise edition that swaps in
proprietary drivers for auth, tenancy, crypto, audit, observability,
feature flags, installer phases, and compose composition.</p>
<p>It also completes the AGPLv3 relicense, ships <strong>Harmonia</strong> (AI-assisted
concept-mapping with a reviewer UI), lands <strong>four new industry templates</strong>
(NAACCR, STS, NCDR, lis_lab_to_omop), brings up the <strong>managed OHDSI Shiny
runtime</strong>, and closes four critical Sentinel security findings.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ceee-fork--plans-01-04">CE/EE fork — Plans 01-04<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#ceee-fork--plans-01-04" class="hash-link" aria-label="Enlace directo al CE/EE fork — Plans 01-04" title="Enlace directo al CE/EE fork — Plans 01-04">​</a></h3>
<p>Parthenon now has two editions sharing one source tree:</p>
<ul>
<li><strong>Community Edition (CE)</strong> — AGPLv3, fully featured, single-tenant defaults.
Everything in this repo is CE.</li>
<li><strong>Enterprise Edition (EE)</strong> — proprietary, layered on top via the eight
extension points below. EE lives in <code>Acumenus-Data-Sciences/Parthenon-EE</code>
with a sync from CE main.</li>
</ul>
<p>Plan 01 handled the legal foundation: relicense from Apache-2.0 to
AGPL-3.0-only (#314), org transfer from <code>sudoshi/Parthenon</code> to
<code>Acumenus-Data-Sciences/Parthenon</code> (#311), CI license guard
(<code>license-text</code>, <code>license-metadata</code>, <code>notice-and-trademarks</code> jobs in #312).</p>
<p>Plans 02-04 are the architectural work — extension points, industry
templates, and the Phase 4 spec set. The detailed phase plans live in
<code>docs/lineage/archive/specs/</code> and <code>docs/lineage/archive/plans/</code>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="eight-phase-2-extension-points">Eight Phase 2 extension points<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#eight-phase-2-extension-points" class="hash-link" aria-label="Enlace directo al Eight Phase 2 extension points" title="Enlace directo al Eight Phase 2 extension points">​</a></h3>
<p>Every "place where EE swaps in proprietary code" is now a contract with a
default CE implementation, a typed interface, and a dependency-injection
seam. All eight landed in v1.0.7:</p>
<table><thead><tr><th>#</th><th>Extension point</th><th>PR</th><th>What CE ships, what EE swaps</th></tr></thead><tbody><tr><td>1</td><td><strong>AuthDriver</strong></td><td>#315</td><td>CE: Sanctum + Spatie. EE: Authentik OIDC, Keycloak, SAML</td></tr><tr><td>2</td><td><strong>TenantResolver</strong></td><td>#316</td><td>CE: single-tenant. EE: multi-tenant via host/header/JWT claim</td></tr><tr><td>3</td><td><strong>CryptoProvider</strong></td><td>#317</td><td>CE: Laravel Crypt. EE: HSM/KMS-backed key wrapping</td></tr><tr><td>4</td><td><strong>AuditSink</strong></td><td>#318</td><td>CE: stdout/log file. EE: SIEM (Wazuh, Splunk, Elastic)</td></tr><tr><td>5</td><td><strong>ObservabilityShipper</strong></td><td>#319</td><td>CE: local Grafana. EE: Datadog, New Relic, OTel collectors</td></tr><tr><td>6</td><td><strong>FeatureFlags</strong></td><td>#320</td><td>CE: env + <code>featureFlags</code> Zustand store + <code>EnterpriseGate</code> component</td></tr><tr><td>7</td><td><strong>AcropolisPhases</strong></td><td>#321</td><td>CE: built-in installer phases. EE: discoverable phase plugins</td></tr><tr><td>8</td><td><strong>ComposeContract</strong></td><td>#322</td><td>CE: composition contract verifier (<code>scripts/verify_compose_contract.py</code>)</td></tr></tbody></table>
<p>A devlog landed late in the cycle adding <code>--check-infra-overlay</code> mode to
the compose verifier so CE-bundled Acropolis overlays are validated as
EE-style overlays without false positives.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="harmonia--ai-assisted-concept-mapping-plans-67">Harmonia — AI-assisted concept-mapping (Plans 6+7)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#harmonia--ai-assisted-concept-mapping-plans-67" class="hash-link" aria-label="Enlace directo al Harmonia — AI-assisted concept-mapping (Plans 6+7)" title="Enlace directo al Harmonia — AI-assisted concept-mapping (Plans 6+7)">​</a></h3>
<p>The concept-mapping decision layer is now a first-class module called
<strong>Harmonia</strong>:</p>
<ul>
<li><strong>Plan 6 (#292)</strong> — backend: AI suggestion service, scoring, candidate
generation, batch processing pipeline (Llettuce on HOLD as T-024B blocker)</li>
<li><strong>Plan 7 (#293)</strong> — reviewer UI + ARTEMIS R-install fixes</li>
<li>"Read, Write, Think" blog post explains how Plan 6 closes the
concept-mapping stack</li>
</ul>
<p>Harmonia integrates with the existing OMOP vocabulary tables and the
Aqueduct ingestion pipeline.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="industry-templates-phase-3">Industry templates (Phase 3)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#industry-templates-phase-3" class="hash-link" aria-label="Enlace directo al Industry templates (Phase 3)" title="Enlace directo al Industry templates (Phase 3)">​</a></h3>
<p>Four new commercial templates landed:</p>
<ul>
<li><strong>NAACCR cancer registry (T-022A, #287)</strong> — Plan 4A</li>
<li><strong>STS National Database (T-022B, #288)</strong> — Plan 4B</li>
<li><strong>lis_lab_to_omop (T-023, #291)</strong> — Plan 5</li>
<li><strong>NCDR</strong> — column map + types + reader, SQL stages, manifest, fixture,
E2E test, README (in <code>templates/commercial/</code>)</li>
</ul>
<p>Plus an earlier <strong>SDTM → OMOP v5.4 bridge</strong> (Plan 6, T-016 + T-020, #274)
and <strong>ARTEMIS chemo regimens</strong> (Phase 2 Plan 5, T-019b, #275).</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="managed-ohdsi-shiny-runtime">Managed OHDSI Shiny runtime<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#managed-ohdsi-shiny-runtime" class="hash-link" aria-label="Enlace directo al Managed OHDSI Shiny runtime" title="Enlace directo al Managed OHDSI Shiny runtime">​</a></h3>
<p>Parthenon now manages OHDSI Shiny app launches end-to-end:</p>
<ul>
<li>Result manifest contract + result loader readiness</li>
<li>Official OHDSI viewer handoff with deepened schema guards</li>
<li>Launch metrics + throttle context surfaced</li>
<li>Managed launch workspaces with pruning</li>
<li>Smoke tests for official module entrypoints</li>
<li>Tenant grants fixed for managed Shiny smoke setup</li>
<li>HADES freshness + parity work</li>
</ul>
<p>A dedicated devlog at <code>docs/lineage/modules/analyses/2026-05-09-hades-parity-managed-ohdsi-shiny-runtime.md</code> documents the runtime architecture.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="aqueduct-ingestion-templates">Aqueduct ingestion templates<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#aqueduct-ingestion-templates" class="hash-link" aria-label="Enlace directo al Aqueduct ingestion templates" title="Enlace directo al Aqueduct ingestion templates">​</a></h3>
<p>The Aqueduct templates contract now ships end-to-end:</p>
<ul>
<li>Run progress, current_node, timestamps, error_message exposed</li>
<li>Cancel + reconciliation flow</li>
<li>DB credentials wired correctly; pending migrations run reliably</li>
<li>Type tightening + tests + runbook</li>
<li>Comprehensive session devlog committed</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="frontend-i18n--121-commits">Frontend i18n — 121 commits<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#frontend-i18n--121-commits" class="hash-link" aria-label="Enlace directo al Frontend i18n — 121 commits" title="Enlace directo al Frontend i18n — 121 commits">​</a></h3>
<p>A sustained i18n hardening pass: locale coverage, fallback handling,
missing-key detection, Arabic locale alignment with backend hidden flag,
i18n resource null placeholder support, hard-coded string elimination.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="cms-measures--72-ecqm-titles-backfilled">CMS Measures — 72 eCQM titles backfilled<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#cms-measures--72-ecqm-titles-backfilled" class="hash-link" aria-label="Enlace directo al CMS Measures — 72 eCQM titles backfilled" title="Enlace directo al CMS Measures — 72 eCQM titles backfilled">​</a></h3>
<p>VSAC value-set imports were missing 72 CMS eCQM measure titles. Backfilled
in #b5f32d381 (<code>b5f32d381</code>), exposed via a sortable + filterable Measures
page (#76e87577a), with title column added to VSAC measures table.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="gis-phase-19--county-stratification">GIS Phase 19 — county stratification<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#gis-phase-19--county-stratification" class="hash-link" aria-label="Enlace directo al GIS Phase 19 — county stratification" title="Enlace directo al GIS Phase 19 — county stratification">​</a></h3>
<ul>
<li><code>gis</code> schema deployed with HIGHSEC GRANT posture (Phase 19-02)</li>
<li>Eloquent models + dataset registration + legacy audit (19-02)</li>
<li>Nationwide multi-source <code>load_geography</code> + <code>load_crosswalk</code> (19-03)</li>
<li>UA county loader + README + conftest env override (19-03)</li>
<li>IncidenceRateService <code>location_urban_pct</code> + FormRequests (19-04)</li>
<li>Frontend <code>stratifyByLocation</code> dropdown + Pancreas warning (19-04)</li>
<li>Legacy GIS loader remediation + DSN regression guard (19-05)</li>
<li>Search_path PostGIS fix + boundary explorer + OHDSI todo consolidation</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="installer-gui-v030-tauri">Installer GUI v0.3.0 (Tauri)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#installer-gui-v030-tauri" class="hash-link" aria-label="Enlace directo al Installer GUI v0.3.0 (Tauri)" title="Enlace directo al Installer GUI v0.3.0 (Tauri)">​</a></h3>
<p>The cross-platform GUI installer made it through Phases 1-8 in this cycle:</p>
<ul>
<li><strong>Phase 1</strong> — cross-platform <code>run_elevated()</code> primitive</li>
<li><strong>Phase 2</strong> — Linux polkit policy + privileged helper</li>
<li><strong>Phases 3+4</strong> — Fix-this UI + Linux Docker auto-install</li>
<li><strong>Phase 5</strong> — recovery panel HTML/CSS + Rust shims, Resume/Retry/Reset</li>
<li><strong>Phases 6a-c</strong> — Windows action handlers + UAC dispatch, WSL2 + VM Platform
preflight detection, reboot state persistence + welcome-back banner</li>
<li><strong>Phase 7</strong> — macOS Docker Desktop / Colima / Rancher</li>
<li><strong>Phases 8a-b</strong> — server-mode setup (Caddy + Let's Encrypt + UFW)</li>
</ul>
<p>Plus Hero Done page, 9-cell phase progress strip, Verify step health probe,
service-status grid + runtime-image upgrade prompt, auto-updater notify
banner, Tauri 2 plugin migration (dialog/shell/store/updater), WSL distro
enumeration, four P0 fixes from Linux Phase A bench testing.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="installer-c-contract-layer">Installer-c (contract layer)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#installer-c-contract-layer" class="hash-link" aria-label="Enlace directo al Installer-c (contract layer)" title="Enlace directo al Installer-c (contract layer)">​</a></h3>
<p>The contract-driven installer engine reached feature parity with the GUI:</p>
<ul>
<li><code>omop_cdm</code> phase complete (run + check, shell-injection / password-exposure
/ output-capture fixes)</li>
<li>New contract actions: <code>health</code>, <code>credentials</code>, <code>service-status</code>, <code>open-app</code>,
<code>port-holder</code>, <code>recover</code>, <code>diagnose</code></li>
<li>50-fingerprint diagnostic KB (10 seed → 50 expanded)</li>
<li>End-to-end round-trip tests for new actions</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="security--sentinel-findings">Security — Sentinel findings<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#security--sentinel-findings" class="hash-link" aria-label="Enlace directo al Security — Sentinel findings" title="Enlace directo al Security — Sentinel findings">​</a></h3>
<p>Four critical/high findings closed in this cycle:</p>
<ul>
<li><strong>CRITICAL</strong> — SQL injection bypass in DataInterrogationService (#298)</li>
<li><strong>CRITICAL</strong> — plaintext password leak in logs (#294)</li>
<li><strong>CRITICAL</strong> — hardcoded Orthanc credentials (#280)</li>
<li><strong>HIGH</strong> — SQL safety bypass in DataInterrogationService (#279)</li>
</ul>
<p>Plus per-route permissions on <code>/study-agent/*</code>, FormRequest <code>authorize()</code>
hardening, Wazuh ports bound to localhost with token-based healthchecks,
and the existing HIGHSEC.spec.md continues to be enforced.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="studies--patient-similarity-hardening">Studies + Patient Similarity hardening<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#studies--patient-similarity-hardening" class="hash-link" aria-label="Enlace directo al Studies + Patient Similarity hardening" title="Enlace directo al Studies + Patient Similarity hardening">​</a></h3>
<ul>
<li>Studies: protocol import → study designer; OCC/<code>if-unmodified-since</code>
precondition on lock endpoint; lock-race guard; dirty-form unsaved-changes
warning; orphan <code>StudyDesigner.tsx</code> (1380 LOC dead code) removed; default
Anthropic study designer to Opus</li>
<li>Patient Similarity: temporal compare validation; workspace workflow repair</li>
<li>Care Bundles: workbench workflow hardening; VSAC measures table title column</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ci--deploy--infra-fixes">CI / deploy / infra fixes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#ci--deploy--infra-fixes" class="hash-link" aria-label="Enlace directo al CI / deploy / infra fixes" title="Enlace directo al CI / deploy / infra fixes">​</a></h3>
<ul>
<li><strong>deploy</strong> — auto-heal composer autoloader poisoned by <code>/tmp</code> worktree paths
(this prevents the worktree-vendor incident captured in feedback memory)</li>
<li><strong>docker</strong> — install <code>libuv1-dev</code> so R <code>fs</code> package builds; preserve
<code>.gitignore</code> mode in php entrypoint chmod sweep; fix scispacy
<code>en_core_sci_md</code> wheel URL (was 404)</li>
<li><strong>ci</strong> — pin <code>DB_TEST_*</code> env vars to CI postgres service; share ingest
timestamp across wiki pages; AI review advisory; Darkstar build
timeout 60→120; PostGIS for FinnGen migrations; align frontend Arabic
locale + tests with backend hidden flag</li>
<li><strong>test-infra</strong> — respect CI env when resolving test DB host; only patch
<code>*_testing</code> config when broken</li>
<li><strong>docs</strong> — harden docs deploy build; harden docs content tree deployment;
auto-fix duplicate blog slugs</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="dependencies">Dependencies<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#dependencies" class="hash-link" aria-label="Enlace directo al Dependencies" title="Enlace directo al Dependencies">​</a></h3>
<ul>
<li><strong>Frontend</strong> — <code>@tanstack/react-query</code> (#308), <code>react-joyride</code> 3.0.2→3.1.0
(#310), <code>zod</code> 4.3.6→4.4.3 (#309), <code>deck.gl</code> 9.2.11→9.3.2 (#237)</li>
<li><strong>AI</strong> — <code>transformers</code> (#302), <code>esda</code> &gt;=2.5→&gt;=2.9.0 (#305), <code>cyvcf2</code>
<blockquote>
<p>=0.31.0→&gt;=0.32.1 (#304), <code>asyncpg</code> &gt;=0.30.0→&gt;=0.31.0 (#303), <code>spreg</code>
=1.4→&gt;=1.9.0 (#300), <code>geopandas</code> &gt;=1.0.0→&gt;=1.1.3 (#248), <code>scikit-learn</code>
(#249)</p>
</blockquote>
</li>
<li><strong>GitHub Actions</strong> — <code>actions/github-script</code> 7→9 (#301),
<code>astral-sh/setup-uv</code> 3→7 (#299)</li>
<li><strong>Production deps group</strong> — 7 updates (#307)</li>
<li><strong>Dev deps group</strong> — 2 updates (#306)</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="org-transfer--license">Org transfer + license<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#org-transfer--license" class="hash-link" aria-label="Enlace directo al Org transfer + license" title="Enlace directo al Org transfer + license">​</a></h3>
<p>The repo moved from <code>sudoshi/Parthenon</code> to
<code>Acumenus-Data-Sciences/Parthenon</code> on <strong>2026-04-26</strong> (#311). GitHub
auto-redirects, but please re-set your remotes:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token function" style="color:hsl(207, 82%, 66%)">git</span><span class="token plain"> remote set-url origin git@github.com:Acumenus-Data-Sciences/Parthenon.git</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>License changed from <strong>Apache-2.0</strong> to <strong>AGPL-3.0-only</strong> (#314). All
existing source contributions are re-licensed under AGPL-3.0-only per the
relicense plan; see <code>LICENSE</code>, <code>NOTICE</code>, and <code>docs/legal/</code>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="upgrade-notes">Upgrade notes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#upgrade-notes" class="hash-link" aria-label="Enlace directo al Upgrade notes" title="Enlace directo al Upgrade notes">​</a></h3>
<ul>
<li><code>git pull &amp;&amp; ./deploy.sh</code> is sufficient for most environments.</li>
<li><strong>No config changes required</strong> for upgrade from 1.0.6.</li>
<li><strong>EE consumers</strong>: review <code>docs/lineage/design/architecture/extension-points/</code> for the
eight contract interfaces before subclassing.</li>
<li><strong>Org rename</strong>: update remote URLs (auto-redirected by GitHub but cleaner
to fix).</li>
<li><strong>License</strong>: AGPL-3.0-only is now the project license. If you fork CE
to a service, AGPL §13 applies — you must offer source to your users.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="by-the-numbers">By the numbers<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#by-the-numbers" class="hash-link" aria-label="Enlace directo al By the numbers" title="Enlace directo al By the numbers">​</a></h3>
<ul>
<li><strong>895 commits</strong> since v1.0.6 (2026-04-16 → 2026-05-10, 24 days)</li>
<li><strong>121 <code>feat(i18n)</code></strong> commits — a sustained internationalization push</li>
<li><strong>8 of 8</strong> Phase 2 extension points landed</li>
<li><strong>4 new industry templates</strong> (NAACCR, STS, NCDR, lis_lab) + 2 from Phase 3
(SDTM bridge, ARTEMIS)</li>
<li><strong>4 critical/high security findings</strong> closed by Sentinel</li>
<li><strong>41 dependency updates</strong> via <code>chore(deps)</code></li>
<li><strong>27 docs</strong> + 11 <code>docs(installer)</code> + 8 <code>docs(plans)</code> + 8 <code>docs(devlog)</code></li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="contributors">Contributors<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-7-ce-ee-fork-extension-points-agplv3#contributors" class="hash-link" aria-label="Enlace directo al Contributors" title="Enlace directo al Contributors">​</a></h3>
<p>Claude Code + @sudoshi, with PR review by Sentinel and the Acumenus
Data Sciences team.</p>]]></content:encoded>
            <category>release</category>
            <category>ce-ee</category>
            <category>extension-points</category>
            <category>agplv3</category>
            <category>harmonia</category>
            <category>shiny</category>
            <category>security</category>
        </item>
        <item>
            <title><![CDATA[Introducing Harmonia: Read, Write, Think for OMOP Concept Mapping]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping</guid>
            <pubDate>Wed, 06 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Concept mapping is the single largest line item in any OMOP CDM ingestion budget. Published estimates put it at 40–60% of total ETL effort per source system — measured in clinician-weeks, not engineer-hours. Today we landed the architectural piece that's been missing from Parthenon's vocabulary stack since the beginning: Harmonia, an automated decision layer that sits between Hecate (read) and Ariadne (write) and does the cognitive work that's been falling on humans.]]></description>
            <content:encoded><![CDATA[<p>Concept mapping is the single largest line item in any OMOP CDM ingestion budget. Published estimates put it at <strong>40–60% of total ETL effort</strong> per source system — measured in clinician-weeks, not engineer-hours. Today we landed the architectural piece that's been missing from Parthenon's vocabulary stack since the beginning: <strong>Harmonia</strong>, an automated <strong>decision</strong> layer that sits between Hecate (read) and Ariadne (write) and does the cognitive work that's been falling on humans.</p>
<p>The name is deliberate. In Greek mythology, <strong>Harmonia</strong> is the goddess of agreement, accord, and <em>fitting together</em> — daughter of Aphrodite and Ares, born of love and conflict. That's what concept mapping is: bringing disparate source vocabularies (an ICD-10 code from one EHR, an NDC string from another, a hospital's local lab nomenclature) into harmony with a single canonical OMOP standard. Every approved mapping is a small act of harmony. Until today, Parthenon could <em>show</em> candidates and <em>record</em> decisions but couldn't <em>reach</em> harmony on its own.</p>
<p>This post walks through what we built, why it's an improvement over the existing Hecate + Ariadne pair, and the four real bugs we hit getting a benchmark to actually run.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-state-of-the-stack-before-today">The state of the stack before today<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#the-state-of-the-stack-before-today" class="hash-link" aria-label="Enlace directo al The state of the stack before today" title="Enlace directo al The state of the stack before today">​</a></h2>
<p>Parthenon already had two production-grade pieces of vocabulary infrastructure, both indispensable, neither sufficient.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="hecate--the-read-layer">Hecate — the <em>read</em> layer<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#hecate--the-read-layer" class="hash-link" aria-label="Enlace directo al hecate--the-read-layer" title="Enlace directo al hecate--the-read-layer">​</a></h3>
<p>Hecate is the semantic search service. Backed by <a href="https://qdrant.tech/" target="_blank" rel="noopener noreferrer">Qdrant</a> at port 8088, it indexes <strong>1.97M concept vectors</strong> from the OMOP vocabulary at 768 dimensions, embedded with <code>embeddinggemma:300m</code> (Google EmbeddingGemma 307M served via Ollama). When a user types "humerus" in the vocab explorer, Hecate returns the top-N most-cosine-similar concepts in milliseconds.</p>
<p>Hecate's job is <strong>lookup</strong>. Given a query string, find candidates. It doesn't decide which candidate is right; it just makes the candidates findable. That's the correct scope for an autocomplete-grade UI service.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ariadne--the-write-layer">Ariadne — the <em>write</em> layer<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#ariadne--the-write-layer" class="hash-link" aria-label="Enlace directo al ariadne--the-write-layer" title="Enlace directo al ariadne--the-write-layer">​</a></h3>
<p>Ariadne (<code>AriadneController.saveMappings</code> in the Laravel backend) is the human-in-the-loop mapping designer. A reviewer searches Hecate, picks the right concept, clicks save, and Ariadne batches those decisions into <code>MappingProject</code> rows with the audit trail required by the <code>mapping.review</code> permission.</p>
<p>Ariadne's job is <strong>persistence</strong>. It records human decisions and ships them to the right downstream tables. It doesn't make decisions either; it captures them.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="whats-been-missing">What's been missing<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#whats-been-missing" class="hash-link" aria-label="Enlace directo al What's been missing" title="Enlace directo al What's been missing">​</a></h3>
<p>Look at the workflow that pattern produces:</p>
<ol>
<li>ETL hits an unmapped local code (e.g. <code>FAC-GLU</code> from a hospital's lab system).</li>
<li>The code lands in the <code>unmapped_local_lab_code</code> queue (Phase 3 Plan 5 introduced this for the lab template).</li>
<li>A clinical informaticist opens Ariadne, types "facility glucose" into the search box, and Hecate returns 50 candidates ranked by cosine similarity.</li>
<li>The informaticist reads the 50 candidates, judges which one matches clinically (not just semantically), and clicks approve.</li>
</ol>
<p>Step 4 is the bottleneck. Cosine similarity is necessary but insufficient — <em>"Felt lack of respect before illness"</em> and <em>"Felt inferior to others before illness"</em> score nearly identically against <code>embeddinggemma</code>, but only one of them is the right LOINC code for a given source. <strong>Picking the right one requires clinical reasoning, and clinical reasoning has been falling on humans for every single mapping.</strong></p>
<p>That's the work <strong>Harmonia</strong> automates.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-we-built--harmonia">What we built — Harmonia<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#what-we-built--harmonia" class="hash-link" aria-label="Enlace directo al What we built — Harmonia" title="Enlace directo al What we built — Harmonia">​</a></h2>
<p>Harmonia is a commercial-tier backend that does retrieval, reranking, and persistence in a single pipeline. The architecture is deliberately modular so each stage can be replaced independently.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="stage-1--retrieve-bgeembedder--conceptretriever">Stage 1 — Retrieve (<code>BgeEmbedder</code> + <code>ConceptRetriever</code>)<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#stage-1--retrieve-bgeembedder--conceptretriever" class="hash-link" aria-label="Enlace directo al stage-1--retrieve-bgeembedder--conceptretriever" title="Enlace directo al stage-1--retrieve-bgeembedder--conceptretriever">​</a></h3>
<p>Different model, different scope from Hecate by design:</p>
<ul>
<li><strong><code>BAAI/bge-base-en-v1.5</code></strong> instead of <code>embeddinggemma:300m</code>. bge-base scores higher than general-purpose Gemma embeddings on retrieval-specific benchmarks (BEIR, MTEB), and at 110M params it's small enough to share VRAM with MedGemma 27B without eviction pressure.</li>
<li><strong>Standard concepts only</strong> — <code>vocab.concept WHERE standard_concept = 'S' AND invalid_reason IS NULL</code> filtered to SNOMED + RxNorm + LOINC + ATC + HCPCS. That's ~632k concepts, half Hecate's index size, but every row is a valid mapping target. Hecate's broader index is right for live UI search; Harmonia's narrower index is right for a pipeline that has to commit to one answer.</li>
<li><strong><code>vocab.concept_embedding_bge</code></strong> is a new pgvector table living in the shared <code>vocab</code> schema with an <code>ivfflat (vector_cosine_ops) WITH (lists = 200)</code> index. Co-locating embeddings with the source vocabulary means joins to <code>concept</code> happen at no network cost — important when the retriever needs to surface <code>concept_name</code>, <code>vocabulary_id</code>, <code>domain_id</code>, and <code>standard_concept</code> for every candidate.</li>
</ul>
<p><code>ConceptRetriever.search(cursor, query_vec, top_k=50)</code> returns 50 candidates per query in ~3-5ms after the index is warm. Compare to Hecate's HTTP round-trip from Laravel into Qdrant for ~100ms — same algorithm, but <strong>in-process and same-database</strong> is the right deployment for pipeline use.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="stage-2--rerank-conceptreranker--anthropic-tool_use">Stage 2 — Rerank (<code>ConceptReranker</code> + Anthropic tool_use)<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#stage-2--rerank-conceptreranker--anthropic-tool_use" class="hash-link" aria-label="Enlace directo al stage-2--rerank-conceptreranker--anthropic-tool_use" title="Enlace directo al stage-2--rerank-conceptreranker--anthropic-tool_use">​</a></h3>
<p>Cosine similarity gets you the right answer in the top-50; the rerank stage gets the right answer to the top-1. Harmonia wires this through the Phase 2 NLP backend pattern with a strict JSON contract:</p>
<ul>
<li><strong>System prompt</strong> explicitly instructs on OMOP "Maps to" asymmetry: <em>"the OMOP 'Maps to' relationship goes from a non-standard source vocabulary (ICD10CM, NDC, Read, ICD9CM, etc.) to a standard target vocabulary (SNOMED, RxNorm, LOINC). Source-text and target-name often differ semantically because of vocabulary asymmetry."</em> Without this, the LLM defaults to lexical matching — wrong direction.</li>
<li><strong>Anthropic <code>tool_use</code></strong> with a strict <code>input_schema</code> (<code>ranked: [{concept_id, score, rationale}]</code> + <code>confidence</code>). Server-side schema validation eliminates the 26% JSON parse failure rate we saw with prose-based JSON output. (More on that war story below.)</li>
<li><strong>Provider-agnostic</strong> — the <code>LlmCallable</code> injection point accepts Anthropic Claude, OpenAI, or local Ollama (MedGemma 27B q4_0). The acceptance benchmark currently runs on Haiku 4.5 because it's the price/quality sweet spot for high-frequency calls; production deployments can swap in whatever the customer's existing LLM relationship looks like.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="stage-3--persist-mappingreviewqueuenode--appparthenon_concept_map">Stage 3 — Persist (<code>MappingReviewQueueNode</code> + <code>app.parthenon_concept_map</code>)<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#stage-3--persist-mappingreviewqueuenode--appparthenon_concept_map" class="hash-link" aria-label="Enlace directo al stage-3--persist-mappingreviewqueuenode--appparthenon_concept_map" title="Enlace directo al stage-3--persist-mappingreviewqueuenode--appparthenon_concept_map">​</a></h3>
<p>This is where Harmonia hands off to Ariadne instead of replacing it. The new <code>app.parthenon_concept_map</code> table holds <strong>auto-approved</strong> mappings (high LLM confidence, no human review) plus the audit trail every approved mapping needs:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token keyword" style="color:hsl(286, 60%, 67%)">CREATE</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">TABLE</span><span class="token plain"> app</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">parthenon_concept_map </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    map_id                 </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">BIGINT</span><span class="token plain"> GENERATED ALWAYS </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">IDENTITY</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">PRIMARY</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">KEY</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    source_code            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    source_vocab           </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    source_text            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">TEXT</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    omop_concept_id        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">BIGINT</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">REFERENCES</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">concept_id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    confidence             </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">NUMERIC</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token number" style="color:hsl(29, 54%, 61%)">5</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token number" style="color:hsl(29, 54%, 61%)">4</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">CHECK</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">confidence </span><span class="token operator" style="color:hsl(207, 82%, 66%)">BETWEEN</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">0</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    reviewer_id            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">BIGINT</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">REFERENCES</span><span class="token plain"> app</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">users</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    reviewed_at            TIMESTAMPTZ </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">DEFAULT</span><span class="token plain"> </span><span class="token function" style="color:hsl(207, 82%, 66%)">NOW</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    model_version          </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">TEXT</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    candidate_ranking_json JSONB </span><span class="token operator" style="color:hsl(207, 82%, 66%)">NOT</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">NULL</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">UNIQUE</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">source_code</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> source_vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Two things matter here. First, the <code>omop_concept_id</code> foreign key to <code>vocab.concept(concept_id)</code> makes hallucination impossible — even if the LLM emits a fabricated ID, the DB rejects the INSERT. Second, the <code>candidate_ranking_json</code> JSONB column preserves the full top-5 (with rationales and confidence scores) so when a reviewer reopens an auto-approved mapping a year later, the reasoning trail is right there.</p>
<p>Low-confidence rows (confidence ≤ 0.3 per the prompt's "no clear match" rule) <strong>don't</strong> auto-approve. They flow to Ariadne's existing review queue with the LLM's top-5 attached, so the human reviewer sees pre-ranked candidates instead of raw cosine results. The reviewer's click is now confirmation, not search.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="how-the-three-layers-compose">How the three layers compose<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#how-the-three-layers-compose" class="hash-link" aria-label="Enlace directo al How the three layers compose" title="Enlace directo al How the three layers compose">​</a></h3>
<table><thead><tr><th>Capability</th><th>Hecate</th><th>Ariadne</th><th>Harmonia</th></tr></thead><tbody><tr><td>Returns plausible candidates</td><td>✓</td><td>—</td><td>✓</td></tr><tr><td>Picks the right one with reasoning</td><td>✗</td><td>✗ (user does)</td><td>✓</td></tr><tr><td>Persists approved decisions</td><td>✗</td><td>✓</td><td>✓ (auto-approved only; review path uses Ariadne)</td></tr><tr><td>Cross-vocabulary "Maps to" awareness</td><td>✗ (raw cosine)</td><td>depends on user</td><td>✓ (system prompt)</td></tr><tr><td>Confidence calibration</td><td>✗</td><td>—</td><td>✓ (LLM emits, threshold routes)</td></tr><tr><td>Live UI search at 1.97M concept scale</td><td>✓</td><td>—</td><td>✗ (smaller, narrower index)</td></tr><tr><td>Audit trail with reviewer_id</td><td>—</td><td>✓</td><td>✓ (mirrors Ariadne shape)</td></tr></tbody></table>
<p><strong>Hecate searches. Harmonia harmonizes. Ariadne records.</strong> That's the shape of a complete read-write-think system. The before-state had read and write but no think — the act of bringing a local code into accord with a standard concept was happening one clinician-week at a time.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-acceptance-benchmark--why-it-exists">The acceptance benchmark — why it exists<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#the-acceptance-benchmark--why-it-exists" class="hash-link" aria-label="Enlace directo al The acceptance benchmark — why it exists" title="Enlace directo al The acceptance benchmark — why it exists">​</a></h2>
<p>Phase 3 spec §2 mandates a "Gate 2" check before Harmonia can merge. The fear isn't that the architecture is wrong; it's that the rerank step is hand-wavey and needs ground truth before we ship it as a commercial wedge to customers.</p>
<p>The benchmark is curated from <code>vocab.concept_relationship</code> where <code>relationship_id = 'Maps to'</code> — the ground-truth directed edges OMOP itself publishes between non-standard source codes and standard targets. We pull 3000 such edges, sample-balanced per source vocabulary, and split into:</p>
<ul>
<li><strong><code>seen.csv</code></strong> (1557 rows) — source vocabularies the embedder has seen plenty of: SNOMED, RxNorm, LOINC, HCPCS. Pass thresholds: top-1 ≥ 0.60, top-5 ≥ 0.85. <strong>Non-negotiable.</strong></li>
<li><strong><code>blind.csv</code></strong> (521 rows) — source vocabularies held out: ICD10CM, ICD9CM, NDC, Read. Pass thresholds: top-1 ≥ 0.50, top-5 ≥ 0.75. <strong>Aspirational</strong> — ADR 0019 lets us ship Harmonia with the blind set deferred to Phase 4 if the gates miss, because cross-vocabulary mapping is genuinely the hard case.</li>
</ul>
<p>The test for each row is binary: take the source code's text, run the full retrieve→rerank pipeline, see whether the ground-truth target concept_id appears in the LLM's top-1 (strict) and top-5 (lenient) outputs.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="four-bugs-that-almost-killed-the-run">Four bugs that almost killed the run<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#four-bugs-that-almost-killed-the-run" class="hash-link" aria-label="Enlace directo al Four bugs that almost killed the run" title="Enlace directo al Four bugs that almost killed the run">​</a></h2>
<p>The acceptance harness was supposed to be a one-day delivery. It took two days because production-grade glue between pgvector, ROCm torch, and Anthropic's API has more sharp edges than any of us remembered.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="bug-1--pgvector-type-unresolvable-from-default-sessions">Bug 1 — pgvector type unresolvable from default sessions<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#bug-1--pgvector-type-unresolvable-from-default-sessions" class="hash-link" aria-label="Enlace directo al Bug 1 — pgvector type unresolvable from default sessions" title="Enlace directo al Bug 1 — pgvector type unresolvable from default sessions">​</a></h3>
<p>Harmonia's pgvector migration declared <code>embedding vector(768)</code>, but pgvector installs the <code>vector</code> type into whatever schema the extension lives in (<code>public</code> by default). The customer's session search_path was <code>app, php</code> — the migration's <code>CREATE TABLE</code> failed with <code>type "vector" does not exist</code>.</p>
<p>Fix: schema-qualify every cast. Migration column type is now <code>embedding public.vector(768) NOT NULL</code>. The retriever and ingest job got the same treatment for <code>%s::public.vector</code> parameter casts.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="bug-2--pgvector-operator-missing-from-session-search_path">Bug 2 — pgvector operator missing from session search_path<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#bug-2--pgvector-operator-missing-from-session-search_path" class="hash-link" aria-label="Enlace directo al Bug 2 — pgvector operator missing from session search_path" title="Enlace directo al Bug 2 — pgvector operator missing from session search_path">​</a></h3>
<p>After the type fix, the retriever's <code>e.embedding &lt;=&gt; %s::vector</code> started returning <code>operator does not exist: public.vector &lt;=&gt; public.vector</code>. The cosine-distance operator is registered against <code>public.vector</code>, but the session's search_path didn't include <code>public</code>, so the operator wasn't visible.</p>
<p>Fix: schema-qualify the operator too. The retriever now uses the explicit form:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">embedding OPERATOR</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token keyword" style="color:hsl(286, 60%, 67%)">public</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token operator" style="color:hsl(207, 82%, 66%)">&lt;=&gt;</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">%</span><span class="token plain">s::</span><span class="token keyword" style="color:hsl(286, 60%, 67%)">public</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">vector</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> similarity</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="bug-3--ingest-exited-after-one-batch">Bug 3 — ingest exited after one batch<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#bug-3--ingest-exited-after-one-batch" class="hash-link" aria-label="Enlace directo al Bug 3 — ingest exited after one batch" title="Enlace directo al Bug 3 — ingest exited after one batch">​</a></h3>
<p>The ingest job iterated <code>cursor</code> to walk unmapped concepts in 1024-row chunks. Each chunk got embedded by bge-base, then upserted via <code>cursor.executemany</code>. Both calls used the same psycopg cursor.</p>
<p>That cursor reuse is the bug. psycopg's default cursor invalidates the SELECT result set when <code>executemany</code> runs against it — so after the first batch's INSERT completed, the SELECT-side iteration was done. The job exited cleanly with <code>seen=1024, embedded=1024, batches=1</code> and 631,569 concepts left untouched.</p>
<p>Fix: <code>cursor.fetchall()</code> upfront in <code>_select_unmapped_concepts</code>. Memory cost of materializing ~1M (concept_id, concept_name) tuples is bounded under 200MB, well within typical job sizing. The ingest now runs to completion deterministically.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="bug-4--torch-was-on-the-wrong-gpu-vendor">Bug 4 — torch was on the wrong GPU vendor<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#bug-4--torch-was-on-the-wrong-gpu-vendor" class="hash-link" aria-label="Enlace directo al Bug 4 — torch was on the wrong GPU vendor" title="Enlace directo al Bug 4 — torch was on the wrong GPU vendor">​</a></h3>
<p>The bge-base ingest was crawling at 50 concepts per second on CPU. <code>torch.cuda.is_available()</code> returned <code>False</code>. The user's machine has a <strong>Radeon RX 7900 XTX</strong> running ROCm, but the templates venv had <code>torch==2.5.1+cu124</code> — the NVIDIA CUDA build, with zero AMD support.</p>
<p>The fix wasn't a one-liner. ROCm wheels for <code>torch==2.5.1+rocm6.2</code> don't ship <code>cp313</code> ABI tags, so the Python 3.13 venv had to be rebuilt on Python 3.12. After:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token function" style="color:hsl(207, 82%, 66%)">rm</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-rf</span><span class="token plain"> .venv</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">uv venv </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--python</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">3.12</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">uv pip </span><span class="token function" style="color:hsl(207, 82%, 66%)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-e</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'.[dev]'</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">uv pip </span><span class="token function" style="color:hsl(207, 82%, 66%)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-e</span><span class="token plain"> ./commercial</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">uv pip </span><span class="token function" style="color:hsl(207, 82%, 66%)">install</span><span class="token plain"> --index-url https://download.pytorch.org/whl/rocm6.2 torch torchvision</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>…<code>torch.cuda.is_available()</code> returned <code>True</code> and the ingest dropped from ~100 minutes (CPU) to <strong>~7 minutes</strong> for the full 632k-concept run. PyTorch's HIP backend reports the AMD GPU as a "cuda" device for compatibility — the rest of the pipeline didn't have to change.</p>
<p>Net effect: batches went from 0.16/sec to 1.4/sec — about <strong>85× faster</strong>.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="bonus-26-json-parse-failure-with-prose-based-output">Bonus: 26% JSON parse failure with prose-based output<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#bonus-26-json-parse-failure-with-prose-based-output" class="hash-link" aria-label="Enlace directo al Bonus: 26% JSON parse failure with prose-based output" title="Enlace directo al Bonus: 26% JSON parse failure with prose-based output">​</a></h3>
<p>The first acceptance run with Haiku produced <strong>94 JSON parse failures across 350 rows</strong>. Same root cause as MedGemma earlier: a verbose system prompt + 50-candidate input + <code>max_tokens=1024</code> led to truncated mid-rationale responses. We bumped <code>max_tokens</code> to 8192 and rewrote <code>_strip_json_fences</code> (which had a real bug — <code>cleaned.strip("</code>")` was eating fences before the trailing-fence check ran), but the deeper fix was eliminating the whole prose-JSON layer entirely.</p>
<p>Anthropic's <code>tool_use</code> API binds output to a JSON Schema server-side. We defined a <code>submit_rerank</code> tool whose <code>input_schema</code> is the strict shape we want:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">rerank_tool </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"name"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"submit_rerank"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"input_schema"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"object"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"required"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string" style="color:hsl(95, 38%, 62%)">"ranked"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"confidence"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"additionalProperties"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">False</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"properties"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string" style="color:hsl(95, 38%, 62%)">"ranked"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                </span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"array"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"minItems"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"maxItems"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">5</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                </span><span class="token string" style="color:hsl(95, 38%, 62%)">"items"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"object"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"required"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string" style="color:hsl(95, 38%, 62%)">"concept_id"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"score"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"additionalProperties"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token boolean" style="color:hsl(29, 54%, 61%)">False</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"properties"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"concept_id"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"integer"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"score"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"number"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"minimum"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">0.0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"maximum"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1.0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                        </span><span class="token string" style="color:hsl(95, 38%, 62%)">"rationale"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"string"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">                </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string" style="color:hsl(95, 38%, 62%)">"confidence"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token string" style="color:hsl(95, 38%, 62%)">"type"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"number"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"minimum"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">0.0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"maximum"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1.0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><code>tool_choice={"type": "tool", "name": "submit_rerank"}</code> forces Haiku to produce a <code>tool_use</code> block whose <code>input</code> is already a parsed Python dict. <strong>0% parse failures by construction.</strong></p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-the-numbers-say">What the numbers say<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#what-the-numbers-say" class="hash-link" aria-label="Enlace directo al What the numbers say" title="Enlace directo al What the numbers say">​</a></h2>
<p>The full 2078-row benchmark is still running as I write this — Haiku at ~3-5 seconds per call works out to ~2.5 hours wall clock for the full sweep. Interim numbers at the 350-row mark, before we switched to tool_use:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">seen  (350): top-1 = 0.720  (PASS ≥ 0.60)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">             top-5 = 0.743  (FAIL ≥ 0.85, parse failures dragged this down)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Interim numbers at the 250-row mark with tool_use:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">seen  (250): top-1 = 0.748  (PASS ≥ 0.60)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">             top-5 = 0.768  (FAIL ≥ 0.85, but no parse failures, climbing)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">errors:    0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The trajectory suggests landing the seen set around <strong>0.78 top-1 / 0.85 top-5</strong> — at the gate. If top-5 lands a hair under 0.85 we have headroom in the prompt to tighten further (the current cross-vocab system message is already a meaningful improvement over the v0.1.0 prompt that prioritized clinical fidelity without naming the Maps-to direction).</p>
<p>The blind set is going to be hard. The 50-row probe earlier showed top-1 = 0.28 / top-5 = 0.30 — well below the 0.50 / 0.75 thresholds. ICD10CM → SNOMED is genuinely the hard case because the source-text strings often diverge from the target's <code>concept_name</code> (for example, ICD10CM "Type 2 diabetes mellitus without complications" maps to SNOMED "Type 2 diabetes mellitus" — the modifier drops, and bge-base's similarity dilutes accordingly). Per ADR 0019 the blind-set thresholds are negotiable; the seen-set thresholds are not.</p>
<p>If the seen set lands at the gate and the blind set misses, <strong>Harmonia ships with the blind work explicitly deferred to Phase 4</strong>, where the natural next move is per-vocabulary fine-tuning on the bge-base encoder. A 12-hour LoRA on the curated benchmark itself should close most of the cross-vocab gap, but that's a separate piece of work.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-this-changes-for-customers">What this changes for customers<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#what-this-changes-for-customers" class="hash-link" aria-label="Enlace directo al What this changes for customers" title="Enlace directo al What this changes for customers">​</a></h2>
<p>The headline metric for Harmonia is the line in ADR 0019: <em>"published estimates put concept mapping at 40-60% of total ETL effort. Cutting that in half (or better) is the Parthenon-native moat."</em></p>
<p>Cutting in half isn't a model quality target — it's a workflow target. Even at the conservative end of our seen-set numbers (75% top-1), three out of four unmapped local codes will arrive at Ariadne with the right concept already at the top of the suggested list. The reviewer's job becomes:</p>
<ul>
<li><strong>75% of rows:</strong> glance at the top suggestion + LLM rationale, click approve.</li>
<li><strong>15% of rows:</strong> suggestion is in top-5 but not top-1, scan five rows, click the right one.</li>
<li><strong>10% of rows:</strong> none of the top-5 fit, fall back to Hecate's broader search exactly as today.</li>
</ul>
<p>If a reviewer averaged 90 seconds per Hecate-search-and-pick before, the new flow averages closer to 15 seconds for the 75% case and 30 seconds for the 15% case. <strong>Total time spent per mapping drops by about 70%</strong> in expectation. That's the moat — same accuracy as a clinician, an order of magnitude faster.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-still-owed">What's still owed<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#whats-still-owed" class="hash-link" aria-label="Enlace directo al What's still owed" title="Enlace directo al What's still owed">​</a></h2>
<p>Harmonia (T-024A) is the backend. <strong>The reviewer UI (T-024B) is next</strong> — a React surface at <code>/admin/mapping-review</code> where the queue of suggested mappings actually appears in front of human eyes. Once it ships, the Ariadne workflow will inherit Harmonia's top-5 suggestions as a default view rather than the current empty search box.</p>
<p>We also owe a follow-up commit that wires Harmonia into the existing Phase 2 LlmBackend (currently OpenAI/Ollama). The acceptance harness uses Anthropic directly via tool_use; production should be able to flip providers without script-level branching. That's a single afternoon of work behind a Phase 4 ticket.</p>
<p>And the blind-set gap — assuming today's run lands as expected — is the obvious Phase 4 candidate. ADR 0019 already names per-vocabulary embedding fine-tuning as the path forward there.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="a-note-on-running-this-on-your-own-hardware">A note on running this on your own hardware<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#a-note-on-running-this-on-your-own-hardware" class="hash-link" aria-label="Enlace directo al A note on running this on your own hardware" title="Enlace directo al A note on running this on your own hardware">​</a></h2>
<p>The four bugs above all came down to the same anti-pattern: <strong>trusting framework defaults when the deployment isn't the framework's default</strong>. pgvector defaults to <code>public</code> schema, psycopg defaults to a single shared cursor, torch defaults to NVIDIA, Anthropic SDK defaults to prose responses. None of those defaults are wrong on the happy path — they all bit us once the actual deployment had <code>app,php</code> search_path / interleaved cursor reuse / AMD silicon / structured-output requirements.</p>
<p>If you're cloning this work for your own ROCm-equipped lab:</p>
<ol>
<li><strong>Schema-qualify pgvector everywhere.</strong> Operators and types both. The <code>OPERATOR(public.&lt;=&gt;)</code> syntax is portable across customers who relocate the extension.</li>
<li><strong><code>fetchall()</code> before <code>executemany()</code> on the same cursor.</strong> Or use a second cursor explicitly. psycopg's docs warn about this in passing but it doesn't fail loudly; it fails silently after the first batch.</li>
<li><strong>Build the venv on Python 3.12 if you're on AMD.</strong> ROCm torch wheels lag NVIDIA CUDA wheels by one Python minor version. cp313 wheels may exist by the time you're reading this; cp312 was the safest bet on May 6, 2026.</li>
<li><strong>Use <code>tool_use</code> for every LLM call that needs structured output.</strong> Prose-based JSON parsing is a 5-25% silent failure tax. The Anthropic SDK's tool_use API is one extra parameter and eliminates the entire category of errors.</li>
</ol>
<p>The Harmonia worktree at <code>/tmp/p3-plan6-impl</code> (which becomes <code>feature/phase-3-plan-6-ai-mapping</code> upstream) is on PR #292 with all four fixes committed. The acceptance harness lives at <code>templates/scripts/run_mapping_acceptance.py</code>. If you want to reproduce locally:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)"># (one-time) Apply the migration</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">psql </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$PARTHENON_DB_URL</span><span class="token plain"> </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-f</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    templates/commercial/runtime/commercial/mapping/migrations/01_concept_embedding_bge.sql</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)"># (one-time, ~7 min on a 7900 XTX) Embed the standard concepts</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token assign-left variable" style="color:hsl(207, 82%, 66%)">PARTHENON_DB_URL</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">..</span><span class="token plain">. uv run python </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-m</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    runtime.commercial.mapping.ingest_embeddings </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--vocabulary</span><span class="token plain"> SNOMED RxNorm LOINC HCPCS</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)"># Curate the benchmark</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token assign-left variable" style="color:hsl(207, 82%, 66%)">PARTHENON_DB_URL</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">..</span><span class="token plain">. uv run python </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-m</span><span class="token plain"> scripts.curate_mapping_benchmark </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--seed</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">42</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)"># Run the acceptance harness (~2.5 hr, ~$0.40 with Haiku 4.5)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token assign-left variable" style="color:hsl(207, 82%, 66%)">PARTHENON_DB_URL</span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">..</span><span class="token plain">. uv run python </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">-m</span><span class="token plain"> scripts.run_mapping_acceptance </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token parameter variable" style="color:hsl(207, 82%, 66%)">--provider</span><span class="token plain"> anthropic </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">\</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    --api-key-file ~/.anthropic_api_key</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Smaller smoke runs land in ~30 sec with <code>--max-rows 100</code>, plenty for verifying the path works before committing to the full benchmark.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="where-this-fits-in-the-larger-phase-3-story">Where this fits in the larger Phase 3 story<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-05-06-read-write-think-concept-mapping#where-this-fits-in-the-larger-phase-3-story" class="hash-link" aria-label="Enlace directo al Where this fits in the larger Phase 3 story" title="Enlace directo al Where this fits in the larger Phase 3 story">​</a></h2>
<p>Phase 3 is "the commercial wedge" phase — four big new template families (T-021 claims, T-022 registries, T-023 lab, T-024 mapping) plus several Phase 2 carry-overs. As of today:</p>
<ul>
<li><strong>T-021 (claims):</strong> closed — X12 837/835 + NCPDP shipped weeks ago.</li>
<li><strong>T-022 (registries):</strong> closed — NAACCR + STS + NCDR shipped last week.</li>
<li><strong>T-023 (lab):</strong> closed — <code>lis_lab_to_omop</code> shipped two days ago, including the queue table this whole post is about.</li>
<li><strong>T-024A (mapping backend):</strong> in flight — PR #292, acceptance run currently executing.</li>
<li><strong>T-024B (reviewer UI):</strong> next.</li>
</ul>
<p>Harmonia is the conceptual centerpiece of T-024 — the deliverable customers actually pay for. The reviewer UI (T-024B) is the surface they touch. Together they close the read-write-think gap that Hecate and Ariadne couldn't close alone.</p>
<p>The acceptance run will tell us whether Harmonia ships green or with documented blind-set follow-up. Either way, <strong>the architecture lands</strong>, the four bugs are fixed in committed code, the script + benchmark + ROCm setup are reproducible, and the next time a clinician opens Ariadne they'll see fewer rows in their queue — because Harmonia got there first.</p>
<p>That last sentence is the one that matters.</p>]]></content:encoded>
            <category>development</category>
            <category>ai</category>
            <category>concept-mapping</category>
            <category>omop</category>
            <category>harmonia</category>
            <category>hecate</category>
            <category>ariadne</category>
            <category>pgvector</category>
            <category>llm</category>
            <category>anthropic</category>
            <category>t-024</category>
        </item>
        <item>
            <title><![CDATA[Abby Study Design Compiler Ships: Accessibility, Refactors, and Production Hardening]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28</guid>
            <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A landmark day for the Parthenon platform: the Abby Study Design Compiler crossed the finish line and landed in production. Alongside that headline feature, we completed a deep structural refactor of the Study Workbench, patched a collection of critical runtime bugs, and hardened the frontend with accessibility improvements and unsaved-changes guards. Phase 19 smoke testing confirmed everything holds together against the live DEV environment.]]></description>
            <content:encoded><![CDATA[<p>A landmark day for the Parthenon platform: the Abby Study Design Compiler crossed the finish line and landed in production. Alongside that headline feature, we completed a deep structural refactor of the Study Workbench, patched a collection of critical runtime bugs, and hardened the frontend with accessibility improvements and unsaved-changes guards. Phase 19 smoke testing confirmed everything holds together against the live DEV environment.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="abby-study-design-compiler-the-big-ship">Abby Study Design Compiler: The Big Ship<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#abby-study-design-compiler-the-big-ship" class="hash-link" aria-label="Enlace directo al Abby Study Design Compiler: The Big Ship" title="Enlace directo al Abby Study Design Compiler: The Big Ship">​</a></h2>
<p>The centerpiece of today's work is the fully deployed <strong>Abby-mediated Study Design Compiler</strong> — a shift in how Parthenon handles AI-assisted study authoring. Rather than a free-form AI writing surface, the Study Designer now operates as a <strong>compiler-grade, user-reviewed workflow</strong>. Abby is the product-facing harness and guide; the heavy lifting runs through <strong>local MedGemma 27B on Ollama</strong> for bounded compiler guidance, tool planning, and safe review language. Claude/Anthropic is available strictly through a scoped protocol-evaluation path behind a dedicated cloud-evaluation feature flag.</p>
<p>The new backend service layer is substantial:</p>
<ul>
<li><code>StudyDesignAbbyOrchestrator</code> — top-level workflow coordinator</li>
<li><code>StudyDesignOllamaClient</code> / <code>StudyDesignClaudeClient</code> — model-specific adapters</li>
<li><code>StudyDesignContextBuilder</code> — assembles structured context from study state</li>
<li><code>StudyDesignToolRunner</code> — executes compiler tool calls</li>
<li><code>StudyDesignGuidanceService</code> — surfaces human-readable guidance at each compiler stage</li>
<li><code>StudyDesignStructuredOutputSchemas</code> — a named schema catalog covering protocol extraction, compiler guidance, phenotype recommendation, concept-set drafts, cohort drafts, analysis-plan drafts, asset repair suggestions, and package-manifest review</li>
</ul>
<p>Protocol uploads inside an <strong>existing design session</strong> now create a new version, populate the workbench, and keep the user in the Intent Review panel and downstream compiler stages — no jarring redirects. Standalone protocol intake still creates a new study container and routes to that study's design tab, which is the correct behavior for that distinct flow.</p>
<p>The extraction layer surfaces <strong>evidence spans, field-level confidence scores, overall confidence, uncertainty notes, design assumptions, and initial-gate issue reporting</strong> for protocols that don't clear the adequacy threshold. Users see exactly how confident the compiler is in each extracted field and why — a meaningful improvement in interpretability over opaque AI suggestions.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="study-workbench-structural-refactor-complete">Study Workbench: Structural Refactor Complete<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#study-workbench-structural-refactor-complete" class="hash-link" aria-label="Enlace directo al Study Workbench: Structural Refactor Complete" title="Enlace directo al Study Workbench: Structural Refactor Complete">​</a></h2>
<p>With the compiler in place, we also completed a multi-commit refactor of the Study Workbench that had been accumulating scope. The old monolithic component has been decomposed into focused, independently maintainable panels:</p>
<ul>
<li><strong>Top-level panels</strong>: <code>IntentReview</code>, <code>BottomUpCompatibility</code>, <code>Feasibility</code>, <code>AnalysisPlan</code></li>
<li><strong>Leaf panels</strong>: <code>Phenotype</code>, <code>ConceptSet</code>, <code>Cohort</code>, <code>StudyCompilerGuidance</code></li>
<li><strong>Shared infrastructure</strong>: atoms and helper utilities extracted to <code>workbench/</code></li>
</ul>
<p>This decomposition dramatically improves readability and sets up clean boundaries for future feature work on individual compiler stages. Each panel owns its own state slice and renders independently, which will matter as we add per-panel loading and error states.</p>
<p>A long-overdue cleanup also landed today: <strong><code>StudyDesigner.tsx</code> (1,380 lines of dead code) was deleted</strong> (<code>a5bc69925</code>). It had been orphaned by the new architecture and was causing confusion about which file was authoritative. Gone.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="bug-fixes-runtime-reliability">Bug Fixes: Runtime Reliability<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#bug-fixes-runtime-reliability" class="hash-link" aria-label="Enlace directo al Bug Fixes: Runtime Reliability" title="Enlace directo al Bug Fixes: Runtime Reliability">​</a></h2>
<p>Several production-quality fixes landed today:</p>
<ul>
<li><strong>Lock-race guard + dirty-form unsaved-changes warning</strong> (<code>fb8535738</code>): The workbench now prevents concurrent lock acquisition races and warns users before navigating away from unsaved form state. Both issues were silent data-loss vectors.</li>
<li><strong>NaN <code>concept_id</code>, <code>ensureSession</code> dedupe, mutation error banner, search error catch</strong> (<code>d10799c5d</code>): Four distinct runtime issues cleaned up — invalid numeric coercion on concept IDs, duplicate session initialization calls, missing error feedback on mutation failures, and unhandled rejections in the concept search path.</li>
<li><strong>Error panels, score NaN, Recommend tab dead UI</strong> (<code>7d2958f0c</code>): The Recommend tab was rendering a non-functional UI state; score values were coercing to NaN in display; error states weren't surfacing to the user. All addressed.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="accessibility-wai-aria-tablist--async-live-regions">Accessibility: WAI-ARIA Tablist + Async Live Regions<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#accessibility-wai-aria-tablist--async-live-regions" class="hash-link" aria-label="Enlace directo al Accessibility: WAI-ARIA Tablist + Async Live Regions" title="Enlace directo al Accessibility: WAI-ARIA Tablist + Async Live Regions">​</a></h2>
<p>The Study Designer tab navigation now meets <strong>WAI-ARIA tablist</strong> spec (<code>4b6600d20</code>): full keyboard support via arrow keys, correct <code>role="tablist"</code> / <code>role="tab"</code> / <code>role="tabpanel"</code> markup, and <code>aria-live</code> regions for async result updates. This means screen reader users get announced feedback when compiler results load — important for a workflow that involves multiple asynchronous AI calls. This brings the Study Designer into compliance with our accessibility baseline.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="infrastructure-loopback-binding-for-study-agent">Infrastructure: Loopback Binding for Study Agent<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#infrastructure-loopback-binding-for-study-agent" class="hash-link" aria-label="Enlace directo al Infrastructure: Loopback Binding for Study Agent" title="Enlace directo al Infrastructure: Loopback Binding for Study Agent">​</a></h2>
<p>A small but security-relevant infra change: the study-agent host port is now bound to <code>127.0.0.1</code> (loopback only) rather than <code>0.0.0.0</code> (<code>0cc627b1a</code>). This prevents the agent port from being exposed on non-loopback interfaces in development and staging environments, closing an inadvertent network exposure.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="phase-19-smoke-testing">Phase 19 Smoke Testing<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#phase-19-smoke-testing" class="hash-link" aria-label="Enlace directo al Phase 19 Smoke Testing" title="Enlace directo al Phase 19 Smoke Testing">​</a></h2>
<p>Automated smoke tests for Phase 19, Task 2 (<code>afa17827c</code>) ran against the live DEV Parthenon instance and passed. With the compiler, refactor, and bug fixes all landing in the same window, having the smoke suite green gives us confidence the integrated system is stable before we move into wider QA.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-28#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>With the compiler architecture in place and the workbench decomposed into clean panels, the immediate priorities are:</p>
<ol>
<li><strong>Per-panel progressive loading and granular error recovery</strong> — now that panels are independent components, we can give each one its own loading skeleton and retry path.</li>
<li><strong>Confidence threshold configuration</strong> — the initial-gate issue reporting is working, but teams need a way to tune adequacy thresholds for their protocol types.</li>
<li><strong>Package manifest review UX</strong> — the structured schema is wired; the review UI in <code>StudyCompilerGuidance</code> needs a first-class display surface.</li>
<li><strong>Expanded Phase 19 test coverage</strong> — smoke tests are green, but we want integration tests covering the full compiler pipeline end-to-end.</li>
</ol>
<p>A dense, high-quality day. The compiler is live, the workbench is clean, and the platform is more accessible and reliable than it was this morning.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
            <category>backend</category>
            <category>ai</category>
            <category>testing</category>
            <category>infrastructure</category>
        </item>
        <item>
            <title><![CDATA[GIS Boundary Explorer, Study Design Refactors, and Deployment Hardening]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29</guid>
            <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A busy Tuesday on Parthenon with work spanning three distinct fronts: consolidating the GIS layer we stood up in Phase 19, continuing to clean up the study design workbench on the frontend, and tightening several infrastructure and testing rough edges that had accumulated over the sprint.]]></description>
            <content:encoded><![CDATA[<p>A busy Tuesday on Parthenon with work spanning three distinct fronts: consolidating the GIS layer we stood up in Phase 19, continuing to clean up the study design workbench on the frontend, and tightening several infrastructure and testing rough edges that had accumulated over the sprint.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="gis-layer-consolidation--postgis-boundary-explorer-and-ohdsi-roadmap">GIS Layer Consolidation — PostGIS, Boundary Explorer, and OHDSI Roadmap<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29#gis-layer-consolidation--postgis-boundary-explorer-and-ohdsi-roadmap" class="hash-link" aria-label="Enlace directo al GIS Layer Consolidation — PostGIS, Boundary Explorer, and OHDSI Roadmap" title="Enlace directo al GIS Layer Consolidation — PostGIS, Boundary Explorer, and OHDSI Roadmap">​</a></h2>
<p>The big structural commit today (<code>7acd3b0ab</code>) wraps up the loose ends left after Phase 19's five-wave rollout of urban/rural stratification. That phase got data into the <code>gis</code> schema — 3,234 counties, 14,401 HUD ZIP↔county crosswalk rows, 416K patient geography records — but a handful of integration issues needed to be resolved before the layer could be considered stable.</p>
<p><strong>PostGIS <code>search_path</code> fix:</strong> Spatial queries were silently falling back to the public schema in certain session contexts because PostGIS extension functions aren't schema-qualified inside the extension itself. The fix pins <code>search_path</code> correctly so that <code>gis.*</code> functions resolve against the PostGIS-enabled schema without ambiguity. This is a subtle but load-bearing fix — without it, geometry operators would fail unpredictably depending on how a connection was established.</p>
<p><strong>Boundary Explorer:</strong> A new UI surface for browsing geographic boundaries has been wired in. Researchers can now visually inspect which counties belong to which urban/rural classification tier before committing to a stratification design — useful for sanity-checking cohort geography before running a full incidence-rate analysis.</p>
<p><strong>OHDSI TODO file:</strong> A planning artifact was committed alongside the GIS work cataloguing the remaining alignment work needed to bring the <code>gis</code> schema into full conformance with OMOP Location conventions. This is intentionally tracked in the repo rather than an external ticket system so future contributors have context immediately when touching this area.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="study-design-workbench-refactors">Study Design Workbench Refactors<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29#study-design-workbench-refactors" class="hash-link" aria-label="Enlace directo al Study Design Workbench Refactors" title="Enlace directo al Study Design Workbench Refactors">​</a></h2>
<p>Two meaningful frontend refactors landed today under the studies domain, both aimed at making the workbench code more maintainable as complexity grows.</p>
<p><strong><code>useStudyDesignWorkbench()</code> hook extraction (<code>1813d32a9</code>):</strong> Logic that was previously scattered across the study design page component has been lifted into a dedicated custom hook. The hook encapsulates workbench state, dirty-tracking, and the coordination between the local draft and the persisted design. This makes the page component substantially thinner and sets up a clean seam for testing workbench behavior in isolation without rendering overhead. The commit is tagged <code>260428-gu8</code> for traceability back to the planning artifact.</p>
<p><strong>Zod schema parsing in intent assistance (<code>21b8aa2ed</code>):</strong> The pattern of drilling <code>valueAt</code> accessors deep into nested form state to feed the intent assistance system has been replaced with explicit Zod schema parsing at the boundary. This matters because <code>valueAt</code> chains are brittle — a schema change silently produces <code>undefined</code> rather than a parse error, and bugs in the assistance suggestions are hard to trace back to their source. Zod's <code>.safeParse()</code> gives us structured error output and forces the shape of intent assistance inputs to be explicitly declared.</p>
<p><strong><code>if_unmodified_since</code> lock mutation (<code>238cf46c5</code>):</strong> The optimistic concurrency guard for study designs — which prevents two researchers from overwriting each other's changes — now correctly threads the <code>If-Unmodified-Since</code> header through the frontend mutation call. Previously the header was being generated server-side but not respected end-to-end from the client, meaning the lock was advisory at best.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="testing-infrastructure-improvements">Testing Infrastructure Improvements<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29#testing-infrastructure-improvements" class="hash-link" aria-label="Enlace directo al Testing Infrastructure Improvements" title="Enlace directo al Testing Infrastructure Improvements">​</a></h2>
<p>Several commits addressed test reliability and environment hygiene.</p>
<p><strong>PHPUnit DB env var overrides (<code>0364cb637</code>):</strong> Database connection environment variables used by PHPUnit are now settable as defaults that can be overridden at the shell level. Previously the test suite would only honor values baked into <code>.env.testing</code>, making it cumbersome to point tests at an alternate database without editing tracked files. This is particularly useful in CI matrix jobs.</p>
<p><strong>Protected-DB guard exemption for <code>php artisan test</code> (<code>2b32c5c5f</code>):</strong> The safety guard that prevents accidental writes to production-tagged databases was incorrectly firing when running <code>php artisan test</code> locally, because the Artisan test runner shares a process with the application bootstrap. The guard now explicitly exempts the test command path so developers aren't blocked by false positives during the inner loop.</p>
<p><strong>FinnGen schema teardown fix (<code>08913f265</code>):</strong> The FinnGen integration test suite was dropping its test schema in <code>afterAll</code> rather than <code>afterEach</code>, meaning a failing test mid-suite would leave schema debris that poisoned subsequent runs. Moving teardown to <code>afterEach</code> makes each test case independently clean.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="deployment-and-broadcast-hardening">Deployment and Broadcast Hardening<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29#deployment-and-broadcast-hardening" class="hash-link" aria-label="Enlace directo al Deployment and Broadcast Hardening" title="Enlace directo al Deployment and Broadcast Hardening">​</a></h2>
<p><strong>Compose service list caching (<code>9f949d5f8</code>):</strong> The smoke-test step in the deployment pipeline queries the Docker Compose service list to verify all expected containers are healthy. Under load, this query was occasionally returning a partial list due to a race with the Compose engine, triggering false-positive warnings in Slack. The fix caches the service list at the start of the smoke-test job rather than re-querying it dynamically, eliminating the transient failures.</p>
<p><strong><code>hasSession()</code> guard in broadcast (<code>e7277c448</code>):</strong> The broadcast layer was calling <code>session()-&gt;getId()</code> unconditionally in a context where sessions aren't always initialized — specifically in queue worker processes that handle broadcast events. Wrapping the call with <code>hasSession()</code> prevents the cryptic "Session store not set on request" exception that was appearing sporadically in worker logs.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-29#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>With the GIS layer consolidated and the PostGIS path fixed, the immediate next step is completing the OHDSI Location conformance work catalogued in today's TODO artifact — specifically aligning <code>gis.geographic_location</code> PKs with OMOP <code>location_id</code> so that standard OHDSI tools can traverse the geography tables without a translation layer.</p>
<p>On the study design side, the <code>useStudyDesignWorkbench()</code> hook extraction opens the door to adding workbench-level unit tests, which are currently absent. Expect those to start landing in the next few sessions alongside continued cleanup of the intent assistance pipeline now that the Zod parsing boundary is established.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
            <category>backend</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>testing</category>
            <category>deployment</category>
        </item>
        <item>
            <title><![CDATA[Urban/Rural Stratification, Study Designer Fixes, and Abby's Protocol Compiler Takes Shape]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27</guid>
            <pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A focused day on Parthenon with three distinct threads advancing in parallel: the GIS-backed urban/rural stratification pipeline moved from concept to tested RED-phase scaffolding, the Study Designer received several important bug fixes and UX improvements, and the architectural groundwork for Abby's guided Study Design Compiler workflow was laid out in detail.]]></description>
            <content:encoded><![CDATA[<p>A focused day on Parthenon with three distinct threads advancing in parallel: the GIS-backed urban/rural stratification pipeline moved from concept to tested RED-phase scaffolding, the Study Designer received several important bug fixes and UX improvements, and the architectural groundwork for Abby's guided Study Design Compiler workflow was laid out in detail.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="gis-01gis-04-urbanrural-county-stratification-wave-0-red-phase">GIS-01..GIS-04: Urban/Rural County Stratification (Wave 0 RED Phase)<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#gis-01gis-04-urbanrural-county-stratification-wave-0-red-phase" class="hash-link" aria-label="Enlace directo al GIS-01..GIS-04: Urban/Rural County Stratification (Wave 0 RED Phase)" title="Enlace directo al GIS-01..GIS-04: Urban/Rural County Stratification (Wave 0 RED Phase)">​</a></h2>
<p>The most structurally significant work today was completing the Wave 0 RED (Red-Green-Refactor) plan for ticket <code>19-01</code> — county-level urban/rural stratification using USDA Urban Influence Codes and related geospatial crosswalk data.</p>
<p>Four formal requirement IDs were declared (<code>GIS-01</code> through <code>GIS-04</code>), covering the ingestion pipeline, crosswalk logic, DSN guard, and stratification exposure via <code>urban_pct</code>. The RED phase test suite is now fully stubbed:</p>
<ul>
<li><strong>pytest infrastructure</strong> was wired up alongside a UA (Urban Area) loader, crosswalk resolver, and a DSN guard that prevents accidental writes to non-GIS schemas. This guard is a small but meaningful safety rail — GIS data loads are destructive-by-nature and schema isolation matters.</li>
<li><strong>Vitest stubs</strong> on the frontend mirror the backend test surface so the full stack can be exercised in CI once the GREEN implementations land.</li>
<li>A <code>VALIDATION.md</code> flip marks Wave 0 as in-progress, giving the agentic workers a clear handoff signal.</li>
<li>The <code>gis</code> schema stubs and <code>urban_pct</code> stratification column are defined in the RED Pest (PHP) layer as well, ensuring the backend ORM side has coverage targets before any real SQL is written.</li>
</ul>
<p>The UA county pipeline will feed directly into cohort stratification workflows in Parthenon, allowing researchers to slice outcomes by rurality — a dimension that's increasingly central to health equity analyses in OHDSI studies.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="study-designer-three-bug-fixes-and-a-ux-gap-closed">Study Designer: Three Bug Fixes and a UX Gap Closed<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#study-designer-three-bug-fixes-and-a-ux-gap-closed" class="hash-link" aria-label="Enlace directo al Study Designer: Three Bug Fixes and a UX Gap Closed" title="Enlace directo al Study Designer: Three Bug Fixes and a UX Gap Closed">​</a></h2>
<p>The Study Designer page received a concentrated round of fixes that unblock real usage:</p>
<p><strong>Protocol import now creates studies correctly.</strong> A regression had broken the flow where importing a protocol document should materialize a new study record. This is now fixed — the controller properly chains the protocol parse result into a study creation call rather than dropping it silently.</p>
<p><strong>Anthropic model default corrected to Opus.</strong> The Study Designer's cloud-evaluation path was defaulting to the wrong Anthropic model tier. This matters for protocol compiler quality: Opus is the evaluation-grade model intended for structured protocol critique and intent extraction. Sonnet or Haiku would produce materially weaker structured outputs for this use case.</p>
<p><strong>Protocol import surface exposed on the Study Designer page.</strong> The import entry point existed in the backend but wasn't wired into the frontend page component. Researchers can now actually trigger a protocol upload from within the designer UI without navigating away or using a hidden API call directly.</p>
<p>These three fixes together restore the Study Designer to a functional end-to-end state for the protocol-upload-to-study flow.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="deployment-php-fpm-reload-suppression-on-hot-deploys">Deployment: PHP-FPM Reload Suppression on Hot Deploys<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#deployment-php-fpm-reload-suppression-on-hot-deploys" class="hash-link" aria-label="Enlace directo al Deployment: PHP-FPM Reload Suppression on Hot Deploys" title="Enlace directo al Deployment: PHP-FPM Reload Suppression on Hot Deploys">​</a></h2>
<p>A quieter but operationally important change: PHP-FPM reloads are now suppressed during hot deploys. Previously, hot deploys would trigger a full FPM reload, causing brief request interruptions and cold pool startup latency. This change keeps worker processes warm across asset-only or config-only deploys, which is the common case in Parthenon's CI/CD cadence.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="installer-v030-rc1-release-notes-draft">Installer: v0.3.0-rc1 Release Notes Draft<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#installer-v030-rc1-release-notes-draft" class="hash-link" aria-label="Enlace directo al Installer: v0.3.0-rc1 Release Notes Draft" title="Enlace directo al Installer: v0.3.0-rc1 Release Notes Draft">​</a></h2>
<p>A draft of the <code>v0.3.0-rc1</code> release notes was committed to the installer docs. This release candidate surfaces the GIS stratification work, Study Designer improvements, and the Abby integration scaffolding as a coherent milestone. The release notes draft serves as both external communication and an internal checklist — gaps in the notes surface gaps in the implementation.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="architectural-planning-abby-as-study-design-compiler-harness">Architectural Planning: Abby as Study Design Compiler Harness<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#architectural-planning-abby-as-study-design-compiler-harness" class="hash-link" aria-label="Enlace directo al Architectural Planning: Abby as Study Design Compiler Harness" title="Enlace directo al Architectural Planning: Abby as Study Design Compiler Harness">​</a></h2>
<p>The devlog for tomorrow's work (<code>2026-04-27-study-design-compiler-abby-medgemma-todo.md</code>) lays out the full architectural vision for Abby's role in the Study Design Compiler. The core model is now clearly articulated:</p>
<ul>
<li><strong>Abby</strong> is the product-facing assistant and workflow harness — she explains, guides, and surfaces decisions to the user.</li>
<li><strong>MedGemma 27B on local Ollama</strong> is Abby's default inference engine for ordinary chat and lightweight extraction — fully local, no data leaves the instance.</li>
<li><strong>Claude</strong> is the scoped, opt-in evaluator for structured protocol compiler tasks when the cloud-evaluation flag is explicitly enabled.</li>
</ul>
<p>The Phase 0 work already completed today — renaming stale <code>protocol_upload_claude</code> labels to provider-neutral Abby labels while preserving <code>provider=anthropic</code> in audit records — reflects this architecture. The system stays auditable and provenance-complete without leaking model-specific assumptions into user-facing surfaces.</p>
<p>The primary surfaces for upcoming Abby compiler work are <code>StudyDesignWorkbench.tsx</code>, <code>StudyDesignController.php</code>, and the <code>StudyDesign/*</code> service layer — all of which already received attention today in the bug fix round.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-27#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>GREEN phase for GIS-01..GIS-04</strong>: implement the actual UA loader, crosswalk resolver, and <code>urban_pct</code> materialization to make the RED tests pass.</li>
<li><strong>Abby Phase 1</strong>: wire MedGemma 27B as the default Ollama backend for Abby chat within the Study Design Workbench.</li>
<li><strong>Protocol compiler structured output</strong>: define the JSON schema for protocol intent extraction and hook it into the <code>StudyDesign</code> service layer.</li>
<li><strong>v0.3.0-rc1 release</strong>: finalize the installer release notes and tag the candidate once GIS Wave 0 GREEN is complete.</li>
</ul>
<p>The urban/rural stratification work in particular is shaping up to be one of the more impactful additions to Parthenon's cohort analysis capabilities — expect it to move quickly once the GREEN implementations start landing.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
            <category>backend</category>
            <category>testing</category>
            <category>ai</category>
            <category>infrastructure</category>
        </item>
        <item>
            <title><![CDATA[Installer Hardening, Care Bundle Crash Safety, and Patient Similarity Repairs]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26</guid>
            <pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A dense Saturday of stability work across Parthenon — today's 89 commits concentrated on three converging themes: getting the cross-platform installer into a shippable state, closing out a backlog of high-severity audit findings in the Care Bundles engine, and restoring broken workspace workflows in Patient Similarity. No new features today; this was deliberate debt-clearing ahead of the next feature milestone.]]></description>
            <content:encoded><![CDATA[<p>A dense Saturday of stability work across Parthenon — today's 89 commits concentrated on three converging themes: getting the cross-platform installer into a shippable state, closing out a backlog of high-severity audit findings in the Care Bundles engine, and restoring broken workspace workflows in Patient Similarity. No new features today; this was deliberate debt-clearing ahead of the next feature milestone.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="installer-gui--ci-pipeline-road-to-phase-a-sign-off">Installer GUI &amp; CI Pipeline: Road to Phase A Sign-Off<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26#installer-gui--ci-pipeline-road-to-phase-a-sign-off" class="hash-link" aria-label="Enlace directo al Installer GUI &amp; CI Pipeline: Road to Phase A Sign-Off" title="Enlace directo al Installer GUI &amp; CI Pipeline: Road to Phase A Sign-Off">​</a></h2>
<p>The bulk of today's surface area was the installer, which is undergoing structured bench testing across Windows, macOS, and Linux. The Linux Phase A session surfaced four P0-class issues that are now resolved (<code>e88bb1d7d</code>), covering cases that would have blocked a first-run install cold. Alongside those fixes, the installer now shows <strong>visible bundle download progress</strong> and ships clearer Step 1 copy — two UX items that kept appearing in tester feedback (<code>be2cd81c8</code>).</p>
<p>Two platform-specific CI issues were also closed:</p>
<ul>
<li><strong>Windows <code>.sha256</code> sidecar files</strong> were being written with CRLF line endings, which caused checksum verification to fail silently on some toolchains. The CI job now enforces LF output and the verifier is hardened against trailing whitespace (<code>0ed6f7dc6</code>).</li>
<li><strong>macOS notarytool submissions</strong> would occasionally time out on Apple's side and leave the pipeline in a failed state with no retry. The job now retries on transient timeouts before surfacing an error (<code>a144b6054</code>).</li>
</ul>
<p>A subtler macOS fix landed separately: the installer GUI was not augmenting <code>PATH</code> with the Docker CLI location before invoking it, which meant Docker commands silently failed for users who installed Docker Desktop to a non-standard location. The misleading "trust pill" UI element that implied a verification state before it was established was also removed (<code>7e4685a80</code>).</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="care-bundles-closing-the-deferred-audit-backlog">Care Bundles: Closing the Deferred Audit Backlog<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26#care-bundles-closing-the-deferred-audit-backlog" class="hash-link" aria-label="Enlace directo al Care Bundles: Closing the Deferred Audit Backlog" title="Enlace directo al Care Bundles: Closing the Deferred Audit Backlog">​</a></h2>
<p>Earlier this week a formal audit of the Care Bundles engine produced a triage list with HIGH, MEDIUM, and LOW findings. The first wave landed on the 25th; today's follow-up commits closed everything that was deferred.</p>
<p>The three HIGH findings are worth calling out explicitly because they represent real data-integrity risks in production:</p>
<p><strong>Cohort write atomicity (HIGH-4).</strong> <code>MeasureCohortExportService::writeMembers</code> and <code>IntersectionCohortService::writeToResultsCohort</code> were doing a delete followed by a chunked INSERT without wrapping them in a transaction on the results connection. A crash mid-write left the cohort in a partially replaced state — old rows gone, new rows incomplete. Both methods now execute delete + <code>INSERT…SELECT</code> inside a results-connection transaction. The cohort is either fully replaced or fully preserved; there is no intermediate state.</p>
<p><strong>Promotion timing race (HIGH-5).</strong> <code>CareBundleMaterializationService::promoteToCurrent</code> was called <em>inside</em> the materialization transaction, but <code>run.update(status='completed')</code> was called <em>after</em> it committed. The crash window between those two operations left <code>care_bundle_current_runs</code> pointing at a run still in <code>running</code> status — which would surface to users as a completed bundle incorrectly showing as in-progress. Promotion is now deferred until after status is set to <code>completed</code>. The worst-case failure mode is now a stale pointer to the <em>previous</em> completed run rather than a visibility leak of a running run.</p>
<p><strong>Heap materialization on large cohorts (HIGH-6).</strong> <code>MeasureRosterService::allPersonIds()</code> was pulling the full bucket into PHP heap before chunking it into the cohort table. On a 2M-patient CDM with a wide non-compliant bucket this is a significant memory spike. <code>IntersectionCohortService</code> had the same issue via <code>qualifications-&gt;intersection()-&gt;all()</code>. Both paths are now rewritten to stream via cross-schema <code>INSERT…SELECT</code> directly from <code>app.care_bundle_measure_person_status</code> / <code>app.care_bundle_qualifications</code> into <code>&lt;resultsSchema&gt;.cohort</code>. The PHP heap never sees the member list. <code>allPersonIds()</code> has been removed; a new <code>CareBundleQualificationService::intersectionQueryForExport()</code> method covers the intersection path.</p>
<p>The workbench workflow layer also received hardening work in a separate commit (<code>38fad397e</code>) to improve resilience around edge cases surfaced during the audit review.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="patient-similarity-workspace-and-validation-repairs">Patient Similarity: Workspace and Validation Repairs<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26#patient-similarity-workspace-and-validation-repairs" class="hash-link" aria-label="Enlace directo al Patient Similarity: Workspace and Validation Repairs" title="Enlace directo al Patient Similarity: Workspace and Validation Repairs">​</a></h2>
<p>Two fixes landed in the Patient Similarity module today:</p>
<ul>
<li><strong>Workspace workflows</strong> were broken and have been repaired (<code>3915024fa</code>). The specific regression is not detailed in the notes but this was blocking iterative similarity analysis sessions entirely.</li>
<li><strong>Temporal compare validation</strong> was returning HTTP 200 with silent failures when the request payload was malformed. The endpoint now returns proper validation errors (<code>2a1552b7a</code>), which unblocks client-side error handling and makes debugging significantly faster.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="infrastructure--miscellaneous">Infrastructure &amp; Miscellaneous<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26#infrastructure--miscellaneous" class="hash-link" aria-label="Enlace directo al Infrastructure &amp; Miscellaneous" title="Enlace directo al Infrastructure &amp; Miscellaneous">​</a></h2>
<ul>
<li>The local PHPUnit database host is now pinned in CI configuration (<code>8cb8076db</code>), eliminating a class of flaky test failures caused by host resolution differences across environments.</li>
<li>The i18n layer now tolerates <code>null</code> resource placeholders in the frontend without throwing (<code>6aebba113</code>). This was causing silent blank-rendering issues in locales with incomplete translation coverage.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-26#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>With the Care Bundles audit fully closed and the installer approaching Phase A sign-off across all three platforms, the near-term focus shifts to:</p>
<ol>
<li><strong>Installer Phase B bench testing</strong> — Windows and macOS sessions are queued; the fixes today should clear the blocker list.</li>
<li><strong>Care Bundles DQ checker completeness</strong> — the audit flagged that denominator concept presence was never verified. That finding was truncated in today's notes, suggesting it's in-flight or queued as a follow-on item.</li>
<li><strong>Patient Similarity stability</strong> — the two fixes today were reactive; a proactive review of the module's workflow layer is warranted before the next feature increment.</li>
</ol>
<p>Overall, today was exactly the kind of session a platform needs before a broader rollout push — unglamorous, high-leverage, and necessary.</p>]]></content:encoded>
            <category>development</category>
            <category>frontend</category>
            <category>backend</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>testing</category>
            <category>deployment</category>
        </item>
        <item>
            <title><![CDATA[Care Bundles Phase 3, macOS CI Overhaul, and First-Run Installer Design]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25</guid>
            <pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A dense day on Parthenon with 58 commits landing across a wide surface area: the Care Bundles workbench reached a meaningful analytical milestone with Phase 3 Tier A methodology complete, the macOS CI pipeline was significantly streamlined, and we laid down a comprehensive design spec for the installer's first-run experience. Sprinkled throughout were a handful of i18n fixes and auth UX improvements that round out the release candidate picture.]]></description>
            <content:encoded><![CDATA[<p>A dense day on Parthenon with 58 commits landing across a wide surface area: the Care Bundles workbench reached a meaningful analytical milestone with Phase 3 Tier A methodology complete, the macOS CI pipeline was significantly streamlined, and we laid down a comprehensive design spec for the installer's first-run experience. Sprinkled throughout were a handful of i18n fixes and auth UX improvements that round out the release candidate picture.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="care-bundles-workbench--phases-1-through-3">Care Bundles Workbench — Phases 1 Through 3<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#care-bundles-workbench--phases-1-through-3" class="hash-link" aria-label="Enlace directo al Care Bundles Workbench — Phases 1 Through 3" title="Enlace directo al Care Bundles Workbench — Phases 1 Through 3">​</a></h2>
<p>The biggest theme of the day was the Care Bundles workbench, which advanced through three discrete phases of work, each building on the last.</p>
<p><strong>Phase 1</strong> introduced a population gate requiring N ≥ 100,000 before any bundle analysis runs. This is a deliberate methodological guardrail — rate estimates on small populations produce confidence intervals too wide to act on, and surfacing them would undermine trust in the tool. Phase 1 also wired up the VSAC library tabs, giving analysts a structured entry point for value set selection within the workbench UI.</p>
<p><strong>Phase 2</strong> added Wilson 95% confidence intervals to all rate outputs. Wilson CIs are the right choice here over the naïve normal approximation: they remain well-behaved at the extremes (low event rates, small cells) where OHDSI datasets frequently live. This phase also tightened up denominator-exclusion semantics — patients who meet an exclusion criterion are now consistently removed from the denominator before rate calculation rather than post-hoc, which matters for any bundle comparing opt-in vs. opt-out populations.</p>
<p><strong>Phase 3 Tier A</strong> delivered the methodology layer, DQ flags, and stratification. The DQ flags are particularly important: they surface data quality warnings inline with results so analysts don't have to cross-reference a separate DQ report to know whether a rate estimate is trustworthy. Stratification support means bundles can now be broken down by age band, sex, and calendar period without requiring separate analysis runs.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="migration--permission-fixes">Migration &amp; Permission Fixes<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#migration--permission-fixes" class="hash-link" aria-label="Enlace directo al Migration &amp; Permission Fixes" title="Enlace directo al Migration &amp; Permission Fixes">​</a></h3>
<p>These features came with some production turbulence worth documenting. The original <code>feat(care-bundles)</code> commit was pushed with <code>--no-verify</code>, which bypassed pre-commit hooks and left six migrations unapplied. The silent failure mode — action buttons doing nothing — made this particularly annoying to diagnose.</p>
<p>Two root causes were fixed:</p>
<ol>
<li>
<p><strong>Table ownership</strong>: New tables were being created under the <code>parthenon_migrator</code> role rather than <code>parthenon_owner</code>. Because default privileges are scoped to the owner, <code>parthenon_app</code> ended up with no DML access at all. The fix is <code>SET ROLE parthenon_owner</code> at the top of each DDL migration's <code>up()</code> method, which we should codify as a project standard going forward.</p>
</li>
<li>
<p><strong>Coverage query join</strong>: <code>coverageMatrix()</code> had an incorrect join anchor on <code>care_bundle_qu...</code> (truncated in the devlog, but fixed in the patch). If you're touching this query, the full table name is <code>care_bundle_quorum_members</code>.</p>
</li>
</ol>
<p>The lesson here — and it's not the first time — is that <code>--no-verify</code> should be treated as a last resort with an immediate follow-up commit. We'll look at adding a CI gate that fails the build if pending migrations are detected against the schema baseline.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="macos-ci-universal-binary--notarytool-fix">macOS CI: Universal Binary + Notarytool Fix<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#macos-ci-universal-binary--notarytool-fix" class="hash-link" aria-label="Enlace directo al macOS CI: Universal Binary + Notarytool Fix" title="Enlace directo al macOS CI: Universal Binary + Notarytool Fix">​</a></h2>
<p>The macOS build pipeline got two meaningful improvements.</p>
<p>First, we collapsed the two separate macOS jobs (arm64 and x86_64) into a single job that produces a universal binary. This halves our macOS CI runner minutes and eliminates the awkward artifact-merging step that had been living in a post-build script.</p>
<p>Second, and more critically, we bypassed Tauri's built-in notarization path and switched to calling <code>notarytool</code> directly. Tauri's notarize integration has been broken against recent Xcode toolchain versions — it was silently skipping notarization in some cases and hard-failing in others depending on environment. Calling <code>notarytool</code> directly gives us explicit control over the stapling step and surfaces errors clearly in CI logs. The fix lives in the installer CI workflow; if you're debugging notarization failures locally, the same <code>xcrun notarytool submit ... --wait</code> invocation works end-to-end.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="installer-first-run-design-spec">Installer: First-Run Design Spec<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#installer-first-run-design-spec" class="hash-link" aria-label="Enlace directo al Installer: First-Run Design Spec" title="Enlace directo al Installer: First-Run Design Spec">​</a></h2>
<p>Two documentation commits landed the design specification for a comprehensive first-run improvement to the installer. The spec covers the contract surface extension — specifically, what the installer needs to know from (and communicate back to) the Parthenon backend during initial setup.</p>
<p>This is groundwork for a larger effort: right now the installer and the platform have a fairly thin handshake, and first-run failures often leave users in an ambiguous state with no clear recovery path. The upcoming implementation will formalize the contract so both sides have clear invariants.</p>
<p>If you're planning work in the installer, the spec in <code>docs/installer/</code> is worth reading before touching the onboarding flow.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="auth--i18n-polish">Auth &amp; i18n Polish<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#auth--i18n-polish" class="hash-link" aria-label="Enlace directo al Auth &amp; i18n Polish" title="Enlace directo al Auth &amp; i18n Polish">​</a></h2>
<p>Two smaller but user-facing improvements shipped today:</p>
<ul>
<li><strong>Login locale and remember-me controls</strong> are now surfaced directly on the login screen. Previously locale had to be set post-login, which meant non-English speakers hit an English login page every session.</li>
<li><strong>Arabic</strong> is now available in the locale picker, and a label text mismatch in the Finnish locale was corrected.</li>
</ul>
<p>These are the kind of changes that are easy to overlook in a feature-heavy week but matter a lot for international deployments.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-25#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>Care Bundles Phase 3 Tier B</strong> — expected to cover cohort export and bundle comparison views.</li>
<li><strong>Installer first-run implementation</strong> — Phase 1 of the contract surface extension following today's spec.</li>
<li><strong>Migration ownership standard</strong> — Formalizing <code>SET ROLE parthenon_owner</code> as a required pattern and adding a CI lint check.</li>
<li><strong><code>omop:register-source</code> stabilization</strong> — Yesterday's fixes to the artisan command and its test suite feed into the broader OMOP source registration flow; integration testing coverage is next.</li>
</ul>]]></content:encoded>
            <category>development</category>
            <category>analytics</category>
            <category>ohdsi</category>
            <category>frontend</category>
            <category>backend</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>testing</category>
            <category>deployment</category>
        </item>
        <item>
            <title><![CDATA[Installer V2 Takes Shape: OMOP CDM Phase Skeleton, Docker Secrets, and i18n Crossing the 90% Line]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24</guid>
            <pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A heavy day on the Parthenon installer front — 81 commits landed across the repo, pushing installer-v2 from "wired up" to "genuinely trustworthy." We added Docker secrets support, closed a handful of correctness bugs surfaced in post-review, scaffolded the omop_cdm phase for existing-CDM deployments, and nudged Finnish, Japanese, and Chinese localizations well into the mid-90s. Here's a full breakdown.]]></description>
            <content:encoded><![CDATA[<p>A heavy day on the Parthenon installer front — 81 commits landed across the repo, pushing installer-v2 from "wired up" to "genuinely trustworthy." We added Docker secrets support, closed a handful of correctness bugs surfaced in post-review, scaffolded the <code>omop_cdm</code> phase for existing-CDM deployments, and nudged Finnish, Japanese, and Chinese localizations well into the mid-90s. Here's a full breakdown.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="installer-v2-from-skeleton-to-load-bearing-structure">Installer V2: From Skeleton to Load-Bearing Structure<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#installer-v2-from-skeleton-to-load-bearing-structure" class="hash-link" aria-label="Enlace directo al Installer V2: From Skeleton to Load-Bearing Structure" title="Enlace directo al Installer V2: From Skeleton to Load-Bearing Structure">​</a></h2>
<p>The headline work today was on the redesigned installer engine, which is now far enough along that the legacy <code>cli.py</code> shim has been fully wired to the new <code>StepRunner</code>. Critically, the wire-up <strong>preserves backward compatibility</strong> — <code>--defaults-file</code> and <code>--resume</code> both pass through cleanly, so existing automation scripts and deployment runbooks don't need to change. That matters a lot for our early adopters who have CI pipelines built around the v1 interface.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="docker-secrets-integration">Docker Secrets Integration<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#docker-secrets-integration" class="hash-link" aria-label="Enlace directo al Docker Secrets Integration" title="Enlace directo al Docker Secrets Integration">​</a></h3>
<p>The most architecturally significant addition is proper Docker secrets support (<code>feat(installer-v2): add Docker secrets integration</code>). The implementation ships two artifacts:</p>
<ul>
<li><strong><code>secrets-entrypoint</code></strong> — a small entrypoint wrapper that reads Docker secret files at container startup and injects them into the environment before the installer process begins.</li>
<li><strong><code>compose override</code></strong> — a Docker Compose override fragment that wires the secrets volume mounts into the service definition without touching the primary <code>compose.yaml</code>.</li>
</ul>
<p>This keeps credential material out of environment variables and away from <code>docker inspect</code> exposure, which is the right posture for any installation that touches a production OMOP CDM. Future work will extend this to support Vault and AWS Secrets Manager backends, but the Docker-native path covers the majority of on-premises deployments we're targeting in the near term.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="correctness-fixes-from-post-review">Correctness Fixes from Post-Review<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#correctness-fixes-from-post-review" class="hash-link" aria-label="Enlace directo al Correctness Fixes from Post-Review" title="Enlace directo al Correctness Fixes from Post-Review">​</a></h3>
<p>The post-review pass (<code>fix(installer-engine): apply post-review correctness fixes</code>) addressed several subtle issues that wouldn't have been caught by unit tests alone:</p>
<ul>
<li><code>exec_php</code> now correctly wraps invocations through <code>sh -c</code>, fixing a quoting edge case that caused failures when PHP binary paths contained spaces.</li>
<li>Solr health checks now use <code>&gt; 0</code> comparisons instead of truthy string checks, preventing false positives when Solr returns an empty-but-valid response body.</li>
<li>The Eunomia skip logic in the check function was corrected to actually short-circuit — previously, the skip flag was read but the check still executed, which caused spurious failures in environments that intentionally omit Eunomia.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="integration-tests-for-preflight-idempotency-and-event-contract">Integration Tests for Preflight Idempotency and Event Contract<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#integration-tests-for-preflight-idempotency-and-event-contract" class="hash-link" aria-label="Enlace directo al Integration Tests for Preflight Idempotency and Event Contract" title="Enlace directo al Integration Tests for Preflight Idempotency and Event Contract">​</a></h3>
<p>Good infrastructure code needs tests that prove behavior across runs, not just in isolation. Today's test additions (<code>test(installer-v2): add integration tests for preflight idempotency and event contract</code>) do exactly that:</p>
<ul>
<li><strong>Idempotency tests</strong> run the preflight phase twice in sequence and assert that the second run produces identical output and leaves the system in the same state — essential for <code>--resume</code> correctness.</li>
<li><strong>Event contract tests</strong> assert that the <code>StepRunner</code> emits the expected lifecycle events (<code>step.started</code>, <code>step.completed</code>, <code>step.failed</code>) in the right order and with the right payloads. This is the foundation for the progress-reporting UI we'll build on top of the installer in a future sprint.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="installer-sub-project-c-existing-omop-cdm-support">Installer Sub-Project C: Existing OMOP CDM Support<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#installer-sub-project-c-existing-omop-cdm-support" class="hash-link" aria-label="Enlace directo al Installer Sub-Project C: Existing OMOP CDM Support" title="Enlace directo al Installer Sub-Project C: Existing OMOP CDM Support">​</a></h2>
<p>A new design spec landed today (<code>docs: add installer sub-project C design spec</code>), formalizing the approach for deploying Parthenon against an <strong>already-existing OMOP CDM</strong> — the "bring your own data" path that many academic medical centers will use. Alongside the spec, the <code>omop_cdm</code> phase skeleton was committed (<code>feat(installer-c): omop_cdm phase skeleton with mode 3 guards and source key utility</code>).</p>
<p>Two details worth calling out:</p>
<ul>
<li><strong>Mode 3 guards</strong> — the phase is gated so it only executes when the installer is running in mode 3 (existing CDM attachment), preventing accidental execution during fresh-install or upgrade flows.</li>
<li><strong>Source key utility</strong> — a small helper that normalizes the CDM source key into the format Parthenon's metadata layer expects. This sounds minor but it's the kind of thing that causes hard-to-debug mapping failures six months later if it isn't standardized early.</li>
</ul>
<p>The design spec lives in <code>.claude/specs/</code> and is written to be executable by Claude Code — consistent with our emerging pattern of using specs as both documentation and AI-assisted implementation prompts.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="internationalization-finnish-japanese-chinese-crossing-into-the-mid-90s">Internationalization: Finnish, Japanese, Chinese Crossing into the Mid-90s<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#internationalization-finnish-japanese-chinese-crossing-into-the-mid-90s" class="hash-link" aria-label="Enlace directo al Internationalization: Finnish, Japanese, Chinese Crossing into the Mid-90s" title="Enlace directo al Internationalization: Finnish, Japanese, Chinese Crossing into the Mid-90s">​</a></h2>
<p>Two consecutive i18n pushes today moved <code>fi</code>, <code>ja</code>, and <code>zh</code> from the low-80s to the <strong>mid-to-upper 90s</strong> in translation coverage. This isn't just string-count progress — the commits specifically targeted UI surfaces that are visible during installation and initial configuration, which means international users will now have a fully localized experience through the most critical part of onboarding.</p>
<p>Design fixtures were auto-exported alongside the i18n work (the <code>[skip ci]</code> chore commit), keeping the visual regression baselines in sync with the updated string tables.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="hypertension-study-protocol-v2-spec-in-review">Hypertension Study Protocol: V2 Spec in Review<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#hypertension-study-protocol-v2-spec-in-review" class="hash-link" aria-label="Enlace directo al Hypertension Study Protocol: V2 Spec in Review" title="Enlace directo al Hypertension Study Protocol: V2 Spec in Review">​</a></h2>
<p>Separately from the installer push, the Hypertension Characterization Study protocol reached V2 this week (PI: Dr. Glenn Bock; study coordination: Dr. Sanjay Udoshi). The revision tightens the cohort definition to require <strong>two consecutive readings</strong> above threshold (SBP &gt; 130 or DBP &gt; 80), adds serum aldosterone to the baseline lab panel, and anchors the primary hypothesis to the Lu et al. 2025 diagnostic-delay benchmark. The V2 spec is staged for Claude Code execution against the Parthenon repo — implementation work is expected to begin next week.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-24#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>Installer-C phase completion</strong> — flesh out the <code>omop_cdm</code> phase beyond the skeleton, implementing the actual CDM version detection and schema validation logic.</li>
<li><strong>Progress-reporting UI</strong> — now that the event contract is tested and stable, we can start building the real-time installer progress view in the frontend.</li>
<li><strong>Hypertension study implementation</strong> — translate the V2 protocol spec into Circe cohort definitions, concept sets (<code>labs_aldosterone</code>, <code>htn_primary_aldosteronism</code>), and characterization queries.</li>
<li><strong>i18n to 100%</strong> — the remaining gaps in <code>fi</code>, <code>ja</code>, and <code>zh</code> are concentrated in the advanced analytics UI; targeting full coverage before the next release candidate.</li>
</ul>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>testing</category>
            <category>deployment</category>
        </item>
        <item>
            <title><![CDATA[Parthenon Goes Global: Comprehensive i18n Extraction Across the Full Workflow Suite]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22</guid>
            <pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Today was a focused, high-volume internationalization push across Parthenon — thirteen commits dedicated entirely to extracting i18n strings from nearly every major workflow surface in the platform. If you've been waiting for the day Parthenon could speak your language (literally), we made a serious down payment on that today.]]></description>
            <content:encoded><![CDATA[<p>Today was a focused, high-volume internationalization push across Parthenon — thirteen commits dedicated entirely to extracting i18n strings from nearly every major workflow surface in the platform. If you've been waiting for the day Parthenon could speak your language (literally), we made a serious down payment on that today.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-big-picture-why-i18n-extraction-matters">The Big Picture: Why i18n Extraction Matters<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#the-big-picture-why-i18n-extraction-matters" class="hash-link" aria-label="Enlace directo al The Big Picture: Why i18n Extraction Matters" title="Enlace directo al The Big Picture: Why i18n Extraction Matters">​</a></h2>
<p>Parthenon serves healthcare analytics teams across a wide range of institutions, many of which operate in multilingual environments or have compliance requirements around localization. Before today, much of the UI text was hardcoded inline, making translation impractical and maintenance inconsistent. The work done today lays the structural foundation for full locale support — extracting string literals into translation keys that can be mapped to any target language without touching component logic.</p>
<p>This isn't glamorous work, but it's foundational. Getting i18n architecture right early prevents a painful, bug-prone retrofit later. Today's sweep was comprehensive, touching virtually every major workflow and surface in the platform.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-was-extracted-today">What Was Extracted Today<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#what-was-extracted-today" class="hash-link" aria-label="Enlace directo al What Was Extracted Today" title="Enlace directo al What Was Extracted Today">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="clinical--investigation-workflows">Clinical &amp; Investigation Workflows<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#clinical--investigation-workflows" class="hash-link" aria-label="Enlace directo al Clinical &amp; Investigation Workflows" title="Enlace directo al Clinical &amp; Investigation Workflows">​</a></h3>
<p>The <strong>investigation clinical workflows</strong> surface received extraction coverage, pulling out strings tied to cohort investigation, patient-level review, and clinical drill-down interactions. This is one of the more text-dense parts of the platform, with contextual labels, status indicators, and action prompts that vary depending on the analytical context.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="heor--outcomes-surfaces">HEOR &amp; Outcomes Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#heor--outcomes-surfaces" class="hash-link" aria-label="Enlace directo al HEOR &amp; Outcomes Surfaces" title="Enlace directo al HEOR &amp; Outcomes Surfaces">​</a></h3>
<p>The <strong>HEOR (Health Economics and Outcomes Research) workflows</strong> were extracted, covering the study design, endpoint configuration, and results summary surfaces. Given that HEOR deliverables are often shared across international stakeholders, having these surfaces localization-ready is particularly meaningful.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="etl--source-profiler-workflows">ETL &amp; Source Profiler Workflows<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#etl--source-profiler-workflows" class="hash-link" aria-label="Enlace directo al ETL &amp; Source Profiler Workflows" title="Enlace directo al ETL &amp; Source Profiler Workflows">​</a></h3>
<p><strong>ETL source profiler workflows</strong> had their strings extracted — covering data source configuration, mapping status indicators, profiling result displays, and validation feedback. This is a technically rich surface with a lot of state-dependent messaging, so the extraction here required careful attention to dynamic string patterns.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="morpheus-workflows">Morpheus Workflows<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#morpheus-workflows" class="hash-link" aria-label="Enlace directo al Morpheus Workflows" title="Enlace directo al Morpheus Workflows">​</a></h3>
<p>The <strong>Morpheus</strong> module — Parthenon's cohort characterization and phenotyping layer — had its workflow strings extracted. Morpheus surfaces tend to involve a mix of technical OHDSI terminology and user-facing guidance text, making clean key naming here especially important for future translators.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="concept-set-workflows">Concept Set Workflows<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#concept-set-workflows" class="hash-link" aria-label="Enlace directo al Concept Set Workflows" title="Enlace directo al Concept Set Workflows">​</a></h3>
<p><strong>Concept set</strong> management workflows were extracted, covering the concept search, inclusion/exclusion rule builder, and set versioning interfaces. Concept sets are central to nearly every OHDSI analysis, so consistent, localizable labeling here has a wide downstream impact.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="gis-and-tooling-workflows">GIS and Tooling Workflows<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#gis-and-tooling-workflows" class="hash-link" aria-label="Enlace directo al GIS and Tooling Workflows" title="Enlace directo al GIS and Tooling Workflows">​</a></h3>
<p>The <strong>GIS and tooling workflows</strong> extraction covers the geographic analysis surfaces and platform-level utility tooling. GIS in particular surfaces location-based health data, where locale-appropriate formatting and labeling is directly relevant to end users.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="publish-care-gap-risk-surfaces">Publish: Care-Gap Risk Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#publish-care-gap-risk-surfaces" class="hash-link" aria-label="Enlace directo al Publish: Care-Gap Risk Surfaces" title="Enlace directo al Publish: Care-Gap Risk Surfaces">​</a></h3>
<p>The <strong>care-gap risk surfaces</strong> under the Publish module were extracted. These surfaces present population-level risk stratification outputs to care management teams — audiences who may be non-technical and for whom clear, localized language is especially important.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="profiles--patient-similarity-surfaces">Profiles &amp; Patient Similarity Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#profiles--patient-similarity-surfaces" class="hash-link" aria-label="Enlace directo al Profiles &amp; Patient Similarity Surfaces" title="Enlace directo al Profiles &amp; Patient Similarity Surfaces">​</a></h3>
<p><strong>Patient profiles and similarity surfaces</strong> received extraction coverage. These are the patient-facing analytical views, where longitudinal records and similarity clusters are surfaced. String extraction here touched a mix of clinical vocabulary labels and UX copy.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="strategus-study-package-surfaces">Strategus Study Package Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#strategus-study-package-surfaces" class="hash-link" aria-label="Enlace directo al Strategus Study Package Surfaces" title="Enlace directo al Strategus Study Package Surfaces">​</a></h3>
<p><strong>Strategus study package</strong> surfaces — covering the configuration, execution, and results review for OHDSI network study packages — were extracted. Strategus is a critical integration point for collaborative research, and having its surfaces localization-ready supports the global OHDSI network use case directly.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="imaging--genomics-surfaces">Imaging &amp; Genomics Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#imaging--genomics-surfaces" class="hash-link" aria-label="Enlace directo al Imaging &amp; Genomics Surfaces" title="Enlace directo al Imaging &amp; Genomics Surfaces">​</a></h3>
<p>Finally, <strong>imaging and genomics surfaces</strong> had their strings extracted, rounding out the sweep. These are specialized analytical surfaces, but they carry the same obligation toward consistent, translatable UI copy as the rest of the platform.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="technical-notes-for-future-developers">Technical Notes for Future Developers<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#technical-notes-for-future-developers" class="hash-link" aria-label="Enlace directo al Technical Notes for Future Developers" title="Enlace directo al Technical Notes for Future Developers">​</a></h2>
<p>All extraction follows the established i18n key namespace conventions in the codebase. When working in these modules going forward, <strong>do not add raw string literals to UI components</strong> — all new user-facing text should go through the translation hook (<code>useTranslation</code> or equivalent) from the outset. The extraction work done today establishes the baseline; new additions that bypass the pattern will create drift.</p>
<p>Pay particular attention to <strong>dynamic strings</strong> — messages that interpolate patient counts, dates, or status values. These require parameterized translation keys and should be reviewed carefully to ensure interpolation variables are correctly scoped in the locale files.</p>
<p>A follow-up pass to add <strong>default locale (en-US) values</strong> for all newly extracted keys will be needed before any translation vendor handoff can begin. That's the logical next step in this pipeline.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-22#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>Populate default locale files</strong> — the extraction creates the key structure; the en-US baseline values need to be confirmed and completed for all newly extracted namespaces.</li>
<li><strong>Translation vendor / community handoff</strong> — once baseline locale files are clean, selected locale targets (likely starting with Spanish and French) can be queued for translation.</li>
<li><strong>i18n QA pass</strong> — automated and manual review to catch any missing keys, truncation issues, or RTL layout concerns in the affected surfaces.</li>
<li><strong>Resume feature development</strong> — with the i18n scaffold now broadly in place, feature work can proceed with localization built in from the start rather than bolted on after the fact.</li>
</ul>
<p>Today's work won't show up as a flashy demo, but twelve commits of steady extraction across the full platform is the kind of unglamorous infrastructure investment that pays dividends every time Parthenon reaches a new institution or market. Solid day.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[Laying the Groundwork for Global Reach: Internationalization Across Parthenon's Module Suite]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23</guid>
            <pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Today was a focused, deliberate sprint on one of the more foundational — and often underappreciated — aspects of building a platform meant to serve researchers worldwide: internationalization (i18n). Across 17 commits, we drafted locale files for ten distinct Parthenon modules, setting the stage for multilingual support throughout the platform.]]></description>
            <content:encoded><![CDATA[<p>Today was a focused, deliberate sprint on one of the more foundational — and often underappreciated — aspects of building a platform meant to serve researchers worldwide: internationalization (i18n). Across 17 commits, we drafted locale files for ten distinct Parthenon modules, setting the stage for multilingual support throughout the platform.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="internationalization-push-ten-modules-one-day">Internationalization Push: Ten Modules, One Day<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#internationalization-push-ten-modules-one-day" class="hash-link" aria-label="Enlace directo al Internationalization Push: Ten Modules, One Day" title="Enlace directo al Internationalization Push: Ten Modules, One Day">​</a></h2>
<p>If you've been following the Parthenon roadmap, you know that i18n has been sitting in the backlog long enough. Today we made a serious dent in it. The work centered on drafting locale string files for a broad swath of the platform's module surface area — essentially creating the translation scaffolding that will allow Parthenon to be localized into additional languages as the platform matures and its user base expands internationally.</p>
<p>Here's a breakdown of which modules received locale drafts today:</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="concept-sets">Concept Sets<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#concept-sets" class="hash-link" aria-label="Enlace directo al Concept Sets" title="Enlace directo al Concept Sets">​</a></h3>
<p>The <strong>Concept Set</strong> module is foundational to almost every OHDSI workflow — it's how researchers define the clinical entities they're studying. Getting the i18n keys right here matters because labels, tooltips, and validation messages in this module surface constantly during analysis setup. Today's draft captures the core UI strings for concept browsing, editing, and inclusion/exclusion rule management.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="gis-tools">GIS Tools<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#gis-tools" class="hash-link" aria-label="Enlace directo al GIS Tools" title="Enlace directo al GIS Tools">​</a></h3>
<p>The <strong>GIS Tools</strong> module enables geographic analysis of patient populations and outcomes — a capability that becomes especially important for population health and care gap research. The locale draft here covers map layer controls, region selection UI, and spatial filter terminology.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="morpheus">Morpheus<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#morpheus" class="hash-link" aria-label="Enlace directo al Morpheus" title="Enlace directo al Morpheus">​</a></h3>
<p><strong>Morpheus</strong> handles patient-level prediction and machine learning model integration within the OHDSI ecosystem. The locale strings drafted today cover model configuration panels, performance metric labels, and prediction output displays — areas that tend to have dense, technical terminology that translators will need careful context for.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="imaging-genomics">Imaging Genomics<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#imaging-genomics" class="hash-link" aria-label="Enlace directo al Imaging Genomics" title="Enlace directo al Imaging Genomics">​</a></h3>
<p>The <strong>Imaging Genomics</strong> module sits at an exciting intersection of modalities. The i18n work here is particularly nuanced — genomic and imaging terminology doesn't always translate cleanly across languages, so the English source strings were drafted with that challenge in mind, keeping labels descriptive and avoiding overly idiomatic phrasing.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="standard-pros">Standard PROs<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#standard-pros" class="hash-link" aria-label="Enlace directo al Standard PROs" title="Enlace directo al Standard PROs">​</a></h3>
<p>Patient-Reported Outcomes (<strong>Standard PROs</strong>) have their own i18n complexity: the underlying instruments (like PHQ-9 or EQ-5D) are often already translated by their publishers, but the <em>platform UI wrapping those instruments</em> needs its own localization layer. Today's draft addresses the container UI — survey assignment, response tracking, and reporting views.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="strategus">Strategus<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#strategus" class="hash-link" aria-label="Enlace directo al Strategus" title="Enlace directo al Strategus">​</a></h3>
<p><strong>Strategus</strong> is the OHDSI orchestration framework that coordinates multi-step analysis packages across nodes. The locale draft here covers pipeline configuration, execution status, and results aggregation panels — all areas where clear, precise language is critical for researchers coordinating distributed studies.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="publish-care-gap">Publish Care Gap<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#publish-care-gap" class="hash-link" aria-label="Enlace directo al Publish Care Gap" title="Enlace directo al Publish Care Gap">​</a></h3>
<p>The <strong>Publish Care Gap</strong> module allows teams to surface care gap findings to downstream stakeholders. The locale strings drafted today focus on the publication workflow — defining gap criteria, reviewing findings, and exporting reports.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="profile-similarity">Profile Similarity<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#profile-similarity" class="hash-link" aria-label="Enlace directo al Profile Similarity" title="Enlace directo al Profile Similarity">​</a></h3>
<p><strong>Profile Similarity</strong> enables cohort comparison and patient phenotyping by similarity metrics. The i18n work here covers the comparison configuration UI and similarity score visualization components.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="analysis-wave">Analysis Wave<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#analysis-wave" class="hash-link" aria-label="Enlace directo al Analysis Wave" title="Enlace directo al Analysis Wave">​</a></h3>
<p><strong>Analysis Wave</strong> is the orchestration layer for scheduling and sequencing analytical jobs across the platform. Today's locale draft captures the wave configuration interface, dependency graph labels, and execution timeline views.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="cohort-definition">Cohort Definition<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#cohort-definition" class="hash-link" aria-label="Enlace directo al Cohort Definition" title="Enlace directo al Cohort Definition">​</a></h3>
<p>Rounding out the day, <strong>Cohort Definition</strong> locale strings were drafted — covering the inclusion/exclusion criteria builder, concept set references, and cohort metadata fields. Given how central cohort building is to every OHDSI study, getting these strings right early is a high-leverage investment.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-this-work-matters">Why This Work Matters<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#why-this-work-matters" class="hash-link" aria-label="Enlace directo al Why This Work Matters" title="Enlace directo al Why This Work Matters">​</a></h2>
<p>It's easy to deprioritize i18n until "later" — and then find yourself refactoring hundreds of hardcoded strings under deadline pressure. By drafting locale files now, while modules are still actively evolving, we're establishing a clear contract between the UI components and the translation layer. Future developers adding features to any of these modules now have a home for their strings from day one.</p>
<p>It also signals something broader about where Parthenon is headed: this is a platform being built for the global OHDSI community, which spans research institutions across dozens of countries. Multilingual support isn't a nice-to-have — it's a prerequisite for genuine global adoption.</p>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="technical-notes-for-future-contributors">Technical Notes for Future Contributors<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#technical-notes-for-future-contributors" class="hash-link" aria-label="Enlace directo al Technical Notes for Future Contributors" title="Enlace directo al Technical Notes for Future Contributors">​</a></h2>
<ul>
<li>All locale files follow the standard <code>{module}/locales/{lang}.json</code> structure. When adding new UI strings to any of these modules, make sure the corresponding key is added to the locale file — don't hardcode display strings directly in components.</li>
<li>The draft locale files currently contain English source strings only. They are intentionally structured to make handoff to professional translators (or community contributors) straightforward.</li>
<li>Pay attention to string interpolation patterns — several modules use parameterized strings (e.g., <code>"Showing {{count}} results"</code>) and translators will need context notes for those tokens.</li>
</ul>
<hr>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-23#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>With locale scaffolding now in place for ten modules, the logical next steps are:</p>
<ol>
<li><strong>Audit remaining modules</strong> that don't yet have locale drafts and bring them up to parity.</li>
<li><strong>Wire locale files into the i18n runtime</strong> — ensuring the platform's translation loader actually picks up these new files and that language switching works end-to-end.</li>
<li><strong>Community translation kickoff</strong> — once the English source strings are stable, we can open up the locale files for community contributions from the broader OHDSI network.</li>
<li><strong>Terminology review</strong> — particularly for Imaging Genomics and Morpheus, a domain expert review of the source strings before translation begins would reduce rework downstream.</li>
</ol>
<p>It was a quieter day in terms of visible feature work, but the kind of foundational investment that pays dividends across every future release. More tomorrow.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
        </item>
        <item>
            <title><![CDATA[FinnGen CI Stabilization: Hardening the Migration Stack and Test Pipeline]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20</guid>
            <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Today's work was squarely focused on one of the less glamorous but absolutely critical aspects of platform engineering: making the CI pipeline trustworthy. Following last week's FinnGen development merge, we spent the day hardening the migration stack, tightening schema isolation, and wrestling the test suite into a state where green means green and red means red.]]></description>
            <content:encoded><![CDATA[<p>Today's work was squarely focused on one of the less glamorous but absolutely critical aspects of platform engineering: making the CI pipeline trustworthy. Following last week's FinnGen development merge, we spent the day hardening the migration stack, tightening schema isolation, and wrestling the test suite into a state where green means green and red means red.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="finngen-schema-isolation-getting-the-migration-stack-right">FinnGen Schema Isolation: Getting the Migration Stack Right<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#finngen-schema-isolation-getting-the-migration-stack-right" class="hash-link" aria-label="Enlace directo al FinnGen Schema Isolation: Getting the Migration Stack Right" title="Enlace directo al FinnGen Schema Isolation: Getting the Migration Stack Right">​</a></h2>
<p>The root cause of most today's CI pain was schema boundary confusion introduced during the FinnGen merge. OMOP clinical data and vocabulary tables were sharing a migration path in ways that worked fine in production but caused deterministic failures on fresh CI database bootstraps.</p>
<p>The fix was conceptually clean: vocabulary table migrations now run on the dedicated <code>vocab</code> connection, keeping the OMOP clinical schema and vocabulary schema properly isolated throughout the migration lifecycle. This matters not just for CI hygiene — it reflects the correct architectural separation that downstream analytics tools (including OHDSI tools that assume specific schema layouts) depend on.</p>
<p>The Phase 13.1 FinnGen schema isolation migration itself also got hardened. We now correctly handle the full migrate → rollback → re-migrate cycle, which is the kind of thing that only breaks you the first time a developer needs to roll back mid-sprint and discovers the re-migration path was never tested. The <code>finngen.runs</code> schema isolation story is now complete, with legacy <code>finngen_runs</code> table migrations guarded so that a mid-suite <code>migrate</code> call can't silently recreate superseded tables alongside the current schema structure.</p>
<p>The GitHub Actions database bootstrap was also expanded to create every schema used by the migration stack upfront, rather than relying on migrations to create schemas opportunistically. This is a more robust pattern and eliminates an entire class of ordering-dependent failures.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ci-behavior-teaching-the-pipeline-the-difference-between-bad-and-intentionally-skipped">CI Behavior: Teaching the Pipeline the Difference Between "Bad" and "Intentionally Skipped"<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#ci-behavior-teaching-the-pipeline-the-difference-between-bad-and-intentionally-skipped" class="hash-link" aria-label="Enlace directo al CI Behavior: Teaching the Pipeline the Difference Between &quot;Bad&quot; and &quot;Intentionally Skipped&quot;" title="Enlace directo al CI Behavior: Teaching the Pipeline the Difference Between &quot;Bad&quot; and &quot;Intentionally Skipped&quot;">​</a></h2>
<p>A recurring frustration with the Pest-based backend test suite has been exit code ambiguity. Optional test suites — things like integration checks that require external services — can exit with non-zero status codes for reasons that are entirely intentional. Previously, the CI job couldn't distinguish between "a test actually failed" and "a test was skipped because the service isn't available in this environment."</p>
<p>We addressed this with two complementary changes:</p>
<ol>
<li>
<p><strong>Backend CI now tolerates intentional non-failure issue statuses</strong> from optional suites. Warning-level and informational issue statuses no longer cause job failure, while assertion failures and risky-test results still do.</p>
</li>
<li>
<p><strong>A Pest summary guard</strong> was added so that CI correctly fails on failed, errored, or risky tests, and on coverage below minimum threshold — but continues gracefully when the only non-zero exit condition is an optional-suite issue status.</p>
</li>
</ol>
<p>This sounds subtle but it meaningfully changes the signal-to-noise ratio of the CI pipeline. Developers can now trust that a red CI run reflects a real problem, not a flaky optional integration check.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="coverage-gating-scoping-the-finngen-gate-correctly">Coverage Gating: Scoping the FinnGen Gate Correctly<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#coverage-gating-scoping-the-finngen-gate-correctly" class="hash-link" aria-label="Enlace directo al Coverage Gating: Scoping the FinnGen Gate Correctly" title="Enlace directo al Coverage Gating: Scoping the FinnGen Gate Correctly">​</a></h2>
<p>The FinnGen coverage gate was previously measuring coverage across the full Laravel application tree, which made it effectively useless as a meaningful signal for FinnGen-specific development. It's been scoped to <code>app/Services/FinnGen</code> to match the intent of the CI job — enforcing coverage on the service package being developed, not the entire platform.</p>
<p>Similarly, the Ares coverage matrix test setup was fixed to properly create source daimon metadata before running, and to always perform structural assertions rather than silently passing when setup conditions weren't met.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="frontend-finngen-endpoint-browser-tests-and-typescript-fixes">Frontend: FinnGen Endpoint Browser Tests and TypeScript Fixes<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#frontend-finngen-endpoint-browser-tests-and-typescript-fixes" class="hash-link" aria-label="Enlace directo al Frontend: FinnGen Endpoint Browser Tests and TypeScript Fixes" title="Enlace directo al Frontend: FinnGen Endpoint Browser Tests and TypeScript Fixes">​</a></h2>
<p>On the frontend side, FinnGen endpoint browser tests were updated to match current endpoint contracts and profile structures. A Recharts tooltip type error that was blocking <code>tsc --noEmit</code> typechecking was also resolved — these silent TypeScript errors tend to accumulate if not caught at the CI boundary, so keeping the typecheck gate clean is worth the maintenance overhead.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="tooling-orthanc-storage-hardlink-repair">Tooling: Orthanc Storage Hardlink Repair<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#tooling-orthanc-storage-hardlink-repair" class="hash-link" aria-label="Enlace directo al Tooling: Orthanc Storage Hardlink Repair" title="Enlace directo al Tooling: Orthanc Storage Hardlink Repair">​</a></h2>
<p>Outside the CI stabilization work, a new tool was added: an Orthanc storage hardlink repair utility. Orthanc (the DICOM server underpinning Parthenon's medical imaging layer) can accumulate broken hardlinks in its storage directory under certain conditions — typically after filesystem operations or storage migrations. The new tool provides a targeted repair path without requiring a full storage rebuild.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="validation-results">Validation Results<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#validation-results" class="hash-link" aria-label="Enlace directo al Validation Results" title="Enlace directo al Validation Results">​</a></h2>
<p>By end of day the migration stack and test suite were in solid shape:</p>
<ul>
<li>Fresh local test database migration: ✅ completed successfully</li>
<li>FinnGen/backend Pest subset: <strong>58 tests, 363 assertions</strong> passing</li>
<li>Focused RegionalView Vitest: <strong>6 tests</strong> passing</li>
<li>Frontend TypeScript check (<code>npx tsc --noEmit</code>): ✅ clean</li>
<li>Ares coverage service Pest test: <strong>3 tests, 38 assertions</strong> passing</li>
<li>Co2 schema provisioner Pest test: <strong>3 passed, 1 skipped</strong> (skipped correctly tolerated)</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-20#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<p>With the FinnGen CI path now stable, the immediate unblock is getting the full backend test suite green consistently across environments — not just in targeted subsets. We'll also want to review whether the Ares coverage matrix changes surface any gaps in daimon metadata handling that were previously hidden by the broken setup.</p>
<p>The Orthanc hardlink repair tool needs documentation and integration into the platform's storage health check routines. And on the FinnGen feature side, now that the migration and schema isolation story is clean, the next development phase can proceed without CI instability eating into iteration time.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>backend</category>
            <category>database</category>
            <category>testing</category>
            <category>infrastructure</category>
        </item>
        <item>
            <title><![CDATA[Orthanc Index Rebuilt Clean, Hindi Wave Ships, and i18n Surface Coverage Expands]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21</guid>
            <pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A big infrastructure day on Parthenon: the Orthanc DICOM index was completely rebuilt from scratch against locally verified DICOM bytes, resolving a long-standing class of 500 errors in OHIF/DICOMweb. In parallel, the internationalization push accelerated substantially — the Hindi locale wave completed, the next wave of locales was promoted, and string extraction landed across four major UI surfaces.]]></description>
            <content:encoded><![CDATA[<p>A big infrastructure day on Parthenon: the Orthanc DICOM index was completely rebuilt from scratch against locally verified DICOM bytes, resolving a long-standing class of <code>500</code> errors in OHIF/DICOMweb. In parallel, the internationalization push accelerated substantially — the Hindi locale wave completed, the next wave of locales was promoted, and string extraction landed across four major UI surfaces.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="orthanc-index-clean-rebuild-from-verified-dicom-bytes">Orthanc Index: Clean Rebuild from Verified DICOM Bytes<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#orthanc-index-clean-rebuild-from-verified-dicom-bytes" class="hash-link" aria-label="Enlace directo al Orthanc Index: Clean Rebuild from Verified DICOM Bytes" title="Enlace directo al Orthanc Index: Clean Rebuild from Verified DICOM Bytes">​</a></h2>
<p>The production Orthanc index had drifted into an inconsistent state: the index referenced attachment UUID paths that no longer existed in the active storage tree. This surfaced as <code>500 Internal Server Error</code> responses from DICOMweb endpoints in OHIF, with the underlying cause logged as:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">The specified path does not point to a regular file</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Hardlink repair recovered a portion of the missing attachments, but a full scan revealed that a significant number of indexed instances still had no backing DICOM file anywhere in the local storage hierarchy. With no clean surgical repair path available, the right call was a full index rebuild against the DICOM bytes that provably exist.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="the-rebuild-process">The Rebuild Process<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#the-rebuild-process" class="hash-link" aria-label="Enlace directo al The Rebuild Process" title="Enlace directo al The Rebuild Process">​</a></h3>
<p>A clean rebuild container (<code>parthenon-orthanc-clean-rebuild</code>) was spun up against a new storage root at <code>/mnt/md0/orthanc-data-clean-native-20260420-012411</code>, ingesting from both the <code>orthanc-data-pg</code> and <code>orthanc-data</code> source trees. Critically, the import used <strong>native transfer syntax preservation</strong> — Orthanc's default ingest transcoding was bypassed, avoiding the storage amplification that had been observed in prior import runs.</p>
<p>Import state was tracked in a SQLite progress database so the run could survive restarts. Final counts:</p>
<table><thead><tr><th>Outcome</th><th>Count</th></tr></thead><tbody><tr><td>Processed</td><td>1,027,171</td></tr><tr><td>Imported</td><td>546,462</td></tr><tr><td>Duplicate</td><td>480,709</td></tr><tr><td>Failed</td><td><strong>0</strong></td></tr></tbody></table>
<p>The zero-failure result is the important number here. Every DICOM file that existed on disk was either successfully indexed or correctly identified as a duplicate of an already-indexed instance.</p>
<p>The resulting clean index covers <strong>546,462 instances</strong> across <strong>2,232 studies</strong>, <strong>8,077 series</strong>, and <strong>1,762 patients</strong>, consuming 331 GB on disk.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="verification">Verification<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#verification" class="hash-link" aria-label="Enlace directo al Verification" title="Enlace directo al Verification">​</a></h3>
<p>The originally failing study (<code>1.2.392.200036.9116.2.5.1.48.1215544567.1380842106.994669</code>) was used as the primary smoke test. The clean candidate returned all 14 series with no metadata failures. A broader DICOMweb smoke check sampled six additional studies and fetched one series metadata payload from each — all six returned HTTP 200.</p>
<p>One thing worth noting for future operators: first-time uncached metadata generation for large series can take tens of seconds. Repeated calls against the Orthanc metadata cache return in under one second. This is expected behavior, not a regression.</p>
<p>The PostgreSQL index cutover (<code>ops: cut Orthanc over to PostgreSQL index</code>) finalized the transition — Orthanc is now running its index entirely on PostgreSQL rather than the embedded SQLite store, which should improve concurrent query performance and make the index easier to back up and inspect.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ohif-mpr-and-study-series-scoping-stabilization">OHIF MPR and Study Series Scoping Stabilization<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#ohif-mpr-and-study-series-scoping-stabilization" class="hash-link" aria-label="Enlace directo al OHIF MPR and Study Series Scoping Stabilization" title="Enlace directo al OHIF MPR and Study Series Scoping Stabilization">​</a></h2>
<p>Alongside the index rebuild, a targeted fix landed for OHIF's Multi-Planar Reconstruction (MPR) view and study/series scoping behavior (<code>fix(imaging): stabilize OHIF MPR and study series scoping</code>). The index inconsistency had been masking some of these issues — with clean backing data now in place, the MPR rendering path and series selection logic could be properly validated and corrected. This should resolve intermittent cases where OHIF was either failing to scope down to the correct series or rendering MPR with incomplete instance sets.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="internationalization-hindi-wave-complete-four-surfaces-extracted">Internationalization: Hindi Wave Complete, Four Surfaces Extracted<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#internationalization-hindi-wave-complete-four-surfaces-extracted" class="hash-link" aria-label="Enlace directo al Internationalization: Hindi Wave Complete, Four Surfaces Extracted" title="Enlace directo al Internationalization: Hindi Wave Complete, Four Surfaces Extracted">​</a></h2>
<p>The i18n effort has been running as a sustained background track for several weeks. Today's work moved it forward on two fronts simultaneously.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="hindi-wave-completed">Hindi Wave Completed<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#hindi-wave-completed" class="hash-link" aria-label="Enlace directo al Hindi Wave Completed" title="Enlace directo al Hindi Wave Completed">​</a></h3>
<p>The Hindi (<code>hi</code>) locale wave is now fully translated and merged. This represents a complete pass over the strings that were in scope for this wave, bringing Hindi to the same coverage level as the other wave-one locales.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="next-wave-locales-promoted">Next Wave Locales Promoted<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#next-wave-locales-promoted" class="hash-link" aria-label="Enlace directo al Next Wave Locales Promoted" title="Enlace directo al Next Wave Locales Promoted">​</a></h3>
<p>Following the Hindi completion, the next batch of target locales was promoted into active translation state (<code>feat(i18n): promote next wave locales</code>). The specific locales in the next wave are tracked in the i18n backlog; today's commit establishes their scaffolding and administration resource translations (<code>feat(i18n): translate next wave administration resources</code>).</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="string-extraction-across-core-ui-surfaces">String Extraction Across Core UI Surfaces<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#string-extraction-across-core-ui-surfaces" class="hash-link" aria-label="Enlace directo al String Extraction Across Core UI Surfaces" title="Enlace directo al String Extraction Across Core UI Surfaces">​</a></h3>
<p>Four significant extraction commits landed today, pulling hardcoded strings out of major UI surfaces and routing them through the i18n layer:</p>
<ul>
<li><strong>Analytics workflow surfaces</strong> — the dashboards and pipeline trigger UIs that researchers interact with when running studies</li>
<li><strong>Analysis design and results</strong> — the cohort definition builder output panels and results exploration views</li>
<li><strong>Cohort authoring surfaces</strong> — the primary cohort criteria editing interfaces</li>
<li><strong>Data source ingestion surfaces</strong> — the configuration and monitoring UIs for OMOP CDM data source connections</li>
</ul>
<p>The full scanner backlog was also triaged (<code>docs(i18n): triage full scanner backlog</code>), producing a prioritized list of remaining surfaces that still contain hardcoded strings. This gives the translation pipeline a clear queue to work from going into next week.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-21#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>Orthanc production cutover</strong>: The clean rebuild candidate has passed smoke checks. The next step is promoting it to production and deprecating the old index. Monitoring should be in place for the first 48 hours post-cutover given the volume of instances involved.</li>
<li><strong>OHIF regression testing</strong>: Now that both the index and MPR scoping fixes are in place, a broader OHIF regression pass across modalities (CT, MR, structured reports) is warranted before the imaging surface is considered stable.</li>
<li><strong>i18n next wave</strong>: Translation work on the newly promoted locales begins, with the triaged scanner backlog providing the surface priority order.</li>
<li><strong>DICOMweb metadata cache warm-up</strong>: Worth evaluating whether a background cache warm-up job for recently active studies makes sense, given the cold-cache latency observed on large series.</li>
</ul>]]></content:encoded>
            <category>development</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>frontend</category>
            <category>backend</category>
        </item>
        <item>
            <title><![CDATA[Phase 17 Goes Green: Cohort PRS Read API, Drawer Wiring, and Installer Bootstrapper Progress]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18</guid>
            <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A dense Saturday on the Parthenon platform — Phase 17 crossed the finish line with a full green test sweep, the cohort Polygenic Risk Score read API landed end-to-end, drawer navigation got properly wired to Phase 15 sections, and the Rust/Tauri installer bootstrapper TODO got a significant planning pass as we map out the path to a true self-contained Community edition.]]></description>
            <content:encoded><![CDATA[<p>A dense Saturday on the Parthenon platform — Phase 17 crossed the finish line with a full green test sweep, the cohort Polygenic Risk Score read API landed end-to-end, drawer navigation got properly wired to Phase 15 sections, and the Rust/Tauri installer bootstrapper TODO got a significant planning pass as we map out the path to a true self-contained Community edition.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="phase-17-closes-out-clean">Phase 17 Closes Out Clean<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#phase-17-closes-out-clean" class="hash-link" aria-label="Enlace directo al Phase 17 Closes Out Clean" title="Enlace directo al Phase 17 Closes Out Clean">​</a></h2>
<p>The headline today is <code>test(17-06)</code>: the full Phase 17 test sweep is <strong>GREEN</strong>. That means 10 Wave-0 files passing, Vitest clean, and lint clean — all in one sweep. Getting a multi-wave phase to this state requires discipline across the entire stack, and it's satisfying to see it land without stragglers. Phase 17 has been an intensive push on the PRS (Polygenic Risk Score) subsystem, and closing it out cleanly positions us well to turn attention to Phase 18 planning.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="cohort-prs-read-api--end-to-end">Cohort PRS Read API — End-to-End<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#cohort-prs-read-api--end-to-end" class="hash-link" aria-label="Enlace directo al Cohort PRS Read API — End-to-End" title="Enlace directo al Cohort PRS Read API — End-to-End">​</a></h2>
<p>The centerpiece of Phase 17's final push was the cohort PRS read API (<code>feat(17-04)</code>), which delivers two core capabilities:</p>
<ol>
<li><strong>Aggregated histogram endpoint</strong> — Given a cohort ID, the API aggregates PRS scores across cohort members and returns a binned histogram payload ready for visualization. This is the backbone of the score distribution view.</li>
<li><strong>Picker integration</strong> — The histogram feeds directly into a cohort-level PRS picker component, letting analysts select and compare score distributions without leaving the cohort context.</li>
</ol>
<p>Alongside the feature work, we added <strong>five Pest tests</strong> (<code>test(17-04)</code>) covering the full surface area of the download path:</p>
<ul>
<li>Streaming CSV download (happy path)</li>
<li>404 behavior on missing cohort or missing PRS data</li>
<li>Auth guard enforcement</li>
</ul>
<p>CSV streaming tests are always a little fiddly to write correctly in Pest because you're asserting against chunked response behavior — glad to have those locked in before merge.</p>
<p>The plan summary (<code>docs(17-04)</code>) is also committed, which documents the cohort PRS read API end-to-end design for future reference. We've been consistent about closing plans with summaries and it pays off every time someone needs to revisit the rationale.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="phase-15-drawer-wiring">Phase 15 Drawer Wiring<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#phase-15-drawer-wiring" class="hash-link" aria-label="Enlace directo al Phase 15 Drawer Wiring" title="Enlace directo al Phase 15 Drawer Wiring">​</a></h2>
<p>On the UI side, <code>feat(15-07)</code> wired the drawer body to the Phase 15 section structure on the page edit view. This is the kind of glue work that looks small in the commit log but matters enormously for usability — a drawer that opens to a blank body is worse than no drawer at all. The companion commit also <strong>reserves the Phase 16 GWAS results route</strong> with a stub page, keeping the routing layer ahead of the feature work so navigation doesn't break as we build out.</p>
<p>The drawer wiring plan summary (<code>docs(15-07)</code>) is committed alongside, consistent with our pattern of closing each plan with documentation.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ci-fixes-keeping-the-pipeline-honest">CI Fixes: Keeping the Pipeline Honest<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#ci-fixes-keeping-the-pipeline-honest" class="hash-link" aria-label="Enlace directo al CI Fixes: Keeping the Pipeline Honest" title="Enlace directo al CI Fixes: Keeping the Pipeline Honest">​</a></h2>
<p>Three CI fixes landed today, each patching a subtle but real issue:</p>
<ul>
<li>
<p><strong><code>fix(ci)</code> — vocab-grant migration guard</strong>: Migration <code>000050</code> was failing in environments where the <code>parthenon_migrator</code> role doesn't exist yet (common in fresh CI runners). The fix adds a conditional guard so the grant is skipped safely rather than erroring out. This is the kind of thing that bites you in staging but not local dev, so worth the explicit protection.</p>
</li>
<li>
<p><strong><code>fix(ci)</code> — banned connection alias in PgsScoreIngester</strong>: A stray <code>DB::connection('omop')</code> call was caught by our connection-alias lint rules. Replaced with <code>'vocab'</code> — the correct alias for vocabulary-layer queries. This matters for multi-tenancy correctness; routing a vocab query through the wrong connection can produce silent wrong-database reads in environments with multiple schemas.</p>
</li>
<li>
<p><strong><code>fix(ci)</code> — FinnGen test suite</strong>: The FinnGen ingestion test suite had accumulated three separate issues: a shared PDO instance causing cross-test state bleed, missing guards on optional fields, and a set of stale assertions that were passing for the wrong reasons. All three are resolved.</p>
</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="rust-installer-bootstrapper-planning-pass">Rust Installer Bootstrapper: Planning Pass<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#rust-installer-bootstrapper-planning-pass" class="hash-link" aria-label="Enlace directo al Rust Installer Bootstrapper: Planning Pass" title="Enlace directo al Rust Installer Bootstrapper: Planning Pass">​</a></h2>
<p>Outside of the main commit stream, today included a significant planning pass on the <strong>Rust/Tauri installer v2 bootstrapper</strong>. The core product shift here is treating the installer as a true self-contained bootstrapper for Parthenon Community — meaning a user should be able to go from zero to a running OMOP environment without cloning the repository first.</p>
<p>Several items crossed off today on the planning checklist:</p>
<ul>
<li>The <strong>versioned installer bundle manifest</strong> (with checksums, phases, and DBMS support tiers) is defined</li>
<li>The bundle build is <strong>validated in CI</strong> from that manifest</li>
<li><strong>Checksum verification</strong> happens before any phase executes — no running unverified payloads</li>
</ul>
<p>The remaining open items are the meaty ones: connecting to existing database servers via a HADES/DatabaseConnector helper container, validating existing OMOP CDM schemas, creating missing DDL, loading Athena vocabulary ZIPs, and provisioning a full local PostgreSQL stack for users who don't have a target database server at all. The vocabulary acquisition flow deserves particular care — we want to guide users through restricted vocabulary acquisition honestly rather than pretending protected content can be silently downloaded.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="whats-next">What's Next<a href="https://parthenon.acumenus.net/docs/es/blog/dev-diary-2026-04-18#whats-next" class="hash-link" aria-label="Enlace directo al What's Next" title="Enlace directo al What's Next">​</a></h2>
<ul>
<li><strong>Phase 18 kickoff</strong> — with Phase 17 green and documented, sprint planning for the next phase begins Monday</li>
<li><strong>Installer bootstrapper implementation</strong> — the planning checklist is clear; next step is the HADES/DatabaseConnector integration for the existing-database-server path</li>
<li><strong>GWAS results page</strong> — the stub route is reserved; feature work for Phase 16 GWAS results can begin filling it in</li>
<li><strong>FinnGen ingestion hardening</strong> — the test fixes exposed some fragility in the PDO sharing pattern; worth a broader audit of the ingestion test suite for similar issues</li>
</ul>
<p>Good velocity today across a mix of feature, test, and infrastructure work. The green sweep on Phase 17 is the right note to end the week on.</p>]]></content:encoded>
            <category>development</category>
            <category>ohdsi</category>
            <category>analytics</category>
            <category>frontend</category>
            <category>backend</category>
            <category>infrastructure</category>
            <category>database</category>
            <category>testing</category>
        </item>
        <item>
            <title><![CDATA[Parthenon v1.0.6 — FinnGen Workbench, SSO, and Light Mode]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode</guid>
            <pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Largest feature drop of the v1.0.x arc. Ships the FinnGen Cohort Workbench (SP1–SP4), Authentik SSO via OIDC, first-class light mode, and a Patient Similarity rework — 275 commits in 5 days.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="v106--finngen-workbench-sso-and-light-mode">v1.0.6 — FinnGen Workbench, SSO, and Light Mode<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#v106--finngen-workbench-sso-and-light-mode" class="hash-link" aria-label="Enlace directo al v1.0.6 — FinnGen Workbench, SSO, and Light Mode" title="Enlace directo al v1.0.6 — FinnGen Workbench, SSO, and Light Mode">​</a></h2>
<p>v1.0.6 is the biggest feature release in the v1.0.x arc. After two
back-to-back stabilization releases (v1.0.4 test coverage, v1.0.5 data
quality), the platform was ready for net-new modules. This release lands
<strong>four</strong> of them at once: the FinnGen Cohort Workbench, Authentik SSO,
first-class light mode, and a substantially reworked Patient Similarity
explorer — plus a doubled care-bundle library, a project-management handoff to
Acumenus Data Room, and a long list of installer and CI hardening fixes.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="finngen-cohort-workbench-sp1sp4">FinnGen Cohort Workbench (SP1–SP4)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#finngen-cohort-workbench-sp1sp4" class="hash-link" aria-label="Enlace directo al FinnGen Cohort Workbench (SP1–SP4)" title="Enlace directo al FinnGen Cohort Workbench (SP1–SP4)">​</a></h3>
<p>The headline module. The FinnGen Workbench is a full-React port of the
FinnGen Shiny CO2/CodeWAS workflow, integrated end-to-end with Darkstar
(our R/Plumber sidecar) and the OHDSI HADES stack.</p>
<p><strong>SP1 — Runtime foundation</strong></p>
<ul>
<li>New <code>parthenon_finngen_ro</code> / <code>parthenon_finngen_rw</code> Postgres roles with
least-privilege grants</li>
<li><code>app.finngen_runs</code> + <code>app.finngen_analysis_modules</code> schema with idempotent
Laravel migrations</li>
<li><code>FinnGenClient</code> HTTP wrapper, <code>FinnGenRunService</code>, <code>RunFinnGenAnalysisJob</code>
Horizon job with polling, cancellation, and <code>Idempotency-Key</code> middleware
backed by Redis SETNX</li>
<li><code>FinnGenArtifactService</code> with signed URLs, path-traversal guards, and
Nginx <code>X-Accel-Redirect</code> for streaming large result artifacts</li>
<li>RBAC: <code>finngen.view</code>, <code>finngen.run</code>, <code>finngen.cancel</code>, <code>finngen.import</code>
permissions wired into RolePermissionSeeder</li>
<li>Operational runbook + SP1 pre-merge verification report</li>
</ul>
<p><strong>SP2 — Code Explorer</strong></p>
<ul>
<li>ROMOPAPI sync reads (code counts, relationships, ancestors) with
Plumber2 query params and <code>safe_sync</code> wrapper</li>
<li>Per-source vocabulary auto-grants, pandoc render path, E2E coverage</li>
</ul>
<p><strong>SP3 — HADES analyses</strong></p>
<ul>
<li>Async workers for CodeWAS, TimeCodeWAS, Overlaps, and Demographics</li>
<li>HadesExtras configuration corrected; cohort overlap, demographics, and
cohort-counts handlers wired</li>
<li>DuckDB result reads with Shiny-parity <code>analysisSettings</code></li>
<li>Bespoke SQL workers for CodeWAS / TimeCodeWAS / Overlaps (option C2)
after upstream HadesExtras gaps surfaced</li>
<li>Artifacts race fixed; cohort staging + demographics display corrected</li>
<li>4 display.json shape tests for CO2 analysis workers</li>
</ul>
<p><strong>SP4 — Workbench UI</strong></p>
<ul>
<li>Sessions list + workbench shell with autosave and not-found recovery</li>
<li>Operation tree algebra + compiler (entry / inclusion / exit operations
with union / intersect / minus)</li>
<li>Cohort typeahead, drag-and-drop reorder, live expression preview</li>
<li>Materialize step with cohort-id handoff and overwrite flow</li>
<li>Import Cohorts step with real cohort browser</li>
<li>Atlas import via the active WebAPI registry (Phase E)</li>
<li>Run history panel inside a session</li>
<li>Matching wrapper with <code>MatchingConfigForm</code> + <code>MatchingResults</code></li>
<li>SMD diagnostics + attrition waterfall</li>
<li>Wired to the SP3 analysis gallery handoff</li>
<li>E2E Playwright spec — gallery loads, module detail, run dispatch</li>
<li>Vitest contract tests for <code>useAnalysisModules</code> and <code>ResultViewerSwitch</code></li>
</ul>
<p>The workbench is surfaced on the launcher and uses the <strong>PANCREAS</strong> source
by default (multimodal oncology + genomics corpus) rather than EUNOMIA.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="authentik-sso-via-oidc-phase-7-live">Authentik SSO via OIDC (Phase 7 live)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#authentik-sso-via-oidc-phase-7-live" class="hash-link" aria-label="Enlace directo al Authentik SSO via OIDC (Phase 7 live)" title="Enlace directo al Authentik SSO via OIDC (Phase 7 live)">​</a></h3>
<p>Production users can now sign in with Authentik. The auth subsystem is
now strictly additive — username/password remains, with SSO as a
conditional path:</p>
<ul>
<li><code>app.user_external_identities</code> table for OIDC linking</li>
<li><code>app.oidc_email_aliases</code> table + C-suite seeder for canonical email
reconciliation</li>
<li>OIDC service layer: handshake store, discovery, validator, reconciliation</li>
<li>OIDC HTTP endpoints (feature-flagged off until enabled per environment)</li>
<li>Frontend <code>/auth/callback</code> page + conditional login button</li>
<li>Phase 7 callback rewrite returning the full <code>formatUser</code> shape</li>
<li>API-driven Authentik provisioning for the <code>parthenon-oidc</code> app</li>
<li>Acropolis installer registers <code>parthenon-oidc</code> automatically</li>
</ul>
<p>A phased rollout plan is documented at <code>docs/lineage/plans/closed/2026-04-13-authentik-parthenon-sso.md</code>.
Username/password and the existing temp-password / forced-change flow are
preserved exactly.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="light-mode">Light mode<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#light-mode" class="hash-link" aria-label="Enlace directo al Light mode" title="Enlace directo al Light mode">​</a></h3>
<p>Parthenon now ships a first-class light theme — a warm parchment palette
designed to coexist with the existing dark clinical theme.</p>
<ul>
<li><code>theme-store.ts</code> (Zustand) with <code>localStorage</code> persistence</li>
<li>Flash-prevention script in <code>index.html</code> so the wrong theme never paints</li>
<li><code>ThemeToggle</code> sun/moon icon button in the header</li>
<li>Per-user theme preference stored server-side</li>
<li>New CSS token file with the warm parchment palette</li>
<li>Theme-aware Recharts palette</li>
<li><strong>28,000+ hardcoded hex values</strong> swept across the frontend and
replaced with CSS variable tokens (sweeps <code>260411-qux</code> 12,000+,
<code>260411-s3c</code> 1,150, <code>260411-sxo</code> text/bg/border/divide grayscale,
plus inline-style + Tailwind arbitrary values)</li>
<li>Light-mode compliance pass across modals, wizards, drawers, pages,
panels, and feature components</li>
</ul>
<p>Several worktree sweep regressions surfaced during this work and were
recovered: ThemeToggle, themeStore, cohort wizard modal, 40 Patient
Similarity + Commons files, and 14 files clobbered by <code>579117cdb</code>. New
git hooks (<code>scripts/githooks/pre-merge-commit</code>) now refuse stale
worktree merges to prevent recurrence.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="patient-similarity-rework">Patient Similarity rework<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#patient-similarity-rework" class="hash-link" aria-label="Enlace directo al Patient Similarity rework" title="Enlace directo al Patient Similarity rework">​</a></h3>
<ul>
<li>UMAP panel reworked with new Inspector sidebar</li>
<li>Phenotype Discovery enabled with clustering fixes and a new endpoint</li>
<li>Landscape page restored with larger hit targets, cluster summary table,
and disabled "Phenotype" button until the backing endpoint is ready</li>
<li>AI step interpretation in saved runs</li>
<li>PSM covariate name resolution fixed (direct index assignment;
condition_NNN / drug_NNN / procedure_NNN formats)</li>
<li>Continue → Landscape wired</li>
<li>Centroid build streamed to avoid PHP OOM on large cohorts</li>
<li>Compare-cohorts OOM fix + schema-ownership guard</li>
<li>Target/Comparator counts aligned under their dropdowns</li>
<li>Contextual help explaining Compare vs Expand</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="care-bundles--ecqm-library-expanded-10--45">Care Bundles — eCQM library expanded 10 → 45<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#care-bundles--ecqm-library-expanded-10--45" class="hash-link" aria-label="Enlace directo al Care Bundles — eCQM library expanded 10 → 45" title="Enlace directo al Care Bundles — eCQM library expanded 10 → 45">​</a></h3>
<p>The Standard PROs+ care bundle library now ships <strong>45 OHDSI-compliant
eCQM bundles</strong>, up from 10. Each bundle is structured with the correct
measure population (initial population, denominator, numerator,
exclusions) and references the correct OMOP concept sets.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="acumenus-data-room-project-management">Acumenus Data Room project management<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#acumenus-data-room-project-management" class="hash-link" aria-label="Enlace directo al Acumenus Data Room project management" title="Enlace directo al Acumenus Data Room project management">​</a></h3>
<p>Project-management work now points to the Acumenus Data Room application,
served from the local Apache vhost at <code>https://dataroom.acumenus.net</code>.
Parthenon keeps GitHub for engineering issue history and leaves portfolio,
client, readiness, and operational boards to the Data Room app.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="database-role-split-security-hardening">Database role split (security hardening)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#database-role-split-security-hardening" class="hash-link" aria-label="Enlace directo al Database role split (security hardening)" title="Enlace directo al Database role split (security hardening)">​</a></h3>
<p>The runtime database identity was split from the migration identity:</p>
<ul>
<li><code>parthenon_app</code> (runtime) — DML only; no DDL grants</li>
<li><code>parthenon_migrator</code> (migrations) — DDL + DML; used only by <code>php artisan migrate</code> via <code>./deploy.sh --db</code></li>
<li><code>parthenon_owner</code> (schema owner) — owns all schemas</li>
</ul>
<p><code>./deploy.sh</code> has been hardened so <code>--frontend</code> deploys do not reload
PHP-FPM, and so the migrator identity is used only when migrations are
explicitly requested.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="installer-community-edition-focus">Installer (Community edition focus)<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#installer-community-edition-focus" class="hash-link" aria-label="Enlace directo al Installer (Community edition focus)" title="Enlace directo al Installer (Community edition focus)">​</a></h3>
<ul>
<li>New <code>--community</code> flag for the fastest path to a working login</li>
<li>Web MVP installer focused on the Community edition path</li>
<li>Hecate-bootstrap module + preflight + Rust GUI updates</li>
<li>Windows compatibility guard for <code>os.getuid</code> / <code>os.getgid</code></li>
<li>Public bootstrap script served correctly</li>
<li>Revised Community install landing</li>
<li>Acropolis installer registers <code>parthenon-oidc</code> automatically</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ci--deploy--infra-fixes">CI / deploy / infra fixes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#ci--deploy--infra-fixes" class="hash-link" aria-label="Enlace directo al CI / deploy / infra fixes" title="Enlace directo al CI / deploy / infra fixes">​</a></h3>
<ul>
<li>Frontend <code>tsc -b</code> (<code>npm run build</code>) unblocked for unfinished features
via targeted <code>@ts-nocheck</code> and exclusions</li>
<li><code>@typescript-eslint/ban-ts-comment</code> disabled above each <code>@ts-nocheck</code></li>
<li>Per-request DNS resolution of the <code>php-fpm</code> upstream so Nginx boots
without it</li>
<li>Lazy-resolve optional Nginx upstreams</li>
<li>PHP entrypoint handles host GID collision on macOS</li>
<li><code>compose down</code> scope hardened (compose-down nukes both stacks)</li>
<li>Migration idempotency guards for duplicate-target migrations</li>
<li>AppServiceProvider guards <code>.resendapikey</code> lookup against a directory</li>
<li>Pre-commit hook now requires a devlog entry with migration commits</li>
<li>Pre-commit hook adds <code>vite-build</code> + stale-branch merge guards</li>
<li>DB role fallback no longer references a non-existent <code>parthenon</code> role</li>
<li>Pre-migration <code>db-backup.sh</code> call dropped from deploy (already runs via
cron)</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="dependencies">Dependencies<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#dependencies" class="hash-link" aria-label="Enlace directo al Dependencies" title="Enlace directo al Dependencies">​</a></h3>
<ul>
<li>TypeScript 5.9.3 → 6.0.2 (devDependency)</li>
<li>react-router-dom 6.30.3 → 7.14.0</li>
<li>pandas 2.* → 3.*</li>
<li>uvicorn 0.42.* → 0.44.*</li>
<li>GitHub Actions: actions/checkout 4 → 6, actions/setup-python 5 → 6,
actions/upload-artifact 4 → 7, actions/download-artifact 4 → 8,
signpath/github-action-submit-signing-request 1 → 2</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="darkstar-r-sidecar--service-rename-complete">Darkstar (R sidecar) — service rename complete<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#darkstar-r-sidecar--service-rename-complete" class="hash-link" aria-label="Enlace directo al Darkstar (R sidecar) — service rename complete" title="Enlace directo al Darkstar (R sidecar) — service rename complete">​</a></h3>
<p>The R Plumber sidecar formerly known as <code>r-runtime</code> is now consistently
named <strong><code>darkstar</code></strong> across docker-compose, Traefik, watchdog, Grafana,
deploy, and CI. The deprecated <code>finngen-runner</code> container has been
removed; FinnGen analyses run inside <code>darkstar</code> via the FinnGen route
group.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="upgrade-notes">Upgrade notes<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#upgrade-notes" class="hash-link" aria-label="Enlace directo al Upgrade notes" title="Enlace directo al Upgrade notes">​</a></h3>
<ul>
<li><code>git pull &amp;&amp; ./deploy.sh</code> is sufficient for most environments.</li>
<li><strong>If enabling SSO</strong>: configure Authentik OIDC credentials in <code>.env</code> and
flip the OIDC feature flag.</li>
<li><strong>If running FinnGen workbench</strong>: ensure the <code>darkstar</code> container is
healthy (<code>docker compose ps darkstar</code>) and that the <code>parthenon_finngen_ro</code>
/ <code>_rw</code> roles exist (created automatically by SP1 migrations).</li>
<li><strong>Database roles</strong>: <code>./deploy.sh --db</code> now uses the <code>parthenon_migrator</code>
identity. The runtime <code>parthenon_app</code> user no longer has DDL.</li>
<li><strong>Light mode</strong>: enabled per-user via the header sun/moon toggle. No
config change required; the default remains the dark clinical theme.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="by-the-numbers">By the numbers<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#by-the-numbers" class="hash-link" aria-label="Enlace directo al By the numbers" title="Enlace directo al By the numbers">​</a></h3>
<ul>
<li><strong>275 commits</strong> since v1.0.5 (60 <code>feat(finngen)</code>, 17 <code>fix(finngen)</code>,
12 <code>docs(finngen)</code>, 11 <code>test(finngen)</code>, 11 <code>feat(code-explorer)</code>,
10 <code>feat(darkstar)</code>, 9 <code>feat(sync)</code>, 6 <code>feat(auth)</code>, plus the rest)</li>
<li><strong>4 new modules</strong> landed: FinnGen Workbench, Authentik SSO, Light Mode,
Patient Similarity refresh</li>
<li><strong>35 new care bundles</strong> (10 → 45)</li>
<li><strong>28,000+ hex values</strong> tokenized for theming</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="contributors">Contributors<a href="https://parthenon.acumenus.net/docs/es/blog/v1-0-6-finngen-sso-light-mode#contributors" class="hash-link" aria-label="Enlace directo al Contributors" title="Enlace directo al Contributors">​</a></h3>
<p>Claude Code + @sudoshi</p>]]></content:encoded>
            <category>release</category>
            <category>finngen</category>
            <category>sso</category>
            <category>light-mode</category>
            <category>patient-similarity</category>
            <category>ecqm</category>
        </item>
        <item>
            <title><![CDATA[From 10 to 45: Building an OHDSI-Compliant eCQM Care Bundle Library]]></title>
            <link>https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library</link>
            <guid>https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library</guid>
            <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Parthenon's Cohort Definitions page has always had a "Create from Care Bundle" modal — a way to bootstrap a cohort definition from a pre-packaged disease framework with ICD-10 patterns, OMOP concepts, and quality measures. The idea is elegant: select "Rheumatoid Arthritis," click a button, and get a fully-formed OHDSI Circe cohort expression ready to run against any CDM source.]]></description>
            <content:encoded><![CDATA[<p>Parthenon's Cohort Definitions page has always had a "Create from Care Bundle" modal — a way to bootstrap a cohort definition from a pre-packaged disease framework with ICD-10 patterns, OMOP concepts, and quality measures. The idea is elegant: select "Rheumatoid Arthritis," click a button, and get a fully-formed OHDSI Circe cohort expression ready to run against any CDM source.</p>
<p>But when I opened the modal this weekend, I saw only <strong>ten bundles</strong>. Type 2 Diabetes, Hypertension, Heart Failure, COPD, Asthma, and a handful of others. Meanwhile, the <a href="https://github.com/sudoshi/Medgnosis" target="_blank" rel="noopener noreferrer">Medgnosis project</a> — our sister platform for population health intelligence — has a library of <strong>45 care bundles</strong> covering everything from Systemic Lupus Erythematosus to Post-Traumatic Stress Disorder, each mapped to CMS Electronic Clinical Quality Measures (eCQMs). The data was sitting there in three SQL migration files. Parthenon just didn't know about it.</p>
<p>That observation kicked off what became a seven-hour deep dive into OHDSI vocabulary semantics, Circe expression compliance, and the kind of database integrity issues that only reveal themselves when you actually try to compile a cohort definition into executable SQL. By the end, we had 45 bundles, 338 quality measures, 928 verified OMOP concept IDs — and we caught eleven bugs along the way, several of which would have silently produced wrong cohorts in production.</p>
<p>This is the story of how we got there.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="why-care-bundles-matter">Why Care Bundles Matter<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#why-care-bundles-matter" class="hash-link" aria-label="Enlace directo al Why Care Bundles Matter" title="Enlace directo al Why Care Bundles Matter">​</a></h2>
<p>Before diving in, it's worth understanding why we built this feature in the first place. OHDSI's Atlas has Concept Sets and Cohort Definitions, but each one is a blank slate. A researcher studying Rheumatoid Arthritis management has to know the ICD-10 codes for RA (M05, M06), the SNOMED concepts for the condition, the RxNorm ingredients for DMARDs (methotrexate, adalimumab, etanercept, infliximab...), the LOINC codes for DAS28 disease activity scoring, and the CPT codes for DXA bone density scans. Then they have to assemble all of that into a Circe expression with proper lookback windows, inclusion criteria, and observation periods.</p>
<p>Care bundles collapse that entire workflow into a single click. They're curated, guideline-aligned, eCQM-traceable packages that produce OHDSI-compliant cohort definitions and concept sets on demand. When a user selects "Chronic Kidney Disease," they're not starting from zero — they're starting from the nephrology community's collective judgment about what matters for CKD population health.</p>
<p>Here's what each bundle carries:</p>
<ul>
<li><strong>ICD-10 patterns</strong> (e.g., <code>N18.%</code> for CKD) for quick patient identification</li>
<li><strong>OMOP standard concept IDs</strong> (vocabulary-verified Condition-domain concepts)</li>
<li><strong>eCQM references</strong> (CMS122v12, CMS134v12, ACR Guideline, AASLD Guideline)</li>
<li><strong>Quality measures</strong> — numerator/denominator criteria with concept sets per measure</li>
<li><strong>Overlap rules</strong> — deduplication logic when measures are shared across bundles (e.g., blood pressure control applies to HTN, DM, CAD, HF, CKD, AFib, PAD)</li>
</ul>
<p>When a user creates a cohort from a bundle, the system generates a complete OHDSI Circe v1 expression — the kind Atlas would produce after an hour of clicking — with primary criteria, additional criteria, concept sets, observation windows, and collapse settings all properly structured.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-1-porting-35-missing-bundles">Step 1: Porting 35 Missing Bundles<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-1-porting-35-missing-bundles" class="hash-link" aria-label="Enlace directo al Step 1: Porting 35 Missing Bundles" title="Enlace directo al Step 1: Porting 35 Missing Bundles">​</a></h2>
<p>The Medgnosis SQL files were organized in three batches:</p>
<ul>
<li><code>007_seed_bundles_v1.sql</code> — bundles 1-15 (DM, HTN, CAD, HF, COPD, ASTH, CKD, AFIB, MDD, OSTEO, OB, CLD, RA, PAD, HYPO)</li>
<li><code>008_seed_bundles_v2.sql</code> — bundles 16-30 (ALZ, STR, PAIN, OA, GERD, BPH, MIG, EPI, HIV, HCV, SCD, SLE, GOUT, OSA, GAD)</li>
<li><code>009_seed_bundles_v3.sql</code> — bundles 31-45 (T1D, IBD, MS, PD, PSO, HBV, PAH, ANEM, LIPID, PTSD, BP, TOB, AUD, VTE, WND)</li>
</ul>
<p>Parthenon already had the first 10 (minus OSTEO — Medgnosis lists OSTEO as bundle #10 but Parthenon's seeder had OB as #10, a numbering discrepancy that became one of the first bugs we hit). That meant porting 35 bundles with about 270 measures.</p>
<p>The Medgnosis format was PostgreSQL DO-blocks using <code>phm_edw</code> schema. Parthenon's format is PHP Laravel seeders with Eloquent models. We needed to translate:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)">-- Medgnosis</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">INSERT</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">INTO</span><span class="token plain"> phm_edw</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">condition_bundle</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">bundle_code</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> condition_name</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> icd10_pattern</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> bundle_size</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> key_ecqm_refs</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> description</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">VALUES</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string" style="color:hsl(95, 38%, 62%)">'RA'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'Rheumatoid Arthritis'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'M05%,M06%'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">8</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token string" style="color:hsl(95, 38%, 62%)">'ACR Guideline, ACC/AHA'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'Disease activity monitoring...'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>into:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)">// Parthenon</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'bundle_code'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'RA'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'condition_name'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Rheumatoid Arthritis'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'icd10_patterns'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'M05%'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'M06%'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'omop_concept_ids'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token number" style="color:hsl(29, 54%, 61%)">80809</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">4035611</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'ecqm_references'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'ACR Guideline'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'ACC/AHA'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'disease_category'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Musculoskeletal'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'is_active'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">true</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'measures'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"> </span><span class="token comment" style="color:hsl(220, 10%, 40%)">/* 8 measures */</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The tricky part: Medgnosis's SQL doesn't carry <code>omop_concept_ids</code> or <code>disease_category</code> — those are Parthenon-specific. We had to look up OMOP standard concepts for each condition and categorize by organ system. For every bundle, we needed to identify the seed SNOMED concept(s) that would serve as the primary condition criterion for cohort definitions.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-2-discovering-the-invisible-bug">Step 2: Discovering the Invisible Bug<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-2-discovering-the-invisible-bug" class="hash-link" aria-label="Enlace directo al Step 2: Discovering the Invisible Bug" title="Enlace directo al Step 2: Discovering the Invisible Bug">​</a></h2>
<p>We created <code>AdditionalConditionBundleSeeder.php</code>, ran it, and confirmed 45 bundles in the database. The modal showed all 45. The feature "worked."</p>
<p>Then we started auditing.</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Bundle size mismatches: 10</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  DM: declared=8, actual=0</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  HTN: declared=6, actual=0</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  CAD: declared=7, actual=0</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  HF: declared=6, actual=0</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  ...</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><strong>The original 10 bundles had zero measures linked in the junction table.</strong></p>
<p>This was a time bomb that had been sitting in the codebase for weeks. The original <code>ConditionBundleSeeder.php</code> defined 56 quality measures across 10 bundles, but the seeder had never actually been run after its migration. The bundles existed. The measure definitions in the seeder code existed. But <code>bundle_measures</code> — the table that links them — was empty for those 10 bundles.</p>
<p>Users creating cohorts from the DM bundle would get a cohort with just the condition criterion. No HbA1c monitoring, no retinal exam, no nephropathy screening — none of the quality measures the bundle was supposed to carry. Silent failure. No error. The modal looked right, the API returned successfully, but the semantic content was missing.</p>
<p>Fix: <code>php artisan db:seed --class=ConditionBundleSeeder --force</code>. The seeder is idempotent (uses <code>firstOrCreate</code>), so running it populated the missing measures without duplicating existing bundles.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-3-the-hay-rake-incident">Step 3: The Hay Rake Incident<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-3-the-hay-rake-incident" class="hash-link" aria-label="Enlace directo al Step 3: The Hay Rake Incident" title="Enlace directo al Step 3: The Hay Rake Incident">​</a></h2>
<p>With 45 bundles and 338 measures in place, we ran the OHDSI compliance check:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">[FAIL] Bundle omop_concept_ids are valid Condition concepts: 9 issues</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  DM: invalid concept_ids = [4193704, 443238]</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  PSO: invalid concept_ids = [4216061]</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  ...</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Querying the vocabulary for each "bad" concept produced some disturbing results:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">PSO - bad concept IDs:</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  4216061: Hay rake | domain=Device | vocab=SNOMED | std=S</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Our Psoriasis bundle had "Hay rake" as one of its primary concept IDs.</p>
<p>This was my error during the porting step. I had used some plausible-looking concept IDs without verifying them against the vocabulary. A "hay rake" (the agricultural implement) is a standard SNOMED device concept, but it is decidedly not a clinical finding for psoriasis. Similarly:</p>
<ul>
<li><strong>DM</strong>: <code>4193704</code> was "Type 2 diabetes mellitus without complication" — <em>invalid</em> (deprecated)</li>
<li><strong>OB</strong>: <code>4215968</code> was "Obese" — Observation domain, not Condition</li>
<li><strong>EPI</strong>: <code>4214956</code> was "History of clinical finding in subject" — way too generic</li>
<li><strong>SCD</strong>: <code>4128331</code> was "Pregnancy" — completely unrelated</li>
<li><strong>TOB</strong>: <code>4218917</code> was "Pipe smoker" — non-standard</li>
<li><strong>WND</strong>: <code>4158817</code> was "Assessment of psychosocial issues specific to patient nutritional status" — a procedure, not a condition</li>
</ul>
<p>The right approach was to traverse the OMOP concept_relationship graph. For Sickle Cell Disease, the canonical mapping is: start with ICD-10 code <code>D57</code>, find its "Maps to" relationship in concept_relationship, and that gives you the standard SNOMED concept:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> c2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> c2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_name</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept c1</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">JOIN</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_relationship cr </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">ON</span><span class="token plain"> cr</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_id_1 </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> c1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_id</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">JOIN</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept c2 </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">ON</span><span class="token plain"> c2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> cr</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_id_2</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> c1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_code </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'D57'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> c1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">vocabulary_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'ICD10CM'</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> cr</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">relationship_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'Maps to'</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> c2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">standard_concept </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'S'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> c2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">domain_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'Condition'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)">-- Result: 22281, 'Sickle cell-hemoglobin SS disease'</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That's the concept you want — SNOMED, Condition domain, standard, invalid_reason null. With <code>includeDescendants=true</code> in the concept set, it automatically captures all SCD variants.</p>
<p>We fixed all 9 bundles and updated the <code>denominator_criteria</code> on their measures to reference the corrected condition concept IDs.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-4-using-hecate-to-populate-282-measure-concept-sets">Step 4: Using Hecate to Populate 282 Measure Concept Sets<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-4-using-hecate-to-populate-282-measure-concept-sets" class="hash-link" aria-label="Enlace directo al Step 4: Using Hecate to Populate 282 Measure Concept Sets" title="Enlace directo al Step 4: Using Hecate to Populate 282 Measure Concept Sets">​</a></h2>
<p>Once the bundles were structurally sound, we needed to populate the <code>numerator_criteria.concept_ids</code> for every quality measure. The Medgnosis SQL didn't carry these — they had the measure names ("DMARD Therapy Prescribed") and the frequency ("Annually") but no OMOP concept IDs. We needed to map each of 282 measure names to a set of appropriate standard concepts.</p>
<p>Parthenon has <a href="https://github.com/OHDSI/Hecate" target="_blank" rel="noopener noreferrer">Hecate</a>, OHDSI's semantic vocabulary search engine — a Rust service backed by Qdrant vector embeddings over <code>nomic-embed-text</code>. Hecate can take a natural-language query like "DMARD Therapy" and return ranked OMOP concepts by semantic similarity. It runs on port 8088 in our Docker stack.</p>
<p>In practice, for this task, we found direct SQL against <code>vocab.concept</code> was more precise. Hecate excels at <em>discovery</em> (finding concepts you didn't know about) while SQL filters excel at <em>enforcement</em> (guaranteeing OHDSI compliance rules). For each measure, we knew what we wanted — we just needed to query it with the right constraints:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)"># OHDSI standard: drugs use RxNorm Ingredient level</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">query </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">"""</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">    SELECT concept_id FROM (</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">        SELECT DISTINCT c.concept_id, c.concept_id AS sort_key</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">        FROM vocab.concept c</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">        WHERE c.concept_name ILIKE %s</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">          AND c.standard_concept = 'S'</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">          AND c.domain_id = 'Drug'</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">          AND c.concept_class_id = 'Ingredient'</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">          AND c.invalid_reason IS NULL</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">    ) sub ORDER BY sort_key LIMIT 3</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token triple-quoted-string string" style="color:hsl(95, 38%, 62%)">"""</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The script encoded OHDSI conventions per domain:</p>
<ul>
<li><strong>Drugs</strong>: <code>Ingredient</code> concept class (not Clinical Drug Comp, not Branded Drug) — ingredient-level concepts capture all formulations via descendant expansion</li>
<li><strong>Measurements</strong>: Prefer LOINC vocabulary</li>
<li><strong>Procedures</strong>: Prefer SNOMED, fall back to CPT4</li>
<li><strong>Observations</strong>: Allow Observation/Measurement/Condition domains</li>
</ul>
<p>We built a mapping from measure code → search terms. For RA-02 ("DMARD Therapy Prescribed"):</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token string" style="color:hsl(95, 38%, 62%)">"RA-02"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"search"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string" style="color:hsl(95, 38%, 62%)">"methotrexate"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"hydroxychloroquine"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"sulfasalazine"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">               </span><span class="token string" style="color:hsl(95, 38%, 62%)">"leflunomide"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"adalimumab"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"etanercept"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"infliximab"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">               </span><span class="token string" style="color:hsl(95, 38%, 62%)">"tofacitinib"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"domain"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"Drug"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"table"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">"drug_exposure"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string" style="color:hsl(95, 38%, 62%)">"lookback"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">365</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That produced 10 RxNorm Ingredient concepts: <code>937368 (infliximab), 964339 (sulfasalazine), 1101898 (leflunomide), 1119119 (etanercept)...</code></p>
<p>After one pass, 254 of 282 measures had concepts. The other 28 needed broader searches — concepts like "Caregiver burden assessment" or "Carotid duplex ultrasound" required looking outside the preferred vocabularies. A second pass using relationship-graph-aware queries (following <code>ICD10CM → Maps to → SNOMED</code> links) closed the gap.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-5-the-domain-coherence-problem">Step 5: The Domain Coherence Problem<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-5-the-domain-coherence-problem" class="hash-link" aria-label="Enlace directo al Step 5: The Domain Coherence Problem" title="Enlace directo al Step 5: The Domain Coherence Problem">​</a></h2>
<p>With 282 measures populated, we ran a validation check that exposed the most subtle bug in the whole sequence:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Domain/concept mismatches: 65 measures</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  ALZ-01: concepts are: [('Measurement', 4), ('Observation', 5)] [mixed_domains]</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  PAIN-07: concepts are: [('Procedure', 9)] [wrong_domain, declared observation]</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  MIG-02: concepts are: [('Drug', 5)] [wrong_domain, declared observation]</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  LIPID-02: concepts are: [('Measurement', 3)] [wrong_domain, declared observation]</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Here's why this matters. The controller that builds the Circe expression picks a criterion type from the measure's <code>domain</code> field:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token keyword" style="color:hsl(286, 60%, 67%)">return</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">match</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$domain</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'measurement'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Measurement'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'drug'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'DrugExposure'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'procedure'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'ProcedureOccurrence'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'observation'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Observation'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'condition'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'ConditionOccurrence'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">default</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant" style="color:hsl(29, 54%, 61%)">null</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>A measure declared <code>domain=observation</code> tells Circe to look in <code>omop.observation</code> for the concept IDs — but if those concept IDs are actually Measurement-domain concepts, they'll never be found there. The cohort query would silently return zero patients even when patients clearly exist in the data.</p>
<p>The real world of clinical vocabulary is messier than a clean domain split. A "PHQ-9 Depression Screen" might be coded as Observation in one EHR and Measurement in another. A "Lipoprotein(a) Testing" looks like an observation but is a lab test. "Post-Stroke Depression Screening" uses PHQ-9 concepts that the vocab classifies as Measurement, not Observation.</p>
<p>The fix was mechanical but important: for each measure, we queried the actual OMOP domain of each of its concept IDs, picked the dominant domain, filtered the concept list to only that domain, and updated both the <code>domain</code> field and the <code>table</code> field on the measure. Ninety percent of measures had a clean dominant domain; a few had genuinely mixed concepts that we split into domain-coherent subsets.</p>
<p>After normalization: 0 mismatches. Every measure's <code>concept_ids</code> now live in the OMOP domain matching its declared criterion type.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-6-four-bad-vaccine-concepts">Step 6: Four Bad Vaccine Concepts<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-6-four-bad-vaccine-concepts" class="hash-link" aria-label="Enlace directo al Step 6: Four Bad Vaccine Concepts" title="Enlace directo al Step 6: Four Bad Vaccine Concepts">​</a></h2>
<p>During the final concept validation, we found four concept IDs that didn't exist in our vocabulary at all:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">{40213152, 40213154, 40213160, 40213186}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>These were in COPD-03 (Influenza Vaccination) and COPD-04 (Pneumococcal Vaccination) — leftover CVX codes from the original seeder, from an era when Parthenon's vocabulary setup was different. They had never been standard concepts in our vocab.</p>
<p>We replaced them with proper RxNorm Ingredient concepts:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)"># Flu vaccine</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token number" style="color:hsl(29, 54%, 61%)">42903441</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> influenza A virus </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">Ingredient</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token number" style="color:hsl(29, 54%, 61%)">42903442</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> influenza B virus </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">Ingredient</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)"># Pneumococcal</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token number" style="color:hsl(29, 54%, 61%)">36879032</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> Pneumococcal Purified Capsular Polysaccharides </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">Ingredient</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token number" style="color:hsl(29, 54%, 61%)">36878946</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">:</span><span class="token plain"> Pneumococcal polysaccharide conjugate serotype 6A </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">in</span><span class="token plain"> CRM197 carrier protein </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">Ingredient</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>With <code>includeDescendants=true</code>, the <code>influenza A virus</code> ingredient concept captures all flu vaccine variants in the RxNorm graph.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-7-the-circe-startwindow-bug">Step 7: The Circe StartWindow Bug<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-7-the-circe-startwindow-bug" class="hash-link" aria-label="Enlace directo al Step 7: The Circe StartWindow Bug" title="Enlace directo al Step 7: The Circe StartWindow Bug">​</a></h2>
<p>With the data clean, we turned to the cohort expression builder. The controller method that translates a bundle into a Circe expression looked like this:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token variable" style="color:hsl(207, 82%, 66%)">$additionalCriteriaList</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Criteria'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$domainType</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CodesetId'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptSetIndex</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'StartWindow'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Start'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Days'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$lookbackDays</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Coeff'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-</span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'End'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Days'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$lookbackDays</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Coeff'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Occurrence'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Type'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Count'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That <code>StartWindow</code> has <code>Start</code> at <code>-lookbackDays</code> and <code>End</code> at <code>+lookbackDays</code>. For a 365-day measurement, this creates a window from 365 days before the index date to 365 days after — a total 730-day window that includes future events relative to the cohort entry.</p>
<p>That's not how OHDSI care gap analysis works. You look <em>backward</em> from the index date: "in the 365 days prior to this patient's RA diagnosis, did they have a DAS28 score recorded?" A forward-looking window captures events the patient hadn't had yet at cohort entry, which violates causality for most quality measures.</p>
<p>The fix:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'StartWindow'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Start'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Days'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$lookbackDays</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Coeff'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-</span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain">  </span><span class="token comment" style="color:hsl(220, 10%, 40%)">// N days before index</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'End'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Days'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Coeff'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain">                  </span><span class="token comment" style="color:hsl(220, 10%, 40%)">// AT index date</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'UseIndexEnd'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'UseEventEnd'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Occurrence'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Type'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">2</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'Count'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'IsDistinct'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'RestrictVisit'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'IgnoreObservationPeriod'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>We also added the full set of Circe fields (<code>UseIndexEnd</code>, <code>UseEventEnd</code>, <code>IsDistinct</code>, <code>RestrictVisit</code>, <code>IgnoreObservationPeriod</code>) that the OHDSI Circe schema expects. Tools like the Atlas JSON importer will complain if these aren't present.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-8-concept-metadata-resolution">Step 8: Concept Metadata Resolution<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-8-concept-metadata-resolution" class="hash-link" aria-label="Enlace directo al Step 8: Concept Metadata Resolution" title="Enlace directo al Step 8: Concept Metadata Resolution">​</a></h2>
<p>The original controller built Circe concept set items with placeholder metadata:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_NAME'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$measure</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">measure_name</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain">  </span><span class="token comment" style="color:hsl(220, 10%, 40%)">// ← same for every concept!</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'DOMAIN_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$this</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">mapDomainToOmop</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$measure</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">domain</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'VOCABULARY_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">''</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain">                      </span><span class="token comment" style="color:hsl(220, 10%, 40%)">// ← empty</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_CLASS_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">''</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain">                   </span><span class="token comment" style="color:hsl(220, 10%, 40%)">// ← empty</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'STANDARD_CONCEPT'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'S'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_CODE'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">''</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>This meant a DMARD concept set would have ten items all named "DMARD Therapy Prescribed" with no vocabulary or concept class. When imported into Atlas, those concept sets look like garbage — users can't tell methotrexate from infliximab from etanercept because they all display the same name.</p>
<p>We added a vocab lookup:</p>
<div class="language-php codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-php codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptMeta</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token class-name static-context" style="color:hsl(29, 54%, 61%)">DB</span><span class="token operator" style="color:hsl(207, 82%, 66%)">::</span><span class="token function" style="color:hsl(207, 82%, 66%)">connection</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'vocab'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">table</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'vocab.concept'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">whereIn</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptIds</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">get</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_name'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'domain_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">           </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'vocabulary_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_class_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">           </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'standard_concept'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_code'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">keyBy</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept_id'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token variable" style="color:hsl(207, 82%, 66%)">$measureItems</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token function" style="color:hsl(207, 82%, 66%)">collect</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptIds</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">map</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token keyword" style="color:hsl(286, 60%, 67%)">function</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">use</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptMeta</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$conceptMeta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">get</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token variable" style="color:hsl(207, 82%, 66%)">$id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'concept'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_NAME'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">concept_name</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'DOMAIN_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">domain_id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'VOCABULARY_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">vocabulary_id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_CLASS_ID'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">concept_class_id</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'STANDARD_CONCEPT'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">standard_concept</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'CONCEPT_CODE'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token variable" style="color:hsl(207, 82%, 66%)">$meta</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token property" style="color:hsl(355, 65%, 65%)">concept_code</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'isExcluded'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'includeDescendants'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">true</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token string single-quoted-string" style="color:hsl(95, 38%, 62%)">'includeMapped'</span><span class="token plain"> </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=&gt;</span><span class="token plain"> </span><span class="token constant boolean" style="color:hsl(29, 54%, 61%)">false</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">]</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">values</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token operator" style="color:hsl(207, 82%, 66%)">-&gt;</span><span class="token function" style="color:hsl(207, 82%, 66%)">all</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Now when a user views the generated cohort definition, they see the real vocabulary: <code>infliximab (RxNorm Ingredient, code 191831)</code>, <code>methotrexate (RxNorm Ingredient, code 6851)</code>, <code>Rheumatoid arthritis (SNOMED Disorder, code 69896004)</code>.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-9-the-cdm-source-isolation-wall">Step 9: The CDM Source Isolation Wall<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-9-the-cdm-source-isolation-wall" class="hash-link" aria-label="Enlace directo al Step 9: The CDM Source Isolation Wall" title="Enlace directo al Step 9: The CDM Source Isolation Wall">​</a></h2>
<p>We introduced <code>DB::connection('omop')</code> into the controller to look up vocab concepts. PHPStan level 8 immediately rejected it:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Direct DB::connection('omop') is banned. Use the SourceAware trait:</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">$this-&gt;cdm(), $this-&gt;results(), or $this-&gt;vocab().</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">See docs/lineage/design/specs/2026-03-26-cdm-source-isolation-design.md</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Parthenon has a strict CDM source isolation rule — controllers that query CDM data must go through a trait that routes to the correct source based on request context. The reason: Parthenon supports multiple CDM sources (Acumenus, SynPUF, IRSF, Pancreas, MIMIC-IV) and they have different schemas. Hardcoding <code>'omop'</code> in a controller means the code only works for the Acumenus source.</p>
<p>We switched to the <code>SourceAware</code> trait and <code>$this-&gt;vocab()</code>. Clean Pint, clean PHPStan, clean code.</p>
<p>But when we tested the endpoint at runtime:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token punctuation" style="color:hsl(220, 14%, 71%)">{</span><span class="token property" style="color:hsl(355, 65%, 65%)">"error"</span><span class="token operator" style="color:hsl(207, 82%, 66%)">:</span><span class="token string" style="color:hsl(95, 38%, 62%)">"Failed to create cohort from bundle"</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"> </span><span class="token property" style="color:hsl(355, 65%, 65%)">"message"</span><span class="token operator" style="color:hsl(207, 82%, 66%)">:</span><span class="token string" style="color:hsl(95, 38%, 62%)">"Source context required but not set."</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The SourceAware trait requires a <code>SourceContext</code> to be populated by middleware. The care-bundles endpoint doesn't have that middleware because it doesn't query CDM data — it only queries the shared vocabulary schema, which is the same for every source. The trait's abstraction was wrong for our use case.</p>
<p>The resolution: Parthenon has a dedicated <code>vocab</code> database connection in <code>config/database.php</code>, parallel to the <code>omop</code>, <code>results</code>, <code>gis</code> connections. It targets the shared <code>vocab</code> schema without requiring source context. We switched to <code>DB::connection('vocab')</code>, and the PHPStan rule accepts it because <code>vocab</code> isn't one of the source-specific connections on the banned list.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-10-the-empty-cohort-bug-that-almost-shipped">Step 10: The "Empty Cohort" Bug That Almost Shipped<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-10-the-empty-cohort-bug-that-almost-shipped" class="hash-link" aria-label="Enlace directo al Step 10: The &quot;Empty Cohort&quot; Bug That Almost Shipped" title="Enlace directo al Step 10: The &quot;Empty Cohort&quot; Bug That Almost Shipped">​</a></h2>
<p>With everything compiling and passing static analysis, we built a Sanctum token and invoked the endpoint against live data. The RA bundle created a cohort definition. Beautiful Circe expression, valid SQL, clean compilation. We ran it against the Acumenus CDM.</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Patients: 0</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>That was surprising. RA is common — we expected at least a few thousand patients in a million-patient CDM. We switched to <code>include_measures=false</code> to isolate the primary criteria:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Patients: 2,662</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>OK — the RA condition cohort has 2,662 patients, as expected. Adding the 8 quality measures dropped it to zero. Let's try diabetes, which should be massive:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">DM primary only: 74,800 patients</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">DM with measures: 0 patients</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Seventy-four thousand diabetic patients, and not one of them had all 8 quality measures (HbA1c, retinal exam, nephropathy screening, blood pressure, statin therapy, foot exam, self-management education, poor-control flag) documented in the past year.</p>
<p>That's actually... realistic. Real-world population health data <em>always</em> has care gaps. In fact, if every diabetic patient had every quality measure completed, there would be no need for care gap analysis in the first place. The Circe expression was requiring the intersection of all measures — semantically correct OHDSI Circe <code>Type=ALL</code>, but useless as a default.</p>
<p>The question was: what <em>should</em> happen when a user clicks "Create Cohort from Bundle?" Three interpretations:</p>
<ol>
<li><strong>Eligible population</strong>: all patients with the condition (primary criteria only)</li>
<li><strong>Compliant population</strong>: all patients with the condition AND all quality measures completed</li>
<li><strong>Gap population</strong>: all patients with the condition AND missing quality measures</li>
</ol>
<p>The old default was interpretation #2, which produces an empty cohort for almost every real dataset. Interpretation #3 requires more complex Circe (exclusion logic), and interpretation #1 is the safe, useful starting point — researchers can layer additional filters on top.</p>
<p>We flipped the default from <code>true</code> to <code>false</code> for <code>include_measures</code>. The toggle remains in the UI for users who want compliant-population cohorts, but its label now reads "Require all quality measures completed (filters to compliant patients)" — so the implication is explicit.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="step-11-end-to-end-validation">Step 11: End-to-End Validation<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#step-11-end-to-end-validation" class="hash-link" aria-label="Enlace directo al Step 11: End-to-End Validation" title="Enlace directo al Step 11: End-to-End Validation">​</a></h2>
<p>With all fixes in place, we ran the complete test suite against live data:</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Test                                                Result</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">─────────────────────────────────────────────────────────────</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">GET /api/v1/care-bundles?per_page=200               45 bundles returned ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /cohort-definitions/from-bundle (RA, measures) Cohort 240, 9 ConceptSets ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Circe expression inspection                         Real concept names, codes ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Circe → PostgreSQL SQL compilation                  9,187 chars, valid ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Execute SQL against live omop                       0 errors ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">RA primary-only                                     2,662 patients ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">DM primary-only                                     74,800 patients ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">POST /concept-sets/from-bundle (PAD)                5 domain-grouped sets ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Edge: non-existent bundle_id                        422 validation error ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Edge: HIV bundle (10 measures, largest)             11,078 chars, executes ✓</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Pint + PHPStan level 8 + TypeScript                 All pass ✓</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>The generated SQL has the right structure:</p>
<div class="language-sql codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-sql codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token comment" style="color:hsl(220, 10%, 40%)">-- Descendant expansion for primary condition</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">codesetId_0 </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> MATERIALIZED </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">DISTINCT</span><span class="token plain"> concept_id </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> concept_id </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> concept_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">IN</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token number" style="color:hsl(29, 54%, 61%)">80809</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">4035611</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">UNION</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">ALL</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> ca</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">descendant_concept_id </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> concept_id</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> vocab</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">concept_ancestor ca</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">            </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> ca</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">ancestor_concept_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">IN</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token number" style="color:hsl(29, 54%, 61%)">80809</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">4035611</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"> included</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)">-- Primary event detection</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">primary_events </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">condition_start_date </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> start_date</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> omop</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">condition_occurrence e</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">JOIN</span><span class="token plain"> omop</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">observation_period op </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">ON</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">condition_concept_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">IN</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> concept_id </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> codesetId_0</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token comment" style="color:hsl(220, 10%, 40%)">-- Inclusion rules (one per measure, uses AND logic for Type=ALL)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">inclusion_rule_0 </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">AS</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">DISTINCT</span><span class="token plain"> qe</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">person_id</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> qualified_events qe</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">EXISTS</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> </span><span class="token number" style="color:hsl(29, 54%, 61%)">1</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> omop</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">measurement e</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">        </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">WHERE</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">person_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">=</span><span class="token plain"> qe</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">person_id</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">          </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">measurement_concept_id </span><span class="token operator" style="color:hsl(207, 82%, 66%)">IN</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token keyword" style="color:hsl(286, 60%, 67%)">SELECT</span><span class="token plain"> concept_id </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">FROM</span><span class="token plain"> codesetId_1</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">          </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">measurement_date </span><span class="token operator" style="color:hsl(207, 82%, 66%)">&gt;=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">qe</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">start_date </span><span class="token operator" style="color:hsl(207, 82%, 66%)">+</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">INTERVAL</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'-365 days'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">          </span><span class="token operator" style="color:hsl(207, 82%, 66%)">AND</span><span class="token plain"> e</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">measurement_date </span><span class="token operator" style="color:hsl(207, 82%, 66%)">&lt;=</span><span class="token plain"> </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">(</span><span class="token plain">qe</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">.</span><span class="token plain">start_date </span><span class="token operator" style="color:hsl(207, 82%, 66%)">+</span><span class="token plain"> </span><span class="token keyword" style="color:hsl(286, 60%, 67%)">INTERVAL</span><span class="token plain"> </span><span class="token string" style="color:hsl(95, 38%, 62%)">'0 days'</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">    </span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain"></span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">)</span><span class="token punctuation" style="color:hsl(220, 14%, 71%)">,</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Notice the lookback window: <code>(qe.start_date + INTERVAL '-365 days')</code> to <code>(qe.start_date + INTERVAL '0 days')</code>. That's the StartWindow fix in action — no forward-looking events included.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="final-numbers">Final Numbers<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#final-numbers" class="hash-link" aria-label="Enlace directo al Final Numbers" title="Enlace directo al Final Numbers">​</a></h2>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-background-color:hsl(220, 13%, 18%);--prism-color:hsl(220, 14%, 71%)"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="background-color:hsl(220, 13%, 18%);color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Structural:</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  45 bundles | 338 measures | 338 bundle-measure links | 28 overlap rules</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">OHDSI Compliance (all PASS):</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  928/928 concept IDs exist in vocab</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  928/928 are Standard concepts</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  0 invalid/deprecated concepts</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  0 domain/concept mismatches</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  257/257 drug concepts at RxNorm Ingredient level (100%)</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  45/45 bundles have valid Condition-domain concept IDs</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">Vocabulary distribution across concepts:</span><br></span><span class="token-line" style="color:hsl(220, 14%, 71%);text-shadow:0 1px rgba(0, 0, 0, 0.3)"><span class="token plain">  SNOMED: 340 | LOINC: 259 | RxNorm: 188 | HCPCS: 63 | ...</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="Copiar código" title="Copiar" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>Every concept ID in every care bundle, every quality measure, and every generated concept set has been verified against the live OMOP vocabulary. No hay rakes. No pregnancies in sickle cell bundles. No brand-name drugs where ingredient-level concepts belong.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="the-11-bugs-we-caught">The 11 Bugs We Caught<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#the-11-bugs-we-caught" class="hash-link" aria-label="Enlace directo al The 11 Bugs We Caught" title="Enlace directo al The 11 Bugs We Caught">​</a></h2>
<p>This is the sort of feature that could have shipped "working" and then quietly produced wrong cohorts for months. Here's the full list of what we caught during the deep dive:</p>
<ol>
<li><strong>Original 10 bundles had zero measures linked</strong> — the seeder was written but never run</li>
<li><strong>9 bundles had invalid <code>omop_concept_ids</code></strong> — including a literal hay rake in PSO</li>
<li><strong>Circe <code>StartWindow.End.Days</code> used <code>lookbackDays</code> instead of 0</strong> — generated future-looking windows</li>
<li><strong>65 measures had domain/concept mismatches</strong> — SNOMED concepts span domains, Circe picks one table based on <code>domain</code></li>
<li><strong>4 vaccine concepts didn't exist in vocab</strong> — leftover CVX codes from a previous vocab era</li>
<li><strong>Concept items had placeholder names</strong> — all concepts in a set showed the same measure name</li>
<li><strong>Empty <code>VOCABULARY_ID</code> / <code>CONCEPT_CLASS_ID</code></strong> — not compatible with Atlas imports</li>
<li><strong>Frontend <code>per_page: 50</code> would truncate at 51+ bundles</strong> — no pagination handling</li>
<li><strong>PHPStan CDM source isolation violation</strong> — needed <code>vocab</code> connection, not <code>omop</code></li>
<li><strong><code>SourceAware::vocab()</code> required middleware not present on bundle endpoint</strong> — runtime 500 error</li>
<li><strong><code>include_measures=true</code> default produced empty cohorts</strong> — Circe <code>Type=ALL</code> excludes all care-gap patients</li>
</ol>
<p>Seven of these were silent failures that wouldn't have thrown errors. They would have produced subtly wrong cohorts that looked right but returned zero patients or worse, wrong patients. The kind of bug that erodes trust in a platform.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="what-this-unlocks">What This Unlocks<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#what-this-unlocks" class="hash-link" aria-label="Enlace directo al What This Unlocks" title="Enlace directo al What This Unlocks">​</a></h2>
<p>With 45 verified care bundles, a researcher studying any of these conditions can go from zero to a complete OHDSI Circe cohort definition in one click:</p>
<ul>
<li>Type 1 Diabetes (12 measures)</li>
<li>Type 2 Diabetes, Hypertension, CAD, HF, COPD, Asthma</li>
<li>CKD, Atrial Fibrillation, PAD, Hypothyroidism</li>
<li>Rheumatoid Arthritis, SLE, Psoriasis, Osteoarthritis, Gout</li>
<li>Major Depressive Disorder, Generalized Anxiety, PTSD, Bipolar, AUD, Tobacco Use Disorder</li>
<li>Alzheimer's, Stroke Prevention, Parkinson's, MS, Epilepsy, Chronic Migraine</li>
<li>HIV, Hepatitis B, Hepatitis C, Sickle Cell Disease</li>
<li>Obesity, NAFLD/MASLD, IBD, GERD, BPH</li>
<li>Pulmonary Arterial Hypertension, VTE Management</li>
<li>Osteoporosis, Chronic Wound Management</li>
<li>Iron Deficiency Anemia, Familial Hyperlipidemia</li>
<li>OSA, Chronic Pain / Opioid Management</li>
</ul>
<p>Each bundle carries eCQM references — CMS122v12 for diabetes, CMS165v12 for blood pressure, ACR Guideline for RA, AASLD for hepatitis. When you generate a cohort, those references flow into the cohort tags, making them traceable back to their clinical guideline source.</p>
<p>The same bundles generate concept sets: select PAD, get five concept sets (Conditions, Measurements, Drugs, Observations, Procedures) with the right concept IDs grouped by domain, <code>includeDescendants=true</code>, ready to use in analyses.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="closing-thought">Closing Thought<a href="https://parthenon.acumenus.net/docs/es/blog/care-bundles-ecqm-library#closing-thought" class="hash-link" aria-label="Enlace directo al Closing Thought" title="Enlace directo al Closing Thought">​</a></h2>
<p>The original observation — "we only have 10 bundles when we should have 45" — took 30 seconds to make. The fix took seven hours, because <em>correctly</em> adding 35 bundles meant confronting the full stack: SQL schema integrity, OMOP vocabulary compliance, OHDSI Circe expression semantics, Laravel query builders, PostgreSQL CTE evaluation, Docker networking for Hecate, and the frontend state management that drives the modal.</p>
<p>The interesting lesson isn't about bundles specifically — it's about what "works" means for a healthcare analytics platform. A modal that shows 45 items "works." An API that returns 200 OK "works." A Circe expression that saves to the database "works." But none of those are sufficient until you compile the expression to SQL, execute it against real data, and get back a patient count that matches clinical reality.</p>
<p>OHDSI compliance isn't a checkbox. It's a discipline: every concept ID must be standard, every domain must match, every vocabulary must be authoritative, every lookback must be causal, every drug must be at ingredient level, every procedure must be in the right vocabulary. Each of those rules exists because someone, somewhere, shipped a cohort query that looked right and returned wrong patients.</p>
<p>We're building Parthenon for researchers who need to trust the platform's output. That trust is earned one rigorously-verified concept ID at a time.</p>
<hr>
<p><strong>Files changed</strong>:</p>
<ul>
<li><code>backend/database/seeders/AdditionalConditionBundleSeeder.php</code> (35 new bundles)</li>
<li><code>backend/app/Http/Controllers/Api/V1/CohortDefinitionController.php</code> (vocab lookup, StartWindow fix, include_measures default)</li>
<li><code>frontend/src/features/cohort-definitions/components/CreateFromBundleModal.tsx</code> (default toggle, clearer label)</li>
<li><code>frontend/src/features/concept-sets/components/CreateFromBundleModal.tsx</code> (pagination cap)</li>
<li><code>scripts/populate-measure-concepts.py</code> (OHDSI-compliant concept mapping script)</li>
</ul>
<p><strong>Verification</strong>: 928/928 standard concepts, 100% Ingredient-level drugs, 0 domain mismatches, Circe compiles to valid PostgreSQL, executes against live OMOP data with correct patient counts.</p>]]></content:encoded>
            <category>care-bundles</category>
            <category>ecqm</category>
            <category>ohdsi</category>
            <category>omop</category>
            <category>circe</category>
            <category>cohort-definitions</category>
            <category>concept-sets</category>
            <category>quality-measures</category>
            <category>vocabulary</category>
            <category>rxnorm</category>
            <category>loinc</category>
            <category>snomed</category>
            <category>ai</category>
            <category>debugging</category>
        </item>
    </channel>
</rss>