Skip to content

Fixes 36189: Complete the native Ontology Studio shell and scoped queries - #36190

Merged
harshach merged 183 commits into
mainfrom
harshach/ontology-studio/08-native-shell
Oct 30, 2026
Merged

harshach merged 183 commits into
mainfrom
harshach/ontology-studio/08-native-shell

Conversation

@harshach

@harshach harshach commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

The stack targets release 2.1.0.

PR1 #35990, PR2 #35994, PR3 #35997, PR4 #35995, PR5 #36183, PR6 #36187, PR7 #36188, PR10 #35991, PR12 #35996 and PR15 #35989 are merged.

This PR targets main. It has no published child. PR9, PR11, PR13, PR14 and PR16 are not published yet.

Current assessment for b873e9a2ae8, recorded 2026-10-30.

Verdict. PASS. The review-bot round passed at 5dba793ba74, and the merge with main's AI-mode restyle passed at 96bb61c28d0. After PR4 landed, a scoped independent review found that no test checked the version chip's disabled rule, and the test-only commit f08687034d3 that adds a case for it passed a scoped review. A scoped independent review of the cascade that brought PR10's and PR12's landings passed this PR at 051024660b3.

What PR12 changed here. PR12 rewrote concept bindings as rows with a paged read, and replaced the binding contract that PR8's asset tests used. In OntologyConceptAssets.tsx, the merge keeps PR12's Card rows and their remove button and maps them over PR8's visibleAssets. View shows three assets and a link that opens Data, and Edit keeps the ten-row pager, in both modes. Both sides added OntologyConceptAssets.test.tsx, and the merge takes PR12's file. The test commit cf81d6493b7 passes presentation={{ kind: 'edit' }} to PR12's two renders, tags four PR8 mocks with kind: 'complete', and ports PR8's four asset cases onto PR12's harness. The review found that each ported case still pins a PR8 behavior, and that each outcome change comes from PR12's code. Three mutants of the merged code each failed a case.

What moved since the review. Later merges brought PR5's, PR6's and PR7's landings and main after PR15 (#35989) and the per-server credential cache (#36217). Twenty-two files merged without a conflict, the 21 locale files and the Playwright impact map, each with separate entries from both sides. This head records PR7's squash e33bd3dab80 with the ours strategy, which leaves the tree unchanged. Against the squash, PR8's patch-id is 0215f6f9583, moved from the reviewed c03841e1f3f by context lines only, and every file's changed lines equal the reviewed diff in all 64 files, so the verdict carries.

The reviews declared four minor issues that do not block.

  • No Jest case renders the pending-review banner through the page.
  • The AI-mode header lacks PR8's fit for the 924 px top bar and may overflow there. The review inferred this from the classes without rendering it.
  • No one-ontology case covers a failed refetch of the ontology list over stale data. Moving the list-error check back into the All-ontologies branch would fail no test.
  • The version chip case's rows use flag combinations that the scopes hook never returns, such as a loading list that already holds an ontology. The chip behaves the same either way, so this cannot hide a defect.

Test evidence. The cascade review's gates ran at 051024660b3, which has this PR's changed lines. Jest passes 131 of 131 in the concept assets, concept inspector, graph view and explorer page suites, and ESLint and Prettier are clean on the files cf81d6493b7 changes. The cascade lane's Jest run passed 296 of 296 in 18 suites, with the i18n check, the Playwright impact map and CI's lint sequence clean. On this head's tree, the i18n check exits 0 and TypeScript reports only main's error at IngestionWorkflowForm.tsx(413,9). The Java impact map check passed on this push.

CI. Checks at b873e9a2ae8 were still running when this block was written. At c74ed0d02b9, the only failing checks were Validate PR Metadata and maven-collate-ci, because its base was PR7's branch.

Still owed. None of these has run yet. Run each IT on -Pmysql-elasticsearch and then -Ppostgres-opensearch, and confirm a Tests run: line.

  • Playwright OntologyNativeTreeShell.spec.ts, OntologyStudioIsolatedToggle.spec.ts and OntologyNativeOperations.spec.ts against a live stack built from this head. They drive classic mode.
  • The plan's live lane 1 at 924 px with AI mode on, to check whether the AI header overflows.
  • A check that the product tour still anchors on the shell header, which carries data-tour-step="ontology-studio" from main and PR8's layout classes.
  • IndependentOntologyStatusIT, IndependentOntologyReleaseValidationIT and the review-list ITs listed on PR6, then every merge-queue IT, run from this head because it holds every merge below it.
  • The container-backed classes listed for PR5 and PR6.
  • The plan's live and performance lanes for this PR.

AI mode. main's Ontology Studio AI-mode restyle #36433 now gates new screens on AI mode. Paths are under openmetadata-ui/src/main/resources/ui/src/components/. AI mode lacks four PR8 controls, which render only in classic branches. The scope menu's Edit ontology item is classic-only in governance/ontology/OntologyStudioScopeMenu/OntologyStudioScopeMenu.tsx, so AI mode reaches Edit only from the Edit mode tab. The concept rail's lifecycle badge and owning-ontology line are classic-only in governance/ontology/OntologyConceptInspector/OntologyConceptInspector.tsx. The native concept tree in OntologyExplorer/NativeOntologyTreeView.tsx has none of main's AI restyle. The 924 px top-bar fit is classic-only in governance/ontology/OntologyStudioHeader/OntologyStudioHeader.tsx.

Not verified yet. This PR is verified only when its unit, live and performance boxes in the plan are all checked. The operator lands the stack.

Describe your changes

Fixes #36189

Studio now uses the native ontology shell, scoped tree and lifecycle inspector. Scope controls, version state, aggregate counts, import/export and graph selection follow the active ontology. Polyhierarchies retain every parent through bounded tree windows.

The Query console waits for its persisted library before creating a draft. Native shared-query display filters only exact unchanged bundled glossary templates, preserves authored and edited records, and keeps complete arrays for replacement writes. Generated query text is consumed only in its owning scope. Aggregate Query retains its owner selector.

This is PR8 of the sixteen-part Ontology Studio program for release 2.1.0. It targets PR7, #36188. PR9 adds class authoring on this branch. This remains a draft while production browser and performance gates are open.

Type of change

  • New feature

High-level design

