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
1 change: 0 additions & 1 deletion .github/workflows/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- main
pull_request:
workflow_dispatch:

concurrency:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/npm-promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
version:
description: Already-published lockstep version to promote
required: true
default: 0.2.1
type: string
release_run_id:
description: npm release run containing the verified release artifact
Expand Down
81 changes: 23 additions & 58 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,13 @@ on:
version:
description: Lockstep version for all release packages
required: true
default: 0.3.2
default: 0.3.3
type: string
publish_to_registry:
description: Publish the fully gated candidate to npm after preflight
required: true
default: false
type: boolean
promote_latest:
description: Promote the verified release from next to latest
required: true
default: false
type: boolean

permissions:
contents: read
Expand All @@ -40,6 +35,7 @@ jobs:
[[ "${RELEASE_VERSION}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
source_version="$(python -c 'import json; print(json.load(open("bindings/node/package.json", encoding="utf-8"))["version"])')"
test "$RELEASE_VERSION" = "$source_version"
python tools/npm_release.py ensure-unpublished --version "$RELEASE_VERSION"
if [[ "${{ inputs.publish_to_registry }}" == "true" ]]; then
test -f contracts/tiled-platform-baselines.json
test -f contracts/apple-provider-baselines.json
Expand Down Expand Up @@ -91,7 +87,7 @@ jobs:
with:
python-version: "3.11"
- name: Install the hash-locked oracle
if: matrix.id != 'windows-arm64'
if: matrix.id == 'linux-x64'
run: python -m pip install --require-hashes -r oracle/requirements.lock
- name: Bootstrap pinned dependencies and model
shell: bash
Expand Down Expand Up @@ -150,7 +146,7 @@ jobs:
-DLIGHT_OCR_BUILD_TESTS=ON
-DLIGHT_OCR_BUILD_TOOLS=ON
-DLIGHT_OCR_ORACLE_PYTHON="$(command -v python)"
-DLIGHT_OCR_PARITY_LIVE_ORACLE=ON
-DLIGHT_OCR_PARITY_LIVE_ORACLE=${{ matrix.id == 'linux-x64' && 'ON' || 'OFF' }}
-DLIGHT_OCR_NODE_INCLUDE_DIR="$NODE_INCLUDE_DIR"
-DLIGHT_OCR_NODE_EXECUTABLE="$(command -v node)"
- name: Configure Node addon (Windows)
Expand All @@ -165,7 +161,7 @@ jobs:
-DLIGHT_OCR_BUILD_TESTS=ON
-DLIGHT_OCR_BUILD_TOOLS=ON
"-DLIGHT_OCR_ORACLE_PYTHON=$((Get-Command python).Source)"
-DLIGHT_OCR_PARITY_LIVE_ORACLE=${{ matrix.id == 'windows-arm64' && 'OFF' || 'ON' }}
-DLIGHT_OCR_PARITY_LIVE_ORACLE=OFF
"-DLIGHT_OCR_NODE_INCLUDE_DIR=$env:NODE_INCLUDE_DIR"
"-DLIGHT_OCR_NODE_LIBRARY=$env:NODE_LIBRARY"
"-DLIGHT_OCR_NODE_EXECUTABLE=$((Get-Command node).Source)"
Expand Down Expand Up @@ -328,50 +324,37 @@ jobs:
runner: macos-15
node: "22"
native: darwin-arm64
- id: macos-arm64
runner: macos-15
node: "24"
native: darwin-arm64
tiled: false
- id: macos-x64
runner: macos-15-intel
node: "22"
native: darwin-x64
- id: macos-x64
runner: macos-15-intel
node: "24"
native: darwin-x64
tiled: false
- id: linux-x64
runner: ubuntu-24.04
node: "22"
native: linux-x64-gnu
tiled: true
- id: linux-x64
runner: ubuntu-24.04
node: "24"
native: linux-x64-gnu
tiled: false
- id: linux-arm64
runner: ubuntu-24.04-arm
node: "22"
native: linux-arm64-gnu
- id: linux-arm64
runner: ubuntu-24.04-arm
node: "24"
native: linux-arm64-gnu
tiled: false
- id: windows-x64
runner: windows-2022
node: "22"
native: win32-x64
- id: windows-x64
runner: windows-2022
node: "24"
native: win32-x64
tiled: false
- id: windows-arm64
runner: windows-11-arm
node: "22"
native: win32-arm64
- id: windows-arm64
runner: windows-11-arm
node: "24"
native: win32-arm64
tiled: false
runs-on: ${{ matrix.runner }}
env:
RELEASE_VERSION: ${{ inputs.version }}
Expand All @@ -394,26 +377,15 @@ jobs:
"../dist/release/arcships-light-ocr-model-ppocrv6-small-${RELEASE_VERSION}.tgz" \
"../dist/release/arcships-light-ocr-${{ matrix.native }}-${RELEASE_VERSION}.tgz" \
"../dist/release/arcships-light-ocr-${RELEASE_VERSION}.tgz"
cp ../tools/npm/smoke.cjs ../tools/npm/smoke.ts ../tools/npm/tiled-smoke.cjs .
cp ../tools/npm/smoke.cjs .
npm ls --all
- name: Run real OCR from CJS and ESM
- name: Run the CLI and real OCR from CJS and ESM
shell: bash
working-directory: package-smoke
env:
LIGHT_OCR_SMOKE_FIXTURE: ${{ github.workspace }}/corpus/fixtures/generated-hello-123
run: |
node smoke.cjs
node tiled-smoke.cjs \
"$GITHUB_WORKSPACE/corpus/tiled-v1/fixtures/tiled-four-way-intersection-2048"
- name: Compile the published TypeScript declarations
shell: bash
working-directory: package-smoke
run: |
npm install --ignore-scripts --no-audit --no-fund --no-save \
typescript@5.9.3 @types/node@22.20.1
npx tsc --strict --noEmit --skipLibCheck false \
--target ES2022 --module NodeNext --moduleResolution NodeNext \
--types node smoke.ts
LIGHT_OCR_SMOKE_TILED: ${{ matrix.tiled && '1' || '0' }}
run: node smoke.cjs
local-registry:
needs: assemble
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -463,13 +435,16 @@ jobs:
npm init --yes
npm install --ignore-scripts --no-audit --no-fund --package-lock=false \
--registry "$LOCAL_REGISTRY" "@arcships/light-ocr@${RELEASE_VERSION}"
cp ../tools/npm/smoke.cjs ../tools/npm/tiled-smoke.cjs .
cp ../tools/npm/smoke.cjs ../tools/npm/smoke.ts .
npm install --ignore-scripts --no-audit --no-fund --no-save \
typescript@5.9.3 @types/node@22.20.1
npx tsc --strict --noEmit --skipLibCheck false \
--target ES2022 --module NodeNext --moduleResolution NodeNext \
--types node smoke.ts
kill "$(cat ../.cache/verdaccio/server.pid)"
wait "$(cat ../.cache/verdaccio/server.pid)" || true
LIGHT_OCR_SMOKE_FIXTURE="$GITHUB_WORKSPACE/corpus/fixtures/generated-hello-123" \
node smoke.cjs
node tiled-smoke.cjs \
"$GITHUB_WORKSPACE/corpus/tiled-v1/fixtures/tiled-four-way-intersection-2048"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
Expand Down Expand Up @@ -512,17 +487,14 @@ jobs:
--tarball-dir dist/release
--phase dependencies
--tag next
- name: Verify facade tarball against registry dependencies
- name: Verify facade dependency resolution against the registry
shell: bash
run: |
mkdir registry-preflight
cd registry-preflight
npm init --yes
npm install --ignore-scripts --no-audit --no-fund --package-lock=false \
"../dist/release/arcships-light-ocr-${RELEASE_VERSION}.tgz"
cp ../tools/npm/smoke.cjs .
LIGHT_OCR_SMOKE_FIXTURE="$GITHUB_WORKSPACE/corpus/fixtures/generated-hello-123" \
node smoke.cjs
- name: Publish facade to next
run: >-
python tools/npm_release.py publish
Expand All @@ -541,13 +513,6 @@ jobs:
sudo unshare --net -- env \
LIGHT_OCR_SMOKE_FIXTURE="$GITHUB_WORKSPACE/corpus/fixtures/generated-hello-123" \
node smoke.cjs
- name: Promote the verified release to latest
if: inputs.promote_latest
run: >-
python tools/npm_release.py promote
--tarball-dir dist/release
--expected-version "${RELEASE_VERSION}"
--tag latest
- name: Show published metadata
shell: bash
run: |
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: pull request

on:
pull_request:

permissions:
contents: read

concurrency:
group: pull-request-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
linux-x64:
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.11"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Validate repository and release tooling contracts
run: python -m unittest discover -s tests/python -p 'test_*.py'
- name: Bootstrap pinned inputs and prove the dependency cache is complete
shell: bash
run: |
python tools/bootstrap_dependencies.py --cache-dir .cache/dependencies
python tools/bootstrap_dependencies.py --cache-dir .cache/dependencies --offline
python tools/bootstrap_models.py --cache-dir .cache/models
python tools/package_model_bundle.py
- name: Install verified Node development files
shell: bash
run: |
node_version="$(node -p process.versions.node)"
node_dev="$PWD/.cache/node-gyp/$node_version"
npx --yes node-gyp@11.4.2 install "$node_version" --devdir "$PWD/.cache/node-gyp"
test -f "$node_dev/include/node/node_api.h"
echo "NODE_INCLUDE_DIR=$node_dev/include/node" >> "$GITHUB_ENV"
- name: Configure the representative release build
shell: bash
run: >-
cmake -S . -B build-pr -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies"
-DLIGHT_OCR_BUILD_NODE=ON
-DLIGHT_OCR_BUILD_TESTS=ON
-DLIGHT_OCR_BUILD_TOOLS=ON
-DLIGHT_OCR_PARITY_LIVE_ORACLE=OFF
-DLIGHT_OCR_NODE_INCLUDE_DIR="$NODE_INCLUDE_DIR"
-DLIGHT_OCR_NODE_EXECUTABLE="$(command -v node)"
- name: Build and test Core and Node contracts
run: |
cmake --build build-pr --parallel
ctest --test-dir build-pr --output-on-failure \
-LE "acceptance|canonical-oracle|webgpu-contract"
- name: Run the complete Node adapter and CLI suite
working-directory: bindings/node
env:
LIGHT_OCR_NODE_BINARY: ${{ github.workspace }}/build-pr/node-runtime/native/light_ocr_node.node
LIGHT_OCR_RUNTIME_DESCRIPTOR: ${{ github.workspace }}/build-pr/node-runtime/native/runtime-descriptor.json
LIGHT_OCR_MODEL_BUNDLE: ${{ github.workspace }}/models/generated/ppocrv6-small-onnx-20260714.2
run: npm test
- name: Prove runtime operation without network access
shell: bash
run: >-
sudo unshare --net --
python tools/run_offline_check.py
--validate build-pr/bin/light_ocr_validate
--bundle models/generated/ppocrv6-small-onnx-20260714.2
--fixture corpus/fixtures/generated-hello-123/fixture.json
--require-network-disabled
1 change: 0 additions & 1 deletion .github/workflows/webgpu-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: native webgpu
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

This file records user-visible changes to `light-ocr`. Published artifact details and immutable hashes remain in [`docs/releases/`](docs/releases/).

## [0.3.3] - 2026-07-22

### Fixed

- Fixed npm facade assembly to include the declared `light-ocr` bin, its executable script, and the required Node shebang. The package smoke now installs and executes `node_modules/.bin/light-ocr info --version`, preventing a source-only CLI from passing release validation again.
- Changed the reserved `detect --crop` flag from a silent no-op to a fail-closed `unsupported_capability` response until crop bytes are implemented.

### Changed

- Split publication and promotion into single-purpose workflows. `npm release` now rejects an already-published facade version before starting the six-platform build; `npm promote` alone may update `latest` from the original immutable release artifact.
- Replaced the ten-job pull-request fan-out with one representative Linux Core/Node/offline gate; the full six-platform, sanitizer, oracle, and WebGPU suites now run once after changes reach `main`.
- Removed release work that produced no additional evidence: live oracle setup now runs only on Linux x64 where its acceptance tests execute, TypeScript declarations compile once, every platform installs on Node 22 while Node 24 compatibility is checked once on Linux x64, tiled OCR runs once, and registry checks no longer repeat the same tiled contract.

### Notes

- npm `0.3.2` is immutable and its facade tarball omitted the CLI bin even though the source and changelog contained it. `0.3.3` is the corrective patch release; the `0.3.2` artifact remains recorded for provenance.

## [0.3.2] - 2026-07-22

### Added
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ if(APPLE AND (NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET OR
"Minimum macOS deployment target" FORCE)
endif()

project(light_ocr VERSION 0.3.2 LANGUAGES CXX)
project(light_ocr VERSION 0.3.3 LANGUAGES CXX)

if(APPLE)
enable_language(OBJCXX)
Expand Down
2 changes: 1 addition & 1 deletion bindings/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ light-ocr info --model-info
| `--schema-version` | `1` | Request exact output schema |
| `--quiet` | — | Suppress non-error stderr |

`detect` does not accept `--format` (output is always JSON with `detections[]`). `detect --crop` attaches a PNG crop per detection.
`detect` does not accept `--format` (output is always JSON with `detections[]`). The reserved `detect --crop` flag currently fails with `unsupported_capability`; use returned boxes with `recognize --region` until crop bytes are implemented.

### Scenarios

Expand Down
21 changes: 9 additions & 12 deletions bindings/node/bin/light-ocr.cjs
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env node
'use strict';

// light-ocr CLI — N1 entry point (cli-design.md §3, D106)
Expand All @@ -18,7 +19,7 @@ const { createEngine, OcrError } = require('../js/index.cjs');
const { parseExifOrientation } = require('../js/exif.cjs');

const PKG_VERSION = require('../package.json').version;
const CORE_VERSION = '0.3.2';
const CORE_VERSION = '0.3.3';

const SUBCOMMANDS = new Set(['recognize', 'detect', 'info']);
const EXIT = {
Expand Down Expand Up @@ -449,16 +450,6 @@ async function runDetect(rest, flags, stdout, stderr) {
},
});

// --crop: each detection gets a PNG crop (base64-encoded)
// Crop is done in CLI JS layer from the decoded image. Since CLI doesn't
// decode (C++ decode is inside recognizeEncoded/detect), crop requires
// either a separate decode or Core returning crop bytes. For now, crop
// is not available (D-N1-3 pending); --crop returns info in appliedTransforms.
if (flags.crop === true) {
// TODO: implement crop when Core detect supports returning crop bytes
// or when CLI can decode independently. For now, note in source.
}

// detect output is always JSON (no --format flag)
stdout.write(JSON.stringify(envelope, null, 2) + '\n');
} finally {
Expand Down Expand Up @@ -514,7 +505,7 @@ function printSubcommandHelp(stdout, subcommand) {
stdout.write('Usage:\n light-ocr detect <path> [flags]\n light-ocr detect --stdin --type <mime> [flags]\n\n');
stdout.write('Flags:\n');
stdout.write(' --region x,y,w,h Restrict detection to a pageSpace rectangle\n');
stdout.write(' --crop Attach a PNG crop per detection\n');
stdout.write(' --crop Reserved; currently fails as unsupported\n');
stdout.write(' --provider auto|cpu|apple|webgpu Execution provider (default: auto)\n');
stdout.write(' --no-exif Disable EXIF orientation correction\n');
stdout.write(' --schema-version 1 Request exact output schema\n');
Expand Down Expand Up @@ -567,6 +558,12 @@ async function main(argv) {
if (parsed.flags.crop !== undefined && parsed.flags.crop !== true) {
throw { code: EXIT.invalid_argument, message: '--crop is a boolean flag' };
}
if (parsed.flags.crop === true) {
throw {
code: EXIT.unsupported_capability,
message: '--crop is not available in this release; use detection boxes with --region',
};
}
await runDetect(rest, parsed.flags, stdout, stderr);
} else {
die(stderr, EXIT.usage, `unknown subcommand: ${subcommand}`);
Expand Down
2 changes: 1 addition & 1 deletion bindings/node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@arcships/light-ocr",
"version": "0.3.2",
"version": "0.3.3",
"private": true,
"description": "Node-API adapter for the light-ocr C++ core",
"license": "Apache-2.0",
Expand Down
Loading
Loading