Skip to content

Internal strain-genomic-segment record class (model side) - #209

Open
jbrestel wants to merge 42 commits into
masterfrom
strain-segment-record
Open

Internal strain-genomic-segment record class (model side)#209
jbrestel wants to merge 42 commits into
masterfrom
strain-segment-record

Conversation

@jbrestel

@jbrestel jbrestel commented Jul 31, 2026

Copy link
Copy Markdown
Member

Adds an internal, non-user-facing WDK record class and search: given one or more strains plus a reference-coordinate location (sequence, start, end, strand), it returns each segment in that strain's consensus-sequence coordinates as a BED feature — one line per strain from a single search. Coordinates are derived by prefix-summing apidb.indel.shift.

Companion PR: VEuPathDB/ApiCommonWebsite#307 (BED reporter + feature provider). Neither works without the other — the <reporter> element here names a class from that PR, and WDK resolves it at model-load time.

Verified end to end on the FungiDB dev instance

Full write-up: docs/superpowers/2026-07-31-strain-segment-e2e-report.md.

  • BED chrom byte-matches the deflines in the strain consensus FASTAs (<strain>_<refSeq>) — the constraint the whole feature exists to satisfy.
  • clustalo alignment of the real consensus sequences at the returned coordinates reproduces the recorded indels exactly: gap sizes match the recorded shifts, and strains with no recorded event show no gap.
  • Control: at the returned coordinates, 99.92% identity to the reference; at unshifted coordinates, 25.0% (chance).
  • Invalid input returns an empty result, not an error (wrong organism, out-of-range); in a multi-strain selection, irrelevant strains drop out and the rest still return.

Design decisions worth a look

  • No includeProjects/excludeProjects anywhere, including the PK columns. project_id comes from the data (apidb.organism.project_name), never @PROJECT_ID@ — so no UniDB fork of the kind DynSpan carries.
  • No individuals.txt entry. That omission is what keeps the search out of the category tree while leaving it reachable by name via the service API.
  • useBasket="false" — otherwise WDK injects basket questions for a record class that supports no saved strategies.
  • strain_start/strain_end are deliberately unclamped so a segment inside a deletion inverts; only strain_length is clamped. The feature provider in #307 rejects the inversion, so that guard is load-bearing — if it goes, these must be clamped instead.
  • <strain>_<refSeq> is opaque and must never be split — strain names contain underscores. Consumers parse the primary key, where : delimits unambiguously.

Note on four cherry-picked commits

This branch carries four commits from dnaseq-merge-experiments (the DeRisi .dst injection, the compound-search attribute, the dormant DNASeq injector, and the communitycount consumer). They are required for the dataset gate to work on a partial-appDb dev instance, and will also arrive via that branch's own merge.

Simulated both merge orders against current master: no conflicts either way, and both orders produce an identical tree. Worth re-checking at merge time if those commits change on the other branch.

jbrestel and others added 26 commits July 31, 2026 11:38
Introduces the design for a new internal `strain-genomic-segment` WDK record
class that translates reference-coordinate genomic segments into isolate/strain
coordinates using apidb.indel, terminating at a BED reporter.

Key decisions recorded:
- Primary key carries reference coordinates plus strain; strain coordinates are
  derived in an attribute query, preserving provenance and picking up indel
  reloads without recomputation.
- Strain vocabulary comes from apidb.indel joined through
  study.protocolappnode (there is no strain column on apidb.indel); scoped per
  organism.
- Queries against partitioned webready.*_p tables must constrain org_abbrev.
- BED chrom must equal <strain>_<refSeq>, the dnaseq consensus FASTA index key;
  the name column is free and becomes the output defline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec flagged "ontology absence hides the search" as an unverified
assumption. Verified against Model/lib/wdk/ontology/individuals.txt and
recorded the result as a new section 6.1.

Two mechanisms exist for hiding a search. Omitting the row is correct here,
matching DynSpansBySegIds and DynSpansByLocation, which are both absent from
individuals.txt while remaining addressable through the service API. The
"internal webservice" scope combination is used only by injected per-dataset
searches, which need ontology presence for categorization.

Consequence: this work adds no ontology node, so wb model suffices and
wb ontology is not required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight tasks, each independently committable, covering the grammar class with
unit tests, the strain vocabulary param, the validating ID query, the
coordinate-conversion attribute query, the record class and model imports, the
BED reporter, end-to-end verification, and a UniDB scope fix on the existing
eQTL segment search.

Two findings from writing it that shaped the tasks:

- The attribute query cannot use webready.*_p at all. Attribute queries receive
  only primary key columns, so there is no org_abbrev to prune partitions with,
  and every index on those tables is org_abbrev-prefixed. There is also no
  unpartitioned GenomicSeqAttributes in this database, which is why DynSpan's
  Bfmv query cannot run here. The plan routes through dots.ExternalNaSequence
  instead, which is unpartitioned and indexed on source_id.
