Skip to content

Merge 9.0.0 back into develop and open 9.1.0-SNAPSHOT - #52

Merged
itpragmatik merged 2 commits into
developfrom
merge-back/9.0.0
Aug 16, 2026
Merged

Merge 9.0.0 back into develop and open 9.1.0-SNAPSHOT#52
itpragmatik merged 2 commits into
developfrom
merge-back/9.0.0

Conversation

@itpragmatik

Copy link
Copy Markdown
Contributor

Steps 4 and 5 of the release process, in one motion: main merges back into develop (so the next release cannot silently revert the version bump), and the version conflicts resolve directly to 9.1.0-SNAPSHOT. VERSIONING.md gains the finding this first release surfaced: the merge back must be a true merge commit, because the containment check — every commit on main reachable from develop — can never be satisfied by a squash. This PR must be merged with a merge commit, not squashed; the merge method is temporarily enabled for it.

* docs: fix Java 25 and Maven as the toolchain baseline

Records Java 25 — the current LTS — as the source, target and release
level rather than merely the JDK that happens to build the project, with
Maven multi-module as the build.

The LTS cadence is the reason. Bioinformatics tools tend to be installed
once and left alone for years, so users should land on a runtime that is
still receiving updates whenever they next get around to upgrading.
conda-forge already ships openjdk 25, so declaring it costs nothing at
the distribution end.

Settles decision D-2. This is the last change made directly on develop;
branch discipline starts with the Maven skeleton.

Authored by itpragmatik and an AI agent

