The deep-link protocol introduced in #1278 lived as bare "ontologyTab" /
"ontologyEntity" literals in five places across three files, each with its own
URLSearchParams plumbing and try/catch. Nothing tied the pieces together: in
particular the rule that a selection written under one ontology must be cleared
when the active ontology changes — otherwise a reload resolves the stale entity
and jumps back to the old ontology — was a comment at one call site with no
mechanism behind it.
ontologyUrlState.ts now owns the parameter names as private constants and
exposes the protocol as intent-named operations, with the write/clear pairing
documented where both halves live. Parsing and serialization are pure functions
over a search string, so they are covered by tests without a DOM; the window
and history.replaceState interaction stays in thin shells.
Absent parameters still read as undefined while blank ones read as empty
strings, which preserves the differing presence checks the workspace shell and
the tab selector each relied on.
One behavior change, inherited from all five original call sites: writing the
query string dropped any URL fragment, because replaceState with a bare "?..."
replaces the whole tail. updateSearch now carries window.location.hash across,
which fixes it for every writer at once — this module is the only place that
knows how the URL is written, so it is the only place the fix belongs.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
TimelinePanel fell back to a hardcoded 2030-01-01 whenever
/api/temporal/bounds reported max: null, then placed the playhead at the
midpoint of that fabricated window. session.get_temporal_bounds leaves max
open for any graph whose nodes carry valid_from instants and no
valid_until, so this was the normal response shape rather than bad data:
the header advertised a range no data supported and the workspace's first
/api/temporal/snapshot request asked about a time years ahead of the
present.
The fallback is now a `now` captured once per mount, and defaultTime is
that same `now` clamped into the range, so the initial snapshot describes
the current state. Three settings tuned for the fictional ~60-year window
follow from it: zoomMin drops from a year to a day, the timeAxis/format
pinning to 5-year ticks is removed so vis-timeline picks a granularity for
the real span, and the fixed 6-month play step becomes span/60 with a
one-day floor, which keeps a play-through at roughly 60 frames whether the
graph covers months or a decade.
The bound and step arithmetic moves into temporalScrubberBounds.ts so it
can be asserted directly, alongside the existing temporalLifecycle
predicate tests.
Closes#1536
Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
Replace the static biomedical category key in Explorer with a live node color legend derived from the active display graph, sharing the canvas's baseColor -> color -> theme fallback resolver.
- Preserve original semantic colors on focused display clones via `semanticBaseColor` so interaction styling (selection, path, neighbor highlights) does not bleed into the legend.
- Suppress the semantic legend while distance visualization modes are active.
- Refactor plugin fallback legend panels to use the shared legend builder with composite (group, color) keys.
- Consolidate duplicate `"test:graph-workspace"` scripts in `explorer/package.json`, recovering 26 previously shadowed tests across markdown and explorer capability suites.
- Add `tests/graphColorLegend.test.ts` to workspace test runs and wire `test:graph-legend-e2e` into `.github/workflows/ci.yml`.
- Closes#1479
The Ontology Hub editor selected a registered ontology but left the canvas empty, and opening an ontology deep link landed on the Welcome workspace instead of the editor. Two independent causes: the application shell ignored `ontologyTab`/`ontologyEntity` URL state at startup, and the editor loaded registry metadata but never fetched the selected ontology's schema nodes and structural edges. The backend now exposes a bounded schema subgraph for one ontology at `GET /api/ontology/graph?uri=...`, and the editor maps that response into React Flow nodes and edges with loading, error, selection, and layout handling.
Five things came out of review on the new endpoint and the editor that consumes it.
The edge selection originally included an edge whenever either its source or its target was a core node. That let a property owned by a completely unrelated ontology leak into the requested one just because its `rdfs:domain` or `rdfs:range` happened to point at one of the requested ontology's classes. Edges are now selected only when their source is a core node, so the requested ontology can still reference outward to external vocabulary, but nothing from an unrelated ontology gets pulled in the other direction.
The backend accepts both compact and full-IRI forms for node types (`owl:Class` and `http://www.w3.org/2002/07/owl#Class` are equivalent), but the frontend classifier only recognized the compact strings, so a full-IRI class or ontology node fell through to `"external"`, wrong panel, wrongly read-only. Classification moved into `ontologyEditorModel.ts` as `classifyNodeType`, which compacts known full IRIs before matching.
An ontology imported through the fallback RDF parser, one with no `owl:Ontology` or `skos:ConceptScheme` declaration, minted a synthetic registry URI but never created a matching graph node or set `scheme_uri` on the classes and properties it imported. `_node_belongs_to_ontology` had nothing to associate those nodes with, so `core_node_ids` ended up empty and the endpoint 404'd for a registered ontology that genuinely had data. The fallback parser now records that ownership and emits a matching `owl:Ontology` node whenever it has to synthesize a URI.
Nested namespaces that were never registered as their own ontology got silently absorbed into whichever parent prefix matched, in both directions: a fragment-delimited nested name (`<stem>/child#Term`) and a path-delimited one (`<stem>/child/Term`). The first fix only handled the fragment form; prefix ownership now only extends to names minted directly in the ontology's own namespace (`<stem>#Term` or `<stem>/Term`), and any further delimiter of either kind marks a nested vocabulary that isn't absorbed until it's registered or carries an explicit owner. Once registered, the nested namespace owns its own nodes as before.
Selecting a node in the editor writes `ontologyEntity=<id>` into the URL. Switching ontologies via the dropdown cleared the in-memory selection but left that parameter pointing at the old ontology, so a reload after switching could resolve the stale ID and jump back. The dropdown now clears the parameter on change.
Regression tests cover each fix directly: inward-edge exclusion, the full-IRI classification matrix, an end-to-end fallback-import test that forces the parser path and opens the resulting ontology, and a nested-namespace ownership matrix covering both delimiter forms in both the registered and unregistered case.
feat(explorer): add deterministic rendering E2E example and test (#1037)
Adds a deterministic Explorer graph baseline and coverage for the full
build -> persist -> API -> frontend hydration -> canvas rendering path,
so a regression anywhere along that chain shows up in CI instead manually.
examples/explorer_deterministic_rendering_example.py builds the
canonical 4-node, 3-edge graph (Alice -WORKS_AT-> Acme, Bob -KNOWS->
Alice, Acme -LOCATED_IN-> New York) with ContextGraph.add_node()/
add_edge(), persists it with save_to_file() and reloads it with
GraphSession.from_file(), printing the setup prerequisites and the
expected node/edge/label checklist for anyone running it by hand.
tests/explorer/test_explorer_deterministic_rendering_e2e.py covers
graph construction, the serialize/deserialize round trip, GraphSession
loading, and the Explorer API's /api/graph/* responses against the
exact expected nodes, edges, and labels, plus all three auth modes
(unconfigured, API-key required, anonymous opt-in).
fix(explorer): address Qodo review findings for deterministic rendering e2e (#1037)
- configure SEMANTICA_ALLOW_ANONYMOUS=true and document
SEMANTICA_API_KEY as the alternative in the reproduction
instructions, so the documented commands don't 503 on a clean
checkout
- add clean-checkout prerequisites and a visual verification
checklist to the example
- add edge-label (WORKS_AT, KNOWS, LOCATED_IN), zoom-tier, and
hover-interaction coverage to the frontend test
- add an explicit auth-enforcement integration test for the
deterministic graph endpoints
fix(explorer): connect deterministic rendering E2E path
The frontend test built its own node/edge objects directly with
batchMergeNodes()/batchMergeEdges(), bypassing the real loading path
entirely -- it never went through useLoadGraph, never mounted the
canvas, and its fixture didn't even carry the same fields the backend
actually returns (e.g. no color values), so a break in API hydration,
the edge.type -> edgeType mapping, or canvas label rendering could
still pass.
Adds deterministicExplorerRendering.e2e.ts, which mounts the real
Explorer app in Chromium, serves API-shaped /api/graph/nodes and
/api/graph/edges responses through route interception, drives the
app through its actual useLoadGraph hydration path into a real Sigma
canvas, and asserts on captured canvas fillText() calls that
WORKS_AT, KNOWS, and LOCATED_IN are genuinely drawn, both after load
and after Zoom In.
fix(explorer): preserve upstream markdown dependencies
ci(explorer): isolate deterministic backend test dependencies
Wires the new Python test into ci.yml as its own focused step (it
previously only ran manually), installs Playwright's Chromium
browser before the frontend suite, and keeps the deterministic
backend test's dependency install separate from the rest of the
pipeline so it doesn't pull in unrelated optional extras during
collection.
fix(explorer): remove redundant edge label hydration
An earlier commit in this PR added an explicit `label` field to
hydrated edge attributes on the theory that it was needed for edge
labels to render. Review traced through GraphCanvas.tsx's label
resolution (`attrs.edgeType || data.label || ""`, from the earlier
#1009 fix already on main) and found that `edgeType` is set
unconditionally on every edge during hydration, so it always wins the
`||` before `data.label` is ever consulted -- the added field and its
plumbing in useLoadGraph.ts and graphStore.ts never did anything.
Removed both; reran the real Chromium E2E test against the reverted
code and confirmed all three labels still render identically, closing
out the question of whether anything else was actually broken.
Explorer was firing temporal requests before the graph even loaded.
When the backend is down, /api/graph/nodes fails but the temporal
bounds and snapshot effects didn't care , they fired anyway, off in
their own corner, ignoring whether the graph actually came up. Every
page load with no backend meant three failed requests instead of one,
and a scrubber that had nothing to scrub.
Added two small predicate functions and gated the temporal effects on
them. Basically: don't ask for time-based data until you know the
graph itself loaded. An empty graph still counts as loaded, so that
case isn't broken.
Confirmed with the backend down, before and after: three failing
requests down to one.
Fixes#982.
Two issues addressed:
1. Plugin-loading useEffect unnecessarily depended on temporalState.
After the #830 fix, no shouldLoad predicate reads temporalState, but
the effect's dep array still included it, causing extra re-runs on
every scrubber update. Removed temporalState from the dep array and
the shouldLoad call site. Made temporalState optional in the
LazyPluginRegistryEntry shouldLoad context type to match.
2. Regression test imported a local copy of shouldLoad instead of the
production predicate. Extracted all three shouldLoad predicates into
pluginRegistryPredicates.ts (pure module, no React/DOM dependencies),
wired GraphWorkspace.tsx to use the imported functions, and updated
the test to import and exercise the real production code via tsx.
Verified: introducing the old broken condition causes the test to fail;
the correct implementation passes all 7 assertions.
temporalDiffState.ts belongs to feat/793-temporal-diff-ui and should not
appear in the #830 diff. Remove it from this branch's tracked files.
Add the pluginRegistry.temporal.test.mjs regression test that covers the
shouldLoad fix committed in the main #830 commit (it was never committed).
Add test:plugin-registry script to package.json so the regression test
can be run via npm run test:plugin-registry.
* security: fix 9 Dependabot/CodeQL alerts — DOMPurify, vite, uuid, workflow permissions
- Add explicit permissions block to defender-for-devops.yml (CodeQL #25)
- Upgrade vite 5.4.x → 6.4.3; bundled esbuild 0.21.5 → 0.25.12 (Dependabot #2, #7)
- Force dompurify ^3.4.0 via npm overrides; resolves 6 DOMPurify XSS alerts (#4–#6, #8–#11)
- Force uuid ^13.0.1 via npm overrides; fixes buffer bounds check (Dependabot #12)
* fix(ci): exclude bandit from MSDO scan on windows-latest
bandit_runner.exe builds a per-file command line; on a large Python repo
the total command string exceeds the Windows CreateProcess limit and the
process fails to start (Win32 ERROR_FILENAME_EXCED_RANGE 206).
Exclude bandit via the tools param and retain checkov, eslint,
templateanalyzer, terrascan, and binskim.
* fix(ci): drop binskim (no binaries), enable Neptune audit logging
- Remove binskim from MSDO tools: repo has no compiled binaries so
BinSkim raises AnalyzeArgumentNoValuesException and breaks the run
- Add EnableCloudwatchLogsExports: [audit] to NeptuneCluster to fix
Checkov CKV_AWS_101 (the one error-level result breaking the build)