- Tasks 2 through 4 land model XML before it is imported, so each commits
  safely without a working model. Task 5 adds the imports and is the first task
  that can fail a build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new record works on every project, so it carries no includeProjects or
excludeProjects anywhere: not on the record class, the question, the queries,
or the primary key columns.

This removes an excludeProjects="UniDB" on the project_id PK column and the
associated UniDB query variants, both carried over from DynSpan without
justification. DynSpan needs that fork because it derives project_id from
@PROJECT_ID@, which is meaningless on the portal. This record takes project_id
from webready.GenomicSeqAttributes_p instead, so one query and one PK are
correct everywhere.

Unaffected: Task 8, which adds UniDB to the pre-existing eQTL segment search's
attributesList scope. That concerns an existing search, not this record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code review of Task 1 tightened the implementation in three ways that the
design documents did not yet reflect.

The strain capture group now excludes underscore. The FASTA key is
<strain>_<refSeq> and reference sequence IDs legitimately contain underscores
(Pf3D7_01_v3), so without that exclusion strain A_B with sequence C and strain A
with sequence B_C both yield the key A_B_C. No current strain name contains an
underscore, so this is inert today and turns a future ambiguous key into a parse
failure instead of a silently unfindable sequence.

Validation moved into the private constructor and gained a refStart < 1 check.
A zero start reaches BedLine.locationToZeroBased() and emits chromStart = -1, a
malformed BED line with nothing pointing back at the ID that caused it. The
original plan only checked start > end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The search is handed a reference sequence ID, so also being handed an organism
was redundant input that only created a consistency question the query then had
to answer. Dropping it removes three problems at once.

No partition-key concern: the ID query now resolves sequences through
unpartitioned dots.ExternalNaSequence rather than webready.GenomicSeqAttributes_p,
which is the same table path the attribute query already used, so the two are
consistent instead of divergent. This design now touches no webready.*_p table.

No @PROJECT_ID@: project_id for the primary key comes from
apidb.organism.project_name via taxon_id, so one query stays correct on every
project.

No vocabulary-shape trap. With an organism param, org_abbrev IN
($$organismSinglePick$$) is wrong, and silently so. That param defaults to
organismVQ.withGenes, a tree vocabulary projecting string_agg(abbrev, ', '), so
a leaf term yields one abbrev but any grouping node yields 'afumAf293,
afumA1163' — a single quoted literal matching no row, with no error logged. The
usual dodge is overriding queryRef to a flat abbrev vocabulary, but every such
vocabulary carries project scoping, which this record may not have.

The original requirement to validate the reference sequence against the
reference organism is now satisfied by construction: the EXISTS gate proves the
strain has indel data on that exact sequence, so strain and sequence are
consistent without trusting a supplied organism.

Strain vocabulary becomes global: 6,119 strains in 2.9s, cached once instead of
once per organism, so cheaper in aggregate than the 2.66s-per-organism version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reference sequence ID already determines its organism, so taking an organism
as well was redundant input. Resolving the sequence through unpartitioned
dots.ExternalNaSequence removes the partition-key constraint, and taking
project_id from apidb.organism.project_name avoids @PROJECT_ID@. The strain
vocabulary becomes global; relevance is enforced by the ID query's EXISTS gate.
Deriving from study.protocolappnode with an EXISTS probe into indel_ix1
replaces a scan of all 43.6M apidb.indel rows: 2802ms to 57ms for an
identical 6119-strain result set. Renames the query, whose StrainsByOrganism
name no longer described it. Records why CONCAT is applied to the parameter
rather than the column, and that the apidb.organism join is also a gate.
…ation

One protocol_app_node_id spans every sequence of a strain, so grouping the
offsets subquery on (strain, ref_start, ref_end) collapsed two segments on
different sequences into one group and summed their shifts into both. Grouping
and joining on source_id makes one-row-per-PK true by construction. Also clamps
strain_length at zero for a segment falling inside a deletion, where the
boundary rule legitimately inverts start and end, and expands WDK_ID_SQL once
via a CTE.
Adds sections 5.3.1 and 5.3.2 covering two findings from review of the
attribute query.

The offsets subquery must group and join on source_id, not on the coordinate
triple. One protocol_app_node_id spans every sequence of a strain, so keying on
(strain, ref_start, ref_end) conflated two segments on different sequences and
summed their shifts into both, silently. Keying on the whole primary key makes
one-row-per-PK true by construction.

A segment lying inside a deletion legitimately inverts, because the boundary
rule shifts the end but not the start. strain_start and strain_end report the
true inverted values so the reporter can reject them; only strain_length is
clamped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EOF
…e claim

