Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ VITE_DICOM_WEB_NAME=ACME Hospital
# Error reporting URL for sentry.io.
VITE_SENTRY_DSN=

# If this env var exists and is true and there is a `save` URL parameter,
# clicking the save button POSTS the session.volview.zip file to the specifed URL.
VITE_ENABLE_REMOTE_SAVE=true

# VolView server remote URL
VITE_REMOTE_SERVER_URL=http://localhost:4014

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ jobs:
echo "Setting version to $VERSION for prerelease"
fi

- name: Build with remote save enabled
run: VITE_REMOTE_SERVER_URL= VITE_ENABLE_REMOTE_SAVE=true npm run build
- name: Build
run: VITE_REMOTE_SERVER_URL= npm run build

- name: Publish to npm
run: npm publish --tag ${{ steps.version.outputs.tag }} --access public --provenance
123 changes: 123 additions & 0 deletions backend-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# backend-contract

> **Status: private draft `0.x`.** No stability promise; shapes may change any
> day. The artifact version is the OpenAPI `info.version` in
> `generated/openapi.json`, mirrored by `package.json` (a drift test keeps them
> in lockstep). Sole known consumer: girder_volview, which reads this tree from
> the installed `volview` npm package.

The neutral VolView backend contract: task discovery, inputs, the job
lifecycle, result intents, and personal job history. No backend speaks these
shapes natively; each backend translates its own task format into this spec.

