Skip to content

feat(apollo): rebuild the Apollo release package as a tested CLI - #224

Open
jbrestel wants to merge 43 commits into
masterfrom
feat/apollo-configs
Open

feat(apollo): rebuild the Apollo release package as a tested CLI#224
jbrestel wants to merge 43 commits into
masterfrom
feat/apollo-configs

Conversation

@jbrestel

Copy link
Copy Markdown
Member

Replaces the hand-maintained script that built the Apollo release package. That
script kept its thresholds, argument handling and rename detection in a file in
a home directory, so nothing ever exercised them — it produced an empty release
for build 71 and exited 0.

Model/bin/createApolloReleasePackage is now thin wiring over
ApiCommonModel::Model::ApolloRelease::*, where every rule with a wrong answer
worth catching is a testable class method.

Two phases

  • --report — minutes, changes nothing. Portal + Apollo + overlay + rename
    resolution + reconciliation, printed. This is what goes to the curation team.
  • --generate — hours. Builds the package for the approved roster and writes
    the command files a human then runs against Apollo.

It never calls a mutating Apollo endpoint. The step between "computed" and
"applied" is always a file a person reads.

Modules

Portal the UniDB organism list, normalised; carries both abbrevs
Apollo the live roster — the SEED for the release
Overlay the human add/remove decisions, reason mandatory
Reconcile set logic → update / add / prune / rename / exception
Rename assembly identity from the .fai, for what the database cannot explain
Absolutize site-relative → absolute URLs, with a mandatory post-condition
Generate the per-organism package
Commands the curl and groovy command files
Report the artifact the curation team approves from
Cli options, preflight, sanity gates, rename resolution

Verified against production

A real --report reconciled the whole roster exactly: 453 update / 34 add /
1 prune / 1 rename / 4 exception. It detected the cneoJEC21 → cdenJEC21 rename
from the database with its 14 annotations preserved, and declined the
cglaCBS138 → nglaCBS138 merge — nglaCBS138 is already in Apollo, and
performing that rename would leave two Apollo organisms sharing one directory,
which Apollo.pm refuses to load on the next release.

508 tests across 11 files.

The failure modes this is built around

  • An add plus a prune for the same genome — creates an empty Apollo organism
    and orphans the curation work, from the same API and the same inputs as the
    correct behaviour. Reconcile::assertInvariants guards it and is deliberately
    noisy.
  • An update silently re-publishing hidden organisms — the previous script
    hardcoded publicMode: true. Visibility is now echoed back from Apollo, and
    absent it, the emitter dies rather than defaulting.
  • An empty release that exits 0assertPortalSane, assertApolloSane,
    assertUpdateBucketSane and assertRosterNonEmpty. The last one takes no
    force argument, deliberately: --force accepts omitted candidates and an
    empty update bucket, but never licenses a package with nothing in it.
  • A silently missed URL rewrite — a track that 404s inside Apollo loads,
    appears, and is empty. Absolutize::assertNoRelative is the post-condition
    for rewrite, sharing one pattern so the two cannot drift.

Known open, not addressed here

From a CLI review, all pre-existing and none blocking a run:

  • A rename bypasses Portal::qualifies, so an organism demoted this build would
    still be generated and repointed rather than surfacing as an exception.
  • report.txt / report.tsv are written after generation, so an hours-long
    run that dies late leaves no paper trail.
  • narrowResult's prune filter is unreachable from the CLI — generationRoster
    rejects a prune abbrev first — so a partial --generate always emits zero
    approved prunes.
  • PORTAL_FLOOR makes --project unusable for non-UniDB models.
  • _write uses >:raw, so a non-ASCII organism name lands as invalid UTF-8 in
    the archival report.

Note for the reviewer

e8670e13a ("fix simple error in TranscriptsFromGenes SQL", one line in
geneQueries.xml) is the base commit of this branch and is unrelated to Apollo.
It rides along because the branch was cut on top of it. Say if you want it
split out.

🤖 Generated with Claude Code