Follow-up review findings on StrainSegmentAttributes.Coords.

Correctness:
- Sum the offsets over a DISTINCT id set (new `segs` CTE) instead of the raw
  ##WDK_ID_SQL## output. A repeated source_id joined every indel row once per
  duplicate and GROUP BY source_id collapsed them into one group whose SUM was
  multiplied by the duplication factor -- silently. PK
  366.1:Pf3D7_10_v3:331757-331757:f supplied twice gave offsets -198/-306
  instead of -99/-153. Not reachable through today's ID query, but keying on the
  PK is meant to make one-row-per-PK true by construction, not by an argument
  about the input.

Documented invariants (the code was right, the stated reasons were wrong):
- strain_seq_id: the comment claimed strain names contain no underscore, so the
  first '_' ends the strain name. Re-measured: of the 6,119 strain names with
  indel data, 1,494 (24%) contain '_' (1_01_01, Af293_resequence2, China_LZCH-36,
  USGS_28834_1_NV) and 0 contain ':'. The concatenation is still correct -- it
  must match the dnaseq FASTA defline -- but the key is OPAQUE and must never be
  split back apart; consumers parse the strain out of the ':'-delimited PK.
  0 actual key collisions today.
- Collision detector: sharpened. All 126 duplicated strain names already have two
  protocol app nodes each carrying indel rows; what keeps the detector dormant is
  that no (name, na_sequence_id) pair is served by two nodes (0 today). That is
  the invariant the na_sequence_id join relies on.
- StrainSegmentFeatureProvider rejecting the deletion inversion is retensed as a
  Task 6 requirement -- the class does not exist yet, so nothing rejects it today.

Simplification:
- offset_end drops its tautological CASE (the '<= ref_end' bound is already in the
  WHERE; verified 0 rows disagreeing) so the boundary asymmetry lives in exactly
  one place. offset_start keeps '<' -- that asymmetry is real.
- strain_length is computed in an outer SELECT from the named strain_start /
  strain_end columns rather than repeating their offset arithmetic, so the length
  cannot drift from its own endpoints.
- Hoist na_sequence_id/taxon_id into the `ids` CTE and drop the second
  dots.ExternalNaSequence join, halving the per-PK index scans and leaving one
  enforcement point for "unknown reference sequence yields no record".

Verified against genomicsdb_rebuild01 (SELECT/EXPLAIN only): NRZ-2016-071 Chr1/Chr2
still 100012/100014; the 1-bp deletion PK still 331658/331604 length 0; three PKs
differing only in strand still 3 rows; the same PK twice now yields 2 rows with
single-copy offsets; EXPLAIN ANALYZE still uses indel_ix1 with `ids` materialized
once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two documentation defects found by review, both of which would have
misled the next reader:

- §5.3 listed only the five derived columns and §6 said "exactly those
  §5.3 emits", which read literally forbade the ref_* attributes that
  §7's defline consumes.  The query has always emitted eleven columns;
  the spec understated it.  Now names all eleven and says why the
  primary-key fields are passed through rather than re-parsed.

- "126 strain names resolve to two protocol app nodes" is imprecise:
  122 resolve to two and 4 to three, which is where §2's count of 256
  nodes comes from.  Corrected in the spec (twice) and in the
  collision-detector comment.  The invariant that keeps the detector
  dormant is unchanged: no (name, na_sequence_id) pair is served by
  more than one node.
':' is the only delimiter in the strain segment primary key, so a
reference sequence whose source_id contains one produces an ID that the
attribute query mis-parses and then dies on -- 'ncRNA'::integer aborts
the ENTIRE attribute query, taking out every row on the page rather
than just the offending record.

10,704 dots.ExternalNaSequence source_ids do contain ':' (transcript
features such as bld68_Tb927.1.05:mRNA).  None of the 20,823
indel-bearing sequences do, so this is unreachable today -- which is
the reason to gate it rather than a reason not to: the grammar should
not depend on which features happen to get indel-called.  Verified the
gate refuses 0 indel-bearing sequences and that all 8 Af293
chromosomes still pass.

Also corrects three comments the review found under-evidenced or
unverifiable:
- the colon-unambiguity claim covered "the strain or the sequence" but
  measured only strain names; now measures both, and points at the gate
- the FASTA defline was quoted as '{sample}_{chrom_name}', which reads
  as a verbatim f-string; now matches the spec's notation and cites
  makeConsensusFastaFromVcfAndBed.py:233
- documents the sres.TaxonName inner join as the second silent
  row-drop point, and the cross-query inconsistency that the ID query
  resolves organism via apidb.organism while this one uses TaxonName
