Skip to content

feat(cli): add hinzu api-diff for cross-language public-surface conformance#34

Open
zmaril wants to merge 2 commits into
mainfrom
feat/hinzu-api-diff
Open

feat(cli): add hinzu api-diff for cross-language public-surface conformance#34
zmaril wants to merge 2 commits into
mainfrom
feat/hinzu-api-diff

Conversation

@zmaril

@zmaril zmaril commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What

A new hinzu api-diff subcommand that takes two hinzu api reports — a SOURCE package's public API and a TARGET package's public API (typically different languages) — and emits a stable JSON conformance grade. Each source public item is classified matched / signatureMismatch / missing, plus target-only items surfaced as extra, with an overall conformance score and a per-kind breakdown. It is advisory and evidence-carrying (every pairing cites source and target id/file/line), the same ethos as port-diff.

Why

It completes the original intent behind hinzu api: knowing both the public API a port must match AND how far a target language's surface has gotten. It composes with port-diff — port-diff bands file progress by graph structure; api-diff grades the actual public-surface conformance. They're complementary, not redundant.

How

A pure hinzu_core::apidiff::build_api_diff(source, target, rules) (no I/O — passes the self-check pure-region policy) does name+kind matching and structural shape comparison; the CLI reads the two JSON reports and the port config.

  • It reuses port-diff's exact naming rules (kebab→snake files, camelCase→snake_case fns, PascalCase types kept, SCREAMING consts kept) plus field-name normalization, so convention renames don't false-miss.
  • Kind-equivalence classes (Callable / Type / Const / Namespace) let TS interface↔Rust struct and function↔method match across languages; cross-kind pairings are noted.
  • Classification is driven by structural facts that survive cross-language rendering (param arity, field-name sets, variant-name sets). Rendered type strings are compared only within a language and are advisory across languages — a signatureMismatch is a signal, not a proof.

Validated on the real pi→pidgin pair (the ai package)

Source pi ai (TypeScript) vs target pidgin ai (Rust). Grade: conformance 0.32 — matched 124, missing 206, signatureMismatch 58, extra 527 (388 source public items).

Spot-checked examples verified against pidgin source:

  • matchedcalculateCostcalculate_cost; AssistantMessage fields align snake_case.
  • signatureMismatchcompleteSimple TS 3 params → Rust 4, splitting out Option<&AbortSignal>.
  • missinganthropicProvider factory fns (pidgin uses registries); ANTHROPIC_MODELS const (pidgin exposes via methods) — genuinely unported as that shape, grep-confirmed.
  • extra — 527 target-only, mostly Rust inherent methods a structurally-typed TS module never declares.

During validation two convention false-misses were caught and fixed (field-name normalization; PascalCase function names now snake-fold).

Honest limitations

  • Cross-language type strings are advisory, not classification drivers.
  • Optional/overloaded params make arity approximate (hinzu api captures only the first overload).
  • A concept ported to a different shape reads as missing.

Docs

  • notes/api-diff.md — schema, matching + naming rules + kind-equivalence table, composition with port-diff, the pi→pidgin result.
  • notes/api.md — links the follow-up.
  • notes/port-pi-pidgin.toml — the atilla→pidgin-renamed port config.

Gate (all green locally)

  • cargo fmt --all --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test — 93 core (incl. 8 new apidiff) + lsp/dbsp/integration, 0 failed
  • hinzu check self-check pure-region policy
  • straitjacket v0.2.3 on a clean clone — 0 findings in 123 files
  • codespell clean

It's a DRAFT — not for merge without review.

@zmaril
zmaril force-pushed the feat/hinzu-api-diff branch from 2827b5d to 818ca02 Compare July 21, 2026 16:15
zmaril pushed a commit that referenced this pull request Jul 21, 2026
Rebasing #34 onto main (which now carries #40's `ranges` subcommand) pushed
crates/hinzu-cli/src/main.rs to 1527 lines, over straitjacket's 1500-line
file-size limit. Move the `hinzu api-diff` CLI flow — the ApiDiffArgs struct
and the api_diff/load_api_report/api_diff_rules handlers — into a new
crates/hinzu-cli/src/api_diff.rs module (mirroring api_ts.rs / api_fluessig.rs),
and make write_json crate-visible. Pure code move; main.rs drops to 1452 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8vNpHV16JdfQGmZLpqRCk
claude added 2 commits July 21, 2026 21:21
…rmance

`hinzu api-diff` takes two `hinzu api` reports (a source package's public API
and a target port's) and emits a stable JSON conformance grade: per source
public item, is there a matching target item (matched / signatureMismatch /
missing), plus target-only extras. Advisory and evidence-carrying, mirroring
port-diff's ethos.

- Pure core `build_api_diff` in hinzu-core `apidiff.rs`: name + kind-class
  matching, structural shape comparison (param arity, field/variant sets),
  a per-kind breakdown and an overall conformance score. No I/O — passes the
  functional-core self-check.
- Reuses port-diff's naming rules (exposed as pub(crate)) so convention renames
  (camelCase<->snake_case fns and fields, PascalCase/SCREAMING kept, kebab<->snake
  files) don't false-miss; a PascalCase *function* name snake-folds since
  keep-Pascal is a type rule.
- CLI `api-diff` subcommand reads the two report JSONs and the port config
  (--config/--package) for the naming rules + crate mapping; all I/O in the CLI.
- notes/api-diff.md documents the schema, matching, kind-equivalence table,
  composition with port-diff, and the pi->pidgin validation (conformance 0.32).
- notes/port-pi-pidgin.toml: the pi->pidgin port config (atilla renamed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8vNpHV16JdfQGmZLpqRCk
Rebasing #34 onto main (which now carries #40's `ranges` subcommand) pushed
crates/hinzu-cli/src/main.rs to 1527 lines, over straitjacket's 1500-line
file-size limit. Move the `hinzu api-diff` CLI flow — the ApiDiffArgs struct
and the api_diff/load_api_report/api_diff_rules handlers — into a new
crates/hinzu-cli/src/api_diff.rs module (mirroring api_ts.rs / api_fluessig.rs),
and make write_json crate-visible. Pure code move; main.rs drops to 1452 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8vNpHV16JdfQGmZLpqRCk
@zmaril
zmaril force-pushed the feat/hinzu-api-diff branch from 6de5bc9 to be78c2e Compare July 21, 2026 21:29
@zmaril
zmaril marked this pull request as ready for review July 21, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants