[
  {
    "description": "Know where your code came from.",
    "languages": [
      "Dockerfile",
      "Just",
      "Rust"
    ],
    "last_updated": "2026-10-19T01:02:22Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/pelicode.jpg"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/pelicode"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/pelicode/latest"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/pelicode"
      },
      {
        "label": "artifact",
        "ref": "quay.io/tama5/pelicode-db:v1",
        "url": "https://quay.io/repository/tama5/pelicode-db"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/pelicode/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/pelicode"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/pelicode/"
      }
    ],
    "name": "pelicode",
    "owner": "tamada",
    "releases": [
      {
        "description": "The first release of **pelicode**, a research prototype that tells you where your code may have come from.\n\npelicode keeps a database of the functions of published open-source packages, and checks a project, or only what a change touches, against it. It reports **similar code candidates to review**, each with the package, version, file, lines and license it was found in. It does not decide whether code was copied: similar code is no proof of copying, and copying is not always a problem.\n\nThe database holds hashes, locations and licenses, never source code.\n\n## What it can do\n\n- **Languages**: Go, Java, Kotlin, Python, Ruby and Rust, function by function (Tree-sitter), normalized at four levels: layout, identifiers and literals, types, structure.\n- **Repositories**: PyPI, Maven Central, the Go module proxy, RubyGems and crates.io.\n  - Downloads are verified against the digests the repositories publish, licenses are identified, and requests are paced.\n  - `crawl --top N` fetches the best-known packages of each.\n  - `crawl --updates` fetches the new versions of the packages in a database.\n- **Matching**: exact matches after normalization, and similar matches through winnowing fingerprints, MinHash and LSH. Code common to many packages is left out, and so are the dependencies of the checked project, read from its lock files.\n- **Checking**: directories, a git range (`--git main...HEAD`), or what is staged (`--staged`, for pre-commit hooks). The results come as text, JSON, CSV, or SARIF for GitHub code scanning.\n- **Settings file**: `.pelicode.yaml` or `.pelicode.json5` in the repository.\n- **Sharing databases**:\n  - `pelicode snapshot` writes a copy for checking only.\n  - Snapshots of the best-known 500 packages of each ecosystem are published monthly in quay.io (`quay.io/tama5/pelicode-db`).\n  - `scan --db oci://quay.io/tama5/pelicode-db:pypi-latest` fetches, verifies and caches one; several can be checked at once.\n- **Maintenance**: `pelicode stats` refreshes the counts of common hashes incrementally, and `pelicode clean` removes data no version refers to any more.\n\n## Install\n\n```sh\nbrew install tamada/tap/pelicode\ncargo install pelicode-cli\ndocker run --rm -v \"$PWD\":/workdir quay.io/tama5/pelicode --help\n```\n\nArchives for Linux, macOS and Windows (x64 and arm64) are attached to this release. See the [installation guide](https://tamada.github.io/pelicode/install/) and [Getting Started](https://tamada.github.io/pelicode/getting-started/).\n\n```sh\npelicode scan ./src --db oci://quay.io/tama5/pelicode-db:pypi-latest\n```\n\n## Known limitations\n\n- **Defaults**: the default parameters (k, thresholds, the stop threshold) rest on preliminary observations; the evaluation planned in §11 of the [specification](https://github.com/tamada/pelicode/blob/main/.github/docs/spec.md) has not been done yet.\n- **git**: `--git` and `--staged` need the `git` command (#6).\n- **Rankings**: the best-known packages of Maven Central and Go are ranked by the number of repositories that depend on them, since those repositories publish no download counts.\n- **Scale**: measured up to 1.3 million functions per database (§11.3 of the specification); larger databases are untried.\n- **GitHub Action**: the action for pull requests lives in [tamada/pelicode-action](https://github.com/tamada/pelicode-action), with releases of its own.\n",
        "name": "Release v0.1.0",
        "release_date": "2026-10-19T01:08:48Z"
      }
    ],
    "topics": [
      "code-clone-detector",
      "code-provenance",
      "license-compliance",
      "theft-detection",
      "tree-sitter"
    ]
  },
  {
    "description": "The cheat sheet of Jujutsu.",
    "languages": [
      "JavaScript",
      "CSS"
    ],
    "last_updated": "2026-09-13T22:53:02Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/jj-cheat-sheet.svg"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/jj-cheat-sheet/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/jj-cheat-sheet"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/jj-cheat-sheet"
      }
    ],
    "name": "jj-cheat-sheet",
    "owner": "tamada",
    "releases": [],
    "topics": [
      "cheatsheet",
      "jujutsu"
    ]
  },
  {
    "description": "My home-brew recipes.",
    "languages": [
      "Ruby",
      "Rust"
    ],
    "last_updated": "2026-10-12T00:23:16Z",
    "license": [
      {
        "name": "Creative Commons Zero v1.0 Universal",
        "spdx": "cc0-1.0",
        "url": "http://choosealicense.com/licenses/cc0-1.0/"
      }
    ],
    "links": [
      {
        "label": "repository",
        "url": "https://github.com/tamada/homebrew-tap"
      },
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/homebrew-tap.png"
      }
    ],
    "name": "homebrew-tap",
    "owner": "tamada",
    "releases": [],
    "topics": []
  },
  {
    "description": "Mou Seiseki Teisei ha Yadayo.",
    "languages": [
      "Dockerfile",
      "Just",
      "Rust"
    ],
    "last_updated": "2026-09-26T10:14:50Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/mosty.svg"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/mosty"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/mosty/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/mosty"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/mosty/"
      }
    ],
    "name": "mosty",
    "owner": "tamada",
    "releases": [
      {
        "description": "## mosty v0.1.0\n\n最初のリリースです。成績処理用の Excel ファイルで、シートをまたいだ参照が別の学生の行を指しているミス（1 行ずれなど）を検出します。\n\n### 使い方\n\n```sh\nmosty init grades.xlsx      # シートの構造を推定して mosty.grades.xlsx.json5 に書き出す\nmosty check grades.xlsx     # 設定ファイルに従って参照を検証する\n```\n\n`mosty init` が書き出した設定ファイル（学生証番号の列、氏名の列、学生の行の範囲）は、`mosty check` の前に確認し、推定が誤っていれば修正してください。\n設定ファイルがないまま `mosty check` を実行した場合は、`mosty init` を自動で実行してから検証します（`--no-init` で無効化できます）。\n\n### 主な機能\n\n- 参照元の行と参照先の行の学生証番号の不一致を検出します。\n- 1 つの学生の行から、複数の学生の行を含む範囲（`SUM(課題!E4:E8)` など）を参照していることを検出します。`VLOOKUP` などの検索関数の検索範囲は対象外です。\n- 同じシート内の学生証番号の重複、設定ファイルと Excel ファイルの食い違い（シートの追加・削除、学生表の外の学生証番号）を検出します。\n- 学生証番号は数値・文字列のどちらでも扱います。書式は正規表現で指定できます（既定は 7 桁の数字）。\n- 学生証番号が数式（`=名簿!A5` など）でも扱います。\n- 検証結果は `default`・`json`・`markdown` の形式で出力できます（`-F`）。\n- 対応形式は `.xlsx` と `.xlsm` です。\n\n### 互換性のない変更\n\n最初のリリースのため、ありません。\n\n## What's Changed\n* Reorganize the about page and update the Hugo theme by @tamada in https://github.com/tamada/mosty/pull/1\n* Add the mosty logo to the document site by @tamada in https://github.com/tamada/mosty/pull/2\n* Release v0.1.0 by @tamada in https://github.com/tamada/mosty/pull/3\n\n## New Contributors\n* @tamada made their first contribution in https://github.com/tamada/mosty/pull/1\n\n**Full Changelog**: https://github.com/tamada/mosty/commits/v0.1.0\n",
        "name": "Release v0.1.0",
        "release_date": "2026-09-25T11:06:26Z"
      }
    ],
    "topics": [
      "excel",
      "reference-verification"
    ]
  },
  {
    "description": "Pseudo-REST static API generator",
    "languages": [
      "Dockerfile",
      "Just",
      "Elvish",
      "Shell",
      "PowerShell"
    ],
    "last_updated": "2026-10-10T00:26:00Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/fauxrest.svg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/fauxrest/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/fauxrest"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/fauxrest"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/fauxrest/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/fauxrest"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/fauxrest"
      }
    ],
    "name": "fauxrest",
    "owner": "tamada",
    "releases": [
      {
        "description": "## What's Changed\n* test: cover numeric derive type preservation by @atharvafulay in https://github.com/tamada/fauxrest/pull/14\n* fix: reject $filter comparisons whose operand kinds cannot be evaluated by @tamada in https://github.com/tamada/fauxrest/pull/24\n* feat: derive template values from filtered data, and add $derive.exclude by @tamada in https://github.com/tamada/fauxrest/pull/25\n* fix: publish output only after the whole build succeeds by @tamada in https://github.com/tamada/fauxrest/pull/26\n* docs: correct the serializer configuration example by @tamada in https://github.com/tamada/fauxrest/pull/30\n* chore: drop the directives $emit replaced by @tamada in https://github.com/tamada/fauxrest/pull/31\n* fix: reject a $-prefixed key that is not a directive by @tamada in https://github.com/tamada/fauxrest/pull/35\n* feat: land $skip and the unknown-directive check on main by @tamada in https://github.com/tamada/fauxrest/pull/36\n* feat: write the whole API as one file with bundle by @tamada in https://github.com/tamada/fauxrest/pull/37\n* fix: send --help and --version to stdout with status 0 by @tamada in https://github.com/tamada/fauxrest/pull/39\n* Release v0.0.5 by @tamada in https://github.com/tamada/fauxrest/pull/40\n\n## New Contributors\n* @atharvafulay made their first contribution in https://github.com/tamada/fauxrest/pull/14\n\n**Full Changelog**: https://github.com/tamada/fauxrest/compare/v0.0.4...v0.0.5",
        "name": "Release v0.0.5",
        "release_date": "2026-08-05T06:24:37Z"
      },
      {
        "description": "## What's Changed\n* fix: unbreak cargo publish and stop shipping testdata in the crate by @tamada in https://github.com/tamada/fauxrest/pull/10\n* fix: stop $filter regex failures from publishing withheld records, and compile patterns once by @tamada in https://github.com/tamada/fauxrest/pull/11\n* feat!: narrow $derive.type to string and int by @tamada in https://github.com/tamada/fauxrest/pull/12\n* fix: order string operands lexicographically in gt/gte/lt/lte by @tamada in https://github.com/tamada/fauxrest/pull/13\n* Ci/force fmt in ci by @tamada in https://github.com/tamada/fauxrest/pull/15\n* fix: report the actual cause of $derive and $aggregate config errors by @tamada in https://github.com/tamada/fauxrest/pull/17\n* docs: add CHANGELOG.md by @tamada in https://github.com/tamada/fauxrest/pull/19\n* docs: offer the powered-by badge in the README by @tamada in https://github.com/tamada/fauxrest/pull/18\n* fix: report the actual cause of $static config errors too by @tamada in https://github.com/tamada/fauxrest/pull/20\n* Release v0.0.4 by @tamada in https://github.com/tamada/fauxrest/pull/22\n\n\n**Full Changelog**: https://github.com/tamada/fauxrest/compare/v0.0.3...v0.0.4",
        "name": "Release v0.0.4",
        "release_date": "2026-08-01T08:44:26Z"
      },
      {
        "description": "## What's Changed\n* feat: add $derive.type to convert derived values to non-string types by @tamada in https://github.com/tamada/fauxrest/pull/9\n* Releases/v0.0.3 by @tamada in https://github.com/tamada/fauxrest/pull/8\n\n## New Contributors\n* @tamada made their first contribution in https://github.com/tamada/fauxrest/pull/9\n\n**Full Changelog**: https://github.com/tamada/fauxrest/compare/v0.0.1...v0.0.3",
        "name": "Release v0.0.3",
        "release_date": "2026-07-28T11:07:07Z"
      }
    ],
    "topics": [
      "rest-api",
      "static-api",
      "static-api-generator"
    ]
  },
  {
    "description": "Simple options picker in CLI.",
    "languages": [
      "Rust"
    ],
    "last_updated": "2026-02-09T03:20:15Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "docs",
        "url": "https://docs.rs/pick-a-boo/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/pick-a-boo"
      },
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/pick-a-boo.png"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/pick-a-boo/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/pick-a-boo"
      }
    ],
    "name": "pick-a-boo",
    "owner": "tamada",
    "releases": [
      {
        "description": "- Add short labels to items (#9 , #10 ).\r\n- As a result, the macro became bloated, so it was separated into a different crate ([pick-a-boo-macros](https://crates.io/crates/pick-a-boo-macros)).\r\n  - This macro's crate is publicly available.\r\n- Subsequently, the version number increased due to changes like GitHub Actions scripts for automated releases, but the core functionality remains unchanged.\r\n\r\nThe binaries of `pick-a-boo` are not available on GitHub. \r\nPlease use `pick-a-boo` with `cargo`.",
        "name": "v0.1.6",
        "release_date": "2026-02-09T03:30:17Z"
      }
    ],
    "topics": [
      "library",
      "select-options",
      "terminal",
      "yes-or-no"
    ]
  },
  {
    "description": "Detects software theft by comparing birthmarks extracted from binaries",
    "languages": [
      "Rust",
      "Shell",
      "Java",
      "Python",
      "Just"
    ],
    "last_updated": "2026-10-18T03:57:00Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/oinkie.png"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/oinkie"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/oinkie/latest"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/oinkie"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/oinkie/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/oinkie"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/oinkie/"
      }
    ],
    "name": "oinkie",
    "owner": "tamada",
    "releases": [
      {
        "description": "## Highlights\n\n- **Binaries for Windows, x86_64 and arm64** (#202). v0.8.0 shipped binaries for Linux and macOS only, though oinkie is built and tested on Windows. Each release now carries `oinkie-X.Y.Z_amd64_windows.zip` and `oinkie-X.Y.Z_arm64_windows.zip`, each built on a Windows runner of its own CPU and, like the native Linux and macOS builds, checked to serve MCP before it is uploaded. A zip rather than a tar.gz, so that Windows' Explorer opens it.\n- **The release binaries are built before a release, not first at it.** Their build moved into a workflow of its own, which a pull request changing it, the scripts it runs or `Cargo.toml` and `Cargo.lock` runs too, building every platform without uploading anything. A missing or broken platform now shows on the pull request rather than after the tag and the draft exist.\n\nNothing else changes from v0.8.0: no code, no options, no scores.\n\n## What's Changed\n* Ship Windows binaries for x86_64 and arm64, and build release binaries on pull requests by @tamada in https://github.com/tamada/oinkie/pull/203\n* Release v0.8.1 by @tamada in https://github.com/tamada/oinkie/pull/204\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.8.0...v0.8.1\n",
        "name": "Release v0.8.1",
        "release_date": "2026-10-08T09:44:31Z"
      },
      {
        "description": "## Results from `run` in earlier releases are wrong for most analyses\n\n**`oinkie run` used only the algorithm half of an analysis** (#150). It compared the two programs' operations whatever birthmark the analysis named, so every `fc-*` analysis and every `op-<k>gram-*` analysis reported an `op-*` score under the wrong name: `run -a fc-set-jaccard` was `op-set-jaccard`, and `run -a op-3gram-seq-levenshtein` was `op-seq-levenshtein`. The MCP server's `oinkie_run` did the same. This was so in every release from v0.2.1 to v0.7.0.\n\n**Recompute** any result `run` or `oinkie_run` produced with an `fc-*` or a k-gram analysis. Results for `op-seq-*`, `op-set-*` and `op-freq-*` are unaffected, and so is anything from `extract` followed by `compare`, which is what `run` now does.\n\n## Upgrading from v0.7.0\n\n**`oinkie reaggregate` is now `oinkie review`.** Aggregation is no longer all that the command is for, so the name stopped fitting. The options are the same; only the default of `-d/--dest-file` changes, from `reaggregate.csv` to `review.csv`.\n\n```sh\n# before\noinkie reaggregate -A topn:3 -d out/top3.csv out/\n# after\noinkie review -A topn:3 -d out/top3.csv out/\n```\n\nThe old name is not an alias. It is a hidden subcommand that refuses, says `review` and exits non-zero, and it will be removed in the next minor release.\n\n**The MCP tool `oinkie_reaggregate` is now `oinkie_review`**, with no old name kept, so `tools/list` holds one tool rather than two of which a model could not tell which is deprecated.\n\nThe documentation page moved to `usage/review/`; the old address redirects.\n\n**The library no longer knows the command line.** Code that uses `oinkie` as a library is affected; the `oinkie` command is not, and its `--help`, `info` and shell completions are byte-for-byte what they were.\n\n- `Ir`, `Algorithm` and `PairingStrategy` no longer derive clap's `ValueEnum`. Each has an `ALL` constant listing its variants, and `Algorithm` implements `FromStr`, which reads either spelling of a name (`weightedjaccard` or `weighted-jaccard`) in any case.\n- `Error::Clap` is gone. A usage error is the command line's own type now.\n- A k-gram of size zero (`op-0gram-set`) is refused where the name is read, and `Extractor::extract` refuses a `BirthmarkType::OpKgramSet(0)` built by hand. Both used to panic on extraction.\n- `Extractor::birthmark_type()` says which birthmark an extractor makes.\n- `Comparator::compare_programs` is gone. It compared two programs' operations directly, whatever birthmark was meant, and was how `run` came to ignore the birthmark (#150). To compare two programs under an analysis, extract each with `Extractor::new(analysis.birthmark().clone())` and compare the birthmarks with `oinkie.comparator(analysis.algorithm()).compare_birthmarks` (see `Oinkie` below).\n- `Error::ToolNotFound { tool, env, candidates }` is new: a lifting tool whose installation could not be found. It replaces a `Parse` whose text told the reader to pass `--home`, which only the command line has; `oinkie lift` still says exactly what it said.\n- `Algorithm::cli_name` is now `Algorithm::name`: it is the algorithm's name inside an analysis name, which is not a command-line matter.\n- `BirthmarkType::advertised`, `BirthmarkType::description`, `AnalysisType::advertised_names` and `MAX_ADVERTISED_K` are gone: they existed to feed completion and `--help`. `BirthmarkType::all(max_k)` lists every family with k-grams up to a ceiling the caller chooses.\n- `extractor::dest_file_name` is gone. The file name an extraction is written to is the command line's to decide.\n- `Lifter` has a `notice` method, which returns something the user is owed before the first lift. The IDA lifter returns its cloud-upload disclosure there instead of printing it; the `oinkie` command still prints it to stderr before the first lift, where `--level` cannot hide it.\n- **`oinkie::prelude` is gone, and every name has one path.** The root holds `Program`, `Oinkie`, `Error` and `Result`; everything else is in the module it belongs to:\n\n  ```rust\n  use oinkie::{Oinkie, Program};\n  use oinkie::birthmarks::{Birthmark, BirthmarkType, AnalysisType};\n  use oinkie::compare::{Aggregator, Algorithm, Comparator, PairingStrategy};\n  use oinkie::extract::Extractor;\n  use oinkie::lift::{Ir, LifterBuilder};\n  ```\n\n  `oinkie::extractor` is now `oinkie::extract`. `compare::Size`, the payload of `Aggregator::TopN`, can be named for the first time.\n- **`AnyProgram` is now `Program`, and the generic `Program<T>` is no longer public.** A program is read with `Program::load`, which picks the operation type from the file, and is opaque: a new lifter is no longer a breaking change to it. `Extractor::extract_any` is now `Extractor::extract`. `Comparator::compare_any` is gone, not renamed: compare two programs by extracting a birthmark from each with `Extractor::extract` and comparing them with `Comparator::compare_birthmarks` (see `compare_programs` above). The generic `Extractor::extract`/`extract_each` and `Comparator::compare_programs` taking a `Program<T>` are gone too, as are `Function<T>`, the `Op` and `Iterable` traits, and the `ghidra`, `ida` and `binaryninja` modules, which held only the per-representation operation types.\n- **`Elements` is now `birthmarks::Function`**, and the types say what the terms say (#131): a `Birthmark` holds one `Function` per function, and a `Function` -- one function's birthmark -- is made of elements, its `Data`. `Birthmark::functions()` returns them, replacing iteration through the `Iterable` trait; `Function::len()` counts a function's elements and `Birthmark::len()` the functions. There is no `Elements` alias: the old name was only reachable through the prelude, which is gone. **Birthmark files keep the key `elements`**, so every file written before still loads, and files written now load in v0.7.0.\n- **The fields of `Birthmark`, `Function` and `AnalysisType` are private**, so a later change to how a birthmark is held is not a breaking one. Use the accessors: `name`, `path`, `ir`, `birthmark_type`, `extracted_at`, `duration`, `json_path` and `functions` on `Birthmark`; `name` and `data` on `Function`; `birthmark` and `comparator` on `AnalysisType`. `Metadata` is no longer public; everything in it is reachable through those accessors.\n- No longer public, because nothing outside the crate had a use for them: `lift::HomeSpec`, `Ir::home_spec`, `Ir::find_home` (`LifterBuilder` is the way in), `BirthmarkType::with_shape`, `Birthmark::check_comparable_with` (the comparator calls it), `Metadata::parse`, `Comparison::new` and `Error::error_or`. `Birthmark::comparable_with` is removed.\n- **`Error`, `Ir`, `BirthmarkType`, `Algorithm`, `Aggregator` and `PairingStrategy` are `#[non_exhaustive]`.** A `match` on any of them outside this crate needs a wildcard arm. In exchange, a new error, representation, birthmark family, algorithm, aggregator or strategy is no longer a breaking change. To enumerate what exists, use `Ir::ALL`, `Algorithm::ALL`, `PairingStrategy::ALL` or `BirthmarkType::all`. `Data` stays exhaustive on purpose: a new birthmark shape changes what every consumer has to do with it, so it should stop the build.\n- **`Error::is_caller_fault()`** says whether the caller could have avoided an error by asking differently -- a name, a pairing or a number they gave, or a file they named. It is the classification the MCP server answers with, moved into the library because only the crate that defines `Error` can still match it exhaustively.\n- **`Error::ToolIo(path, cause)` is new**, and `Error::Io` now means an I/O failure on a path the caller gave. Starting a lifting tool and writing its scratch files used to report `Io`; they report `ToolIo`, which reads the same but is not counted as the caller's fault.\n- **The score CSV is the command line's.** `Comparison::store`, the `CsvInfo` trait, `Metadata::csv_info` and `escape_csv_string` are gone from the library; `oinkie compare`, `run` and the MCP server write the same files as before. A library user who wants a comparison written down has what it is made of instead: `Comparison::columns`, `rows` and `matrix`, alongside `similarity` and `duration`, and the new accessors `Birthmark::ir`, `Birthmark::json_path`, `Program::json_path`, `Program::symbol_count` and `Program::function_names`.\n- **A carriage return in a path or a function name is quoted in the score CSV.** It ends a record as surely as a line feed, and was left bare, so a name containing one split its line and `review` read back a truncated path.\n- **Comparators are made from an `Oinkie`** (#182), which owns the threads they compute on. `Algorithm::comparator()` and `AnalysisType::comparator()` are gone; `AnalysisType::algorithm()` says which algorithm an analysis uses.\n\n  ```rust\n  // before\n  let comparator = Algorithm::Jaccard.comparator();\n  // after: one thread per core, or Oinkie::builder().threads(n).build()?\n  let oinkie = Oinkie::new()?;\n  let comparator = oinkie.comparator(&Algorithm::Jaccard);\n  ```\n\n  Make one `Oinkie` and keep it; cloning it shares its threads, and `Oinkie::install` runs a closure of your own on them, so that your loops and oinkie's computing do not start two pools. oinkie no longer uses rayon's global pool, which is left to the application, and no longer reads `RAYON_NUM_THREADS`. `Error::ThreadPool` is new: the threads could not be started.\n- **A pair's matrix is a `compare::Matrix`, not an `ndarray::Array2<f64>`** (#175), and `oinkie` no longer depends on `ndarray` or `lapjv`. `Comparison::matrix` returns one and `Aggregator::aggregate` takes one; index it with `matrix[[i, j]]` as before, and read its shape with `dim()`. **It is no longer padded to a square**: it has one index per function of each side, `columns().len()` by `rows().len()`, so a pair of 1,000 and 5,000 functions holds 5 million cells rather than 25 million. The scores are what they were: `aggregate` counts the shorter side's missing functions as sharing nothing, exactly as the zeros of the padding did. `Matrix::new` refuses a cell that is not a finite number, which no pairing can maximise. `Error::LapJV` and `Error::ShapeError` are gone with the crates they wrapped.\n- **`Aggregator::aggregate` returns the pair's similarity, and takes the functions' sizes** (#194): `aggregate(&matrix, sizes)` gives an `f64` where it gave the `Vec<f64>` that `Comparison::similarity` averaged, and applies the rule for empty sides itself, so a caller scoring a stored matrix no longer repeats it. `sizes` is each function's element count, in the order of `Matrix::dim`; pass `None` for every aggregator but the new `Aggregator::Weighted`, which `Aggregator::uses_sizes()` reports and which fails with the new `Error::FunctionSizes` without them. Every score is what it was.\n- **oinkie declares the Rust it needs: 1.88 or later** (`rust-version`). It needed that already -- edition 2024 and `if let` chains -- but an older Rust failed in the compiler without saying why; cargo now says so, and resolves dependencies to versions that build on 1.88. The library, and the command with or without `mcp`, were built with Rust 1.88.\n- `clap` and `sha2` are optional dependencies behind the `cli` feature, which `gencomp` and `mcp` now imply. `cargo add oinkie --no-default-features` pulls in neither.\n\n## Highlights\n\n- **The `weighted` aggregator lets a function count by its size** (#194). Under `hungarian` every matched pair counts alike, so two three-instruction functions -- which agree by chance, and two empty ones exactly -- count as much as two of three hundred. `weighted` finds the one-to-one matching that maximises the similarities each weighted by the two functions' element counts, and divides by the element count of every function of both sides; an unmatched function lowers the score by its size. Short functions then count for little with no threshold to choose, where `review --min-elements` drops them outright. With every function of one size it is 2Σs/(n₁+n₂), Dice's way of counting both sides, which equals `hungarian` only when the two have as many functions. `review -A weighted` reads the sizes from the birthmarks the pair CSVs name, as `--min-elements` does. The weighting follows the Earth Mover's Distance, in which each item carries a mass (Rubner, Tomasi and Guibas 2000); no earlier work weighting a function matching by size for birthmarks was found.\n- **Two aggregators that read the `hungarian` matching another way: `containment` and `matched:T`** (#194). `containment` averages the matched similarities over the smaller birthmark's functions rather than the larger's: how much of the smaller is found in the larger, after Broder's containment of documents (1997). Code copied whole into a program ten times its size scores 1.0 under it, where `hungarian` gives 0.1 -- and since theft is often of part of a program, that is a case worth catching. It says the smaller is contained, not that the two are alike, and a score has to be read with that in mind. `matched:T` is the proportion of the larger birthmark's functions matched with a similarity of at least T, a figure easier to report than a mean; T is above 0 and at most 1. Both take `-A` on `compare`, `run` and `review`, and the MCP tools' `aggregator`, and since they need only the stored matrices, `review` sweeps them over a finished comparison without comparing anything again.\n- **Two algorithms over frequencies: `jensen-shannon` and `tanimoto`** (#192). `jensen-shannon` compares the proportions of each element in two functions, whatever their sizes -- two functions whose counts are in the same proportions score 1.0 -- as 1 − √JSD with the Jensen–Shannon divergence in bits (Lin 1991; the root is a metric, Endres and Schindelin 2003). `tanimoto` is a·b / (|a|² + |b|² − a·b) over the counts, which differs from `weighted-jaccard` and from `cosine`; on sets it would be Jaccard's index, so like them it takes `seq` and `freq` birthmarks only. Both follow the rule for empty functions, and both sum over the elements the two functions share as #180's do: `tanimoto` in whole numbers and so exactly, `jensen-shannon` through logarithms, within 10⁻⁷ of the sum over every element. Analyses are spelled `op-freq-jensenshannon` (or `op-freq-jensen-shannon`) and `op-freq-tanimoto`.\n- **One large pair uses every core** (#179). A pair's matrix is filled by rows in parallel, where it was filled on one thread while the other cores waited: on a 10-core machine, `lcs` over two birthmarks of 5,000 functions takes 47 s rather than 170 s, and `jaccard` 4.6 s rather than 10.5 s. Many small pairs, which already ran in parallel, take as long as before, and the scores are bit-for-bit what they were. The library now depends on `rayon` without the `cli` feature.\n- **A large pair shows how far its matrix has got** (#183). `compare` and `run` counted a pair as one step (`compare` three: two loads and the comparison; it now counts pairs), so the bar stood still for as long as a large pair took. Below the command's bar there is now a line for each thread, for as long as the comparison runs: the pair the thread is comparing, named by its place among the pairs (`thread 3  pair 87/666`), the rows of its matrix with an estimate of the time left, then a spinner while it is aggregated, or `idle`. The lines stay put however quickly pairs come and go. `review` gets a bar too, counting the pairs it rescores; every command's bar shows an estimate of the time left, and each is drawn in a colour of its own, so `compare` and `run` are no longer uncoloured. Every bar stays on one line, its message cut at the terminal's edge, where the messages that named the files being loaded and compared wrapped it. **`--no-progress`** (`oinkie --no-progress compare ...`) draws none of them, for a terminal whose output is kept. For library users, `Comparator::compare_birthmarks_with` takes an `Observer`, which is told when the matrix starts, as each row is filled and when the aggregation begins; `compare_birthmarks` is unchanged.\n- **`--threads N` sets how many threads a command computes on** (#182), on `compare`, `run`, `review`, `extract`, `stats` and `mcp`; it defaults to one per core, as before. It bounds everything the command does in parallel -- pairs, files and the rows of each pair's matrix share the same N threads -- and is the only thing that does: `RAYON_NUM_THREADS` no longer has a say. `lift` keeps `--jobs`, which counts decompiler processes, not threads.\n- **`cosine`, `euclidean` and `weighted-jaccard` compute each cell 2–4× faster** (#180). They summed over every element of either function, gathering both functions' elements into a new set for every cell; they now sum over the elements the two share, in whole numbers, with nothing allocated. 1,000 functions against 1,000 under `weighted-jaccard` take 0.3 s rather than 1.2 s. The scores are bit-for-bit what they were: the sums are of counts, so the rearranged ones are exactly equal.\n- **Comparing a sequence under a set or frequency algorithm is 3–14× faster** (#176). `jaccard`, `dice`, `simpson`, `cosine`, `euclidean` and `weighted-jaccard` convert each function to the shape they compare once, where they converted both functions again for every cell of the matrix. Two birthmarks of 5,000 functions under `op-seq` and `jaccard` compare in 12 s rather than 92 s. The scores are bit-for-bit what they were. `lcs` and `levenshtein` convert nothing and are unchanged.\n- **The `hungarian` aggregator needs half the memory, or less** (#175). It reads a pair's function similarities where they lie, where it used to copy them into a matrix of costs, and no longer pads the matrix to a square. Two binaries of 50,000 functions each now fit in 20 GB, where they needed 40; two of different sizes take far less, and are solved far faster: on random similarities, 16 s rather than 976 s for 30,000 functions against 24,000.\n- **`review` refuses a summary it cannot read to the end** (#167). A line of `results.csv` that could not be read ended the list there, so `review` rescored the pairs before it and said nothing about the rest. And only ASCII digits name a pair now: a file named in another script's digits was taken for a pair CSV and failed the whole review.\n- **Every algorithm scores empty functions by the same rule** (#162): two empty functions agree (1.0), and an empty one against one that is not does not (0.0) -- the rule the birthmark-level comparison already followed. Three did not. `cosine` scored an empty function 1.0 against *anything*, so empty functions matched every function; `euclidean` gave two empty ones NaN, which reached the pair's similarity and `results.csv`; `lcs` gave two empty ones 0.0. **Recompute** results from `cosine` or `euclidean` analyses whose birthmarks have empty functions; `lcs` results change only for those.\n- **The shell completions know every option again, and complete values better** (#190). They had fallen behind the command line: `--threads` was completed by no shell. They are now kept by hand, starting from what `gencomp` generates. In bash, zsh and fish, an option that takes a directory completes directories, the birthmark and lifted-program arguments complete `*.json` (and directories to descend into), `review -d` completes `*.csv`, `--threads` offers 1 up to the number of processors, `-A` completes the aggregators with example values for `topn:N` and `matched:T`, and `--min-elements` turns a typed fraction into the ratio it must be written as (`0.3` becomes `0.3x`). The hidden `reaggregate` is no longer completed as a command. elvish and PowerShell complete command and option names, as before.\n- **`--help` fits in 100 columns, and `-h` in one line per option** (#198). Help was never wrapped, so `compare -h` ran to 218 columns, and the aggregators' description, written with its own hanging indent, was pushed 31 columns further right. Help now wraps to the terminal, at most 100 columns wide. `-A`, `extract -b`, `run -a`, `review --min-elements` and `mcp --root` give a line under `-h` and their full description under `--help`.\n- **`--help` and `--version` no longer write colour codes into a pipe or a file** (#163). They were coloured whatever stdout was, so `oinkie -h | less` and `oinkie review -h > help.txt` got `^[[1m` and the like as text, and `NO_COLOR` made no difference. Now they are coloured on a terminal only, and the extra blank line that ended both is gone.\n- **The documentation says what v0.8.0 changed** (#188). The glossary and the `compare` page say how a pair of different sizes is scored, with a worked example: the functions the smaller birthmark cannot match count as zeros under `hungarian` and `topn:all`, and the glossary had said `hungarian` averaged over the matched pairs alone. A new section of the usage pages says what a comparison costs in memory and time and how `--threads` bounds it, apart from `lift --jobs`. The README and the install page show how to use oinkie as a library and how to install it from crates.io.\n- **The library's whole public API is documented** (#173). Every public item has rustdoc, in the glossary's terms, and each module says what it is for and how its pieces fit; five doctests, run with the tests, show a comparison end to end, parsing an analysis name, extracting a birthmark and building a lifter. A public item without documentation now fails the build.\n- **A glossary** (#156) on the site defines each term once -- birthmark, function, element, similarity and the rest -- and the documentation and `--help` now use them consistently. What a pair's matrix holds is a *function similarity*, not an \"element-wise\" one: elements are what a function's birthmark is made of. A pair's one number is its *similarity*, not a \"birthmark-wise\" or \"program-wide\" score. The description of `topn:N` now says what it does: each function's best match in the other birthmark, on both sides, and the mean of the N best from each side.\n- **The Ghidra lifting script's string handling is tested** (#138), on every runner and without starting Ghidra: its JSON escaper, and its reading of Ghidra's Windows path spelling -- the two places it has shipped wrong (#77, #136). `compile-check.sh --run CLASS` compiles a runner beside the script and runs it; `just test-ghidra-script` is the shortcut.\n- **The MCP server has `oinkie_stats`** (#130), the counterpart of `oinkie stats`: the same groups, per-file rows and skipped files, as structured JSON rather than a table, with `paths`, `recursive`, `per_file` and `top`. The per-file rows come back only when `per_file` is given. Every path is confined to the roots, and so is every file found in a directory, since one can be a symlink out of them.\n- **`review --min-elements` drops the functions too short to be evidence** (#128). A function with few elements agrees with many others by chance, and two empty ones are identical, so they lift the score of unrelated programs. `--min-elements 5` drops the functions with fewer than five elements from every pair, and `--min-elements 0.3x` those with fewer than 0.3 times the mean over every function in the directory, before the pair is aggregated again; nothing is compared again, so one comparison can be swept over several thresholds. A side left with no functions scores 0.0, and two such sides 1.0. The summary's last line records the threshold, since scores under different thresholds are not comparable. The counts come from the birthmarks the score CSVs name. The MCP server's `oinkie_review` takes the same as `min_elements`, and reads those birthmarks only under its roots.\n- **`review` reads back a comparison with an empty birthmark.** `compare` and `run` wrote such a pair's matrix with no functions on that side, and `review` failed on it with a shape error, so a directory holding one empty birthmark could not be reviewed at all.\n- **A function name that needs quoting is read back whole.** Each matrix row wrote its function name after a space, which kept the reader from seeing the quotes, so a name with a comma in it -- `foo<int, int>` -- split the row and `review` refused the file. The space is gone. A name in an older file is still read, unless it has a comma in it.\n- **`reaggregate` is renamed `review`** (#132).\n- **The library does not depend on the command line** (#133).\n- **`run` compares the birthmark the analysis names** (#150); see the top.\n- **`run` writes the birthmarks it compares** into `<dest>/birthmarks/`, one per input, extracted once rather than once per pair, and each pair's CSV names the two it used (#128). A score directory from `run` therefore holds everything `review` needs, as one from `compare` does. With `--skip`, a birthmark already there is reused if it is of the analysis's type, and a pair's CSV only if both its birthmarks were; anything else is extracted or compared again. An input given twice is extracted and written once. The MCP server's `oinkie_run` does the same when it is given a `dest`, and confines the birthmarks it writes below `dest` to the server's roots as it confines `dest` itself.\n- **The MCP server refuses a path through a symlink to nothing.** Such a link inside a root looked like a file not yet created, and writing to it created the link's target wherever it pointed, outside every root.\n- **The MCP server confines every file it writes, not only the directory it was given.** The pairs' CSVs and `results.csv` that `oinkie_run` and `oinkie_compare` write, and the birthmarks `oinkie_extract` writes, are each resolved through the roots before anything is written, so one already there that is a symlink out of every root is refused rather than written through. **`extract` and `oinkie_extract` write each birthmark file once**: an input given twice, or two copies of one program under one stem, used to have two workers write one file at once, which could leave it invalid JSON. `oinkie_extract` reports one birthmark per file written.\n- **A summary CSV lists its pairs in index order** (#146). `compare`, `run`, `review` and the MCP server wrote the rows in the order the parallel comparisons happened to finish, so two runs over the same input wrote the same rows in a different order. The same input now writes the same file, timings aside.\n\n## What's Changed\n* Rename reaggregate to review by @tamada in https://github.com/tamada/oinkie/pull/141\n* Take the command line out of the library by @tamada in https://github.com/tamada/oinkie/pull/142\n* Give every public name one path, and drop the prelude by @tamada in https://github.com/tamada/oinkie/pull/143\n* Make the growing enums non-exhaustive, and classify errors in the library by @tamada in https://github.com/tamada/oinkie/pull/144\n* Move the score CSV to the command line by @tamada in https://github.com/tamada/oinkie/pull/145\n* Name a function's birthmark Function, and make the fields private by @tamada in https://github.com/tamada/oinkie/pull/147\n* Check the library's public API, and that it does not depend on the CLI by @tamada in https://github.com/tamada/oinkie/pull/148\n* Write the rows of a summary CSV in index order by @tamada in https://github.com/tamada/oinkie/pull/149\n* Make run compare the birthmark the analysis names by @tamada in https://github.com/tamada/oinkie/pull/151\n* Let run write the birthmarks it compares into the score directory by @tamada in https://github.com/tamada/oinkie/pull/153\n* Confine every file the MCP tools write, and extract each file once by @tamada in https://github.com/tamada/oinkie/pull/154\n* Let review drop the functions too short to be evidence by @tamada in https://github.com/tamada/oinkie/pull/155\n* Expose stats over MCP as oinkie_stats by @tamada in https://github.com/tamada/oinkie/pull/157\n* Test the Ghidra lifting script's string handling without Ghidra by @tamada in https://github.com/tamada/oinkie/pull/158\n* Analyse the repository with SonarQube Cloud by @tamada in https://github.com/tamada/oinkie/pull/160\n* Fix the rotted external links, and stop reporting ones that are not by @tamada in https://github.com/tamada/oinkie/pull/161\n* Add a glossary, and use its terms in the documentation and help by @tamada in https://github.com/tamada/oinkie/pull/164\n* Colour --help and --version on a terminal only by @tamada in https://github.com/tamada/oinkie/pull/165\n* Score empty functions by one rule in every algorithm by @tamada in https://github.com/tamada/oinkie/pull/166\n* Clear SonarQube's Rust code smells, and keep them cleared by @tamada in https://github.com/tamada/oinkie/pull/170\n* Clear SonarQube's shell script code smells by @tamada in https://github.com/tamada/oinkie/pull/171\n* Clear SonarQube's Python findings by @tamada in https://github.com/tamada/oinkie/pull/172\n* Document the public API and every module by @tamada in https://github.com/tamada/oinkie/pull/174\n* Aggregate with oinkie's own assignment solver; drop lapjv and ndarray by @tamada in https://github.com/tamada/oinkie/pull/177\n* Convert each function to the algorithm's shape once, not once per cell by @tamada in https://github.com/tamada/oinkie/pull/178\n* Fill a pair's matrix by rows in parallel by @tamada in https://github.com/tamada/oinkie/pull/181\n* Let an Oinkie own the threads oinkie computes on; --threads on every computing command by @tamada in https://github.com/tamada/oinkie/pull/184\n* Write the v0.4.0 to v0.7.0 release notes one line per paragraph by @tamada in https://github.com/tamada/oinkie/pull/185\n* Sum the frequency algorithms over the elements two functions share by @tamada in https://github.com/tamada/oinkie/pull/186\n* Show each thread's pair as its matrix fills, and add --no-progress by @tamada in https://github.com/tamada/oinkie/pull/189\n* Let each command's long help open with its own description by @tamada in https://github.com/tamada/oinkie/pull/191\n* Bring the documentation up to v0.8.0 by @tamada in https://github.com/tamada/oinkie/pull/193\n* Add the jensen-shannon and tanimoto algorithms over frequencies by @tamada in https://github.com/tamada/oinkie/pull/195\n* Add the containment and matched:T aggregators by @tamada in https://github.com/tamada/oinkie/pull/196\n* Add the weighted aggregator, which counts a function by its size by @tamada in https://github.com/tamada/oinkie/pull/197\n* Wrap help at 100 columns, and give -A a one-line description under -h by @tamada in https://github.com/tamada/oinkie/pull/199\n* Bring the shell completions up to the command line, and complete values better by @tamada in https://github.com/tamada/oinkie/pull/200\n* Release v0.8.0 by @tamada in https://github.com/tamada/oinkie/pull/201\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.7.0...v0.8.0\n",
        "name": "Release v0.8.0",
        "release_date": "2026-10-08T04:13:00Z"
      },
      {
        "description": "## Upgrading from v0.6.0\n\n**An `fc-*` birthmark that would be empty is returned with a warning instead of\nrefused.** A program that calls nothing gets a birthmark, and one call-less\nfile in a corpus no longer stops the extraction of every file after it.\n\n`Error::NoCallOperations` and `Error::UnresolvedCalls` are gone. Code matching\non them will not compile.\n\n## Highlights\n\n- **`oinkie stats` summarises a set of birthmarks.** For each group of\n  birthmarks sharing an intermediate representation and a type it reports how\n  many files there are, how many functions each holds, and how many elements\n  each function's birthmark has — min, quartiles, max, mean and standard\n  deviation — plus the empty functions, the distinct elements, and for the\n  `freq` shapes the total of the counts.\n\n  ```sh\n  oinkie stats birthmarks/ -f markdown -o stats.md\n  oinkie stats birthmarks/ --per-file -f csv\n  oinkie stats birthmarks/ --top 10 -f json\n  ```\n\n  Two counts the output keeps apart by name: **functions** are the entries in a\n  birthmark file, and **elements** are the items in one function's birthmark.\n  The birthmark length is the second.\n\n  A file that does not read as a birthmark is skipped with a warning and\n  counted, so a summary never covers fewer files than it was given without\n  saying so. A path that does not exist is an error rather than a skip.\n\n  `Kgram::ops` is new in the library, for reading a k-gram's operations without\n  going through serde.\n\n- **Ghidra lifts on Windows.** `lift -r ghidra-pcode` looked for\n  `support/analyzeHeadless`, which is the Unix entry point, so a Windows\n  installation that had Ghidra was reported as not having it. Windows also\n  needs nothing built: an official release ships the decompiler binaries for\n  it, where macOS has to build its own.\n\n- **Two path checks in the MCP server behaved differently on Windows.** The\n  refusal of a `..` component did not fire inside the verbatim paths\n  (`\\\\?\\C:\\...`) that `canonicalize` returns there, and a path running\n  through a file -- `pcodes/a.json/deeper/out.csv` -- was accepted rather than\n  refused.\n\n  Neither let a path out of a `--root`: the root check caught the first\n  afterwards, and the second stayed inside. What they cost was a refusal that\n  described the wrong problem, and one that did not happen at all. Both are\n  decided over the path itself now rather than left to the operating system to\n  report.\n\n- **A lift that comes back with no functions says so.** Such a file used to be\n  accepted without a word, and every birthmark from it is empty -- two empty\n  birthmarks score 1.0, so unrelated programs came back identical.\n\n  It warns rather than fails, because a binary can genuinely have no functions\n  and the file does not say which it is. For `ghidra-pcode` the warning names\n  the usual cause and the fix: an installation whose decompiler binary is\n  missing produces exactly this, and `support/gradle/gradlew buildNatives` in\n  the Ghidra directory builds one.\n\n- **An empty `fc-*` birthmark is a measurement rather than a failure.** A\n  program that calls nothing is a program that calls nothing, and the family\n  does not distinguish it. The warning that replaces the refusal says which of\n  the two cases it found -- nothing was a call, or nothing resolved -- and names\n  the file and the representation.\n\n- Dependencies updated: 66 packages within the ranges the manifest already\n  declared, and three past them -- `itertools` 0.14 to 0.15, `num-derive` 0.4\n  to 0.5, and `sha2` 0.10 to 0.11. The hashes `sha2` produces are unchanged, so\n  no birthmark changes its filename.\n\n  `ndarray` stays at 0.16. `lapjv`, which solves the assignment problem for the\n  `hungarian` aggregator, requires `ndarray ^0.16`, and the only maintained\n  alternative is slower, uses more memory, and needs the similarities rounded\n  to integers. A dependent crate wanting `ndarray` 0.17 cannot have it through\n  oinkie yet.\n\n- **The crates.io upload authenticates by trusted publishing**, so the\n  repository holds no publishing secret.\n\n## What's Changed\n* ci: publish to crates.io by trusted publishing by @tamada in https://github.com/tamada/oinkie/pull/122\n* fix: warn about an empty fc-* birthmark rather than refusing it by @tamada in https://github.com/tamada/oinkie/pull/123\n* chore: update dependencies by @tamada in https://github.com/tamada/oinkie/pull/124\n* test: make the suite honest on Windows by @tamada in https://github.com/tamada/oinkie/pull/127\n* fix: name the Windows headless entry point, so Windows can lift by @tamada in https://github.com/tamada/oinkie/pull/137\n* Add a stats command summarising a set of birthmarks by @tamada in https://github.com/tamada/oinkie/pull/134\n* fix: warn about a lift that found no functions by @tamada in https://github.com/tamada/oinkie/pull/139\n* Release v0.7.0 by @tamada in https://github.com/tamada/oinkie/pull/140\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.6.0...v0.7.0\n",
        "name": "Release v0.7.0",
        "release_date": "2026-09-30T23:30:27Z"
      },
      {
        "description": "## Upgrading from v0.5.0\n\n**`lift` takes `-r/--ir` in place of `-l/--lifter-type`, and names a\nrepresentation rather than a tool.** `-l ghidra` becomes `-r ghidra-pcode`.\nThere are five values now — `ghidra-pcode`, `binary-ninja-llil` / `-mlil` /\n`-hlil` and `ida-microcode` — and all of them are implemented.\n\n`Error::UnsupportedIr` and `Ir::readable` are gone with the last unreadable\nrepresentation. They reported which representations a build could read, and\nevery one can be read now.\n\nNothing about the file format changed. A program lifted by v0.5.0 carries\n`\"ir\": \"ghidra-pcode\"` and reads exactly as it did.\n\n`LifterType` is gone from the library's public API, and `LifterBuilder::new`\ntakes an `Ir`. `AnyProgram` has gained three variants, so a `match` over it in\ndownstream code needs three more arms.\n\nangr is gone from that list as well. It never lifted anything — it parsed,\nand then reported that the angr lifter was not implemented — so the only\nscripts it can break are ones that were already failing.\n\n## Highlights\n\n- **IDA Pro lifts, to the Hex-Rays microcode.** `-r ida-microcode`, read at\n  `MMAT_LVARS`, the last of the decompiler's maturities.\n\n  The earlier maturities are not offered. They look like a parallel to Binary\n  Ninja's three levels and are not: LLIL, MLIL and HLIL are representations\n  Binary Ninja publishes, while the maturities are where a plugin may intervene\n  in the decompiler's pipeline — the same enum holds `MMAT_ZERO`, \"microcode\n  does not exist\". Nothing says their shape is stable across releases, and a\n  representation name is permanent. On one binary four of the eight produced\n  the same operations anyway.\n\n  It runs through `idat`, so nothing touches your Python installation —\n  `idalib` would have required rewriting it.\n\n  **Producing microcode runs the decompiler, and a cloud decompiler sends each\n  function to Hex-Rays' servers.** `lift` warns before it starts, and only when\n  the installation has no local decompiler to use: a decompiler plugin is named\n  `hex<arch>` locally and `hexc<arch>` when it calls out, and the API cannot be\n  asked. It does not prompt — that would hang a script — and does not refuse,\n  since asking for the representation is the consent. A local licence is not\n  told its functions are being uploaded.\n\n- **Binary Ninja lifts, to all three of its intermediate languages.**\n  `-r binary-ninja-llil`, `-mlil` or `-hlil`. Which one answers a question is\n  not something a tool should decide, so all three are offered and none is\n  recommended; they are different vocabularies rather than different amounts\n  of one, and birthmarks are not compared across them.\n\n  It runs through `bnpython3`, Binary Ninja's own interpreter, which has the\n  API importable already, so nothing touches your `python3` or `PYTHONPATH`.\n  `BINARY_NINJA_HOME`, `--home`, and the usual install locations work as they\n  do for Ghidra.\n\n  The symbol table each file carries is keyed by whatever its level renders a\n  call target as: an address at LLIL and MLIL, the callee's own name at HLIL,\n  which has already resolved it.\n\n  Binary Ninja is licensed and cannot be installed on a CI runner, so nothing\n  in CI lifts with it. Its script is parsed on every push, and fixtures under\n  `testdata/lifted/bnil_*/` are read back — including an assertion that\n  each level finds the call that is in them, since a reader recognising none\n  of its own call operations would report unrelated programs as identical.\n\n- **The representation is the only thing you name.** `lift` used to take both\n  a tool and, in the plan for Binary Ninja, a level within it — two arguments\n  that could contradict each other, so something would have had to check for\n  a combination that only existed because there were two of them. There is one\n  now, and `-l ghidra --il hlil` cannot be written down.\n\n  A representation implies its tool; a tool does not imply a representation,\n  since Binary Ninja has three. Keeping the tool as the argument would have\n  kept the one that cannot answer the question being asked — which reader can\n  read this file, and may these two birthmarks be compared.\n\n  Binary Ninja's three levels were declared by that change before anything\n  lifted to them, the way `ida-microcode` still is; the entry above is what\n  filled them in.\n\n- **An `fc-*` birthmark that would be empty is refused, not returned.**\n  `extract` already refused a program in which nothing is a call, because two\n  empty birthmarks score as a perfect match and unrelated programs would come\n  back identical. It did not refuse one whose calls all resolve to nothing,\n  which empties the family just as thoroughly and was the way two lifters\n  reached it during this release: the Hex-Rays microcode renders a global as\n  `$name` and the table was keyed by the resolved name, and Binary Ninja's HLIL\n  resolves the callee before oinkie sees it. Neither made a sound.\n\n  It affects Ghidra too — a program whose calls are all indirect has been\n  producing an empty `fc-*` family silently.\n\n  The new refusal is its own error rather than the existing one, because the\n  two send the reader to different places: nothing being a call is `is_call`,\n  nothing resolving is the symbol table.\n\n- **Internal links are checked on every push.** `cli/mcp/README.md` pointed at\n  `lifter/README.md` for the whole of v0.5.0, after #91 moved that directory,\n  and nothing noticed. There are 32 internal links now, across three kinds of\n  README and the site.\n\n  It is a script rather than an off-the-shelf checker because the repository\n  holds two kinds of link that do not resolve the same way. A README's\n  `assets/lifters/ghidra/README.md` is a path on disk; the site's `../run` is\n  the URL `/usage/run`, whose file is `usage/run.md`. A filesystem checker\n  reports every site link as broken.\n\n  It reads Markdown and looks at the filesystem — no network, no build — so it\n  costs seconds. Code regions are not scanned: link-shaped text inside a fenced\n  example is not a link, and these READMEs are mostly examples. Reference-style\n  links are, because a check that only saw one syntax could be bypassed by\n  choosing the other.\n\n  External links are checked weekly instead, opening an issue rather than\n  failing a push — someone else's site being down for ten minutes is not a\n  reason to fail a build — and updating that issue rather than opening another\n  one every Monday.\n\n- **oinkie is ready to go to crates.io, as a library and as a binary.** The\n  manifest had four keys and none of them a licence, which crates.io refuses an\n  upload without — the `LICENSE` file has been here since the first commit and\n  the manifest had never said so.\n\n  It also had no `exclude`, so the crate carried 100 files and 2.2 MB, most of\n  it `testdata/`, the Hugo site and the workflows. It carries 62 files and\n  690 KiB now.\n\n  Written as exclusions rather than as an `include` whitelist on purpose: three\n  lifting scripts are compiled in with `include_str!`, and a whitelist that\n  forgot the next one would build here and fail for whoever ran `cargo add`.\n  `tests/package_test.rs` asserts that no `exclude` rule covers a compiled-in\n  file, and that its own list has not fallen behind the `include_str!` calls in\n  `src/`.\n\n  `AnyProgram` derives `Debug`, without which `unwrap_err` does not compile —\n  found by writing the downstream crate rather than imagining it.\n\n  The library and the CLI stay one crate, so `cargo add oinkie` and\n  `cargo install oinkie` both work under one name and a release is one upload\n  rather than two that can half-succeed. What the CLI needs and the library does\n  not — `env_logger`, `indicatif` and `rayon`, measured rather than assumed — is\n  behind a `cli` feature that is on by default. `cargo add oinkie\n  --no-default-features` builds 62 crates where the default builds 79; `regex`\n  and the `crossbeam` family go with them.\n\n  `clap` is not among them: `Ir` and `Algorithm` derive `ValueEnum`, so the\n  library needs it too. It is the largest of the CLI's dependencies, which is\n  why splitting the crate in two would not have bought much more than this.\n\n  Nothing is published yet. `publish.yaml` gained a `crates-io` job, last in\n  the graph because `cargo publish` cannot be undone, and it skips until\n  `CARGO_REGISTRY_TOKEN` exists. The first upload is a decision rather than a\n  side effect of merging.\n\n- **A container image that is already the MCP server**, and one fewer name for\n  the one that lifts.\n\n  `quay.io/tama5/oinkie:mcp` is the light image with `mcp --root /work` as its\n  default command, so a client's configuration ends at the image name. The\n  binary is identical — `--features mcp` has always been passed once for every\n  image — and so is the size, since it adds no layer.\n\n  There is no Ghidra-plus-MCP image and there will not be one: the MCP server\n  exposes no lift tool, so it would carry a gigabyte nothing in it can reach.\n  `ghidra` is both the CLI and the server already.\n\n  **The `full` tag is gone; use `ghidra`.** They have always been the same\n  image under two names. A tag that stops being pushed does not disappear — it\n  stays at the last version that had it — so `quay.io/tama5/oinkie:full` and\n  `:0.5.0-full` should be deleted rather than left to look current.\n\n- angr is no longer an advertised lifter. It was declared early, ahead of any\n  work on it, and stayed a name in a list whose only reachable outcome was an\n  error. It comes back when there is something behind it.\n\n  Removing it also removed the only backend with no installation directory, so\n  `home_spec` stops returning an `Option` and `find_home` loses the branch that\n  said `--home` means nothing here. Neither could be reached once angr was the\n  only case, and an unreachable error path is one nothing keeps honest.\n- The install page names the JDK Ghidra actually needs. It said \"typically JDK\n  17 or later\"; Ghidra 12 refuses 17, and the repository's other two statements\n  of the requirement — the lifter's README and the `full` container image —\n  already said 21. Following the install page led to a failure whose message,\n  `Unable to prompt user for JDK path, no TTY detected`, says nothing about\n  versions at all, so the page now names that symptom too.\n- The publication list in the README runs newest first, the way the site's\n  Academic page already did, and both gain a submission to the International\n  Journal of Software Innovation on how statically linked libraries distort\n  cross-platform birthmark similarity.\n\n## What's Changed\n* docs: name the JDK Ghidra actually needs by @tamada in https://github.com/tamada/oinkie/pull/108\n* refactor: drop angr from the advertised lifters by @tamada in https://github.com/tamada/oinkie/pull/109\n* docs: order the publications newest first, and add a submission by @tamada in https://github.com/tamada/oinkie/pull/110\n* refactor: make the representation the only thing lift is asked for by @tamada in https://github.com/tamada/oinkie/pull/112\n* feat: lift with Binary Ninja, at all three of its levels by @tamada in https://github.com/tamada/oinkie/pull/113\n* fix: refuse an fc-* birthmark that would be empty, whichever way by @tamada in https://github.com/tamada/oinkie/pull/117\n* feat: lift with IDA Pro, at every maturity of the microcode by @tamada in https://github.com/tamada/oinkie/pull/116\n* ci: check the internal links on every push, the external ones weekly by @tamada in https://github.com/tamada/oinkie/pull/118\n* feat: make oinkie publishable, as a library and as a binary by @tamada in https://github.com/tamada/oinkie/pull/119\n* feat: ship an image that is already the MCP server, and drop the `full` name by @tamada in https://github.com/tamada/oinkie/pull/120\n* Release v0.6.0 by @tamada in https://github.com/tamada/oinkie/pull/121\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.5.0...v0.6.0\n",
        "name": "Release v0.6.0",
        "release_date": "2026-09-29T08:35:40Z"
      },
      {
        "description": "## Upgrading from v0.4.0\n\n**Re-lift anything whose symbols could hold a quote, a backslash or a control\ncharacter.** The lifting script escaped none of them (#77), so files produced by\nv0.4.0 and earlier fall into two cases.\n\nA name holding a `\"` produced a file that cannot be parsed at all. If you have\nsuch a file, `extract` has been refusing it with a JSON error naming a line and\ncolumn; lifting it again is the fix, and any copy repaired by hand can be\ndiscarded.\n\nA name holding a `\\` is the case worth knowing about, because nothing announced\nit. The file parses. A name written as `\\t` is read back as a name holding a\ntab, so the birthmark was extracted from a name the program does not have, and\nthe similarity it took part in was computed against the wrong string. `fc-*`\nbirthmarks are built from symbol names and are affected directly; the `op-*`\nfamilies do not use names and are not.\n\nNothing else about the format changed, so files without such names are\nunaffected and need not be regenerated. A lift of the same binary before and\nafter this release is byte-identical unless one of these characters appears.\n\n## Highlights\n\n- **#77** — `lift` escapes the names it writes into the intermediate JSON.\n  Nine values reached the file unescaped, so a C++ user-defined literal\n  operator — which Ghidra demangles as `operator\"\"__km` — produced a file\n  `extract` could not read, while `lift` itself exited 0 and reported success.\n  Escaping is now done by a helper that returns a complete JSON string\n  literal, quotes included, so a site that forgets to call it breaks on every\n  input rather than only on a rare one. The writer also stopped swallowing\n  `IOException`, which had let a failed write leave a truncated file behind\n  while `analyzeHeadless` still exited successfully\n- **#85** — the lifting script is compiled on every push. Ghidra compiles it at\n  lift time, so an error in it was never a build failure — it was a lift that\n  produced nothing, reported as a missing output because `analyzeHeadless`\n  exits 0 whether or not its script compiled, with the compiler's diagnostic\n  buried in the captured process output. `assets/lifters/ghidra/compile-check.sh`, also\n  reachable as `just compile-check`, does the same check locally in about two\n  seconds instead of a Ghidra round trip\n- **#84** — the lifter's README states what a `--script` replacement has to\n  satisfy. Four things, two of which are not guessable: the file must be named\n  after its public class, and it must escape the names it embeds. It also says\n  plainly that `--script` is not a sandbox — the script runs inside Ghidra with\n  everything Ghidra can reach\n- **#83** — `lift` reads back what it wrote before calling the lift a success.\n  A lifting script can write bytes, return normally, and leave behind a file\n  nothing can parse; `analyzeHeadless` exits 0 either way, so that used to be a\n  successful lift and the failure surfaced at `extract`, as a parse error\n  naming a line and column in a file the reader had never seen. The check sits\n  in `LifterBuilder::build`, so it covers a script supplied with `--script` —\n  arbitrary Java that oinkie never inspects — and any lifter added later,\n  without its author having to remember. The error names the binary that was\n  lifted, not only the JSON. Related: `lift` and `extract` no longer print\n  \"completed\" on a run that failed\n- **#78** — `oinkie mcp` serves oinkie over the Model Context Protocol, on\n  stdin and stdout, so an agent can drive the birthmark pipeline directly. This\n  release carries the scaffold and one tool, `oinkie_info`, which reports the\n  birthmark types, the algorithms and the shapes they operate on, every\n  canonical `{birthmark}-{algorithm}` analysis name, the pairing strategies and\n  the aggregators — all generated from the same code that parses those names,\n  so the list cannot drift from what is accepted. Lifting is deliberately not\n  exposed: it runs a whole decompiler process per binary, and a replacement\n  script is arbitrary code. The subcommand is behind the `mcp` cargo feature,\n  as `gencomp` is, and the released binaries are built with it\n- **#80** — the MCP server confines what it will touch. `oinkie mcp --root\n  DIRECTORY`, repeatable and defaulting to the working directory, bounds every\n  path a tool is handed, input and output alike; anything resolving outside is\n  refused with a message naming the directories that are allowed. The paths a\n  tool receives are written by a language model rather than by the person\n  running it, which the rest of oinkie was never built to assume. A `..`\n  component is refused outright rather than resolved, and the existing part of\n  a path is canonicalized, so a symlink pointing out of a root does not get\n  through either. `oinkie_reaggregate` arrives with it: it recomputes the score\n  for every pair in a directory of element-wise CSVs under a different\n  aggregator, without comparing anything again\n- **#79** — the MCP server compares. `oinkie_run` takes lifted programs and\n  reports how similar each pair is, which is the whole question in one call;\n  `oinkie_extract` writes birthmarks and `oinkie_compare` scores them, for\n  trying several algorithms without re-reading the programs. Each takes its\n  vocabulary as names `oinkie_info` lists, and a name the library refuses comes\n  back in the library's own words — `op-seq-euclidean` is answered with \"use\n  op-freq-euclidean\" rather than a generic rejection. A destination directory\n  is optional and, when given, is written in the same shape `oinkie run -d`\n  produces, so it can be handed straight back to `oinkie_reaggregate`. Work\n  runs off the async runtime, and a request for more than 500 pairs is refused\n  before anything is read rather than after — `all-and-self` is quadratic, and\n  a model handed a directory will pass all of it\n- **#81** — the MCP server is documented. `cli/mcp/README.md` covers\n  configuring a client, the five tools, `--root`, and why lifting is not among\n  them; the same is on the site under Usage, and `cli/README.md` carries the\n  subcommand's own help. Both container images are now built with the feature,\n  so either is an MCP server as it stands — point clients at `light`, which has\n  no Ghidra to carry, and remember `-i` without `-t`, since a TTY breaks the\n  JSON-RPC framing\n- The docs say accurately why lifting is not an MCP tool. Two of the three\n  reasons they gave had stopped holding: `analyzeHeadless` exiting 0 on a failed\n  script is no longer an obstacle now that #83 makes `lift` read its output\n  back, and \"minutes at a time\" was asserted rather than measured — a\n  hello-world lifts in about six seconds. The decision itself is unchanged, and\n  now reads as settled rather than pending: lifting stays out, and you run\n  `oinkie lift` yourself, on a host with Ghidra or in the `full` image. Nor does\n  it any longer imply a model must be kept away from lifting — one with a shell\n  can run the CLI like anyone else. The link to the lifter's README, left\n  pointing at `lifter/` when #91 moved it, is fixed\n- The product is described the same way everywhere. `Cargo.toml`, both\n  container labels, the README, the site and its metadata carried three\n  different wordings, one of which — \"Birthmarking toolkit for Ghidra P-Code\" —\n  named the only lifter there is as though it were the point. All of them now\n  open with what the repository's own description already said: *detects\n  software theft by comparing birthmarks extracted from binaries*. Where there\n  is room, a second sentence says which lifter exists today\n- `lift.sh` says it is superseded by `oinkie lift`. It is kept for reference\n  and had been introduced as though it were the way to lift, while assuming a\n  working directory, a Homebrew path to Ghidra, and the word `executables`\n  somewhere in the input's path\n- Both container images build again, and something checks that they can. #91\n  moved `lifter/` under `assets/` and the Containerfiles went on copying\n  `lifter`, which no longer existed — a break that sat on `main` unnoticed\n  because nothing builds an image until a release, by which point the tag\n  already exists. CI builds the light image on every push now and asks the\n  result to serve MCP over `docker run -i`, which is how the documentation says\n  to run it. One image on one architecture covers both: the two Containerfiles\n  copy the same paths and share their whole builder stage\n- The released binaries are asked whether they actually serve MCP.\n  `publish.yaml` passed `--features mcp` and nothing looked at the result, so a\n  flag lost in an edit would have shipped an oinkie with no `mcp` subcommand\n  and the first sign of it would have been a client failing to start.\n  `.github/scripts/verify_mcp.sh` speaks the protocol to the artifact — a\n  subcommand that exists but does not work would pass a `--help` grep\n- The version bump no longer breaks the release it is preparing.\n  `update_version.sh` rewrote `Cargo.toml` and left `Cargo.lock`, which names\n  this package's version too — and the container builds with `cargo build\n  --locked`, which exits rather than reconciling the two. The release flow ran\n  the script, committed, and then tried to build an image that could not build.\n  It never bit before because v0.4.0's bump was done by hand, with cargo run at\n  some point along the way; the script path had never been exercised against\n  `--locked`. `cargo update --workspace` now follows the rewrite, and\n  moves exactly the one line that names this package\n\n## What's Changed\n* fix: escape the names the lifting script writes into JSON by @tamada in https://github.com/tamada/oinkie/pull/82\n* feat: compile the lifting script, and say what a replacement must satisfy by @tamada in https://github.com/tamada/oinkie/pull/86\n* feat: read the lifted file back before calling the lift a success by @tamada in https://github.com/tamada/oinkie/pull/87\n* feat: serve oinkie over MCP, with the tool that says what it accepts by @tamada in https://github.com/tamada/oinkie/pull/88\n* feat: confine what the MCP server will touch, and reaggregate over it by @tamada in https://github.com/tamada/oinkie/pull/89\n* feat: compare over MCP -- run, extract and compare by @tamada in https://github.com/tamada/oinkie/pull/90\n* docs: say how to serve oinkie to an agent, and build both images with it by @tamada in https://github.com/tamada/oinkie/pull/92\n* refactor: move the lifter and the analysis scripts under assets by @tamada in https://github.com/tamada/oinkie/pull/93\n* docs: describe the product the same way everywhere, and retire lift.sh by @tamada in https://github.com/tamada/oinkie/pull/95\n* fix: build the images again, and check the things nothing was checking by @tamada in https://github.com/tamada/oinkie/pull/97\n* refactor: one Containerfile with two runtime stages by @tamada in https://github.com/tamada/oinkie/pull/99\n* fix: sync Cargo.lock when the version is bumped by @tamada in https://github.com/tamada/oinkie/pull/101\n* fix: drop --offline, which made the version bump fail on a fresh runner by @tamada in https://github.com/tamada/oinkie/pull/103\n* chore: update version to 0.5.0 by @tamada in https://github.com/tamada/oinkie/pull/104\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.4.0...v0.5.0\n",
        "name": "Release v0.5.0",
        "release_date": "2026-09-06T09:29:47Z"
      },
      {
        "description": "## Upgrading from v0.3.0\n\nTwo changes in this release alter files that oinkie generates. Neither affects\nsimilarity scores, but both matter if you keep a working directory between runs.\n\n**The `-B/--binary-type` flag is gone.** `oinkie extract` accepted it but never\nread it, so passing `-B llvm` silently did Ghidra parsing anyway. Scripts that\npass the flag will now fail with an unknown-argument error; remove it. Backend\nselection lives on `oinkie lift --lifter-type`, which is the option that has\nalways actually chosen a backend.\n\n**Birthmark file names have changed again.** `oinkie extract` derives the output\nname as `{file_stem}_{hash}.json`, and the hash prefix widened from 8 to 16 hex\ncharacters (#30) so that the disambiguator carries 64 bits instead of 32. At 32\nbits a collision became likely at roughly 77,000 files, and a collision was\nsilent — one binary's birthmark simply overwrote another's.\n\nNames from v0.3.0 therefore do not match those from v0.4.0. On the first run\nwith `--skip`, every input is re-extracted. That is expected, not an error.\nOlder files remain valid inputs to `compare` and `reaggregate`; the only cost of\nkeeping them is that the same binary ends up present under two names.\n\n**Similarity CSVs record the pair in the correct order.** The `left` record used\nto hold the *second* birthmark and `right` the first (#29), so a `--skip` rerun\nread the pair back in the opposite order from a fresh run. The score was never\naffected — the metrics are symmetric — but `results.csv` could not be relied on\nto say which file was on which side.\n\nOnly the writer changed. Similarity CSVs written by v0.3.0 or earlier still\ncarry the old ordering and will keep being read reversed, and mixing old and new\nfiles in one directory means the interpretation varies per file. Regenerate the\ndirectory to get consistent results.\n\n**Shell completions moved.** They are now generated under `assets/completions`,\none directory per shell, with conventional file names (`oinkie`, `oinkie.elv`,\n`oinkie.fish`, `oinkie.ps1`, `_oinkie`). Packaging scripts that referenced the\nold `completions` directory need updating.\n\n**`fc-*` analyses produced a false positive.** The symbol lookup compared the\ncall operand `\"(ram, 0x100000480, 8)\"` against a symbol table keyed\n`\"0x100000480\"`, so every call was discarded and every `fc-*` birthmark came out\nempty (#40). Two empty sets are identical, so any two programs compared as a\nperfect match through `fc-freq-cosine`, `fc-set-dice`, `fc-freq-euclidean`,\n`fc-set-jaccard`, `fc-seq-levenshtein`, `fc-seq-lcs`, `fc-set-simpson` and\n`fc-freq-weightedjaccard`.\n\nAny conclusion drawn from an `fc-*` comparison on v0.3.0 or earlier should be\ntreated as unreliable and the comparison re-run. Birthmark files extracted with\n`fc-*` are empty and carry no information; regenerate them.\n\n**Extracting an `fc-*` birthmark from a program that calls nothing is now an\nerror** (#44). It used to succeed and write an empty birthmark, which is the\nshape of the bug above: empty birthmarks compare as a perfect match, so the\noutput was worse than no output. `oinkie extract -b fc-set` on such a program\nnow stops with\n\n```\nbin/sample: no operation is a call, so every fc-* birthmark of it would be\nempty -- and two empty birthmarks score as a perfect match. Either the program\nreally calls nothing, or oinkie's reader for ghidra-pcode does not recognise\nthat representation's call operations\n```\n\nThe `op-*` families are unaffected, and so is any program that makes at least\none call — which, for a lifted binary, means every realistic input.\n\n**k-grams are named the same way everywhere, and it is the library's way**\n(#25). The command line had invented two spellings of its own, neither of them\nthe one the documentation and the file format use:\n\n| | v0.3.0 | v0.4.0 |\n| --- | --- | --- |\n| `extract -b` | `op-tri-gram-set` | `op-3gram-set` |\n| `run -a` | `op3gram-set-dice` | `op-3gram-set-dice` |\n\nBoth came from hand-written `ValueEnum`s whose command-line names were derived\nfrom Rust identifiers, so `OpTriGramSet` became `op-tri-gram-set` and\n`Op3gramSetDice` became `op3gram-set-dice`. The library has always parsed\n`op-3gram-set`, and `oinkie info` has always *printed* `op-3gram-set` — which\n`-b` then refused. Scripts passing a k-gram to `-b` or `-a` need updating;\n`fc-*`, `op-seq`, `op-set`, `op-freq` and their analyses are unchanged.\n\nTwo things follow from deleting the hand-written lists. **Any k now works**:\n`--analysis op-12gram-set-jaccard` and `-b op-12gram-set` run, where `-a`\nstopped at k=6. And **`-b` and `-a` now agree on what exists** — `-b` reached\nk=8 while `-a` reached k=6, so three k-gram families could be extracted but not\nanalysed in one step.\n\n`oinkie info` and shell completion list up to k=8. That bound is only what gets\n*suggested*; it is not what parses.\n\n**A half-written similarity file is no longer read as a score of zero.**\n`compare --skip` and `run --skip` reuse the CSVs an earlier run left behind. A\nfile holding the pair but no `result,` line — what an interrupted run leaves —\nwas accepted, and the missing score defaulted to **0.0**: two programs reported\nas having nothing in common, written into `results.csv` beside real scores,\nwith the run exiting successfully.\n\nSuch a file is now refused, naming it:\n\n```\nError: Parse error: Result line not found in similarities/00000.csv\n```\n\nA `--skip` run over a directory containing one used to succeed and will now\nstop. Delete the offending file, or drop `--skip` to recompute. Any\n`results.csv` produced by a `--skip` run over an interrupted directory should\nbe treated as unreliable in the same way as the `fc-*` scores above: a zero\nthere is indistinguishable from a real one.\n\n`reaggregate` is unaffected. It recomputes the score from the stored matrix\nrather than reading it, and a truncated file has no matrix either — which it\nalready refuses.\n\n**A usage error is no longer announced twice.** clap's own message begins\n`error: `, and oinkie printed it behind `Error: `, so a mistyped flag came back\nas `Error: error: unexpected argument ...` (#62). The prefix is gone from that\npath. oinkie's own errors carry no prefix of their own and keep theirs:\n\n```\n$ oinkie run --bogus-flag\nerror: unexpected argument '--bogus-flag' found\n\n$ oinkie extract no-such-file.json\nError: IO error for no-such-file.json: No such file or directory (os error 2)\n```\n\nScripts matching on the old `Error: error:` text need updating. The exit codes\nare unchanged: 1 for a usage error, 2 for anything else.\n\n**`op-*gram-freq` birthmarks can be extracted at all.** Every non-empty one\nfailed at write time with\n\n```\nError: hello_clang_3380b9841726541e.json: JSON error: key must be a string\n```\n\nA k-gram is a list of operations and a JSON object's keys are strings, so\n`serde_json` refused the map outright (#59). `op-1gram-freq` through\n`op-8gram-freq` — **eight of the thirty birthmark types** the CLI advertises,\nand one of the three shapes a k-gram comes in — could not produce a file, and\nwith them **twenty-four of its eighty analyses** could not be reached through\n`extract` + `compare`.\n\nIt looked as though only small k were affected. It was not: a program with\nfewer than k operations yields an empty map, and an empty map has no key to\nrefuse. Any k fails on any program long enough to produce a k-gram.\n\nThe frequency map is now written as a list of `[kgram, count]` pairs, sorted,\nso extracting the same program twice writes the same bytes:\n\n```json\n{\"KgramFreq\": [[[\"CALL\", \"COPY\"], 1], [[\"COPY\", \"COPY\"], 1]]}\n```\n\n**Birthmark files are written in a canonical order.** A map and a set have no\norder of their own, and oinkie wrote them in hash order — which comes from the\ninsertion history and the hasher rather than from what the birthmark holds\n(#68). Two equal birthmarks could be written differently, and a `rustc-hash`\nbump would relay every file for no reason.\n\n`Freq`, `Set` and `KgramSet` are sorted on the way out now, joining `KgramFreq`.\n`Seq` and `KgramSeq` are untouched and must be: their order is the program's,\nand sorting them would not canonicalise the file but destroy the birthmark.\n\nNothing about any file's meaning changes, every existing file still reads, and\nscores are unaffected — checked against a build of the previous commit across\nnine families. What changes is the byte layout of newly written files, so a\nchecksum taken over a birthmark from an earlier version will not match one\ntaken now, and a `--skip` rerun still skips because it goes by the file\nexisting rather than by its contents.\n\n**A frequency stated more than once is refused, in both shapes.** `op-freq`\nand `fc-freq` used serde's derived map deserializer, which inserts in a loop,\nso a birthmark saying `{\"COPY\": 3, \"COPY\": 5}` loaded as 5 and was scored with\nnothing said (#66). It is refused now, naming the operation:\n\n```\nCOPY: listed more than once; a birthmark names each operation once\n```\n\nEvery repeat is refused, including `{\"COPY\": 3, \"COPY\": 3}`, whose counts\nagree: a repeated key is a malformed object however the values fall, and\nreading it would mean accepting a broken file whenever it was broken\nconsistently.\n\nOnly the frequency shapes changed. A `Set` repeating an element denotes the\nsame set and a `Seq` repeating one is a sequence doing its job — neither can\nmean two things, and both still load. Nothing oinkie writes can produce a\nrepeat in any of them, since every map and set is serialized from a map or a\nset, so no file it wrote is affected.\n\nA k-gram listed more than once is refused rather than resolved. The list stands for a\nmap, so collecting it would let the last pair win: a file saying a k-gram\noccurred 3 times and again 5 times would load as 5 and be scored, with nothing\nsaid. Nothing oinkie writes can produce one, since the pairs come from a map.\n\nNo existing file changes. `op-*gram-seq` and `op-*gram-set` already wrote a\nk-gram as a plain list and still do — which is why the map was changed rather\nthan the k-gram, since making `Kgram` itself serialize as a string would have\nrewritten the two families that work. `run -a op-Kgram-freq-*` was never\naffected: it compares in memory and never writes a birthmark, which is why the\nwhole thing went unnoticed.\n\n**`compare` stopped making one syscall per byte.** `Birthmark::try_from` and\n`Program::try_from` handed `serde_json::from_reader` a bare `std::fs::File`.\nserde_json's `IoRead` takes one byte at a time, so an unbuffered file meant one\n`read` syscall per byte of JSON (#51). Loading a 9 MB birthmark took **2.7 s**;\nit now takes 15 ms.\n\n`compare` loads its inputs once per *pair*, so the loader runs O(n²) times.\nOver three pairs of 9 MB birthmarks the whole command went from **22.0 s to\n7.8 s**, and the kernel time in it from **37.5 s to 0.08 s** — the parsing was\nnever the cost. Output is byte-identical.\n\nNothing to change on your side, and **`compare` is the only subcommand\naffected**. `extract` and `run` read their programs through `AnyProgram::load`,\nwhich has read whole files since #45, and neither of them loads a birthmark\nfrom disk. The other half of the fix is for library callers: `Program::try_from`\nand `Birthmark::try_from` were both slow, and both are public.\n\n**An analysis name built from what `oinkie info` prints now parses.** `info`\nlists the algorithms as `weighted-jaccard`, but `run --analysis` only accepted\n`op-freq-weightedjaccard` — a name assembled from what the tool itself showed\nyou was refused (#71). Both spellings work now, and so does\n`compare --algorithm weighted-jaccard`, which was always the only one it took.\n\nThe other seven algorithms are single words and were already spelled the same\nin both places.\n\nThe two halves of an analysis name are no longer split on the last hyphen.\nEach hyphen is tried, rightmost first, and the split whose tail names an\nalgorithm and whose head is a birthmark type wins — so an algorithm added\nlater may be two words without breaking anything. Every name that parsed\nbefore still parses, and a name whose birthmark half is wrong still reports\nthat half:\n\n```\nop-nonsense-jaccard  ->  op-nonsense: unknown birthmark type\n```\n\n## Library API\n\nThese affect code using oinkie as a crate. The command-line interface is\nunchanged by them.\n\n**`AnalysisType::new` now returns `Result`.** It validates that the algorithm\noperates on the birthmark's shape, which `try_from` already did and `new` did\nnot — so the check could be walked around by constructing the pair directly.\nCallers add a `?` or handle `Error::IncompatibleAnalysis`, a new variant.\n\n**Analysis names that pair an algorithm with the wrong shape are rejected.**\nEach algorithm computes over one representation and converts anything else\nfirst, so `op-seq-euclidean` returned exactly what `op-freq-euclidean` returns\nwhile its name promised a comparison over sequences, and `op-set-levenshtein`\nscored 0.0 because no conversion exists. The error names the pairing that was\nmeant:\n\n```\nop-seq-euclidean: euclidean operates on frequency vectors; use op-freq-euclidean\n```\n\nIn practice this only changes k-gram names: `op-3gram-set-levenshtein` and its\nkind used to parse and now do not. Every pairing the CLI offers is canonical, so\nnothing reachable through `--analysis` is affected.\n\n**More names parse than before.** `AnalysisType::try_from` reads the algorithm\nand hands the rest to `BirthmarkType::try_from` (#38), so the eight `fc-*`\ncombinations and the seven `*-lcs` ones now work, as does any `k` — previously\nfifteen of the sixty-four combinations the CLI offers could not be parsed at all.\n\n**`Program::new` takes the representation.** A lifted program now records\nwhich intermediate representation its operations are written in, and the\nconstructor requires it rather than assuming one. Code building a `Program`\ndirectly needs the extra argument; reading one from JSON does not, since the\nfield is defaulted for files written before it existed.\n\nThe argument is required rather than defaulted on purpose. A constructor that\nfilled it in would let a program lifted by one tool be labelled as another's,\nwhich is the confusion the field exists to prevent.\n\n**Birthmarks record the representation they were extracted from, and a\ncomparison across two of them is refused.** A birthmark is only meaningful\nagainst another built the same way, and two lifters describe the same\ninstruction with different operations, so comparing across them would measure\nthe disagreement between the tools rather than anything about the programs\n(#42, #43). Only Ghidra exists today, so nothing currently reachable is\nrefused.\n\n`Metadata` gains an `ir` field, and the `birthmark` record in a similarity CSV\ngains a seventh column for it. The column is appended rather than inserted, so\nthe positions earlier readers depend on are unchanged, and a six-column record\nwritten by an earlier version still parses.\n\n**`Op` gains a required method, `symbol_key`.** Any crate implementing `Op` for\nits own lifter must add it. It renders the operation's first operand as the\nprogram's symbol table keys it, so a call target can be resolved; returning\n`None` when the operand cannot name a symbol is what keeps an unresolvable\ntarget distinguishable from a lookup that found nothing. Ghidra's\nimplementation parses the operand through the existing `Value` type.\n\n**`Op` gains a second required method, `is_call`.** Which operation is a call\nwas decided in the extractor by comparing the mnemonic against the literal\n`\"CALL\"`, which is P-Code's spelling; the Hex-Rays microcode and Binary Ninja's\nLLIL name it differently and some name more than one operation, so any other\nlifter would have matched nothing (#44). The question now belongs to the\noperation, and so to the lifter that defines it.\n\nThe method has no default on purpose. A `false` default would let a new lifter\ncompile while recognising no call at all, which produces empty `fc-*`\nbirthmarks rather than an error — and empty birthmarks score as a perfect\nmatch. Requiring it makes that a build failure. Ghidra answers for `CALL` and\n`CALLIND`, but not `CALLOTHER`, which indexes a table of user-defined\noperations rather than naming a function. Nothing changes in what Ghidra\nextracts today: `CALLIND` reaches its target through a register, so no symbol\nresolves for it.\n\n**`Op` loses `code`.** It returned the numeric opcode and had no callers: the\nbirthmark is built from `mnemonic`, a string. Keeping it would have made every\nfuture lifter invent a numbering for a value nothing reads (#45).\n\n**Which operation type reads a lifted file is now decided by the file.**\n`Program<T>` was generic, but nothing chose `T` — every caller wrote\n`Program<ghidra::Op>`, so a file lifted by another tool would have been read\nagainst P-Code's vocabulary. `AnyProgram::load` reads the `ir` field first and\npicks the reader from it, and `Extractor::extract_any` and\n`Comparator::compare_any` take what it returns (#45).\n\nA representation with no reader is now refused by name:\n\n```\nforeign.json: no reader for ida-microcode; this build can read ghidra-pcode\n```\n\nRead as P-Code it would have failed anyway, since the opcode enum is closed —\nbut on whichever foreign opcode it happened to meet first, reported as an\nunknown variant among seventy-five alternatives. That check is unchanged for\nfiles that really are Ghidra's; an unknown opcode is still an error.\n\n`Program::new` and the typed `Extractor::extract_each` and\n`Comparator::compare_programs` are unchanged, so code that knows its operation\ntype keeps working.\n\n**`lift::find_ghidra_home` is gone; use `LifterType::find_home`.** Discovery was\na Ghidra function re-exported as if it were the general one. It is now a method\non the backend, so each names its own environment variable and its own default\nlocations, and the error says which variable to set rather than naming\n`GHIDRA_HOME` for whatever tool was asked for (#46). `LifterType` also gains\n`name` and `home_spec`; `home_spec` is `None` for angr, which is imported as a\nPython library and has no installation directory to point at.\n\n**New public items** for asking whether a pairing is valid before building one:\n`Shape`, `BirthmarkType::shape`, `BirthmarkType::with_shape`,\n`BirthmarkType::pairs_with` and `Algorithm::shape`.\n\n**`Error` reports its cause.** `impl std::error::Error for Error` was empty, so\n`source()` returned `None` even for the seven variants holding a wrapped error\n(#62). Each now returns it, and an error chain has the depth it should. Nothing\ncalled `source()` before, because there was nothing to get.\n\n`Error` is derived with `thiserror` rather than hand-writing `Display`, so the\nmessage sits on the variant it belongs to. Every variant renders exactly as it\ndid, with one deliberate exception: `Error::Clap` no longer prefixes clap's\nmessage with `Clap error: `, since clap's already begins `error: `. No `#[from]`\nimpls were added, so nothing converts implicitly through `?` that did not\nbefore.\n\n**New public items for listing the vocabulary rather than hand-writing it**:\n`BirthmarkType::advertised`, `BirthmarkType::description`,\n`AnalysisType::advertised_names` and `MAX_ADVERTISED_K`; `Algorithm::cli_name`\nbecame public. `advertised_names` pairs each birthmark with the algorithms that\noperate on its shape, so a generated list cannot offer a pairing that\n`AnalysisType::new` refuses. These exist for building a completion list, and\nnone of them bounds what `try_from` accepts.\n\n**`oinkie lift` now lifts one file at a time.** It used to lift every input\nconcurrently with no bound. Pass `-j/--jobs N` to get the old behaviour, with N\nnaming how many files may be lifted at once (#55).\n\nLifting runs a whole decompiler process per file, and every input went to\nrayon's global pool, whose size is the machine's core count -- ten JVMs at once\non a ten-core laptop, with no way to ask for fewer. It is also unsafe on a fresh\ninstallation: Ghidra compiles its SLEIGH language definitions on first use and\ncaches them **inside its own installation**, so parallel lifts race to write the\nsame file and the loser reads a half-written one. `analyzeHeadless` exits\nsuccessfully regardless, so this arrives as a missing output rather than an\nerror (#54). Once the cache is built it cannot happen again, which is why `-j`\nprints a notice rather than being refused:\n\n```\nnotice: lifting up to 4 files at a time.\nGhidra builds its language cache on first use, inside its own installation,\nand parallel lifts can corrupt a cache that has not been built yet --\nreported as a missing output, not as an error.\nIf Ghidra was installed recently, lift one file without -j first.\n```\n\nScripts that lift more than one file and depend on the old speed need `-j`\nadded. `extract` and `compare` are unchanged and remain parallel: they are\ncomputation over local files, with no shared external state and no separate\nprocess per input.\n\n**`oinkie lift --intermediate` is the lifter's working directory.** It was\ndescribed as somewhere to keep \"Ghidra project directories\". Every headless\nlifter needs a directory to run in, because that is where its script writes its\nJSON; Ghidra additionally keeps its project there. The option, the flag and the\nbehaviour are unchanged — only what it is documented to mean, which now holds\nfor a lifter that is not Ghidra's.\n\n`--home` and `--script` keep one help string each. They mean the same thing for\nevery backend — an installation directory, and a script in whatever language\nthat backend runs — and the part that has to be per-lifter is the error, which\nnow is.\n\n## Highlights\n\n- **#30** — widened the birthmark hash prefix to 64 bits, removing a silent\n  overwrite that was reachable at corpus scale\n- **#29** — fixed the `left`/`right` inversion in similarity CSVs\n- **#40** — `fc-*` birthmarks resolve their symbols again; they were empty, so\n  unrelated programs scored a perfect match through all eight of them\n- **#36** — `lift -i` with a relative path no longer resolves it twice into\n  `irs/irs`, and the directory is created rather than having to exist\n- **#38** — analysis names are parsed in one place, and a pairing that cannot\n  mean what it says is rejected with the canonical form named\n- **#44** — which operation is a call is now the lifter's answer rather than a\n  literal `\"CALL\"` in the extractor, and a program in which nothing is a call is\n  refused instead of yielding an empty `fc-*` birthmark\n- **#45** — the lifted file decides which operation type reads it, so the\n  pipeline no longer assumes P-Code; a representation with no reader is refused\n  by name\n- **#54** — the tests that run a real Ghidra no longer run at the same time as\n  each other, so they cannot corrupt its language cache; two of them that\n  differed only in their `-i` path are now one\n- **#71** — an analysis name built from what `oinkie info` prints parses;\n  the split between the two halves is searched for rather than assumed to be\n  the last hyphen, so `weighted-jaccard` works inside a name and a two-word\n  algorithm added later needs no special care\n- **LLVM IR/BC is no longer named as a target.** The tagline and the site\n  described oinkie as a toolkit for \"Ghidra Pcode, LLVM IR/BC, and Binary\n  Ninja\", but LLVM is in neither `Ir` nor `LifterType` and is not in the\n  multi-lifter design work, which is about Binary Ninja and IDA Pro. The\n  documentation says what is there and what is planned\n- **The publication list is current, and the same in both places.** Three\n  papers are added to the README's references, and the two that\n  `docs/content/academic.md` was missing are added there with a description of\n  what each contributes\n- **The release script no longer rewrites text that merely contains the old\n  version.** It replaced the bare number everywhere in `README.md` and the\n  landing page, so cutting 0.3.0 to 0.4.0 would have turned a cited paper's\n  NII CRID `1572824500.3.007232` into `...500.4.007232` — silently, in the\n  released README (#75). Each replacement is anchored to the badge or link it\n  belongs to\n- **The CLI reference matches the CLI.** `cli/README.md` documented\n  `-B/--binary-type`, which this release removes, and was missing `lift -j`\n  and `run -A`; `lift`'s lifter list still read `ghidra, llvm, binary-ninja`\n  where it is `ghidra, angr, ida-pro, binary-ninja`; and the `oinkie info`\n  sample in the site docs was from v0.2.0 and showed a section the command\n  does not print. All of it is regenerated from the real output\n- **#68** — birthmark files are written in a canonical order, so the bytes are\n  a function of what the birthmark holds rather than of hash order; sequences\n  keep the program's order, which is theirs to keep\n- **#66** — a birthmark stating one operation's frequency twice is refused\n  rather than silently read as the last of them, in `op-freq` and `fc-freq` as\n  well as the k-gram shapes\n- **#59** — `op-*gram-freq` birthmarks can be written. Every non-empty one\n  failed with \"key must be a string\", so eight of the thirty birthmark types\n  the CLI advertises could not produce a file and twenty-four of its eighty\n  analyses were unreachable; the frequency map is a list of `[kgram, count]`\n  pairs now, and no existing file changes\n- **#51** — birthmark and program files are read whole and parsed from the\n  bytes, rather than through `serde_json::from_reader` on an unbuffered\n  `File`, which cost one syscall per byte. A 9 MB birthmark loads in 15 ms\n  where it took 2.7 s, and a `compare` over three such pairs went from 22.0 s\n  to 7.8 s\n- **`--skip` no longer reads a half-written similarity file as a score of\n  zero.** Found by covering the parser it lives in: the score defaulted when\n  the `result,` line was absent, so an interrupted run's leftovers came back\n  as a successful comparison finding nothing in common\n\n- **#62** — `Error` is derived rather than hand-written, and reports its\n  cause: `source()` was `None` for every variant, including the seven wrapping\n  a foreign error. A usage error is also no longer printed as\n  `Error: error: ...`\n- **#28** — line coverage is **95.3%**, up from 91.3%. `Error`'s `Display` is\n  covered for every variant, and the CLI's own parsers — the `--skip` reader\n  for a stored comparison, and everything `reaggregate` loads — are tested\n  against fixture files rather than only through end-to-end runs. Six of the\n  new tests were checked by breaking the code they cover\n- **#25** — the CLI stopped keeping its own copy of the analysis vocabulary.\n  Two hand-written `ValueEnum`s — 64 variants with 64 mapping arms, and 31 more\n  — are gone, and `--analysis` and `--birthmark-type` are parsed by the library\n  that already parsed those names. The k ceiling went with them, the two\n  options now agree with each other and with `oinkie info`, and the completion\n  list is generated from the pairing rules, so it cannot offer a pairing that\n  would be refused\n- **#24** — the tests no longer set `GHIDRA_HOME` and put it back afterwards.\n  Two of them ran at once in the same binary and saw each other's writes, which\n  is what made CI fail on a branch whose own checks were green; in edition 2024\n  `set_var` is `unsafe` precisely because a concurrent read of the environment\n  is undefined behaviour rather than merely a wrong answer. The search now takes\n  its environment, and its test for whether a path exists, as parameters — so a\n  test describes a machine instead of having to become one, and the case where\n  nothing is installed can be asserted for the first time\n- **#55** — `lift` is serial by default, with `-j/--jobs N` to opt back into\n  parallelism; the old default could corrupt Ghidra's language cache on a fresh\n  installation, and the corruption was reported as a missing output\n- **#46** — home discovery is per-backend, and the path handling around a\n  headless lifter — resolving before the working directory changes, running in\n  its own directory, moving the result across file systems — is written once\n  instead of once per tool. A lift that produced nothing now reports what the\n  tool said, since Ghidra exits successfully even when its script throws\n- The crate is now formatted with `cargo fmt`, and CI enforces it with\n  `cargo fmt --all -- --check`\n\n## What's Changed\n* fix: write the left CSV record from the first birthmark, not the second by @tamada in https://github.com/tamada/oinkie/pull/29\n* fix: widen the birthmark file name hash to 64 bits by @tamada in https://github.com/tamada/oinkie/pull/30\n* ci: take release notes from a file kept in the repository by @tamada in https://github.com/tamada/oinkie/pull/32\n* fix: remove the -B/--binary-type flag that was never read by @tamada in https://github.com/tamada/oinkie/pull/34\n* refactor: build completions from a separate gencomp binary by @tamada in https://github.com/tamada/oinkie/pull/35\n* fix: resolve the -i path before handing it to Ghidra by @tamada in https://github.com/tamada/oinkie/pull/37\n* refactor: delegate the birthmark half, and reject pairings that cannot mean what they say by @tamada in https://github.com/tamada/oinkie/pull/39\n* fix: resolve the symbol a call refers to, so fc-* is not empty by @tamada in https://github.com/tamada/oinkie/pull/47\n* feat: record which intermediate representation a lifted file holds by @tamada in https://github.com/tamada/oinkie/pull/48\n* feat: refuse to compare birthmarks from different representations by @tamada in https://github.com/tamada/oinkie/pull/49\n* feat: let the lifter say which operation is a call by @tamada in https://github.com/tamada/oinkie/pull/50\n* feat: let the lifted file choose the type that reads it by @tamada in https://github.com/tamada/oinkie/pull/52\n* refactor: write the headless lifter shape once, not once per tool by @tamada in https://github.com/tamada/oinkie/pull/53\n* feat: lift one file at a time unless asked for more by @tamada in https://github.com/tamada/oinkie/pull/56\n* test: stop the Ghidra tests from running at the same time by @tamada in https://github.com/tamada/oinkie/pull/57\n* test: hand the home search its environment instead of writing to ours by @tamada in https://github.com/tamada/oinkie/pull/58\n* refactor: let the library parse the names it already parses by @tamada in https://github.com/tamada/oinkie/pull/60\n* test: cover the error messages and the CLI's own parsers by @tamada in https://github.com/tamada/oinkie/pull/61\n* refactor: derive Error, and let it report its cause by @tamada in https://github.com/tamada/oinkie/pull/63\n* perf: read a JSON file whole instead of one syscall per byte by @tamada in https://github.com/tamada/oinkie/pull/64\n* fix: write a k-gram frequency as pairs, not as object keys by @tamada in https://github.com/tamada/oinkie/pull/65\n* fix: land #66 and #68, which merged into a dead end by @tamada in https://github.com/tamada/oinkie/pull/70\n* fix: find where an analysis name splits, rather than assume it by @tamada in https://github.com/tamada/oinkie/pull/72\n* docs: regenerate the CLI reference from the CLI by @tamada in https://github.com/tamada/oinkie/pull/73\n* fix: anchor the version replace to what it is replacing by @tamada in https://github.com/tamada/oinkie/pull/76\n* chore: update version to 0.4.0 by @tamada in https://github.com/tamada/oinkie/pull/74\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.3.0...v0.4.0\n",
        "name": "Release v0.4.0",
        "release_date": "2026-09-02T09:16:43Z"
      },
      {
        "description": "## Upgrading from v0.2.1\n\n**Birthmark file names have changed.** `oinkie extract` derives the output name as `{file_stem}_{hash}.json`, and the hash computation was corrected in #12: the previous code re-read the head of the file instead of its tail for inputs between 4KB and 8KB, so binaries differing only after the first 4KB produced the same name and silently overwrote each other.\n\nThe practical consequence is that names generated by v0.2.1 do not match those generated by v0.3.0:\n\n- **Using `--skip`**: existing birthmark files will not be recognised and every input is re-extracted. This is correct behaviour, not an error — but plan for a full extraction pass on the first v0.3.0 run.\n- **Existing birthmark directories**: names from v0.2.1 remain valid inputs to `compare` and `reaggregate`; only newly extracted files use the new scheme. Mixing the two in one directory is safe but leaves duplicates of the same binary under two names. Regenerating the directory is the cleaner option.\n\nNo other command-line interface or file format changed in this release.\n\n## Highlights\n\nSeven correctness and robustness fixes landed via #19, most of which affect results rather than merely tidying code:\n\n- **#12** — the `get_hash` bug above, which could cause one binary's birthmark to overwrite another's\n- **#13** — `lift` collected Ghidra's output from the process working directory, so running it in parallel over same-named binaries raced on a single path; it now runs each lift in its own project directory\n- **#16** — paths and file names were written to the result CSVs unescaped, and read back with a naive `split(',')`. A path containing a comma corrupted the records and was silently misread on `--skip` reruns and during reaggregation\n- **#15** — removed a panic on empty input, rejected `topn:0` (which produced NaN similarities), and stopped `Comparison::store` from discarding write errors, which had let a full disk report success with a truncated CSV\n\nTest coverage rose from 72.5% to 87.3% (#21), and the release pipeline now runs on the shared reusable workflows in `tamada/.github` (#22).\n\n## What's Changed\n* fix(security): correct tail read in get_hash to prevent birthmark file collisions by @tamada in https://github.com/tamada/oinkie/pull/12\n* fix(security): fix output races in CWD, panics, and cross-device rename in the Ghidra lifter by @tamada in https://github.com/tamada/oinkie/pull/13\n* fix: initialize the logger without unsafe environment mutation by @tamada in https://github.com/tamada/oinkie/pull/14\n* fix: harden the compare module against panics, NaN results, and swallowed write errors by @tamada in https://github.com/tamada/oinkie/pull/15\n* fix(security): escape CSV fields consistently and parse result files with the csv crate by @tamada in https://github.com/tamada/oinkie/pull/16\n* refactor: remove unimplemented!() stubs and commented-out code (tech debt) by @tamada in https://github.com/tamada/oinkie/pull/17\n* perf: return borrows from Op trait accessors instead of cloning by @tamada in https://github.com/tamada/oinkie/pull/18\n* Fix/by fable5 by @tamada in https://github.com/tamada/oinkie/pull/19\n* test: raise coverage of birthmarks.rs and compare.rs to ~99% by @tamada in https://github.com/tamada/oinkie/pull/21\n* ci: build the release on tamada/.github reusable workflows by @tamada in https://github.com/tamada/oinkie/pull/22\n* Release v0.3.0 by @tamada in https://github.com/tamada/oinkie/pull/20\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/compare/v0.2.1...v0.3.0\n",
        "name": "Release v0.3.0",
        "release_date": "2026-08-14T01:59:06Z"
      },
      {
        "description": "## What's Changed\n* Improve path handling in CSV outputs by @tamada in https://github.com/tamada/oinkie/pull/6\n* feat: implement lift subcommand by @tamada in https://github.com/tamada/oinkie/pull/8\n* Release: Merge develop2 into main by @tamada in https://github.com/tamada/oinkie/pull/7\n* docs: implement structured Hugo website content by @tamada in https://github.com/tamada/oinkie/pull/9\n* feat: add full and lightweight Dockerfiles for oinkie by @tamada in https://github.com/tamada/oinkie/pull/10\n* Releases/v0.2.1 -- Add shell completions, lock dependencies, and automate publishing workflows by @tamada in https://github.com/tamada/oinkie/pull/11\n\n\n**Full Changelog**: https://github.com/tamada/oinkie/commits/v0.2.1",
        "name": "Release v0.2.1",
        "release_date": "2026-06-25T07:23:45Z"
      }
    ],
    "topics": [
      "birthmark-toolkit",
      "birthmarks",
      "plagiarism-detection",
      "software-theft-detection",
      "ghidra-pcode"
    ]
  },
  {
    "description": "Minimal and alternative ls implementation in Rust.",
    "languages": [
      "Rust",
      "Dockerfile",
      "Just"
    ],
    "last_updated": "2026-07-25T08:06:22Z",
    "license": [
      {
        "name": "Creative Commons Zero v1.0 Universal",
        "spdx": "cc0-1.0",
        "url": "http://choosealicense.com/licenses/cc0-1.0/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/lis.png"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/lis"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/lis/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/lis"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/lis"
      }
    ],
    "name": "lis",
    "owner": "tamada",
    "releases": [
      {
        "description": "## What's Changed\n* Improve tests and docs by @tamada in https://github.com/tamada/lis/pull/14\n* Gemini by @tamada in https://github.com/tamada/lis/pull/13\n* feat: add Containerfile and Justfile for Docker containerization by @tamada in https://github.com/tamada/lis/pull/16\n* feat: add documentation and configuration files for lis project by @tamada in https://github.com/tamada/lis/pull/15\n* Releases/v0.0.11 by @tamada in https://github.com/tamada/lis/pull/17\n\n\n**Full Changelog**: https://github.com/tamada/lis/compare/v0.0.10...v0.0.11",
        "name": "Release v0.0.11",
        "release_date": "2026-06-19T03:22:04Z"
      },
      {
        "description": "## What's Changed\n* Releases/v0.0.10 by @tamada in https://github.com/tamada/lis/pull/12\n\n\n**Full Changelog**: https://github.com/tamada/lis/compare/v0.0.9...v0.0.10",
        "name": "Release v0.0.10",
        "release_date": "2026-05-01T05:47:21Z"
      },
      {
        "description": "## What's Changed\n* Releases/v0.0.9 by @tamada in https://github.com/tamada/lis/pull/11\n\n\n**Full Changelog**: https://github.com/tamada/lis/compare/v0.0.8...v0.0.9",
        "name": "Release v0.0.9",
        "release_date": "2026-05-01T04:30:37Z"
      }
    ],
    "topics": [
      "filesystem",
      "list-files"
    ]
  },
  {
    "description": "A phonetic code encoder/decoder written in Rust",
    "languages": [
      "Shell",
      "Rust",
      "Dockerfile"
    ],
    "last_updated": "2026-06-30T07:12:30Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/spellout.svg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/spellout/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/spellout"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/spellout"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/spellout/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/spellout"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/spellout/"
      }
    ],
    "name": "spellout",
    "owner": "tamada",
    "releases": [
      {
        "description": "## Commits\n- 67c9b83: Add permissions for id-token in publish_to_crates_io job (Haruaki Tamada) [#3](https://github.com/tamada/spellout/pull/3)",
        "name": "Release v0.1.1",
        "release_date": "2026-02-20T04:36:32Z"
      },
      {
        "description": "",
        "name": "Release v0.1.0",
        "release_date": "2026-02-20T01:57:55Z"
      }
    ],
    "topics": [
      "encoder-decoder",
      "phonetic-code"
    ]
  },
  {
    "description": "Git Ignore Managenement System for Multiple Repositories.",
    "languages": [
      "Rust",
      "Dockerfile",
      "Just",
      "HTML"
    ],
    "last_updated": "2026-10-17T02:14:08Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/gixor.svg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/gixor/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/gixor"
      },
      {
        "label": "container",
        "url": "https://quay.io/repository/tama5/gixor"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/gixor/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/gixor"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/gixor/"
      }
    ],
    "name": "gixor",
    "owner": "tamada",
    "releases": [
      {
        "description": "## What's Changed\n* Release v0.5.2 by @tamada in https://github.com/tamada/gixor/pull/52\n\n\n**Full Changelog**: https://github.com/tamada/gixor/compare/v0.5.1...v0.5.2",
        "name": "Release v0.5.2",
        "release_date": "2026-08-08T11:44:53Z"
      },
      {
        "description": "## What's Changed\n* Bring the readme back in step with the command by @tamada in https://github.com/tamada/gixor/pull/49\n* Release v0.5.1 by @tamada in https://github.com/tamada/gixor/pull/50\n\n\n**Full Changelog**: https://github.com/tamada/gixor/compare/v0.5.0...v0.5.1",
        "name": "Release v0.5.1",
        "release_date": "2026-08-08T09:04:16Z"
      },
      {
        "description": "## What's Changed\n* feat: embed build information into binary and update CLI version display by @tamada in https://github.com/tamada/gixor/pull/38\n* refactor: enhance Dockerfile and Justfile for multi-feature builds; i… by @tamada in https://github.com/tamada/gixor/pull/39\n* Fix identified issues and improve stability by @tamada in https://github.com/tamada/gixor/pull/44\n* Drop git2, split the workspace, and carry the boilerplates into the browser by @tamada in https://github.com/tamada/gixor/pull/47\n* Release/v0.4.3 by @tamada in https://github.com/tamada/gixor/pull/41\n* Release v0.5.0 by @tamada in https://github.com/tamada/gixor/pull/48\n\n\n**Full Changelog**: https://github.com/tamada/gixor/compare/v0.4.2...v0.5.0",
        "name": "Release v0.5.0",
        "release_date": "2026-08-08T06:16:42Z"
      },
      {
        "description": "## What's Changed\n* Release/v0.4.2 by @tamada in https://github.com/tamada/gixor/pull/35\n\n\n**Full Changelog**: https://github.com/tamada/gixor/compare/v0.4.1...v0.4.2",
        "name": "Release v0.4.2",
        "release_date": "2026-01-11T03:38:21Z"
      },
      {
        "description": "## What's Changed\n* Bump slab from 0.4.10 to 0.4.11 by @dependabot[bot] in https://github.com/tamada/gixor/pull/32\n* Release/v0.4.1 by @tamada in https://github.com/tamada/gixor/pull/33\n\n## New Contributors\n* @dependabot[bot] made their first contribution in https://github.com/tamada/gixor/pull/32\n\n**Full Changelog**: https://github.com/tamada/gixor/compare/v0.4.0...v0.4.1",
        "name": "Release v0.4.1",
        "release_date": "2025-11-05T06:40:31Z"
      },
      {
        "description": "",
        "name": "Release v0.4.0",
        "release_date": "2025-11-04T23:05:23Z"
      },
      {
        "description": "",
        "name": "Release v0.3.4",
        "release_date": "2025-09-09T13:10:31Z"
      },
      {
        "description": "",
        "name": "Release v0.3.2",
        "release_date": "2025-09-09T02:45:00Z"
      },
      {
        "description": "",
        "name": "Release v0.3.1",
        "release_date": "2025-09-07T03:41:40Z"
      },
      {
        "description": "",
        "name": "Release v0.3.0",
        "release_date": "2025-03-15T10:11:27Z"
      }
    ],
    "topics": [
      "gitignore",
      "gitignore-generator",
      "cli",
      "command-line-tool",
      "git"
    ]
  },
  {
    "description": "A tool for extracting/archiving files and directories in several formats.",
    "languages": [
      "Rust",
      "Dockerfile",
      "Just",
      "Elvish",
      "Shell"
    ],
    "last_updated": "2026-10-12T00:13:25Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/totebag.jpg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/totebag/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/totebag"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/totebag"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/totebag/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/totebag"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/totebag"
      }
    ],
    "name": "totebag",
    "owner": "tamada",
    "releases": [
      {
        "description": "## Highlights\n\n### totebag is now pure Rust, RAR included\n\nRAR extraction now uses [`rars`](https://crates.io/crates/rars), a pure Rust RAR implementation, instead of the C UnRAR library (#95). RAR archives (RAR 1.3 through RAR 7) can now be listed and extracted by every build, with no extra Cargo feature and no C toolchain. Together with the zstd change below, `totebag` no longer needs to compile or bundle any C library. Entry names that would escape the destination directory (`..`, absolute paths, drive prefixes) are rejected, and Windows-style `\\` separators in older archives are handled.\n\n`totebag` can now also **create** RAR archives: `totebag -o out.rar <files>` writes RAR5 (the format WinRAR 5 and later read). `--level` maps onto RAR's compression methods:\n\n| `--level` | RAR |\n|---|---|\n| 0 | store (no compression) |\n| 1-9 | RAR's 1-5 (fastest to best); the default 5 gives RAR's own default, 3 |\n\nUnix permissions and modification times are recorded, and empty directories are kept.\n\n### `-B, --base-dir`: store entries relative to a directory\n\n`totebag -B build/dist -o app.zip build/dist` stores `build/dist/app.js` as `app.js` (#108). The base directory is stripped before `-C/--rebase-dir` is prefixed, so `-B build/dist -C myapp` stores it as `myapp/app.js`. Paths are compared as absolute paths, so either side may be relative or absolute. If a target is outside the base directory, every such path is reported (`Error::OutsideBaseDir`) and nothing is written: no archive is created, and an existing one given with `--overwrite` keeps its contents. In the library, this is `ArchiveConfig::base_dir`.\n\n### `.tar.zst`: compression levels work again, in pure Rust\n\n`.tar.zst` archives are now compressed with [`structured-zstd`](https://crates.io/crates/structured-zstd), a pure Rust zstd implementation that supports every zstd level. `--level` now affects `.tar.zst` output again (#96):\n\n| `--level` | zstd |\n|---|---|\n| 0 | stored (no compression) |\n| 1-9 | zstd levels 3-22, mapped linearly (the default 5 is level 13) |\n\nIn v0.9.0, the default pure Rust backend (`ruzstd`) only implemented zstd's fastest level, so `--level` 1-9 all produced the same output, and the released binaries were affected too. This release fixes that without bringing back a C dependency. Extraction also uses `structured-zstd`. Archives written by `zstd(1)` extract as before, and archives written by totebag pass `zstd -t`.\n\n## Security\n\n- **Path traversal on extraction (#103).** In v0.9.0 and earlier, extracting a crafted archive could write files outside the destination directory: entry names containing `..`, absolute paths (`/tmp/...`) and Windows drive prefixes were joined onto the destination unchecked, for every format. Such entries are now refused with the new `Error::UnsafePath` and are not written; the other entries of the archive are still extracted, and `totebag` exits with an error. Every format is covered: zip, tar (and its compressed variants), 7z, cpio, ar, cab and lha here, and RAR through `rars` (#95). **Upgrade if you extract archives you did not create.**\n\n## Breaking changes\n\n- **The `zstd-native` feature has been removed** from both `totebag` and `totebag-cli` (#96). It switched to the C zstd library, which is no longer needed. Remove `--features zstd-native` from your build commands (and `\"zstd-native\"` from the `features` of your `Cargo.toml` dependency), or Cargo will report an unknown feature.\n- **The `rar` feature has been removed** from both `totebag` and `totebag-cli` (#95). RAR extraction is always available now, so drop `--features rar` (or `\"rar\"` from `features`) in the same way.\n- **`Error::FeatureDisabled` has been removed** from the library's `Error` enum (#95). It was only returned for RAR when the `rar` feature was off. Code that matches on it must drop that arm.\n- **`Error::UnsafePath(PathBuf)` has been added** to the `Error` enum (#103). `Error` is not `#[non_exhaustive]`, so an exhaustive `match` on it needs a new arm.\n- **`ArchiveConfig::path_in_archive` returns `Result<PathBuf>`** instead of `PathBuf` (#108), so that a path outside `base_dir` can be reported. **`Error::OutsideBaseDir(PathBuf)`** is added to the `Error` enum for it.\n- **`ToteArchiver::perform` takes `&mut BufWriter<File>` instead of `File`** (#106). This only affects code that implements `ToteArchiver` itself. `totebag::archive` now owns the buffered writer and flushes it after `perform` returns, so an implementation must finish any encoder it wraps the writer in, rather than rely on dropping it. Its `targets` are now `&[Target]` (#108): `totebag::archive` walks the targets and names every entry before it creates the destination, and passes each path together with its name in the archive (`Target::path`, `Target::dest_path`), so an archiver only writes them.\n\n## Bug fixes\n\n- `cargo install totebag-cli` installed the command as `totebag-cli`, while the release binaries, Homebrew and the Docker image call it `totebag`. It is now `totebag` everywhere (#124). If you installed an earlier version with Cargo, run `cargo uninstall totebag-cli` before installing this one, or remove the leftover `~/.cargo/bin/totebag-cli`.\n- Archiving: a target that does not exist was skipped silently, so `totebag -o a.zip missing_dir` wrote an empty archive and exited successfully, and a misspelled target among others was simply missing from the archive. Every missing target is now reported as `<path>: File not found`, and nothing is written; an existing archive given with `--overwrite` keeps its contents (#52).\n- Archiving: per-file errors were collected and then dropped by the zip, cab and cpio archivers whenever the archive itself was finished successfully, so a file that could not be read was silently missing from a `.zip`, `.cab` or `.cpio` that was reported as a success. They are now returned, like the other formats already did (#108).\n- Archiving: write errors at the end of an archive were ignored. The gzip, bzip2 and xz encoders wrote their final block and trailer when they were dropped, which discards errors, so a full disk could leave a truncated `.tar.gz` / `.tar.bz2` / `.tar.xz` reported as a success. The encoders are now finished explicitly, and the buffered output is flushed explicitly, so these errors are returned (#106).\n- ar: errors while extracting were collected and then dropped, so extraction reported success. They are now returned (#103).\n- cab: entry names use `\\` as the separator, so `dir\\file.txt` was extracted as one file with a backslash in its name on Unix. It is now extracted as `dir/file.txt` (#103).\n- 7z: an error in one block no longer abandons the blocks after it, and an entry with an unsafe name is skipped without stopping the rest of its block (#103).\n- tar: entries whose path is longer than 100 bytes (stored with GNU long names / PAX) were listed and extracted under a name truncated to 100 bytes. Extraction then failed or wrote the file to the wrong path. Both listing and extraction now read the full name.\n- Non-UTF-8 paths no longer cause a panic when an error message is displayed or when the format is detected from the file extension (#98, #102, thanks @nightcityblade).\n- Shell completion generation no longer panics on a destination without a parent directory (#99, #101, thanks @nightcityblade).\n- `totebag --help` no longer shows `[default: 5]` twice for `--level` (#97, #100, thanks @Jah-yee).\n\n## Packaging\n\n- The Docker image (`quay.io/tama5/totebag`) is built on Debian 13 (trixie) instead of Debian 11, whose support has ended, and carries the standard OCI labels (source, version, revision, licenses, ...) (#120).\n- The shell completions shipped in the release archives are regenerated and include `--rebase-dir` and `-B/--base-dir`. The fish, elvish and PowerShell files are now named `totebag.fish`, `totebag.elv` and `totebag.ps1`; fish only loads `totebag.fish`, so fish completion now works, including through Homebrew (#122).\n\n## Performance\n\n- Archive I/O is buffered (#106). Archiving writes through a `BufWriter`, and extraction and listing read the archive through a `BufReader` for tar (and every compressed tar), zip, 7z, cpio, ar, cab and lha. Formats that read or write many small headers no longer issue a system call for each one. RAR does its own I/O and is unchanged.\n\n  Measured with 5,000 files of 2 KiB on macOS (release builds, best of three runs):\n\n  | Format | Archive (v0.9 → v0.10) | Extract (v0.9 → v0.10) |\n  |---|---|---|\n  | tar | 0.17 s → 0.15 s | 0.49 s → 0.50 s |\n  | cpio | 0.33 s → 0.16 s | 0.44 s → 0.13 s |\n  | ar | 0.53 s → 0.15 s | 0.39 s → 0.38 s |\n\n  Extracting tar and ar barely changes, because creating the 5,000 files dominates the time.\n\n## Dependencies\n\n- `kpea` (cpio) 0.2.5 → 0.4.0 (#87, #104).\n- `ruzstd`, the optional `zstd` (C), and `tempfile` were removed; `structured-zstd` 0.0.59 was added (#96).\n- `unrar` (C UnRAR) was replaced by `rars` 0.10 (#95). Every dependency is now pure Rust.\n- The lower bound of `time` is raised from 0.3.44 to 0.3.47, the version `zip` 8.6 already requires. Every requirement is now the lowest version that builds and passes the tests, and CI keeps it that way (#116).\n\n## Internal\n\n- `Error::error_or` is now implemented in terms of `Error::error_or_else` (#104).\n- Extractors create parent directories through one shared helper, which `totebag::archive` uses for the destination directory too (#103).\n- `testdata/unsafe/` holds committed archives with unsafe entry names, together with the script that generates them (#103).\n- The archivers share one walk of the targets (`walk_targets`) instead of seven copies of the same loop (#117), and the code smells SonarQube Cloud reported are cleared (#114).\n\n## CI\n\n- A `minimal-versions` job resolves every direct dependency to the lowest version its requirement allows, and builds and tests at those versions (#116).\n- Dependabot opens a weekly pull request for Cargo dependencies, only for releases that the current requirements do not already allow (`versioning-strategy: increase-if-necessary`) (#116).\n- The cross-compile action no longer takes a `features` input; release binaries are built with the default features, which now include RAR (#95).\n- The release-published notification now fires; the release is published with the GitHub App token instead of `GITHUB_TOKEN` (#94).\n- SonarQube Cloud analyses every push to `main` and every pull request, with coverage and Clippy results imported (#110, #111). Coveralls stays in use.\n- The release description starts with `.github/release-notes/vX.Y.Z.md` when that file exists, followed by the generated list of pull requests (#123).\n\n\n## What's Changed\n* fix: handle completion paths without parents by @nightcityblade in https://github.com/tamada/totebag/pull/101\n* fix(lib): handle non-UTF-8 paths without panicking by @nightcityblade in https://github.com/tamada/totebag/pull/102\n* fix(cli): remove duplicate [default: 5] in --level help text by @Jah-yee in https://github.com/tamada/totebag/pull/100\n* ci: make the release-published notification actually fire by @tamada in https://github.com/tamada/totebag/pull/94\n* build: upgrade kpea (cpio) from 0.2.5 to 0.4.0 by @tamada in https://github.com/tamada/totebag/pull/104\n* build: replace ruzstd and zstd-native with structured-zstd by @tamada in https://github.com/tamada/totebag/pull/105\n* build: replace unrar with the pure Rust rars, and create RAR archives by @tamada in https://github.com/tamada/totebag/pull/107\n* fix: refuse archive entries that would be extracted outside the destination by @tamada in https://github.com/tamada/totebag/pull/109\n* ci: add SonarQube Cloud analysis by @tamada in https://github.com/tamada/totebag/pull/111\n* perf: buffer archive I/O and report errors at the end of an archive by @tamada in https://github.com/tamada/totebag/pull/112\n* feat: add --base-dir to store entries relative to a directory by @tamada in https://github.com/tamada/totebag/pull/113\n* refactor: clear the code smells SonarQube Cloud reported by @tamada in https://github.com/tamada/totebag/pull/115\n* refactor: walk the archive targets once, before creating the archive by @tamada in https://github.com/tamada/totebag/pull/117\n* fix: report missing targets in archive mode instead of ignoring them by @tamada in https://github.com/tamada/totebag/pull/118\n* build: make dependency lower bounds accurate, check them in CI, and add Dependabot by @tamada in https://github.com/tamada/totebag/pull/119\n* build: fix the container image build, move it to Debian 13, and fill its labels by @tamada in https://github.com/tamada/totebag/pull/126\n* docs: add the SonarQube badge to the README template by @tamada in https://github.com/tamada/totebag/pull/127\n* fix: regenerate the shell completions, with the names shells load by @tamada in https://github.com/tamada/totebag/pull/128\n* ci: start the release description with the hand-written notes by @tamada in https://github.com/tamada/totebag/pull/129\n* docs: sync usage help and add cargo install; name the command totebag by @tamada in https://github.com/tamada/totebag/pull/130\n* Release v0.10.0 by @tamada in https://github.com/tamada/totebag/pull/131\n* ci: pin cargo-zigbuild 0.23.3 and cross-compile release branches before merging by @tamada in https://github.com/tamada/totebag/pull/132\n* Release v0.10.0 by @tamada in https://github.com/tamada/totebag/pull/133\n\n## New Contributors\n* @nightcityblade made their first contribution in https://github.com/tamada/totebag/pull/101\n* @Jah-yee made their first contribution in https://github.com/tamada/totebag/pull/100\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.9.0...v0.10.0",
        "name": "Release v0.10.0",
        "release_date": "2026-10-12T00:17:14Z"
      },
      {
        "description": "## What's Changed\n* Release v0.9.0: pure Rust dependencies and consistency cleanup by @tamada in https://github.com/tamada/totebag/pull/93\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.8.16...v0.9.0",
        "name": "Release v0.9.0",
        "release_date": "2026-08-12T08:26:20Z"
      },
      {
        "description": "## What's Changed\n* feat: add crate publishing step to the release workflow with trusted … by @tamada in https://github.com/tamada/totebag/pull/72\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.8.15...v0.8.16",
        "name": "Release v0.8.16",
        "release_date": "2026-01-25T10:53:27Z"
      },
      {
        "description": "## What's Changed\n* Release/v0.8.15 by @tamada in https://github.com/tamada/totebag/pull/71\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.8.14...v0.8.15",
        "name": "Release v0.8.15",
        "release_date": "2026-01-25T10:30:19Z"
      },
      {
        "description": "## What's Changed\n* Release/v0.8.11 by @tamada in https://github.com/tamada/totebag/pull/67\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.8.10...v0.8.11",
        "name": "Release v0.8.11",
        "release_date": "2026-01-25T05:26:27Z"
      },
      {
        "description": "## What's Changed\n* Release/v0.8.10 by @tamada in https://github.com/tamada/totebag/pull/66\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.8.9...v0.8.10",
        "name": "Release v0.8.10",
        "release_date": "2026-01-24T22:41:40Z"
      },
      {
        "description": "## What's Changed\n* Release/v0.8.0 by @tamada in https://github.com/tamada/totebag/pull/54\n* fix: update publish workflow to create draft releases by @tamada in https://github.com/tamada/totebag/pull/55\n* fix: streamline release creation command in publish workflow by @tamada in https://github.com/tamada/totebag/pull/56\n* Release/v0.8.3 by @tamada in https://github.com/tamada/totebag/pull/59\n* Release/v0.8.4 by @tamada in https://github.com/tamada/totebag/pull/60\n* Release/v0.8.5 by @tamada in https://github.com/tamada/totebag/pull/61\n* add shell specification for artifact listing step in cross-compile action by @tamada in https://github.com/tamada/totebag/pull/62\n* fix: update release upload command to use correct tag output by @tamada in https://github.com/tamada/totebag/pull/63\n* Release/v0.8.8 by @tamada in https://github.com/tamada/totebag/pull/64\n* Release/v0.8.9 by @tamada in https://github.com/tamada/totebag/pull/65\n\n\n**Full Changelog**: https://github.com/tamada/totebag/compare/v0.7.10...v0.8.9",
        "name": "Release v0.8.9",
        "release_date": "2026-01-24T07:00:12Z"
      },
      {
        "description": "",
        "name": "Release v0.7.10",
        "release_date": "2025-07-18T05:48:37Z"
      },
      {
        "description": "",
        "name": "Release v0.7.9",
        "release_date": "2025-07-18T05:38:03Z"
      },
      {
        "description": "",
        "name": "Release v0.7.8",
        "release_date": "2025-02-05T10:19:44Z"
      }
    ],
    "topics": [
      "archiver",
      "command-line-utilities",
      "compressor",
      "decompressor",
      "unarchiver"
    ]
  },
  {
    "description": "Detecting the build tools in use.",
    "languages": [
      "Rust",
      "Dockerfile",
      "HTML",
      "CSS",
      "Pkl"
    ],
    "last_updated": "2026-07-20T06:40:13Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/btmeister.svg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/btmeister/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/btmeister"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/btmeister"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/btmeister/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/btmeister"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/btmeister"
      }
    ],
    "name": "btmeister",
    "owner": "tamada",
    "releases": [
      {
        "description": "",
        "name": "Release v0.7.4",
        "release_date": "2025-01-31T11:47:39Z"
      },
      {
        "description": "",
        "name": "Release v0.7.3",
        "release_date": "2025-01-30T08:40:18Z"
      },
      {
        "description": "- Supports project archive file (tar, tar.bz2, tar.gz, tar.xz, tar.zstd, and zip).",
        "name": "Release v0.7.1",
        "release_date": "2025-01-15T13:58:24Z"
      },
      {
        "description": "",
        "name": "Release v0.7.0",
        "release_date": "2025-01-15T13:40:45Z"
      },
      {
        "description": "",
        "name": "Release v0.6.6",
        "release_date": "2025-01-15T05:39:54Z"
      },
      {
        "description": "",
        "name": "Release v0.6.5",
        "release_date": "2025-01-14T12:21:35Z"
      },
      {
        "description": "",
        "name": "Release v0.6.4",
        "release_date": "2025-01-14T10:36:37Z"
      },
      {
        "description": "",
        "name": "Release v0.6.3",
        "release_date": "2025-01-14T08:25:52Z"
      },
      {
        "description": "",
        "name": "Release v0.6.2",
        "release_date": "2025-01-14T07:54:50Z"
      },
      {
        "description": "",
        "name": "Release v0.6.1",
        "release_date": "2025-01-14T07:27:13Z"
      }
    ],
    "topics": [
      "build-tool",
      "project-analysis"
    ]
  },
  {
    "description": "Heatmap generator.",
    "languages": [
      "Shell",
      "Dockerfile",
      "Rust"
    ],
    "last_updated": "2026-07-25T08:08:04Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/heatman.svg"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/heatman/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/heatman"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/heatman"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/heatman/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/heatman"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/heatman"
      }
    ],
    "name": "heatman",
    "owner": "tamada",
    "releases": [
      {
        "description": "",
        "name": "Release v0.1.1",
        "release_date": "2026-06-19T07:09:27Z"
      },
      {
        "description": "## Bug Fixes\n- update artifact naming convention for Windows executables [#4](https://github.com/tamada/heatman/pull/4) ([Haruaki Tamada](https://github.com/tamada/heatman/commit/b56cd5845f13ea8ef56bab96efc4eaee7e2336b4))",
        "name": "Release v0.1.0",
        "release_date": "2026-03-26T11:56:49Z"
      }
    ],
    "topics": [
      "command-line-utilities",
      "csv",
      "data-visualization",
      "heatmap"
    ]
  },
  {
    "description": "Small Object Programming Checker.",
    "languages": [
      "Java",
      "HTML",
      "Shell",
      "Makefile",
      "Dockerfile"
    ],
    "last_updated": "2026-07-20T06:41:06Z",
    "license": [
      {
        "name": "Apache License 2.0",
        "spdx": "apache-2.0",
        "url": "http://choosealicense.com/licenses/apache-2.0/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/9rules.svg"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/9rules/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/9rules"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/9rules/"
      }
    ],
    "name": "9rules",
    "owner": "tamada",
    "releases": [
      {
        "description": "",
        "name": "v1.1.1",
        "release_date": "2020-09-23T07:22:55Z"
      },
      {
        "description": "Completely passing the self-check.",
        "name": "release v1.1.0",
        "release_date": "2020-09-14T00:45:04Z"
      },
      {
        "description": "This is the first release.",
        "name": "release v1.0.0",
        "release_date": "2019-09-04T03:40:05Z"
      }
    ],
    "topics": [
      "java8",
      "code-analysis",
      "code-review",
      "code-quality"
    ]
  },
  {
    "description": "peripheral utility commands for the shell.",
    "languages": [
      "Dockerfile",
      "Makefile",
      "Go",
      "HTML",
      "CSS"
    ],
    "last_updated": "2023-08-05T08:56:35Z",
    "license": [
      {
        "name": "MIT License",
        "spdx": "mit",
        "url": "http://choosealicense.com/licenses/mit/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/peripherals.svg"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/peripherals/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/peripherals"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/peripherals"
      }
    ],
    "name": "peripherals",
    "owner": "tamada",
    "releases": [
      {
        "description": "",
        "name": "Release v1.0.1",
        "release_date": "2023-08-05T08:56:35Z"
      },
      {
        "description": "",
        "name": "Release v1.0.0",
        "release_date": "2023-08-05T06:20:28Z"
      },
      {
        "description": "",
        "name": "Release v0.9.6",
        "release_date": "2022-08-23T06:38:31Z"
      },
      {
        "description": "",
        "name": "Release v0.9.5",
        "release_date": "2022-08-06T14:06:42Z"
      },
      {
        "description": "",
        "name": "Release v0.9.4",
        "release_date": "2022-08-06T14:03:02Z"
      },
      {
        "description": "",
        "name": "Release v0.9.3",
        "release_date": "2022-08-06T13:58:40Z"
      },
      {
        "description": "",
        "name": "Release v0.9.2",
        "release_date": "2022-08-06T13:49:27Z"
      },
      {
        "description": "",
        "name": "Release v0.9.1",
        "release_date": "2022-08-06T13:39:59Z"
      }
    ],
    "topics": []
  },
  {
    "description": "Java birthmark toolkit, detecting the software theft by native characteristics of the programs.",
    "languages": [
      "Java",
      "HTML",
      "JavaScript",
      "CSS",
      "Dockerfile"
    ],
    "last_updated": "2026-05-08T17:53:32Z",
    "license": [
      {
        "name": "Apache License 2.0",
        "spdx": "apache-2.0",
        "url": "http://choosealicense.com/licenses/apache-2.0/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/pochi.png"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/pochi"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/pochi/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/pochi"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/pochi"
      }
    ],
    "name": "pochi",
    "owner": "tamada",
    "releases": [
      {
        "description": "",
        "name": "Release v2.6.0",
        "release_date": "2021-12-13T05:24:16Z"
      },
      {
        "description": "",
        "name": "Release v2.5.2",
        "release_date": "2021-09-14T02:56:47Z"
      },
      {
        "description": "",
        "name": "Release v2.5.1",
        "release_date": "2021-09-14T02:08:17Z"
      },
      {
        "description": "* Introduce `SpecifiedPairMatcher` and related classes.\r\n* Change to `Comparable` the following classes.\r\n    * `Similarity`\r\n    * `Comparison`\r\n    * `Threshold`\r\n    * `Percentage`\r\n    * `Birthmark`\r\n    * `Metadata`",
        "name": "Release v2.5.0",
        "release_date": "2021-09-08T05:36:44Z"
      },
      {
        "description": "",
        "name": "Release v2.4.6",
        "release_date": "2021-07-28T23:06:52Z"
      },
      {
        "description": "- support generics for Birthmark.\r\n- introduce a new birthmark for frequencies of used classes birthmark.\r\n- introduce a new birthmark for verbs of method names birthmark.\r\n- introduce CosineSimilarityComparator for comparing birthmarks with cosine similarity.",
        "name": "Release v2.4.0",
        "release_date": "2021-07-28T07:22:17Z"
      },
      {
        "description": "",
        "name": "Release v2.3.24",
        "release_date": "2021-07-22T12:46:44Z"
      },
      {
        "description": "",
        "name": "Release v2.3.23",
        "release_date": "2021-07-22T02:19:53Z"
      },
      {
        "description": "",
        "name": "Release v2.3.21",
        "release_date": "2021-07-22T01:58:57Z"
      },
      {
        "description": "",
        "name": "Release v2.3.19",
        "release_date": "2021-07-22T01:11:42Z"
      }
    ],
    "topics": [
      "birthmarks",
      "plagiarism-detection",
      "software-theft-detection",
      "java9module",
      "birthmark-toolkit"
    ]
  },
  {
    "description": "get next/previous sibling directory name.",
    "languages": [
      "Shell",
      "Rust",
      "Just",
      "AppleScript",
      "Elvish"
    ],
    "last_updated": "2026-10-12T23:08:15Z",
    "license": [
      {
        "name": "Do What The F*ck You Want To Public License",
        "spdx": "wtfpl",
        "url": "http://choosealicense.com/licenses/wtfpl/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/sibling.png"
      },
      {
        "label": "docs",
        "url": "https://docs.rs/sibling/latest"
      },
      {
        "label": "registry",
        "url": "https://crates.io/crates/sibling"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/sibling/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/sibling"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/sibling/"
      }
    ],
    "name": "sibling",
    "owner": "tamada",
    "releases": [
      {
        "description": "## Bug Fixes\n- build the document site with the Hugo which the theme asks for [#47](https://github.com/tamada/sibling/pull/47) ([Haruaki Tamada](https://github.com/tamada/sibling/commit/37955eb3e39191c6db73078eab6aa2aefaa62d71))",
        "name": "Release v3.0.0",
        "release_date": "2026-08-07T04:51:08Z"
      },
      {
        "description": "",
        "name": "Release v2.0.4",
        "release_date": "2026-01-26T00:59:13Z"
      },
      {
        "description": "",
        "name": "Release v2.0.3",
        "release_date": "2026-01-22T11:49:13Z"
      },
      {
        "description": "",
        "name": "Release v2.0.2",
        "release_date": "2025-07-18T08:31:44Z"
      },
      {
        "description": "",
        "name": "Release v2.0.1",
        "release_date": "2025-07-18T08:19:08Z"
      },
      {
        "description": "",
        "name": "Release v2.0.0-beta-7",
        "release_date": "2024-12-31T02:48:11Z"
      },
      {
        "description": "",
        "name": "Release v2.0.0-beta-6",
        "release_date": "2024-12-23T05:04:13Z"
      },
      {
        "description": "",
        "name": "Release v2.0.0-beta-5",
        "release_date": "2024-12-06T01:23:16Z"
      },
      {
        "description": "",
        "name": "Release v2.0.0-beta-4",
        "release_date": "2024-12-06T01:20:13Z"
      },
      {
        "description": "",
        "name": "Release v2.0.0-beta-3",
        "release_date": "2024-12-05T02:10:35Z"
      }
    ],
    "topics": [
      "directory-traversal",
      "utility",
      "shell"
    ]
  },
  {
    "description": "another implementation of wc (word count) accepting archives, directories, and file lists.",
    "languages": [
      "Makefile",
      "Go",
      "Dockerfile",
      "Shell"
    ],
    "last_updated": "2026-07-20T06:39:06Z",
    "license": [
      {
        "name": "Apache License 2.0",
        "spdx": "apache-2.0",
        "url": "http://choosealicense.com/licenses/apache-2.0/"
      }
    ],
    "links": [
      {
        "label": "logo",
        "url": "https://tamada.github.io/api/assets/wildcat.svg"
      },
      {
        "label": "container",
        "url": "https://ghcr.io/tamada/wildcat"
      },
      {
        "label": "sbom",
        "url": "https://api.github.com/repos/tamada/wildcat/dependency-graph/sbom"
      },
      {
        "label": "repository",
        "url": "https://github.com/tamada/wildcat"
      },
      {
        "label": "www",
        "url": "https://tamada.github.io/wildcat"
      }
    ],
    "name": "wildcat",
    "owner": "tamada",
    "releases": [
      {
        "description": "* show progress bar (#30),\r\n* do not read the hidden files by the default (#32),\r\n* show entry count in the total line (#33),\r\n* fix the bug fails to open the directory contains the space on the PowerShell, Windows (#34), and\r\n* change order of options in the help message.",
        "name": "v1.2.0",
        "release_date": "2021-04-28T06:47:01Z"
      },
      {
        "description": "* Fix the bug hangs on reading the large files.\r\n    * #30, #31 \r\n",
        "name": "v1.1.1",
        "release_date": "2021-03-25T06:03:46Z"
      },
      {
        "description": "* parse file types from the reader (magic numbers)\r\n    * in the previous versions, wildcat parses the file types by the file names).\r\n* introduce goroutine for counting the files.\r\n* enable to store the content given by the URLs.",
        "name": "v1.1.0",
        "release_date": "2021-03-19T14:28:15Z"
      },
      {
        "description": "",
        "name": "v1.0.3",
        "release_date": "2021-03-05T14:07:08Z"
      },
      {
        "description": "",
        "name": "support darwin_arm64 (Apple Silicon)",
        "release_date": "2021-02-24T07:04:13Z"
      },
      {
        "description": "",
        "name": "release",
        "release_date": "2021-02-22T03:17:35Z"
      },
      {
        "description": "",
        "name": "The first release",
        "release_date": "2021-02-21T14:40:31Z"
      }
    ],
    "topics": [
      "word-count",
      "golang",
      "rest-api",
      "docker",
      "cli-app"
    ]
  }
]