The page retains scope and mode ownership, and forwards scoped selection and authoring requests to the native workspace. The inspector exposes definition status and authorized asset actions. The version chip and unpublished count use current native state with bounded permission work.

The tree and count-worker decision records the 2,048-placement tree window and four concurrent count workers. Existing PR6 publication recovery and PR7 task identity, fixture and badge-test repairs are preserved.

Tests

Use cases covered

Native scope and shell controls, aggregate behavior, polyhierarchy windows, inspector status and assets, graph selection, permissions and counts, Query readiness, exact canonical seed matching, full replacement writes, and scoped generated-text transfer.

Unit tests

At 2d614805dc5, the merge gate ran 267 Jest tests in 17 suites and 450 Java unit tests, with no failures. An earlier focused run passed 344 assertions across 24 suites. Seven standalone controls exercise the actual visible-text scanner with an explicit jsdom visibility shim. Those controls establish selector and text behavior, not browser layout or visibility acceptance.

Organize-imports, ESLint, Prettier, license checks, locale synchronization and app-doc generation pass. All 60 source hashes and the excluded parent paths match the reviewed integration. TypeScript 7.0.2 reports 0 app errors at 2d614805dc5.

Backend and ingestion integration tests

This PR changes frontend source and its design record. Backend contracts come from the preceding stack entries.

Playwright and UI recording

OntologyNativeTreeShell.spec.ts and the isolated-toggle regression are included. Current production build, the repaired Query browser flow, ten live lanes, the 924px layout, screenshots and video remain open. Historical checks do not establish the behavior of this newly assembled head.

Performance

The original 500-concept load comparison and 50-multiparent tree-window measurements remain open. No performance threshold was changed.

Checklist

  • Approved Query repair integrated with its complete caller and test changes.
  • Current focused tests, scanner controls and formatting pass.
  • Parent publication and task repairs preserved.
  • Comment audit passed.
  • Current production browser, performance and review-media gates complete.

harshach and others added 30 commits October 22, 2026 08:07
First unit of #35983. The enum carries the Any or All reviewer rule that
later commits put on the ontology, freeze into the review target, and
turn into the review task's approval threshold.
…ionship types

Decision record for the native relation picker: registry types become native
object properties matched by exact predicate IRI, inverses stay reciprocal on
write, and the create option is gated on the relationshipType permission.
Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.
The IT states the contract before the implementation: setting, creating with,
repointing, clearing, deleting and restoring an object property's inverse keeps
both sides reciprocal; a one-directional import normalizes on apply and is
reused by exact IRI; the release export carries owl:inverseOf both ways.
Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.
…sion

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.
Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.
Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.
…sion

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.
OntologyWorkingState runs registered finalizers at the outermost
mutation's exit, before validation. OntologyPropertyRepository registers
one that settles inverse links over the affected neighbourhood: the side
that states a link decides it, the latest statement wins, and a partner
no longer pointed back at is released. Partners are written through the
updater so they version, index and validate like any edit. The RDF
importer reads owl:inverseOf as symmetric, so a one-directional file
describes both sides and a re-import changes nothing.
IndependentOntologyAxiomIT drops its indirect inverse variant, which
relied on one-sided inverse chains.
An ontology carries an approvalRule, AnyReviewer by default or
AllReviewers. prepareReview freezes the rule into the task's review
target and leaves the requester's and the author's own user entries out
of the assignees. The rule reaches the workflow as the start variable
approvalMode=allAssignees, which CreateTask turns into the task's
approvalThreshold, so the shared counters stay unchanged.

Each counted vote is recorded on the task in reviewVotes with its
comment; the deciding vote is recorded in the decision transaction with
any voter whose record was lost. Reviewers and the approval rule are
locked with 409 while a release of the ontology is in review.

GET /v1/ontologyReleases/{id}/review and
GET /v1/ontologies/reviews?view=assigned|created|pending read a review
from the task's recorded votes.

Amends ADR:2026-10-14-ontology-reviews-bind-workflow-tasks-to-revisions
as v13.
Only an ontology review reconciles the deciding vote, so recording
counted votes on any other multi-approval task would always leave its
last vote out and add version bumps to glossary approvals. The waiting
branch now records a vote only for an ontology review.

OntologyReleaseReviewReaderTest reads reviews through the reader with
literal tasks and votes. OntologyReleaseRepository.page and offset are
static so the cursor math has a test with literal rows.
A restore relinks or drops its inverse through the reciprocity finalizer,
which writes the same row a second time inside one mutation. Validation
kept the first row per id, so it still saw the dangling inverse and
refused the restore. OntologyWorkingState now folds a mutation's changes
to one per definition before finalizers and validation see them.

A restored property whose partner was hard-deleted meanwhile drops the
link before its updater runs, so the restore response matches the stored
row, and IndependentOntologyInverseIT covers it on both profiles.
The native relation picker lists the relationship-type registry in two
groups, Core and Custom, with each type's inverse beside it, plus the
ontology's own object properties that no registry predicate backs.
Picking a type writes the missing property pair with the registry
predicates as IRIs, linked through inverse, then the restriction axiom.
An admin can create a custom type with its inverse from the picker; the
dialog writes both registry types and links them before instantiating.
The concept inspector labels incoming relations with the inverse. The
Playwright spec covers the groups, Part Of, a custom type, and a steward
who sees the types but no create option.
…RDF ontology

The release inherits its reviewers from the ontology, and the entity
updater drops inherited references from a reviewer list, so the task's
REVIEWS rows vanished on the workflow's first stage update.
OntologyReview.eligibleReviewers now returns plain copies.

A counted vote whose record loses every optimistic-locking retry answers
200 with a warning, because the vote already counted in Flowable and the
decision records it.

openmetadata.ttl declares om:approvalRule, which the ontology projection
writes. The ITs unwrap the SDK's wrapped 409 and fetch the task with its
reviewers.
The server fills only `id` on a relationship type's `inverse` reference,
so the catalog and the picker now resolve a type's inverse by `id`, with
`fullyQualifiedName` as the fallback, instead of by a `name` that is
never set. Custom types with an inverse show both labels again.

When both properties of a pair are already live but the one the type
names does not point at the type's partner, the plan patches that
property's `inverse` instead of leaving the pair unlinked. The spec
covers a second Part Of that reuses the pair and a Subclass Of that
writes only the hierarchy axiom.
…sets

