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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 23 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ score automatically.

Package root: `org.horizon36596.simloop`. Licence: BSD 3-Clause.

Run SimLoop from your robot project's tests or simulation harness and view the result in a separate
AdvantageScope window. This release covers that library-and-viewer workflow.

The source tree can be newer than the latest tag. Distortion, occlusion, tilted tags and synthetic
camera frames are development additions after `v0.2.0-alpha.1`; they are not in that pinned artifact.
Use the starter and documentation from the tag you install, or pin the exact source commit you test.

> **SimLoop is an alpha.** `v0.2.0-alpha.1` is the current alpha and the first public one. The API may
> change between alpha versions, so pin an exact version in your build. It needs **FTC SDK 12.0**.
> Support is best effort: a small FTC team maintains it in its own time. `v0.1.0-beta1`, the earlier
Expand Down Expand Up @@ -63,11 +70,11 @@ the starter folder is what declares it.
| Contribute a fix or a feature | [`CONTRIBUTING.md`](CONTRIBUTING.md) |
| Change code in this repository | [`SimLoop/CLAUDE.md`](SimLoop/CLAUDE.md) |

`SimLoop/docs/` is written to be read as Markdown in the repository and also builds into a site. That
site is live at **<https://libraries.horizon36596.org/simloop/>**, with the generated API reference
under [`/javadoc/`](https://libraries.horizon36596.org/simloop/javadoc/). The
`horizon-36596.github.io/simloop/` address still answers and redirects there. How the hosting is put
together is in [`SimLoop/PUBLISHING.md`](SimLoop/PUBLISHING.md).
`SimLoop/docs/` can be read as Markdown and built into a site. Its publishing address is
**<https://libraries.horizon36596.org/simloop/>**, with generated Javadoc under
[`/javadoc/`](https://libraries.horizon36596.org/simloop/javadoc/). If the site is unavailable, read the
docs at your pinned tag or build them locally with `./gradlew :SimLoop:docsSite`.
[`SimLoop/PUBLISHING.md`](SimLoop/PUBLISHING.md) explains the hosting and deployment checks.

That path is this repository's name, in lower case, because GitHub serves a project site at its
repository name under the organisation's domain and the path is case-sensitive. It is also why the
Expand Down Expand Up @@ -106,20 +113,12 @@ something that will not fit.

## Why the library sits in a subdirectory

The library is a Gradle **subproject** rather than the root project. That reads as redundant and is not,
for a reason that is cheaper to keep than to change:

- The Android Gradle Plugin's library plugin is applied to something it expects. A root project that is
itself an Android library is a layout nothing else in the FTC ecosystem uses.
What does **not** depend on it is the published coordinate, which is worth saying because this file used
to claim the opposite. JitPack names the artifact after the repository, not after the Gradle module, as
long as the build publishes exactly one module — measured, not assumed: the first build of
`v0.1.0-beta1` served `com.github.Horizon-36596:simloop`. What would rename it is publishing a **second**
module, which switches JitPack to its multi-module spelling. So the rule to keep is "one published
module", not "one particular directory layout".
`SimLoop/` is the Android library subproject; the root build coordinates it with the starter examples
and desktop keyboard window. Gradle tasks therefore use the `:SimLoop` prefix.

So `SimLoop/` in a path and `:SimLoop` in a Gradle task are correct everywhere they appear, including in
the CI workflows.
The published coordinate follows the repository name, `com.github.Horizon-36596:simloop`. Keep one
published module: the desktop jar is a classifier of that same publication, not a second module.
Publishing a second module would change JitPack's artifact naming.

## Building and testing it

Expand All @@ -129,20 +128,18 @@ From the repository root:
./gradlew test
```

1,218 JVM tests (1,212 in the library, 4 in the keyboard-window module, 2 in the example robot), with no
device and no emulator. The
documentation site and the API reference:
The library, keyboard-window and starter-example tests run on the JVM, with no device or emulator.
To build the documentation site and API reference:

```bash
pip install -r SimLoop/docs-requirements.txt
./gradlew :SimLoop:docsSite
```

All of it is built on every push and every pull request by
[`.github/workflows/test.yml`](.github/workflows/test.yml), so none of it can quietly become something
that only builds on one laptop. Pull requests also run a leak check
([`tools/leak-gate/`](tools/leak-gate/leak-gate.sh)) and a secret scan
([`.github/workflows/leak-gate.yml`](.github/workflows/leak-gate.yml)).
[`.github/workflows/test.yml`](.github/workflows/test.yml) defines the automated test and documentation
checks. Pull-request checks also include the [leak gate](tools/leak-gate/leak-gate.sh) and
[secret scan](.github/workflows/leak-gate.yml). Check the workflow results for the commit you use; the
commands above run the tests and documentation build locally.

## Licence

Expand Down
97 changes: 48 additions & 49 deletions SimLoop/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Central. The build wiring this describes lives at the bottom of `SimLoop/build.g

**JitPack ignores every one of those first three values and derives its own.** The group becomes
`com.github.<owner>`, the artifact becomes the **repository** name, and the version is the git tag
verbatim, leading `v` included. So a consumer writes `com.github.Horizon-36596:simloop:v0.2.0-alpha.1`
while the POM inside says `org.horizon36596:SimLoop:0.2.0-alpha.1`, and both are correct at once — the
declared values are what `publishToMavenLocal` and any future Maven Central release use.
verbatim, leading `v` included. A consumer writes `com.github.Horizon-36596:simloop:v0.2.0-alpha.1`;
JitPack's served POM uses that coordinate too. The declared `org.horizon36596:SimLoop:0.2.0-alpha.1`
coordinate belongs to `publishToMavenLocal` and a possible Maven Central publication.

That is JitPack's **single-module** spelling, and this build gets it because `jitpack.yml` installs
exactly one module. A build that published two would get the **multi-module** spelling,
Expand Down Expand Up @@ -58,35 +58,33 @@ except where a row says it was overtaken.
| `EnvelopeGuardrails` guardrail names | `withinBound`, `loopTimeUnderBudget` and `safeStopAtEnd` now embed their thresholds in `Guardrail.name()`. Any persisted last-best score from an earlier version will read as a different envelope in `Gate` and must be re-baselined. Non-finite and negative thresholds are now rejected at construction. |
| `RunResult.fromRlog` | `RealOutputs/Console` and `RealOutputs/Logger/QueuedCycles` are no longer scoreable, so `keys()` and `frames()` no longer contain them. |

`SimLoop/build.gradle` is the only place the version is *authoritative*, but it is not the only place
it is written: `README.md`'s install snippet, the coordinates table above and the verification note
below all quote it, and none of them is generated. Step 2 of the checklist below exists because of
that — a bump that misses the README leaves teams copying a version that was never tagged.
`SimLoop/build.gradle` declares the source version. Installation snippets, the starter, agent guides
and the site footer repeat it. Review all current-version references during a release; keep references
to older tags where they explain compatibility or historical checks.

## Cutting a release

1. **Green first.** `./gradlew :SimLoop:testDebugUnitTest` — all of it, not the tests you touched.
Then `./gradlew :SimLoop:apiDocs`, which must report **zero warnings**: the API reference is published
and read by people and agents who have nothing else, so a missing unit is a release defect there in a
way it is not in an IDE.
2. **Bump** `version` in `SimLoop/build.gradle`, **and the two copies of it**: `README.md`'s install
snippet, and the `copyright:` line in `SimLoop/mkdocs.yml`, which is the version stamped in the footer
of every documentation page. None of the three is linked to the others; see the note above.
`Select-String -Path SimLoop\README.md,SimLoop\mkdocs.yml -Pattern '0\.2\.0-alpha\.1'` finds them.
2. **Update the approved version** in `SimLoop/build.gradle` and its current-version references.
Search the root and module READMEs, `SimLoop/docs/`, `SimLoop/mkdocs.yml`, `simloop-starter/` and
`simloop-agents/`. Review each match rather than replacing historical tags blindly. Run
`./gradlew :SimLoop:docsSite`; its version check verifies the footer, and its example check verifies
the first-test snippet. These checks do not replace reviewing installation coordinates.
3. **Verify the artifact locally before anyone can consume it** — the section below says how, and it is
the part people skip.
4. **Commit, tag, push the tag.** The tag is the version, e.g. `v0.2.0-alpha.1`.
5. **Build that tag on JitPack yourself, and read the log**, at
`https://jitpack.io/#Horizon-36596/simloop`. This is a gate, not a warm-up, and until it has passed
once nobody should be given the coordinate. Two specific things can only be learned here:
`https://jitpack.io/#Horizon-36596/simloop`. Before giving teams a new coordinate, verify:
- **Whether the coordinate is right at all.** JitPack derives the artifact id from the module, and
the only way to be certain which spelling it serves is to see it serve one.
- **Whether JitPack's environment builds it at all.** `jitpack.yml` pins `openjdk17` because the
Android Gradle Plugin requires it and JitPack's default is older. That pin has never been exercised
by a real JitPack build; the first tag is what exercises it.
- **Whether JitPack's environment builds the tag.** `jitpack.yml` pins `openjdk17`. Check the new
build's log rather than assuming a local build proves the hosted environment works.

The repository is public, which is what JitPack's free tier requires - it does not build private
repositories.
Check repository visibility and JitPack access before relying on a new hosted build. The owner
decides any visibility or hosting change separately.
6. **Tell the teams the coordinate and the two repository lines**, not just the coordinate. The
PsiKit one is the thing that bites — see below.

Expand All @@ -100,9 +98,9 @@ Tagging and pushing leave the machine, so they are a human's call, not an agent'

writes the AAR, the sources jar, the POM and the Gradle module metadata into `~/.m2/repository/`.

Then check the POM lists **RobotCore, Hardware, psikit-core and psikit-ftc at `compile` scope**. If any
of them says `runtime`, someone changed an `api` back to `implementation` and consumers will not be
able to name the types they are handed.
Then check the POM lists **`RobotCore`, `Hardware` and PsiKit `core` at `compile` scope**. The current
build does not depend on PsiKit `ftc`. If a required dependency moves to `runtime`, a consumer can
lose the SDK types or logging API it needs while compiling tests.

The real check is to compile something against it from outside this repo: a throwaway Android library
module whose only declared dependency is `org.horizon36596:SimLoop:<version>`, with `mavenLocal()`
Expand All @@ -120,6 +118,12 @@ That was run for `0.1.0-beta1` and passed, and the same project with the depende
`implementation` failed with `cannot access HardwareMap` — the scope claim above is measured, not
assumed.

On 2026-10-24, JitPack's [alpha build-status API](https://jitpack.io/api/builds/com.github.Horizon-36596/simloop/v0.2.0-alpha.1)
reported `ok` for tag commit `9116406` with JDK 17. Its
[served POM](https://jitpack.io/com/github/Horizon-36596/simloop/v0.2.0-alpha.1/simloop-v0.2.0-alpha.1.pom)
returned HTTP 200 and listed those three dependencies at compile scope. This confirms the existing
tag's hosted metadata; it does not validate unpublished source or guarantee a future build's access.

## Why the library is a subproject and not the root project

This repository holds one library, and that library is a Gradle **subproject**. Both the `SimLoop/` path
Expand Down Expand Up @@ -163,11 +167,10 @@ publishes, and drops the API reference inside the site at `/javadoc/`. It writes
pip install -r SimLoop/docs-requirements.txt
```

`.github/workflows/docs-publish.yml` publishes it on a `v*` tag. No tag has been cut, so it has never
run, and four settings still have to exist before a deployment can succeed - three of them in a
different repository, because the site is served under a shared domain. The CI job in
`test.yml` builds the site on every push and keeps it as a downloadable artifact regardless, so a break
in the site is caught by the push that causes it rather than by a release.
`.github/workflows/docs-publish.yml` is configured to publish on a `v*` tag. Existing tags do not prove
that the workflow deployed successfully; check its latest run and the actual site. The settings below
span this repository and the shared-domain landing repository. The `test.yml` workflow also builds the
site and uploads an artifact without deploying it.

### Turning it on — four steps, all of them a human's

Expand All @@ -189,12 +192,9 @@ the path is `/simloop`. It was renamed from `SimLoop` on 2026-09-19 for exactly
tag existed — which also moved the published coordinate to `com.github.Horizon-36596:simloop`, since
JitPack takes the artifact id straight from the repository name.

> **Steps 1 and 4 are done.** Both were done on 2026-09-19 and neither needs doing again. The two that
> remain — **2** and **3** — are the DNS record and the custom domain, and they are the two nobody but a
> human with the domain's credentials can do. Until they are done, both sites are already live at
> GitHub's own addresses: <https://horizon-36596.github.io/> and
> <https://horizon-36596.github.io/simloop/>. Steps 2 and 3 move them onto `libraries.horizon36596.org`;
> they do not turn them on.
The landing repository was created on 2026-09-19. Check the current DNS, Pages settings and latest
deployment before repeating setup steps. A successful local build does not show that the project
site is deployed.

**1. Create the landing repository.** ✅ **Done 2026-09-19** — `Horizon-36596/Horizon-36596.github.io`,
public, with the landing page at its root and Pages serving `main` / `(root)`. The name is not a choice:
Expand Down Expand Up @@ -289,12 +289,14 @@ and depends on your registrar. Both are waiting, not failure.
If this one fails, nothing below it can work — the domain is not attached yet. The github.io addresses
keep working throughout; they are the same site, and comparing the two is how you tell a DNS problem
from a site problem.
- `https://libraries.horizon36596.org/simloop/` loads the documentation site. A 404 here with the landing
page working means either the tag has not been pushed, or a custom domain got set on this repository
after all.
- `https://libraries.horizon36596.org/simloop/` loads the documentation site. If the landing page works
but this path returns 404, check this repository's Pages configuration, visibility, workflow result
and published artifact. Also confirm that no separate custom domain is set on this project site.
- The footer of every page reads the version you just tagged.
- `https://libraries.horizon36596.org/simloop/javadoc/` loads the API reference. If the site loads and
this 404s, the `docsSite` task did not run — the deploy published a bare `mkdocs build` from somewhere.
- `https://libraries.horizon36596.org/simloop/api-reference/` loads the API guide page, and its button
opens `https://libraries.horizon36596.org/simloop/javadoc/`. Check that both routes exist in the
artifact. If only Javadoc is missing, verify that the deployment used `docsSite`, which copies it,
rather than MkDocs alone.

## What a Maven Central release would additionally need

Expand All @@ -318,7 +320,8 @@ Not done, and not needed for an alpha with a handful of teams. For when it is:
build, which is also why there is nothing to keep in sync.
- A `repositories { maven { url = ...; credentials { ... } } }` block, with credentials read from an
environment variable or `~/.gradle/gradle.properties` and never from a file in the repository.
- Releases become **immutable**. An alpha tag can be re-cut after a mistake; a Central version cannot.
- Central versions are **immutable**. Treat published alpha tags as immutable too: correct a mistake
with a new version so cached artifacts and source keep referring to the same code.

The POM already carries everything else Central validates — name, description, url, licence, developer
and SCM — so this is a known and bounded list, not an unknown.
Expand All @@ -339,19 +342,15 @@ file assumes.
`v<version>` is what declares a commit usable, and everything downstream reacts to it. No workflow
creates a tag. This is the same rule root `CLAUDE.md` applies to push, PR and merge — the act that
leaves the machine is a human's.
- **The repository is public, and the documentation site is bound to that.** Made public on 2026-09-17;
the site goes public with the code or not at all, so there is deliberately no separate repository
holding the built site and no paid JitPack plan. What is still not true is that anything has been
*released*: until a `v*` tag exists, the JitPack coordinate resolves for nobody and
`publishToMavenLocal` is the only route that works. That is stated wherever it matters rather than
papered over.
- **The library is a Gradle subproject of a repository that holds only it.** The reasoning is above,
under "Why the library is a subproject and not the root project"; the short version is that
flattening the layout would rename the published artifact.
- **The original plan tied the documentation site to public source.** Published tags now exist and the alpha's JitPack metadata is available. Check current access before
the next release; do not infer visibility, publication or artifact access from the original plan.
- **The library is a Gradle subproject, not the root project.** The reasoning is above,
under "Why the library is a subproject and not the root project". Publishing one module is what
preserves the JitPack artifact name; the directory name alone does not determine it.
- **Documentation is not versioned** until the library is past alpha. One site, no version selector, with
the version stamped on every page.
- **Bugs go to GitHub Issues, questions to GitHub Discussions,** both on `Horizon-36596/simloop`. Both
are public and leave a record the next person with the same problem can find.
- **Bugs go to GitHub Issues, questions to GitHub Discussions,** both on `Horizon-36596/simloop`.
Access follows the repository's current visibility and permissions.

### Settled on 2026-09-17: the package and the Maven group

Expand Down
15 changes: 11 additions & 4 deletions SimLoop/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,19 @@ tasks.register('checkDocsVersion') {
}
}

// `--strict` turns a broken internal link into a failed build. A documentation site's most common defect
// is a link that goes nowhere, and it is invisible to everyone except the reader who followed it.
tasks.register('checkDocExamples', Exec) {
group = 'documentation'
description = 'Checks that the complete tutorial example matches its compiled test source.'
workingDir projectDir
commandLine 'python', 'docs-tools/check-examples.py'
}

// `--strict` fails the build on broken guide links. Generated Javadoc is copied by docsSite afterwards.
tasks.register('mkdocsBuild', Exec) {
group = 'documentation'
description = 'Renders the markdown pages. Needs, in this directory: pip install -r docs-requirements.txt'

dependsOn 'checkDocsVersion'
dependsOn 'checkDocsVersion', 'checkDocExamples'

workingDir projectDir
commandLine 'python', '-m', 'mkdocs', 'build', '-f', 'mkdocs.yml', '--strict'
Expand All @@ -426,7 +432,8 @@ tasks.register('docsSite', Copy) {
def builtSiteDir = layout.buildDirectory.dir('docs/site').get().asFile
doLast {
logger.lifecycle('Documentation site built: ' + builtSiteDir)
logger.lifecycle('Read it with: python -m mkdocs serve - run from the SimLoop directory.')
logger.lifecycle('Preview the complete site: python -m http.server 8000 --bind 127.0.0.1 --directory "'
+ builtSiteDir + '"')
}
}

Expand Down
Loading
Loading