From f9e1ff5d686234da476add0181eb2825e67cc791 Mon Sep 17 00:00:00 2001 From: pushkraj-space Date: Mon, 28 Sep 2026 23:02:45 +0530 Subject: [PATCH] feat(licensing): add default-deny engine and model licensing audit Murmur is about to integrate FluidAudio (#42) and sherpa-onnx (#26), and their SDK licenses do not cover the model weights, tokenizers, phonemizers, voices, and prebuilt binaries they fetch or link. #33 needs a machine-readable, fail-closed audit record before any engine lands. Add licensing/manifest.json, a single default-deny inventory of 71 entries: - the FluidAudio v0.17.4 and sherpa-onnx v1.13.8 engines, each with one named build configuration - their native closures: NemoTextProcessing plus the 37 Rust crates, std, and compiler-builtins it links statically, verified against the release binary; fastcluster; VBx; Japanese G2P code; kaldi-native-fbank, kaldi-decoder, kaldifst, OpenFst, Eigen, simple-sentencepiece, nlohmann/json; and the ONNX Runtime rebuild - the model, tokenizer, phonemizer, and voice components #42 needs - the two synthetic conformance-fixture sets. Every non-fixture entry is pinned by a 40-hex revision and/or byte-hashed downloads, cites license evidence in its own repository at its own commit, records upstream hops, terms, and download-presentation obligations, and is classified bundle, user-download, or blocked. Nothing is legally approved. Add tool/check_licensing.py, a stdlib-only checker, with 100 unittest cases, wired into `make check` and the CI protocol job. It enforces: - exact field shapes and an acyclic `requires` graph - ambiguity forcing `blocked`, and custom licenses needing exact names - Hugging Face downloads bound to the entry's repository and revision - evidence that is a file in the reviewed repository at the reviewed commit - approvals bound to a SHA-256 fingerprint of the reviewed fields - notices for approved bundles, and byte hashes for approved downloads and content bundles - synthetic-fixture provenance, and a case-insensitive scan that fails on any unregistered tracked model, native binary, or audio file. Document the contract, policy, download rule, runtime boundary, findings, update procedure, and questions for counsel in licensing/README.md. Point THIRD_PARTY_NOTICES.md, docs/voice-runtime.md, and CONTRIBUTING.md at it. Key findings, all recorded as blocked: - FluidAudio bundles an espeak-ng-harvested LuxTTS lexicon with no license. - The Kokoro English G2P and lexicon sources are undocumented. - The Parakeet EOU terms and the legacy diarization models are unresolved. - sherpa-onnx compiles an unlicensed table and a StackOverflow snippet. - The ONNX Runtime rebuild declares no license. Refs #33 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 7 + Makefile | 8 +- THIRD_PARTY_NOTICES.md | 8 + docs/voice-runtime.md | 12 +- licensing/README.md | 351 ++++ licensing/manifest.json | 3771 ++++++++++++++++++++++++++++++++++ tool/check_licensing.py | 616 ++++++ tool/test_check_licensing.py | 758 +++++++ 9 files changed, 5526 insertions(+), 7 deletions(-) create mode 100644 licensing/README.md create mode 100644 licensing/manifest.json create mode 100644 tool/check_licensing.py create mode 100644 tool/test_check_licensing.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c03394..c4ca704 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" - - run: make check-protocol check-conformance + - run: make check-protocol check-conformance check-licensing dart-and-flutter: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e02ab4..c29e0ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,13 @@ Keep product commands and billing policy outside the runtime. Provider-specific dependencies belong behind adapters, with their licenses and model terms called out in the pull request. +A change that adds or bumps an engine, native library, model, voice, tokenizer, +phonemizer, or test fixture follows the update procedure in +[licensing/README.md](licensing/README.md). Include the `licensing/manifest.json` +change and any legal review reference. Never rely on an SDK repository's license +alone for the artifacts it downloads. A pull request that adds a downloader or a +packaging path also adds its licensing cross-check in the same pull request. + ## Connector pull requests A connector contribution should document: diff --git a/Makefile b/Makefile index c08da74..03d54ab 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ PROTO_FILES := $(shell find spec/proto -name '*.proto' -type f | sort) -.PHONY: check check-protocol check-conformance check-dart check-flutter check-flutter-plugin check-typescript check-python check-rust +.PHONY: check check-protocol check-conformance check-licensing check-dart check-flutter check-flutter-plugin check-typescript check-python check-rust -check: check-protocol check-conformance check-dart check-flutter check-flutter-plugin check-typescript check-python check-rust +check: check-protocol check-conformance check-licensing check-dart check-flutter check-flutter-plugin check-typescript check-python check-rust check-protocol: @descriptor="$$(mktemp)"; \ @@ -12,6 +12,10 @@ check-protocol: check-conformance: python3 tool/check_conformance.py +check-licensing: + python3 tool/check_licensing.py + python3 -m unittest discover -s tool -p 'test_check_licensing.py' -v + check-dart: cd sdks/dart/murmur_protocol && dart pub get && dart format --output=none --set-exit-if-changed . && dart analyze && dart test diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1de01cc..86031c0 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -89,3 +89,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + +## Engine dependencies and model artifacts + +Murmur does not currently distribute any native engine dependency, model +weight, voice, tokenizer, or phonemizer. The inventory, terms, and review state +live in [licensing/manifest.json](licensing/manifest.json) and +[licensing/README.md](licensing/README.md). An artifact's full notice is added +here before it may be packaged; `make check-licensing` enforces this. diff --git a/docs/voice-runtime.md b/docs/voice-runtime.md index 0b61cb1..4548435 100644 --- a/docs/voice-runtime.md +++ b/docs/voice-runtime.md @@ -181,8 +181,12 @@ atomic pack manager instead of making each screen infer readiness. preparation - unsupported components are explicit rather than reported as ready -Individual engine adapters remain responsible for their upstream licenses, -model terms, supported platforms, and redistribution rules. +Packs resolve only entries approved in [licensing/](../licensing/README.md), +verify each file's hash and size before activation, and present terms as each +entry's `terms.download.presentation` requires before the first network +request. An engine adapter's license never stands in for the terms of the +models, voices, tokenizers, or phonemizers it loads, and each adapter still +documents its supported platforms. ## Speech feedback and interruption @@ -226,8 +230,8 @@ regressing. - subscription, trial, entitlement, and usage-metering policy - private services, endpoints, analytics, and account identifiers - host UI components, IPC names, and application state -- native engines or model assets whose licenses and redistribution terms have - not been reviewed for Murmur +- native engines or model assets that are not approved in + [licensing/manifest.json](../licensing/manifest.json) - constants tuned to one product without a public configuration and test basis Murmur can define adapters for these capabilities without making any one diff --git a/licensing/README.md b/licensing/README.md new file mode 100644 index 0000000..d593016 --- /dev/null +++ b/licensing/README.md @@ -0,0 +1,351 @@ +# Engine and model licensing + +`manifest.json` is the single, default-deny inventory of native engine +dependencies, model weights, voices, tokenizers, phonemizers, and test fixtures +that Murmur may package or download. `tool/check_licensing.py` enforces it, and +runs through `make check-licensing` locally and in CI. + +## Scope and disclaimer + +This is an engineering compliance record, not legal advice. It records what the +pinned upstream sources state and what Murmur's policy requires. The legal +review recorded per entry is the only approval. + +Murmur currently bundles and downloads no engine, model, voice, tokenizer, or +phonemizer, and has no model downloader or release packaging. A `user-download` +classification does not imply that a downloader exists. **Anything not listed +in the manifest is unapproved.** + +Out of scope: pub and npm application dependencies, and the existing Omi, +Omarchy, and Expo notices in `THIRD_PARTY_NOTICES.md`. + +## Status against #33 + +| Criterion | Status | +| --- | --- | +| Inventory by exact version and source | Met by the manifest. It lists the FluidAudio (#42) and sherpa-onnx (#26) candidates with their configured native closure, the model, tokenizer, G2P, and voice components #42 needs, and the test fixtures. | +| Record license and terms | Met by `license`, `upstream`, `terms`, and `review.notes`. | +| Separate bundled from downloaded artifacts | Met by `distribution`. | +| Machine-readable manifest with hashes or revisions | Met: every non-fixture entry has a 40-hex `revision`, byte-hashed `downloads`, or both. | +| Notices | Met for the current distribution, which contains no engine or model material. The checker requires a notice heading before any `bundle` entry can be approved. In-package copies arrive with the first packaged artifact (#21). | +| Terms before automated downloads | Met fail-closed for the current distribution, because no automated download exists. The manifest records each artifact's required presentation, and the checker refuses an approved download without terms and hashes. | +| Block unsupported or ambiguous artifacts | Met fail-closed for the current distribution. Nothing is approved, ambiguous entries must be `blocked`, and any tracked model, binary, or audio file must be registered. | +| Update procedure | Below. | +| Synthetic fixture provenance | Met by the two `fixture` entries. | +| Qualified legal review | **Outstanding external gate.** Every non-fixture entry is `pending`. #33 closes only after qualified reviewers record their outcome per entry. | + +**Criteria 6 and 7 re-open when a new path is added.** A pull request that adds +a downloader (#16) or a packaging path (#21, #42, #26) re-opens both criteria +for that path, and must add the matching cross-check in the same pull request. +The checks expected later are: + +- `Package.resolved`, CMake, and model-URL cross-checks against this manifest + (#42, #26, #16). The sherpa-onnx check must also assert the configuration's + options, including `SHERPA_ONNX_USE_PRE_INSTALLED_ONNXRUNTIME_IF_AVAILABLE=OFF` + (so no unrecorded ONNX Runtime from the build machine can be linked) and the + dynamic runtime linkage (`SHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=OFF`, + `SHERPA_ONNX_USE_STATIC_CRT=OFF`, with no `CMAKE_MSVC_RUNTIME_LIBRARY` + override). It must also inspect the linked artifacts for statically embedded + runtimes. +- a download UI that enforces `terms.download.presentation` (#16) +- package-contents and in-package notice checks (#21). + +## Manifest contract + +`manifestVersion` is `1`. `artifacts` is a non-empty list. Unknown fields and +duplicate JSON keys are rejected. + +### Artifact entries + +| Field | Contract | +| --- | --- | +| `id` | Unique lowercase kebab-case, with dots allowed between alphanumerics (`parakeet-tdt-0.6b-v3-coreml`). | +| `kind` | `engine` or `native-library` (code kinds), or `model`, `voice`, `tokenizer`, or `phonemizer` (content kinds). | +| `name`, `version` | Non-empty. `name` is also the notice heading an approved bundle needs. | +| `source` | `https://` repository or model page. A commit in a `source` URL (for example `/tree//path`) must equal `revision`. | +| `vcs` | Optional. The github.com, gitlab.com, or huggingface.co repository that `revision` belongs to, when `source` is a package page such as crates.io. It is the explicit, reviewed record of a source/evidence host difference. | +| `revision` | 40-hex git or Hugging Face commit, or `null`. | +| `downloads` | Every fetched file or archive: `{ "url", "sha256", "size", "platform"? }`. The URL is `https://`, `sha256` is 64 hex characters of the actual bytes (for Hugging Face, the LFS object hash, not the pointer hash), and `size` is a positive integer. There is one element per platform where the bytes differ, and each `url` and `platform` pair is unique. A Hugging Face URL is bound to its own entry: it must use the canonical host and start with `https://huggingface.co//resolve//`, so the fetched bytes come from the reviewed repository and revision. Other hosts, ports, and `.`/`..` segments are rejected. | +| `paths` | Exact repository-relative tracked files, for committed binaries. Each path belongs to one entry and must be tracked. | +| `configuration` | Required and non-empty for an `engine`, and `null` for every other kind. | +| `requires` | Component ids that must exist. The graph must be acyclic, and nothing may require an `engine` or a `fixture`. | +| `usedBy` | Non-owning labels (issues, engine ids). They play no part in approval. | +| `license` | `{ "code", "content", "name", "evidence" }`. See below. | +| `upstream` | Derived-from hops: `{ "name", "source", "vcs"?, "revision"?, "license", "licenseName"?, "evidence" }`. `licenseName` is the exact name of a custom (`other` or `LicenseRef-*`) license. | +| `terms` | `redistribution`, `commercialUse`, and `modification` are each `allowed`, `prohibited`, or `unknown`. `attribution` is a string. `download` is `{ "access", "presentation" }`. `termsUrl` is the current user-facing `https://` link, or `null`. | +| `distribution` | `bundle`, `user-download`, or `blocked`. | +| `review` | `date` (`YYYY-MM-DD`), `notes` (engineering findings, download facts, block reasons), and `legal`: `{ "status", "reference", "date", "fingerprint"? }`. | + +A pin is required: every non-fixture entry has a `revision`, a non-empty +`downloads`, or both. + +**License fields.** Code kinds must identify `license.code`, and content kinds +must identify `license.content`. Identified means neither `null` nor +`NOASSERTION`. The other field may also be recorded. This stops a vocabulary +from inheriting a surrounding code license. An upstream artifact that ships +code and data under different terms is split into a code entry and a content +entry. + +**Evidence.** `license.evidence` and every `upstream[].evidence` must be a file +on github.com, gitlab.com, or huggingface.co, in the reviewed repository, at the +reviewed commit: + +- for `license.evidence`, the entry's `vcs` (or else its `source`) repository at + its `revision` +- for a hop, the hop's `vcs` (or else its `source`) repository at the hop's + `revision`, which is required whenever the hop cites evidence. + +The URL must name one file: a `blob` or `raw` path on github.com or gitlab.com, +or a `blob`, `resolve`, or `raw` path on huggingface.co, with a non-empty path +after the commit. The following are all rejected: + +- directory views (`tree`) and pathless `blob`/`resolve` URLs +- evidence from another repository or another commit +- `blob/main`, tags, and bare repository pages +- other hosts or ports +- `.` or `..` segments. + +`terms.termsUrl` is exempt because it is the link shown to users. + +**Dependency direction.** An engine requires the native code and +engine-vendored data that its configuration compiles or links. A model requires +its own tokenizer, G2P or phonemizer data, and voices. A model never requires an +engine: compatibility is recorded in `usedBy`. + +**Download terms.** `access` is `public`, `gated` (an account or accepted terms +at the host), or `unknown`. `presentation` is what must be shown before the +first network request: `none`, `link`, `acknowledgement`, or `unknown`. `none` +is valid only for `bundle`, and `user-download` requires `link` or +`acknowledgement`. + +**Legal review.** `status` is `pending`, `approved`, or `rejected`. `approved` +and `rejected` require a non-empty `reference` and `date`. The repository stores +only the non-privileged reference and date, never counsel's advice. + +**Reviewed fingerprint.** An `approved` entry must record +`review.legal.fingerprint`: the SHA-256 of the canonical JSON (sorted keys, no +whitespace) of every entry field except `usedBy` and `review`. That covers +identity, pins, downloads, paths, configuration, `requires`, license, upstream, +terms, and distribution. Print it with +`python3 tool/check_licensing.py --fingerprint `. Any later change to those +fields fails the check until a fresh review records a new fingerprint, so a +bump cannot keep an old approval. Changes to a required entry invalidate that +entry's own approval, which in turn blocks every entry that requires it. + +### Fixture entries + +Fixture entries have only `id`, `kind: fixture`, `name`, `paths`, +`synthetic: true`, a non-empty `method`, and `review.notes`. The tracked files +under `conformance/fixtures/` must equal the union of fixture `paths`, with no +path listed twice. + +Fixtures have no remote source, revision, license, or terms. They are +Murmur-authored, live in this repository under its Apache-2.0 license, and +their exact `paths` and `method` *are* their source and provenance. +`conformance/manifest.json` remains the only fixture index. The checker cannot +prove that content is synthetic, so that stays a review step. + +### Tracked-file scan + +The checker fails if any tracked file matching one of the patterns below is not +listed exactly in some entry's `paths`. Matching ignores case, so `Model.ONNX` +and `Voice.WAV` are scanned too: + +- models: `.onnx`, `.ort`, `.mlmodel`, `.safetensors`, `.gguf`, `.pt`, `.pth`, + `.tflite`, and `.bin` files, plus anything inside an `.mlpackage/` or + `.mlmodelc/` directory +- native binaries: `.so`, `.dylib`, `.a`, and `.dll` files, plus anything inside + an `.xcframework/` directory +- audio: `.wav`, `.flac`, `.mp3`, `.ogg`, `.opus`, and `.m4a` files. + +Resolve a false positive with an entry or a reviewed change to this list, never +with a blanket exclude. + +## Policy + +- **Three classes.** `bundle` may ship inside a Murmur package. `user-download` + must be fetched by the user's device. `blocked` may do neither. The + classification is an engineering decision. Release is enabled only by an + `approved` legal review. +- **Ambiguity forces `blocked`.** An entry must be `blocked` if any of the + following hold: + - any permission, `terms.download.access`, or `terms.download.presentation` + is `unknown` + - its kind's license field is missing or `NOASSERTION` + - `license.evidence` is missing + - a custom license (`other` or `LicenseRef-*`) lacks an exact `license.name` + or evidence + - an upstream hop lacks an identified license or evidence, or has a custom + license without an exact `licenseName` + - its legal review is `rejected`. + + Custom terms are not missing terms: once the name and evidence are captured, + the normal rules apply. A `blocked` entry is never `approved`, and a completed + negative review stays in the history as `rejected` and `blocked`. +- **Approval** requires all of the following: + - a legal reference and date, and the fingerprint of the fields reviewed + - every answer known + - `commercialUse: allowed` + - a recorded `attribution` + - every `requires` entry approved. + + In addition, a `bundle` needs `redistribution: allowed` and a `##` heading in + `THIRD_PARTY_NOTICES.md` containing its `name`. A bundled model, voice, + tokenizer, or phonemizer also needs non-empty, byte-hashed `downloads`, so + approval binds to the exact bytes shipped. A `user-download` needs a + `termsUrl` and non-empty, byte-hashed `downloads`. A download is not a + loophole. +- **Commercial use.** Requiring `commercialUse: allowed` is a conservative + project policy, not a legal inference from a license: Murmur does not present + a use-restricted artifact as generally supported. The legal reviewer remains + the authority on the recorded terms. +- **The most restrictive hop decides.** An SDK's license is not the license of + the weights, tokenizers, phonemizers, voices, or binaries it fetches, and no + part of a pack inherits an archive's or engine's license by assumption. Packs + are split per component wherever provenance or terms differ. +- **One configuration per engine entry.** `requires` is complete for exactly + that configuration. Another build variant, such as sherpa-onnx with TTS or + FluidAudio without NemoTextProcessing, needs its own reviewed engine entry. + The current configurations are provisional until #42 and #26 confirm them. +- **Vendored code.** An engine's `upstream` lists every project whose code its + configured sources copy, adapt, or port, each with its own license and + evidence. The most restrictive hop decides, so an unlicensed or unpinnable + snippet blocks the engine. +- **Runtime boundary.** Each engine configuration names its C, C++, and + language runtime linkage. The current configurations link these runtimes + dynamically from the target operating system and do not package them: + - glibc, libstdc++, and libgcc_s on Linux + - the MSVC `/MD` runtime on Windows, where the Visual C++ Redistributable is + a system prerequisite + - libSystem, libc++, and the Swift runtime on macOS. + + Static runtime linkage, or shipping a runtime app-locally, needs its own + reviewed entry. +- **Modification.** Murmur modifies no third-party artifact. A Murmur-made + derivative is its own entry with its own review. + +## Download rule + +This rule binds #16 and every engine adapter: + +- fetch only `approved` entries, and only from their `downloads[].url` +- verify `sha256` and `size` before activating anything +- before the first network request, show `terms.termsUrl` and + `terms.attribution`, and require an explicit accept when `presentation` is + `acknowledgement` +- a cancelled prompt makes no request +- never download silently on first use. + +FluidAudio v0.17.4's own model downloader fetches Hugging Face `main` for every +repository except `speaker-diarization-coreml`. #42 must therefore fetch from +this manifest's pinned files instead of relying on FluidAudio's default. + +## Current findings + +These were verified on 2026-09-28 against the pinned commits. The manifest is +the per-field record, and every non-fixture entry is `legal.status: pending`. + +**Bundle candidates:** + +- FluidAudio engine: `fluidaudio`, `nemo-text-processing`, `fastcluster`, + `vbx`, `japanese-g2p` +- NemoTextProcessing's statically linked closure: 30 `rust-crate-*` entries, + plus `rust-std`, `rust-compiler-builtins`, and seven more `rust-crate-*` + entries that the standard library links. The closure comes from + `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` over + the seven Apple targets that build-xcframework.sh builds. It was checked + against the v0.3.1 release binary, whose panic locations name only crate + versions in that closure and rustc 1.98.1 (`48a229ce`). Each crate is pinned + by its crates.io archive hash (equal to the `Cargo.lock` checksum) and its + VCS commit. +- sherpa-onnx's dependencies: `kaldi-native-fbank`, `kaldi-decoder`, + `kaldifst`, `openfst`, `eigen`, `simple-sentencepiece`, `nlohmann-json` +- `silero-vad-coreml` (MIT, small). It still needs per-file `downloads` before + approval. + +`fluidaudio` also records, as upstream hops, the projects whose code its +library ports: FunASR, NeMo, misaki, ZipVoice, Chatterbox, StyleTTS2, and +mobius, all MIT or Apache-2.0. + +**User-download candidates:** + +- `parakeet-tdt-0.6b-v3-coreml` and `parakeet-tdt-0.6b-v3-vocab` (CC-BY-4.0, + attribution to NVIDIA) +- `kokoro-82m-coreml-ane`, `kokoro-82m-ane-vocab`, and `kokoro-voice-af-heart` + (Apache-2.0). + +Each still needs per-file `downloads` before approval. + +**Blocked:** + +- `luxtts-en-us-g2p-lexicon`: FluidAudio bundles this espeak-ng-harvested + lexicon into every build, with no stated license. **FluidAudio cannot be + approved until it is resolved.** +- `kokoro-english-lexicon`: Misaki does not document the dictionaries its + English gold and silver lexicons were compiled from, so its repository + license cannot be inherited by the data. +- `kokoro-english-g2p`: the BART G2P weights have an undocumented source and + license. The English Kokoro model cannot be approved until both English G2P + entries are resolved. +- `parakeet-realtime-eou-120m-coreml` and `parakeet-realtime-eou-120m-vocab`: + the exact NVIDIA Open Model License text is not captured. #42 streaming must + wait for its review or use TDT v3 chunking. +- `pyannote-segmentation-legacy-coreml` and `wespeaker-v2-legacy-coreml`: the + two files FluidAudio's speaker-embedding path loads. The repository's NOTICE + explicitly excludes them from its CC-BY-4.0 scope, and neither records a + source checkpoint, author, or license. +- `kokoro-spanish-french-g2p`: espeak-ng-generated pronunciations with no stated + data license, outside the English configuration. +- `sherpa-onnx`: its compiled sources copy code from Kaldi, k2, icefall, CATT + (all Apache-2.0) and cpp-base64 (zlib-style notice). They also copy the + Buckwalter transliteration table, whose repository has no license, and a + StackOverflow trim snippet with no pinnable source or recorded license. Those + two need a qualified review, or an upstream replacement, before the engine + can be classified as a bundle again. +- `onnxruntime`: a third-party rebuild with no declared license or reproducible + provenance. **sherpa-onnx cannot be approved until it is resolved.** The + sherpa-onnx configuration sets + `SHERPA_ONNX_USE_PRE_INSTALLED_ONNXRUNTIME_IF_AVAILABLE=OFF`. Otherwise + sherpa-onnx links any ONNX Runtime it finds on the build machine without a + hash check, bypassing the pinned archives. +- `espeak-ng` and `piper-phonemize`: GPL-3.0 TTS dependencies, unreachable from + the recognition-only configuration. + +## Update procedure + +A version-only or URL-only bump cannot pass the checker. For every new or +changed artifact: + +1. Resolve the release or tag to a 40-hex commit. +2. Name the engine configuration, enumerate its complete `requires`, and check + for cycles. +3. Trace every upstream hop (base model, conversion, training data, vendored + code) to its license. +4. Cite commit-pinned evidence for the entry and every hop. +5. Record `downloads` for every fetched file: URL, byte SHA-256, size, and + platform. +6. Set `terms.download.access` and `presentation`. +7. Update `THIRD_PARTY_NOTICES.md` and the findings above. +8. Record the legal outcome, reference, and date, or leave it `pending`. An + approval also records the fingerprint that + `python3 tool/check_licensing.py --fingerprint ` prints for the reviewed + entry. +9. Run `make check-licensing`. + +A pull request that adds a downloader or packaging path adds its licensing +cross-check in the same pull request. + +## Questions for counsel + +- CC-BY-4.0 attribution in apps and on-device downloads (Parakeet, pyannote) +- the NVIDIA Open Model License and the scoped CC-BY-4.0 notice on the + diarization mirror, including the ungated mirror of a gated upstream +- the status of data generated or harvested from GPL-3.0 espeak-ng +- GPL-3.0 phonemizer linking, should a TTS variant ever be proposed +- ONNX Runtime rebuild provenance and its embedded third-party notices +- the sources behind Misaki's English lexicons and the Kokoro BART G2P +- the provenance of the legacy pyannote segmentation and WeSpeaker Core ML files +- the unlicensed Buckwalter table and the StackOverflow snippet compiled into + sherpa-onnx, and whether ported algorithms create derivative-work obligations + for FluidAudio's upstream projects +- commercial use across all of the above. diff --git a/licensing/manifest.json b/licensing/manifest.json new file mode 100644 index 0000000..a0f35a7 --- /dev/null +++ b/licensing/manifest.json @@ -0,0 +1,3771 @@ +{ + "manifestVersion": 1, + "artifacts": [ + { + "id": "fluidaudio", + "kind": "engine", + "name": "FluidAudio", + "version": "v0.17.4", + "source": "https://github.com/FluidInference/FluidAudio", + "revision": "21493f8dac5a97e65742e6ff26f42f164c2fda0f", + "downloads": [], + "paths": [], + "configuration": "FluidAudio v0.17.4 `FluidAudio` library product through SwiftPM with default traits (NemoTextProcessing linked), macOS 14+ on arm64; uses Parakeet TDT v3 ASR, Silero VAD, Kokoro ANE English TTS, and speaker embeddings (#42). The Swift runtime, libc++, and system frameworks come from macOS and are not packaged. Provisional until #42 confirms it.", + "requires": [ + "nemo-text-processing", + "fastcluster", + "vbx", + "japanese-g2p", + "luxtts-en-us-g2p-lexicon" + ], + "usedBy": [ + "#42" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/FluidInference/FluidAudio/blob/21493f8dac5a97e65742e6ff26f42f164c2fda0f/LICENSE" + }, + "upstream": [ + { + "name": "modelscope/FunASR (Swift ports: FSMN-VAD decision logic, Paraformer CIF)", + "source": "https://github.com/modelscope/FunASR", + "revision": "55203a99956fb2b18f9c1ed9cc7cd78ce29a9a52", + "license": "MIT", + "evidence": "https://github.com/modelscope/FunASR/blob/55203a99956fb2b18f9c1ed9cc7cd78ce29a9a52/LICENSE" + }, + { + "name": "NVIDIA/NeMo (Swift ports: Sortformer and Nemotron-3 state updaters, CTC word spotter)", + "source": "https://github.com/NVIDIA/NeMo", + "revision": "cf724ac337d1ebc7d0dda1e23fb80916f52927a5", + "license": "Apache-2.0", + "evidence": "https://github.com/NVIDIA/NeMo/blob/cf724ac337d1ebc7d0dda1e23fb80916f52927a5/LICENSE" + }, + { + "name": "hexgrad/misaki (Swift ports: zh_frontend ZH_MAP, English lexicon rules)", + "source": "https://github.com/hexgrad/misaki", + "revision": "fba1236595f2d2bf21d414ba6e57d25256afada3", + "license": "Apache-2.0", + "evidence": "https://github.com/hexgrad/misaki/blob/fba1236595f2d2bf21d414ba6e57d25256afada3/LICENSE" + }, + { + "name": "k2-fsa/ZipVoice (Swift ports: EnglishTextNormalizer, VocosFbank mel frontend used by LuxTTS)", + "source": "https://github.com/k2-fsa/ZipVoice", + "revision": "2f7326fbfe999a3ad179e3f1af82a424d4a62819", + "license": "Apache-2.0", + "evidence": "https://github.com/k2-fsa/ZipVoice/blob/2f7326fbfe999a3ad179e3f1af82a424d4a62819/LICENSE" + }, + { + "name": "resemble-ai/chatterbox (Swift port: AlignmentStreamAnalyzer)", + "source": "https://github.com/resemble-ai/chatterbox", + "revision": "5de7a54aa4e5e2baadb0182dde554908b48b85c2", + "license": "MIT", + "evidence": "https://github.com/resemble-ai/chatterbox/blob/5de7a54aa4e5e2baadb0182dde554908b48b85c2/LICENSE" + }, + { + "name": "yl4579/StyleTTS2 (Swift port: TextCleaner symbol table)", + "source": "https://github.com/yl4579/StyleTTS2", + "revision": "5cedc71c333f8d8b8551ca59378bdcc7af4c9529", + "license": "MIT", + "evidence": "https://github.com/yl4579/StyleTTS2/blob/5cedc71c333f8d8b8551ca59378bdcc7af4c9529/LICENSE" + }, + { + "name": "FluidInference/mobius (conversion references the Swift ports mirror)", + "source": "https://github.com/FluidInference/mobius", + "revision": "864ef8050f2f281d0761de26e3a03108f9f1ce73", + "license": "Apache-2.0", + "evidence": "https://github.com/FluidInference/mobius/blob/864ef8050f2f281d0761de26e3a03108f9f1ce73/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text and FluidAudio copyright notice; the texts in its ThirdPartyLicenses/ folder for linked components; MIT notices for FunASR, Chatterbox, and StyleTTS2, and Apache-2.0 notices for NeMo, misaki, ZipVoice, and mobius, whose code the library ports.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "SDK code license only: it does not cover any model, voice, tokenizer, or G2P asset the SDK downloads. `requires` lists what the named configuration compiles or links. The SwiftPM `.process(\"TTS/LuxTts/G2p/Resources\")` rule bundles LuxTTS G2P data into every build, so the engine cannot be approved while that entry is blocked. FluidAudio's model downloader fetches Hugging Face `main` for every repository except speaker-diarization-coreml, so #42 must fetch model files from this manifest's pinned revisions rather than from FluidAudio's default. `upstream` records the projects whose code the library source says it ports (every such file is compiled into the library regardless of which feature is used); these are not listed in FluidAudio's ThirdPartyLicenses/, and each hop cites that project's license at the commit inspected on 2026-09-28, because FluidAudio does not record the upstream revision it ported.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "nemo-text-processing", + "kind": "native-library", + "name": "NemoTextProcessing (text-processing-rs)", + "version": "v0.3.1", + "source": "https://github.com/FluidInference/text-processing-rs", + "revision": "46ebddcea5e1ff673c2cb5256a1b6b0e6c4ff527", + "downloads": [ + { + "url": "https://github.com/FluidInference/text-processing-rs/releases/download/v0.3.1/NemoTextProcessing.xcframework.zip", + "sha256": "5fa8c10d4ec26c1bb2413125f351a7222a4c68a23b74476680fbada7e26fc6aa", + "size": 87267919, + "platform": "apple" + } + ], + "paths": [], + "configuration": null, + "requires": [ + "rust-crate-adler2-2.0.1", + "rust-crate-anyhow-1.0.103", + "rust-crate-bimap-0.6.3", + "rust-crate-bitflags-2.13.0", + "rust-crate-cfg-if-1.0.4", + "rust-crate-crc32fast-1.5.0", + "rust-crate-either-1.16.0", + "rust-crate-flate2-1.1.9", + "rust-crate-generic-array-1.4.3", + "rust-crate-getrandom-0.3.4", + "rust-crate-itertools-0.14.0", + "rust-crate-lazy-static-1.5.0", + "rust-crate-libc-0.2.186", + "rust-crate-memchr-2.8.3", + "rust-crate-minimal-lexical-0.2.1", + "rust-crate-miniz-oxide-0.8.9", + "rust-crate-nom-7.1.3", + "rust-crate-num-traits-0.2.19", + "rust-crate-ordered-float-5.3.0", + "rust-crate-ppv-lite86-0.2.21", + "rust-crate-rand-0.9.4", + "rust-crate-rand-chacha-0.9.0", + "rust-crate-rand-core-0.9.5", + "rust-crate-rustfst-1.3.1", + "rust-crate-serde-1.0.228", + "rust-crate-serde-core-1.0.228", + "rust-crate-simd-adler32-0.3.9", + "rust-crate-superslice-1.0.0", + "rust-crate-typenum-1.20.1", + "rust-crate-zerocopy-0.8.53", + "rust-std" + ], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/FluidInference/text-processing-rs/blob/46ebddcea5e1ff673c2cb5256a1b6b0e6c4ff527/LICENSE" + }, + "upstream": [ + { + "name": "NVIDIA NeMo Text Processing grammars", + "source": "https://github.com/NVIDIA/NeMo-text-processing", + "revision": "1f1263579fe57ba7ed783cad3dddee710fcc5064", + "license": "Apache-2.0", + "evidence": "https://github.com/NVIDIA/NeMo-text-processing/blob/1f1263579fe57ba7ed783cad3dddee710fcc5064/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text; NVIDIA NeMo Text Processing notice; plus the notices of every `requires` entry, all statically linked.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Prebuilt Rust staticlib xcframework that FluidAudio's `.binaryTarget` links, built by build-xcframework.sh with `--features ffi,fst-engine` for seven Apple targets. `requires` is the exact linked closure: the 30 crates from `cargo tree --locked -e normal,no-proc-macro` over those targets (build scripts and proc-macros excluded), plus the Rust standard library. Verified against the release binary: every crate version in its panic locations is in that closure, and every slice was built by rustc 1.98.1 (48a229ce). The SwiftPM checksum equals the GitHub release asset digest. FluidAudio's ThirdPartyLicenses file names v0.3.0 while Package.swift pins the v0.3.1 asset, and Cargo.toml at the v0.3.1 tag still reads 0.3.0. The binary is not a reproducible build of the source commit, so the closure is established from Cargo.lock plus binary inspection.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "rust-std", + "kind": "native-library", + "name": "Rust standard library (std, core, alloc, std_detect, panic_unwind, unwind)", + "version": "1.98.1", + "source": "https://github.com/rust-lang/rust", + "revision": "48a229ceaefd4985c50990b14116b6d856af0985", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [ + "rust-compiler-builtins", + "rust-crate-addr2line-0.25.1", + "rust-crate-gimli-0.32.3", + "rust-crate-hashbrown-0.17.1", + "rust-crate-libc-0.2.185", + "rust-crate-memchr-2.7.6", + "rust-crate-object-0.37.3", + "rust-crate-rustc-demangle-0.1.27", + "rust-crate-adler2-2.0.1", + "rust-crate-cfg-if-1.0.4", + "rust-crate-miniz-oxide-0.8.9" + ], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/rust/blob/48a229ceaefd4985c50990b14116b6d856af0985/COPYRIGHT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected), per the Rust project's COPYRIGHT file.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into every NemoTextProcessing slice: the release binary's /rustc/48a229ce…/library paths and its `rustc version 1.98.1 (48a229cea 2026-09-01)` string identify the toolchain (tag 1.98.1 resolves to this commit). `requires` lists compiler-builtins and the crates.io dependencies std links on Apple targets per library/Cargo.lock at this commit.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "rust-compiler-builtins", + "kind": "native-library", + "name": "compiler-builtins (Rust)", + "version": "1.98.1", + "source": "https://github.com/rust-lang/rust/tree/48a229ceaefd4985c50990b14116b6d856af0985/library/compiler-builtins", + "revision": "48a229ceaefd4985c50990b14116b6d856af0985", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "MIT AND Apache-2.0 WITH LLVM-exception AND (MIT OR Apache-2.0)", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/rust/blob/48a229ceaefd4985c50990b14116b6d856af0985/library/compiler-builtins/LICENSE.txt" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "MIT license text, Apache-2.0 with LLVM exception (compiler-rt-derived code), per LICENSE.txt.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://llvm.org/LICENSE.txt" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Split from rust-std because its terms differ: it carries LLVM compiler-rt-derived code under Apache-2.0 WITH LLVM-exception and a MIT libm port (license field of library/compiler-builtins/compiler-builtins/Cargo.toml at this commit).", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "fastcluster", + "kind": "native-library", + "name": "fastcluster (FluidAudio FastClusterWrapper)", + "version": "vendored in FluidAudio v0.17.4", + "source": "https://github.com/FluidInference/FluidAudio/tree/21493f8dac5a97e65742e6ff26f42f164c2fda0f/Sources/FastClusterWrapper", + "revision": "21493f8dac5a97e65742e6ff26f42f164c2fda0f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": "BSD-2-Clause", + "content": null, + "name": null, + "evidence": "https://github.com/FluidInference/FluidAudio/blob/21493f8dac5a97e65742e6ff26f42f164c2fda0f/ThirdPartyLicenses/fastcluster-LICENSE.md" + }, + "upstream": [ + { + "name": "fastcluster", + "source": "https://github.com/fastcluster/fastcluster", + "revision": "9d0e48609283edaae90593a6967d3f2217786d2d", + "license": "BSD-2-Clause", + "evidence": "https://github.com/fastcluster/fastcluster/blob/9d0e48609283edaae90593a6967d3f2217786d2d/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "BSD-2-Clause copyright notice (Daniel Müllner; Google Inc.) and license text in binary distributions.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/bsd-2-clause" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "C++ source vendored in FluidAudio and compiled into every build. Pinned by the FluidAudio commit; the upstream fastcluster release it was copied from is not recorded upstream.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "vbx", + "kind": "native-library", + "name": "VBx clustering (FluidAudio Swift port)", + "version": "vendored in FluidAudio v0.17.4", + "source": "https://github.com/FluidInference/FluidAudio/tree/21493f8dac5a97e65742e6ff26f42f164c2fda0f/Sources/FluidAudio/Diarizer/Offline/Clustering", + "revision": "21493f8dac5a97e65742e6ff26f42f164c2fda0f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/FluidInference/FluidAudio/blob/21493f8dac5a97e65742e6ff26f42f164c2fda0f/ThirdPartyLicenses/vbx-LICENSE.md" + }, + "upstream": [ + { + "name": "BUT Speech@FIT VBx", + "source": "https://github.com/BUTSpeechFIT/VBx", + "revision": "57466e6e245d5cdfe2e88ee6503702ace3ffdd03", + "license": "Apache-2.0", + "evidence": "https://github.com/BUTSpeechFIT/VBx/blob/57466e6e245d5cdfe2e88ee6503702ace3ffdd03/README.md" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text and the BUT Speech@FIT copyright notice.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Swift port compiled into every FluidAudio build. Upstream VBx states Apache-2.0 in its README and has no LICENSE file at the repository root.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "japanese-g2p", + "kind": "native-library", + "name": "Japanese G2P frontend code (FluidAudio Kokoro ANE)", + "version": "vendored in FluidAudio v0.17.4", + "source": "https://github.com/FluidInference/FluidAudio/tree/21493f8dac5a97e65742e6ff26f42f164c2fda0f/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese", + "revision": "21493f8dac5a97e65742e6ff26f42f164c2fda0f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": "Apache-2.0 AND MIT", + "content": null, + "name": null, + "evidence": "https://github.com/FluidInference/FluidAudio/blob/21493f8dac5a97e65742e6ff26f42f164c2fda0f/ThirdPartyLicenses/JapaneseG2P-LICENSE.md" + }, + "upstream": [ + { + "name": "hexgrad/misaki cutlet.py and num2kana.py", + "source": "https://github.com/hexgrad/misaki", + "revision": "fba1236595f2d2bf21d414ba6e57d25256afada3", + "license": "Apache-2.0", + "evidence": "https://github.com/hexgrad/misaki/blob/fba1236595f2d2bf21d414ba6e57d25256afada3/LICENSE" + }, + { + "name": "polm/cutlet", + "source": "https://github.com/polm/cutlet", + "revision": "e97e15c287b0d6a0d936cae4219f85afd47aa509", + "license": "MIT", + "evidence": "https://github.com/polm/cutlet/blob/e97e15c287b0d6a0d936cae4219f85afd47aa509/LICENSE" + }, + { + "name": "Greatdane/Convert-Numbers-to-Japanese", + "source": "https://github.com/Greatdane/Convert-Numbers-to-Japanese", + "revision": "c01b072e8d52c89c307dcbcb69958d05e953d4c1", + "license": "MIT", + "evidence": "https://github.com/Greatdane/Convert-Numbers-to-Japanese/blob/c01b072e8d52c89c307dcbcb69958d05e953d4c1/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 (misaki) and MIT (cutlet, Convert-Numbers-to-Japanese) notices.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Swift port compiled into every FluidAudio build. Only the code is covered here. The Japanese G2P data (trimmed unidic-lite, ja_words.txt) is downloaded from kokoro-82m-coreml/ANE-ja, is not in the #42 English configuration, and is therefore not listed and not approved.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "luxtts-en-us-g2p-lexicon", + "kind": "phonemizer", + "name": "LuxTTS English G2P lexicon (FluidAudio bundled resource)", + "version": "vendored in FluidAudio v0.17.4", + "source": "https://github.com/FluidInference/FluidAudio/tree/21493f8dac5a97e65742e6ff26f42f164c2fda0f/Sources/FluidAudio/TTS/LuxTts/G2p/Resources", + "revision": "21493f8dac5a97e65742e6ff26f42f164c2fda0f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "espeak-ng pronunciations (harvested oracle output)", + "source": "https://github.com/espeak-ng/espeak-ng", + "revision": "ba90c8e9f440ad544f674a790bb5f53878b6ffc5", + "license": "GPL-3.0-or-later", + "evidence": "https://github.com/espeak-ng/espeak-ng/blob/ba90c8e9f440ad544f674a790bb5f53878b6ffc5/COPYING" + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: `luxtts_en_us_lexicon.tsv.zz` and `luxtts_en_us_g2p_aux.json` are processed into every FluidAudio build, and LuxTtsG2p.swift describes them as harvested by probing espeak-ng. FluidAudio's ThirdPartyLicenses/ does not cover them and no data license is stated. Resolve through legal review of espeak-derived data, or an upstream change that makes the resource optional, before FluidAudio can be approved.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "parakeet-tdt-0.6b-v3-coreml", + "kind": "model", + "name": "Parakeet TDT 0.6B v3 Core ML", + "version": "v3", + "source": "https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v3-coreml", + "revision": "7dd20fe6b1797d35f5e3307e8b1732d9a178edfe", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [ + "parakeet-tdt-0.6b-v3-vocab" + ], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "CC-BY-4.0", + "name": null, + "evidence": "https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v3-coreml/blob/7dd20fe6b1797d35f5e3307e8b1732d9a178edfe/README.md" + }, + "upstream": [ + { + "name": "nvidia/parakeet-tdt-0.6b-v3", + "source": "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3", + "revision": "541d1f99c6b0c3cd0b11a95167540bb8edefd82b", + "license": "CC-BY-4.0", + "evidence": "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3/blob/541d1f99c6b0c3cd0b11a95167540bb8edefd82b/README.md" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Credit NVIDIA (parakeet-tdt-0.6b-v3) and FluidInference (Core ML conversion), link CC-BY-4.0, and state that the files are modified conversions.", + "download": { + "access": "public", + "presentation": "link" + }, + "termsUrl": "https://creativecommons.org/licenses/by/4.0/" + }, + "distribution": "user-download", + "review": { + "date": "2026-09-28", + "notes": "FluidAudio v0.17.4 loads Preprocessor.mlmodelc, Encoder.mlmodelc (int8 default), Decoder.mlmodelc, and JointDecisionv3.mlmodelc. Public, ungated download. downloads[] with per-file byte hashes and sizes must be recorded before approval.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "parakeet-tdt-0.6b-v3-vocab", + "kind": "tokenizer", + "name": "Parakeet TDT 0.6B v3 vocabulary", + "version": "v3", + "source": "https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v3-coreml", + "revision": "7dd20fe6b1797d35f5e3307e8b1732d9a178edfe", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "CC-BY-4.0", + "name": null, + "evidence": "https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v3-coreml/blob/7dd20fe6b1797d35f5e3307e8b1732d9a178edfe/README.md" + }, + "upstream": [ + { + "name": "nvidia/parakeet-tdt-0.6b-v3 SentencePiece tokenizer", + "source": "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3", + "revision": "541d1f99c6b0c3cd0b11a95167540bb8edefd82b", + "license": "CC-BY-4.0", + "evidence": "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3/blob/541d1f99c6b0c3cd0b11a95167540bb8edefd82b/README.md" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Credit NVIDIA and FluidInference and link CC-BY-4.0.", + "download": { + "access": "public", + "presentation": "link" + }, + "termsUrl": "https://creativecommons.org/licenses/by/4.0/" + }, + "distribution": "user-download", + "review": { + "date": "2026-09-28", + "notes": "parakeet_vocab.json at the pinned revision.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "parakeet-realtime-eou-120m-coreml", + "kind": "model", + "name": "Parakeet Realtime EOU 120M Core ML", + "version": "v1", + "source": "https://huggingface.co/FluidInference/parakeet-realtime-eou-120m-coreml", + "revision": "40a23f4c0b333aa17ad8c0f2ea47ec2347f2f355", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [ + "parakeet-realtime-eou-120m-vocab" + ], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "other", + "name": "NVIDIA Open Model License Agreement", + "evidence": "https://huggingface.co/FluidInference/parakeet-realtime-eou-120m-coreml/blob/40a23f4c0b333aa17ad8c0f2ea47ec2347f2f355/README.md" + }, + "upstream": [ + { + "name": "nvidia/parakeet_realtime_eou_120m-v1", + "source": "https://huggingface.co/nvidia/parakeet_realtime_eou_120m-v1", + "revision": "a7e2b4629593dce0ec19f600e00e9904353fda2d", + "license": "other", + "licenseName": "NVIDIA Open Model License Agreement", + "evidence": null + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: the model card names the NVIDIA Open Model License but links a mutable nvidia.com page; the exact agreement text and version have not been captured, so redistribution, commercial-use, and modification terms are unknown. FluidAudio v0.17.4 (StreamingEouAsrManager) loads streaming_encoder.mlmodelc, decoder.mlmodelc, and joint_decision.mlmodelc from the 160ms/, 320ms/, or 1280ms/ directory, plus that directory's vocab.json (a separate entry). #42 streaming must wait for these terms to be captured and reviewed, or use TDT v3 chunking.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "parakeet-realtime-eou-120m-vocab", + "kind": "tokenizer", + "name": "Parakeet Realtime EOU 120M vocabulary", + "version": "v1", + "source": "https://huggingface.co/FluidInference/parakeet-realtime-eou-120m-coreml", + "revision": "40a23f4c0b333aa17ad8c0f2ea47ec2347f2f355", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "other", + "name": "NVIDIA Open Model License Agreement", + "evidence": "https://huggingface.co/FluidInference/parakeet-realtime-eou-120m-coreml/blob/40a23f4c0b333aa17ad8c0f2ea47ec2347f2f355/README.md" + }, + "upstream": [ + { + "name": "nvidia/parakeet_realtime_eou_120m-v1 SentencePiece tokenizer", + "source": "https://huggingface.co/nvidia/parakeet_realtime_eou_120m-v1", + "revision": "a7e2b4629593dce0ec19f600e00e9904353fda2d", + "license": "other", + "licenseName": "NVIDIA Open Model License Agreement", + "evidence": null + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked with its model: the NVIDIA Open Model License text is not captured. FluidAudio loads /vocab.json; 160ms/vocab.json, 320ms/vocab.json, 1280ms/vocab.json, and the root vocab.json are the same git blob (462081190dbb331a0dde13e99d5cfa99b302f0a7, 17,437 bytes) at the pinned revision. The root tokenizer.model is not loaded by FluidAudio and is not listed.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "silero-vad-coreml", + "kind": "model", + "name": "Silero VAD Core ML", + "version": "v6.2.1 (unified 256 ms)", + "source": "https://huggingface.co/FluidInference/silero-vad-coreml", + "revision": "b419383c55c110e2c9271fa6ee0ea83d03c70d96", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "MIT", + "name": null, + "evidence": "https://huggingface.co/FluidInference/silero-vad-coreml/blob/b419383c55c110e2c9271fa6ee0ea83d03c70d96/README.md" + }, + "upstream": [ + { + "name": "onnx-community/silero-vad", + "source": "https://huggingface.co/onnx-community/silero-vad", + "revision": "e71cae966052b992a7eca6b17738916ce0eca4ec", + "license": "MIT", + "evidence": "https://huggingface.co/onnx-community/silero-vad/blob/e71cae966052b992a7eca6b17738916ce0eca4ec/README.md" + }, + { + "name": "snakers4/silero-vad", + "source": "https://github.com/snakers4/silero-vad", + "revision": "7e30209a3e901f9842f81b225f3e93d8199902b1", + "license": "MIT", + "evidence": "https://github.com/snakers4/silero-vad/blob/7e30209a3e901f9842f81b225f3e93d8199902b1/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "MIT copyright notice and license text for Silero VAD.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "FluidAudio v0.17.4 loads silero-vad-unified-256ms-v6.2.1.mlmodelc. Small and MIT-licensed, so classified as a bundle candidate. It cannot be approved until downloads[] records the byte hash and size of every file in that .mlmodelc at the pinned revision (the checker requires downloads for any approved content bundle), and legal approval is recorded.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-82m-coreml-ane", + "kind": "model", + "name": "Kokoro 82M Core ML (ANE, English)", + "version": "v1.0", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [ + "kokoro-82m-ane-vocab", + "kokoro-voice-af-heart", + "kokoro-english-lexicon", + "kokoro-english-g2p" + ], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "Apache-2.0", + "name": null, + "evidence": "https://huggingface.co/FluidInference/kokoro-82m-coreml/blob/006395f65025af251858b1ab0a7178a6a1e73f9f/ANE/LICENSE" + }, + "upstream": [ + { + "name": "hexgrad/Kokoro-82M", + "source": "https://huggingface.co/hexgrad/Kokoro-82M", + "revision": "f3ff3571791e39611d31c381e3a41a3af07b4987", + "license": "Apache-2.0", + "evidence": "https://huggingface.co/hexgrad/Kokoro-82M/blob/f3ff3571791e39611d31c381e3a41a3af07b4987/README.md" + }, + { + "name": "laishere/kokoro-coreml", + "source": "https://github.com/laishere/kokoro-coreml", + "revision": "484907db6a8347a6afb6e7b86850ea2878c6a3fb", + "license": "Apache-2.0", + "evidence": "https://github.com/laishere/kokoro-coreml/blob/484907db6a8347a6afb6e7b86850ea2878c6a3fb/LICENSE" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text; credit hexgrad (Kokoro-82M), laishere (kokoro-coreml), and FluidInference.", + "download": { + "access": "public", + "presentation": "link" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "user-download", + "review": { + "date": "2026-09-28", + "notes": "The seven ANE/ .mlmodelc stages FluidAudio v0.17.4 names (KokoroAlbert, KokoroPostAlbert, KokoroAlignment, KokoroProsody, KokoroNoise_v2, KokoroVocoder, KokoroTail_v2). Cannot be approved while kokoro-english-lexicon and kokoro-english-g2p are blocked.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-82m-ane-vocab", + "kind": "tokenizer", + "name": "Kokoro 82M ANE phoneme vocabulary", + "version": "v1.0", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "Apache-2.0", + "name": null, + "evidence": "https://huggingface.co/FluidInference/kokoro-82m-coreml/blob/006395f65025af251858b1ab0a7178a6a1e73f9f/ANE/LICENSE" + }, + "upstream": [ + { + "name": "hexgrad/Kokoro-82M", + "source": "https://huggingface.co/hexgrad/Kokoro-82M", + "revision": "f3ff3571791e39611d31c381e3a41a3af07b4987", + "license": "Apache-2.0", + "evidence": "https://huggingface.co/hexgrad/Kokoro-82M/blob/f3ff3571791e39611d31c381e3a41a3af07b4987/README.md" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text; credit hexgrad.", + "download": { + "access": "public", + "presentation": "link" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "user-download", + "review": { + "date": "2026-09-28", + "notes": "ANE/vocab.json at the pinned revision.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-voice-af-heart", + "kind": "voice", + "name": "Kokoro voice af_heart", + "version": "v1.0", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "Apache-2.0", + "name": null, + "evidence": "https://huggingface.co/FluidInference/kokoro-82m-coreml/blob/006395f65025af251858b1ab0a7178a6a1e73f9f/ANE/LICENSE" + }, + "upstream": [ + { + "name": "hexgrad/Kokoro-82M voices/af_heart.pt", + "source": "https://huggingface.co/hexgrad/Kokoro-82M", + "revision": "f3ff3571791e39611d31c381e3a41a3af07b4987", + "license": "Apache-2.0", + "evidence": "https://huggingface.co/hexgrad/Kokoro-82M/blob/f3ff3571791e39611d31c381e3a41a3af07b4987/README.md" + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text; credit hexgrad.", + "download": { + "access": "public", + "presentation": "link" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "user-download", + "review": { + "date": "2026-09-28", + "notes": "ANE/af_heart.bin, FluidAudio's default English voice. Other voices are not listed and not approved.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-english-lexicon", + "kind": "phonemizer", + "name": "Kokoro English lexicon cache (Misaki)", + "version": "v1.0", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "hexgrad/misaki us_gold.json and us_silver.json", + "source": "https://github.com/hexgrad/misaki", + "revision": "fba1236595f2d2bf21d414ba6e57d25256afada3", + "license": "Apache-2.0", + "evidence": "https://github.com/hexgrad/misaki/blob/fba1236595f2d2bf21d414ba6e57d25256afada3/LICENSE" + }, + { + "name": "Source dictionaries misaki compiled its English lexicons from (undocumented)", + "source": "https://github.com/hexgrad/misaki", + "license": "NOASSERTION", + "evidence": null + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: us_lexicon_cache.json at the repository root, which FluidAudio documents as the Misaki lexicon. Misaki's repository license covers its code and its own contribution, but misaki does not document the dictionaries its gold and silver lexicons were compiled from, so the data's terms cannot be inherited from the repository license. Trace those sources (or obtain a qualified review) before reclassifying.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-english-g2p", + "kind": "phonemizer", + "name": "Kokoro English BART G2P model", + "version": "unversioned", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: G2PEncoder.mlmodelc, G2PDecoder.mlmodelc, and g2p_vocab.json at the repository root. FluidAudio documents them only as a 'BART G2P CoreML model'. Neither the training source nor the weights' license is recorded, and the repository's apache-2.0 tag cannot be assumed to cover separately sourced weights.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kokoro-spanish-french-g2p", + "kind": "phonemizer", + "name": "Kokoro Spanish and French lexicon caches", + "version": "unversioned", + "source": "https://huggingface.co/FluidInference/kokoro-82m-coreml", + "revision": "006395f65025af251858b1ab0a7178a6a1e73f9f", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "ipa-dict fr_FR and es_ES word lists", + "source": "https://github.com/open-dict-data/ipa-dict", + "revision": "43c3570eb3553bdd19fccd2bd0091534889af023", + "license": "MIT", + "evidence": "https://github.com/open-dict-data/ipa-dict/blob/43c3570eb3553bdd19fccd2bd0091534889af023/LICENSE" + }, + { + "name": "espeak-ng pronunciations (generated output)", + "source": "https://github.com/espeak-ng/espeak-ng", + "revision": "ba90c8e9f440ad544f674a790bb5f53878b6ffc5", + "license": "GPL-3.0-or-later", + "evidence": "https://github.com/espeak-ng/espeak-ng/blob/ba90c8e9f440ad544f674a790bb5f53878b6ffc5/COPYING" + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: es_lexicon_cache.json and fr_lexicon_cache.json combine MIT ipa-dict words with pronunciations generated by espeak-ng (GPL-3.0), and the data's license is not stated. They are also outside the #42 English configuration. Recorded because FluidAudio's ThirdPartyLicenses/ lists them; that description (ThirdPartyLicenses/KokoroAneSpanishFrenchG2P-LICENSE.md at FluidAudio 21493f8) is FluidAudio's account, not license evidence inside this repository at the pinned revision.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "pyannote-segmentation-legacy-coreml", + "kind": "model", + "name": "pyannote_segmentation.mlmodelc (legacy speaker segmentation)", + "version": "unversioned", + "source": "https://huggingface.co/FluidInference/speaker-diarization-coreml", + "revision": "df2625ac79a7ac6b65ad868fee6d80f320da4232", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "Unrecorded pyannote segmentation checkpoint (the model card cites Plaquet and Bredin 2023)", + "source": "https://github.com/pyannote/pyannote-audio", + "license": "NOASSERTION", + "evidence": null + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: loaded by FluidAudio's online diarizer and speaker-embedding path (ModelNames.Diarizer). NOTICE.md at the pinned revision excludes it from the repository's scoped CC-BY-4.0 ('their original source and licensing must be evaluated separately'); its Core ML metadata records only torch 2.6.0 and coremltools 8.3.0, with no author, license, or source checkpoint. The in-scope Community-1 Segmentation.mlmodelc is a different artifact.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "wespeaker-v2-legacy-coreml", + "kind": "model", + "name": "wespeaker_v2.mlmodelc (legacy speaker embedding)", + "version": "unversioned", + "source": "https://huggingface.co/FluidInference/speaker-diarization-coreml", + "revision": "df2625ac79a7ac6b65ad868fee6d80f320da4232", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "fluidaudio" + ], + "license": { + "code": null, + "content": "NOASSERTION", + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "Unrecorded WeSpeaker speaker-embedding checkpoint (the model card cites Wang et al. 2023)", + "source": "https://github.com/wenet-e2e/wespeaker", + "license": "NOASSERTION", + "evidence": null + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked: loaded by FluidAudio's speaker-embedding path (ModelNames.Diarizer). NOTICE.md at the pinned revision excludes it from the repository's scoped CC-BY-4.0; its Core ML metadata records only torch 2.6.0 and coremltools 8.3.0 (generated class name wespeaker_int8), with no author, license, or source checkpoint, so neither the WeSpeaker model nor its training data can be identified. The in-scope Community-1 Embedding.mlmodelc is a different artifact.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "sherpa-onnx", + "kind": "engine", + "name": "sherpa-onnx", + "version": "v1.13.8", + "source": "https://github.com/k2-fsa/sherpa-onnx", + "revision": "11afbd009a7f8c08f4bcf2fc1b265d0df4670fbf", + "downloads": [], + "paths": [], + "configuration": "sherpa-onnx v1.13.8 static libraries with the C API, recognition only: SHERPA_ONNX_ENABLE_TTS=OFF, SHERPA_ONNX_ENABLE_SPEAKER_DIARIZATION=OFF, SHERPA_ONNX_ENABLE_WEBSOCKET=OFF, SHERPA_ONNX_ENABLE_PORTAUDIO=OFF, SHERPA_ONNX_ENABLE_BINARY=OFF, SHERPA_ONNX_ENABLE_PYTHON=OFF, SHERPA_ONNX_ENABLE_GPU=OFF, SHERPA_ONNX_USE_PRE_INSTALLED_ONNXRUNTIME_IF_AVAILABLE=OFF, SHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=OFF, SHERPA_ONNX_USE_STATIC_CRT=OFF with CMAKE_MSVC_RUNTIME_LIBRARY left unset, CMAKE_BUILD_TYPE=Release; linux x86_64, linux aarch64, windows x64, macOS arm64. C and C++ runtimes are linked dynamically from the target system and are not packaged: glibc, libstdc++, and libgcc_s on Linux; the MSVC /MD runtime (Visual C++ Redistributable, a system prerequisite) on Windows; libSystem and libc++ on macOS. Provisional until #26 confirms it.", + "requires": [ + "onnxruntime", + "kaldi-native-fbank", + "kaldi-decoder", + "simple-sentencepiece", + "nlohmann-json" + ], + "usedBy": [ + "#26" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/k2-fsa/sherpa-onnx/blob/11afbd009a7f8c08f4bcf2fc1b265d0df4670fbf/LICENSE" + }, + "upstream": [ + { + "name": "kaldi-asr/kaldi (copied/modified: text-utils, parse-options, timer, fst-utils, resample)", + "source": "https://github.com/kaldi-asr/kaldi", + "revision": "e02e35f0254bb033fab73d1df99fc34123e31d56", + "license": "Apache-2.0", + "evidence": "https://github.com/kaldi-asr/kaldi/blob/e02e35f0254bb033fab73d1df99fc34123e31d56/COPYING" + }, + { + "name": "k2-fsa/k2 (copied: csrc/math.h from k2/csrc/utils.h)", + "source": "https://github.com/k2-fsa/k2", + "revision": "ec31d2c96e04665eadb276ebcfd436892f74b0aa", + "license": "Apache-2.0", + "evidence": "https://github.com/k2-fsa/k2/blob/ec31d2c96e04665eadb276ebcfd436892f74b0aa/LICENSE" + }, + { + "name": "k2-fsa/icefall (copied: lodr-fst from icefall/utils/ngram_lm.py)", + "source": "https://github.com/k2-fsa/icefall", + "revision": "3f848bb6d0acc970c9b294a30ca0a04a7c9c78d1", + "license": "Apache-2.0", + "evidence": "https://github.com/k2-fsa/icefall/blob/3f848bb6d0acc970c9b294a30ca0a04a7c9c78d1/LICENSE" + }, + { + "name": "abjadai/catt (adapted: tashkeel-tokenizer from tashkeel_tokenizer.py)", + "source": "https://github.com/abjadai/catt", + "revision": "8d5330499feb85f6625e6af632141ed2ed6065fd", + "license": "Apache-2.0", + "evidence": "https://github.com/abjadai/catt/blob/8d5330499feb85f6625e6af632141ed2ed6065fd/LICENSE" + }, + { + "name": "ReneNyffenegger/cpp-base64 (adapted: base64-decode.cc)", + "source": "https://github.com/ReneNyffenegger/cpp-base64", + "revision": "951de609dbe27ce8864dfe47323c4ade96bee86e", + "license": "LicenseRef-cpp-base64", + "licenseName": "cpp-base64 license (zlib-style), Copyright 2004-2017 René Nyffenegger", + "evidence": "https://github.com/ReneNyffenegger/cpp-base64/blob/951de609dbe27ce8864dfe47323c4ade96bee86e/LICENSE" + }, + { + "name": "KentonMurray/Buckwalter transliteration table (copied into tashkeel-tokenizer.cc)", + "source": "https://github.com/KentonMurray/Buckwalter", + "revision": "66e42d8b80112aa370ef9ce2bbecfd9b40286588", + "license": "NOASSERTION", + "evidence": null + }, + { + "name": "StackOverflow answer to question 216823 (copied: string trim helpers in symbol-table.cc)", + "source": "https://stackoverflow.com/questions/216823/how-to-trim-a-stdstring", + "license": "NOASSERTION", + "evidence": null + } + ], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text and sherpa-onnx copyright notice; Apache-2.0 notices for Kaldi (including its COPYING authorship note), k2, icefall, and CATT; the cpp-base64 notice, retained unaltered in any source distribution, with base64-decode.cc marked as an altered version.", + "download": { + "access": "public", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "SDK code license only; #26 has not chosen a model, so none is listed. `requires` follows CMakeLists.txt at the pinned commit for the named options. SHERPA_ONNX_USE_PRE_INSTALLED_ONNXRUNTIME_IF_AVAILABLE defaults to ON, which makes cmake/onnxruntime.cmake use any ONNX Runtime found through SHERPA_ONNXRUNTIME_INCLUDE_DIR/SHERPA_ONNXRUNTIME_LIB_DIR or system paths without a hash check; the configuration turns it OFF so only the hash-pinned onnxruntime archives can be linked, and #26's build cross-check must assert it. Every other dependency's local-file fallback is still verified by its CMake URL_HASH. espeak-ng and piper-phonemize are only fetched when TTS is enabled, and hclust-cpp only with speaker diarization. Any other variant needs its own reviewed engine entry. The engine cannot be approved while onnxruntime is blocked. Runtime linkage: sherpa-onnx defaults to SHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=ON (injecting -static-libstdc++ -static-libgcc on Linux static builds) and SHERPA_ONNX_USE_STATIC_CRT=ON (MSVC /MT); the configuration turns both OFF so no GCC or Microsoft runtime code is linked into Murmur's artifacts, and the Windows onnxruntime archive is therefore the MD variant. Shipping any runtime app-locally would need its own reviewed entry. Blocked: sources compiled in this configuration copy code from projects outside sherpa-onnx's Apache-2.0 license (upstream). Kaldi, k2, icefall, and CATT are Apache-2.0 and cpp-base64 has its own zlib-style notice, but the Buckwalter table's repository has no license and the StackOverflow trim snippet has no pinnable source or recorded license. Those two need a qualified review, or an upstream replacement, before the engine can be classified as a bundle again. websocket-only tee-stream.h (copied from wordaligned.org) is excluded by SHERPA_ONNX_ENABLE_WEBSOCKET=OFF.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "onnxruntime", + "kind": "native-library", + "name": "ONNX Runtime static libraries (csukuangfj/onnxruntime-libs rebuild)", + "version": "v1.28.2", + "source": "https://github.com/csukuangfj/onnxruntime-libs", + "revision": "5cc3d2e84d9eade2562cf29a93fa3a520a75ca57", + "downloads": [ + { + "url": "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.28.2/onnxruntime-linux-x64-static_lib-1.28.2-glibc2_17.zip", + "sha256": "2dada92465b40a16fb5011956d93eaf90e3091cf621a953e5371a5e6a687794e", + "size": 24400093, + "platform": "linux-x86_64" + }, + { + "url": "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.28.2/onnxruntime-linux-aarch64-static_lib-1.28.2-glibc2_17.zip", + "sha256": "fba13fa68dbc9305512869bfa74f53a15e7af983c44ba8a9cbecbce5c21b33b3", + "size": 22453157, + "platform": "linux-aarch64" + }, + { + "url": "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.28.2/onnxruntime-win-x64-static_lib-MD-Release-1.28.2.tar.bz2", + "sha256": "6fdf64a246ff8c46a303f84b7514e94eda63ae8fefa94491b157f9c14449dbaa", + "size": 85157422, + "platform": "windows-x64" + }, + { + "url": "https://github.com/csukuangfj/onnxruntime-libs/releases/download/v1.28.2/onnxruntime-osx-arm64-static_lib-1.28.2.zip", + "sha256": "bf6e35dc9440d2899c3d21f52b1045fb99176a6ae822281b7245c5418cc338ab", + "size": 20324964, + "platform": "macos-arm64" + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "NOASSERTION", + "content": null, + "name": null, + "evidence": null + }, + "upstream": [ + { + "name": "microsoft/onnxruntime", + "source": "https://github.com/microsoft/onnxruntime", + "revision": "33ca9628233dc8f002435e868d4c2e9f82766ca1", + "license": "MIT", + "evidence": "https://github.com/microsoft/onnxruntime/blob/33ca9628233dc8f002435e868d4c2e9f82766ca1/LICENSE" + } + ], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked until the rebuild's provenance is accepted: the redistributing repository declares no license and does not tie the prebuilt archives to a reproducible build of the upstream v1.28.2 commit. Upstream ONNX Runtime is MIT, and its static libraries also embed third-party code covered by ThirdPartyNotices.txt. Hashes and sizes match the GitHub release digests and sherpa-onnx's CMake pins.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kaldi-native-fbank", + "kind": "native-library", + "name": "kaldi-native-fbank", + "version": "v1.22.3", + "source": "https://github.com/csukuangfj/kaldi-native-fbank", + "revision": "b09e686fe2084732ddd30d1ef80acfc0f13eaf01", + "downloads": [ + { + "url": "https://github.com/csukuangfj/kaldi-native-fbank/archive/refs/tags/v1.22.3.tar.gz", + "sha256": "9176cc66fc7ce1edf85cf355b06e320c57db6297df74277f575183468893cf61", + "size": 71144 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/csukuangfj/kaldi-native-fbank/blob/b09e686fe2084732ddd30d1ef80acfc0f13eaf01/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Source archive fetched by sherpa-onnx cmake/kaldi-native-fbank.cmake; the hash matches its pin.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kaldi-decoder", + "kind": "native-library", + "name": "kaldi-decoder", + "version": "v0.3.0", + "source": "https://github.com/k2-fsa/kaldi-decoder", + "revision": "62fda2d74246a90db9570e46fb16b012f813fae7", + "downloads": [ + { + "url": "https://github.com/k2-fsa/kaldi-decoder/archive/refs/tags/v0.3.0.tar.gz", + "sha256": "b9f34cfb4fd3b1344100eead79ef4d37aa15962274b9e3056de345021f76a1b0", + "size": 51199 + } + ], + "paths": [], + "configuration": null, + "requires": [ + "kaldifst", + "eigen" + ], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/k2-fsa/kaldi-decoder/blob/62fda2d74246a90db9570e46fb16b012f813fae7/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Source archive fetched by sherpa-onnx cmake/kaldi-decoder.cmake; the hash matches its pin. Its CMakeLists includes kaldifst (from its own cmake/) and eigen (resolved to sherpa-onnx cmake/eigen.cmake).", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "kaldifst", + "kind": "native-library", + "name": "kaldifst", + "version": "v1.8.0", + "source": "https://github.com/k2-fsa/kaldifst", + "revision": "ab5bdd013bdf13921e6aeee77db5722ebf9955fb", + "downloads": [ + { + "url": "https://github.com/k2-fsa/kaldifst/archive/refs/tags/v1.8.0.tar.gz", + "sha256": "3f247b7e5a2409071202f5e2bc6200060f66728c0a3443c03923ad2723e040b3", + "size": 172147 + } + ], + "paths": [], + "configuration": null, + "requires": [ + "openfst" + ], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/k2-fsa/kaldifst/blob/ab5bdd013bdf13921e6aeee77db5722ebf9955fb/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Fetched by kaldi-decoder v0.3.0 cmake/kaldifst.cmake; the hash matches its pin. GitHub reports NOASSERTION because the LICENSE file carries OpenFst's legal-notices header over the Apache-2.0 text.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "openfst", + "kind": "native-library", + "name": "OpenFst (csukuangfj fork)", + "version": "v1.8.5-2026-07-09", + "source": "https://github.com/csukuangfj/openfst", + "revision": "f702a6317d600ac953fdbb4978674c3a28b6115c", + "downloads": [ + { + "url": "https://github.com/csukuangfj/openfst/archive/refs/tags/v1.8.5-2026-07-09.tar.gz", + "sha256": "2ff712a32952fcb01d351121a6bc8ccf4fdc6b2aa06ce8df2b3095dedd518c0e", + "size": 1501685 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/csukuangfj/openfst/blob/f702a6317d600ac953fdbb4978674c3a28b6115c/COPYING" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Source archive fetched through sherpa-onnx cmake/openfst.cmake (included by kaldifst); the hash matches its pin.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "eigen", + "kind": "native-library", + "name": "Eigen", + "version": "5.0.1", + "source": "https://gitlab.com/libeigen/eigen", + "revision": "bc3b39870ecb690a623a3f49149a358b95c5781d", + "downloads": [ + { + "url": "https://gitlab.com/libeigen/eigen/-/archive/5.0.1/eigen-5.0.1.tar.gz", + "sha256": "e9c326dc8c05cd1e044c71f30f1b2e34a6161a3b6ecf445d56b53ff1669e3dec", + "size": 2967272 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "MPL-2.0 AND BSD-3-Clause AND Apache-2.0 AND Minpack", + "content": null, + "name": null, + "evidence": "https://gitlab.com/libeigen/eigen/-/blob/bc3b39870ecb690a623a3f49149a358b95c5781d/COPYING.README" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "MPL-2.0 notice with a pointer to Eigen's source; BSD-3-Clause, Apache-2.0, and Minpack notices for the files they cover.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.mozilla.org/en-US/MPL/2.0/" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Header-only; fetched by sherpa-onnx cmake/eigen.cmake for kaldi-decoder; the hash matches its pin. MPL-2.0 is file-level copyleft: modified Eigen files must be offered as source.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "simple-sentencepiece", + "kind": "native-library", + "name": "simple-sentencepiece", + "version": "v0.7", + "source": "https://github.com/pkufool/simple-sentencepiece", + "revision": "62dd423df1d51da5ea06f1c3a046fc04f01b4f39", + "downloads": [ + { + "url": "https://github.com/pkufool/simple-sentencepiece/archive/refs/tags/v0.7.tar.gz", + "sha256": "1748a822060a35baa9f6609f84efc8eb54dc0e74b9ece3d82367b7119fdc75af", + "size": 355335 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/pkufool/simple-sentencepiece/blob/62dd423df1d51da5ea06f1c3a046fc04f01b4f39/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Tokenizer library code fetched by sherpa-onnx cmake/simple-sentencepiece.cmake; the hash matches its pin. Tokenizer data ships with the model #26 chooses.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "nlohmann-json", + "kind": "native-library", + "name": "nlohmann/json", + "version": "v3.12.0", + "source": "https://github.com/nlohmann/json", + "revision": "55f93686c01528224f448c19128836e7df245f72", + "downloads": [ + { + "url": "https://github.com/nlohmann/json/archive/refs/tags/v3.12.0.tar.gz", + "sha256": "4b92eb0c06d10683f7447ce9406cb97cd4b453be18d7279320f7b2f025c10187", + "size": 9678593 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/nlohmann/json/blob/55f93686c01528224f448c19128836e7df245f72/LICENSE.MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "MIT copyright notice and license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Header-only; fetched by sherpa-onnx cmake/json.cmake unconditionally; the hash matches its pin.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "espeak-ng", + "kind": "native-library", + "name": "espeak-ng (csukuangfj fork)", + "version": "ed530aa", + "source": "https://github.com/csukuangfj/espeak-ng", + "revision": "ed530aa113046142eb5115cf2fc9157854d0ffe1", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "GPL-3.0-or-later", + "content": null, + "name": null, + "evidence": "https://github.com/csukuangfj/espeak-ng/blob/ed530aa113046142eb5115cf2fc9157854d0ffe1/COPYING" + }, + "upstream": [], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked and unreachable from the named sherpa-onnx configuration: only fetched when SHERPA_ONNX_ENABLE_TTS=ON. Statically linking GPL-3.0 code into Murmur's Apache-2.0 distributions is not supported.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "piper-phonemize", + "kind": "native-library", + "name": "piper-phonemize (csukuangfj fork)", + "version": "f3ff95a", + "source": "https://github.com/csukuangfj/piper-phonemize", + "revision": "f3ff95afc03640bc1399e113e83361192a2fafb4", + "downloads": [], + "paths": [], + "configuration": null, + "requires": [ + "espeak-ng" + ], + "usedBy": [ + "#26", + "sherpa-onnx" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/csukuangfj/piper-phonemize/blob/f3ff95afc03640bc1399e113e83361192a2fafb4/LICENSE.md" + }, + "upstream": [], + "terms": { + "redistribution": "unknown", + "commercialUse": "unknown", + "modification": "unknown", + "attribution": "", + "download": { + "access": "unknown", + "presentation": "unknown" + }, + "termsUrl": null + }, + "distribution": "blocked", + "review": { + "date": "2026-09-28", + "notes": "Blocked and unreachable from the named sherpa-onnx configuration: only fetched when SHERPA_ONNX_ENABLE_TTS=ON. Its own code is MIT, but it requires GPL-3.0 espeak-ng, and GitHub reports the repository as GPL-3.0.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + } + }, + { + "id": "rust-crate-adler2-2.0.1", + "kind": "native-library", + "name": "adler2 (Rust crate)", + "version": "2.0.1", + "source": "https://crates.io/crates/adler2/2.0.1", + "revision": "89a031a0f42eeff31c70dc598b398cbf31f1680f", + "downloads": [ + { + "url": "https://static.crates.io/crates/adler2/adler2-2.0.1.crate", + "sha256": "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa", + "size": 13366 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing", + "rust-std" + ], + "license": { + "code": "0BSD OR MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/oyvindln/adler2/blob/89a031a0f42eeff31c70dc598b398cbf31f1680f/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c), as a dependency of miniz_oxide. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/oyvindln/adler2" + }, + { + "id": "rust-crate-anyhow-1.0.103", + "kind": "native-library", + "name": "anyhow (Rust crate)", + "version": "1.0.103", + "source": "https://crates.io/crates/anyhow/1.0.103", + "revision": "5bdb0e24db3994be119d42f18fe2d655e1f68f4a", + "downloads": [ + { + "url": "https://static.crates.io/crates/anyhow/anyhow-1.0.103.crate", + "sha256": "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3", + "size": 48746 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/dtolnay/anyhow/blob/5bdb0e24db3994be119d42f18fe2d655e1f68f4a/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/dtolnay/anyhow" + }, + { + "id": "rust-crate-bimap-0.6.3", + "kind": "native-library", + "name": "bimap (Rust crate)", + "version": "0.6.3", + "source": "https://crates.io/crates/bimap/0.6.3", + "revision": "8e5a4e224b95857da8a28a7dd4a1a9eec241ef7d", + "downloads": [ + { + "url": "https://static.crates.io/crates/bimap/bimap-0.6.3.crate", + "sha256": "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7", + "size": 26660 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "Apache-2.0 OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/billyrieger/bimap-rs/blob/8e5a4e224b95857da8a28a7dd4a1a9eec241ef7d/LICENSE_MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/billyrieger/bimap-rs" + }, + { + "id": "rust-crate-bitflags-2.13.0", + "kind": "native-library", + "name": "bitflags (Rust crate)", + "version": "2.13.0", + "source": "https://crates.io/crates/bitflags/2.13.0", + "revision": "9253889ee93921055309908c46272bd6c5c6ad99", + "downloads": [ + { + "url": "https://static.crates.io/crates/bitflags/bitflags-2.13.0.crate", + "sha256": "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8", + "size": 51280 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/bitflags/bitflags/blob/9253889ee93921055309908c46272bd6c5c6ad99/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/bitflags/bitflags" + }, + { + "id": "rust-crate-cfg-if-1.0.4", + "kind": "native-library", + "name": "cfg-if (Rust crate)", + "version": "1.0.4", + "source": "https://crates.io/crates/cfg-if/1.0.4", + "revision": "3510ca6abea34cbbc702509a4e50ea9709925eda", + "downloads": [ + { + "url": "https://static.crates.io/crates/cfg-if/cfg-if-1.0.4.crate", + "sha256": "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801", + "size": 9360 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing", + "rust-std" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/cfg-if/blob/3510ca6abea34cbbc702509a4e50ea9709925eda/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/cfg-if" + }, + { + "id": "rust-crate-crc32fast-1.5.0", + "kind": "native-library", + "name": "crc32fast (Rust crate)", + "version": "1.5.0", + "source": "https://crates.io/crates/crc32fast/1.5.0", + "revision": "dbf4f76cd71cdcc57d9164cbd46890d53ce0423c", + "downloads": [ + { + "url": "https://static.crates.io/crates/crc32fast/crc32fast-1.5.0.crate", + "sha256": "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511", + "size": 40723 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/srijs/rust-crc32fast/blob/dbf4f76cd71cdcc57d9164cbd46890d53ce0423c/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/srijs/rust-crc32fast" + }, + { + "id": "rust-crate-either-1.16.0", + "kind": "native-library", + "name": "either (Rust crate)", + "version": "1.16.0", + "source": "https://crates.io/crates/either/1.16.0", + "revision": "8f4ecd9964ea909a7f989a3668a2710310a0e62f", + "downloads": [ + { + "url": "https://static.crates.io/crates/either/either-1.16.0.crate", + "sha256": "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e", + "size": 21248 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rayon-rs/either/blob/8f4ecd9964ea909a7f989a3668a2710310a0e62f/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rayon-rs/either" + }, + { + "id": "rust-crate-flate2-1.1.9", + "kind": "native-library", + "name": "flate2 (Rust crate)", + "version": "1.1.9", + "source": "https://crates.io/crates/flate2/1.1.9", + "revision": "19ddb18bf11199858fbc6504d079448fafd1606e", + "downloads": [ + { + "url": "https://static.crates.io/crates/flate2/flate2-1.1.9.crate", + "sha256": "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c", + "size": 82745 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/flate2-rs/blob/19ddb18bf11199858fbc6504d079448fafd1606e/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/flate2-rs" + }, + { + "id": "rust-crate-generic-array-1.4.3", + "kind": "native-library", + "name": "generic-array (Rust crate)", + "version": "1.4.3", + "source": "https://crates.io/crates/generic-array/1.4.3", + "revision": "778e6dd954d2d06e2258c3bf3acea604130b0c99", + "downloads": [ + { + "url": "https://static.crates.io/crates/generic-array/generic-array-1.4.3.crate", + "sha256": "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351", + "size": 50172 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/fizyk20/generic-array/blob/778e6dd954d2d06e2258c3bf3acea604130b0c99/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/fizyk20/generic-array" + }, + { + "id": "rust-crate-getrandom-0.3.4", + "kind": "native-library", + "name": "getrandom (Rust crate)", + "version": "0.3.4", + "source": "https://crates.io/crates/getrandom/0.3.4", + "revision": "38e4ad38309a85b56eef4fc759535ccfc322ba9a", + "downloads": [ + { + "url": "https://static.crates.io/crates/getrandom/getrandom-0.3.4.crate", + "sha256": "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd", + "size": 50932 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-random/getrandom/blob/38e4ad38309a85b56eef4fc759535ccfc322ba9a/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-random/getrandom" + }, + { + "id": "rust-crate-itertools-0.14.0", + "kind": "native-library", + "name": "itertools (Rust crate)", + "version": "0.14.0", + "source": "https://crates.io/crates/itertools/0.14.0", + "revision": "a015a6831525ee1637df747d3f530a627d9741bf", + "downloads": [ + { + "url": "https://static.crates.io/crates/itertools/itertools-0.14.0.crate", + "sha256": "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285", + "size": 152715 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-itertools/itertools/blob/a015a6831525ee1637df747d3f530a627d9741bf/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-itertools/itertools" + }, + { + "id": "rust-crate-lazy-static-1.5.0", + "kind": "native-library", + "name": "lazy_static (Rust crate)", + "version": "1.5.0", + "source": "https://crates.io/crates/lazy_static/1.5.0", + "revision": "be7c1c43f264699f956b70ce8e29941bd1e61bde", + "downloads": [ + { + "url": "https://static.crates.io/crates/lazy_static/lazy_static-1.5.0.crate", + "sha256": "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe", + "size": 14025 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang-nursery/lazy-static.rs/blob/be7c1c43f264699f956b70ce8e29941bd1e61bde/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang-nursery/lazy-static.rs" + }, + { + "id": "rust-crate-libc-0.2.186", + "kind": "native-library", + "name": "libc (Rust crate)", + "version": "0.2.186", + "source": "https://crates.io/crates/libc/0.2.186", + "revision": "42620ffc4109dc32e02f1cae9e63a3f4311b4b71", + "downloads": [ + { + "url": "https://static.crates.io/crates/libc/libc-0.2.186.crate", + "sha256": "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66", + "size": 821883 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/libc/blob/42620ffc4109dc32e02f1cae9e63a3f4311b4b71/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/libc" + }, + { + "id": "rust-crate-memchr-2.8.3", + "kind": "native-library", + "name": "memchr (Rust crate)", + "version": "2.8.3", + "source": "https://crates.io/crates/memchr/2.8.3", + "revision": "5fdb40c054e1fff359a2f7bdf7f87a13b34b465d", + "downloads": [ + { + "url": "https://static.crates.io/crates/memchr/memchr-2.8.3.crate", + "sha256": "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98", + "size": 99165 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "Unlicense OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/BurntSushi/memchr/blob/5fdb40c054e1fff359a2f7bdf7f87a13b34b465d/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/BurntSushi/memchr" + }, + { + "id": "rust-crate-minimal-lexical-0.2.1", + "kind": "native-library", + "name": "minimal-lexical (Rust crate)", + "version": "0.2.1", + "source": "https://crates.io/crates/minimal-lexical/0.2.1", + "revision": "e997c46656ebe83e696b866bd954da1fa3f64eef", + "downloads": [ + { + "url": "https://static.crates.io/crates/minimal-lexical/minimal-lexical-0.2.1.crate", + "sha256": "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a", + "size": 94841 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/Alexhuszagh/minimal-lexical/blob/e997c46656ebe83e696b866bd954da1fa3f64eef/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/Alexhuszagh/minimal-lexical" + }, + { + "id": "rust-crate-miniz-oxide-0.8.9", + "kind": "native-library", + "name": "miniz_oxide (Rust crate)", + "version": "0.8.9", + "source": "https://crates.io/crates/miniz_oxide/0.8.9", + "revision": "44e43c7786e379b2b1a7fde4aa0e63be719e583d", + "downloads": [ + { + "url": "https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.8.9.crate", + "sha256": "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316", + "size": 67132 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing", + "rust-std" + ], + "license": { + "code": "MIT OR Zlib OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/Frommi/miniz_oxide/blob/44e43c7786e379b2b1a7fde4aa0e63be719e583d/miniz_oxide/LICENSE-MIT.md" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/Frommi/miniz_oxide" + }, + { + "id": "rust-crate-nom-7.1.3", + "kind": "native-library", + "name": "nom (Rust crate)", + "version": "7.1.3", + "source": "https://crates.io/crates/nom/7.1.3", + "revision": "869f8972a4383b13cf89574fda28cb7dbfd56517", + "downloads": [ + { + "url": "https://static.crates.io/crates/nom/nom-7.1.3.crate", + "sha256": "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a", + "size": 117570 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/Geal/nom/blob/869f8972a4383b13cf89574fda28cb7dbfd56517/LICENSE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/Geal/nom" + }, + { + "id": "rust-crate-num-traits-0.2.19", + "kind": "native-library", + "name": "num-traits (Rust crate)", + "version": "0.2.19", + "source": "https://crates.io/crates/num-traits/0.2.19", + "revision": "7ec3d41d39b28190ec1d42db38021107b3951f3a", + "downloads": [ + { + "url": "https://static.crates.io/crates/num-traits/num-traits-0.2.19.crate", + "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841", + "size": 51631 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-num/num-traits/blob/7ec3d41d39b28190ec1d42db38021107b3951f3a/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-num/num-traits" + }, + { + "id": "rust-crate-ordered-float-5.3.0", + "kind": "native-library", + "name": "ordered-float (Rust crate)", + "version": "5.3.0", + "source": "https://crates.io/crates/ordered-float/5.3.0", + "revision": "6cca9b87766aa086e4dde8e646f66aa975bdf757", + "downloads": [ + { + "url": "https://static.crates.io/crates/ordered-float/ordered-float-5.3.0.crate", + "sha256": "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e", + "size": 34539 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/reem/rust-ordered-float/blob/6cca9b87766aa086e4dde8e646f66aa975bdf757/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/reem/rust-ordered-float" + }, + { + "id": "rust-crate-ppv-lite86-0.2.21", + "kind": "native-library", + "name": "ppv-lite86 (Rust crate)", + "version": "0.2.21", + "source": "https://crates.io/crates/ppv-lite86/0.2.21", + "revision": "000a6cd6bbcb0b091381dc5f8fe6d6efa480b818", + "downloads": [ + { + "url": "https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.21.crate", + "sha256": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9", + "size": 22522 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/cryptocorrosion/cryptocorrosion/blob/000a6cd6bbcb0b091381dc5f8fe6d6efa480b818/utils-simd/ppv-lite86/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/cryptocorrosion/cryptocorrosion" + }, + { + "id": "rust-crate-rand-0.9.4", + "kind": "native-library", + "name": "rand (Rust crate)", + "version": "0.9.4", + "source": "https://crates.io/crates/rand/0.9.4", + "revision": "ba4c4c62d5a3dc73a5c975a53d9a139372c011cc", + "downloads": [ + { + "url": "https://static.crates.io/crates/rand/rand-0.9.4.crate", + "sha256": "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea", + "size": 99786 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-random/rand/blob/ba4c4c62d5a3dc73a5c975a53d9a139372c011cc/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-random/rand" + }, + { + "id": "rust-crate-rand-chacha-0.9.0", + "kind": "native-library", + "name": "rand_chacha (Rust crate)", + "version": "0.9.0", + "source": "https://crates.io/crates/rand_chacha/0.9.0", + "revision": "96f8df65ee6b4368d91a006f9c5b4a8050abae49", + "downloads": [ + { + "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.9.0.crate", + "sha256": "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb", + "size": 18258 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-random/rand/blob/96f8df65ee6b4368d91a006f9c5b4a8050abae49/rand_chacha/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-random/rand" + }, + { + "id": "rust-crate-rand-core-0.9.5", + "kind": "native-library", + "name": "rand_core (Rust crate)", + "version": "0.9.5", + "source": "https://crates.io/crates/rand_core/0.9.5", + "revision": "a34dab266970cd0986ecbefe6b5b7afa619234f4", + "downloads": [ + { + "url": "https://static.crates.io/crates/rand_core/rand_core-0.9.5.crate", + "sha256": "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c", + "size": 24129 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-random/rand_core/blob/a34dab266970cd0986ecbefe6b5b7afa619234f4/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json. Its Cargo.toml names rust-random/rand, but the recorded commit exists only in rust-random/rand_core, which is cited as evidence.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-random/rand_core" + }, + { + "id": "rust-crate-rustfst-1.3.1", + "kind": "native-library", + "name": "rustfst (Rust crate)", + "version": "1.3.1", + "source": "https://crates.io/crates/rustfst/1.3.1", + "revision": "8e1391df1ef3dfb85e309dd4ee8af45251d28c9f", + "downloads": [ + { + "url": "https://static.crates.io/crates/rustfst/rustfst-1.3.1.crate", + "sha256": "3096239ad3664245480c50bf34a6ed45af5f40c0f6258d4b28a4c8082813a519", + "size": 213573 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/Garvys/rustfst/blob/8e1391df1ef3dfb85e309dd4ee8af45251d28c9f/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/Garvys/rustfst" + }, + { + "id": "rust-crate-serde-1.0.228", + "kind": "native-library", + "name": "serde (Rust crate)", + "version": "1.0.228", + "source": "https://crates.io/crates/serde/1.0.228", + "revision": "a866b336f14aa57a07f0d0be9f8762746e64ecb4", + "downloads": [ + { + "url": "https://static.crates.io/crates/serde/serde-1.0.228.crate", + "sha256": "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e", + "size": 83652 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/serde-rs/serde/blob/a866b336f14aa57a07f0d0be9f8762746e64ecb4/serde/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/serde-rs/serde" + }, + { + "id": "rust-crate-serde-core-1.0.228", + "kind": "native-library", + "name": "serde_core (Rust crate)", + "version": "1.0.228", + "source": "https://crates.io/crates/serde_core/1.0.228", + "revision": "a866b336f14aa57a07f0d0be9f8762746e64ecb4", + "downloads": [ + { + "url": "https://static.crates.io/crates/serde_core/serde_core-1.0.228.crate", + "sha256": "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad", + "size": 63111 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/serde-rs/serde/blob/a866b336f14aa57a07f0d0be9f8762746e64ecb4/serde_core/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/serde-rs/serde" + }, + { + "id": "rust-crate-simd-adler32-0.3.9", + "kind": "native-library", + "name": "simd-adler32 (Rust crate)", + "version": "0.3.9", + "source": "https://crates.io/crates/simd-adler32/0.3.9", + "revision": "0b94f6a278f5662f24d311c9c1f16bcf272b7f43", + "downloads": [ + { + "url": "https://static.crates.io/crates/simd-adler32/simd-adler32-0.3.9.crate", + "sha256": "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214", + "size": 18572 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT", + "content": null, + "name": null, + "evidence": "https://github.com/mcountryman/simd-adler32/blob/0b94f6a278f5662f24d311c9c1f16bcf272b7f43/LICENSE.md" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/mcountryman/simd-adler32" + }, + { + "id": "rust-crate-superslice-1.0.0", + "kind": "native-library", + "name": "superslice (Rust crate)", + "version": "1.0.0", + "source": "https://crates.io/crates/superslice/1.0.0", + "revision": "3dac9f39997028ef43e58facba4dccd82332303c", + "downloads": [ + { + "url": "https://static.crates.io/crates/superslice/superslice-1.0.0.crate", + "sha256": "ab16ced94dbd8a46c82fd81e3ed9a8727dac2977ea869d217bcc4ea1f122e81f", + "size": 9168 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/alkis/superslice-rs/blob/3dac9f39997028ef43e58facba4dccd82332303c/LICENCE" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text and any NOTICE file.", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; its registry path appears in the release binary's panic locations. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/alkis/superslice-rs" + }, + { + "id": "rust-crate-typenum-1.20.1", + "kind": "native-library", + "name": "typenum (Rust crate)", + "version": "1.20.1", + "source": "https://crates.io/crates/typenum/1.20.1", + "revision": "0db9a0f731981f29266b63586c29fa07e4477b1a", + "downloads": [ + { + "url": "https://static.crates.io/crates/typenum/typenum-1.20.1.crate", + "sha256": "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20", + "size": 105479 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/paholg/typenum/blob/0db9a0f731981f29266b63586c29fa07e4477b1a/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/paholg/typenum" + }, + { + "id": "rust-crate-zerocopy-0.8.53", + "kind": "native-library", + "name": "zerocopy (Rust crate)", + "version": "0.8.53", + "source": "https://crates.io/crates/zerocopy/0.8.53", + "revision": "c8fb1ca16968099ef7c032db4e65c0a45830d188", + "downloads": [ + { + "url": "https://static.crates.io/crates/zerocopy/zerocopy-0.8.53.crate", + "sha256": "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1", + "size": 284705 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "nemo-text-processing" + ], + "license": { + "code": "BSD-2-Clause OR Apache-2.0 OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/google/zerocopy/blob/c8fb1ca16968099ef7c032db4e65c0a45830d188/zerocopy/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Statically linked into the NemoTextProcessing v0.3.1 xcframework: in the `cargo tree --locked -e normal,no-proc-macro --features ffi,fst-engine` closure over the seven Apple targets of build-xcframework.sh at text-processing-rs 46ebddc; no panic location for it survives in the release binary, as expected for panic-free or fully inlined code. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/google/zerocopy" + }, + { + "id": "rust-crate-addr2line-0.25.1", + "kind": "native-library", + "name": "addr2line (Rust crate)", + "version": "0.25.1", + "source": "https://crates.io/crates/addr2line/0.25.1", + "revision": "f02db009deb9b441818afa49cb1b17453c1e4243", + "downloads": [ + { + "url": "https://static.crates.io/crates/addr2line/addr2line-0.25.1.crate", + "sha256": "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b", + "size": 43134 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "Apache-2.0 OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/gimli-rs/addr2line/blob/f02db009deb9b441818afa49cb1b17453c1e4243/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/gimli-rs/addr2line" + }, + { + "id": "rust-crate-gimli-0.32.3", + "kind": "native-library", + "name": "gimli (Rust crate)", + "version": "0.32.3", + "source": "https://crates.io/crates/gimli/0.32.3", + "revision": "8bc8e622fcb9be20fc9f03c96bc6335d936b869d", + "downloads": [ + { + "url": "https://static.crates.io/crates/gimli/gimli-0.32.3.crate", + "sha256": "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7", + "size": 289789 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/gimli-rs/gimli/blob/8bc8e622fcb9be20fc9f03c96bc6335d936b869d/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/gimli-rs/gimli" + }, + { + "id": "rust-crate-hashbrown-0.17.1", + "kind": "native-library", + "name": "hashbrown (Rust crate)", + "version": "0.17.1", + "source": "https://crates.io/crates/hashbrown/0.17.1", + "revision": "c62a63a61b7caf2de8f9ecb7b06a66b0ab6bdf3d", + "downloads": [ + { + "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.17.1.crate", + "sha256": "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a", + "size": 155512 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/hashbrown/blob/c62a63a61b7caf2de8f9ecb7b06a66b0ab6bdf3d/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/hashbrown" + }, + { + "id": "rust-crate-libc-0.2.185", + "kind": "native-library", + "name": "libc (Rust crate)", + "version": "0.2.185", + "source": "https://crates.io/crates/libc/0.2.185", + "revision": "71d5bfcc1bda05da1783666fc2cd7d9669c9c4c8", + "downloads": [ + { + "url": "https://static.crates.io/crates/libc/libc-0.2.185.crate", + "sha256": "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f", + "size": 821775 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/libc/blob/71d5bfcc1bda05da1783666fc2cd7d9669c9c4c8/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/libc" + }, + { + "id": "rust-crate-memchr-2.7.6", + "kind": "native-library", + "name": "memchr (Rust crate)", + "version": "2.7.6", + "source": "https://crates.io/crates/memchr/2.7.6", + "revision": "9ba486e4ba7e865c0510305c5dacba73988d9f31", + "downloads": [ + { + "url": "https://static.crates.io/crates/memchr/memchr-2.7.6.crate", + "sha256": "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273", + "size": 97616 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "Unlicense OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/BurntSushi/memchr/blob/9ba486e4ba7e865c0510305c5dacba73988d9f31/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/BurntSushi/memchr" + }, + { + "id": "rust-crate-object-0.37.3", + "kind": "native-library", + "name": "object (Rust crate)", + "version": "0.37.3", + "source": "https://crates.io/crates/object/0.37.3", + "revision": "916c47b90e5c0bea139ca4bdfc53811f7d2c3383", + "downloads": [ + { + "url": "https://static.crates.io/crates/object/object-0.37.3.crate", + "sha256": "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe", + "size": 344032 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "Apache-2.0 OR MIT", + "content": null, + "name": null, + "evidence": "https://github.com/gimli-rs/object/blob/916c47b90e5c0bea139ca4bdfc53811f7d2c3383/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/gimli-rs/object" + }, + { + "id": "rust-crate-rustc-demangle-0.1.27", + "kind": "native-library", + "name": "rustc-demangle (Rust crate)", + "version": "0.1.27", + "source": "https://crates.io/crates/rustc-demangle/0.1.27", + "revision": "f65a6d8f63a7656c71649d92ff3cb000951c11a6", + "downloads": [ + { + "url": "https://static.crates.io/crates/rustc-demangle/rustc-demangle-0.1.27.crate", + "sha256": "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d", + "size": 30448 + } + ], + "paths": [], + "configuration": null, + "requires": [], + "usedBy": [ + "#42", + "rust-std" + ], + "license": { + "code": "MIT OR Apache-2.0", + "content": null, + "name": null, + "evidence": "https://github.com/rust-lang/rustc-demangle/blob/f65a6d8f63a7656c71649d92ff3cb000951c11a6/LICENSE-MIT" + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Copyright notice and MIT license text (MIT elected from the offered licenses).", + "download": { + "access": "public", + "presentation": "none" + }, + "termsUrl": "https://opensource.org/license/mit" + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Linked through the Rust 1.98.1 standard library (library/Cargo.lock at rust-lang/rust 48a229c); its /rust/deps path appears in the release binary. The crate archive SHA-256 equals its Cargo.lock checksum; `revision` is the commit in its .cargo_vcs_info.json.", + "legal": { + "status": "pending", + "reference": null, + "date": null + } + }, + "vcs": "https://github.com/rust-lang/rustc-demangle" + }, + { + "id": "conformance-audio-fixtures", + "kind": "fixture", + "name": "Conformance audio frames", + "paths": [ + "conformance/fixtures/audio-frames.jsonl", + "conformance/fixtures/invalid/audio-frames.jsonl" + ], + "synthetic": true, + "method": "Generated PCM from the recipe in conformance/README.md (Synthetic audio): a 16 kHz mono s16le 10 ms 1 kHz sine and 320 zero bytes; the invalid set uses hand-authored malformed or all-zero base64 payloads.", + "review": { + "notes": "Murmur-authored under the repository's Apache-2.0 license; no recording or third-party audio." + } + }, + { + "id": "conformance-json-fixtures", + "kind": "fixture", + "name": "Conformance ProtoJSON fixtures", + "paths": [ + "conformance/fixtures/forward-compatible-events.jsonl", + "conformance/fixtures/invalid/ambiguous-oneof.jsonl", + "conformance/fixtures/invalid/ambiguous-session-command.jsonl", + "conformance/fixtures/invalid/enum.jsonl", + "conformance/fixtures/invalid/missing-payload.jsonl", + "conformance/fixtures/invalid/missing-session-command.jsonl", + "conformance/fixtures/invalid/protocol-major.jsonl", + "conformance/fixtures/invalid/protocol-version.jsonl", + "conformance/fixtures/invalid/sequence-order.jsonl", + "conformance/fixtures/invalid/uint64.jsonl", + "conformance/fixtures/runtime-events.jsonl", + "conformance/fixtures/session-control.jsonl", + "conformance/fixtures/source-discovery.jsonl" + ], + "synthetic": true, + "method": "Hand-authored synthetic ProtoJSON; transcript text is marked synthetic and identifiers are placeholders.", + "review": { + "notes": "Murmur-authored under the repository's Apache-2.0 license; contains no recordings, conversations, credentials, or real identifiers." + } + } + ] +} diff --git a/tool/check_licensing.py b/tool/check_licensing.py new file mode 100644 index 0000000..580fd08 --- /dev/null +++ b/tool/check_licensing.py @@ -0,0 +1,616 @@ +#!/usr/bin/env python3 +"""Validate the default-deny engine dependency and model licensing manifest.""" + +from __future__ import annotations + +import hashlib +import json +import re +import subprocess +import sys +from datetime import date +from pathlib import Path +from typing import Any +from urllib.parse import unquote, urlparse + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST_PATH = "licensing/manifest.json" +NOTICES_PATH = "THIRD_PARTY_NOTICES.md" +FIXTURE_ROOT = "conformance/fixtures/" + +CODE_KINDS = {"engine", "native-library"} +CONTENT_KINDS = {"model", "voice", "tokenizer", "phonemizer"} +KINDS = CODE_KINDS | CONTENT_KINDS | {"fixture"} +DISTRIBUTIONS = {"bundle", "user-download", "blocked"} +PERMISSIONS = ("redistribution", "commercialUse", "modification") +PERMISSION_VALUES = {"allowed", "prohibited", "unknown"} +ACCESS_VALUES = {"public", "gated", "unknown"} +PRESENTATION_VALUES = {"none", "link", "acknowledgement", "unknown"} +LEGAL_STATUSES = {"pending", "approved", "rejected"} + +ENTRY_FIELDS = { + "id", "kind", "name", "version", "source", "revision", "downloads", "paths", + "configuration", "requires", "usedBy", "license", "upstream", "terms", + "distribution", "review", "vcs", +} +FIXTURE_FIELDS = {"id", "kind", "name", "paths", "synthetic", "method", "review"} +LICENSE_FIELDS = {"code", "content", "name", "evidence"} +UPSTREAM_FIELDS = {"name", "source", "vcs", "revision", "license", "licenseName", "evidence"} +TERMS_FIELDS = {"redistribution", "commercialUse", "modification", "attribution", "download", "termsUrl"} +DOWNLOAD_FIELDS = {"url", "sha256", "size", "platform"} +REVIEW_FIELDS = {"date", "notes", "legal"} +LEGAL_FIELDS = {"status", "reference", "date", "fingerprint"} +# Every field except these is covered by the reviewed fingerprint an approval is bound to. +FINGERPRINT_EXCLUDED = {"usedBy", "review"} + +# Tracked files that look like models, native binaries, or audio must be registered. +SCANNED_SUFFIXES = ( + ".onnx", ".ort", ".mlmodel", ".safetensors", ".gguf", ".pt", ".pth", ".tflite", ".bin", + ".so", ".dylib", ".a", ".dll", + ".wav", ".flac", ".mp3", ".ogg", ".opus", ".m4a", +) +SCANNED_DIRECTORIES = (".mlpackage", ".mlmodelc", ".xcframework") +HUGGING_FACE_HOST = "huggingface.co" +REPOSITORY_HOSTS = {"github.com", "gitlab.com", HUGGING_FACE_HOST} +FILE_VIEWS = {"blob", "tree", "raw", "resolve"} +# Views that name a single file; `tree` names a directory and cannot be license evidence. +EVIDENCE_VIEWS = { + "github.com": {"blob", "raw"}, "gitlab.com": {"blob", "raw"}, HUGGING_FACE_HOST: {"blob", "resolve", "raw"}, +} +HUGGING_FACE_DOMAINS = ("huggingface.co", "hf.co") + +KEBAB_ID = re.compile(r"^[a-z0-9]+(?:[.-][a-z0-9]+)*$") +COMMIT = re.compile(r"^[0-9a-f]{40}$") +SHA256 = re.compile(r"^[0-9a-f]{64}$") +ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") + + +def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key {key!r}") + result[key] = value + return result + + +def is_text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def is_one_of(value: Any, choices: set[str]) -> bool: + return isinstance(value, str) and value in choices + + +def is_https(value: Any) -> bool: + if not isinstance(value, str): + return False + try: + parsed = urlparse(value) + except ValueError: + return False + return parsed.scheme == "https" and bool(parsed.netloc) + + +def parse_location(url: Any) -> tuple[str, str | None, str | None, list[str]] | None: + """Return (repository, view, commit, path) for a github.com, gitlab.com, or huggingface.co URL.""" + if not is_https(url): + return None + parsed = urlparse(url) + if parsed.netloc not in REPOSITORY_HOSTS: + return None + parts = [unquote(part) for part in parsed.path.split("/") if part] + if any(part in {".", ".."} for part in parts): + return None + if parsed.netloc == "github.com": + split = 2 + elif parsed.netloc == "gitlab.com": + split = parts.index("-") if "-" in parts else len(parts) + else: + split = next((index for index, part in enumerate(parts) if part in FILE_VIEWS), len(parts)) + repository, rest = parts[:split], parts[split:] + if parsed.netloc == "gitlab.com" and rest: + rest = rest[1:] + if len(repository) < 2 or (parsed.netloc == "github.com" and len(parts) < 2): + return None + view = rest[0] if rest else None + commit = rest[1] if len(rest) >= 2 and view in FILE_VIEWS and COMMIT.fullmatch(rest[1]) else None + if rest and commit is None: + return None + return f"{parsed.netloc}/{'/'.join(repository)}".casefold(), view, commit, rest[2:] + + +def repository_location(url: Any) -> tuple[str, str | None] | None: + """Return (repository, commit or None) for a repository, directory, or file URL.""" + location = parse_location(url) + return None if location is None else (location[0], location[2]) + + +def check_evidence(errors: list[str], owner: str, evidence: str, repository: Any, revision: Any) -> None: + """Evidence must be a file in the reviewed repository at the reviewed revision.""" + location = parse_location(evidence) + if ( + location is None or location[2] is None or not location[3] or urlparse(evidence).path.endswith("/") + or location[1] not in EVIDENCE_VIEWS[urlparse(evidence).netloc] + ): + errors.append(f"{owner}: must be a file on {', '.join(sorted(REPOSITORY_HOSTS))} " + "(a blob, raw, or resolve path) at a 40-hex commit") + return + location = (location[0], location[2]) + expected = repository_location(repository) + if expected is None: + errors.append(f"{owner}: the reviewed repository (vcs, or else source) must be on " + f"{', '.join(sorted(REPOSITORY_HOSTS))}") + elif location[0] != expected[0]: + errors.append(f"{owner}: must be in the reviewed repository {expected[0]}, not {location[0]}") + if not isinstance(revision, str) or location[1] != revision: + errors.append(f"{owner}: must be at the reviewed revision {revision}, not {location[1]}") + + +def fingerprint(entry: dict[str, Any]) -> str: + """SHA-256 of the canonical JSON of every reviewed field of an artifact entry.""" + material = {key: value for key, value in entry.items() if key not in FINGERPRINT_EXCLUDED} + canonical = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def is_date(value: Any) -> bool: + if not isinstance(value, str) or not ISO_DATE.fullmatch(value): + return False + try: + date.fromisoformat(value) + except ValueError: + return False + return True + + +def is_identified(license_id: Any) -> bool: + return is_text(license_id) and license_id.strip() != "NOASSERTION" + + +def is_custom(license_id: Any) -> bool: + if not isinstance(license_id, str): + return False + tokens = re.split(r"[\s()]+", license_id) + return any(token == "other" or token.startswith("LicenseRef-") for token in tokens) + + +def is_scanned(path: str) -> bool: + parts = path.casefold().split("/") + if any(part.endswith(SCANNED_DIRECTORIES) for part in parts[:-1]): + return True + return parts[-1].endswith(SCANNED_SUFFIXES + SCANNED_DIRECTORIES) + + +def notice_headings(notices_text: str) -> list[str]: + return [line[3:].strip() for line in notices_text.splitlines() if line.startswith("## ")] + + +def check_fields( + errors: list[str], owner: str, value: Any, expected: set[str], optional: frozenset[str] = frozenset(), +) -> bool: + if not isinstance(value, dict): + errors.append(f"{owner}: must be an object") + return False + missing = expected - optional - value.keys() + unknown = value.keys() - expected + if missing: + errors.append(f"{owner}: missing fields {sorted(missing)}") + if unknown: + errors.append(f"{owner}: unknown fields {sorted(unknown)}") + return not missing + + +def check_string_list(errors: list[str], owner: str, value: Any) -> list[str]: + if not isinstance(value, list) or not all(is_text(item) for item in value): + errors.append(f"{owner}: must be a list of non-empty strings") + return [] + if len(set(value)) != len(value): + errors.append(f"{owner}: contains duplicates") + return value + + +def is_hugging_face(url: str) -> bool: + host = (urlparse(url).hostname or "").casefold() + return any(host == domain or host.endswith(f".{domain}") for domain in HUGGING_FACE_DOMAINS) + + +def check_hugging_face_download(errors: list[str], owner: str, entry: dict[str, Any], url: str) -> None: + """Bind a Hugging Face download to the reviewed repository and revision of its own entry.""" + parsed, source, revision = urlparse(url), entry["source"], entry["revision"] + if parsed.netloc != HUGGING_FACE_HOST: + errors.append(f"{owner}: Hugging Face url must use the canonical host https://{HUGGING_FACE_HOST}") + return + if not is_https(source) or urlparse(source).netloc != HUGGING_FACE_HOST: + errors.append(f"{owner}: a Hugging Face download needs a https://{HUGGING_FACE_HOST} source repository") + return + if not isinstance(revision, str) or not COMMIT.fullmatch(revision): + errors.append(f"{owner}: a Hugging Face download needs the entry's 40-hex revision") + return + prefix = f"{urlparse(source).path.rstrip('/')}/resolve/{revision}/" + segments = unquote(parsed.path).split("/") + if not parsed.path.startswith(prefix) or any(segment in {".", ".."} for segment in segments): + errors.append(f"{owner}: Hugging Face url must start with https://{HUGGING_FACE_HOST}{prefix}") + + +def check_downloads(errors: list[str], entry: dict[str, Any]) -> None: + entry_id, downloads = entry["id"], entry["downloads"] + if not isinstance(downloads, list): + errors.append(f"{entry_id}: downloads: must be a list") + return + seen: set[tuple[str, str]] = set() + for index, item in enumerate(downloads): + owner = f"{entry_id}: downloads[{index}]" + if not check_fields(errors, owner, item, DOWNLOAD_FIELDS, optional=frozenset({"platform"})): + continue + url = item["url"] + if not is_https(url): + errors.append(f"{owner}: url must be https://") + elif is_hugging_face(url): + check_hugging_face_download(errors, owner, entry, url) + if not isinstance(item["sha256"], str) or not SHA256.fullmatch(item["sha256"]): + errors.append(f"{owner}: sha256 must be 64 lowercase hex characters of the file bytes") + size = item["size"] + if isinstance(size, bool) or not isinstance(size, int) or size <= 0: + errors.append(f"{owner}: size must be a positive integer") + platform = item.get("platform") + if "platform" in item and not is_text(platform): + errors.append(f"{owner}: platform must be a non-empty string when present") + key = (repr(url), repr(platform)) + if key in seen: + errors.append(f"{owner}: duplicate url and platform") + seen.add(key) + + +def check_license(errors: list[str], entry: dict[str, Any], blocked: bool) -> None: + entry_id, kind, license_record = entry["id"], entry["kind"], entry["license"] + if not check_fields(errors, f"{entry_id}: license", license_record, LICENSE_FIELDS): + return + for field in ("code", "content", "name"): + value = license_record[field] + if value is not None and not is_text(value): + errors.append(f"{entry_id}: license.{field}: must be a non-empty string or null") + evidence = license_record["evidence"] + if evidence is not None: + check_evidence(errors, f"{entry_id}: license.evidence", evidence, entry.get("vcs") or entry["source"], + entry["revision"]) + if blocked: + return + field = "code" if kind in CODE_KINDS else "content" + if not is_identified(license_record[field]): + errors.append(f"{entry_id}: license.{field}: must be identified for kind {kind} unless blocked") + if evidence is None: + errors.append(f"{entry_id}: license.evidence: missing evidence requires distribution blocked") + if any(is_custom(license_record[name]) for name in ("code", "content")) and not ( + is_text(license_record["name"]) and evidence is not None + ): + errors.append(f"{entry_id}: license.name: a custom license needs an exact name and evidence unless blocked") + + +def check_upstream(errors: list[str], entry_id: str, upstream: Any, blocked: bool) -> None: + if not isinstance(upstream, list): + errors.append(f"{entry_id}: upstream: must be a list") + return + for index, hop in enumerate(upstream): + owner = f"{entry_id}: upstream[{index}]" + optional = frozenset({"vcs", "revision", "licenseName"}) + if not check_fields(errors, owner, hop, UPSTREAM_FIELDS, optional=optional): + continue + if not is_text(hop["name"]): + errors.append(f"{owner}: name must be a non-empty string") + if not is_https(hop["source"]): + errors.append(f"{owner}: source must be https://") + if "vcs" in hop and repository_location(hop["vcs"]) is None: + errors.append(f"{owner}: vcs must be a github.com, gitlab.com, or huggingface.co repository") + revision = hop.get("revision") + if revision is not None and (not isinstance(revision, str) or not COMMIT.fullmatch(revision)): + errors.append(f"{owner}: revision must be a 40-hex commit") + license_name = hop.get("licenseName") + if "licenseName" in hop and not is_text(license_name): + errors.append(f"{owner}: licenseName must be a non-empty string when present") + evidence = hop["evidence"] + if evidence is not None: + check_evidence(errors, f"{owner}: evidence", evidence, hop.get("vcs") or hop["source"], revision) + if blocked: + continue + if not is_identified(hop["license"]) or evidence is None: + errors.append(f"{owner}: an upstream hop without an identified license and evidence requires blocked") + elif is_custom(hop["license"]) and not is_text(license_name): + errors.append(f"{owner}: a custom upstream license needs an exact licenseName unless blocked") + + +def check_terms(errors: list[str], entry: dict[str, Any], blocked: bool) -> None: + entry_id, terms = entry["id"], entry["terms"] + if not check_fields(errors, f"{entry_id}: terms", terms, TERMS_FIELDS): + return + for field in PERMISSIONS: + if not is_one_of(terms[field], PERMISSION_VALUES): + errors.append(f"{entry_id}: terms.{field}: must be one of {sorted(PERMISSION_VALUES)}") + elif terms[field] == "unknown" and not blocked: + errors.append(f"{entry_id}: terms.{field}: unknown requires distribution blocked") + if not isinstance(terms["attribution"], str): + errors.append(f"{entry_id}: terms.attribution: must be a string") + if terms["termsUrl"] is not None and not is_https(terms["termsUrl"]): + errors.append(f"{entry_id}: terms.termsUrl: must be https:// or null") + download = terms["download"] + if not check_fields(errors, f"{entry_id}: terms.download", download, {"access", "presentation"}): + return + access, presentation = download["access"], download["presentation"] + if not is_one_of(access, ACCESS_VALUES): + errors.append(f"{entry_id}: terms.download.access: must be one of {sorted(ACCESS_VALUES)}") + elif access == "unknown" and not blocked: + errors.append(f"{entry_id}: terms.download.access: unknown requires distribution blocked") + if not is_one_of(presentation, PRESENTATION_VALUES): + errors.append(f"{entry_id}: terms.download.presentation: must be one of {sorted(PRESENTATION_VALUES)}") + return + if presentation == "unknown" and not blocked: + errors.append(f"{entry_id}: terms.download.presentation: unknown requires distribution blocked") + if presentation == "none" and entry["distribution"] != "bundle": + errors.append(f"{entry_id}: terms.download.presentation: none is only valid for bundle") + if entry["distribution"] == "user-download" and presentation not in {"link", "acknowledgement"}: + errors.append(f"{entry_id}: terms.download.presentation: user-download requires link or acknowledgement") + + +def check_review(errors: list[str], entry: dict[str, Any], fixture: bool) -> None: + entry_id, review = entry["id"], entry["review"] + if fixture: + if check_fields(errors, f"{entry_id}: review", review, {"notes"}) and not is_text(review["notes"]): + errors.append(f"{entry_id}: review.notes: must be a non-empty string") + return + if not check_fields(errors, f"{entry_id}: review", review, REVIEW_FIELDS): + return + if not is_date(review["date"]): + errors.append(f"{entry_id}: review.date: must be YYYY-MM-DD") + if not is_text(review["notes"]): + errors.append(f"{entry_id}: review.notes: must be a non-empty string") + legal = review["legal"] + if not check_fields(errors, f"{entry_id}: review.legal", legal, LEGAL_FIELDS, optional=frozenset({"fingerprint"})): + return + status = legal["status"] + if not is_one_of(status, LEGAL_STATUSES): + errors.append(f"{entry_id}: review.legal.status: must be one of {sorted(LEGAL_STATUSES)}") + return + if legal["reference"] is not None and not is_text(legal["reference"]): + errors.append(f"{entry_id}: review.legal.reference: must be a non-empty string or null") + if legal["date"] is not None and not is_date(legal["date"]): + errors.append(f"{entry_id}: review.legal.date: must be YYYY-MM-DD or null") + if status in {"approved", "rejected"} and not (is_text(legal["reference"]) and is_date(legal["date"])): + errors.append(f"{entry_id}: review.legal: {status} requires a reference and date") + if status == "rejected" and entry["distribution"] != "blocked": + errors.append(f"{entry_id}: review.legal.status: rejected requires distribution blocked") + if status == "approved" and entry["distribution"] == "blocked": + errors.append(f"{entry_id}: review.legal.status: a blocked entry cannot be approved") + recorded = legal.get("fingerprint") + if recorded is not None and (not isinstance(recorded, str) or not SHA256.fullmatch(recorded)): + errors.append(f"{entry_id}: review.legal.fingerprint: must be 64 lowercase hex characters") + elif status == "approved" and recorded is None: + errors.append(f"{entry_id}: review.legal.fingerprint: approved requires the reviewed fingerprint") + elif status == "approved" and recorded != fingerprint(entry): + errors.append(f"{entry_id}: review.legal.fingerprint: reviewed fields changed since approval; " + "a fresh legal review is required") + + +def check_artifact(errors: list[str], entry: dict[str, Any]) -> None: + entry_id, kind = entry["id"], entry["kind"] + for field in ("name", "version"): + if not is_text(entry[field]): + errors.append(f"{entry_id}: {field}: must be a non-empty string") + if not is_https(entry["source"]): + errors.append(f"{entry_id}: source: must be https://") + if "vcs" in entry and repository_location(entry["vcs"]) is None: + errors.append(f"{entry_id}: vcs: must be a github.com, gitlab.com, or huggingface.co repository") + source_location = repository_location(entry["source"]) + if source_location and source_location[1] is not None and source_location[1] != entry["revision"]: + errors.append(f"{entry_id}: source: a commit in source must equal the entry revision") + distribution = entry["distribution"] + if not is_one_of(distribution, DISTRIBUTIONS): + errors.append(f"{entry_id}: distribution: must be one of {sorted(DISTRIBUTIONS)}") + blocked = distribution == "blocked" + revision = entry["revision"] + if revision is not None and (not isinstance(revision, str) or not COMMIT.fullmatch(revision)): + errors.append(f"{entry_id}: revision: must be a 40-hex commit or null") + check_downloads(errors, entry) + if revision is None and not entry["downloads"]: + errors.append(f"{entry_id}: revision: needs a commit revision, downloads, or both") + configuration = entry["configuration"] + if kind == "engine" and not is_text(configuration): + errors.append(f"{entry_id}: configuration: an engine needs a named build configuration") + if kind != "engine" and configuration is not None: + errors.append(f"{entry_id}: configuration: only engines have a configuration") + check_string_list(errors, f"{entry_id}: usedBy", entry["usedBy"]) + check_license(errors, entry, blocked) + check_upstream(errors, entry_id, entry["upstream"], blocked) + check_terms(errors, entry, blocked) + check_review(errors, entry, fixture=False) + + +def check_fixture(errors: list[str], entry: dict[str, Any]) -> None: + entry_id = entry["id"] + if not is_text(entry["name"]): + errors.append(f"{entry_id}: name: must be a non-empty string") + if entry["synthetic"] is not True: + errors.append(f"{entry_id}: synthetic: fixtures must be synthetic") + if not is_text(entry["method"]): + errors.append(f"{entry_id}: method: must describe how the fixture was produced") + if not entry["paths"]: + errors.append(f"{entry_id}: paths: a fixture entry must list its files") + check_review(errors, entry, fixture=True) + + +def find_cycles(errors: list[str], graph: dict[str, list[str]]) -> None: + state: dict[str, str] = {} + + def visit(node: str, trail: list[str]) -> None: + state[node] = "visiting" + for child in graph[node]: + if child not in graph: + continue + if state.get(child) == "visiting": + cycle = trail[trail.index(child):] + [child] + errors.append(f"{node}: requires: cycle {' -> '.join(cycle)}") + elif child not in state: + visit(child, trail + [child]) + state[node] = "done" + + for node in graph: + if node not in state: + visit(node, [node]) + + +def is_approved(entry: dict[str, Any]) -> bool: + review = entry.get("review") + if not isinstance(review, dict) or not isinstance(review.get("legal"), dict): + return False + return review["legal"].get("status") == "approved" + + +def check_approval( + errors: list[str], entry: dict[str, Any], requires: list[str], by_id: dict[str, dict[str, Any]], + headings: list[str], +) -> None: + # Unknown answers are already rejected for every entry that is not blocked, + # and an approved entry is never blocked. + entry_id, terms = entry["id"], entry["terms"] + if terms.get("commercialUse") != "allowed": + errors.append(f"{entry_id}: approval: requires terms.commercialUse allowed") + if not is_text(terms.get("attribution")): + errors.append(f"{entry_id}: approval: terms.attribution must be recorded") + for required in requires: + if required in by_id and not is_approved(by_id[required]): + errors.append(f"{entry_id}: approval: requires unapproved {required}") + if entry["distribution"] == "bundle": + if terms.get("redistribution") != "allowed": + errors.append(f"{entry_id}: approval: bundle requires terms.redistribution allowed") + if not is_text(entry["name"]) or not any(entry["name"] in heading for heading in headings): + errors.append(f"{entry_id}: approval: bundle requires a {NOTICES_PATH} '## ' heading containing its name") + if entry["kind"] in CONTENT_KINDS and not entry["downloads"]: + errors.append(f"{entry_id}: approval: a bundled {entry['kind']} requires downloads with hashes and sizes") + if entry["distribution"] == "user-download": + if not is_https(terms.get("termsUrl")): + errors.append(f"{entry_id}: approval: user-download requires terms.termsUrl") + if not entry["downloads"]: + errors.append(f"{entry_id}: approval: user-download requires downloads with hashes and sizes") + + +def validate(manifest: Any, notices_text: str, tracked_files: list[str]) -> list[str]: + errors: list[str] = [] + if not check_fields(errors, "manifest", manifest, {"manifestVersion", "artifacts"}): + return errors + if manifest["manifestVersion"] != 1: + errors.append("manifest: manifestVersion: unsupported version") + artifacts = manifest["artifacts"] + if not isinstance(artifacts, list) or not artifacts: + errors.append("manifest: artifacts: must be a non-empty list") + return errors + + tracked = set(tracked_files) + by_id: dict[str, dict[str, Any]] = {} + components: list[dict[str, Any]] = [] + path_owner: dict[str, str] = {} + for index, entry in enumerate(artifacts): + if not isinstance(entry, dict): + errors.append(f"artifacts[{index}]: must be an object") + continue + entry_id = entry.get("id") + if not isinstance(entry_id, str) or not KEBAB_ID.fullmatch(entry_id): + errors.append(f"artifacts[{index}]: id: must be lowercase kebab-case (dots allowed between alphanumerics)") + continue + if entry_id in by_id: + errors.append(f"{entry_id}: id: duplicate id") + continue + by_id[entry_id] = entry + kind = entry.get("kind") + if not is_one_of(kind, KINDS): + errors.append(f"{entry_id}: kind: must be one of {sorted(KINDS)}") + continue + fixture = kind == "fixture" + if fixture and not check_fields(errors, entry_id, entry, FIXTURE_FIELDS): + continue + if not fixture and not check_fields(errors, entry_id, entry, ENTRY_FIELDS, optional=frozenset({"vcs"})): + continue + for path in check_string_list(errors, f"{entry_id}: paths", entry["paths"]): + if path in path_owner and path_owner[path] != entry_id: + errors.append(f"{entry_id}: paths: {path} is already listed by {path_owner[path]}") + path_owner.setdefault(path, entry_id) + if path not in tracked: + errors.append(f"{entry_id}: paths: {path} is not a tracked file") + if fixture: + check_fixture(errors, entry) + continue + check_string_list(errors, f"{entry_id}: requires", entry["requires"]) + check_artifact(errors, entry) + components.append(entry) + + graph: dict[str, list[str]] = {} + for entry in components: + requires = entry["requires"] if isinstance(entry["requires"], list) else [] + graph[entry["id"]] = [item for item in requires if isinstance(item, str)] + for required in graph[entry["id"]]: + target = by_id.get(required) + if target is None: + errors.append(f"{entry['id']}: requires: unknown id {required}") + elif is_one_of(target.get("kind"), {"engine", "fixture"}): + errors.append(f"{entry['id']}: requires: must not require {target.get('kind')} {required}") + find_cycles(errors, graph) + + headings = notice_headings(notices_text) + for entry in components: + if is_approved(entry) and isinstance(entry["terms"], dict): + check_approval(errors, entry, graph[entry["id"]], by_id, headings) + + fixture_files = {path for path in tracked if path.startswith(FIXTURE_ROOT)} + fixture_paths = { + path for entry in by_id.values() if entry.get("kind") == "fixture" + for path in (entry.get("paths") if isinstance(entry.get("paths"), list) else []) + if isinstance(path, str) + } + for path in sorted(fixture_files - fixture_paths): + errors.append(f"tracked: {path}: fixture file has no provenance entry") + for path in sorted(fixture_paths - fixture_files): + errors.append(f"tracked: {path}: fixture path is outside {FIXTURE_ROOT} or not tracked") + for path in sorted(tracked): + if is_scanned(path) and path not in path_owner: + errors.append(f"tracked: {path}: model, native binary, or audio file is not registered in {MANIFEST_PATH}") + return errors + + +def tracked_files() -> list[str]: + output = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, check=True, capture_output=True, + ).stdout.decode("utf-8") + return [path for path in output.split("\0") if path] + + +def main(argv: list[str]) -> None: + manifest = json.loads( + (ROOT / MANIFEST_PATH).read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys, + ) + if argv[:1] == ["--fingerprint"]: + entries = {entry.get("id"): entry for entry in manifest["artifacts"] if entry.get("kind") != "fixture"} + if len(argv) < 2 or any(entry_id not in entries for entry_id in argv[1:]): + sys.exit("usage: check_licensing.py --fingerprint ...") + for entry_id in argv[1:]: + print(f"{entry_id} {fingerprint(entries[entry_id])}") + return + notices = (ROOT / NOTICES_PATH).read_text(encoding="utf-8") + errors = validate(manifest, notices, tracked_files()) + if errors: + for error in errors: + print(error, file=sys.stderr) + sys.exit(1) + artifacts = manifest["artifacts"] + counts = { + distribution: sum(1 for entry in artifacts if entry.get("distribution") == distribution) + for distribution in ("bundle", "user-download", "blocked") + } + approved = sum(1 for entry in artifacts if is_approved(entry)) + fixtures = sum(1 for entry in artifacts if entry["kind"] == "fixture") + print( + f"Licensing manifest is consistent ({len(artifacts)} entries: {counts['bundle']} bundle, " + f"{counts['user-download']} user-download, {counts['blocked']} blocked, {fixtures} fixture; " + f"{approved} legally approved)." + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tool/test_check_licensing.py b/tool/test_check_licensing.py new file mode 100644 index 0000000..e6336c3 --- /dev/null +++ b/tool/test_check_licensing.py @@ -0,0 +1,758 @@ +"""Tests for tool/check_licensing.py.""" + +from __future__ import annotations + +import copy +import json +import unittest +from typing import Any + +import check_licensing +from check_licensing import validate + +COMMIT = "0123456789abcdef0123456789abcdef01234567" +OTHER_COMMIT = "89abcdef0123456789abcdef0123456789abcdef" +SHA = "ab" * 32 +FIXTURE = "conformance/fixtures/runtime-events.jsonl" +NOTICES = "# Third-party notices\n\n## Example Engine\n\nApache-2.0 text.\n" +BASE_EVIDENCE = f"https://github.com/example/base/blob/{OTHER_COMMIT}/LICENSE" + + +def base_hop(**changes: Any) -> dict[str, Any]: + """An identified upstream hop whose evidence is bound to its own repository and revision.""" + hop = {"name": "Base", "source": "https://github.com/example/base", "revision": OTHER_COMMIT, "license": "MIT", + "evidence": BASE_EVIDENCE} + hop.update(changes) + return hop + + +def approve(entry: dict[str, Any]) -> dict[str, Any]: + """Record an approval bound to the entry's current reviewed fields.""" + entry["review"]["legal"] = { + "status": "approved", "reference": "LR-1", "date": "2026-09-28", + "fingerprint": check_licensing.fingerprint(entry), + } + return entry + + +def base_entry() -> dict[str, Any]: + """An approved bundled engine, pinned only by revision, with a notice.""" + return approve({ + "id": "example-engine", + "kind": "engine", + "name": "Example Engine", + "version": "v1.0.0", + "source": "https://github.com/example/engine", + "revision": COMMIT, + "downloads": [], + "paths": [], + "configuration": "v1.0.0 default build, macOS arm64", + "requires": [], + "usedBy": ["#42"], + "license": { + "code": "Apache-2.0", + "content": None, + "name": None, + "evidence": f"https://github.com/example/engine/blob/{COMMIT}/LICENSE", + }, + "upstream": [], + "terms": { + "redistribution": "allowed", + "commercialUse": "allowed", + "modification": "allowed", + "attribution": "Apache-2.0 license text", + "download": {"access": "public", "presentation": "none"}, + "termsUrl": "https://www.apache.org/licenses/LICENSE-2.0", + }, + "distribution": "bundle", + "review": { + "date": "2026-09-28", + "notes": "Synthetic test entry.", + "legal": {"status": "pending", "reference": None, "date": None}, + }, + }) + + +def model_entry() -> dict[str, Any]: + """An approved user-downloaded model with per-platform downloads.""" + entry = base_entry() + entry.update({ + "id": "example-model", + "kind": "model", + "name": "Example Model", + "source": "https://huggingface.co/example/model", + "configuration": None, + "downloads": [ + {"url": f"https://huggingface.co/example/model/resolve/{COMMIT}/a.bin", "sha256": SHA, "size": 10, + "platform": "apple"}, + {"url": f"https://huggingface.co/example/model/resolve/{COMMIT}/b.bin", "sha256": SHA, "size": 20, + "platform": "linux"}, + ], + "distribution": "user-download", + }) + entry["license"] = { + "code": None, + "content": "CC-BY-4.0", + "name": None, + "evidence": f"https://huggingface.co/example/model/blob/{COMMIT}/README.md", + } + entry["terms"]["download"] = {"access": "public", "presentation": "link"} + return approve(entry) + + +def fixture_entry() -> dict[str, Any]: + return { + "id": "example-fixtures", + "kind": "fixture", + "name": "Example fixtures", + "paths": [FIXTURE], + "synthetic": True, + "method": "hand-authored synthetic ProtoJSON", + "review": {"notes": "Murmur-authored."}, + } + + +def manifest(*entries: dict[str, Any]) -> dict[str, Any]: + return {"manifestVersion": 1, "artifacts": [*entries, fixture_entry()]} + + +def run(value: dict[str, Any], notices: str = NOTICES, tracked: list[str] | None = None) -> list[str]: + return validate(value, notices, [FIXTURE] if tracked is None else tracked) + + +class RepositoryManifestTest(unittest.TestCase): + def test_repository_manifest_passes(self) -> None: + root = check_licensing.ROOT + value = json.loads( + (root / check_licensing.MANIFEST_PATH).read_text(encoding="utf-8"), + object_pairs_hook=check_licensing.reject_duplicate_keys, + ) + notices = (root / check_licensing.NOTICES_PATH).read_text(encoding="utf-8") + self.assertEqual(validate(value, notices, check_licensing.tracked_files()), []) + + def test_sherpa_configuration_pins_runtime_and_onnxruntime(self) -> None: + value = json.loads((check_licensing.ROOT / check_licensing.MANIFEST_PATH).read_text(encoding="utf-8")) + sherpa = next(entry for entry in value["artifacts"] if entry["id"] == "sherpa-onnx") + for option in ("SHERPA_ONNX_USE_PRE_INSTALLED_ONNXRUNTIME_IF_AVAILABLE=OFF", + "SHERPA_ONNX_LINK_LIBSTDCPP_STATICALLY=OFF", "SHERPA_ONNX_USE_STATIC_CRT=OFF"): + self.assertIn(option, sherpa["configuration"]) + onnxruntime = next(entry for entry in value["artifacts"] if entry["id"] == "onnxruntime") + windows = [item["url"] for item in onnxruntime["downloads"] if item.get("platform") == "windows-x64"] + self.assertEqual(len(windows), 1) + self.assertIn("-MD-Release-", windows[0]) + + def test_duplicate_json_keys_are_rejected(self) -> None: + with self.assertRaises(ValueError): + json.loads('{"a": 1, "a": 2}', object_pairs_hook=check_licensing.reject_duplicate_keys) + + +class ValidCasesTest(unittest.TestCase): + def assert_valid(self, value: dict[str, Any], **kwargs: Any) -> None: + self.assertEqual(run(value, **kwargs), []) + + def test_approved_bundle_engine_pinned_by_revision(self) -> None: + self.assert_valid(manifest(base_entry())) + + def test_approved_custom_license_with_name_and_evidence(self) -> None: + entry = model_entry() + entry["license"].update({"content": "LicenseRef-Example", "name": "Example Model License v1"}) + self.assert_valid(manifest(approve(entry))) + + def test_approved_user_download_with_platform_downloads(self) -> None: + self.assert_valid(manifest(model_entry())) + + def test_rejected_and_blocked_with_reference(self) -> None: + entry = base_entry() + entry["distribution"] = "blocked" + entry["terms"]["download"]["presentation"] = "unknown" + entry["review"]["legal"] = {"status": "rejected", "reference": "LR-2", "date": "2026-09-28"} + self.assert_valid(manifest(entry)) + + def test_blocked_entry_tolerates_ambiguity(self) -> None: + entry = model_entry() + entry["distribution"] = "blocked" + entry["license"].update({"content": "NOASSERTION", "evidence": None}) + entry["terms"]["redistribution"] = "unknown" + entry["upstream"] = [{"name": "Unknown", "source": "https://example.com", "license": "NOASSERTION", + "evidence": None}] + entry["review"]["legal"] = {"status": "pending", "reference": None, "date": None} + self.assert_valid(manifest(entry)) + + def test_approved_requirement_chain(self) -> None: + dependency = base_entry() + dependency.update({"id": "example-library", "kind": "native-library", "name": "Example Library", + "configuration": None}) + engine = base_entry() + engine["requires"] = ["example-library"] + self.assert_valid(manifest(approve(engine), approve(dependency)), notices=NOTICES + "\n## Example Library\n") + + def test_registered_tracked_model_passes(self) -> None: + entry = model_entry() + entry["paths"] = ["models/example.onnx"] + self.assert_valid(manifest(approve(entry)), tracked=[FIXTURE, "models/example.onnx"]) + + def test_approved_content_bundle_with_downloads(self) -> None: + entry = model_entry() + entry["distribution"] = "bundle" + entry["terms"]["download"]["presentation"] = "none" + self.assert_valid(manifest(approve(entry)), notices=NOTICES + "\n## Example Model\n") + + def test_raw_and_resolve_file_evidence(self) -> None: + engine = base_entry() + engine["license"]["evidence"] = f"https://github.com/example/engine/raw/{COMMIT}/LICENSE" + model = model_entry() + model["license"]["evidence"] = f"https://huggingface.co/example/model/resolve/{COMMIT}/LICENSE" + self.assert_valid(manifest(approve(engine), approve(model))) + + def test_package_source_with_vcs_repository(self) -> None: + entry = base_entry() + entry.update({"kind": "native-library", "configuration": None, + "source": "https://crates.io/crates/example/1.0.0", "vcs": "https://github.com/example/engine"}) + self.assert_valid(manifest(approve(entry))) + + def test_gitlab_and_source_tree_evidence(self) -> None: + entry = base_entry() + entry["source"] = f"https://gitlab.com/example/engine/-/tree/{COMMIT}/src" + entry["license"]["evidence"] = f"https://gitlab.com/example/engine/-/blob/{COMMIT}/COPYING" + self.assert_valid(manifest(approve(entry))) + + def test_approved_upstream_custom_license_with_name(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(source="https://huggingface.co/example/base", license="other", + licenseName="Example Base Model License v2", + evidence=f"https://huggingface.co/example/base/blob/{OTHER_COMMIT}/LICENSE")] + self.assert_valid(manifest(approve(entry))) + + def test_non_reviewed_fields_keep_approval(self) -> None: + entry = base_entry() + entry["usedBy"] = ["#42", "#26"] + entry["review"]["notes"] = "Edited notes." + self.assert_valid(manifest(entry)) + + def test_fingerprint_ignores_key_order(self) -> None: + entry = base_entry() + reordered = dict(reversed(list(entry.items()))) + self.assertEqual(check_licensing.fingerprint(entry), check_licensing.fingerprint(reordered)) + + +class InvalidCasesTest(unittest.TestCase): + def assert_error(self, value: dict[str, Any], fragment: str, **kwargs: Any) -> None: + errors = run(value, **kwargs) + self.assertTrue(any(fragment in error for error in errors), f"{fragment!r} not in {errors}") + + # Pins and shape + + def test_tag_revision(self) -> None: + entry = base_entry() + entry["revision"] = "v1.0.0" + self.assert_error(manifest(entry), "example-engine: revision: must be a 40-hex commit") + + def test_short_sha_revision(self) -> None: + entry = base_entry() + entry["revision"] = COMMIT[:7] + self.assert_error(manifest(entry), "example-engine: revision: must be a 40-hex commit") + + def test_no_revision_and_no_downloads(self) -> None: + entry = base_entry() + entry["revision"] = None + self.assert_error(manifest(entry), "needs a commit revision, downloads, or both") + + def test_download_without_sha256(self) -> None: + entry = model_entry() + del entry["downloads"][0]["sha256"] + self.assert_error(manifest(entry), "downloads[0]: missing fields ['sha256']") + + def test_download_without_size(self) -> None: + entry = model_entry() + del entry["downloads"][0]["size"] + self.assert_error(manifest(entry), "downloads[0]: missing fields ['size']") + + def test_download_with_non_positive_size(self) -> None: + entry = model_entry() + entry["downloads"][0]["size"] = 0 + self.assert_error(manifest(entry), "downloads[0]: size must be a positive integer") + + def assert_download_error(self, url: str, fragment: str) -> None: + entry = model_entry() + entry["downloads"][0]["url"] = url + self.assert_error(manifest(entry), f"example-model: downloads[0]: Hugging Face url {fragment}") + + def test_hugging_face_resolve_main(self) -> None: + self.assert_download_error("https://huggingface.co/example/model/resolve/main/a.bin", + f"must start with https://huggingface.co/example/model/resolve/{COMMIT}/") + + def test_hugging_face_other_revision(self) -> None: + self.assert_download_error(f"https://huggingface.co/example/model/resolve/{OTHER_COMMIT}/a.bin", + f"must start with https://huggingface.co/example/model/resolve/{COMMIT}/") + + def test_hugging_face_other_repository(self) -> None: + self.assert_download_error(f"https://huggingface.co/example/other/resolve/{COMMIT}/a.bin", + f"must start with https://huggingface.co/example/model/resolve/{COMMIT}/") + + def test_hugging_face_repository_prefix(self) -> None: + self.assert_download_error(f"https://huggingface.co/example/model-fork/resolve/{COMMIT}/a.bin", + f"must start with https://huggingface.co/example/model/resolve/{COMMIT}/") + + def test_hugging_face_path_traversal(self) -> None: + for url in ( + f"https://huggingface.co/example/model/resolve/{COMMIT}/../../other/resolve/main/a.bin", + f"https://huggingface.co/example/model/resolve/{COMMIT}/%2e%2e/a.bin", + ): + self.assert_download_error(url, "must start with https://huggingface.co/example/model/resolve/") + + def test_hugging_face_host_variants(self) -> None: + for host in ("huggingface.co:443", "www.huggingface.co", "hf.co", "HuggingFace.CO", "user@huggingface.co", + "cdn-lfs.hf.co"): + self.assert_download_error(f"https://{host}/example/model/resolve/{COMMIT}/a.bin", + "must use the canonical host https://huggingface.co") + + def test_hugging_face_download_without_revision(self) -> None: + entry = model_entry() + entry["revision"] = None + self.assert_error(manifest(entry), "downloads[0]: a Hugging Face download needs the entry's 40-hex revision") + + def test_hugging_face_download_from_non_hugging_face_source(self) -> None: + entry = model_entry() + entry["source"] = "https://github.com/example/model" + self.assert_error(manifest(entry), "downloads[0]: a Hugging Face download needs a https://huggingface.co") + + def test_duplicate_download_url_and_platform(self) -> None: + entry = model_entry() + entry["downloads"][1] = copy.deepcopy(entry["downloads"][0]) + self.assert_error(manifest(entry), "downloads[1]: duplicate url and platform") + + def test_http_source(self) -> None: + entry = base_entry() + entry["source"] = "http://github.com/example/engine" + self.assert_error(manifest(entry), "example-engine: source: must be https://") + + def test_malformed_url(self) -> None: + entry = base_entry() + entry["source"] = "https://[::1" + self.assert_error(manifest(entry), "example-engine: source: must be https://") + + def test_duplicate_id(self) -> None: + self.assert_error(manifest(base_entry(), base_entry()), "example-engine: id: duplicate id") + + def test_unknown_field(self) -> None: + entry = base_entry() + entry["reveiw"] = {} + self.assert_error(manifest(entry), "example-engine: unknown fields ['reveiw']") + + def test_dangling_requires(self) -> None: + entry = base_entry() + entry["requires"] = ["missing-library"] + self.assert_error(manifest(entry), "example-engine: requires: unknown id missing-library") + + def test_direct_cycle(self) -> None: + entry = model_entry() + entry["requires"] = ["example-model"] + self.assert_error(manifest(entry), "cycle example-model -> example-model") + + def test_indirect_cycle(self) -> None: + first = model_entry() + second = model_entry() + second.update({"id": "example-vocab", "kind": "tokenizer", "name": "Example Vocab"}) + first["requires"] = ["example-vocab"] + second["requires"] = ["example-model"] + self.assert_error(manifest(first, second), "cycle example-model -> example-vocab -> example-model") + + def test_model_requires_engine(self) -> None: + entry = model_entry() + entry["requires"] = ["example-engine"] + self.assert_error(manifest(base_entry(), entry), "must not require engine example-engine") + + def test_engine_without_configuration(self) -> None: + entry = base_entry() + entry["configuration"] = None + self.assert_error(manifest(entry), "an engine needs a named build configuration") + + def test_non_engine_with_configuration(self) -> None: + entry = model_entry() + entry["configuration"] = "default" + self.assert_error(manifest(entry), "only engines have a configuration") + + # Ambiguity that is not blocked + + def test_unknown_permission(self) -> None: + entry = base_entry() + entry["terms"]["modification"] = "unknown" + self.assert_error(manifest(entry), "terms.modification: unknown requires distribution blocked") + + def test_unknown_download_access(self) -> None: + entry = model_entry() + entry["terms"]["download"]["access"] = "unknown" + self.assert_error(manifest(entry), "terms.download.access: unknown requires distribution blocked") + + def test_unknown_download_presentation(self) -> None: + entry = model_entry() + entry["terms"]["download"]["presentation"] = "unknown" + self.assert_error(manifest(entry), "terms.download.presentation: unknown requires distribution blocked") + + def test_native_library_without_code_license(self) -> None: + entry = base_entry() + entry.update({"kind": "native-library", "configuration": None}) + entry["license"]["code"] = None + self.assert_error(manifest(entry), "license.code: must be identified for kind native-library") + + def test_tokenizer_with_only_code_license(self) -> None: + entry = model_entry() + entry["kind"] = "tokenizer" + entry["license"].update({"code": "Apache-2.0", "content": None}) + self.assert_error(manifest(entry), "license.content: must be identified for kind tokenizer") + + def test_model_without_content_license(self) -> None: + entry = model_entry() + entry["license"]["content"] = "NOASSERTION" + self.assert_error(manifest(entry), "license.content: must be identified for kind model") + + def test_other_license_without_name(self) -> None: + entry = model_entry() + entry["license"]["content"] = "other" + self.assert_error(manifest(entry), "a custom license needs an exact name and evidence") + + def test_other_license_without_evidence(self) -> None: + entry = model_entry() + entry["license"].update({"content": "other", "name": "Example License", "evidence": None}) + self.assert_error(manifest(entry), "a custom license needs an exact name and evidence") + + def test_missing_evidence(self) -> None: + entry = base_entry() + entry["license"]["evidence"] = None + self.assert_error(manifest(entry), "missing evidence requires distribution blocked") + + def test_weak_upstream_hop(self) -> None: + entry = model_entry() + entry["upstream"] = [{"name": "Base", "source": "https://example.com", "license": "NOASSERTION", + "evidence": None}] + self.assert_error(manifest(entry), "upstream[0]: an upstream hop without an identified license") + + # Mutable evidence + + def test_blob_main_license_evidence(self) -> None: + entry = base_entry() + entry["license"]["evidence"] = "https://github.com/example/engine/blob/main/LICENSE" + self.assert_error(manifest(entry), "license.evidence: must be a file on github.com, gitlab.com, huggingface.co") + + def test_blob_main_upstream_evidence(self) -> None: + entry = model_entry() + entry["upstream"] = [{"name": "Base", "source": "https://github.com/example/base", "license": "MIT", + "evidence": "https://github.com/example/base/blob/main/LICENSE"}] + self.assert_error(manifest(entry), "upstream[0]: evidence: must be a file on github.com, gitlab.com") + + # Evidence binding + + def test_evidence_must_name_a_file(self) -> None: + for url in ( + f"https://github.com/example/engine/tree/{COMMIT}", + f"https://github.com/example/engine/tree/{COMMIT}/LICENSE", + f"https://github.com/example/engine/blob/{COMMIT}", + f"https://github.com/example/engine/blob/{COMMIT}/", + f"https://github.com/example/engine/blob/{COMMIT}/docs/", + f"https://github.com/example/engine/resolve/{COMMIT}/LICENSE", + ): + with self.subTest(url=url): + entry = base_entry() + entry["license"]["evidence"] = url + self.assert_error(manifest(approve(entry)), "license.evidence: must be a file on github.com") + + def test_hugging_face_and_gitlab_evidence_must_name_a_file(self) -> None: + for url in (f"https://huggingface.co/example/model/resolve/{COMMIT}", + f"https://huggingface.co/example/model/tree/{COMMIT}/voices"): + with self.subTest(url=url): + entry = model_entry() + entry["license"]["evidence"] = url + self.assert_error(manifest(approve(entry)), "license.evidence: must be a file on github.com") + entry = base_entry() + entry["source"] = "https://gitlab.com/example/engine" + entry["license"]["evidence"] = f"https://gitlab.com/example/engine/-/tree/{COMMIT}/COPYING" + self.assert_error(manifest(approve(entry)), "license.evidence: must be a file on github.com") + + def test_upstream_evidence_must_name_a_file(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(evidence=f"https://github.com/example/base/tree/{OTHER_COMMIT}")] + self.assert_error(manifest(approve(entry)), "upstream[0]: evidence: must be a file on github.com") + + def test_evidence_in_another_repository(self) -> None: + entry = base_entry() + entry["license"]["evidence"] = f"https://github.com/unrelated/project/blob/{COMMIT}/LICENSE" + self.assert_error(manifest(approve(entry)), + "license.evidence: must be in the reviewed repository github.com/example/engine") + + def test_evidence_at_another_revision(self) -> None: + entry = base_entry() + entry["license"]["evidence"] = f"https://github.com/example/engine/blob/{OTHER_COMMIT}/LICENSE" + self.assert_error(manifest(approve(entry)), f"license.evidence: must be at the reviewed revision {COMMIT}") + + def test_evidence_on_non_repository_host(self) -> None: + for url in (f"https://example.com/{COMMIT}/LICENSE", + f"https://raw.githubusercontent.com/example/engine/{COMMIT}/LICENSE", + f"https://github.com:443/example/engine/blob/{COMMIT}/LICENSE", + f"https://github.com/example/engine/blob/{COMMIT}/../../other/LICENSE"): + entry = base_entry() + entry["license"]["evidence"] = url + self.assert_error(manifest(approve(entry)), "license.evidence: must be a file on github.com") + + def test_evidence_without_entry_revision(self) -> None: + entry = model_entry() + entry["revision"] = None + entry["downloads"] = [{"url": "https://example.com/model.bin", "sha256": SHA, "size": 1}] + self.assert_error(manifest(approve(entry)), "license.evidence: must be at the reviewed revision None") + + def test_package_source_without_vcs(self) -> None: + entry = base_entry() + entry.update({"kind": "native-library", "configuration": None, + "source": "https://crates.io/crates/example/1.0.0"}) + self.assert_error(manifest(approve(entry)), "license.evidence: the reviewed repository (vcs, or else source)") + + def test_invalid_vcs(self) -> None: + entry = base_entry() + entry["vcs"] = "https://example.com/example/engine" + self.assert_error(manifest(approve(entry)), "example-engine: vcs: must be a github.com") + + def test_source_commit_differs_from_revision(self) -> None: + entry = base_entry() + entry["source"] = f"https://github.com/example/engine/tree/{OTHER_COMMIT}/src" + self.assert_error(manifest(approve(entry)), "source: a commit in source must equal the entry revision") + + def test_upstream_evidence_in_another_repository(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(evidence=f"https://github.com/unrelated/project/blob/{OTHER_COMMIT}/LICENSE")] + self.assert_error(manifest(approve(entry)), + "upstream[0]: evidence: must be in the reviewed repository github.com/example/base") + + def test_upstream_evidence_at_another_revision(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(evidence=f"https://github.com/example/base/blob/{COMMIT}/LICENSE")] + self.assert_error(manifest(approve(entry)), + f"upstream[0]: evidence: must be at the reviewed revision {OTHER_COMMIT}") + + def test_upstream_evidence_without_revision(self) -> None: + entry = model_entry() + hop = base_hop() + del hop["revision"] + entry["upstream"] = [hop] + self.assert_error(manifest(approve(entry)), "upstream[0]: evidence: must be at the reviewed revision None") + + # Custom upstream licenses + + def test_approved_upstream_other_license_without_name(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(license="other")] + self.assert_error(manifest(approve(entry)), "upstream[0]: a custom upstream license needs an exact licenseName") + + def test_approved_upstream_licenseref_without_name(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(license="MIT AND LicenseRef-Base")] + self.assert_error(manifest(approve(entry)), "upstream[0]: a custom upstream license needs an exact licenseName") + + def test_empty_upstream_license_name(self) -> None: + entry = model_entry() + entry["upstream"] = [base_hop(license="other", licenseName=" ")] + self.assert_error(manifest(approve(entry)), "upstream[0]: licenseName must be a non-empty string") + + # Approval bound to the reviewed fields + + def test_approved_without_fingerprint(self) -> None: + entry = base_entry() + del entry["review"]["legal"]["fingerprint"] + self.assert_error(manifest(entry), "review.legal.fingerprint: approved requires the reviewed fingerprint") + + def test_malformed_fingerprint(self) -> None: + entry = base_entry() + entry["review"]["legal"]["fingerprint"] = "abc" + self.assert_error(manifest(entry), "review.legal.fingerprint: must be 64 lowercase hex characters") + + def test_every_reviewed_field_change_invalidates_approval(self) -> None: + changes = { + "name": lambda e: e.update(name="Example Engine Fork"), + "version": lambda e: e.update(version="v2.0.0"), + "source": lambda e: e.update(source="https://github.com/other/engine"), + "revision": lambda e: e.update(revision=OTHER_COMMIT), + "vcs": lambda e: e.update(vcs="https://github.com/example/engine"), + "downloads": lambda e: e["downloads"][0].update(sha256="cd" * 32), + "download url": lambda e: e["downloads"][1].update( + url=f"https://huggingface.co/example/model/resolve/{COMMIT}/c.bin"), + "paths": lambda e: e.update(paths=["models/example.onnx"]), + "configuration": lambda e: e.update(configuration="v1.0.0 build with TTS"), + "requires": lambda e: e.update(requires=["example-vocab"]), + "license": lambda e: e["license"].update(content="CC-BY-SA-4.0"), + "license evidence": lambda e: e["license"].update( + evidence=f"https://huggingface.co/example/model/blob/{COMMIT}/LICENSE"), + "upstream": lambda e: e["upstream"].append(base_hop()), + "terms": lambda e: e["terms"].update(attribution="Credit Example."), + "download terms": lambda e: e["terms"]["download"].update(presentation="acknowledgement"), + "terms url": lambda e: e["terms"].update(termsUrl="https://example.com/terms"), + "distribution": lambda e: e.update(distribution="bundle"), + } + vocab = model_entry() + vocab.update({"id": "example-vocab", "kind": "tokenizer", "name": "Example Vocab"}) + approve(vocab) + tracked = [FIXTURE, "models/example.onnx"] + for field, change in changes.items(): + with self.subTest(field=field): + entry = model_entry() + change(entry) + self.assert_error(manifest(entry, vocab), + "example-model: review.legal.fingerprint: reviewed fields changed", + notices=NOTICES + "\n## Example Model\n", tracked=tracked) + + def test_rebinding_an_approval_to_another_artifact(self) -> None: + entry = base_entry() + entry.update({"version": "v9.9.9", "source": "https://github.com/unrelated/engine", "revision": OTHER_COMMIT}) + entry["license"]["evidence"] = f"https://github.com/unrelated/engine/blob/{OTHER_COMMIT}/LICENSE" + self.assert_error(manifest(entry), "example-engine: review.legal.fingerprint: reviewed fields changed") + + # Download terms + + def test_user_download_with_presentation_none(self) -> None: + entry = model_entry() + entry["terms"]["download"]["presentation"] = "none" + self.assert_error(manifest(entry), "presentation: user-download requires link or acknowledgement") + + def test_blocked_with_presentation_none(self) -> None: + entry = base_entry() + entry["distribution"] = "blocked" + entry["review"]["legal"]["status"] = "pending" + self.assert_error(manifest(entry), "presentation: none is only valid for bundle") + + # Legal review and approval + + def test_rejected_without_reference(self) -> None: + entry = base_entry() + entry["distribution"] = "blocked" + entry["terms"]["download"]["presentation"] = "unknown" + entry["review"]["legal"] = {"status": "rejected", "reference": None, "date": "2026-09-28"} + self.assert_error(manifest(entry), "review.legal: rejected requires a reference and date") + + def test_rejected_without_date(self) -> None: + entry = base_entry() + entry["distribution"] = "blocked" + entry["terms"]["download"]["presentation"] = "unknown" + entry["review"]["legal"] = {"status": "rejected", "reference": "LR-2", "date": None} + self.assert_error(manifest(entry), "review.legal: rejected requires a reference and date") + + def test_rejected_but_not_blocked(self) -> None: + entry = base_entry() + entry["review"]["legal"]["status"] = "rejected" + self.assert_error(manifest(entry), "rejected requires distribution blocked") + + def test_blocked_and_approved(self) -> None: + entry = base_entry() + entry["distribution"] = "blocked" + entry["terms"]["download"]["presentation"] = "unknown" + self.assert_error(manifest(entry), "a blocked entry cannot be approved") + + def test_approved_without_reference(self) -> None: + entry = base_entry() + entry["review"]["legal"]["reference"] = None + self.assert_error(manifest(entry), "review.legal: approved requires a reference and date") + + def test_approved_without_date(self) -> None: + entry = base_entry() + entry["review"]["legal"]["date"] = None + self.assert_error(manifest(entry), "review.legal: approved requires a reference and date") + + def test_approved_with_invalid_date(self) -> None: + entry = base_entry() + entry["review"]["legal"]["date"] = "2026-02-30" + self.assert_error(manifest(entry), "review.legal.date: must be YYYY-MM-DD or null") + + def test_approved_non_commercial(self) -> None: + entry = base_entry() + entry["terms"]["commercialUse"] = "prohibited" + self.assert_error(manifest(entry), "approval: requires terms.commercialUse allowed") + + def test_approved_without_attribution(self) -> None: + entry = base_entry() + entry["terms"]["attribution"] = "" + self.assert_error(manifest(entry), "approval: terms.attribution must be recorded") + + def test_approved_with_unapproved_requirement(self) -> None: + dependency = model_entry() + dependency.update({"id": "example-vocab", "kind": "tokenizer", "name": "Example Vocab"}) + dependency["review"]["legal"] = {"status": "pending", "reference": None, "date": None} + entry = model_entry() + entry["requires"] = ["example-vocab"] + self.assert_error(manifest(entry, dependency), "approval: requires unapproved example-vocab") + + def test_approved_bundle_with_prohibited_redistribution(self) -> None: + entry = base_entry() + entry["terms"]["redistribution"] = "prohibited" + self.assert_error(manifest(entry), "approval: bundle requires terms.redistribution allowed") + + def test_approved_bundle_without_notice(self) -> None: + self.assert_error(manifest(base_entry()), "approval: bundle requires a THIRD_PARTY_NOTICES.md", + notices="# Third-party notices\n\nExample Engine is mentioned but has no heading.\n") + + def test_approved_content_bundle_without_downloads(self) -> None: + # The shape of silero-vad-coreml: a small model classified as a bundle, pinned only by revision. + for kind in ("model", "voice", "tokenizer", "phonemizer"): + with self.subTest(kind=kind): + entry = model_entry() + entry.update({"kind": kind, "distribution": "bundle", "downloads": []}) + entry["terms"]["download"]["presentation"] = "none" + self.assert_error(manifest(approve(entry)), + f"example-model: approval: a bundled {kind} requires downloads with hashes and sizes", + notices=NOTICES + "\n## Example Model\n") + + def test_approved_user_download_without_downloads(self) -> None: + entry = model_entry() + entry["downloads"] = [] + self.assert_error(manifest(entry), "approval: user-download requires downloads with hashes and sizes") + + def test_approved_user_download_without_terms_url(self) -> None: + entry = model_entry() + entry["terms"]["termsUrl"] = None + self.assert_error(manifest(entry), "approval: user-download requires terms.termsUrl") + + # Fixtures and tracked files + + def test_non_synthetic_fixture(self) -> None: + value = manifest(base_entry()) + value["artifacts"][-1]["synthetic"] = False + self.assert_error(value, "example-fixtures: synthetic: fixtures must be synthetic") + + def test_fixture_without_method(self) -> None: + value = manifest(base_entry()) + value["artifacts"][-1]["method"] = " " + self.assert_error(value, "example-fixtures: method: must describe how the fixture was produced") + + def test_tracked_fixture_without_provenance(self) -> None: + extra = "conformance/fixtures/new.jsonl" + self.assert_error(manifest(base_entry()), f"tracked: {extra}: fixture file has no provenance entry", + tracked=[FIXTURE, extra]) + + def test_fixture_path_listed_twice(self) -> None: + value = manifest(base_entry()) + second = fixture_entry() + second["id"] = "other-fixtures" + value["artifacts"].append(second) + self.assert_error(value, f"other-fixtures: paths: {FIXTURE} is already listed by example-fixtures") + + def test_fixture_path_not_tracked(self) -> None: + self.assert_error(manifest(base_entry()), f"example-fixtures: paths: {FIXTURE} is not a tracked file", + tracked=[]) + + def test_unregistered_tracked_onnx(self) -> None: + self.assert_error(manifest(base_entry()), "tracked: models/example.onnx: model, native binary, or audio", + tracked=[FIXTURE, "models/example.onnx"]) + + def test_unregistered_file_inside_model_bundle(self) -> None: + path = "models/Example.mlmodelc/weights/weight.bin" + self.assert_error(manifest(base_entry()), f"tracked: {path}: model, native binary", tracked=[FIXTURE, path]) + path = "models/Example.mlmodelc/metadata.json" + self.assert_error(manifest(base_entry()), f"tracked: {path}: model, native binary", tracked=[FIXTURE, path]) + + def test_scan_is_case_insensitive(self) -> None: + for path in ("models/unsafe.ONNX", "test/Voice.WAV", "libs/Engine.DyLib", "models/Model.MLMODELC/model.mil", + "vendor/Lib.XCFramework/Info.plist"): + self.assert_error(manifest(base_entry()), f"tracked: {path}: model, native binary", tracked=[FIXTURE, path]) + + def test_unregistered_tracked_audio(self) -> None: + self.assert_error(manifest(base_entry()), "tracked: test/voice.wav: model, native binary, or audio", + tracked=[FIXTURE, "test/voice.wav"]) + + +if __name__ == "__main__": + unittest.main()