* build: add Maven multi-module skeleton (#1)

Fixes the shape of the codebase before there is code to be shaped by it.
A parent POM with groupId com.janookgenomics aggregating janook-core,
janook-cli, janook-store and janook-web, each an empty module that
builds.

janook-core declares no dependencies at all. That is the load-bearing
constraint: it is what lets the classification engine be embedded in
someone else's pipeline without dragging in a persistence layer, a web
server or a CLI framework. The pressure arrives with the criterion
definitions, and both acceptable answers — parsing outside core, or
generating core's criterion table at build time — keep core clean.

janook-store and janook-web are declared now and empty until phases 2
and 4. Two POMs is a cheaper price than revisiting the boundary rules
every time a module is added later.

Every plugin bound to the default lifecycle is pinned, so a clean-clone
build resolves the same versions on any machine and at any future date.

No formatter is configured, so the "fail rather than reformat" criterion
is satisfied by having nothing to violate. Configuring one against zero
Java sources would mean shipping a rule nothing has exercised; it lands
with the contribution surface instead.

Java package directories are deliberately not pre-created — package
naming belongs with the first class, not with the skeleton.

Authored by itpragmatik and an AI agent

* build: enforce the core boundary in the build (#3)

* build: enforce the core boundary in the build

Makes the architecture a build failure rather than a review comment.
Three rules, three mechanisms, because they fail in different ways.

Dependencies (maven-enforcer, bound to validate so a violation costs a
second rather than a full build): janook-core may hold no third-party
dependency at compile or runtime scope, and may not depend on the outer
modules at any scope. Test scope is exempt from the first rule, because
the boundary is about what an embedder is forced to accept and nobody
inherits your test classpath.

Bytecode (ArchUnit): no class in core may reach java.io, java.nio.file,
java.net or java.sql. Reading bytecode rather than imports is the point,
since I/O arrives through a field type or a supertype as easily as
through an import statement.

Sources (a token scan): no species name in core. This one is a tripwire,
not a proof, and the script says so in its header — "no species
knowledge" is semantic and no build can verify it. Naive matching was
unusable, since "cat" sits inside concatenate, category and allocate, so
each line is split on underscores and lowercase-to-uppercase transitions
before whole-word matching. Suppression requires a typed reason: a
justification nobody had to write is one nobody thought about.

The rules live in the parent POM rather than in janook-core so the
tripwire projects inherit and trigger the real rules. A tripwire that
carries its own copy of a rule proves only that the copy works.

Every rule is exercised by something that deliberately breaks it: three
invoker projects for the build-level rules, ArchUnit fixtures evaluated
in-process for the bytecode rule, and fourteen cases for the scan script
covering the false positives a build-level tripwire cannot express.

That mattered. Two defects were caught this way and neither was visible
from a green build. The post-build assertions were named verify.groovy,
which maven-invoker silently ignores — it looks for postbuild — so all
three tripwires were asserting nothing; a deliberately false assertion
still passed. And the tripwire for the outer-module rule was failing on
an unrelated missing script path while still reporting the failure the
harness expected. Both are the exact failure the tripwires exist to
catch, found on the day they were written rather than the day they were
needed.

Package naming is settled as com.janookgenomics.janook.<module>: the
house is Janook Genomics and Janook is the first tool, so the product
belongs in the namespace.

Authored by itpragmatik and an AI agent

* build: add the missing adjectival species forms

Every species in the scan carried its common and adjectival name — cat
and feline, cow and bovine — except chicken and rabbit, which had only
the common form. Someone writing leporineProfile would have gone
unnoticed.

Adds leporine and gallinaceous. Deliberately not avian, which would flag
any bird-related word regardless of species and is the kind of noise
that gets a check switched off.

The gap is a reminder of what the script's own header already says: a
deny-list is a guess about what somebody will type, and will always be
incomplete. It catches the obvious mistake, not every mistake.

Authored by itpragmatik and an AI agent

* ci: run the build and the branching rules on every push (#4)

* ci: run the build and the branching rules on every push

The build is only enforced if it runs somewhere the author cannot skip.

Three workflows.

ci.yml runs on every push and every pull request: the public-safety
check, mvn clean verify, and the species scan self-test. Those are the
commands a contributor runs locally, verbatim. The moment CI runs
something a human does not, a green tick stops meaning "your clone
works" and starts meaning "CI works".

The checkout is deliberately unshallow. The public-safety check reads
every commit message, and a shallow clone would hand it one commit and
let it pass by seeing almost nothing — a check that is technically
running and effectively blind.

ci-uncached.yml runs the same build weekly with no dependency cache and
a relocated local repository. A cache can hide a missing declaration:
an artifact that resolves only because an earlier run left it behind.
Every push then passes and the first person to clone the repository
loses an evening to a failure nobody else can reproduce.

branch-policy.yml mechanises the two gitflow rules that decay first —
that only release and hotfix branches may target main, and that main is
always fully contained in develop. Both are invisible until a release
goes wrong. Branch names reach these scripts through the environment
rather than through template interpolation, since a branch name is
attacker-controlled text and pasting it into a shell is an injection
hole.

Java 25 is pinned explicitly rather than inherited. On at least one
machine here java_home reports 21 while the shell has 25, and a runner
default would differ again.

Authored by itpragmatik and an AI agent

* ci: run the public-safety check's own test suite

CI ran the safety check and the species scan's self-test, but not the
safety check's self-test — the one asserting each of its rules fails
when violated.

That is the gap the self-tests exist to close. A scan that is quietly
misconfigured passes every run until the day it was needed, and the
scan whose job is keeping unpublishable material out of a public
repository is the worst one to have silently blind.

Authored by itpragmatik and an AI agent

* ci: move to actions that run on Node 24 (#6)

GitHub is deprecating Node 20 on its runners. checkout@v4 and
setup-java@v4 both target it and were already being forced onto Node 24,
and setup-java v4 is explicitly end-of-life.

checkout goes to v7 and setup-java to v5. Both v5 majors are the Node 24
upgrade and nothing else; no inputs changed, and this build uses only
fetch-depth. checkout's later majors add credential handling changes and
block fork checkouts under pull_request_target, which does not apply
here since these workflows use pull_request.

Taking the warning now rather than when the forced upgrade becomes a
hard failure and CI stops on a morning when something else is wrong.

Authored by itpragmatik and an AI agent

* docs: add the licence and contribution surface (#7)

* docs: add the licence and contribution surface

The files a stranger reads before deciding whether this project is real,
and before reading a line of code.

Apache-2.0, verbatim from apache.org rather than transcribed. Permissive
because adoption runs through methods-section citations and a licence
that makes a commercial lab take legal advice works against that; Apache
rather than MIT for the explicit patent grant, which matters for a
classification tool a testing company might later claim reads on a
patent. It also matches the licence of the standards ecosystem this sits
beside, so an embedder has one licence to reason about rather than two.

LICENSE and NOTICE are packaged into every jar, not merely left at the
repository root. A jar pulled from an artifact repository arrives without
the repository around it, and Apache-2.0 section 4 requires the notice to
travel with the distribution. The resource path is a plain "..", not
${project.parent.basedir}: resource directories resolve against the
module basedir before that property exists, so the property form survives
into the effective POM uninterpolated and silently copies nothing. That
failed quietly first time and was caught by looking inside the jars
rather than by the build going red.

NOTICE carries the third-party carve-out. The AVCG paper is redistributed
under CC BY 4.0 and is not covered by Apache-2.0; it is included so a
reader checking whether the implementation is faithful has the
specification to hand.

CONTRIBUTING states the build, the checks, the branch model and the
attribution convention. It describes disclosing AI assistance without
naming any product: which tool was used is not a property of the software
and would age badly in a permanent record, while the fact of assistance
is worth knowing.

The code of conduct names conduct@janook.org, which receives, sends,
signs with DKIM and is covered by SPF and DMARC. A code of conduct whose
reporting route does not work claims a protection that does not exist.
It also says explicitly that arguing a criterion is implemented wrongly
is the work rather than a conduct problem.

The bug report template asks for species, version, input file and
observed versus expected classification. A classification bug without
its input is unreproducible in a determinism-critical tool.

Authored by itpragmatik and an AI agent

* docs: attribute copyright to The Janook Authors

A domain cannot hold copyright and Janook Genomics is a name rather than
an incorporated company, so neither can be the holder. Naming one person
reads as a personal project and needs editing the moment anyone else
contributes.

"The Janook Authors" is the convention Go, Kubernetes and Chromium use.
It is not an invented entity — it is a collective reference to the
individuals who wrote the code, which is legally accurate, since
copyright vests in each contributor until assigned. NOTICE says so
explicitly and points at the commit history as the record of who they
are.

The commit attribution line is unaffected: that names the human
accountable for a specific change, which is a different question from
who holds the copyright.

Authored by itpragmatik and an AI agent

* docs: define the version scheme and the release rules (#8)

* docs: define the version scheme and the release rules

Two versions have to be legible in every classification — Janook's own and
the AVCG edition it was produced under — and they move independently. A
result that names only one of them cannot be reproduced later, because
nothing says whether a changed answer came from the tool or the rulebook.

Names the guideline edition AVCG-2024 and pins it to the publication DOI,
since AVCG carries no version of its own and its authors expect revisions.
The DOI is shown as a resolvable URL rather than the doi: prefix form, per
Crossref's current display guidance.

Sets one rule stricter than semver requires: any change to a criterion
weight or the decision tree is a major version. A patch is what people
apply without reading, and a silently altered classification is the worst
failure this tool can have.

Records where gitflow and Maven meet — -SNAPSHOT while work is unreleased,
the bump on the release branch before the merge, so the tag and the
artifact agree by construction rather than by care.

The CLI output and the release check land next; this is what they are
built to. Also drops the stale line in PLAN saying the distribution paths
need an open licence "which is the plan anyway" — that was decided.

Authored by itpragmatik and an AI agent

* docs: make the guideline identifier a name for a DOI, not a year

AVCG-2024 read as though the year were the key. It is not: the year is
what the field says out loud, and the DOI is the only immutable part. A
publication year cannot carry the weight of identifying an edition.

Two collision rules follow. A second full edition in one calendar year
takes a suffix — unlikely on a multi-year revision cycle, but the
identifier must not be capable of colliding at all. More importantly, an
amendment carrying its own DOI gets its own identifier: a rulebook like
this changes by correction, erratum or criterion-specific specification
far more often than by republication, and the real risk is the criteria
moving with no new citation to point at.

Already visible in practice — a 2026 reproducibility study by the working
group makes recommendations against the guidelines, several of which are
reported as being implemented. That is exactly the kind of change a
year-stamped identifier would miss.

Also names the maintaining body correctly: ISAG's Variant Pathogenicity
Working Group, under its Animal Genetic Testing Standardization committee.

Authored by itpragmatik and an AI agent

* docs: cite the working group on recording the guideline edition

The rule that a classification must name the edition it was made under
was reasoned from first principles. It does not have to be: the 2026
reproducibility study recommends it directly, and names two fields beyond
the edition — the date of classification and the references used. Those
belong to the record format, not here, so they are noted rather than
specified.

Also replaces the paraphrase of "the authors expect revisions" with what
they actually wrote: the guidelines are not final and are not expected
ever to become final. That is the load-bearing sentence under the whole
identifier scheme, and it reads stronger in their words than in ours.

Authored by itpragmatik and an AI agent

* build: start the version series at 9.0.0

The project owner wants Janook's first release to be 9.0.0 rather than
1.0.0. Semantic versioning fixes what a bump means, not the number a
project counts from, and nothing downstream disagrees: Maven, Bioconda and
conda-forge require only that versions never move backwards.

This removes the 0.x carve-out rather than renumbering around it. There is
now no phase during which a breaking change may hide in a minor bump — the
rules apply from the first release, so early churn in the input format
goes to 10.0.0 and says so. That is a simpler document and a more honest
number.

Declares 9.0.0-SNAPSHOT across the reactor, including the three tripwire
projects under janook-core/src/it, which inherit the parent version and
would stop resolving otherwise. mvn clean verify passes, tripwires
included.

Authored by itpragmatik and an AI agent

* feat: report the tool version and the guideline edition (#9)

The project's first Java. A classification that cannot name both the
software and the rulebook that produced it is not reproducible, and those
two versions move independently, so the command reports them separately.

The edition lives in janook-core as a record pinned to the publication
DOI. The edition an engine implements is part of what that engine is, not
a detail of how a command prints itself, and an embedder needs it as much
as the CLI does. It costs the core nothing: a string, no I/O, no
dependency.

Reading the tool's own version lives in janook-cli, because it must —
loading a classpath resource is I/O, which core forbids. Maven fills the
resource in at build time so the declared version is the only place a
version number exists; a constant in Java would be a second source of
truth, and the two would disagree on the day a release forgot one.

No argument-parsing library. One flag does not earn a dependency, and the
core's no-third-party rule is easier to hold to if the outer modules take
it seriously too.

The jar carries a Class-Path and the runtime dependencies are copied
beside it. Without that the tests pass — they run against the reactor
classpath, where core is always present — while java -jar throws
NoClassDefFoundError on the first line of real work. A single-file
artifact is a distribution decision and waits for its own epic.

Prints two of the three facts. The build commit arrives with the stamp.

Authored by itpragmatik and an AI agent

* feat: stamp the build commit into the version output (#10)

* feat: stamp the build commit into the version output

The third fact. A version number names a release; the commit names an
exact state of the source, and two jars can both say 9.0.0-SNAPSHOT and
be different software. A dirty tree is marked in the commit string itself
so the marker travels wherever somebody copies the line.

A build with no git history reports "build unknown" rather than failing.
Bioconda builds from a release tarball, which carries no .git, as do
source zips and Docker contexts that exclude it. Failing those builds
would make the tool unbuildable on the path most users install through,
to police a rule that only bites at release time — the release check is
where that belongs, and it can refuse absolutely. Verified by building
from a copy of the tree with .git removed: it builds, and prints unknown.

Printed rather than omitted, because an absent line reads as clean to
anyone skimming.

Resource loading moves to BuildProperties, shared by the version and the
stamp. BuildStamp parses from a supplied Properties so the cases that
only happen in someone else's build — no git, no filtering — are asserted
here rather than discovered by a user.

Still owed, and now written down: a release tarball should carry its own
commit so a Bioconda-installed jar can name its source. That is
distribution's work.

Authored by itpragmatik and an AI agent

* fix: read git natively so a successful build prints nothing alarming

The plugin added in this branch uses JGit, which measures the
filesystem's timestamp resolution by writing .git/.probe-<uuid>, reading
it back and deleting it. On macOS it intermittently loses that race and
prints a FileNotFoundException stack trace after BUILD SUCCESS. It
recurs because JGit caches that measurement in ~/.config/jgit/config and
a machine without that file re-measures on every build.

A stack trace after a successful build is worse than the thing it
reports: it teaches people that ERROR lines are noise, and this project
tells contributors that a green build is the signal.

Shelling out to git removes JGit from the build entirely, and makes the
dirty flag mean exactly what git status means rather than JGit's
approximation — releases are marked on that flag, so the two agreeing is
not cosmetic.

The cost is one environment: a repository present with a broken or
missing git binary now fails the build, because
failOnUnableToExtractRepoInfo does not cover a git command exiting
non-zero. Deliberate. It cannot reach the path packagers use, since a
release tarball has no .git and still reports "build unknown", and what
remains is a broken environment saying so with an actionable message.

All three environments verified by running them, not by reading flags.

Authored by itpragmatik and an AI agent

* feat: fail the build when a version and its branch disagree (#11)

Closes the loop the version scheme opened. A version identifies exactly
one behaviour or it identifies nothing, and until now that rested on
remembering to bump in the right place.

Reads the version out of the built jar rather than the pom. The pom says
what the build was asked to produce; the jar says what it produced, and a
filtering fault, a stale target directory or a packaging mistake sits
precisely in that gap. The jar is also what gets attached to a paper.

Runs on every push rather than only at a release, so a version bumped on
the wrong branch fails while the mistake is one commit old instead of
buried under a merge. What it enforces depends on where the commit lives,
because the rule genuinely differs: -SNAPSHOT is required on develop and
feature branches, forbidden on main, and a release or hotfix branch must
already name the version it is preparing. A tag must match the artifact
exactly, and both main and a tag must come from a known commit and a
clean tree — which is where "a dirty build is not presentable as a
released version" stops being a sentence.

An unrecognised ref is skipped and says so. A check that invents a rule
for a context it does not understand teaches people to work around it.

Exit 2 for "could not run", distinct from 1 for "found a violation", so a
check that cannot read the jar never reads like a check that found
nothing. The self-test covers all 25 cases including that distinction.

Authored by itpragmatik and an AI agent

* feat: encode the AVCG criteria, and publish what was encoded (#12)

* feat: encode the AVCG criteria, and publish what was encoded

The first four of twenty-three, and the whole pipeline that makes the
rest reviewable.

The criteria are Java records in janook-core rather than a data file
parsed at runtime, because core takes no third-party dependency and so
can hold no parser. Definitions are verbatim Table 4 text — the
guideline's words, not ours, quoted under CC BY 4.0.

docs/criteria/AVCG-2024.md is generated from that model and committed. It
is not a second source of truth: the build regenerates it in memory and
fails if the committed copy has drifted, so the file a reviewer diffs
against the paper always describes the engine that actually runs. A human
refreshes it deliberately with -Djanook.criteria.refresh=true. The build
never writes it, because a build that dirties its own working tree flips
the dirty marker and the release check then rejects the artifact.

The four seeded criteria are chosen to exercise every kind of provenance
rather than to be the first four in the table: PVS1 retained but amended
to allow cross-species evidence, PS5 renumbered from ACMG PP1 and
reweighted supporting to strong, PP1 new in AVCG, BP6 renumbered from
ACMG BP7. A shared code is not a shared criterion, and that is the
mistake this audience is most likely to make, so the relationship is
machine-readable rather than a comment.

Weights are stored, not derived. The paper's naming does designate weight
and AVCG renumbered to keep it true — but a stored value is one a
reviewer can check against the table, and a test asserts the two agree,
which is what catches a mistyped weight.

The README says plainly that this is a hand transcription, points at the
generated file, and routes a discrepancy to an issue as a correctness bug
rather than a documentation one.

No structural test here can tell anyone the transcription is faithful.
That is a human reading four rows against page 8.

Authored by itpragmatik and an AI agent

* docs: say which column is the guideline's and which is ours

The definition column is verbatim Table 4. The ACMG/AMP origin column is
not in either paper — it is our annotation, summarising a difference in
our words. Printing them side by side without saying so invites a reader
to attribute our reasoning to the guidelines, which is the one thing this
file exists to make impossible.

The mappings are now checked against the ACMG/AMP source rather than
asserted from the codes. All four hold: ACMG PVS1 ends at "known
mechanism of disease" where AVCG continues into another species; ACMG PP1
and AVCG PS5 are the same sentence at different weights; ACMG BP7 and
AVCG BP6 are the same sentence; and ACMG BP6 is the reputable-source
criterion Table 5 lists as removed.

Adds that source to BACKGROUND's references, with a note that it cannot
be redistributed here — unlike the AVCG paper it is not CC BY, so it is
cited rather than shipped.

Authored by itpragmatik and an AI agent

* feat: encode the remaining pathogenic criteria (#13)

All fourteen are now present. Definitions verbatim from Table 4; each
ACMG/AMP origin checked against Richards et al. 2015 rather than inferred
from the code.

Seven are unchanged from ACMG/AMP: PS1, PS3, PS4, PM3, PM4, PP2, PP4.
Wording differs trivially in places — "compared with" for "compared to",
"non-repetitive" for "non-repeat" — which is not an amendment and is not
recorded as one.

Three were amended, and each amendment is a substantive difference a
reader carrying ACMG knowledge across would otherwise miss:

  PS2  asks for unaffected parental samples that tested negative, where
       ACMG asked for confirmed parentage and no family history. This is
       why ACMG PM6 no longer exists — "assumed de novo" stopped being a
       separate weaker criterion once this one said what counts.
  PM1  assesses benign variation across breeds and/or species. Breed
       structure is what makes an animal population unlike a human one.
  PP3  requires all computational evidence to agree; ACMG accepted
       multiple lines of it. Easy to read past, and it is a real
       tightening.

One was renumbered: PM2 is ACMG PM5, moved because ACMG's own PM2 — an
allele-frequency criterion — was removed, animal populations rarely
having the databases it assumed.

The reference file was refreshed in the same commit. The staleness check
fired first, as designed.

Authored by itpragmatik and an AI agent

* feat: encode the benign criteria and close the inventory at 23 (#14)

Fourteen pathogenic, nine benign. Definitions verbatim from Table 4; each
ACMG/AMP origin checked against Richards et al. 2015.

Five are unchanged: BS2, BS3, BP2, BP3, BP5.

BP4 is amended in the same way PP3 is — all computational evidence must
agree, where ACMG accepted multiple lines of it suggesting no impact. The
pathogenic and benign sides tightened together, and reading either as its
ACMG version would let a variant through on weaker evidence than AVCG
allows.

BS1 is ACMG BS4, renumbered because both ACMG allele-frequency criteria
on the benign side went. Breed structure, founder effects and popular
sires make an animal allele frequency mean something different, and the
population databases those criteria assumed largely do not exist.

BP1 is new, the benign half of the cross-species approach that PP1
introduces. It is also the code most likely to be misread: ACMG's BP1 was
a missense criterion, removed for being too restrictive without evidence,
and has nothing to do with this one.

The inventory test spells the 23 codes out as a literal rather than
deriving them from the model, which would only assert that the code
agrees with itself. Adding, removing or renaming a criterion now fails
until someone changes that list deliberately — a major version under
docs/VERSIONING.md. Separate assertions pin the absence of BS4, BP7, BA1,
PM5, PM6 and PP5, the codes an ACMG reader is most likely to reach for.

Authored by itpragmatik and an AI agent

* feat: janook explain — report a criterion from the model that classifies (#15)

* feat: janook explain — report a criterion from the model that classifies

Pulled forward from the CLI epic because the criteria were encoded and
nothing could read them. A published file can always be something the
engine ignores; this prints from the same model a classification will
use, which is the stronger audit and the one worth having early.

The ACMG comparison is marked as our annotation here exactly as it is in
the generated reference, and on its own line so wrapping can never
separate the disclaimer from what it disclaims.

A miss on a code AVCG renumbered is answered rather than guessed at:
"ACMG/AMP BP7 is AVCG-2024 BP6." That is the mistake this audience will
actually make, and the mapping is already data, so the switch over
AcmgOrigin resolves it exactly. The compiler enforces exhaustiveness — a
new kind of provenance will not compile until it is handled.

Three exit statuses, distinct on purpose: answered, input rejected, and
command not understood. A script retrying an unknown criterion is wrong
in a different way from one retrying a malformed command line, and
neither should have to parse stdout to find out.

Two defects the tests caught rather than review: the origin line ran to
155 characters unwrapped, and truncating the list at the first comma
turned PVS1 into "Null variant (nonsense" — a truncation that reads like
a complete thought is worse than one that obviously is not.

Authored by itpragmatik and an AI agent

* feat: add a dev wrapper so the documented examples actually run

The README shows "janook explain PS5" and no such command exists — the
installed launcher comes from packaging, which is not built. Examples
that cannot be run are worse than no examples: the first thing a reader
tries fails, and nothing tells them why.

scripts/janook finds the newest jar under janook-cli/target and runs it,
so nobody types a version number. It deliberately does not build. A
wrapper that quietly runs Maven turns "this is slow" into a mystery and
hides which tree the jar came from.

It exits 2 when it cannot run — no jar, no lib directory, no java — for
the same reason the check scripts do: a missing jar must never read like
the tool answering. The lib check earns its place because without it the
failure is a NoClassDefFoundError from deep inside the command rather
than one line saying to build first.

CONTRIBUTING now names it as a development convenience and says plainly
that the shipped launcher is packaging's job, so this is not mistaken for
it. The README says there is no installable janook yet and points here.

Authored by itpragmatik and an AI agent

* docs: check the background figures against the papers (#16)

Every number in this file was internally consistent, which proves it was
copied carefully rather than copied correctly. The weights it describes
go into the engine, so the difference matters.

Checked against the PDF. §3.4.2's figures hold: 39/51 (76%), 12
disagreements, 8/51 P vs LP, 2/51 VUS vs benign, 2/51 that could change
clinical management. Adds what was missing around them — the three
subgroups showing agreement falling from 89% to 78% to 60% as the
variants get harder, and that no variant ever received three different
labels.

Two things it now states that it did not: the metric is pairwise
agreement, which makes 76% and the 2026 study's 65% the same measure and
therefore comparable; and the benign set was drawn randomly from Ensembl
and screened against OMIA and ClinVar, deliberately not by allele
frequency, which would classify by a criterion the guidelines then use.

The 2026 reproducibility study is now recorded with per-figure
provenance: 93/65/83 confirmed against the published abstract, everything
else marked as read from the preprint and unverified against the version
of record. The version of record is what to cite; the preprint is
CC-BY-NC-ND and can be neither committed nor used as fixture material.

The 74%/76% discrepancy is recorded, not resolved. The 2026 paper
describes the earlier study as 74%; the 2024 paper says 39 of 51. We
follow the primary source and say so.

Corrects the reason for keeping an explicit weight table. The naming
convention does encode weight and AVCG renumbered to keep it true — the
trap is that a shared code is a different criterion.

Also drops the stale note saying the preprint had not been read.

Authored by itpragmatik and an AI agent

* Reduce the enforcement machinery around a core that has none of the product yet (#17)

* refactor: make the species scan a test instead of a build step

The rule was enforced by a 117-line shell script, a 105-line self-test, an
exec-maven-plugin binding in two poms, an overridable script-path property, an
invoker property override, and a deliberately-failing Maven sub-project. That is
222 lines of machinery plus a sub-build to scan seven files, for a rule that has
never fired — Table 4 contains no species name at all.

It is now one test class beside the other core boundary rules, with its tripwire
as an ordinary assertion rather than a whole Maven build. Same token list, same
word-splitting on underscores and camelCase transitions, same file-and-line
report, same explanation of the rule on failure.

Two deliberate reductions:

  - the suppression marker no longer requires a machine-checked reason. Write one
    anyway; the grammar was guarding a suppression nobody has needed yet.
  - the scan runs at test time rather than process-sources, so a violation costs
    a compile it used to skip. Worth it to delete a whole enforcement technology.

Unchanged: this remains a tripwire, not a proof. It catches a hardcoded binomial
and misses a threshold tuned on feline data, which is the likelier leak. The
javadoc says so, as the script did.

Authored by itpragmatik and an AI agent

* docs: move two build war stories out of the poms

The JGit .probe-<uuid> story and the NoClassDefFoundError story were 40-odd
lines of debugging history sitting in janook-cli/pom.xml, where they are reread
by everyone who touches a plugin version and are useful to almost none of them.

Both are now records in docs/DECISIONS.md, with the verified-behaviour table
that made the JGit one worth writing down. The poms keep enough to stop somebody
"simplifying" the setting — what breaks, in one sentence — and point at the rest.

No behaviour change. janook-cli/pom.xml drops from 212 lines to 185.

Authored by itpragmatik and an AI agent

* refactor: stop guessing which criterion a bad code meant

The miss path had two answers. One is the reason this command exists: an ACMG
code AVCG renumbered is answered exactly, because we hold the mapping — BP7 is
BP6, PM5 is PM2. That stays.

The other guessed. Prefix matching in either direction, then a fallback on the
code stem with the digits stripped, to narrow a list of twenty-three that the
very next line of output offers to print in full. Deleted, with the test that
pinned it; the test that mattered — a renumbered code is answered rather than
guessed at — is now true by construction rather than by assertion.

Authored by itpragmatik and an AI agent

* refactor: hold a weight's letters as the String they already are

Weight stored its code letters as a char[] and rebuilt a String on every call.
That is the defensive-copy pattern applied to a String, which is immutable and
needed no defending — it only cost an allocation per lookup and a reader a
moment working out what the array was protecting.

Authored by itpragmatik and an AI agent

* feat: record what was decided about a variant, and count it by weight (#18)

The engine's input shape, and the count every decision rule is written in
terms of. Nothing classifies anything yet — there are no labels and no rules.

AssertedCriteria holds one state per criterion for one variant: MET, NOT_MET
or NOT_ASSESSED. The three never collapse into two. A criterion nobody
mentioned is NOT_ASSESSED, never NOT_MET, because "nobody looked" is a gap in
the work while "we checked and it does not apply" is evidence, and a report
that confuses them claims work nobody did.

It deliberately does not know which variant it belongs to — no gene, no
species, no HGVS. The engine turns evidence into a classification and never
looks at what the variant is, so carrying that would only invite rules that
depend on it. Keeping species out is also what keeps this module
species-agnostic in practice rather than only in principle.

WeightTally groups the met criteria by direction and weight. Two things about
its shape are load-bearing:

  - a group holds the criteria, not a count. Every rule has to report which
    criteria satisfied it so the decision path can be built, and a group of
    integers makes that impossible after the fact. The count is the size.
  - six groups, not eight. AVCG's weights are not symmetric: the benign
    criteria are only BS (strong) and BP (supportive), so there is no benign
    very-strong and no benign moderate. Those are not offered rather than
    offered and always empty.

Rejected rather than absorbed: a criterion outside the edition, a criterion
from another edition sharing a code, a repeated assertion, and anything
missing. A shared code is not a shared criterion, so identity is compared
within the edition rather than by code string — a future edition could keep
PS5 and change what it means.

An empty tally is valid. It is what happens when nobody has gathered enough
evidence yet, and the rules will turn it into uncertain significance. That is
an answer, not an error, and it must never print like a rejected input.

The evidence set takes its inventory alongside the edition rather than deriving
it: GuidelineEdition is a name and a DOI and does not carry its criteria, and
having the criteria package hand out evidence builders would make the two
packages depend on each other.

Authored by itpragmatik and an AI agent

* Fix the flaws a full-repo review turned up (#19)

* fix: locale-proof the reference generator, and warn when a code was reused

CriteriaReference lowercased the direction name with the JVM's default
locale. On a Turkish-locale machine that turns PATHOGENIC into pathogenıc
(dotless i), the rendered file no longer matches the committed one, and the
staleness check fails for that contributor. Locale.ROOT, matching what
ExplainCommand already does.

janook explain PP1 printed AVCG's PP1 with nothing pointing at PS5, even
though ACMG/AMP's PP1 — cosegregation, the criterion a reader who knows the
human guidelines expects — is AVCG's PS5. The renumbering warning only fired
for codes AVCG does not have at all. Now the output adds a note whenever the
code being explained was reused from a different ACMG/AMP criterion; today
that is only PP1.

WeightTally's group components were named pathogenicSupporting and
benignSupporting, but the paper's word for that weight is supportive (Table 4
footnote), which is what the Weight enum already says. Renamed before anything
depends on them.

Also: firstSentence() cut on length, not sentences, so it is now preview();
and an inline java.util.ArrayList became an import.

Authored by itpragmatik and an AI agent

* fix: close three gaps in the checks that only show up when they matter

Tag pushes never triggered CI, so the refs/tags/v* rules in
check-release-version.sh — exact version, known commit, clean tree, the
strictest rules it has — could never run. ci.yml now triggers on v* tags.

The merge-back check ran only on pushes to main. During a release the merge
back into develop happens after the push to main, so the run on main always
failed — correctly, as a reminder — but nothing ever turned green afterwards,
and the red run just sat there teaching people to ignore it. The job now also
runs on pushes to develop and compares origin/main against origin/develop, so
the push that completes the merge back produces a passing run.

The branch-name scan in check-public-safe.sh looked only at refs/heads. In
CI the checkout has a single local branch; every other branch on the
repository is a remote-tracking ref, so the scan was checking almost nothing
there. It now scans refs/remotes too.

Also: check-release-version.sh's jar discovery now excludes -javadoc jars,
the same way scripts/janook already did.

Authored by itpragmatik and an AI agent

* docs: correct the README where it had drifted from the code and the paper

The status still said pre-code; the criteria model and janook explain exist.

The README claimed typing an ACMG code always gets you the AVCG equivalent.
That was only true for codes AVCG dropped (BP7, PM5); typing PP1 got AVCG's
PP1 with no mention of PS5. The sentence now matches what the command
actually does, including the new note it prints for PP1.

The docs table did not list docs/DECISIONS.md.

BACKGROUND.md called the lowest weight 'supporting'; the paper's word is
'supportive' (Table 4 footnote), and the code already followed the paper.

Authored by itpragmatik and an AI agent

* docs: rewrite the docs in plain language (#20)

The prose had drifted toward compressed, clever phrasing that a reader
outside bioinformatics could not follow. Rewritten under the writing
rules now recorded in the spec: define a domain term the first time it
appears, one idea per sentence, and explain instead of coining a phrase.
Every figure, table and provenance note is unchanged.

One content fix rides along: the README's motivation now leads with the
2026 reproducibility study (65% agreement on 405 expert classifications)
instead of only the 2024 pilot's 76% on 51, since the larger study is
the stronger evidence.

Authored by itpragmatik and an AI agent

* refactor: rewrite comments in plain language, and simplify two spots they annotate (#21)

The comments carried the same compressed, aphoristic style the docs did
before #20 — phrases like 'a version identifies exactly one behaviour,
or it identifies nothing' explain nothing to a reader outside the joke.
Rewritten under the same rules: say the reason plainly, one idea per
sentence.

Two code cleanups ride along because their surrounding prose changed
anyway: CriteriaReference builds its header from a text block instead
of forty chained appends, and the generated criteria reference is
refreshed from it; WeightTally groups criteria with an inline switch
instead of threading six lists through a helper.

No behaviour changes. Full build, both check scripts and both of their
self-test suites pass.

Authored by itpragmatik and an AI agent

* feat: name the five labels a classification can carry (#22)

The first piece of the decision rules: the closed set of five labels
from Table 6, in a new decision package. Pinned as a literal in the
test because stored classifications carry these names — a rename would
change the meaning of results already recorded.

Uncertain significance is documented as a join-step outcome that no
single rule returns; the branch stories that follow depend on that
distinction.

Authored by itpragmatik and an AI agent

* feat: record what a satisfied decision rule reports (#23)

A rule that fires reports three things: the label it assigns, the
rule's name as Table 6 prints it, and the criteria that satisfied it.
The criteria are carried because the tally is deliberately lossy —
swapping PS5 for PS3 leaves every count identical, so only the rule
that fired can name the evidence, and no later step can reconstruct it.

Two invariants are enforced in the record itself: no rule can claim to
assign uncertain significance (that label belongs to the joining step,
which needs both branches first), and no rule can claim to be satisfied
by nothing.

Authored by itpragmatik and an AI agent

* feat: decide whether the evidence says a variant is benign (#24)

Branch B of Table 6: the Benign rule (two or more strong criteria) and
the two Likely Benign rules, evaluated in the table's order. Likely
Benign is reachable only by failing Benign — the shape of the chain,
not a written precedence. When nothing fires the branch reports
nothing; whether that means uncertain significance is the joining
step's call, once branch A has also been heard.

Two readings are recorded in place rather than folded in silently.
Table 6 prints these rules over BS1-BS4 and BP1-BP7, the ACMG ranges
that AVCG renumbered away; the rules read the tally's benign groups,
which can only hold BS1-BS3 and BP1-BP6, and a test pins the widest
possible match to exactly the criteria that exist. And LB.i's counts
are implemented exactly as printed, because unlike branch A's disputed
counts the literal reading leaves no gap - one strong with two or more
supporting falls through to LB.ii.

Authored by itpragmatik and an AI agent

* feat: let a rule match record which alternative clause was satisfied (#25)

Some Table 6 rules offer alternative ways to be satisfied - rule P.i
lists four. A result that says only 'rule P.i matched' does not tell a
reviewer which alternative applied, and that is exactly what they need
to check against the table. So a match can now carry the satisfied
clause, worded as the table prints it.

Rules with no alternatives - all of branch B's - use a new delegating
constructor and carry no clause, which is why BenignBranch needed no
change.

Authored by itpragmatik and an AI agent

* feat: name the reading in force for Table 6's one disputed count (#26)

Rule P.iii's third clause prints '1 moderate and 4 supporting' where
its ACMG/AMP counterpart prints '>=4', and every comparable count in
the table carries the '>=' its counterpart does. AVCG documents its
deliberate departures from ACMG/AMP and this is not among them, so the
likelier explanation is a dropped symbol, not a silent tightening.
Read literally it also means a fifth supporting criterion would demote
a Pathogenic variant - an effect nobody designs.

So the at-least reading is in force, held in one named constant. The
choice is provisional: the guideline's authors have been asked, and
flipping the reading is the constant plus one test expectation -
nothing else may encode the decision. A test keeps both readings'
behaviour demonstrable whichever is in force.

Authored by itpragmatik and an AI agent

* feat: decide whether the evidence says a variant is pathogenic (#27)

The Pathogenic half of branch A: rules P.i, P.ii and P.iii in the
table's order, first match wins, each match naming its rule, the clause
that applied where the rule offers alternatives, and the criteria that
satisfied it. The Likely Pathogenic fall-through arrives next; until
then a tally below every P rule gets nothing from this branch.

Counts are implemented exactly as printed, which is gap-free because
every exact clause has a neighbouring '>=' clause of the same rule (or
P.ii before it) catching anything that exceeds it - tests walk those
edges. The one disputed count, P.iii's '4 supporting', goes through
DisputedCount; wiring it up surfaced that the two readings cannot
disagree in this edition, because only four supportive pathogenic
criteria exist, so DisputedCount's javadoc now says the choice records
a transcription, not a behavioural difference.

Authored by itpragmatik and an AI agent

* feat: name the reading in force for the second disputed count (#28)

LP.iv prints '3 moderate' where its ACMG/AMP counterpart prints '>=3'.
Unlike P.iii's disputed count, this one has behavioural stakes: four
moderate criteria exist, and the readings disagree about a variant with
all four met - at-least calls it Likely Pathogenic, the literal reading
strands it as uncertain significance, with three moderates having been
enough for a label. More evidence weakening the call is an effect
nobody designs, so at-least is in force, provisionally, pending the
authors' answer.

The class javadoc now covers both counts and says plainly which one
can change classifications and which records only a transcription.

Authored by itpragmatik and an AI agent

* feat: fall through to Likely Pathogenic when no Pathogenic rule holds (#29)

The six LP rules of Table 6, wired below the three P rules in the same
first-match-wins chain, completing branch A. Likely Pathogenic is
reachable only by failing every Pathogenic rule - the shape of the
chain, not a written precedence - and a test feeds evidence satisfying
both P.i and LP.i to prove P wins by position alone.

LP.iv goes through DisputedCount. The four-moderates test is the one
that flips with the reading: under at-least in force they are Likely
Pathogenic; read literally they would match nothing and finish
uncertain while three moderates earned a label. The remaining counts
are exact as printed and gap-free, because an earlier rule claims
anything above each of them.

Authored by itpragmatik and an AI agent

* feat: join the branches into a classification, behind a strategy interface (#30)

The first code in the project that answers the question the tool
exists to answer. DecisionTree runs both branches on every evidence
set and judges the pair: exactly one label wins outright, neither is
uncertain for lack of criteria, both is uncertain because the evidence
contradicts itself. A label from a branch is an input to the join,
never an exit - the opposing-evidence test exists because the natural
sequential shortcut deletes exactly that case and passes every other
test.

Classification is the record: label, an enum reason separating the two
uncertain routes, both branch results, the untrimmed evidence set, the
edition, and the name of the strategy that produced it. Its constructor
refuses inconsistent combinations, so a stored record can be trusted
without cross-checking.

The tree sits behind Classifier because the combining arithmetic is
the unstable half of a guideline - human guidance is moving to a
points model. A second strategy implements the same interface, carries
its own name, and touches nothing in the criteria model.

Authored by itpragmatik and an AI agent

* feat: hold the facts about one species as a profile (#31)

The in-memory shape of a species profile: identifier, display name,
reference assembly, annotation source, OMIA species number, and the
predictors validated for the species by variant kind. Nothing reads
files yet - that is the next story - and nothing in janook-core
changes, which is the point: the engine stays species-blind while the
species facts get a place to live at the edge.

The predictor lists cover missense and splice only, because those are
the only kinds AVCG names tool combinations for, and an empty list is
valid because it is the honest state of every species except the cat.
The species identifier's shape is pinned (lowercase genus_species,
subspecies allowed) since files will be named after it and users will
type it.

Authored by itpragmatik and an AI agent

* feat: read a species profile from a file (#32)

One YAML file in, one profile out, rejecting anything not fully
understood: a missing field names the field and the file, a syntax
fault names the line and column, and an unrecognised field is an error
rather than a shrug - a tolerant loader would turn a mistyped field
into one that silently never applies. Both predictor lists must be
present even when empty, because an explicit empty list can only mean
'none validated' while an absent one could also mean 'forgot'.

This brings the project's first third-party runtime dependency outside
core: SnakeYAML, chosen because it is a single jar with no transitive
dependencies and everything in lib/ ships to every user. The YAML
syntax is shared with the variant input to come, so the tool has one
config format. DECISIONS.md's note about lib/ holding only the core is
updated to match.

Authored by itpragmatik and an AI agent

* feat: ship the nine species profiles inside the jar (#33)

The cat profile is complete: the truth set's assembly and annotation
(Felis_catus_9.0, Ensembl 111), and the predictor combinations the
paper validated - including SSPnn, listed because validity is the
paper's fact even though the tool has no programmable interface; how to
execute it is the adapters' problem, where the substitution question
lives. The other eight species of the cross-species check ship as
stubs: verified assembly names and taxon numbers, and explicitly empty
predictor lists, because the paper benchmarked predictors on the cat
only.

Which species janook knows is now a question about files, not code: an
index resource lists the profiles, a test pins the index to the files
on disk so the two cannot drift, and every shipped profile is loaded
and validated on every build. Asking for an unknown species names what
was asked and lists what is known; a near-miss is never guessed at.

Every assembly name and taxon number was checked against Ensembl
release 116 and the NCBI taxonomy, not recalled.

Authored by itpragmatik and an AI agent

* feat: let a profile switch a criterion off (#34)

The smallest real version of 'profiles modify criteria': an optional
disabled_criteria list in the profile, empty in every shipped profile,
for the lab that needs a criterion not to apply locally.

Three rules make it safe. A switched-off code must name a criterion the
edition has - treating a typo as a no-op would hide an edit meant to
change classifications. Any assertion about a switched-off criterion is
rejected naming the criterion and the profile, never quietly ignored.
And the mechanism is inventory reduction at the edge: the engine
receives an evidence set that simply does not include the criterion, so
nothing in janook-core changes - which was the proof the epic demanded.

A test walks the consequence end to end: BS1+BS2 met is Benign under a
stock profile, and with BS1 switched off the same lab can assert only
BS2, which earns no label and comes out uncertain for lack of criteria.

Authored by itpragmatik and an AI agent

* docs: invite scientific review, and update the status to match the code (#35)

A new Scientific review section says plainly what the project is and is
not: an implementation of AVCG, not an authority on it, built from a
software engineering perspective and open to correction from the people
who know the domain. The posture is checkability, not credentials -
every criterion cites its table and page, and interpretation points are
visible in the code rather than folded in.

The Status section still said classification does not exist; it has
existed since the decision tree was joined. It now describes what is
true: engine done, species profiles shipped, input and output layers
next.

Authored by itpragmatik and an AI agent

* docs: disclose AI-assisted development in the scientific review section (#36)

The disclosure already lives in every commit trailer and in
CONTRIBUTING.md; the README's most scrutiny-inviting section should say
it too, rather than leaving reviewers to find it in the git log. The
audience being invited to check the science deserves to know how the
software was made.

Authored by itpragmatik and an AI agent

* docs: say who leads the work and what the AI tools assist with (#37)

The disclosure now names the shape of the collaboration plainly: a
software engineer designs and builds the tool, AI tools assist with
research, design, implementation and testing, and the work and
technical decisions are human-led.

Authored by itpragmatik and an AI agent

* docs: say why this project exists (#38)

A personal why, in first person: years of consuming open source around
software and genetics, the wish to contribute something back, and the
gap that made animal variant classification the place to do it. It sits
between the introduction and the status, where a first-time reader asks
the question it answers.

Authored by itpragmatik and an AI agent

* docs: revise the why section, and own the Java choice plainly (#39)

The origin story reads more naturally restructured, and it gains the
paragraph a reader was most likely to wonder about: why Java. The
answer given is the personal one - fluency and productivity in a
personal-time project - which sits alongside the technical case the
Decisions section already makes.

Authored by itpragmatik and an AI agent

* docs: name the evidence file, and use the name everywhere (#40)

The file a user hands janook had no consistent name - the docs said
'variant file', 'input files', 'YAML per variant' - and none of them
said the thing that matters: this file is where the user records their
evidence, and everything janook concludes traces back to it. It is now
the evidence file, defined where the input format is introduced in
PLAN.md, explained in passing in the README's status, and used
consistently in the one code comment that mentioned it.

Authored by itpragmatik and an AI agent

* feat: hold one variant's full input together (#41)

What a parsed evidence file becomes: the variant's identity, the
evidence set the engine will read, and the justifications the report
will show, held as one value so the caller never keeps the association
by hand. The identity travels around the engine, never through it.

The evidence is built under the variant's species profile, so
everything the profile layer enforces holds here without restating it -
switched-off criteria refused naming the profile, duplicates refused
rather than overwritten, and a justification offered with a refused
decision is not kept. A justification needs at least one part; an empty
one is refused so 'none given' has exactly one representation. The
protein notation is optional on the identity because a splice-site
variant genuinely has none.

Authored by itpragmatik and an AI agent

* feat: parse the evidence file (#42)

The tool's front door: one YAML evidence file in, one variant's full
input out, with the same reject-what-you-do-not-understand contract the
profile loader set - and more at stake, since everything janook
concludes traces back to this file. Syntax faults name line and column;
missing, unknown and wrongly typed fields name themselves at every
level; an unknown criterion code points at explain --list; an unknown
species lists the nine janook knows; and met takes exactly true, false
or not_assessed, because 'checked and does not apply' and 'nobody
looked' must never blur.

A justification remains optional even for a met criterion - the parked
policy question, unchanged. Duplicate YAML keys are now refused instead
of last-one-wins, here and in the profile loader, where the same silent-
overwrite gap existed. PLAN's example swaps its note field for evidence,
matching the real schema.

Authored by itpragmatik and an AI agent

* feat: janook init prints the evidence-file template (#43)

Nobody starts from a blank page: the template carries the variant block
with placeholders, the nine species janook knows, and all 23 criteria
as commented stubs generated from the criterion model - code,
direction, weight and the start of each definition, so the file
documents itself. Uncommenting a stub is removing one # from each of
its two lines.

Printing to stdout is the design: janook init > variant.yaml creates
the file and the command never touches the filesystem. The stubs carry
only the met line, deliberately - pre-printed empty evidence fields
would hand every user a blank-value rejection trap; the header shows
the justification fields once, as an example to copy. The load-bearing
test round-trips the template through the real parser: filled in, it
parses with every criterion not assessed.

Authored by itpragmatik and an AI agent

* feat: read a spreadsheet of variants (#44)

One variant per TSV row, because the audience lives in spreadsheets:
identity columns plus any subset of criterion-code columns, each cell
true, false, not_assessed or empty - empty meaning not assessed, the
natural spreadsheet spelling of nobody looked. Spreadsheet realities
are accommodated deliberately and documented: TRUE and False match
case-insensitively, surrounding spaces are ignored, and missing
trailing cells read as empty because exports drop trailing tabs. Rows
may mix species.

Two hard lines. A fault anywhere rejects the whole file naming the
line and column - a batch is never returned with broken rows quietly
missing, because an answer that looks complete and is not is the worst
answer this tool could give. And the format's limit is stated, not
silent: rows carry no justification prose; a variant whose reasons
matter belongs in an evidence file, where they have room.

A test pins each row to exactly what the evidence-file parser would
have produced for it.

Authored by itpragmatik and an AI agent

* feat: render a classification as a summary, as JSON, and as the report (#45)

The record - variant input, classification, provenance - and its three
renderings, which only show what the record holds. Rendering is a pure
function: the date and operator arrive as inputs, never from a clock or
the environment, which is how identical records produce identical bytes
while still carrying the date the working group asked for. The record
is coherent by construction: its classification must have been produced
from exactly its input's evidence, enforced at the object level.

The terminal summary shows what somebody engaged with and counts the
rest. The JSON is schema-versioned and hand-emitted - one fixed shape
is a page of code, where a library is three jars shipped to every user -
with escaping proven by parsing the output back. The Markdown report is
complete: every criterion with its justification, the decision path,
full provenance, the re-derivation sentence, and a profile that
switched criteria off announced where it cannot be missed. The two
uncertain routes read differently in every rendering.

Authored by itpragmatik and an AI agent

* feat: janook classify - an evidence file in, the answer out (#46)

The wiring epic: everything this command does existed and was tested
before it. The command supplies what only a command can know - the hash
of the input, computed from exactly the bytes that get parsed, the date
of the run, its one clock read, and the operator, only when --operator
names one, never from a login name. One artifact per run to stdout:
the summary, --json, or --report; saving is redirection; batch mode
prints one scannable line per variant or a JSON array sharing the batch
file's hash, and classifies every row before printing anything so a
fault never leaves a half-written artifact.

The surface is finished with its safety net: an unexpected failure is
janook's own bug, says so plainly above the trace, and exits with its
own code so a script never mistakes our crash for the user's file.
janook help lists every command and documents the four exit codes;
the bare command prints the same text but exits 2, because a typo is
not a success.

Authored by itpragmatik and an AI agent

* feat: a brief flag for when the answer is all you want (#47)

--brief prints only the classification line, with the two uncertain
routes still kept apart as everywhere else, and with --json a minimal
label-and-reason document spelled exactly as the full document spells
them. It refuses --report, which contradicts it, and --batch, which is
already one line per variant. Added once real use found the summary too
much for a quick check - which is when a flag like this earns its
place, and not before.

Authored by itpragmatik and an AI agent

* feat: one archive a machine can install (#48)

The distribution decision DECISIONS.md deferred is made: the release
artifact is janook-<version>-dist.tar.gz - the command jar at the
archive root exactly as tests run it, dependencies under lib/, the
shipped janook launcher under bin/, LICENSE and NOTICE visible at the
top. An archive rather than a shaded jar, because shading would make
the released artifact the one build nobody tested the internals of;
the full weighing is in DECISIONS.md. The jar stays at the root for a
concrete reason: Class-Path entries resolve relative to the jar's own
directory, and tidying it into lib/ would break them as lib/lib/.

Builds are reproducible - outputTimestamp pins archive entry times, and
CI now builds twice and compares checksums, because a checksum that
cannot be reproduced identifies nothing. check-dist.sh verifies the
artifact by using it: unpack, run version, explain, init, and a real
classification of init's own template through the shipped launcher,
exit codes checked on the way. The launcher execs, so scripts see
exactly janook's four codes; a missing java is 127, never one of ours.

Authored by itpragmatik and an AI agent

* feat: a pushed tag builds its own release (#49)

The release workflow: on a v* tag, run exactly the commands a
contributor runs - public safety, the full build, the release-version
check under its strictest tag rules, the dist check - then the one
inherently-remote step, creating the GitHub release with the artifact
and its checksum. A tag that fails any check publishes nothing.

Notes are a person's job, enforced: the workflow refuses to publish
without docs/release-notes/<tag>.md, because saying what a release
contains and omits is honesty, not generation. The checksum is appended
mechanically. CONTRIBUTING names the one exception to its no-CI-only-
steps rule.

The artifact also loses an internal name: a user downloads
janook-9.0.0-dist.tar.gz, not janook-cli-anything.

Authored by itpragmatik and an AI agent

* release: 9.0.0

Drop the -SNAPSHOT suffix across the reactor and the tripwire poms, and
write the release notes the tag will publish: what the fir…
Steps 4 and 5 of the release process in one motion: main merges back so
the next release cannot silently revert the version bump, and the
version conflicts resolve directly to the next -SNAPSHOT. VERSIONING.md
gains the finding this first release surfaced - the merge back must be
a true merge commit, because the containment check can never be
satisfied by a squash.

Authored by itpragmatik and an AI agent
@itpragmatik
itpragmatik merged commit 4f61a2e into develop Aug 16, 2026
2 checks passed
@itpragmatik
itpragmatik deleted the merge-back/9.0.0 branch August 16, 2026 20:40
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