To bring a new backend online, implement the [OpenAPI surface](#the-neutral-rest-surface-openapi)
and validate against the fixtures and generated schemas. Zero VolView client
change, and no need to read girder_volview source: girder_volview is a
consumer of this package, not its owner.

## Layout

```
backend-contract/
index.ts top-level barrel; re-exports ./processing
processing/
task-spec.ts VolView's zod task-spec schema
wire.ts neutral wire shapes: input value, job status, job history,
result-intent vocabulary
openapi.ts the REST surface as an OpenAPI 3.1 document (wire schemas
injected from the zod codegen)
schema-json.ts zod -> JSON Schema codegen
index.ts re-exports schemas + types (not codegen/openapi)
__tests__/ every fixture validates; negatives fail; generated
artifacts stay in sync with the zod source
generated/ checked-in *.schema.json (one per wire schema) + openapi.json
fixtures/
task-spec/ synthetic golden task specs (no backend-native formats;
translation is a backend concern)
negative/ payloads that MUST fail validation
wire/ input values, job statuses, result intents, job handle,
result-read payloads
scripts/
generate-json-schema.ts regenerates generated/*.schema.json
generate-openapi.ts regenerates generated/openapi.json
```

## The single normative definition

The zod sources are the one normative definition. The golden JSON fixtures are
the interchange format both sides pin, and the generated JSON Schemas are the
backend's validator, codegen'd from the zod source so the two can't drift.

Task specs require two validation passes. First validate the generated
`task-spec.schema.json`, then enforce the cross-field rules implemented by
`validateTaskSpecSemantics` (or an equivalent implementation in the backend's
language). Standard JSON Schema cannot compare sibling values such as
`default <= max`. Backend conformance tests must also assert that every payload
under `fixtures/negative/` is rejected by the combined validation path.

## The neutral REST surface (OpenAPI)

`generated/openapi.json` (built from `processing/openapi.ts`) describes exactly
the endpoints the client calls, in neutral terms: no Girder routes, ids, or
enums.

| operation | method + path | request → response |
| --------------------- | ---------------------------- | ------------------------------------------------------------------------------------- |
| `listTasks` | `GET /tasks` | → `TaskSummary[]` |
| `getTaskSpec` | `GET /tasks/{taskId}/spec` | → `TaskSpec` |
| `runTask` | `POST /tasks/{taskId}/run` | `RunTaskRequest` → `JobRef` |
| `listJobHistory` | `GET /jobs` | → paged `JobHistorySummary[]` (optional) |
| `getJobHistoryDetail` | `GET /jobs/{jobId}/detail` | → logs + submitted parameters on demand |
| `deleteJob` | `DELETE /jobs/{jobId}` | → cascading deletion: execution record, results, staged inputs (terminal jobs only; 409 otherwise) |
| `stageInput` | `POST /stage` | parent-bound labelmap multipart → `StageResponse` (optional) |
| `getJob` | `GET /jobs/{jobId}` | → `NeutralJobStatus` |
| `getJobResults` | `GET /jobs/{jobId}/results` | → result intents, or explicit error |
| `cancelJob` | `POST /jobs/{jobId}/cancel` | → `NeutralJobStatus` |

Notes:

- The lifecycle is poll-only (`getJob`); push (SSE) is an additive backend-only
enhancement, not described here.
- Job-addressed routes are keyed by the opaque job id alone; the job's own
access control is the gate, so no context leaks into the path.
- `JobHistorySummary.outputSummary.recorded` counts declared outputs that
recorded and resolve to a readable file; `missing` counts the rest (never
recorded, or no longer readable).

## Job-state names

The neutral job states are `pending | running | success | error | cancelled`,
the names the client store consumes at runtime. A backend-side conformance test
(girder_volview `tests/test_status_conformance.py`) validates the projected
status against the generated `neutral-job-status` schema.

## Versioning

Two versions on separate clocks:

- **Artifact version**: `package.json` `version` and the OpenAPI
`info.version`, kept in lockstep by `processing/__tests__/openapi.spec.ts`.
Versions this package as a published thing.
- **Shape versions**: `INTENT_VOCABULARY_VERSION` (`processing/wire.ts`) and
the task-spec `specVersion`. These version the wire vocabulary for additive
compatibility negotiation.

## Regenerating

```
npx tsx backend-contract/scripts/generate-json-schema.ts # rewrite generated/*.schema.json
npx tsx backend-contract/scripts/generate-openapi.ts # rewrite generated/openapi.json
```

Drift guards (`processing/__tests__/generated-schema.spec.ts`,
`processing/__tests__/openapi.spec.ts`) fail if the checked-in artifacts fall
out of sync with the zod source.

## How the backend consumes this

The `volview` npm package ships `backend-contract/` in its `files`;
girder_volview reads fixtures + generated schemas from the installed package
via its `tests/contract_loader.py`. No vendored copy, no sync step: the
exact-pinned `volview` version in girder_volview's `web_client/package.json` IS
the contract pin. For unreleased branches use `npm link` or the
`GIRDER_VOLVIEW_CONTRACT_DIR` escape hatch (see the loader's docstring).
26 changes: 26 additions & 0 deletions backend-contract/fixtures/negative/constraint-violation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"specVersion": 1,
"id": "ConstraintViolation",
"title": "Constraint Violation (must fail validation)",
"description": "An int parameter whose `default` (99) exceeds its `max` (10). The schema must reject a self-inconsistent constraint set.",
"parameters": [
{
"kind": "sourceRef",
"id": "inputVolume",
"title": "Input Volume",
"required": true,
"accepts": [
"image"
]
},
{
"kind": "int",
"id": "radius",
"title": "Radius",
"min": 1,
"max": 10,
"default": 99
}
],
"outputs": []
}
5 changes: 5 additions & 0 deletions backend-contract/fixtures/negative/empty-uris.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "image",
"format": "nrrd",
"uris": []
}
24 changes: 24 additions & 0 deletions backend-contract/fixtures/negative/unknown-field-kind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"specVersion": 1,
"id": "UnknownFieldKind",
"title": "Unknown Field Kind (must fail validation)",
"description": "A parameter with an unknown `kind` must fail whole-spec validation. The client fails closed on an unknown field kind (Chunk 5 negative fixture; Chunk 7 hides the param and refuses submit if it was required).",
"parameters": [
{
"kind": "sourceRef",
"id": "inputVolume",
"title": "Input Volume",
"required": true,
"accepts": [
"image"
]
},
{
"kind": "color",
"id": "tint",
"title": "Tint",
"default": "#ff0000"
}
],
"outputs": []
}
22 changes: 22 additions & 0 deletions backend-contract/fixtures/negative/wrong-length-color.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "6600000000000000000000e1",
"intent": "add-segment-group",
"url": "/api/v1/file/6600000000000000000000e1/proxiable/otsu.nii.gz",
"name": "otsu.nii.gz",
"segments": [
{
"value": 1,
"name": "Bin 1",
"color": [
255,
0,
0
]
}
],
"source": {
"providerId": "analysis-provider",
"jobId": "job-abc123",
"outputId": "outputLabelmap"
}
}
86 changes: 86 additions & 0 deletions backend-contract/fixtures/task-spec/synthetic-all-kinds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"specVersion": 1,
"id": "SyntheticAllKinds",
"title": "Synthetic All Field Kinds",
"description": "Synthetic spec (not a real tool) exercising every scalar parameter kind plus an image input, a labelmap input, and both an image and a labelmap output.",
"parameters": [
{
"kind": "sourceRef",
"id": "inputVolume",
"title": "Input Volume",
"help": "Background volume to process.",
"section": "IO",
"order": 0,
"required": true,
"accepts": [
"image"
]
},
{
"kind": "sourceRef",
"id": "inputLabelmap",
"title": "Mask Labelmap",
"help": "Optional labelmap selecting where to process.",
"section": "IO",
"order": 1,
"accepts": [
"labelmap"
]
},
{
"kind": "int",
"id": "radius",
"title": "Radius",
"help": "An integer with numeric constraints and a default.",
"section": "Parameters",
"order": 2,
"min": 1,
"max": 10,
"step": 1,
"default": 1
},
{
"kind": "float",
"id": "threshold",
"title": "Threshold",
"help": "A floating-point value with a default.",
"section": "Parameters",
"order": 3,
"default": 50
},
{
"kind": "bool",
"id": "smooth",
"title": "Smooth",
"help": "A boolean toggle.",
"section": "Parameters",
"order": 4,
"default": true
},
{
"kind": "string",
"id": "label",
"title": "Label",
"help": "A free-text string.",
"section": "Parameters",
"order": 5,
"default": "result"
}
],
"outputs": [
{
"id": "outputVolume",
"title": "Output Volume",
"help": "Processed image output.",
"type": "image",
"format": ".nii.gz"
},
{
"id": "outputLabelmap",
"title": "Output Labelmap",
"help": "Segmentation output.",
"type": "labelmap",
"format": ".nii.gz"
}
]
}
65 changes: 65 additions & 0 deletions backend-contract/fixtures/task-spec/synthetic-bounds-enum.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"specVersion": 1,
"id": "SyntheticRegionEnum",
"title": "Synthetic Region and Enum",
"description": "Synthetic CLI (not a real radiology tool) exercising a region-derived bounds field, an enum, and UI hints for the translator conformance test.",
"parameters": [
{
"kind": "sourceRef",
"id": "inputVolume",
"title": "Input Volume",
"help": "Input volume (DICOM, NIfTI, NRRD, MHA, etc.)",
"section": "IO",
"order": 0,
"required": true,
"accepts": [
"image"
]
},
{
"kind": "bounds",
"id": "roi",
"title": "Region of Interest",
"help": "World-space box (LPS) to restrict processing.",
"section": "Region and options",
"order": 1
},
{
"kind": "enum",
"id": "method",
"title": "Method",
"help": "Which algorithm variant to run.",
"section": "Region and options",
"order": 2,
"options": [
"otsu",
"kmeans",
"manual"
],
"default": "otsu"
},
{
"kind": "enum",
"id": "iterations",
"title": "Iterations",
"help": "Number of refinement iterations.",
"section": "Region and options",
"order": 3,
"options": [
1,
2,
4
],
"default": 2
}
],
"outputs": [
{
"id": "outputLabelmap",
"title": "Output Labelmap",
"help": "Segmentation output.",
"type": "labelmap",
"format": ".nii.gz"
}
]
}
Loading
Loading