John Brestelli and others added 30 commits August 21, 2026 16:57
jbrowseOrganismList did not compile: line 11 passed an undeclared
$organismAbbrev to the JBrowseUtil constructor under use strict, so
/service/jbrowse/organismList returned a Perl error with HTTP 200.
getDbh only needs projectName.

Two further defects in that script are documented but not yet fixed
(they need the same pass): $historySql joins to a nonexistent alias
`nt`, and addHistoryToOrganism reads Oracle-cased hash keys that
DBD::Pg returns lowercase -- so HISTORY is silently always absent.

Spec covers rebuilding createApolloReleasePackage against the UniDB
portal on cedar, seeded from live prod Apollo rather than a frozen
build-68 organism list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yew's faToTwoBit is a 2016 build against libssl.so.10 / libpng15,
none of which exist on cedar (Rocky 9), so it cannot be copied.
Installed UCSC's current linux.x86_64 build to ~/bin.

Verified the two produce byte-identical output (md5
a8b9cb95a7794c1ed7dcc3568edeaff9) over the cneoJEC21/cdenJEC21
genome, which also confirms the rename at the byte level.

Measured 0.17-0.74s per genome, so the spec now says always
regenerate rather than copying .2bit forward between releases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Release-68 is not a valid baseline. Between b68 and b71 the track
layer moved REST -> flat files, the database moved Oracle ->
Postgres, and many track definitions changed; output matching
release-68 would indicate a bug, not correctness.

Verification is now equivalence with what the portal serves today
for the same organism, where every difference must fall into one of
four declared classes (absolutization, refseq track swap, include
rewriting, deliberate removals). Adds a track-count floor and URL
liveness sampling, since empty-but-valid JSON is the characteristic
failure of the flat-file migration.

Release-68 is retained only as the file-name inventory and for the
faToTwoBit reproducibility check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 tasks, TDD throughout, against the 2026-08-21 design spec.
Reconciliation is pure and fixture-tested; only Portal, Apollo,
Generate and Commands touch the world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The history query joined to an alias 'nt' absent from its FROM clause,
so it threw while the script still exited 0 and printed JSON with no
history. addHistoryToOrganism then read Oracle-cased keys that DBD::Pg
returns lowercase. Together these meant HISTORY was always missing, and
with it the annotation version every Apollo organism name depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The string "0.0" is true under Perl's boolean rules but zero numerically, so
'? 1 : 0' on a flag would silently invert it. Coerce via looks_like_number and
die on a non-numeric flag rather than guess. Compare build_number numerically
by intent, skipping a non-numeric row loudly (stderr is fatal for the caller).

Expose normalise() as the public seam so tests exercise the real rules without
a fixture, subprocess or database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The synthetic history used 19.9 and 71, but "71" gt "19.9" lexically too, so
the old string comparison would have passed it. Add a build_number of 9, which
is lexically larger than both and numerically smaller: lexical ordering now
picks 'older' and only numeric ordering yields 'newest'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Organisms are keyed by the abbrev parsed from Apollo's directory field,
never by commonName, which curators can edit in the GUI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured against prod: 17 organisms have publicMode=false, and 16 of
them are on the portal with 15 qualifying as reference+annotated. They
are curator-hidden, not retired -- the workflow Paul's notes describe
for hiding an organism from users while admins retain access. Three
carry annotations (iscaPalLabHiFi 20, treeQM6a 11, etenHoughton2021 2).

Paul's updateOrganismInfo curl hardcodes "publicMode":"true", so running
the generated commands re-publishes all 17. The plan reproduced that
bug faithfully.

Update and rename now echo back the visibility Apollo currently holds,
and refuse to run when it is unknown rather than defaulting. Only an
approved prune may change visibility.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the third hardening behaviour, which had no test. Verified by mutation:
deleting the looks_like_number guard makes test 13 fail.

The obvious form of this test does not discriminate. A bad row among good rows
passes with the guard deleted too, because 'not-a-number' + 0 is 0 and any real
build number beats it -- row order does not change that. So the case is pinned
with a second organism whose only history row is the bad one: with the guard it
yields no latest version, without it the row becomes $best and returns 'bogus'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every entry requires a reason. The file it replaces was a hardcoded
__DATA__ block that froze two builds ago, where decisions and staleness
had become indistinguishable.

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