The binding read on main now returns one page of at most 100 visible
assets, and a page can end short with a cursor when hidden rows use up
its scan budget. The deprecation impact now follows each after cursor
until it is null, then sorts and counts every visible asset.

The release resource builds its binding store with the registered asset
tables, matching the store's new constructor. Both call sites failed to
compile against main.

The decision record now describes the paged read.
The merge brings release validation's paged binding read. The deprecation impact now reads every page of a concept's bindings, and the release resource builds its binding store with the registered asset tables. This branch changes none of those files, and the merge had no conflict.
The merge brings release validation's paged binding read through review publish. This branch changes none of its files, and the merge had no conflict.
The merge brings release validation's paged binding read through review tasks. This branch changes none of its files, and the merge had no conflict.
The merge brings library install state (#35989) and the per-server credential cache (#36217).

Git merged docs/generated/api-reference.md without a conflict, but both sides had raised the endpoint count from 1988 to 1989, so the merged count was one short. The doc is regenerated, which gives 1990 endpoints and 1980 summaries, and the POST /v1/users/generateToken row keeps main's text. .github/java-tests/impact-map.json merged without a conflict and keeps both sides' entries.
The merge brings release validation's last head, which merged main after library install state (#35989) and the per-server credential cache (#36217). Twenty-four files merged without a conflict: the 21 locale files, rest/ontologyAPI.ts, the generated ontology.ts type and the Playwright impact map. In each, the two sides added separate entries, and the merge keeps both.
Release validation landed on main as the squash 374dfa7 (#36183). Its tree equals the release validation head this branch just merged, so the ours strategy records the squash and leaves the tree unchanged.
The merge brings review publish's update for main after release validation (#36183), library install state (#35989) and the per-server credential cache (#36217) landed. Twenty-three files merged without a conflict: the 21 locale files, rest/ontologyAPI.ts and the Playwright impact map. In each, the two sides added separate entries, and the merge keeps both.
The merge brings review tasks' update for main after release validation (#36183), library install state (#35989) and the per-server credential cache (#36217) landed. Twenty-two files merged without a conflict: the 21 locale files and the Playwright impact map. In each, the two sides added separate entries, and the merge keeps both.
Review publish landed on main as the squash eaa47a3 (#36187). Its tree equals the review publish head this branch already merged, so the ours strategy records the squash and leaves the tree unchanged.
The merge brings review tasks' record of the review publish squash eaa47a3 (#36187). It changes no file.
Base automatically changed from harshach/ontology-studio/07-review-tasks to main October 30, 2026 04:28
Review tasks landed on main as the squash e33bd3d (#36188). Its tree equals the review tasks head this branch already merged, so the ours strategy records the squash and leaves the tree unchanged.
@gitar-bot

gitar-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 closed / 4 findings

🟡 Medium risk · The native shell rewires ontology scope, selection, publication, and import/export flows; mistakes could direct users to the wrong ontology or release.

Completes the native Ontology Studio shell with scoped tree, lifecycle inspector, version chip and unpublished counts following the active ontology. Four issues were resolved: tree row click no longer snaps back under router transitions, the canonical-template hiding rule is now recorded in docs/decisions, the version chip aria-label no longer hides its visible text, and the Publish count shows loading instead of 'Unavailable' while the ontology list loads. No open findings remain.

✅ 4 closed
✅ Bug: Tree row click snaps selection back under router transitions

📄 openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/NativeOntologyTreeView.tsx:54-59 📄 openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/NativeOntologyTreeView.tsx:284-295 📄 openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyStudio.utils.ts:971-985
AppRoot.tsx mounts BrowserRouter with useTransitions={!isPlaywrightEnv()}, so in production the ?concept= update that onSelect → selectNativeConcept → setSearchParams makes runs as a low-priority transition. The tree's own setState in onAction is urgent and renders first. In that render state.selectedId already holds the new id but selectedNodeId still holds the old one, so the render-time check at line 57 calls revealNativeTreeSelection(index, oldId, state). That throws away the click, selects the old concept again and moves the window anchor to it. When the transition lands, the prop no longer matches, so the tree reveals the new concept by its shortest path. For a polyhierarchical concept clicked under its second parent, the first-parent placement ends up highlighted, and the window jumps every time. The e2e spec can't catch this because transitions are turned off under Playwright.

Fix: reconcile against the last prop value the tree has seen, not against state.selectedId, and keep the clicked placement when the prop catches up to a local click.

✅ Quality: Canonical-template hiding rule is not recorded in docs/decisions

📄 openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyStudio.utils.ts:87-90 📄 openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyStudioQueryConsole.tsx:930-934 📄 docs/decisions/2026-10-24-ontology-studio-bounds-tree-windows-and-count-workers.md:15-29
The console now hides a shared template in native ontology scopes when the template deep-equals (isEqual) a seed in openmetadata-service/.../sparqlQuerySettings.json. This ties the UI's display to the exact shape of a backend seed file, and the backend has to keep that shape stable. If a field is added, a default changes or a seed is re-serialized, the seeds quietly show up again. Native owners also lose shared templates that only differ from a seed by chance. The description states the rule ("Native shared-query display filters only exact unchanged bundled glossary templates, preserving authored and edited records"), but the new ADR only covers tree windows and count workers, and no other record in the tree states this rule.

Fix: add a record (or a section in the new ADR) saying that in native scopes, a shared template is hidden only when it is field-for-field identical to a bundled seed in sparqlQuerySettings.json, and that any edited or authored record stays visible.

✅ Quality: Version chip aria-label hides its visible version text

📄 openmetadata-ui/src/main/resources/ui/src/components/governance/ontology/OntologyVersionChip/OntologyVersionChip.tsx:77-85
OntologyVersionChip shows text such as v1.2 · Oct 3 · Draft · 2 unpublished but sets aria-label={t('label.release-history')}. That label replaces the accessible name, so screen readers announce only "Release history" and never the version or unpublished count. Voice-control users also can't activate the chip by saying the words they see (WCAG 2.5.3). The repo's a11y rules allow aria-label only when a control has no visible text.

Fix: drop the aria-label and use the visible text as the name. If "Release history" context is needed, attach it with aria-describedby or a tooltip.

✅ Edge Case: Publish count reads 'Unavailable' while the ontology list is loading

📄 openmetadata-ui/src/main/resources/ui/src/pages/OntologyExplorerPage/OntologyExplorerPage.tsx:758-760 📄 openmetadata-ui/src/main/resources/ui/src/pages/OntologyExplorerPage/OntologyExplorerPage.tsx:1278-1292
For a single ontology, publicationPendingCount() looks up publicationStatus in studioScopes.ontologies. While that list query is still loading, the lookup finds nothing and the Publish button shows Publish (Unavailable), or an 'Unavailable' badge in AI mode. That label is misleading: selectedOntology can already be resolved from getOntology at that point, and the aggregate branch shows label.loading in the same situation. The fix is to check studioScopes.isOntologyLoading first and show the loading label, and show 'Unavailable' only on an error or a missing unpublishedChanges.

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 1 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 1 warning(s) across 1 changed file(s).

Count Rule
1 openmetadata-imports/review-sequential-api-calls
All findings
Location Rule Message
🟡 src/components/governance/ontology/OntologyGraphView/OntologyGraphView.tsx:536:9 openmetadata-imports/review-sequential-api-calls Review these sequential API requests. If they are independent, start them together with Promise.all/Promise.allSettled; keep sequencing only when data-dependent

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

@sonarqubecloud

Copy link
Copy Markdown

@harshach
harshach merged commit d83aaf3 into main Oct 30, 2026
68 of 69 checks passed
@harshach
harshach deleted the harshach/ontology-studio/08-native-shell branch October 30, 2026 05:12
anuj-kumary added a commit that referenced this pull request Oct 30, 2026
…36190

- OntologyReleaseReviewers: allow an empty collection once text is typed.
  The search starts on focus, so its results land after a fast fill;
  React Aria only opens the list on a text change with options present,
  so the reviewer options never appeared (OntologyReview.spec.ts:348).
- OntologyStudioHeader (AI mode): move the scope menu and actions onto
  the wrapping tab row. PageHeader's actions slot never shrinks, so at
  924px beside the Governance panel the Tasks button ran ~95px past the
  viewport and the subtitle collapsed to one word per line.
- OntologyNativeTreeShell.spec.ts: drop the one-row assertion; the AI
  header puts the mode tabs on their own row by design.
- OntologyNativeOperations.spec.ts: since #36187 only a definitive
  rejection offers Retry review, on ontology-recovery-action; a 5xx only
  offers Check status. Mock a 422 and use that button.
anuj-kumary added a commit that referenced this pull request Oct 30, 2026
#35989 renders the library drawer title sentence-case; #36190's spec
still expected title case. It never ran that far before, because it
failed earlier at the 924px header check.
Vansh0310 pushed a commit to Vansh0310/OpenMetadata that referenced this pull request Oct 30, 2026
… Studio Playwright failures (open-metadata#36645)

* fix(ui): record Studio Tasks drawer counts in the Classic tour fixtures

open-metadata#36188 mounted OntologyTasksDrawer on the Ontology Explorer page; its
pending/created count queries run on mount, so the Classic product tour
sent them to the server and ProductTour.spec.ts failed on every
merge-queue run.

* fix(ui): repair the ontology Studio Playwright failures from open-metadata#36187-open-metadata#36190

- OntologyReleaseReviewers: allow an empty collection once text is typed.
  The search starts on focus, so its results land after a fast fill;
  React Aria only opens the list on a text change with options present,
  so the reviewer options never appeared (OntologyReview.spec.ts:348).
- OntologyStudioHeader (AI mode): move the scope menu and actions onto
  the wrapping tab row. PageHeader's actions slot never shrinks, so at
  924px beside the Governance panel the Tasks button ran ~95px past the
  viewport and the subtitle collapsed to one word per line.
- OntologyNativeTreeShell.spec.ts: drop the one-row assertion; the AI
  header puts the mode tabs on their own row by design.
- OntologyNativeOperations.spec.ts: since open-metadata#36187 only a definitive
  rejection offers Retry review, on ontology-recovery-action; a 5xx only
  offers Check status. Mock a 422 and use that button.

* test(playwright): expect the sentence-case Ontology library title

open-metadata#35989 renders the library drawer title sentence-case; open-metadata#36190's spec
still expected title case. It never ran that far before, because it
failed earlier at the 924px header check.

* test(playwright): load reviewer options before typing in OntologyReview

The reviewer picker searches only once focused, and React Aria opens
its list only on a text change while options are already loaded, so
filling the whole name at once left the list closed. Click the picker
and wait for its first user search before typing.

Reverts the allowsEmptyCollection change: it read the previous render's
search text, so it never opened the list for a single fill.

* test(playwright): note why OntologyReview loads reviewer options first

* test(playwright): match the disabled approval-rule select in OntologyReview

In a locked review the rule control also renders a focusable tooltip
trigger with the same name, so getByRole('button', { name: 'All' })
hit a strict-mode violation. The step never ran before the reviewer
picker step was fixed.
manerow pushed a commit that referenced this pull request Oct 30, 2026
…ries (#36190)

* WIP: add OntologyApprovalRule schema for release review rules

First unit of #35983. The enum carries the Any or All reviewer rule that
later commits put on the ontology, freeze into the review target, and
turn into the review task's approval threshold.

* WIP: record that ontology definition status is derived from releases

* Fixes #35987: Record that native relations instantiate registry relationship types

Decision record for the native relation picker: registry types become native
object properties matched by exact predicate IRI, inverses stay reciprocal on
write, and the create option is gated on the relationshipType permission.

* WIP: add abstract, source, and deprecation fields to native classes

Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.

* Update generated TypeScript types and dereferenced schemas

* Add IndependentOntologyInverseIT for reciprocal native inverses

The IT states the contract before the implementation: setting, creating with,
repointing, clearing, deleting and restoring an object property's inverse keeps
both sides reciprocal; a one-directional import normalizes on apply and is
reused by exact IRI; the release export carries owl:inverseOf both ways.

* Validate, project, and diff the class model fields

Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.

* Cover the class model fields in integration tests and record the decision

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.

* WIP: add abstract, source, and deprecation fields to native classes

Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.

* Validate, project, and diff the class model fields

Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.

* Cover the class model fields in integration tests and record the decision

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.

* Fix class model integration test assertions for wrapped SDK errors and per-operation revisions

* Fixes #35981: Derive concept lifecycle status, ontology publication status and release change summary

* WIP: record that ontology definition status is derived from releases

* Fixes #35981: Derive concept lifecycle status, ontology publication status and release change summary

* WIP: add approvalRule, reviewVotes and the release review read schemas

* Keep native inverses reciprocal on write

OntologyWorkingState runs registered finalizers at the outermost
mutation's exit, before validation. OntologyPropertyRepository registers
one that settles inverse links over the affected neighbourhood: the side
that states a link decides it, the latest statement wins, and a partner
no longer pointed back at is released. Partners are written through the
updater so they version, index and validate like any edit. The RDF
importer reads owl:inverseOf as symmetric, so a one-directional file
describes both sides and a re-import changes nothing.
IndependentOntologyAxiomIT drops its indirect inverse variant, which
relied on one-sided inverse chains.

* Decide ontology release reviews by the ontology's approval rule

An ontology carries an approvalRule, AnyReviewer by default or
AllReviewers. prepareReview freezes the rule into the task's review
target and leaves the requester's and the author's own user entries out
of the assignees. The rule reaches the workflow as the start variable
approvalMode=allAssignees, which CreateTask turns into the task's
approvalThreshold, so the shared counters stay unchanged.

Each counted vote is recorded on the task in reviewVotes with its
comment; the deciding vote is recorded in the decision transaction with
any voter whose record was lost. Reviewers and the approval rule are
locked with 409 while a release of the ontology is in review.

GET /v1/ontologyReleases/{id}/review and
GET /v1/ontologies/reviews?view=assigned|created|pending read a review
from the task's recorded votes.

Amends ADR:2026-10-14-ontology-reviews-bind-workflow-tasks-to-revisions
as v13.

* Record votes only for ontology reviews and test the review read model

Only an ontology review reconciles the deciding vote, so recording
counted votes on any other multi-approval task would always leave its
last vote out and add version bumps to glossary approvals. The waiting
branch now records a vote only for an ontology review.

OntologyReleaseReviewReaderTest reads reviews through the reader with
literal tasks and votes. OntologyReleaseRepository.page and offset are
static so the cursor math has a test with literal rows.

* Validate the net change of each ontology definition

A restore relinks or drops its inverse through the reciprocity finalizer,
which writes the same row a second time inside one mutation. Validation
kept the first row per id, so it still saw the dangling inverse and
refused the restore. OntologyWorkingState now folds a mutation's changes
to one per definition before finalizers and validation see them.

A restored property whose partner was hard-deleted meanwhile drops the
link before its updater runs, so the restore response matches the stored
row, and IndependentOntologyInverseIT covers it on both profiles.

* Offer registry relation types with their inverses in the native picker

The native relation picker lists the relationship-type registry in two
groups, Core and Custom, with each type's inverse beside it, plus the
ontology's own object properties that no registry predicate backs.
Picking a type writes the missing property pair with the registry
predicates as IRIs, linked through inverse, then the restriction axiom.
An admin can create a custom type with its inverse from the picker; the
dialog writes both registry types and links them before instantiating.
The concept inspector labels incoming relations with the inverse. The
Playwright spec covers the groups, Part Of, a custom type, and a steward
who sees the types but no create option.

* chore(playwright): auto-refresh impact-map.generated.json

* Skip the reference delete for a definition that does not exist yet

* Keep the review task's own reviewers and declare approvalRule in the RDF ontology

The release inherits its reviewers from the ontology, and the entity
updater drops inherited references from a reviewer list, so the task's
REVIEWS rows vanished on the workflow's first stage update.
OntologyReview.eligibleReviewers now returns plain copies.

A counted vote whose record loses every optimistic-locking retry answers
200 with a warning, because the vote already counted in Flowable and the
decision records it.

openmetadata.ttl declares om:approvalRule, which the ontology projection
writes. The ITs unwrap the SDK's wrapped 409 and fetch the task with its
reviewers.

* Fixes #35981: Cover property and ontology edits in the unpublished-changes count, drop restating comments

* Match registry inverses by id and link reused property pairs

The server fills only `id` on a relationship type's `inverse` reference,
so the catalog and the picker now resolve a type's inverse by `id`, with
`fullyQualifiedName` as the fallback, instead of by a `name` that is
never set. Custom types with an inverse show both labels again.

When both properties of a pair are already live but the one the type
names does not point at the type's partner, the plan patches that
property's `inverse` instead of leaving the pair unlinked. The spec
covers a second Part Of that reuses the pair and a Subclass Of that
writes only the hierarchy axiom.

* Fix review vote durability and authorized pagination

* Keep restored inverses and load every registry page

* Attribute inverse cleanup to the deleting user

* fix: persist workflow votes with terminal task decisions

* Fixes #35981: Clarify the derived publication status contract

* Fixes #35987: Map native ontology changes to browser coverage

* test: assert ontology review approval rule round trips

* Fix inverse deletion actor test email length

* test(ontology): cover property validation and grouped diff cursors

* test(ontology): reject release headers as captured definitions

* feat(ontology): validate captured releases with native SHACL

* Use the registered fixture actor for inverse rollback tests

* feat: review and publish native ontology releases in Studio

* feat: review ontology releases from the Studio Tasks drawer

* feat: complete the native Ontology Studio shell and scoped queries

* Recheck review setup under the lock, list reviews by caller, vote per stage

Starting a review now rechecks the frozen approval rule and the chosen
reviewers against the ontology under the review lock and refuses with
409 when either changed since the request was prepared, so the lock has
no window before the release is In Review.

The review list starts from the caller's own open approval tasks, which
the database narrows to the caller as reviewer, assignee or requester,
instead of scanning every release in review and loading its task.

A reviewer's vote is recorded once per workflow stage, so a task that
moves through several approval stages keeps every stage's vote.

Regenerate the API reference so it lists the two review endpoints.

* Fixes #36186: Add the Open task label and record the approve-and-publish rule

* Cover relationship type reuse after the picker's list went stale

The 409 fallback in OntologyGraphView adopted whichever registry type
held a planned name. A reused primary lost the inverse it already had,
because the inverse was resolved against only the two types in hand,
and a reused inverse had its own partner overwritten by the back-link
patch. These tests pin the behavior the picker should have and fail on
the current code.

* Reload the registry before registering a relationship type

The picker planned names against a list up to 30 s old and recovered
from a 409 by fetching the existing type by name. That treated a name
collision as the same type: a reused primary dropped the inverse the
registry already held, and a reused inverse was re-pointed at the new
primary, orphaning its partner for every glossary and ontology that
uses it.

The picker now reloads the registry after the dialog. A type registered
since the list was cached is reused as it is, with its real inverse.
Otherwise names are planned apart from everything the registry holds,
and the back link is written only to the inverse this flow just
created. A 409 after that is a race and surfaces as an error instead of
being papered over.

* Clear a lost capture receipt once its revisions have moved, and record the recovery rules

* Fixes #36189: Keep clicked tree placement under transitions, name version chip by its text, record template hiding rule

* Test ontology review votes through TaskWorkflowHandler.resolveTask

A non-final vote leaves the task open and records one vote with its comment.
A repeat voter is recorded once, and a voter keeps one vote in each approval stage.

* Cover relationship type creation for users who cannot edit registry types

A user whose policy grants Create but not EditAll on relationshipType
could register a primary type and its inverse and then fail on the
back-link patch, which the server authorizes as EditAll. The new tests
expect the dialog to disable its inverse field for that user and the
flow to write a single type, plain or symmetric, with no patch. The
first test fails on the current component.

* Offer a registry inverse only to users who can edit relationship types

The inverse's back link is a PATCH of /inverse on the inverse type,
which the server authorizes as EditAll on relationshipType, and the
creator holds no owner right on a type it just made. The create dialog
now disables the inverse field for a user without that right and shows
the standard permission message under it, so a create-only user
registers one type, plain or symmetric, and never leaves a half-linked
pair. The decision record gains a v2 amendment with the rule.

* Sort imports in the release panel test

* Keep release and member ID binds within one parameter budget in definition status queries

The status resolver and the unpublished-change counter chunked only the member IDs and bound every release ID into each chunk. A large page could pass the database bind-parameter limit and fail the list request. Both lists now share one budget and every release and member pair is still queried once.

* Test that restoring an ontology keeps inverse links between its properties

* Keep inverse links between properties that a restore brings back together

* Test that relation choices follow the rights their writes need

A user who can create axioms but not properties is offered only the
relations an axiom completes, and a reused pair is offered from the side
whose inverse the user can edit. Submitting a choice the user cannot
complete shows the permission message and writes nothing.

* Offer only the relations whose writes the user may make

The native relation picker offered every registry type to any user who
could create axioms. Picking a type can also declare the properties the
ontology lacks, which needs create rights on properties, or point a
reused property at its partner, which needs edit rights on that
property. A user without those rights picked the type, and the server
refused the first write.

The picker now offers a type only when the user holds every right its
planned writes need, and it reads edit rights per property. Submitting
a type the user cannot complete shows the permission message before any
write. Hierarchy relations and pairs that the ontology already holds
linked need only the axiom right, so they stay available.

Planning every type on each model refresh made the planner quadratic in
the number of properties, because each plan built a set of all property
names and scanned the properties by IRI. The planner now indexes the
properties by IRI once per model and builds the name set only when it
declares a property. With 100 registry types and 10,000 properties,
planning the whole catalog takes about 41 ms instead of about 1.9 s, and
the plans match the previous planner's.

The decision record gains a v3 amendment that states the rule.

* Test that restoring an ontology drops a link its partner no longer names back

A property deleted on its own keeps its inverse, while the server clears
the partner's side. The new case gives the partner another inverse,
deletes and restores the ontology, and expects the partner's current
pair back with the older claim cleared. It runs every order of the three
properties' ids, because a write applies its changes in id order.

* Drop a restored inverse that its returning partner no longer names

Restoring an ontology also brings back the properties that were deleted
on their own. Such a property keeps the inverse it held, but the server
cleared its partner's side when it was deleted, and the partner may have
taken another inverse since. The restore kept that older claim whenever
the partner came back in the same write, so the pair that survived
depended on the order of the properties' ids.

A link to a partner that the same restore brings back now survives only
when the partner's stored inverse names the property. Pairs that were
linked on both sides come back as they were, and the older claim is
cleared, whatever the id order. A property whose partner was left
without an inverse comes back unlinked. A single restore still relinks a
live partner and drops a partner that stays deleted.

* Test that restoring an ontology drops claims that compete for a live partner

A property can be restored on its own inside a deleted ontology. When
two properties that were deleted earlier still name it as their inverse,
restoring the ontology brings both claims back at once, and both pass
the check that the partner is live. The claim that reconciles last
wins, so the pair that survives depends on the order of the properties'
ids.

The new test runs every order of three properties' ids on both engines
and expects all three properties to come back unlinked. Two more tests
cover the claims an ontology restore keeps. A lone claim on a live
partner relinks it. Two claims on a vocabulary property both survive,
because the server never writes the side of a vocabulary property.

* Drop restored claims that compete for one live partner

Restoring an ontology brings back every property deleted in it,
including properties that were deleted on their own earlier, and each
keeps the inverse it held. A partner they name can already be live,
because a property can be restored on its own inside a deleted
ontology. When two of the restored properties named the same live
partner, both claims passed the check that the partner is live, and
the claim that reconciled last won. The pair that survived depended on
the order of the properties' ids.

A restore states its links all at once, so none of these claims is the
latest. When two or more properties that one restore brings back name
the same live partner, the restore now clears every one of those
claims and leaves the partner as it was. A lone claim on a live
partner still relinks it. A claim on a partner that returns in the same
restore is still kept only when that partner names the property back.
Claims on a vocabulary property that the ontology does not declare
never compete, because no write changes that property, so a round trip
keeps them.

The decision record states the rule beside its restore exception, and
its Guard names the test that covers it.

* Reword the restore rule in the record and split the restore test setup

* Test each ontology review view over stored tasks

List the assigned, pending and created views through listReviews on
MySQL and PostgreSQL, over approval tasks stored the way TaskRepository
stores them: about, the requester and the reviewers live in relationship
rows, not in the task JSON. The test database gains user_entity so the
task's people resolve.

Every view returns no reviews on the current head.

* List ontology reviews with the release each task is about

TaskRepository keeps a task's about out of the stored JSON and clears it
unless the read asks for it. The review list read tasks without about, so
it matched no release and returned no rows in any view.

* Test that the review list never reads the caller's glossary approvals

A steward who reviews glossary approvals and one release review lists
pending reviews, and a requester who created them lists created reviews.
Each gets the one release, and no statement the list runs names a
glossary approval task.

* List only the approval tasks that are about an ontology release

The review list read every open approval task the caller was on,
glossary and data product approvals included, and hydrated each one.
The task filter now also requires a task about an ontology release, and
the list reads the releases of the listed tasks in one query instead of
one query per task.

* Name the tasks in the glossary approval list test

* Test that a lost capture checks history before it clears

Pins two orders the moved-revision clear relies on: history is scanned
before a moved revision clears the receipt, and the state is read
before the history. Renames the test that guards the unchanged-revision
case to say what it covers.

* Correct when a stuck capture receipt clears in its decision record

* Test that the task button renders the Open task key exactly

* Record the native template hiding rule in its own decision record

Moves the rule out of the tree-window and count-worker record, which
gets back its original title and guard, and cites the new record where
the console decides which templates to hide.

* Test that the version chip shows the release history tooltip

* Keep the review list test from registering a glossary repository

Surefire runs every service test class in one JVM, and a repository
the test registers stays bound to its database after the class ends.
The fixed list never reads a glossary term, so the test now names the
term's type on its reference instead of registering the repository.

* Test that the review list drops a superseded review task

A release can be in review under a new task while the task it was in
review under before is still open. The list must show the release once,
under the current task. No test covered the check in reviewedRelease
that drops the older task, so every test passed without it.

* Test that the review list reads its releases in one statement

The list loads the releases of all listed tasks in one batched read.
The list test now asserts that exactly one statement reads release rows
and that it covers both releases, so a read per task fails it. Resolving
each task's about reads only the names of its release, so the assertion
does not count that lookup.

* Order the AI-mode import and space the graph helpers

* Show the review state's retry in AI mode

AI mode's write actions wait on a readable review state, so its panel now shows the loading line and the retry alert that the classic footer shows, unless its own state-read alert is already showing. New AI cases cover that retry in both views, the single alert after a failed state read, and the storage-unavailable alert.

* Test that the version chip is named by its visible version

The release-history trigger shows the active version, so a screen reader
and a speech user should reach it by that text.

* Name the version chip by its visible version

The release-history trigger replaced its accessible name with "Release
history", so screen readers never read the version it shows and a speech
user could not press it by that text. The visible version is now its
name, and the purpose moves to the button's tooltip, as the native shell's
version chip already does.

* Test that Previous returns to page 1 past the cursor history cap

The Tasks drawer keeps a bounded history of page cursors for Previous.
Paging past the cap should still let Previous reach page 1.

* Keep page 1 reachable from a trimmed Tasks drawer history

The drawer kept at most 40 Previous cursors by dropping the oldest one,
which was page 1. After 40 pages, Previous stopped at the oldest kept page
and only First returned to page 1. The trim now keeps page 1 first and the
39 newest cursors after it, so Previous from the oldest kept page goes to
page 1.

The 25-row page and the 40-cursor cap are named constants, and a new
decision record states both values and why:
ADR:2026-10-29-studio-tasks-drawer-pages-25-rows-and-keeps-page-one-in-a-40-cursor-history.

* Test the Publish count while the ontology list loads

With one ontology selected and the ontology list still loading, the
Publish count should say it is loading in both the classic and AI
headers, not that it is unavailable.

* Show loading on the Publish count while the ontology list loads

The single-ontology count reads unpublishedChanges from the ontology list.
While that list loaded, the lookup found nothing, so the Publish button
and the AI badge said Unavailable even when the selected ontology had
already resolved. The count now says Loading while the list loads, and
Unavailable only when the list failed or the loaded entry has no count,
as the All-ontologies count already does.

* Test that a retried relation type finishes the pair a failed write left

The type dialog registers the inverse, then the primary, then patches the
inverse to name the primary. These cases fail one of the later writes once
and retry the same draft through the dialog. A retry after a failed primary
write must reuse the inverse already registered, and a retry after a failed
back link must patch the inverse to name the primary. Either way the
registry ends with one reciprocal pair and no suffixed duplicate.

* Test the version chip's tooltip

The chip's purpose moved from its accessible name into its tooltip, and
no case read the tooltip, so dropping it failed nothing. The case now
focuses the chip by keyboard and reads the tooltip's text.

renderNativeStudio now gives its ontology argument to getOntology and
listOntologies as well as the scopes mock, so a case that passes an
ontology sees the same one everywhere.

* Correct the Tasks drawer paging record and bound the long paging case

The record said neither list returns a before cursor. The review queue
returns an offset before cursor; only the release events list lacks
one. The Context and Consequences now say what each list returns and
why the drawer keeps one client history for both.

The case that pages past the kept history presses Next and Previous
more than 80 times and runs close to Jest's 5 s default. It now has an
explicit 20 s timeout, so a slow worker does not fail it and the cases
after it.

* Finish a relation type pair on retry after a registry write fails

The type dialog registers the inverse, then the primary, then patches the
inverse to name the primary. When the primary write failed, a retry
registered a second inverse with a numeric suffix and left the first one
unlinked. When the back link failed, a retry found the primary and returned
it, so the pair stayed one-way.

The view now remembers the inverse it registered until the back link is
written. On a retry, the planner names the draft's inverse after that type
and reuses it, or, when the primary is already registered and names it, the
view patches the back link. No other type is patched, and the error from
the failed write is still shown.

A rollback would need the Delete right on relationship types, which the
dialog does not check, and the registry's soft delete keeps the name taken,
so the retry's create would conflict.

* Test that a retry reuses no other inverse and draws the relation once

A new case puts another author's unlinked "approves" in the registry and
creates "approved by" with the inverse "approves". The view registers
"approves2" and links only that type back, so it never claims a type it
did not register.

The case where the back link failed now also checks that the retry
writes the relation once and that only the first failure is reported.

* Test that a relabelled retry finishes the earlier pair first

After the back link of "approved by" fails, the author retries with the
label "signed off by" and the same inverse "approves". The new case
expects the retry to link "approves" back to "approvedBy" and to give
"signedOffBy" its own inverse, "approves2", so no type is the inverse of
two primaries. It fails until the view finishes the earlier pair before
it plans the retry.

* Finish the earlier pair before planning a relabelled retry

When the back link fails, the primary names the inverse but the inverse
names nothing. A retry under a new label reused that inverse for the new
primary, so the inverse ended up named by two primaries.

Before it plans a retry, the view now links the inverse it registered
back to the type that already names it, whatever the retry is called,
and then plans the draft with no reuse, so the new primary gets its own
inverse. The inverse is reused only while nothing names it, after a
failed primary write. Only the inverse this view registered is patched.

* Test that a failed repair keeps the inverse and a reverse retry links it

Two cases in OntologyGraphView.test.tsx pin behavior the view already has.
A retry whose back-link repair fails keeps the remembered inverse, so a
later retry links it back. A retry drawn from the other end, under the
inverse's own label, still links the earlier pair back.

* Check the property writes when a retry is drawn as the inverse

After the back link of "approved by" fails, the author draws the
relation from the other end and types "approves". The case already
checks that the retry links the earlier pair back. It now also checks
the relation the retry writes: the property "approvedBy" first, then
"approves" naming it as its inverse, and one axiom on "approves". That
is what a first draw of "approves" writes. It fails until the view
looks the label up in the registry as the repair left it, because the
retry still writes "approves" with no inverse.

* Look up the retry label in the repaired registry

When the back link of "approved by" fails and the author retries by
drawing "approves", the view links the pair back in the registry. It
then looked the label up in the list it loaded before that repair,
where "approves" names no inverse. So the retry wrote the property
"approves" with no inverse and never declared "approvedBy". A first
draw of "approves" writes both properties, linked.

finishUnlinkedPair now returns the registry with the patched inverse in
place of the stale entry, and the view looks the label up and plans the
draft in that list. The retry now writes "approvedBy", then "approves"
naming it, then one axiom, as a first draw does. The view still patches
only the inverse it registered, keeps it when the repair fails, and
reports the failed write once.

* Bring the merged Studio tests in line with the native shell

OntologyGraphView now requires onRequestGraphSurface, so the seven inverse-pair cases that PR4's review round added render with onRequestGraphSurface={jest.fn()} and change nothing else.

The native shell's OntologyVersionChip shows an active release as v<semanticVersion>, so the version chip case that review publish added finds the chip by the name v1.2.0.

The merge already gave this branch's renderNativeStudio review publish's ontology argument, fed to the scopes mock, getOntology and listOntologies, with nativeOntology as the default for every other caller.

* Test that the version chip is disabled while the ontology list loads

The page disables the version chip while the scopes hook reports the
ontology list as loading or failed, but no case checked it. Add one row
for each state that asserts the chip is disabled.

* Fit the concept asset tests to PR12's binding contract and the presentation prop

PR12's binding cases in OntologyConceptAssets.test.tsx render OntologyConceptAssets without presentation, which this branch's e8b13be made required. tsc rejected both renders, and all six cases threw on presentation.kind. Both renders now pass presentation={{ kind: 'edit' }}, the mode they test.

This branch's four cases in that file tested the binding contract PR12 replaced, so the merge kept PR12's file. They are ported onto PR12's axiosClient harness with the same names and intent, except where PR12 changed the outcome. Revisions now come from the re-read after each write, and the remove call takes the request object. A refused write now stops the batch with the unconfirmed status and keeps the unsent selection checked, in place of a per-asset error. The add button now shows while bindings load and stays disabled until a read succeeds, where before it was hidden.

PR12 changed getOntologyConceptBindings to return a snapshot tagged kind: 'complete'. Four of this branch's mocks in OntologyConceptInspector.test.tsx lacked the tag, and tsc rejected them. They now carry it.

* Read every page of bindings before counting a deprecated concept's assets

The binding read on main now returns one page of at most 100 visible
assets, and a page can end short with a cursor when hidden rows use up
its scan budget. The deprecation impact now follows each after cursor
until it is null, then sorts and counts every visible asset.

The release resource builds its binding store with the registered asset
tables, matching the store's new constructor. Both call sites failed to
compile against main.

The decision record now describes the paged read.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
manerow pushed a commit that referenced this pull request Oct 30, 2026
… Studio Playwright failures (#36645)

* fix(ui): record Studio Tasks drawer counts in the Classic tour fixtures

#36188 mounted OntologyTasksDrawer on the Ontology Explorer page; its
pending/created count queries run on mount, so the Classic product tour
sent them to the server and ProductTour.spec.ts failed on every
merge-queue run.

* fix(ui): repair the ontology Studio Playwright failures from #36187-#36190

- OntologyReleaseReviewers: allow an empty collection once text is typed.
  The search starts on focus, so its results land after a fast fill;
  React Aria only opens the list on a text change with options present,
  so the reviewer options never appeared (OntologyReview.spec.ts:348).
- OntologyStudioHeader (AI mode): move the scope menu and actions onto
  the wrapping tab row. PageHeader's actions slot never shrinks, so at
  924px beside the Governance panel the Tasks button ran ~95px past the
  viewport and the subtitle collapsed to one word per line.
- OntologyNativeTreeShell.spec.ts: drop the one-row assertion; the AI
  header puts the mode tabs on their own row by design.
- OntologyNativeOperations.spec.ts: since #36187 only a definitive
  rejection offers Retry review, on ontology-recovery-action; a 5xx only
  offers Check status. Mock a 422 and use that button.

* test(playwright): expect the sentence-case Ontology library title

#35989 renders the library drawer title sentence-case; #36190's spec
still expected title case. It never ran that far before, because it
failed earlier at the 924px header check.

* test(playwright): load reviewer options before typing in OntologyReview

The reviewer picker searches only once focused, and React Aria opens
its list only on a text change while options are already loaded, so
filling the whole name at once left the list closed. Click the picker
and wait for its first user search before typing.

Reverts the allowsEmptyCollection change: it read the previous render's
search text, so it never opened the list for a single fill.

* test(playwright): note why OntologyReview loads reviewer options first

* test(playwright): match the disabled approval-rule select in OntologyReview

In a locked review the rule control also renders a focusable tooltip
trigger with the same name, so getByRole('button', { name: 'All' })
hit a strict-mode violation. The step never ran before the reviewer
picker step was fixed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ontology Studio: finish the native shell, tree and scoped Query experience

1 participant