The record class is deliberately bare: primary key, idAttribute, and the nine
non-PK columnAttributes from StrainSegmentAttributes.Coords. No tables, no
summary view, no display attributes, so a record-page request fails rather than
rendering an empty page. No includeProjects/excludeProjects anywhere -- unlike
DynSpanRecordClass, project_id comes from the data, so no per-project fork is
needed.

The bed reporter element is NOT included yet: ReporterRef.resolveReferences does
a Class.forName on the implementation at model-load time and throws, so the
element cannot land before BedStrainSegmentReporter does. Task 6 adds both.

No individuals.txt row, which is what keeps the search out of the category tree
while leaving it addressable by name through the service API. wb model therefore
suffices; wb ontology is not required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 5's spec review found two plan defects with real downstream cost,
plus staleness that had already caused one live bug.

Task 6 could not have succeeded as written:
- Its Files list named only the two ApiCommonWebsite Java files, with no
  step registering the <reporter> element on the record class.  Task 5
  had to omit that element -- WDK's ReporterRef.resolveReferences calls
  Class.forName at model-load time, so registering a class that does not
  exist yet hard-fails the build -- and the deferral note lived only in
  the XML and a commit message, neither on a Task 6 implementer's path.
  Without the registration the BED download is simply not offered, with
  no error; Task 7 would have failed on an unknown format.  Task 6 now
  spans both repos and has explicit register + rebuild steps.
- Task 6 now also states that rejecting the inverted interval is a
  required deliverable, not a nicety.  The attribute query deliberately
  leaves strain_start/strain_end unclamped so a segment inside a
  deletion inverts; nothing upstream rejects it, so if the provider
  does not, an inverted interval reaches a FASTA lookup silently.

Removed the stale organismSinglePick expectations from Task 5 Step 5 and
Task 7 Step 3.  There is no organism param; WDK rejects unknown
parameters, so Task 7 would have failed on its first call.

Corrected the false measurement at the root of the Task 1 regex bug.
Task 2's preamble asserted that no strain name contains an underscore;
1,494 of 6,119 (24%) do.  That claim propagated into the PK grammar as
[^:_]+ and silently rejected a quarter of legitimate primary keys.  The
plan and spec now record the real numbers, the fix, and why narrowing
the grammar could never have worked: reference sequence IDs contain
underscores too, so <strain>_<refSeq> is not reversible at all.

Also: Task 5 Step 5 claimed -showQuery would print the attribute query
with ##WDK_ID_SQL## expanded.  It does not, and that is correct -- run
standalone through QueryTester there is no answer to supply the ID SQL.
The step now says what it actually proves.  And §3's project_id
provenance named webready.GenomicSeqAttributes_p; the implementation
uses apidb.organism.project_name.
Turn off the basket, drop a costly sort, and stop the comments claiming
guarantees WDK does not make.

- useBasket="false" on StrainSegmentRecordClass. The default is true
  (RecordClass.java:303), so WdkModel.addBasketReferences injected
  StrainSegmentRecordClasses_StrainSegmentRecordClass{Realtime,Snapshot}Basket
  questions and user_baskets id queries, and RecordClassFormatter:75
  advertised basket affordances for a class with no record page and no
  saved strategies. Matches fileRecord.xml, userfileRecords.xml,
  ajaxRecords.xml; verified gone from wdkXml -model FungiDB.
- Drop sorting="strain_seq_id asc". AnswerValue.prepareSortingSqls splices
  a QueryColumnAttributeField's attribute query into the ordering SQL, so
  it re-ran the apidb.indel prefix-sum aggregation just to order rows.
  Undeclared, WDK sorts on the idAttribute against the id query alone;
  strain_seq_id is a pure function of the PK, so nothing is lost.
- Correct a false precedent: DynSpansByLocation is only an sqlQuery with
  no question and no references anywhere, so it says nothing about
  service-API reachability. Cite DynSpansBySegIds alone, and record in
  spec 5.1 that the param shape is modeled on a query no question has
  ever exercised.
- Rewrite the record-class comment as intent, not a guarantee: WDK always
  injects a _default summary view, a _default "Overview" record view, and
  DefaultJsonReporter, and the nine columnAttributes are display
  attributes (they must stay so; the BED reporter reads them).
- Spec: add the model-side <reporter name="bed" scopes="results"> element
  as a third Task 6 deliverable and track it in the risk table, since a
  missing reporter means no BED download and no error anywhere.
- attributesList summary now mirrors what the BED download consumes; add
  a <description> alongside <summary>; note that the querySet
  StrainSegmentId is unrelated to the Java class of the same name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rewrite that closed the previous round introduced a new false claim
of exactly the kind that round existed to remove, plus two overclaims:

