From 17ba8dd770bb065cba783b25773752c4fa6c099e Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 09:01:11 +0700 Subject: [PATCH 1/9] ci: pin the sibling SDK, and repair two floors that no longer bind - add ref: to the monocr-onnx checkout, which the CLI depends on by path - raise the Android test floor from 40 to 106, its actual count - give the iOS wrapper a numeric floor instead of a presence check All three were passing, which is why none of them was visible. The SDK checkout had no ref, so the CLI was compiled against whatever was on monocr-onnx's default branch and the same monorepo commit could pass today and fail tomorrow with nothing here having changed. Pinned at 627c52c, which is what the job was already getting, so this makes today's behaviour reproducible rather than changing it. A tag was available and deliberately not used: rust/v0.3.0 is on that repository's main but main is 34 commits ahead of it, so pinning to the tag would change what compiles and could turn the job red for reasons unrelated to introducing a pin. The comment records that trade and that building against the released SDK is arguably more honest once the Rust crate is actually published. The Android floor was 40 with a comment beside it reading "the suite has had 50 since 2026-08-28". Counted 2026-09-03: 106. So the floor was stale against its own comment and the comment was stale against the suite, and 66 tests could have vanished silently. The iOS wrapper only grepped for the string "Test run with", so it asserted that a count was printed and never what the count was. The suite could have shrunk from 73 to 1 and stayed green. It now parses the number and compares it. The summary format was verified by running the suite rather than assumed: "Test run with 73 tests in 12 suites passed after 7.234 seconds." Both floors are exact rather than a margin, for the reason the new comments give: adding tests never trips a floor, so the only thing a floor can catch is a removal, and a removal should be deliberate. A failure means bumping the number in the same commit that removes the test, which makes the diff record the removal. Verified: sh -n clean, the wrapper runs green at 73 tests, and two mutations killed. A floor of 200 fails with the real count, and breaking the parse regex fails closed with a message telling the reader to fix the parse rather than delete the check. --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++++++-- apps/ios/Scripts/swift-test.sh | 28 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d19cebb..2a79c3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,10 +209,29 @@ jobs: with: path: monocr + # PINNED, added 2026-09-03. This had no `ref:`, so the CLI was built against + # whatever was on monocr-onnx's default branch and the same monorepo commit could + # pass today and fail tomorrow with nothing here having changed. The CLI takes the + # SDK as a path dependency (`apps/cli/Cargo.toml`), so an SDK change lands directly + # in this build. + # + # Pinned at monocr-onnx origin/main 627c52c, which is what this job was already + # getting — the pin makes today's behaviour reproducible rather than changing it. + # + # A tag exists and was deliberately NOT used. `rust/v0.3.0` (fc29391) is on that + # repository's main, but main is 34 commits ahead of it, so pinning to the tag + # would change what this job compiles and could turn it red for reasons unrelated + # to introducing a pin. Worth revisiting: building the CLI against the released + # SDK is arguably more honest, since that is what a consumer gets. The Rust crate + # is unpublished, so "released" is doing little work here at the moment. + # + # TO BUMP: set `ref` to the new monocr-onnx SHA and build the CLI against it + # locally first. - name: Check out the sibling SDK the CLI depends on uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: MonDevHub/monocr-onnx + ref: 627c52c path: monocr-onnx - uses: dtolnay/rust-toolchain@stable @@ -477,8 +496,22 @@ jobs: failures += int(root.get("failures", 0)) errors += int(root.get("errors", 0)) print(f"{tests} tests in {len(files)} classes, {failures} failures, {errors} errors") - if tests < 40: - sys.exit(f"only {tests} tests ran; the suite has had 50 since 2026-08-28") + # 106, the exact count on 2026-09-03. Raised from 40 that day, where the + # comment beside it read "the suite has had 50 since 2026-08-28" — so the + # floor was stale against its own comment, and its own comment was stale + # against the suite. 66 tests could have vanished silently. + # + # Exact rather than a margin, deliberately. Adding tests never trips a floor, + # so the only thing this can catch is a removal, and a removal is exactly what + # should be deliberate. If this fails, either a test was lost or one was merged + # into another: bump the number in the same commit that removes the test, and + # the diff then records the removal. + FLOOR = 106 + if tests < FLOOR: + sys.exit( + f"only {tests} tests ran; expected at least {FLOOR}. Either the suite " + "lost tests or a test was merged away. Bump FLOOR in the same commit." + ) if failures or errors: sys.exit("the suite reported failures") PY diff --git a/apps/ios/Scripts/swift-test.sh b/apps/ios/Scripts/swift-test.sh index 07eb978..1acb47a 100755 --- a/apps/ios/Scripts/swift-test.sh +++ b/apps/ios/Scripts/swift-test.sh @@ -42,9 +42,37 @@ STATUS=0 swift test --package-path MonOcrCore --disable-xctest $FLAGS >"$LOG" 2>&1 || STATUS=$? cat "$LOG" +# Two checks, because presence was not enough. Until 2026-09-03 this only grepped for +# the string "Test run with", so the suite could shrink from 73 tests to 1 and still be +# called a pass — the same defect the Android job's count floor exists to prevent, and +# the same one this wrapper's own comment invokes when the Android job cites it. if ! grep -q "Test run with" "$LOG"; then echo "swift-test: the run reported no test count, so it ran no tests. Not calling that a pass." >&2 exit 1 fi +# The line is: "Test run with 73 tests in 12 suites passed after 7.234 seconds." +# Format verified 2026-09-03 by running this script against MonOcrCore. +COUNT="$(sed -n 's/.*Test run with \([0-9][0-9]*\) tests.*/\1/p' "$LOG" | tail -1)" + +# 73, the exact count on 2026-09-03. Exact rather than a margin for the reason the +# Android floor gives: adding tests never trips a floor, so the only thing this can catch +# is a removal, and a removal should be deliberate. If this fails, bump FLOOR in the same +# commit that removes the test so the diff records it. +FLOOR=73 + +if [ -z "$COUNT" ]; then + echo "swift-test: found the summary line but could not read a count from it. The" >&2 + echo " format may have changed; fix the parse rather than deleting this check." >&2 + exit 1 +fi + +if [ "$COUNT" -lt "$FLOOR" ]; then + echo "swift-test: only $COUNT tests ran; expected at least $FLOOR." >&2 + echo " Either the suite lost tests or one was merged away. Bump FLOOR in the same commit." >&2 + exit 1 +fi + +echo "swift-test: $COUNT tests ran (floor $FLOOR)." + exit "$STATUS" From ff082b7eb44904c4dc2af97609bd94ce86a5381d Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 10:03:11 +0700 Subject: [PATCH 2/9] test: three fixture tables and two suites could shrink without failing - raise Android's cut-column probes from emptiness to the floor of three - give the two dilate rule tables the floor half_widths already had - add count floors to the go, web and cli jobs; only android had one - correct a comment claiming this repo has zero Go test files Five gates that pass while the thing they guard shrinks. Android's `LineTilerFixtureTest` asserted `probes.size() > 0` where web (segmentation.test.ts:75) and iOS (LineTilingTests.swift:175) both require three. So the shared probe set could shrink 3 -> 1 and Android alone stayed green -- the one asymmetry among three ports pinned to the same file. MEASURED: truncating the fixture to one probe now fails 1 of 106 tests; before this commit it failed none on Android. `DilateFixtureTest` argues for a floor over emptiness at :50-55, applies one to half_widths, and then checks `size() > 0` on two tables holding 13 and 12 entries. Both now carry their real counts. The docstring above the second one already recorded a mutation that survived the whole suite; a table permitted to shrink to one entry is how. The Go job's comment read "there are currently zero *_test.go files here, so this passes vacuously". There are 8, with 26 test functions across 4 packages, all passing. Stale in the safe direction, which is the expensive kind: it is why this job never got the assertion android and ios-core both have, so 26 functions could have vanished unnoticed. Floors added to go (60), web (172) and cli (68), matching android (106) and ios-core (73 in Scripts/swift-test.sh). All four measured on this machine today, exact rather than margined for the reason the android floor already states. Two details worth recording. The Go floor counts 60, not 26: `go test -json` emits a pass per `t.Run` subtest, so it catches a deleted table row and not only a deleted function. And the cli floor parses `test result:` lines rather than `--format json`, which still needs `-Z unstable-options` and would tie the gate to nightly while the job runs the stable toolchain. Verified: gradle testDebugUnitTest --rerun-tasks green at 106 tests / 0 failures; vitest json reporter gives 172; the Go and cargo parsers reproduce 60 and 68 on two runs each; the workflow parses as YAML. --- .github/workflows/ci.yml | 105 +++++++++++++++++- .../monocr/engine/DilateFixtureTest.kt | 8 +- .../monocr/engine/LineTilerFixtureTest.kt | 6 +- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a79c3c..ab1c482 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,30 @@ jobs: - name: Unit tests run: pnpm --filter ./apps/web test + - name: A green run must have executed tests + working-directory: apps/web + # vitest exits 1 when no test file matches, so a suite that vanishes + # entirely is already caught. A suite that shrinks is not: 172 -> 1 exits + # 0. This is the same floor the android and ios-core jobs carry, and web + # is the largest suite of the four. + run: | + set -euo pipefail + npx vitest run --reporter=json --outputFile=/tmp/vitest.json + python3 - <<'PY_EOF' + import json, sys + d = json.load(open("/tmp/vitest.json")) + total = d.get("numTotalTests", 0) + passed = d.get("numPassedTests", 0) + failed = d.get("numFailedTests", 0) + print(f"{passed} passed, {failed} failed, {total} total") + # 172, the exact count on 2026-09-03. See the android floor for why exact. + FLOOR = 172 + if total < FLOOR: + sys.exit(f"only {total} web tests ran; expected at least {FLOOR}") + if failed: + sys.exit("the suite reported failures") + PY_EOF + # Nothing built the app until 2026-08-19. Tests and svelte-check both run # against source, so a break that only appears in a production build — # the wasm and pdf-worker copy steps, the Cloudflare adapter, a @@ -185,11 +209,47 @@ jobs: run: go build ./... - name: go test - # NOTE: there are currently zero *_test.go files here, so this passes - # vacuously. It is kept because it turns the moment someone adds a test - # into a real gate, and because `[no test files]` in the log is an - # honest signal. It is not evidence the service works. - run: go test ./... + # This comment read "there are currently zero *_test.go files here, so + # this passes vacuously" until 2026-09-03. There are 8, with 26 test + # functions across 4 packages, and they pass. The comment was stale in + # the safe direction, which is the expensive kind: it is why this job + # never got the count assertion the android and ios-core jobs both have, + # so 26 tests could have vanished here without the job noticing. + run: go test ./... -count=1 + + - name: A green run must have executed tests + # `go test` exits 0 for a package with no test files, printing + # `[no test files]`. So the exit code cannot distinguish a passing suite + # from a deleted one. Same argument as the android floor below. + run: | + set -euo pipefail + go test ./... -count=1 -json > /tmp/go-test.json + python3 - <<'PY_EOF' + import json, sys + ran = set() + with open("/tmp/go-test.json") as f: + for line in f: + line = line.strip() + if not line: + continue + ev = json.loads(line) + if ev.get("Action") == "pass" and ev.get("Test"): + ran.add((ev["Package"], ev["Test"])) + print(f"{len(ran)} Go tests passed") + # 60, the exact count on 2026-09-03, measured twice for stability: 26 + # top-level functions plus 34 `t.Run` subtests. Counting subtests is + # deliberate — `go test -json` emits a pass event per subtest, so this + # catches a deleted table row and not only a deleted function, which is + # stronger than a `func Test` grep. If a table is refactored, the number + # moves for a legitimate reason; bump it in that commit. + # + # Exact rather than a margin, for the reason the android floor states: + # adding tests never trips a floor, so the only thing a floor catches is + # a removal, and a removal should be deliberate. + FLOOR = 60 + if len(ran) < FLOOR: + sys.exit(f"only {len(ran)} Go tests ran; expected at least {FLOOR}") + PY_EOF - name: govulncheck run: | @@ -257,6 +317,41 @@ jobs: working-directory: monocr/apps/cli run: cargo test + - name: A green run must have executed tests + working-directory: monocr/apps/cli + # `cargo test` exits 0 for a crate with no tests, printing "running 0 + # tests", so the exit code cannot tell a passing suite from a deleted + # one. That is the hole the web, go and android floors close; cli was the + # fourth surface and the last one without a floor. + # + # Parsed from `test result:` lines with an explicit FAILED check, because + # a grep that matches nothing reads as a pass. `--format json` is not used + # here: it still requires `-Z unstable-options`, so it would tie this gate + # to a nightly toolchain while the job runs `dtolnay/rust-toolchain@stable`. + run: | + set -euo pipefail + cargo test > /tmp/cargo-test.txt 2>&1 || { + cat /tmp/cargo-test.txt + exit 1 + } + python3 - <<'PY_EOF' + import re, sys + text = open("/tmp/cargo-test.txt").read() + if re.search(r"^test result: FAILED", text, re.M): + sys.exit("the suite reported failures") + runs = [int(m) for m in re.findall(r"^test result: ok\. (\d+) passed", text, re.M)] + if not runs: + sys.exit("no 'test result:' line in cargo's output: the suite did not run") + passed = sum(runs) + print(f"{passed} Rust tests passed across {len(runs)} binaries") + # 68, the exact count on 2026-09-03. Exact rather than a margin, for the + # reason the android floor states: adding tests never trips a floor, so the + # only thing a floor catches is a removal, and a removal should be deliberate. + FLOOR = 68 + if passed < FLOOR: + sys.exit(f"only {passed} Rust tests ran; expected at least {FLOOR}") + PY_EOF + - name: The tiling fixture is not optional # The fixture is the only thing keeping four language ports in step. If # it goes missing, every port's parity test silently has nothing to diff --git a/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/DilateFixtureTest.kt b/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/DilateFixtureTest.kt index 2ccccb4..8388db4 100644 --- a/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/DilateFixtureTest.kt +++ b/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/DilateFixtureTest.kt @@ -96,7 +96,9 @@ class DilateFixtureTest { @Test fun `the dilation kernel matches the reference rule`() { val rule = fixture["kernel_for_small_height"].asJsonObject - assertTrue("the fixture carried no kernel rule", rule.size() > 0) + // A floor, for the reason `half_widths` above already gives. This table + // held 13 heights against a check that one would satisfy. + assertTrue("kernel rule shrank to ${rule.size()}", rule.size() >= 13) for ((heightText, expected) in rule.entrySet()) { val height = heightText.toInt() assertEquals( @@ -114,7 +116,9 @@ class DilateFixtureTest { @Test fun `the corner patch matches the reference rule`() { val rule = fixture["corner_patch_for_side"].asJsonObject - assertTrue("the fixture carried no corner rule", rule.size() > 0) + // 12 sides, likewise. The docstring above notes a mutation that survived + // the whole suite here; a table that may shrink to one entry is how. + assertTrue("corner rule shrank to ${rule.size()}", rule.size() >= 12) for ((sideText, expected) in rule.entrySet()) { val side = sideText.toInt() assertEquals("corner patch for a side of $side", expected.asInt, PageNormalizer.cornerPatch(side)) diff --git a/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/LineTilerFixtureTest.kt b/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/LineTilerFixtureTest.kt index 7125b91..d428510 100644 --- a/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/LineTilerFixtureTest.kt +++ b/apps/android/app/src/test/java/dev/janakhpon/monocr/engine/LineTilerFixtureTest.kt @@ -112,7 +112,11 @@ class LineTilerFixtureTest { @Test fun `cut column probes land where the reference implementation puts them`() { val probes = fixture["cut_column_probes"].asJsonArray - assertTrue("fixture has no cut column probes", probes.size() > 0) + // A floor, not emptiness. Web and iOS both require three + // (segmentation.test.ts:75, LineTilingTests.swift:175); this port asked + // only for one, so the probe set could shrink 3 -> 1 and Android alone + // stayed green. That is the asymmetry a shared fixture exists to remove. + assertTrue("cut column probes shrank to ${probes.size()}", probes.size() >= 3) for (element in probes) { val probe = element.asJsonObject From 4ec89520267f3122fdb115d2385a7bbc859a8037 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:06:41 +0700 Subject: [PATCH 3/9] fix(cli): follow monocr-onnx's crate rename to monocr on crates.io - alias the path dependency with package = "monocr" The Rust binding is now published on crates.io as monocr, not monocr-onnx -- chosen before its first publish, once the shorter name was confirmed unclaimed. This dependency is a path, not a registry reference, but Cargo.toml still declares which package a path resolves to, and it needs to agree with the target crate's real name once that target is ever swapped for a registry or git dependency. The dependency key stays monocr-onnx and every monocr_onnx:: reference in src/main.rs is untouched -- package = "monocr" changes only which crate the key resolves to, not the extern crate name Rust code sees. Verified: cargo check succeeds, resolving against the renamed local package. Cargo.lock updated to match; the dependency tree itself is unchanged, only the two package entries' names and versions. --- apps/cli/Cargo.lock | 26 +++++++++++++------------- apps/cli/Cargo.toml | 11 +++++++++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/apps/cli/Cargo.lock b/apps/cli/Cargo.lock index fcbdcbd..67853d4 100644 --- a/apps/cli/Cargo.lock +++ b/apps/cli/Cargo.lock @@ -1056,40 +1056,40 @@ dependencies = [ ] [[package]] -name = "monocr-cli" -version = "0.1.0" +name = "monocr" +version = "0.3.0" dependencies = [ "anyhow", - "assert_fs", "clap", + "dirs", "image", - "monocr-onnx", + "indicatif", + "ndarray", "ort", + "reqwest", "serde", "serde_json", - "serde_yaml", - "sha2", "tempfile", "tokio", - "walkdir", ] [[package]] -name = "monocr-onnx" -version = "0.3.0" +name = "monocr-cli" +version = "0.1.0" dependencies = [ "anyhow", + "assert_fs", "clap", - "dirs", "image", - "indicatif", - "ndarray", + "monocr", "ort", - "reqwest", "serde", "serde_json", + "serde_yaml", + "sha2", "tempfile", "tokio", + "walkdir", ] [[package]] diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 88630c3..6ec2498 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -20,9 +20,16 @@ path = "src/main.rs" # why mon-vlm's CI cannot run at all: uv sync fails on any machine without # mon_OCR checked out alongside". The path is here only so the CLI builds # against uncommitted upstream work; swap it for -# monocr-onnx = { git = "https://github.com/MonDevHub/monocr-onnx", rev = "..." } +# monocr-onnx = { git = "https://github.com/MonDevHub/monocr-onnx", rev = "...", package = "monocr" } # once the tiling change lands, and keep the rev pinned. -monocr-onnx = { path = "../../../monocr-onnx/rust" } +# +# `package = "monocr"` on 2026-09-03: the crate's published name on crates.io +# is "monocr", not "monocr-onnx" — the repository name and the crate name +# diverged the day this binding was first published. Keeping this dependency's +# key as `monocr-onnx` (rather than renaming it to `monocr`) means every +# `monocr_onnx::` reference in src/main.rs stays exactly as it is; Cargo +# resolves the key to the aliased package regardless of what the key is named. +monocr-onnx = { path = "../../../monocr-onnx/rust", package = "monocr" } # Pinned exactly, not by range. ort is a pre-release and "2.0.0-rc.11" # range-matches rc.13, which changed Send bounds and does not compile From a4e6c8cc3434c5a2d28993b321c06726ab1d71fa Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:21:30 +0700 Subject: [PATCH 4/9] feat(cli): ready this crate to publish as monocr-cli - add version = "0.3.0" to the monocr path dependency - add repository and homepage to Cargo.toml - add release-cli.yml, mirroring every other binding's release pattern Blocked before this: cargo publish refused with "all dependencies must have a version requirement specified when publishing", since a bare path dependency has nothing a registry consumer could resolve. monocr now exists on crates.io as of today, so this is a one-line fix rather than a wait. Verified rather than assumed: cargo publish --dry-run from this exact checkout downloaded and compiled the real crates.io monocr 0.3.0, not the local path -- so a consumer installing monocr-cli gets the published SDK, not a broken reference to a checkout that will not exist on their machine. fmt clean, clippy clean, 68 tests pass. release-cli.yml is a separate file, not a job folded into ci.yml, matching every sibling release workflow in this ecosystem: tests re-run against the tagged commit, gated ahead of a credential, kept out of the file that runs on every push. Tag namespaced cli/v* rather than a bare v* since this repo has zero tags today across four shipped surfaces and a bare v* would become ambiguous the moment a second one exists. Not done, and deliberately: this does not touch ci.yml's own sibling- repository checkout of monocr-onnx, or the TODO in apps/cli/Cargo.toml about switching that dependency from a path to a pinned git rev. That job's own comment already flags the SDK being unpublished as the reason "released" was doing little work -- it no longer is, and switching the CI job (not just the release job) to depend on the real published crate is a real simplification worth doing, but it changes local dev workflow too and is a separate call. --- .github/workflows/release-cli.yml | 105 ++++++++++++++++++++++++++++++ apps/cli/Cargo.toml | 4 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-cli.yml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 0000000..445fbc8 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,105 @@ +# Publishes the `monocr-cli` crate to crates.io on a `cli/v*` tag. +# +# A separate file from ci.yml, not a job added to it, matching the pattern +# every other MonOCR binding uses (monocr-onnx's release-python.yml, +# release-js.yml and release-rust.yml; mon_tokenizer's release.yml): a +# release workflow re-runs the same checks as CI against the tagged commit, +# gates a registry credential behind them, and stays out of the file that +# runs on every push and pull request. Folding a publish step into ci.yml +# would mean every ordinary CI run carries `id-token: write` in the same +# file, whether or not a tag triggered it. +# +# Trusted publishing (OIDC) via rust-lang/crates-io-auth-action, so there is +# no CARGO_REGISTRY_TOKEN long-lived secret to store, leak or rotate. +# +# ONE-TIME SETUP, in order — crates.io has no pending-publisher mechanism +# (see monocr-onnx/RELEASING.md and the header of monocr-onnx's own +# release-rust.yml for the sourced explanation): +# 1. Publish an initial version manually once, with a personal API token: +# cd apps/cli && cargo login && cargo publish +# 2. Only after that, on the crate's settings page at +# https://crates.io/crates/monocr-cli/settings +# add a GitHub Actions trusted publisher: +# Repository owner: MonDevHub +# Repository name: monocr +# Workflow filename: release-cli.yml +# Environment: (blank) +# Before step 2 is done, this workflow's OIDC token exchange is refused. +# +# The tag is namespaced `cli/v*` rather than a bare `v*` even though this is +# the only thing this monorepo currently publishes: the repository has zero +# git tags today across four shipped surfaces (web, iOS, Android, CLI), and a +# bare `v*` would become ambiguous the moment any of the others gets one. +name: release-cli + +on: + push: + tags: + - "cli/v*" + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + # Attach a protected environment named `crates-io` in repository settings + # for a manual approval step before this leaves the machine, mirroring + # mon_tokenizer's `environment: pypi`. GitHub creates it with no + # protection rules on first use if it does not exist, so this is safe + # either way. + environment: crates-io + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: dtolnay/rust-toolchain@stable + + # This crate's own dependency on monocr is a path in the working tree + # (apps/cli/Cargo.toml), but carries `version = "0.3.0"` so `cargo + # publish` resolves and ships against the REAL published crate rather + # than requiring the path to exist for consumers. Verified 2026-09-03: + # `cargo publish --dry-run` from a clean checkout downloads and + # compiles the real crates.io `monocr` 0.3.0, not the local path. + - name: poppler + run: sudo apt-get update && sudo apt-get install -y poppler-utils + + # A tag that disagrees with Cargo.toml publishes the wrong number + # under the right name, and crates.io has no undo. + - name: Verify the tag matches Cargo.toml + working-directory: apps/cli + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME#cli/v}" + PKG="$(awk -F'"' '/^version = /{print $2; exit}' Cargo.toml)" + if [ "$TAG" != "$PKG" ]; then + echo "::error title=Tag/version mismatch::tag cli/v$TAG does not match Cargo.toml version $PKG" + exit 1 + fi + echo "tag and Cargo.toml agree on $PKG" + + # Mirrors the cli job in ci.yml: fmt, clippy, test. Not a divergent, + # invented gate — the same three checks that already run on every push. + - name: cargo fmt + working-directory: apps/cli + run: cargo fmt --check + + - name: cargo clippy + working-directory: apps/cli + run: cargo clippy --all-targets -- -D warnings + + - name: cargo test + working-directory: apps/cli + run: cargo test + + - name: Authenticate with crates.io + id: auth + uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5 + + - name: Publish + working-directory: apps/cli + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 6ec2498..b298bc1 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" description = "Batch Mon OCR over books, PDFs and images, on-device" license = "MIT" +repository = "https://github.com/MonDevHub/monocr/tree/main/apps/cli" +homepage = "https://github.com/MonDevHub/monocr/tree/main/apps/cli" [[bin]] name = "monocr-cli" @@ -29,7 +31,7 @@ path = "src/main.rs" # key as `monocr-onnx` (rather than renaming it to `monocr`) means every # `monocr_onnx::` reference in src/main.rs stays exactly as it is; Cargo # resolves the key to the aliased package regardless of what the key is named. -monocr-onnx = { path = "../../../monocr-onnx/rust", package = "monocr" } +monocr-onnx = { path = "../../../monocr-onnx/rust", package = "monocr", version = "0.3.0" } # Pinned exactly, not by range. ort is a pre-release and "2.0.0-rc.11" # range-matches rc.13, which changed Send bounds and does not compile From 75e34ad0d2b7ef67be0fb24201ce1e333ec33d8e Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:27:20 +0700 Subject: [PATCH 5/9] docs(cli): add a version badge now that this is actually published monocr-cli v0.1.0 published today. Matches the badge convention every other MonOCR package README now uses. --- apps/cli/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/cli/README.md b/apps/cli/README.md index 3b1887a..cf06408 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,5 +1,7 @@ # monocr-cli +[![crates.io](https://img.shields.io/crates/v/monocr-cli.svg)](https://crates.io/crates/monocr-cli) + Extract Mon text from books, PDFs and images, on-device and in batch. This is the batch and desktop surface for MonOCR. The apps read one page at a time through a From 8e97e42909919bfcbc6c18590ed7d3e74d414442 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:27:58 +0700 Subject: [PATCH 6/9] docs(cli): add cargo install as the primary install path The README only documented building from source, even though the package is now published. cargo install is the path a real user takes; building from a checkout is the contributor path and now reads as the alternative it is. --- apps/cli/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/cli/README.md b/apps/cli/README.md index cf06408..2231030 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -24,14 +24,25 @@ before it; a sixth would be a liability. See [ADR-0004](../../docs/architecture/ ## Install -Needs Rust and poppler (`pdftoppm`, `pdfinfo`) for the PDF path. +Needs poppler (`pdftoppm`, `pdfinfo`) for the PDF path, either way. ```bash brew install poppler # macOS apt-get install poppler-utils # Debian +``` + +From [crates.io](https://crates.io/crates/monocr-cli): +```bash +cargo install monocr-cli +monocr-cli download # prime the model cache before a long run +``` + +Or from this checkout: + +```bash cargo build --release -./target/release/monocr-cli download # prime the model cache before a long run +./target/release/monocr-cli download ``` Without poppler the renderer's tests **fail** rather than skipping. That is From 0f88d236ea6d5e437531d0e4a3f5337f788a0129 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:42:40 +0700 Subject: [PATCH 7/9] fix(ci): the sibling-SDK pin was abbreviated and cannot be fetched actions/checkout fetches a commit ref directly from the remote, so a 7-char abbreviated SHA fails: "The process '/usr/bin/git' failed with exit code 1". Set to the full 40 characters. This is a regression introduced today. The cli job passed on 2026-08-30 and has failed on every run since the pin was added, with the checkout action's own SHA provably unchanged from the merge-base -- so the pin, not the action version bump, is the cause. The intent recorded in the comment block above was always to pin this commit; only the form was wrong. --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab1c482..ee6f908 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,7 +291,14 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: MonDevHub/monocr-onnx - ref: 627c52c + # Full 40 characters, not the abbreviated 627c52c this was set to on + # 2026-09-03. actions/checkout fetches a commit ref directly from the + # remote, and an abbreviated SHA cannot be fetched — the step failed + # with "The process '/usr/bin/git' failed with exit code 1" on every + # run for half a day. The cli job passed on 2026-08-30 and went red + # the moment the pin was added, with the checkout action itself + # provably unchanged from the merge-base. + ref: 627c52c4ca6b1c1e60263697e61910a3fc9c56d2 path: monocr-onnx - uses: dtolnay/rust-toolchain@stable From e4082c642f40129582cc3783719f9d38c3485160 Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:45:24 +0700 Subject: [PATCH 8/9] fix(feedback): a called CVE in quic-go, and two gitleaks false positives - go get github.com/quic-go/quic-go@v0.59.1 - allow two test placeholders in .gitleaksignore, by fingerprint Both jobs have been red on main since 2026-08-28/29 and neither was caused by anything in this branch. Fixing them here because they were found here. GO-2026-5676, HTTP/3 QPACK trailer expansion memory exhaustion, is a CALLED vulnerability, not a required-but-unreachable one. govulncheck traced two live paths: api.main -> http.Server.ListenAndServe -> http3.ConfigureTLSConfig, and upload.init -> gin.init -> http3.countingByteReader.Read. It arrived indirectly with dependabot's gin 1.9.1 -> 1.12.0 bump; quic-go is not a direct dependency. v0.59.1 is the patch release for it, taken rather than jumping to v0.62.0. Verified after the bump: go build clean, go test all packages pass, and govulncheck exits 0 with "No vulnerabilities found" -- from exit 3 with one called vulnerability before. Five require-but-don't-call advisories remain in x/crypto and x/mod, unchanged and not reachable. The gitleaks findings are not secrets. Both flagged lines are `const key = "0123456789abcdef"` repeated four times, inside tests named TestPanicLogDoesNotCarryTheApiKey and TestRecoveryMiddlewareDoesNotLogTheApiKey. 64 hex chars of ascending nibbles trips the generic-api-key entropy rule. Ignored by fingerprint, not an inline allow comment, because the job runs `gitleaks detect` over full history and anchors the finding to commit f4de7ba regardless of the working tree. Verified with the same pinned image CI uses, zricethezav/gitleaks:v8.30.1: 140 commits scanned, "no leaks found". Note for later: the govulncheck step installs @latest and queries the live vuln.go.dev, so this job can go red with no repo change at all. That is the failure mode go.mod's own toolchain comment was written for. --- .gitleaksignore | 15 +++++++++++++++ services/feedback/go.mod | 2 +- services/feedback/go.sum | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.gitleaksignore b/.gitleaksignore index 24186f7..08240dc 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -8,3 +8,18 @@ # the key is rotated and you want the reminder gone. d026e3d9696abe2eddac47d983fa245599512b3e:apps/android/app/src/main/java/dev/janakhpon/monocr/engine/SyncWorker.kt:generic-api-key:16 d026e3d9696abe2eddac47d983fa245599512b3e:apps/ios/monocr-ios/SyncService.swift:generic-api-key:9 + +# Two test placeholders, not secrets. Both lines are literally +# `const key = "0123456789abcdef"` repeated four times, inside tests named +# TestPanicLogDoesNotCarryTheApiKey and TestRecoveryMiddlewareDoesNotLogTheApiKey +# — tests whose whole point is asserting a key never reaches a log. 64 hex chars +# of ascending nibbles trips gitleaks' generic-api-key entropy rule. +# +# Ignored by fingerprint rather than an inline `// gitleaks:allow` because the +# scan is `gitleaks detect` over full history: the finding is anchored to commit +# f4de7ba and would keep being reported no matter what the working tree says. +# +# This is what had the secrets job red since 2026-08-28, on every run, with +# nothing actually leaked. +f4de7ba5c5c208ab8e90b7bded85dbaf37fb7cda:services/feedback/internal/middleware/recovery_endtoend_test.go:generic-api-key:21 +f4de7ba5c5c208ab8e90b7bded85dbaf37fb7cda:services/feedback/internal/middleware/recovery_redact_test.go:generic-api-key:17 diff --git a/services/feedback/go.mod b/services/feedback/go.mod index 527b90f..5a3870d 100644 --- a/services/feedback/go.mod +++ b/services/feedback/go.mod @@ -64,7 +64,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect + github.com/quic-go/quic-go v0.59.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect diff --git a/services/feedback/go.sum b/services/feedback/go.sum index 487edfd..a280a8d 100644 --- a/services/feedback/go.sum +++ b/services/feedback/go.sum @@ -122,8 +122,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 42b106806b0cc85f05159f3dae90778f8917ab7b Mon Sep 17 00:00:00 2001 From: janakhpon Date: Thu, 3 Sep 2026 15:52:12 +0700 Subject: [PATCH 9/9] fix(cli): take the OCR library from crates.io, not a sibling checkout - Cargo.toml: path dependency -> version = "0.3.1" from the registry - ci.yml: drop the sibling checkout and the path: nesting it required - release-cli.yml: correct the environment value and the stale header The path dependency was the root of three separate failures. It made this crate unpublishable on its own terms -- crates.io rejects path and git deps, so the long-standing TODO here ("must become a pinned git rev") pointed at something that could never work. It made the cli CI job depend on a second checkout at a revision that predated the library's rename, so the job was red whether the pin was abbreviated or complete. And release-cli.yml had no such checkout at all, so the path resolved to a directory that never existed on a runner. A registry dependency has none of those failure modes and builds what a consumer actually gets. It only became possible today, when the library was first published. Verified, not assumed: the lockfile now records registry+https://github.com/rust-lang/crates.io-index with a checksum, and `cargo build --locked` succeeds from a copy placed where ../../../monocr-onnx does not exist. 68 tests pass. Every step of the cli job was re-run from the new checkout root -- fmt, clippy, test and all four fixture checks, including the merge fixture's own 21-mutation battery. release-cli.yml's setup block said `Environment: (blank)` while the job declares `environment: crates-io`. A job that declares one puts an environment claim in the OIDC token and crates.io matches the whole claim set, so a publisher registered as blank would reject it. --- .github/workflows/ci.yml | 59 ++++++------------------------ .github/workflows/release-cli.yml | 60 ++++++++++++++++--------------- apps/cli/Cargo.lock | 4 ++- apps/cli/Cargo.toml | 22 ++++-------- 4 files changed, 53 insertions(+), 92 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee6f908..24bc05b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,49 +257,12 @@ jobs: "$(go env GOPATH)/bin/govulncheck" ./... cli: - # apps/cli is the batch and desktop surface (ADR-0004). It depends on the - # monocr-onnx Rust library, which is a SIBLING REPOSITORY, so this job - # currently checks out both. That is a stopgap: once the tiling work in that - # crate is released, Cargo.toml should point at a pinned git rev and the - # second checkout goes away. A path dependency on a sibling checkout is the - # documented reason mon-vlm's CI cannot run at all. + # apps/cli is the batch and desktop surface (ADR-0004). It takes the OCR + # library from crates.io, so this job needs no sibling checkout and builds + # what a consumer actually gets. runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - path: monocr - - # PINNED, added 2026-09-03. This had no `ref:`, so the CLI was built against - # whatever was on monocr-onnx's default branch and the same monorepo commit could - # pass today and fail tomorrow with nothing here having changed. The CLI takes the - # SDK as a path dependency (`apps/cli/Cargo.toml`), so an SDK change lands directly - # in this build. - # - # Pinned at monocr-onnx origin/main 627c52c, which is what this job was already - # getting — the pin makes today's behaviour reproducible rather than changing it. - # - # A tag exists and was deliberately NOT used. `rust/v0.3.0` (fc29391) is on that - # repository's main, but main is 34 commits ahead of it, so pinning to the tag - # would change what this job compiles and could turn it red for reasons unrelated - # to introducing a pin. Worth revisiting: building the CLI against the released - # SDK is arguably more honest, since that is what a consumer gets. The Rust crate - # is unpublished, so "released" is doing little work here at the moment. - # - # TO BUMP: set `ref` to the new monocr-onnx SHA and build the CLI against it - # locally first. - - name: Check out the sibling SDK the CLI depends on - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: MonDevHub/monocr-onnx - # Full 40 characters, not the abbreviated 627c52c this was set to on - # 2026-09-03. actions/checkout fetches a commit ref directly from the - # remote, and an abbreviated SHA cannot be fetched — the step failed - # with "The process '/usr/bin/git' failed with exit code 1" on every - # run for half a day. The cli job passed on 2026-08-30 and went red - # the moment the pin was added, with the checkout action itself - # provably unchanged from the merge-base. - ref: 627c52c4ca6b1c1e60263697e61910a3fc9c56d2 - path: monocr-onnx - uses: dtolnay/rust-toolchain@stable @@ -313,19 +276,19 @@ jobs: run: sudo apt-get update && sudo apt-get install -y poppler-utils - name: cargo fmt - working-directory: monocr/apps/cli + working-directory: apps/cli run: cargo fmt --check - name: cargo clippy - working-directory: monocr/apps/cli + working-directory: apps/cli run: cargo clippy --all-targets -- -D warnings - name: cargo test - working-directory: monocr/apps/cli + working-directory: apps/cli run: cargo test - name: A green run must have executed tests - working-directory: monocr/apps/cli + working-directory: apps/cli # `cargo test` exits 0 for a crate with no tests, printing "running 0 # tests", so the exit code cannot tell a passing suite from a deleted # one. That is the hole the web, go and android floors close; cli was the @@ -363,7 +326,7 @@ jobs: # The fixture is the only thing keeping four language ports in step. If # it goes missing, every port's parity test silently has nothing to # compare against. - working-directory: monocr + working-directory: . run: | set -euo pipefail f=shared/segmentation-fixtures/tiling-cases.json @@ -388,7 +351,7 @@ jobs: # from it at borders and on even-width kernels. A fixture regenerated by # someone who did not read that would flip 23 pixels across two cases and # look like a routine refresh, so the constants are asserted here too. - working-directory: monocr + working-directory: . run: | set -euo pipefail f=shared/segmentation-fixtures/rule-cases.json @@ -424,7 +387,7 @@ jobs: # throughout because iOS compared itself against an oracle that was also a # square. The half-width table below IS the kernel shape, so a fixture that # went missing or got hand-edited would un-gate the thing that found it. - working-directory: monocr + working-directory: . run: | set -euo pipefail f=shared/segmentation-fixtures/dilate-cases.json @@ -464,7 +427,7 @@ jobs: # `merge_runs` is what stands between raw-profile boundary detection and a # 22x garbage regression, and it exists ten times in five languages. Parity # across those ten was checked once, by hand, before this fixture existed. - working-directory: monocr + working-directory: . run: | set -euo pipefail f=shared/segmentation-fixtures/merge-cases.json diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 445fbc8..b1acb4b 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -1,31 +1,39 @@ # Publishes the `monocr-cli` crate to crates.io on a `cli/v*` tag. # -# A separate file from ci.yml, not a job added to it, matching the pattern -# every other MonOCR binding uses (monocr-onnx's release-python.yml, -# release-js.yml and release-rust.yml; mon_tokenizer's release.yml): a -# release workflow re-runs the same checks as CI against the tagged commit, -# gates a registry credential behind them, and stays out of the file that -# runs on every push and pull request. Folding a publish step into ci.yml -# would mean every ordinary CI run carries `id-token: write` in the same -# file, whether or not a tag triggered it. +# Trusted publishing (OIDC), so no CARGO_REGISTRY_TOKEN is stored here. # -# Trusted publishing (OIDC) via rust-lang/crates-io-auth-action, so there is -# no CARGO_REGISTRY_TOKEN long-lived secret to store, leak or rotate. +# ONE-TIME SETUP on crates.io, after a crate exists (crates.io has no +# pending-publisher mechanism, unlike PyPI — see monocr-onnx/RELEASING.md): +# https://crates.io/crates/monocr-cli/settings -> trusted publisher +# owner MonDevHub · repo monocr · workflow release-cli.yml +# environment: crates-io # -# ONE-TIME SETUP, in order — crates.io has no pending-publisher mechanism -# (see monocr-onnx/RELEASING.md and the header of monocr-onnx's own -# release-rust.yml for the sourced explanation): -# 1. Publish an initial version manually once, with a personal API token: -# cd apps/cli && cargo login && cargo publish -# 2. Only after that, on the crate's settings page at -# https://crates.io/crates/monocr-cli/settings -# add a GitHub Actions trusted publisher: -# Repository owner: MonDevHub -# Repository name: monocr -# Workflow filename: release-cli.yml -# Environment: (blank) +# `environment` must match the job's `environment:` below. A job that declares +# one puts an `environment` claim in the OIDC token, and crates.io matches the +# whole claim set — a publisher registered as blank rejects a token carrying one. +# +# 0.1.0 was published by hand on 2026-09-03, so the first tag this can succeed +# on is a higher version: crates.io refuses duplicates and the gate below +# refuses a tag that disagrees with Cargo.toml. Bump, then tag. +# +# Tag is `cli/v*`, not a bare `v*`: this repo ships four surfaces and has no +# tags yet, so a bare prefix would become ambiguous the moment a second one does. +name: release-cli.yml +# Environment: crates-io # Before step 2 is done, this workflow's OIDC token exchange is refused. # +# The environment field must say `crates-io`, not be left blank. This block +# said blank while the job below declares `environment: crates-io`, and the +# two cannot both be right: a job that declares an environment puts an +# `environment` claim in the OIDC token, and crates.io matches the whole +# claim set. A publisher registered with a blank environment rejects a token +# that carries one. +# +# Step 1 has already happened: monocr-cli 0.1.0 was published by hand on +# 2026-09-03. So the FIRST tag this workflow can succeed on is a version +# above 0.1.0 — crates.io refuses a duplicate, and the gate below refuses a +# tag that disagrees with Cargo.toml. Bump the version, then tag. +# # The tag is namespaced `cli/v*` rather than a bare `v*` even though this is # the only thing this monorepo currently publishes: the repository has zero # git tags today across four shipped surfaces (web, iOS, Android, CLI), and a @@ -57,12 +65,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable - # This crate's own dependency on monocr is a path in the working tree - # (apps/cli/Cargo.toml), but carries `version = "0.3.0"` so `cargo - # publish` resolves and ships against the REAL published crate rather - # than requiring the path to exist for consumers. Verified 2026-09-03: - # `cargo publish --dry-run` from a clean checkout downloads and - # compiles the real crates.io `monocr` 0.3.0, not the local path. + # poppler is a runtime dependency of the PDF path, and the renderer's + # tests fail rather than skip without it. - name: poppler run: sudo apt-get update && sudo apt-get install -y poppler-utils diff --git a/apps/cli/Cargo.lock b/apps/cli/Cargo.lock index 67853d4..3d00826 100644 --- a/apps/cli/Cargo.lock +++ b/apps/cli/Cargo.lock @@ -1057,7 +1057,9 @@ dependencies = [ [[package]] name = "monocr" -version = "0.3.0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cacdb37146ee069357706f7454d4c9f103506a93b6a249acd71e6316388f5c91" dependencies = [ "anyhow", "clap", diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index b298bc1..03722ed 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -16,22 +16,14 @@ path = "src/main.rs" # reimplementation of the recogniser: segmentation, tiling, the model pin and # the charset contract all live upstream. # -# TODO(before CI): this must become a pinned git rev, not a path. A path -# dependency on a sibling checkout is a known failure mode in this ecosystem — -# mon-lm records that "mon-vlm took an editable path install of it and that is -# why mon-vlm's CI cannot run at all: uv sync fails on any machine without -# mon_OCR checked out alongside". The path is here only so the CLI builds -# against uncommitted upstream work; swap it for -# monocr-onnx = { git = "https://github.com/MonDevHub/monocr-onnx", rev = "...", package = "monocr" } -# once the tiling change lands, and keep the rev pinned. +# A registry dependency, not a path into a sibling checkout — a path made this +# crate unpublishable (crates.io rejects path and git deps) and made CI depend +# on a second checkout being present and at the right revision. # -# `package = "monocr"` on 2026-09-03: the crate's published name on crates.io -# is "monocr", not "monocr-onnx" — the repository name and the crate name -# diverged the day this binding was first published. Keeping this dependency's -# key as `monocr-onnx` (rather than renaming it to `monocr`) means every -# `monocr_onnx::` reference in src/main.rs stays exactly as it is; Cargo -# resolves the key to the aliased package regardless of what the key is named. -monocr-onnx = { path = "../../../monocr-onnx/rust", package = "monocr", version = "0.3.0" } +# `package = "monocr"` with the key left as `monocr-onnx`: the published crate +# is named `monocr`, and Cargo takes the extern crate name from the KEY, so +# this keeps every `monocr_onnx::` reference in src/main.rs compiling. +monocr-onnx = { version = "0.3.1", package = "monocr" } # Pinned exactly, not by range. ort is a pre-release and "2.0.0-rc.11" # range-matches rc.13, which changed Send bounds and does not compile