The warning capture apparatus asserted nothing, so deleting the warn in
Apollo.pm would not have failed the suite.  Assert the offending Apollo id
appears -- the actionable content -- rather than the surrounding prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
loadFromCommand treats any stderr byte from its child as proof the output is
untrustworthy, while the module itself warned to stderr from the build-number
skip path. A caller applying this module's own policy to it would read a
handled skip as a release-breaking error. Warnings are now collected and
exposed via warnings(), reset per normalise() call; the caller decides whether
to print. Matches the pattern specified for ApolloRelease::Rename.

Also: capture jbrowseOrganismList's stderr to a File::Temp rather than a fixed
/tmp path (concurrent runs could truncate each other, and the size check is
only meaningful if nothing else can write the file), keeping it on the failure
paths for inspection; shell-quote the project name, which reaches the shell
from a --project flag; die on a duplicate organism_abbrev rather than silently
discarding an organism, as the sibling Apollo module does; and warn rather than
die on a missing flag, since a NULL column should not stop a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update and rename entries carry the organism's current publicMode so the
command generator can echo it back; 17 live organisms are deliberately
hidden and must not be re-published by a routine update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconcile now reports overlay entries that no longer have any effect --
an add for an organism already live, or a remove naming an abbrev in
neither Apollo nor the portal. Fatal would break a release over file
hygiene; silence would discard a recorded human decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s itself

A stray trailing byte on Apollo's directory field yielded the abbrev
'tgonME49 ', which matches no portal organism -- reconciliation would then
report the real genome as an add and the tainted one as a prune, the
add-plus-prune-for-one-genome case this project exists to prevent.  Trim
surrounding whitespace along with trailing slashes, and validate the abbrev's
shape so interior junk is skipped rather than admitted.

Guard decode_json so an HTML login page served with a 200 status reports its
source and likely cause instead of a bare 'malformed JSON string'.

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

Parse failures now name the actual defect (missing organism vs missing
reason vs unparseable) instead of always blaming the reason. Duplicate
and contradiction errors cite the earlier line as well as the current
one. The shipped-overlay test reads the repo copy, so it no longer
depends on a prior bld into a shared GUS_HOME.

Return shape unchanged: {add,remove}{abbrev} stays a reason string,
which Reconcile.pm prints directly. Line numbers live in a private hash.

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

Add treeQM6a (real prod data: publicMode false, 11 annotations, on the
portal) to both fixtures.  Every previous publicMode assertion used a
visible organism, so hardcoding public_mode => 1 -- the bug that would
re-publish 17 deliberately hidden organisms -- passed all 26 tests.  Both
the update and rename paths are now mutation-checked.

Also: refuse a rename onto an abbrev Apollo already holds (a merge, not a
rename, and the same duplicate-directory corruption the two-sources check
prevents), and consult the rename map when reporting redundant overlay
lines -- a rename consumes both abbrevs and silently disarmed a line
naming either end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Species taxon ID cannot be used: it changed in both known renames.
Strain abbrev narrows candidates, identical sequence names and lengths
decide, and an ambiguous match produces no rename rather than a guess.

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

A text pass rather than a typed transformation: ~25 of these URLs live
inside HTML blobs and JavaScript function bodies where a typed
getApolloObject cannot reach. The previous script did the same rewrite
with no verification, so a missed URL became a silently empty track.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hanged genome

Three review findings:

- readFai warned only on a malformed line, not on an unopenable file.  A
  permissions problem or a missing directory degraded silently to "no
  match", which sameAssembly cannot distinguish from a different assembly.
- The ambiguity test alternated "names both candidates" with "says it is
  refusing", so a generic refusal naming neither would have kept it green.
  Split into two assertions and confirmed by mutation.
- A candidate skipped for a missing current index fell through to "no
  portal organism shares its assembly", which is false and invites
  approving a prune over a build problem.  Checked and uncheckable
  candidates are now counted and reported separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion shape