- The naming-caution comment, whose only job is to stop a reader
  conflating the querySet StrainSegmentId with the Java class of the
  same name, gave the class's package as
  org.apidb.apicommon.model.StrainSegmentId.  Nothing exists there; it
  is org.apidb.apicommon.model.report.bed.util.StrainSegmentId.  A
  disambiguating comment that resolves to nothing is worse than none.

- "no reference anywhere in ApiCommonModel..." for DynSpansByLocation
  was absolute and false: the Model/vp2TuningTablesEffort inventories
  name it twice.  The substantive point survives and is stronger for
  being precise, because tableUsageMap.json records an EMPTY usage list
  for it -- an independent inventory agreeing nothing uses it.

- The attributesList comment claimed the summary "mirrors exactly what
  the BED download consumes".  It does not: per spec section 7 the
  provider needs four attributes and reads strain and ref_seq out of the
  primary key instead, so the claim was false by two columns against the
  section it cited.  It also cited getRequiredAttributeNames() in the
  present indicative for a class Task 6 has not written yet.

Also: two line cites had drifted (addBasketReferences is at 646-652,
prepareSortingSqls spans 748-824); the sorting fallback now traces from
Question.getSortingAttributeMap, where the removed attribute actually
lived, rather than starting at the record class; and spec 5.1.1
attributed source_id_uniq to dots.ExternalNaSequence, which is a view
and can hold no index.  The index is on the base table nasequenceimp,
making the uniqueness guarantee broader than the spec claimed.

Verified: wb model builds and the webapp reloads.
scopes="results" only, not "results,record" as dynSpanRecord.xml and
genomicRecords.xml use: this class has no record page to download from.
name="bed" is the literal reportName a download request passes, so it must match
exactly, and nothing references the reporter by name at build time, which is why
omitting the element yields no BED download and no error anywhere.

Verified with wb model: ReporterRef.resolveReferences Class.forName's the
implementation at model-load time, so a successful load is the proof that
BedStrainSegmentReporter is compiled into the deployed webapp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 6's first wb model failed with 'Implementation class for reporter
bed ... cannot be found'.  The XML registration was correct; the class
had never been deployed.  Verified in gus_home/bin/wb: the model target
runs bld EbrcModelCommon/Model and bld ApiCommonModel/Model only, site
covers Website/Site, ontology adds Presenters/Model, and no wb target
builds ApiCommonWebsite/Model at all.  mvn test compiles to
target/classes, which is not on wdkXml's classpath.

Worth documenting because the error names the wrong file: it reads as a
bad reporter element and invites reverting a correct registration.  The
fix is bld ApiCommonWebsite/Model first.
Spec section 7 showed the two coordinate ranges as 'ref 100 to 200 |
strain 143 to 241'.  Nothing emits that.  DeflineBuilder.appendPosition
renders '<chrom>, forward strand, 100 to 200', so matching the example
would have meant adding a builder method or hand-rolling strings, and
the invented form silently dropped the strand.

The name column is declared Free and the normative requirement is only
that it carry provenance through DeflineBuilder honouring
RequestedDeflineFields, which the implementation does.  Corrected so
nobody 'fixes' working code to match a retired example, and so Task 7
compares against the real string.
Task 6's re-review found the inverse of the usual defect: comments that
were accurate when written and that landing Task 6 made false.  Each was
scoped "as of this commit", which is defensible authorship but reads as
wrong to anyone looking at the current tree.

The consequential one is in strainSegmentAttributeQueries.xml, sitting
directly beside the deliberately unclamped strain_start/strain_end and
telling the next reader that nothing downstream rejects an inverted
segment.  Something does, since a3c062e03.  Leaving those columns
unclamped is safe ONLY because that guard exists, so the comment now
says that explicitly and names all three conditions the guard checks.

Spec 5.3.2 likewise flipped from "Not yet implemented" to implemented,
and now enumerates the full contract: the deletion inversion,
strain_start < 1, and the strain_seq_id-versus-primary-key agreement
check added during hardening.  It also states the converse, which is the
part worth keeping: if that guard is ever removed, these columns must be
clamped instead.

Also hardened the plan's test invocations.  This module inherits surefire
2.12.4, where -Dtest=A+B matches ZERO tests; combined with the plan's
-DfailIfNoTests=false that reported BUILD SUCCESS having run nothing.
Switched to -DfailIfNoTests=true, corrected the stale "Tests run: 12"
expectation to the 20 + 10 = 30 actually observed from a comma-separated
run, and recorded the trap where someone will hit it.

Verified: wb model builds and the webapp reloads.
Found while starting Task 7.  Every SQL fact in this spec and plan was
measured against genomicsdb_rebuild01 (localhost:5439), but the site
resolves its appDb through LDAP genomicsdb_devn, which is
genomicsdb_070n on ares13.  On that database apidb.indel does not exist
and there are 0 _Indel protocol app nodes.

