ISA-JSON compliance¶
SEEK can hold an ISA structure in two different shapes. The shape metaseed currently writes is accepted, stored and readable, but it is not ISA-JSON compliant, and SEEK refuses to export it as ISA-JSON. This page records what compliance requires, how it is reachable over HTTP, and what it costs metaseed to produce.
Verified against SEEK 1.18.1 with isa_json_compliance_enabled = true.
Prerequisites on the SEEK instance¶
The feature flags are the ones listed under Required SEEK settings in SEEK Export — enabled in that order, under Server administration → Enable/disable features. What matters specifically here:
| Prerequisite | Verified value | Why |
|---|---|---|
isa_enabled |
true |
provides Investigation/Study/Assay |
samples_enabled |
true |
provides Sample Types and Samples |
project_single_page_enabled |
true |
prerequisite of ISA-JSON compliance, and the page both ISA endpoints redirect to |
isa_json_compliance_enabled |
true |
puts the instance in ISA mode |
| Seeded ISA tags | 11 | tags are resolved by title; a bare instance has them |
| Seeded assay classes | EXP, MODEL, STREAM |
STREAM is required to create an assay stream |
One qualification, because it is easy to over-read: isa_json_compliance_enabled is not consulted by ISAStudiesController, ISAAssaysController, Study#is_isa_json_compliant?, Assay#is_isa_json_compliant?, or ISAExporter::Exporter. Only the views check it. Turning it off therefore does not stop the sequence below from working or the export from succeeding — it hides SEEK's ISA affordances in the UI. Compliance is enforced by structure, and the flag is what makes that structure visible to a user.
Beyond the flags, the API token must belong to a person who is a member of the target project: every resource is created with that person as contributor, and the ISA endpoints read User.current_user directly.
What compliance is¶
is_isa_json_compliant is a boolean column on Investigation, and a derived predicate on Study and Assay:
| Level | Predicate | Requirement |
|---|---|---|
| Investigation | is_isa_json_compliant |
the column is set |
| Study | Study#is_isa_json_compliant? |
the Investigation is compliant and the Study owns at least one Sample Type |
| Assay | Assay#is_isa_json_compliant? |
the Investigation is compliant and the Assay owns a Sample Type, or is an assay stream |
Compliance is therefore structural, not a label. Setting the flag alone does not make a Study compliant, and does not make the Investigation exportable.
Why it matters¶
ISAExporter::Exporter refuses a non-compliant Investigation outright:
Every Investigation metaseed creates today has is_isa_json_compliant = nil, so none of them can be exported as ISA-JSON. Setting the flag on an existing metaseed Investigation does not fix it: the export then fails on the Study, which owns no Sample Types.
What compliance requires¶
A Study owns exactly two Sample Types, in order, and the second links to the first:
- Source — its attributes carry the
sourceandsource_characteristicISA tags. - Sample Collection — carries
protocol,sampleandsample_characteristictags, plus an input attribute: typeRegistered Sample List, ISA taginput,linked_sample_type_idpointing at the Source type.
An Assay hangs off an assay stream (an Assay whose class is STREAM) via assay_stream_id, and owns its own Sample Type, which must have:
- exactly one attribute tagged
protocol, - exactly one tagged
data_fileorother_material, - an ISA tag on every attribute,
- an input attribute linking back to the previous Sample Type in the chain.
The chain is what makes the stream a stream: Assay#first_assay_in_stream matches on sample_type.previous_linked_sample_type == study.sample_types.second. Streams are held together by linked Sample Types, not by tree position.
ISA tags are looked up by title rather than id — the numeric ids are seeded per instance and are not part of any published contract.
The transport: two APIs, not one¶
The ISA structure is not reachable over the JSON:API alone.
| Resource | Endpoint | Encoding |
|---|---|---|
| Investigation (incl. the compliance flag) | POST /investigations |
JSON:API |
| Study + Source + Sample Collection types | POST /isa_studies |
form-encoded |
| Assay stream | POST /isa_assays |
form-encoded |
| Assay + its Sample Type | POST /isa_assays |
form-encoded |
| Sample | POST /samples |
JSON:API |
/isa_studies and /isa_assays back SEEK's web forms, but they authenticate with the same API token and need no browser session. Two things force their use:
assay_stream_idis absent fromassay_paramsand from theassayPostschema, soPOST /assaysaccepts it, answers200 OK, and discards it.SampleType has_and_belongs_to_many :studies, butstudy_idsis not permitted onPOST /sample_types, so a Sample Type cannot be attached to a Study over the JSON:API.
Both endpoints require form encoding. Their format.json branch is unreachable: check_json_id_type demands a JSON:API data member, and convert_json_params then drops the isa_assay / isa_study key. Repeated form keys must use [] rather than numeric indices — indexed keys parse as a Hash, which the controller iterates as an Array and crashes with a TypeError.
The verified request sequence¶
Each step returns 302; the whole sequence needs only an API token.
POST /investigationswithis_isa_json_compliant: true.POST /isa_studieswith the Study, the Source type and the Sample Collection type. The Sample Collection's input attribute needs alinked_sample_type_idthat passes validation beforeISAStudy#saveoverwrites it with the real Source type — validation runs before the link is assigned, so any existing Sample Type id serves as a placeholder.POST /isa_assayswithassay_class_id= theSTREAMclass to create the stream.POST /isa_assaysper assay, withassay_stream_id,input_sample_type_id(the Study's Sample Collection type) and the assay's own tagged Sample Type.POST /samplesas today.
The result exports: ISAExporter::Exporter returns an ISA-JSON document with the study, its assays, and materials containing sources and samples.
What this costs metaseed¶
Provisioning becomes dataset-shaped. Sample Types are currently derived from the profile — one per sample-bearing entity, shared across every Assay that uses it. A compliant stream needs one Sample Type per assay instance, because each type links to the previous one in that assay's chain. Two assays of the same profile entity need two Sample Types with different links, which a profile-time projection cannot express.
Profiles need ISA tags. Every attribute of a compliant Sample Type carries an ISA tag, and the tag set is constrained per level. FieldSpec has no field for this today, so the profile cannot say which of its fields is the protocol, which is the data file, and which are characteristics.
Ordering becomes significant. study.sample_types.first and .second are load-bearing — Source and Sample Collection respectively. Nothing in the current model records that ordering.
Upstream defects encountered¶
Reported separately; none has a workaround inside SEEK's settings.
| Defect | Effect |
|---|---|
POST /assays silently discards assay_stream_id |
200 OK for a structure that was not built |
/isa_assays and /isa_studies have unreachable format.json branches |
JSON callers get 422 or 500 regardless of body |
Hash-shaped sample_attributes raise TypeError |
500 instead of a 4xx |
isa_exporter.rb:42 interpolates bare investigation instead of @investigation |
the "studies should be ISA-JSON compliant" error raises NameError, masking the real cause |
ISAStudy validates the input attribute's link before save assigns it |
a placeholder linked_sample_type_id is required to create a Study |
Compliance is not sufficient for export¶
A compliant structure is what SEEK's validation requires. Its exporter requires more, and the difference only appears once the Study holds Samples.
ISAExporter walks a material chain: a Source sample, the Sample Collection sample produced from it, and the assay sample produced from that — each naming its predecessor in its input attribute. It also refuses a Sample with no protocol value:
Sample {2594: expsample} has no protocol
isa_exporter.rb:726 inputs.map -> undefined method `map' for nil
An Investigation with compliant Studies and no Samples exports cleanly, which is misleading: it is the empty case, not the working one.
Two consequences:
- Every Sample the sync creates records the Assay that produced it as its protocol. The Protocol attribute stays optional on the Sample Type, so a Sample created by other means is not refused.
- The material chain needs a profile with the levels to carry it.
seek-ready-template2.0 has a single Sample level, nested under Assay, so an assay sample has no predecessor to point at. 3.0 carries the chain:Study -> Source -> Sample -> AssayMaterial, each level naming its parent as its input. AnAssayMaterialnames the Assay that measured it byreference, because an Assay measures materials derived from many Samples and containment cannot express that. - Each level's Sample Type is built from its own profile entity. Deriving all three from one entity makes every type demand the others' required attributes, and SEEK rejects each Sample for the fields belonging to a different level.
The export needs Sample Types built from ISA Templates¶
Even with the chain complete, the export fails:
process_sequence_output reads each Sample Type's ISA Template to decide whether the material is a data_file or an other_material. A Sample Type built attribute-by-attribute — which is the only thing the API allows — has no template, so the export raises regardless of how correct the structure is.
Sample Types can be created from a template (template_id is permitted on POST /sample_types), but Templates themselves cannot be created over the API: POST /templates drops the nested attributes, and the ISA Template API is admin-UI-only. So the export path is gated on a template being installed by an administrator first, and on the profile's fields matching it.
This is the deepest of the upstream gates found, and unlike the others it has no workaround from the client side.
Current status¶
The sync builds compliant content: Investigations carry is_isa_json_compliant, each Study owns a Source and a Sample Collection Sample Type, each Study gets one assay stream, every Assay hangs off it owning its own Sample Type chained to the Study's Sample Collection type, and Samples are created into their Assay's type. Sample Types are therefore created per dataset node rather than per profile entity; provisioning still builds its own for the FAIR-Data-Station file route, which matches samples by attribute PID.
Not yet exportable with Samples present, for the reason above. tests/test_seek/test_live.py::test_a_pushed_dataset_is_exportable_as_isa_json is a strict xfail recording exactly that gap, so it will fail — and demand attention — the moment it starts passing.
A Sample with no Assay ancestor is reported in SyncResult.unlinked rather than pushed: under compliance a Sample's type is the one its Assay owns, so with no Assay there is no type to create it in.
Creating a Study needs a placeholder linked_sample_type_id (see the ISAStudy defect above). The sync creates one Sample Type per project named <profile> ISA placeholder, reused by title; it is an artifact of that defect, not part of the ISA structure.