Absolutize now validates the base so its idempotency guarantee is true
rather than approximately true. Rename test 6 required the abbrev but
not the reason. Reconcile exception entries now carry public_mode for
symmetry with update and rename.

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

Task 9 measured every producing script against three organisms on
build 71. The five track producers are sound -- clean exits, zero
stderr, plausible counts across two component databases including
partitioned pfal3D7. Build on them.

Two defects found:

  16. jbrowseRefSeqs is dead for every organism. e0e9a61 commented
      out the getCacheFile/setCacheFile accessors but left two callers.
      Served through responseFromCommand, so it corrupts the response
      body rather than just logging.

  17. The two script families disagree about which organism abbrev they
      take. apidb.organism holds abbrev=cneoJEC21, public_abbrev=
      cdenJEC21; jbrowseTracks consumes the public one, the five track
      producers consume the internal one.

Also measured: no organism carries a [tracks.refseq] stanza, so the
planned strip step currently has nothing to strip.

Findings salvaged from a stub Task 9 created in the wrong repo.

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

e0e9a61 ("comment out stuff to do with CACHE") commented out the
getCacheFile/setCacheFile accessors and the setCacheFileName() call in
new(), but left printFromCache() and setCacheFileName() calling them.
Perl resolves methods at run time, so nothing complained until the
script ran: jbrowseRefSeqs died at its first statement with

  Can't locate object method "getCacheFile" via package
  ApiCommonModel::Model::JBrowseUtil at .../JBrowseUtil.pm line 419

exit 255, 211 bytes on stderr, for every organism and both types since
2025-02-26. These scripts are served through responseFromCommand, which
merges stderr into the JSON response body, so this was corrupting the
live payload rather than only logging.

Removed rather than restored. The disable was deliberate; the only
caller of printFromCache is jbrowseRefSeqs, and the only caller of
setCacheFileName was the line new() already commented out -- so the
whole path is unreachable and restoring an accessor would merely re-arm
dead code. Worse, the cache it would re-arm has no invalidation: it
wrote _refSeqsCache.json into GUS_HOME once and returned it forever,
outliving the build whose sequence names and lengths it held. The
queries are two indexed lookups; there is nothing here worth caching
incorrectly.

Model/t/jbrowseutil.t pins it: every $jbrowseUtil->method() call in the
shipped Model/bin/jbrowse* scripts must resolve against the package
(scanned, not hand-listed, so a new script is covered for free), and the
four cache subs must stay absent.

Measured after installing, model UniDB, build 71, zero bytes on stderr
and exit 0 in all six runs:

  tgonME49  genomic 2265 entries   protein 8204 entries
  pfal3D7   genomic   16 entries   protein 5389 entries
  cneoJEC21 genomic   14 entries   protein 6862 entries

Each entry is {name, start, end, length}. Suite: 188 tests, all pass.

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

apidb.organism holds two identifiers and the jbrowse scripts disagree
about which they consume: jbrowseTracks selects `where public_abbrev = ?`
and returns the internal one, while the five track producers
(jbrowseRnaAndChipSeqTracks, jbrowseRNASeqJunctionTracks,
jbrowseOrganismSpecificTracks, jbrowseDNASeqTracks) take the internal
abbrev and key auto_generated/<abbrev>/ off it. Feeding a producer the
public abbrev gives exit 2 on a missing datasetAndPresenterProps.conf.

jbrowseOrganismList now also selects `o.abbrev as internal_abbrev` and
Portal carries it through normalise, so the release tool can resolve
public -> internal once and hand each script the abbrev it expects.

organism_abbrev stays the PUBLIC abbrev and remains the hash key: the
Apollo roster, the overlay file and reconciliation all key on it, and it
is what the portal shows. Nothing existing moved.

Measured on live UniDB, build 71, through Portal->loadFromCommand:
831 organisms, exit 0, zero bytes on stderr, zero warnings, and
internal_abbrev defined for all 831. **37 organisms have
internal_abbrev ne abbrev** -- that is the live population where the
trap bites, not just the one Cryptococcus. Several are cross-genus
renames (nglaCBS138 -> cglaCBS138, pory70-15 -> mory70-15) and one is
mere case (scerS288C -> scerS288c), which no eyeball would catch.