Consequence: the ID query's EXISTS over apidb.indel does not return zero
rows on the deployed site, it raises "relation does not exist".  Task 7
Steps 3-6 (run the search, download BED, prove the coordinates shifted,
check the defline) cannot run there.  Steps 1-2 read model metadata and
are unaffected.

What still stands: the queries were verified against a database that
really holds this data, so the SQL correctness work is not in doubt.
What was never established is that any deployed site can execute them.
Also worth recording: the earlier switch from giardiadb to fungidb was
justified by querying rebuild01, so it picked the right data but said
nothing about either instance's actual appDb.

Deliberately not worked around -- pointing an instance at a different
appDb is John's call, and model-config.xml is conifer-generated rather
than hand-edited.
jbrestel and others added 3 commits July 31, 2026 20:27
… failed

The instance was repointed at unidb_shu_a, which does have indel data
(1,855,449 rows, 452 strains, 120 sequences, 13 organisms) -- a third
database, so the spec section 2 figures, measured on rebuild01, do not
describe it.  Recorded the real numbers and two verified test cases for
Steps 3-6, including the required offset_start=0 / offset_end!=0 case.

Also recorded why the first gated rebuild still failed: the flag was
present but the gate implementation is not on master.  It lives on the
dnaseq-merge-experiments branch, so presenterInjectTemplates ignored the
property, injected a presenter whose dataset is absent from
apidb.datasource, and the resulting EDA meta query referenced a table
that does not exist.
IsolatesHTS no longer describes how DNASeq datasets are processed on this branch,
and both of its methods fail on the new shape:

- injectTemplates() derived per-sample gbrowse/jbrowse databases from getSampleList(),
  which keys samples on organismAbbrev + datasetClassCategory + experimentName. The
  new dnaseqExperiment class carries an empty category while its samples span two
  ("Genetic variation" for SNPs, "Structural variation" for CNVs), so the key cannot
  match and getSampleList() throws "No sample names found for dataset ...".
- addModelReferences() registered SnpQuestions / SnpRecordClasses references. That XML
  is not in the compiled model and is to be superseded by the variation record.

Both bodies are commented out rather than deleted, and the class stays in place as a
no-op so the presenters naming it keep resolving. getPropertiesDeclaration() is left
alone: presenters still supply hasCNVData, and DatasetInjector.addPropValues validates
supplied props against the declaration.

Note the deeper issue for whoever writes the replacement: keying the experiment ->
sample lookup on a single datasetClassCategory cannot work for an experiment that
aggregates SNP and CNV samples, even once dnaseqExperiment declares a category.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9968576)
Commit 643ddb4 commented out the CommunityCount intermediate table and the
communityCount column of OrganismAttributes in apiTuningManager.xml (it read from a
hardcoded usercomments_userdb_betan.mappedComment), but left every consumer in place,
so model load failed with:

  Database error while attempting to parse sqlQuery OrganismAttributes.organismAttrs:
  ERROR: column oa.communitycount does not exist

Consumers brought into line with the producer, following the snpcount/popsetcount
precedent already in these files:

- organismAttributeQueries.xml: the communitycount column declaration and its select
  expression are commented out.
- organismRecords.xml: the communitycount columnAttribute is commented out.
- organismQuestions.xml: communitycount removed from five summary attribute lists,
  which is mandatory since WDK resolves every attribute a summary names.

community_flag is kept, as a literal 'none' rather than derived from the missing
column. That is exactly what "CASE oa.communitycount WHEN 0 THEN 'none'" produced for
a zero count, so the organism page renders as it would with no comments, rather than
losing an attribute that organismRecords.xml:305 and its red-dot <img> still use.
Deriving it from NULL would have been worse than removing it: CASE NULL WHEN 0 falls
through to ELSE, so every organism would have advertised comments it does not have.

The individuals.txt ontology entry is left alone, matching how snpcount and
popsetcount were handled: the category tree tolerates entries for absent attributes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 60a3e36)
jbrestel and others added 13 commits July 31, 2026 20:28
These two questions were written by hand in geneQuestions.xml while the attribute
their summaries name is generated by the presenter:

  summary="...,exprGraphAttrpfal3D7_microarrayExpression_Derisi_TimeSeries_RSRC_expr_graph"

Expression.java:136 builds that name and injects the textAttribute that defines it. So
a hand-written question depended on generated output, coupled only by a name string
with nothing enforcing that both exist. On an instance where this dataset is not
loaded, the presenter is skipped and the question survives, referencing an attribute
nobody generated:

  Summary attribute field [exprGraphAttrpfal3D7_..._expr_graph] defined in question
  [GeneQuestions.GenesByMicroarraypfal3D7_..._RSRC] is invalid

