Parthenon v1.0.8: The Research Surface Grows Up
Parthenon v1.0.8 is the release where the research surface grows up.
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.
More than 200 commits landed in 18 days (May 10 to May 28). This post is the engineering story behind them.
The theme: from "a pile of artifacts" to "a managed library"
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.
That is not a UI problem. It is a missing concept. Artifacts had no state.
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 output 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.
Three feature lines, one coherent direction: make the research surface something a serious team can keep clean and keep moving.
Feature line 1 — Library Lifecycle
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.
The state machine
The model is deliberately small. An artifact is in one of three working states:
draft ──promote──▶ active ──archive──▶ archived
▲ ▲ │
└────────────────────┴──────restore─────────┘
- draft — a private working copy. Visible to its owner; hidden from everyone else by a default query scope.
- active — broadly visible and reusable across studies. This is the "canonical, build on me" state.
- archived — retired but recoverable. Hidden from the active library, still fully accessible to admins, and one click away from being restored.
The transitions are enforced in one place. A reusable Eloquent trait,
HasLibraryLifecycle, gives all nine models the same promote, archive, and
restore_lifecycle methods, the status cast, the archived_at /
archived_by / promoted_at columns, and the active / draft / archived
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.
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.
Authorization without ceremony
Lifecycle authorization lives in a single trait, AuthorizesLibraryLifecycle,
shared by the concept-set, cohort-definition, and analysis policies:
public function archive(User $user, Model $item): bool
{
return $this->isOwner($user, $item) || $user->hasRole('super-admin');
}
Owners manage their own artifacts; super-admins manage everyone's. That single
|| hasRole('super-admin') clause is what makes the admin console possible
without a second, parallel set of endpoints — more on that below.
The API contract
The lifecycle is exposed as a small, uniform set of endpoints, generated per entity behind the right permission gate:
POST /api/v1/{entity}/{id}/promote
POST /api/v1/{entity}/{id}/archive
POST /api/v1/{entity}/{id}/restore
POST /api/v1/{entity}/bulk-archive { ids: [...] }
POST /api/v1/{entity}/bulk-restore { ids: [...] }
The bulk endpoints return a precise { done, skipped, missing } triple, so the
UI can tell you "archived 9, skipped 2 (no permission), 1 not found" instead of
a vague success or failure.
One contract detail is worth calling out: attaching a draft artifact to a
study raises a RequiresPromotionException, which surfaces as an HTTP 409
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.
The user-facing surface
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:
- a color-coded status badge (green active, grey draft, amber archived),
- a per-row action menu that offers only the transitions valid for the current state,
- status tabs with live counts (Active / Drafts / Archived / All),
- a bulk toolbar that slides in when rows are selected, and
- a single confirm modal that handles both single-item and batch copy and explains the consequence of each action before you commit it.
Super-admins get one extra control on these pages: an All users toggle that
flips the list endpoint into scope=all, so an administrator can see and manage
artifacts across the entire user base, not just their own.
The admin console
The highest-leverage piece is the unified admin surface at /admin/library. 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.
The console does the governance work that does not belong on a researcher's page:
- Hard delete 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.
- Owner reassignment 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
.viewpermission. - A nightly 30-day purge of soft-deleted items, and a one-time lifecycle notice so existing users learn the new model exists.
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
confirm() dialogs.
The implementation detail that made this clean is worth a sentence, because it
is a pattern we will reuse. The admin console does not 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
item_type to the kebab-case route slug:
Record<AdminLibraryItemType, LibraryEntity>
Because it is a Record 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.
Cleanup suggestions and the eleventh entity
Two smaller pieces complete the line. A nightly job surfaces unused artifacts as
cleanup suggestions — a cache table, an API endpoint, and a banner that
nudges you toward the stale drafts worth archiving. And characterizations
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 null to characterizations.
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.
This line alone was 33 feat(library) commits.
Feature line 2 — The Publish module
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.
- Server-side drafts. The publish page loads and saves through the API
rather than browser state, with a stable
documentHashthat makes autosave deduplication reliable. - Debounced autosave with retry and a
beforeunloadguard, 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. - Snapshots. 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.
- Study-scoped sharing. A
PublicationDraftPolicy, per-draft visibility, a visibility badge, a share dropdown, and a read-only wizard mode for viewer collaborators viaStudy::scopeAccessibleBy. The people on a study can read the draft; the wider world cannot, until you decide otherwise. - A publication library at
/publish/libraryto find prior write-ups.
This line was 31 feat(publish) commits plus a dedicated test(publish)
series, and it shipped across two PR phases.
Feature line 3 — Agentic copilots on the Claude Agent SDK
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.
- Study Designer — a read-only assistant that helps design a study from inside the Studies workspace.
- Publication assistant — 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.
- A generalized agent core so both copilots share one engine across multiple profiles, rather than each reimplementing orchestration.
- A runtime AI Agents toggle — a single admin switch that gates both copilots, replacing the earlier per-feature flag.
The design principle here is the same one that runs through the whole release: capability behind an explicit, auditable control. The copilots are off by default. An administrator turns them on. There is no quiet AI in the loop that an operator did not choose.
Studies v2
Underneath the new surfaces, the Studies engine took its v2 step:
- Compiler Workbench v2 was promoted to the default, with v1 fidelity restored so nothing regressed in the switch.
- A create-wizard shell with an eight-step stepper and a version popover wired into the wizard footer.
- A large post-flip audit closeout and 204 new i18n keys, because a research platform that only speaks English is not a serious international tool.
Study::scopeAccessibleByfor collaborator lookups — the same scope the Publish sharing model leans on.
11 feat(studies) commits, plus the workbench promotion.
The proof: Hypertension v3, end to end on a 1M-patient CDM
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 one million patients — as a working test of the new surfaces.
- the v3 cohort was redesigned and the manuscript updated,
- 12 OHDSI negative controls were added with empirical-null calibration, the standard OHDSI method for quantifying residual systematic error, and
- the whole thing ran end-to-end on the 1M-patient CDM.
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.
By the numbers
Window v1.0.7 → v1.0.8, May 10–28 (18 days)
Commits 200+
feat(library) 33 — lifecycle model, API, list UX, admin console, cleanup
feat(publish) 31 — drafts, autosave, snapshots, sharing, library
feat(studies) 11 — Compiler Workbench v2, wizard, audit closeout
docs 46 — devlogs, release notes, plans, this blog
agents — Study Designer, Publication assistant, shared core, toggle
i18n 204 new keys in Studies v2
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 (Study::scopeAccessibleBy underpins both
Publish sharing and Studies v2) and surfaces (the Publication agent lives inside
the Publish module).
Engineering discipline
The release was held to the same gates that guard every change. The pre-commit
hook runs Pint, PHPStan at level 8, TypeScript tsc --noEmit, 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.
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.
Upgrade notes
For most environments git pull && ./deploy.sh is enough. Two release-specific
steps:
- Run
./deploy.sh --dbto apply the idempotent lifecycle migrations, thenphp artisan library:backfill-lifecycleonce to set lifecycle state on pre-existing rows. - The AI Agents (Study Designer and Publication assistant) are off by default. Enable them from the admin AI Agents toggle when you are ready; the legacy per-feature flag is no longer read.
The nightly purge and cleanup-suggestion jobs schedule themselves. The full per-PR changelog is in the v1.0.8 release notes.
What v1.0.8 sets up next
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.
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.
The principle going forward
Artifacts have state.
State is enforced in one trait, authorized in one policy.
Admin reuses user endpoints; it does not fork them.
Capability lives behind explicit, auditable controls.
A real study is the validation, not a checklist.
Parthenon v1.0.8 is live. Pull it, run library:backfill-lifecycle, and go
archive the six stale cohorts you already know are cluttering the list.