Model/t/fixtures/portal.json regenerated from the live command rather
than hand-edited, same nine organisms. Two of them now exercise the
split: cdenJEC21 (cneoJEC21) and nglaCBS138 (cglaCBS138). portal.t pins
cdenJEC21's internal_abbrev and that its public abbrev is unchanged.

Suite: 191 tests, all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured: 37 of 831 organisms have an internal abbrev differing from
their public one, and of Apollo's 459 directories, 457 name a public
abbrev, 2 name an internal abbrev whose public has changed, and 0 name
neither. Both live renames therefore fall out of the database exactly,
with no file I/O.

Assembly identity stays as the fallback for an orphan the database
cannot explain -- an organism whose internal abbrev also moved. The
Rename module is unchanged; the caller resolves the database renames
first and passes only the residue to it.

Also recorded: two case-insensitive collisions exist in the abbrev
namespace (scerS288C/scerS288c, and one under bnonp57), so every
comparison must be case-sensitive. A lc() here merges two organisms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both JBrowseService endpoints that called jbrowseRefSeqs are commented
out, with the note "THIS SHOULD BE REPLACED BY INDEXED FASTA IN
WEBSERVICES", and nothing else references the script. It was already
dead when it broke -- which is why nobody noticed for six months.

Corrects an earlier claim in this branch that the breakage was
corrupting the live site's response payload. It was not; the endpoint
serves nothing.

The package does not need it either: release-68's trackList already
pointed refSeqs at seq/<abbrev>.fa.fai with an IndexedFasta store, and
the refSeqs.json in seq/ is referenced by nothing. The tool now derives
it from the .fai it already copies -- same shape, no database round
trip, no dependency on a dead script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ip and has junk

20. Apollo's Organism record points at seq/<abbrev>.fa relative to its own
    data dir and derives its sequences count from that index, while genomic
    sites fetch the reference through the store service. The store location
    genuinely differs, so the package must ship its own copy.

21. The refseqs track is no longer in tracks.conf -- it is in
    organismSpecific.json, pointing at the store URL, and must be stripped
    there instead or Apollo ends up with two reference tracks.

    While measuring that: addChipChipTracks pushes the return value of
    makeChipChipPeak/makeChipChipSmoothed, which already push and so return
    the new array length. 56 bare integers among 158 track entries for
    tgonME49, 28 of 103 for pfal3D7. That endpoint is live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both fixes were made off-branch and pushed to master; bringing them in so
the branch builds against them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
John Brestelli and others added 13 commits August 21, 2026 22:45
Resolves the public/internal abbrev split -- the track scripts take the
internal abbrev, output paths and Apollo directories use the public one.
Strips the store-URL reference track from organismSpecific.json so Apollo
uses its own local copy, and derives refSeqs.json from the .fai rather
than the orphaned jbrowseRefSeqs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JBrowse/bin/dumpConfigurationsForApollo.pl is the 2021 ancestor of the
script being replaced; Paul's copy forked from it into a home directory
and drifted there while the in-repo original rotted. That is precisely
the failure section 8 corrects by putting the tool back in the repo.

Its history also settles apollo_gene_tracks.conf: the in-repo version
includes it unconditionally, and the 2021 "only dump annotated genomes"
commit is the TODO being satisfied. The gate now lives in
Portal::qualifies. The file is static, checked in, and required -- it
defines the Draggable Annotation track curators drag genes into.

Also marks the organismSpecific track counts in the section 10 table as
superseded by the addChipChipTracks fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the [tracks.refseq] strip that spec section 6 states and section
10 warned not to assume away. Census on cedar (build 71, 2026-08-21): 0 of
835 auto_generated tracks.conf files carry the stanza, so it is a no-op
today -- shipped anyway, with the census recorded in the code, because an
unimplemented stated requirement reads to the next person exactly like a
forgotten one. Line based rather than the previous regex, whose character
class stopped at the first comment inside the stanza.

Covers generateOrganism, including the public/internal abbrev split that
affects only 37 of 831 organisms and so survives any spot check. Verified
by mutation: feeding the track producers the public abbrev, and writing
output under the internal one, each fail the suite.