The questions are curated rather than generic -- they use the PFTimeSeries vocabulary
queries, an extra samples_fc_comp_generic param, and fold_change_chosen_display in
place of fold_change_chosen -- which is why the presenter sets hasMultipleSamples,
hasPercentileData and hasPageData to false and why they were hardcoded in the first
place. So they move to their own microarrayDeRisiTimeSeries.dst rather than being
folded into the generic expression templates, and MicroarrayDeRisiTimeSeries injects
them. Question and attribute now appear and disappear together.

Only three values are parameterised -- ${datasetName} for the question and graph
attribute names, ${includeProjects} and ${includeProjectsExcludeEuPathDB} for scoping,
both already set by Expression.java to precisely the literals that were hardcoded.
Display strings stay literal because they describe this experiment. addModelReferences
now derives the question names from getDatasetName() rather than spelling out the
dataset, so the references cannot drift from what is injected.

Not yet verified: that the generated question is equivalent to the deleted static XML.
Nothing is generated on an instance where this dataset is absent, so this branch can
only show that its absence is handled. Before this reaches a full-data site, run a
build with the dataset declared and diff the generated question against the static
version in this commit's parent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 28b06e4)
CompoundQuestions.CompoundsByFoldChange named one dataset's graph column in its summary:

  summary="...,metaboliteProfiles_LlinasMetabolites_RSRC_metabolite_graph"

That attribute is injected per metabolomics dataset by metabolomics.dst, so naming a
particular dataset's copy in a generic search has two consequences: model load fails
wherever that dataset is not loaded ("Summary attribute field [...] is invalid"), and
where it is loaded the search shows that one dataset's graph regardless of what the
user searched for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit e785390)
End-to-end verification failed with PSQLException "syntax error at or
near WITH".  Cause: two comments in the attribute query mentioned the
##WDK_ID_SQL## macro by name.  WDK substitutes it by blind textual
replacement, so it expanded inside the comments too, and because the
injected SQL is multi-line it escaped the '--' and corrupted the
statement.  One of those comments was added during review to document
that row counts match the injected id SQL -- a comment written to
explain correctness is what broke the query.

Notably wdkQuery -showQuery cannot catch this: standalone it never
expands the macro at all, which this plan had already recorded as
correct behaviour.  Only a real answer run reaches it.  Added a warning
at the top of the query telling the next person never to write the macro
name in a comment here.

Also corrected the spec's claim that the pipeline emits one merged
multi-strain FASTA.  The delivered artifacts are one gzipped file per
strain; deflines verified byte-identical to the chrom this record emits.
Established by reconstructing strain sequence from reference with
samtools and comparing against the real consensus FASTAs: the only
deletion offsets that reproduce the observed strain sequence start at
location+1, never at location.  Verified on two independent events
(A17-10A-1 offset 1207 -> valid 1208-1212; E-1-75s-2 offset 2492 ->
valid 2493-2495).

This confirms rather than contradicts the < / <= asymmetry in section
5.3: an event whose location equals refStart deletes bases starting at
refStart+1, so it is genuinely inside the segment and must shift the end
but not the start.

Also records two caveats the exercise exposed.  Event position is
ambiguous within repeat context (a 5-wide window of offsets reproduces
the same consensus), which is the honest answer to the deferred
refStart-boundary question -- an event straddling a boundary has no
exact strain coordinate because the reference base does not exist in the
strain.  And only the net shift is meaningful: on an insertion region a
clustalo alignment resolved recorded events -1,-6,+31,-1 as -1,+25,-1,
identical in sum but not one-to-one.
The EXISTS gate in StrainSegmentId.StrainSegmentsByRefSegment matched on
na_sequence_id, so it did two jobs: prove strain and sequence are mutually
consistent (required, and what lets this search drop the organism param) and
prove indel data exists on that exact contig (not required, and wrong).

Zero indels on a contig is an ordinary valid state, not missing data: the
strain matches the reference there, so strain coordinates equal reference
coordinates and the contig is present in the strain's consensus FASTA. Those
requests returned an empty result: 689 of 6,068 valid strain/sequence pairs
(11%). Surfaced by A17-10A-1 + mito_A_fumigatus_Af293, whose defline is in
A17-10A-1_consensus.fa.gz.

Match on taxon_id instead. The gate now proves the strain was sequenced
against this organism. The substitution is exact, not approximate: (strain
name, taxon) resolves to exactly one protocol app node in 452/452 pairs and no
protocol_app_node_id spans more than one organism. The attribute query needs
no change; its LEFT JOIN + COALESCE already yields the identity mapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plain-text summary of Task 7 verification against the running site:
service checks, BED coordinates for five strains on Chr1 and mito, the
clustalo alignments confirming the recorded indels, the unshifted
control, and the VCF-anchor-base finding.
The report's audience reads the alignment against the reference and
never sees apidb.indel, so the 3-4 bp difference between a recorded
indel location and the aligned gap position is internal noise that reads
as an unexplained discrepancy.  Removed it and the anchor-base
derivation, keeping the parts that describe the output: gap sizes match
the recorded shifts, and a segment boundary falling inside a deletion
has no exact strain coordinate.