Tests assertToolsAvailable, and records in a comment that nothing here
calls it by design -- the CLI does, once, before the loop.

Replaces a vacuous assertion: the bad-entry fixture had its index equal to
the bad-entry count, so qr/\b1\b/ matched the count and deleting the whole
'at index(es)' clause left it green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Visibility is echoed from what Apollo currently holds, never defaulted;
a rename targets the existing organism id so its annotations survive;
prune unpublishes rather than deletes; passwords are emitted as a shell
variable, never interpolated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…vy URLs

The command files now carry a header and a WARNING line above each
annotated prune, so the plan's line-count check must ignore comments.

Also records a pre-flight check: add_organism.groovy is invoked with
-url and alter_group_permissions.groovy with -destinationurl plus a
trailing slash. Both came from the legacy script, which is inconsistent
the same way, and neither groovy script is in these repos. An add is the
one non-idempotent command, so this is worth confirming before the first
real one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Designed for the real asymmetry: 457 routine updates against 2 renames
and a handful of decisions that need a human. The rows that matter must
not be buried by the rows that do not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Strain and isolate names carry punctuation and the portal is the source
of that string, so an apostrophe reaching a --data line is reachable,
not hypothetical. Pins that a single quote, a double quote and a
backslash each refuse to emit, that the error names the organism and
shows the offending character, and that the shell-quoting and JSON
diagnoses are distinguishable. Also pins that ordinary punctuation is
still accepted, so the guard cannot widen into general distrust.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pendingDecisions derived annotated_prune from the already-filtered
unapproved list, so the field disagreed with the report it summarises:
the text flagged two organisms losing annotations while a caller reading
the field was told one. Approval gates the action, not the consequence.

The banner warning moves out of the pending branch for the same reason --
an approved annotated prune is exactly the case where the release looks
ready to run unread.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves renames from the database first and falls back to assembly
identity, refuses absurd input rather than producing an empty release,
and will not generate while human decisions are pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both gaps were invisible to a green suite: disabling the assembly-path
merge decline, or hardcoding narrowResult to drop every add candidate,
left all 97 cli tests passing.

Also removes an unreachable guard in resolveRenames rather than pinning
it with a test that could only assert an equivalence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A gate that dies must not swallow the evidence for its own message.
assertPortalSane, assertApolloSane and assertUpdateBucketSane all ran
before _printWarnings, so a NULL is_reference column -- one warning per
organism, then an empty update bucket -- died telling the engineer to
check --project while the warnings naming the real cause went unprinted.
Portal.pm collects rather than writing to stderr precisely because its
caller distrusts a child's stderr byte, which makes printing them this
script's job and the order load-bearing.

Also gives Apollo the single apiUrl() the CLI labels its report from.
The default was written twice -- once to fetch, once to label -- so the
two could drift and the report would name a host it never read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
assertUpdateBucketSane's own error recommends --force, and that override
carried all the way through: generationRoster returned nothing, the run
wrote empty command files and exited 0. A wrong --project that still
clears the portal floor reached it. That is the silent empty release this
tool was written to replace, reachable from the message suggesting the
flag.

assertRosterNonEmpty takes no force argument, unlike the two gates either
side of it. The asymmetry is the fix, so a test pins the signature: a
later consistency edit that adds one turns red rather than reopening the
hole. Called before make_path so a refused run leaves no half-created
release directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comments only -- every changed line in this commit begins with '#', and
the suite is unchanged at 508 tests.

Three passes: drop what the code already says, compress multi-paragraph
rationale to the two or three lines that carry the reason, and strip
measured-today specifics (organism counts, roster sizes, dated censuses,
named ids and annotation counts) that were accurate when written and go
stale silently. Where a number justified a decision -- the portal floor
sitting just above the live Apollo roster -- the relationship is kept and
the figure dropped.

Rationale that stops a later edit from quietly weakening a guard is kept:
the two-abbrev split, the shared Absolutize pattern, why assertRosterNonEmpty
takes no force argument, why several tests assert a pair rather than a
branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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