The full derivation stays in the design spec, where it belongs.
…ones

Documentation-only pass over the organism-scoping change; no SQL semantics
touched. Two code reviews agreed the SQL is right and every finding was a
number or a comment that described a different database.

- Separate unidb_shu_a (live appDb) from genomicsdb_rebuild01 everywhere the
  two were conflated: strain-name ambiguity (0 of 452 live vs 126 on
  rebuild01), colon-bearing source_ids (0 of 160,581 vs 10,704), indel-bearing
  sequences (120 vs 20,823), row counts (1,855,449 / 1,356 vs 43.6M / 61,512).
- Enumerate all FIVE ID-query gates, in the spec's numbering, in both the spec
  and the XML header, so a cited "gate 3" means the same gate in both.
- Measure and record the REJECT path, which the old "0.35 ms, no sequential
  scan" claim omitted: ~101 ms vs ~53 ms for the largest strain, scaling with
  the strain's indel row count. Record the rejected LIMIT 1 rewrite and why
  (it trades slow for silently wrong).
- Replace the Af293-only safety evidence with a three-organism check including
  TREU927, where 144 newly-admitted pairs sit on 36 never-indeled contigs and
  all 131 deflines match the DB set exactly.
- Reframe the residual assumption as set equality that can drift from the DB
  side too, and add it to the section 10 risk register.
- Fix the stale param comment describing the removed exact-sequence gate, the
  "8 Af293 chromosomes" (it is 9), and a below/above cross-reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut roughly a third, and lead with what the thing actually is: a search
taking a strain name plus reference coordinates as input and returning
the segment in that strain's coordinates.  Previous version described
the internals before saying what was being run.
Same content, rendered: tables for the identity figures and fenced
blocks for the BED lines and alignment excerpts, so it reads properly
in the PR and on GitHub.
StrainSegmentId.StrainSegmentsByRefSegment took a single strain, so a
five-strain BED meant five searches.  The strain param becomes multiPick and
the ID query crosses the sequence with the unnested strain list, minting one
primary key per strain.  Downstream was already multi-record capable
(StrainSegmentAttributes.Coords keys on source_id; BedReporter streams one
line per record) and is untouched.

quote="true" is what makes this work: on PostgreSQL, EnumParamHandler +
DBPlatform.prepareExpressionList render the selection as 'a','b','c', which is
exactly an ARRAY[...] element list.  Verified against the running service.

The organism gate keeps pan.name on the bare side of an equality
(CONCAT(st.strain, '_Indel')) so pan_named_ix stays an Index Only Scan, one
index search per strain, rather than a regexp_replace that would force a scan.
Gates now run per (sequence, strain) pair, so an irrelevant strain drops out of
a mixed selection instead of failing the search.

maxSelectedCount="500" is a correctness rail: above the whole 452-strain
vocabulary on unidb_shu_a so it refuses nothing today, but below Oracle's
EXPRESSION_LIMIT of 999, past which prepareExpressionList emits a form
ARRAY[...] cannot consume.

Verified end to end against jbrestel.fungidb.org (bed reporter): 5 strains on
Chr1_A_fumigatus_Af293 396000-399000 reproduce the known single-strain
coordinates exactly; 1 strain unchanged; 5 strains on mito 500-2000 all
identity-mapped; a PlasmoDB sequence yields 0 rows; an out-of-organism strain
mixed with two valid ones yields 2 rows; all 452 selected yields 232 distinct
rows in ~2.3 s.  Cost on unidb_shu_a: 1 strain 238.24 / ~0.35 ms, 5 strains
939.65 / ~0.61 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passing a real JSON array returns 400 '"strain" is not a string
(class org.json.JSONArray)'.  Worth stating in the report because it is
the first thing a caller gets wrong, and the multi-strain search is
useless until they get it right.
The mito table reported E-1-75s-2 at 99.933% but the excerpt (columns
401-460) contained no difference, so the report asserted a mismatch the
reader could not see.  99.933% of 1501 is exactly one SNP; it is at
column 700, T -> A.  Swapped the excerpt for a window containing it and
marked the position.

Also stated once that identity is measured over the whole alignment
while the blocks are windows chosen to show a feature -- which is what
made the discrepancy confusing rather than obvious.
@jbrestel
jbrestel marked this pull request as ready for review August 1, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant