From 76653b9440a1d6284ce259970fec030fad13ad28 Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Sun, 13 Sep 2026 20:43:08 -0300 Subject: [PATCH 1/5] Some changes --- CHANGELOG.md | 13 ++++++++ RUST_DIVERGENCES.md | 44 ++++++++++++++----------- tests/__snapshots__/golden.test.ts.snap | 4 +-- tests/differential.test.ts | 13 ++++++-- tests/rust-validation/Cargo.lock | 20 ----------- tests/rust-validation/Cargo.toml | 2 +- tests/vectors/vectors.json | 4 +-- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 668b9d3..3e2e6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +### Changed + +- **Tags 2 and 3 are unnamed in the default registry**, as in the reference's + default build: dcbor 1.0.0-beta.2 names the bignum tags only on request + (`registerStandardTags(store, { bignum: true })`), and `bc-tags` depends on + dcbor without its `num-bigint` feature, so `registerTags` now produces the + same registry as `bc_tags::register_tags_in`. No bc-tags code changed: the + registry vector and golden snapshot are regenerated, the differential pins + the flip, and the Rust harness runs on dcbor's default features (75 tags, + 80 probes, 0 mismatches). Requires `@blockchaincommons/dcbor` 1.0.0-beta.2. + ## 1.0.0-beta.2 Documents the registry behavior and updates repository tooling. Public tag values, diff --git a/RUST_DIVERGENCES.md b/RUST_DIVERGENCES.md index 011fad7..f037033 100644 --- a/RUST_DIVERGENCES.md +++ b/RUST_DIVERGENCES.md @@ -4,23 +4,26 @@ This package tracks `bc-tags` **0.12.0**, commit [`fc30b65c82eeca3124288fb9096c31a5631adc87`](https://github.com/BlockchainCommons/bc-tags-rust/commit/fc30b65c82eeca3124288fb9096c31a5631adc87). The version and commit are recorded in [`.github/versions.yml`](./.github/versions.yml). -## Remaining difference: default bignum registration +## Resolved: default bignum registration -TypeScript's `registerTags` calls dcbor's `registerStandardTags`, which registers -`date` (tag 1), `positive-bignum` (tag 2), and `negative-bignum` (tag 3), including -their summarizers. The TypeScript bignum functionality is always available. +TypeScript's `registerTags` calls dcbor's `registerStandardTags`, which +registers `date` (tag 1) and — since dcbor 1.0.0-beta.2 — the bignum tags +`positive-bignum` (2) and `negative-bignum` (3) only when asked +(`registerStandardTags(store, { bignum: true })`). Rust's +`bc_tags::register_tags_in` calls `dcbor::register_tags_in`, which names tags +2 and 3 only when dcbor's `num-bigint` feature is enabled; `bc-tags` depends +on dcbor without that feature, so the reference's default registry returns +the numeric names `"2"` and `"3"` and has no bignum summarizers. The +TypeScript registry now does the same: `registerTags` produces the +reference's default registry probe for probe (executed: the harness built +with dcbor's default features reports `"2"` / `"3"` on both sides). A +consumer that wants the `num-bigint` registry calls +`registerStandardTags(store, { bignum: true })` as well. -Rust's equivalent registers tags 2 and 3 only when dcbor's `num-bigint` feature -is enabled. Without it, the registry returns the numeric names `"2"` and `"3"` -and has no bignum summarizers. This is an observable registry difference for the -same input, rather than an input that only JavaScript can express. It originates -in the dcbor dependency; bc-tags does not change CBOR tag numbers or encoded bytes. - -The Rust validation harness enables `num-bigint` explicitly. Its matching results -therefore establish compatibility with that feature configuration, not with a -Rust build lacking bignum support. An optional registration setting in dcbor-ts -would be needed to reproduce the feature-disabled registry without changing its -default behavior. +Before dcbor 1.0.0-beta.2 the TypeScript registry always named tags 2 and 3, +and the harness enabled `num-bigint` to match it; the frozen baseline +(`tests/differential.test.ts`) still names them, and the differential pins +exactly that flip. bc-tags does not change CBOR tag numbers or encoded bytes. ## Validation @@ -30,7 +33,7 @@ Run from `tests/rust-validation`: cargo run --release -- ../vectors/vectors.json ``` -Result on 2026-09-12, with `num-bigint` enabled: +Result on 2026-09-13, with dcbor's default features (no `num-bigint`): ``` 75 tags, 80 registry probes - 0 MISMATCH @@ -40,7 +43,7 @@ The harness checks registered names by value, values by name, registry probes, and the expected count of 75 constants. It does not exhaust every possible store state or prove error-message equivalence. TypeScript tests additionally cover registration and exported constants. No other behavioral difference is currently -recorded for the matching Rust feature configuration. +recorded. ## API and language mappings @@ -59,12 +62,13 @@ recorded for the matching Rust feature configuration. - **Registration:** `registerTags(store?)` maps to `register_tags_in` for a supplied store and `register_tags` for the global store. Repeating a matching value/name registration is a no-op. Conflicting names for an existing value - cause a dcbor Error in TypeScript and a panic in Rust. These are language-specific - failure mechanisms, not a promise of identical exception types. + cause a dcbor `CborError` (`Custom`) in TypeScript and a panic in Rust. These are + language-specific failure mechanisms, not a promise of identical exception types. ## Maintenance When the reference changes, review its diff, update `.github/versions.yml` and the harness dependency, regenerate vectors, and run both package tests and Rust -validation. Keep the feature configuration explicit. Record newly observed +validation. Keep the harness on dcbor's default features, as `bc-tags` is +built. Record newly observed behavioral differences with reproducible inputs and outcomes. diff --git a/tests/__snapshots__/golden.test.ts.snap b/tests/__snapshots__/golden.test.ts.snap index c9a6cf1..029be22 100644 --- a/tests/__snapshots__/golden.test.ts.snap +++ b/tests/__snapshots__/golden.test.ts.snap @@ -87,8 +87,8 @@ exports[`golden: freeze additions > mutate a constant, then registerTags into a exports[`golden: tag table > registry after registerTags on a fresh store 1`] = ` [ "1 → date", - "2 → positive-bignum", - "3 → negative-bignum", + "2 → 2", + "3 → 3", "24 → encoded-cbor", "32 → url", "37 → uuid", diff --git a/tests/differential.test.ts b/tests/differential.test.ts index cff1025..f5de0e8 100644 --- a/tests/differential.test.ts +++ b/tests/differential.test.ts @@ -44,7 +44,16 @@ describe("differential: baseline vs working tree", () => { it("table (const column modulo the TAG_ prefix)", () => { expect(b.table.map(unprefixed)).toEqual(a.table); }); - it("registry", () => { - expect(b.registry).toEqual(a.registry); + it("registry (tags 2 and 3 unnamed since dcbor 1.0.0-beta.2)", () => { + // The baseline's inlined dcbor-compat always named the bignum tags; + // dcbor 1.0.0-beta.2 names them only on request, as the reference names + // them only under its `num-bigint` feature — which `bc-tags` does not + // enable, so `bc_tags::register_tags_in` leaves them unnamed (executed: + // the harness's default build reports `"2"` / `"3"`). Every other probe + // is compared verbatim. + const expected = a.registry.map(([value, name]) => + value === 2 || value === 3 ? [value, String(value)] : [value, name], + ); + expect(b.registry).toEqual(expected); }); }); diff --git a/tests/rust-validation/Cargo.lock b/tests/rust-validation/Cargo.lock index a2298d7..d7e5f3d 100644 --- a/tests/rust-validation/Cargo.lock +++ b/tests/rust-validation/Cargo.lock @@ -83,7 +83,6 @@ dependencies = [ "chrono", "half", "hex", - "num-bigint", "paste", "thiserror", "unicode-normalization", @@ -195,25 +194,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" diff --git a/tests/rust-validation/Cargo.toml b/tests/rust-validation/Cargo.toml index 63b9728..7f45def 100644 --- a/tests/rust-validation/Cargo.toml +++ b/tests/rust-validation/Cargo.toml @@ -8,6 +8,6 @@ publish = false bc-tags = "=0.12.0" # TypeScript always registers bignum tags. Enable the corresponding Rust # feature for comparison; see RUST_DIVERGENCES.md. Without it, probes 2 and 3 differ. -dcbor = { version = "0.25", features = ["num-bigint"] } +dcbor = "0.25" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/tests/vectors/vectors.json b/tests/vectors/vectors.json index 9a2575b..25e1f25 100644 --- a/tests/vectors/vectors.json +++ b/tests/vectors/vectors.json @@ -384,11 +384,11 @@ ], [ 2, - "positive-bignum" + "2" ], [ 3, - "negative-bignum" + "3" ], [ 24, From aed6a31aefeef00ea2d13ff159408a8908c32ef1 Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Mon, 14 Sep 2026 16:40:59 -0300 Subject: [PATCH 2/5] More improvements --- .github/workflows/ci.yml | 31 + CHANGELOG.md | 71 +- MIGRATION.md | 16 +- README.md | 3 +- RUST_DIVERGENCES.md | 123 +-- api/index.d.mts | 19 +- bun.lock | 22 +- examples/register-and-annotate.ts | 2 +- package.json | 4 +- scripts/generate-vectors.ts | 40 +- src/register.ts | 21 +- src/tags.ts | 26 +- tests/dist-packaging.test.ts | 20 + tests/global-store.test.ts | 42 ++ tests/golden-vectors.test.ts | 24 +- tests/rust-validation/Cargo.lock | 21 + tests/rust-validation/Cargo.toml | 19 +- tests/rust-validation/README.md | 67 +- tests/rust-validation/build.rs | 29 + .../fixtures/swapped-identifiers.json | 702 ++++++++++++++++++ tests/rust-validation/src/main.rs | 366 ++++++++- tests/tags.test.ts | 124 +++- tests/vectors/semantics.ts | 167 +++++ tests/vectors/table.ts | 59 +- tests/vectors/vectors.json | 331 ++++++++- 25 files changed, 2186 insertions(+), 163 deletions(-) create mode 100644 tests/global-store.test.ts create mode 100644 tests/rust-validation/build.rs create mode 100644 tests/rust-validation/fixtures/swapped-identifiers.json create mode 100644 tests/vectors/semantics.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d198c3c..b02ee3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,3 +57,34 @@ jobs: - name: Check dependency hygiene run: bun run check:deps + + rust-validation: + name: Cross-validate vectors against the Rust reference + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + # The runner image ships a stable Rust toolchain. The default build is + # the reference every Rust consumer uses (dcbor without num-bigint). + - name: Validate against bc-tags (default build) + working-directory: tests/rust-validation + run: cargo run --release --locked -- ../vectors/vectors.json + + - name: Validate the bignum recipe (num-bigint build) + working-directory: tests/rust-validation + run: cargo run --release --locked --features bignum -- ../vectors/vectors.json + + # The identifier check must bite: the swapped-label fixture fails with + # exactly two identifier mismatches. + - name: Reject the swapped-identifier fixture + working-directory: tests/rust-validation + run: | + set +e + output=$(cargo run --release --locked --quiet -- fixtures/swapped-identifiers.json 2>&1 >/dev/null) + status=$? + set -e + printf '%s\n' "$output" + test "$status" -eq 1 + test "$(printf '%s\n' "$output" | grep -c '^MISMATCH ident ')" -eq 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2e6a3..4f9667c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,75 @@ ## Unreleased +Requires `@blockchaincommons/dcbor` ^1.0.0-beta.3. The registry produced by +`registerTags` matches `bc_tags::register_tags_in` probe for probe, and the +Rust harness now proves it for identifiers, order, summarizers, names, +conflicts and rendering in both dcbor builds. + ### Changed - **Tags 2 and 3 are unnamed in the default registry**, as in the reference's - default build: dcbor 1.0.0-beta.2 names the bignum tags only on request + default build: dcbor names the bignum tags only on request (`registerStandardTags(store, { bignum: true })`), and `bc-tags` depends on dcbor without its `num-bigint` feature, so `registerTags` now produces the - same registry as `bc_tags::register_tags_in`. No bc-tags code changed: the - registry vector and golden snapshot are regenerated, the differential pins - the flip, and the Rust harness runs on dcbor's default features (75 tags, - 80 probes, 0 mismatches). Requires `@blockchaincommons/dcbor` 1.0.0-beta.2. + same registry as `bc_tags::register_tags_in`. The registry vector and golden + snapshot are regenerated and the differential pins the flip. For the + `num-bigint` registry call `registerStandardTags(store, { bignum: true })` + **before** `registerTags(store)`, the reference's registration order. +- **The dcbor floor is `^1.0.0-beta.3`, and `bun.lock` resolves it.** The + committed lock used to pin dcbor 1.0.0-beta.1, which names tags 2 and 3, + so a standalone install tested a registry that contradicts the snapshot. + dcbor 1.0.0-beta.3 is the release whose `registerStandardTags` registers + unconditionally (a name registered under another value moves back to the + standard value, as the reference's `insert_all` does), whose stored and + created tags are frozen, and whose global store is one per process across + the ESM and CommonJS builds; this package's registry depends on all three. +- **The IANA tag numbers are written in this package.** dcbor 1.0.0-beta.3 no + longer exports numeric `TAG_URI`, `TAG_UUID` and `TAG_ENCODED_CBOR`, so 32, + 37 and 24 are literals here, as `bc-tags` writes them. Values and names are + unchanged. +- `registerTags` registers `ALL_TAGS` directly (dcbor's `registerAll` takes + any iterable); no copy is made. + +### Added + +- Rust harness (`tests/rust-validation`): every constant is checked **by + identifier** against a table `build.rs` compiles from the vectors + (`TAG_SEED` ↔ `bc_tags::TAG_SEED`/`TAG_NAME_SEED`, `LEGACY_TAGS.SEED_V1` ↔ + `TAG_SEED_V1`; an identifier the crate lacks fails the build), plus new + vector blocks for summarizer presence, dcbor's names, the registration + order, conflicting-registration messages and the store after a + re-pointed name, and annotated, summarised and hex-annotated rendering + through the registry. Two builds: the default (the reference every Rust + consumer uses) and `--features bignum` for the documented recipe, each + skipping the rows that describe the other. `fixtures/swapped-identifiers.json` + is a committed negative fixture that must fail with two identifier + mismatches. A `rust-validation` CI job runs both builds and the fixture. +- Registration-contract tests: the conflict text and the store after it, + name re-pointing including dcbor's `date`, the default registry's unnamed + 2 and 3, the bignum recipe in both orders and its register sequence, frozen + stored tags held by identity, the global store after a conflict, non-store + arguments, and `registerTags` through the CommonJS entry naming the ESM + global store. + +### Fixed + +- Documentation: a conflicting registration throws dcbor's `CborError` (code + `Custom`) with the reference's panic text, not a bare `Error` (JSDoc, API + report, README); `registerTags` registers the `date` tag only, not the + bignum tags (MIGRATION, example); `RUST_DIVERGENCES.md` rewritten with the + harness result lines, the kept post-conflict differences and the mapping + table. + +### Consumers + +- Raise `@blockchaincommons/dcbor` to `^1.0.0-beta.3` and this package to the + release carrying this entry, then regenerate lockfiles. The published + `@blockchaincommons/tags` 1.0.0-beta.2 over dcbor 1.0.0-beta.1 still names + tags 2 and 3. +- Only `TAG_*` names are exported (never `PROVENANCE_MARK`, `XID` or + `KNOWN_VALUE`); a Rust harness that enables `dcbor/num-bigint` validates a + registry no Blockchain Commons crate builds. ## 1.0.0-beta.2 @@ -26,4 +85,4 @@ names, and registration logic are unchanged. ## 1.0.0-beta.1 -Initial beta implementation. \ No newline at end of file +Initial beta implementation. diff --git a/MIGRATION.md b/MIGRATION.md index 233025d..fdc67df 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -14,6 +14,10 @@ - [ ] The tags are now canonical `@blockchaincommons/dcbor` `Tag` values, not `dcbor-compat` ones. If you build tagged CBOR with them, use dcbor. - [ ] `registerTagsIn(store)` → `registerTags(store)`; `registerTags()` is unchanged. +- [ ] `registerTags` no longer names tags 2 and 3 (the reference's default + build leaves them unnamed). If you relied on `positive-bignum` / + `bignum(…)` output, call `registerStandardTags(store, { bignum: true })` + before `registerTags(store)`. - [ ] `SEED_V1`, `EC_KEY_V1`, `SSKR_SHARE_V1` and the other `*_V1` tags moved under `LEGACY_TAGS` (`LEGACY_TAGS.SEED_V1`). - [ ] Import `getGlobalTagsStore` and `TagsStore` from @@ -56,10 +60,14 @@ They are still in `ALL_TAGS` and still registered by `registerTags()`. ## 4. Registration semantics -`registerTags(store)` calls dcbor's `registerStandardTags(store)` (date and -bignum tags with their summarizers) and then registers `ALL_TAGS`. It is -idempotent. Registering a value that is already present under a *different* -name throws, as dcbor's store does. +`registerTags(store)` calls dcbor's `registerStandardTags(store)` (the `date` +tag and its summarizer; tags 2 and 3 stay unnamed, as in the reference's +default build) and then registers `ALL_TAGS`. It is idempotent, and a name +already registered under another value moves to this package's value, as the +reference's `insert_all` does. Registering a value that is already present +under a *different* name throws dcbor's `CborError` (code `Custom`) with the +reference's panic text; tags registered earlier in the call stay registered +and the rejected entry is unchanged. ## 5. Identity and immutability diff --git a/README.md b/README.md index 16579ed..d3eb923 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ registerTags(); getGlobalTagsStore().nameForValue(TAG_ENVELOPE.value); // "envelope" // Or into a store of your own. A value already registered under a -// different name throws (dcbor's Error); the same name is a no-op. +// different name throws dcbor's CborError (code Custom); the same name is a no-op. const store = new TagsStore(); registerTags(store); @@ -56,6 +56,7 @@ Runnable examples live in the [`examples/`](https://github.com/BlockchainCommons ### Version History +- **Unreleased** - Requires `@blockchaincommons/dcbor` ^1.0.0-beta.3, so tags 2 and 3 are unnamed as in the reference's default build; the Rust harness checks identifiers, registration order, summarizers, dcbor's names, conflicts and rendering in both dcbor builds; registration-contract tests; conflicts documented as dcbor's `CborError`. - **1.0.0-beta.2 (September 12, 2026)** - Documents the bignum-registration difference from Rust builds without `num-bigint`. - **1.0.0-beta.1 (September 9, 2026)** - Initial beta implementation. diff --git a/RUST_DIVERGENCES.md b/RUST_DIVERGENCES.md index f037033..127f528 100644 --- a/RUST_DIVERGENCES.md +++ b/RUST_DIVERGENCES.md @@ -1,74 +1,75 @@ # Compatibility with the Rust reference -This package tracks `bc-tags` **0.12.0**, commit -[`fc30b65c82eeca3124288fb9096c31a5631adc87`](https://github.com/BlockchainCommons/bc-tags-rust/commit/fc30b65c82eeca3124288fb9096c31a5631adc87). -The version and commit are recorded in [`.github/versions.yml`](./.github/versions.yml). - -## Resolved: default bignum registration - -TypeScript's `registerTags` calls dcbor's `registerStandardTags`, which -registers `date` (tag 1) and — since dcbor 1.0.0-beta.2 — the bignum tags -`positive-bignum` (2) and `negative-bignum` (3) only when asked -(`registerStandardTags(store, { bignum: true })`). Rust's -`bc_tags::register_tags_in` calls `dcbor::register_tags_in`, which names tags -2 and 3 only when dcbor's `num-bigint` feature is enabled; `bc-tags` depends -on dcbor without that feature, so the reference's default registry returns -the numeric names `"2"` and `"3"` and has no bignum summarizers. The -TypeScript registry now does the same: `registerTags` produces the -reference's default registry probe for probe (executed: the harness built -with dcbor's default features reports `"2"` / `"3"` on both sides). A -consumer that wants the `num-bigint` registry calls -`registerStandardTags(store, { bignum: true })` as well. - -Before dcbor 1.0.0-beta.2 the TypeScript registry always named tags 2 and 3, -and the harness enabled `num-bigint` to match it; the frozen baseline -(`tests/differential.test.ts`) still names them, and the differential pins -exactly that flip. bc-tags does not change CBOR tag numbers or encoded bytes. - -## Validation - -Run from `tests/rust-validation`: +`@blockchaincommons/tags` is a port of [bc-tags-rust](https://github.com/BlockchainCommons/bc-tags-rust) (crate `bc-tags`), **pinned at `bc-tags = 0.12.0`**, commit [`fc30b65`](https://github.com/BlockchainCommons/bc-tags-rust/commit/fc30b65c82eeca3124288fb9096c31a5631adc87), over `dcbor = 0.25.2`. The version and commit are recorded in [`.github/versions.yml`](./.github/versions.yml). The committed vectors (`tests/vectors/vectors.json`) are cross-validated by `tests/rust-validation/`. The harness runs against the build every Rust consumer uses (dcbor default features, no `num-bigint`), and again with `--features bignum` to validate the documented bignum recipe. ```sh -cargo run --release -- ../vectors/vectors.json +cd tests/rust-validation +cargo run --release --offline -- ../vectors/vectors.json +cargo run --release --offline --features bignum -- ../vectors/vectors.json ``` -Result on 2026-09-13, with dcbor's default features (no `num-bigint`): +Result on 2026-09-14: ``` -75 tags, 80 registry probes - 0 MISMATCH +75 tags, 75 identifiers, 80 registry probes, 75 order, 6 summarizers, 4 names, 6 conflicts, 6 format - 0 MISMATCH (bignum block: 9 skipped) +75 tags, 75 identifiers, 78 registry probes, 75 order, 4 summarizers, 2 names, 5 conflicts, 6 format, bignum block 9 - 0 MISMATCH (default-only: 7 skipped) ``` -The harness checks registered names by value, values by name, registry probes, -and the expected count of 75 constants. It does not exhaust every possible store -state or prove error-message equivalence. TypeScript tests additionally cover -registration and exported constants. No other behavioral difference is currently -recorded. - -## API and language mappings - -- **Constants:** Rust exposes numeric `TAG_` and string `TAG_NAME_` - constants; TypeScript exposes a frozen `Tag` carrying both. Use `.value` for - the number and `Tag.equals` for value equality, rather than object identity. -- **ALL_TAGS:** TypeScript exports the frozen registration list. Rust keeps its - registration list inside `register_tags_in`. -- **LEGACY_TAGS:** TypeScript groups the legacy constants under this object; - Rust exposes flat constants. Their values and names match. -- **Immutability:** TypeScript freezes tag objects and arrays. Assignments fail - with TypeError in strict-mode code; non-strict assignments may silently do - nothing. Rust constants cannot be mutated. -- **IANA tags:** URI (32), UUID (37), and encoded CBOR (24) use the same values, - whether imported from dcbor-ts or written directly in Rust's tag definitions. -- **Registration:** `registerTags(store?)` maps to `register_tags_in` for a - supplied store and `register_tags` for the global store. Repeating a matching - value/name registration is a no-op. Conflicting names for an existing value - cause a dcbor `CborError` (`Custom`) in TypeScript and a panic in Rust. These are - language-specific failure mechanisms, not a promise of identical exception types. +Against the pinned crate, the harness checks: +- every constant **by identifier** (`TAG_SEED` ↔ `bc_tags::TAG_SEED`/`TAG_NAME_SEED`; `LEGACY_TAGS.SEED_V1` ↔ `TAG_SEED_V1`), value and name, plus the crate's constant count; +- `name_for_value` for every tag value plus 1, 2, 3, 100 and 999999; +- `tag_for_name` for every tag name plus `date`, `positive-bignum`, `negative-bignum`; +- which tags have summarizers; +- the registration order; +- the panic text of conflicting registrations, and the store after registrations that re-point a name; +- annotated, summarised and hex-annotated rendering of tagged items through the registry. + +`fixtures/swapped-identifiers.json` must fail with two identifier mismatches. + +No tag number, name, order, message or encoded byte differs. + +## 1. True behavioral divergences (same input, different outcome) + +### 1.1 Conflicting registration + +When a store already holds one of these values under a different name: +- `bc_tags::register_tags_in` panics in dcbor's `TagsStore::insert`. +- `registerTags` throws dcbor's `CborError` with code `Custom`. + +Both fail at the same tag, in the same order, with the same text (`Attempt to register tag: 200 'not-envelope' with different name: 'envelope'`); this is a harness vector. Tags registered earlier in the same call stay registered on both sides. + +The store left behind is not a contract: +- **The conflicting value's entry.** The reference has already replaced it when it panics, and its name map is not updated. TypeScript leaves the entry unchanged, so a caught error has no effect on the entry it rejects. TypeScript could copy the reference's half-written state, but it would then leave `nameForValue` and `tagForName` disagreeing after an ordinary, recoverable error; the reference state is observable only through `catch_unwind`. +- **The global store.** A panic in `register_tags()` poisons the reference's global store, and every later access panics. The TypeScript global store stays usable. + +A name already registered under a different value moves back to the registered value on both sides, without an error. This includes dcbor's `date`, and with the bignum recipe `positive-bignum`/`negative-bignum`. The unnamed and empty-name registration errors cannot be reached through this package; see dcbor's `RUST_DIVERGENCES.md` §1.2. + +## 2. JS-only input domain (no Rust analog exists) + +- **Non-store arguments.** `registerTags(null)`, `registerTags({})` and other values that are not a `TagsStore` throw a `TypeError` from the first store call, before anything is registered. `undefined` selects the global store. +- **Mutating a tag.** Every constant, `LEGACY_TAGS` and `ALL_TAGS` are frozen, and dcbor stores and returns frozen tags. An assignment throws `TypeError` in strict-mode code (every ES module) and does nothing in sloppy-mode scripts, so a registered name cannot be changed through a `Tag`. + +## 3. Mapping equivalences (JS-specific inputs validated via their byte-target) + +- **Constants.** Rust's `TAG_: u64` and `TAG_NAME_: &str` are one frozen dcbor `Tag` here: use `.value` and `.name`. Equality is by value on both sides (`Tag.equals`, Rust's `PartialEq`), never by object identity. +- **Legacy tags.** Rust's flat `TAG_SEED_V1` … `TAG_ACCOUNT_V1` are `LEGACY_TAGS.SEED_V1` … `LEGACY_TAGS.ACCOUNT_V1`. +- **`ALL_TAGS`.** The reference's private registration vector, public and frozen here, in the same order. +- **IANA tags.** URI (32), UUID (37) and encoded CBOR (24) are written in this package, as Rust's `bc-tags` writes them; dcbor defines only the date and bignum tags on both sides. +- **Registration.** `registerTags(store?)` is `register_tags_in(&mut store)` with a store and `register_tags()` without one; repeating it is a no-op. +- **Global store.** `registerTags()` fills dcbor's single process-wide store. The ESM and CommonJS builds share it, as Rust has one `GLOBAL_TAGS` per semver-compatible dcbor. +- **No dcbor re-export.** Rust's `bc_tags` re-exports `dcbor::prelude::*`. Import `TagsStore`, `Tag` and the formatters from `@blockchaincommons/dcbor`. +- **Standard tags 2 and 3.** `registerTags` registers dcbor's standard tags as the reference's default build does: + - `date` (1) with its summarizer; + - tags 2 and 3 unnamed, with no summarizers, because `bc-tags` depends on dcbor without `num-bigint`. + + For the `num-bigint` registry (`positive-bignum`/`negative-bignum`, `bignum(…)` summaries), call `registerStandardTags(store, { bignum: true })` **before** `registerTags(store)`. That is the reference's registration order. The reverse order gives the same registry on a store without conflicts. The harness's `--features bignum` run validates this recipe. It needs `@blockchaincommons/dcbor` ≥ the declared floor. ## Maintenance -When the reference changes, review its diff, update `.github/versions.yml` and -the harness dependency, regenerate vectors, and run both package tests and Rust -validation. Keep the harness on dcbor's default features, as `bc-tags` is -built. Record newly observed -behavioral differences with reproducible inputs and outcomes. +When the reference changes: +- Review its diff and update `.github/versions.yml`. +- Update the harness pins (`bc-tags`, `dcbor`) and `RUST_TAG_COUNT` (`grep -c 'const_cbor_tag!'`). The identifier rows are generated from the vectors. +- Regenerate vectors (`bun run vectors:generate`, which also rewrites the swapped-identifier fixture). +- Run the package tests and both harness builds, and update the result lines above. + +Keep the default build as the reference, since that is how `bc-tags` is built; the `bignum` build validates only the recipe. Keep the `@blockchaincommons/dcbor` floor at a release whose standard-tag registration and global store match this record. Record any newly observed difference here with its input and both outcomes. diff --git a/api/index.d.mts b/api/index.d.mts index 9c91ea0..26bcf76 100644 --- a/api/index.d.mts +++ b/api/index.d.mts @@ -170,15 +170,22 @@ export declare const ALL_TAGS: readonly Tag[]; //#region src/register.d.ts /** * Register dcbor's standard tags and every tag in {@link ALL_TAGS} into - * `store` (default: the global store). + * `store` (default: the global store), in the reference's order: `date` + * (tag 1) with its summarizer, then the 75 tags of this package. * - * Idempotent: the store compares names, so a value already registered under - * the same name is a no-op, which is what makes repeated calls safe. + * Idempotent: a value already registered under the same name is a no-op, + * and a name already registered under another value moves to this + * package's value, as the reference's `insert_all` does. Tags 2 and 3 stay + * unnamed, as in the reference's default build; for the `num-bigint` + * registry call `registerStandardTags(store, { bignum: true })` before this + * function. * * @param store - The store to register into; defaults to dcbor's global store. - * @throws {Error} dcbor's store throws a bare `Error` when a value is - * already registered under a *different* name - * (`Attempt to register tag: 200 'foo' with different name: 'envelope'`). + * @throws {CborError} Code `Custom` (dcbor's store) when a value is already + * registered under a different name; the message is the reference's panic + * text, e.g. `Attempt to register tag: 200 'foo' with different name: 'envelope'`. + * Tags registered earlier in the same call stay registered, and the rejected + * entry is unchanged. */ export declare function registerTags(store?: TagsStore): void; //#endregion diff --git a/bun.lock b/bun.lock index 98be09d..70f8271 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@blockchaincommons/tags", "dependencies": { - "@blockchaincommons/dcbor": "^1.0.0-beta.1", + "@blockchaincommons/dcbor": "^1.0.0-beta.3", }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", @@ -45,7 +45,7 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@blockchaincommons/dcbor": ["@blockchaincommons/dcbor@1.0.0-beta.1", "", {}, "sha512-VJrG/3KTYtSP0UJel1MA3HpatTpsgtbre/W9ejfjepAaNCJchJgv6Efv1UDPwLdS1MhJtXd2bM3GNUSK79dhdw=="], + "@blockchaincommons/dcbor": ["@blockchaincommons/dcbor@1.0.0-beta.3", "", {}, "sha512-WMX+A+ohCNl5vbq8jv+8/N+ygmgFG9V2V60dggRa8WfO+Ab/vvhl7W8mMeY9Rm9iSll3CK1q0bSNoWHPWfu9eg=="], "@braidai/lang": ["@braidai/lang@1.1.2", "", {}, "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA=="], @@ -215,11 +215,11 @@ "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - "@size-limit/esbuild": ["@size-limit/esbuild@13.0.3", "", { "dependencies": { "esbuild": "^0.28.1", "nanoid": "^6.0.0" }, "peerDependencies": { "size-limit": "13.0.3" } }, "sha512-g24wsTxM3N/SaGv1MiiDjTShNrIna1WCNJ7NXMrlsWBHWv1OL0nCyllR1bDDHf+gV41lF7QxX7vknswoOr71DQ=="], + "@size-limit/esbuild": ["@size-limit/esbuild@13.1.1", "", { "dependencies": { "esbuild": "^0.28.2", "nanoid": "^6.0.1" }, "peerDependencies": { "size-limit": "13.1.1" } }, "sha512-+aWz9zUJTtnkc1UejLzrFGBOmM/H6QpYvL39BfgmxUULCeLn6xMb2sQw2w8n1CaF70/DvH9/J9QdNl4kAhdrHA=="], - "@size-limit/file": ["@size-limit/file@13.0.3", "", { "peerDependencies": { "size-limit": "13.0.3" } }, "sha512-PWTITIXH5p9aGIf6qq2Fruihn/b9nBQyfkyoAyb6DzFJgS1Ek9MSPJYKxKFLO8jdo0aqSgBPd3sevbS6PyBiJw=="], + "@size-limit/file": ["@size-limit/file@13.1.1", "", { "peerDependencies": { "size-limit": "13.1.1" } }, "sha512-hWsrLRKBSh1vVFa86e2WcpBWcTYnwRvvZ54o6wzmPduhYUQPgOCr9eWqNMLCxvqLP5EiJeGCwLQFDjdgusGFSw=="], - "@size-limit/preset-small-lib": ["@size-limit/preset-small-lib@13.0.3", "", { "dependencies": { "@size-limit/esbuild": "13.0.3", "@size-limit/file": "13.0.3", "size-limit": "13.0.3" } }, "sha512-rqKn1+JkVF5ckZRmcxeQPZ8g0e9Fqddh6bjmDotijXJtN40KJQ+5TG6pTiYq3RaA4nkuEkixHULpDBGcgAARAg=="], + "@size-limit/preset-small-lib": ["@size-limit/preset-small-lib@13.1.1", "", { "dependencies": { "@size-limit/esbuild": "13.1.1", "@size-limit/file": "13.1.1", "size-limit": "13.1.1" } }, "sha512-by5zdyqKuIU3umUo3WQFwDyv1/cOJnYR/cE8VEZeW9lPRyfaExiWu1A3jzIn+LA4GGZaYvQ5Z6THHMlGij9Jmw=="], "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], @@ -581,9 +581,9 @@ "magic-string": ["magic-string@1.3.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg=="], - "magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="], + "magicast": ["magicast@0.5.5", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA=="], - "markdown-it": ["markdown-it@14.3.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA=="], + "markdown-it": ["markdown-it@14.3.2", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q=="], "marked": ["marked@9.1.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q=="], @@ -671,7 +671,7 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "size-limit": ["size-limit@13.0.3", "", { "dependencies": { "bytes-iec": "^3.1.1", "lilconfig": "^3.1.3", "nanospinner": "^1.2.2" }, "bin": { "size-limit": "bin.js" } }, "sha512-KVb2aNEU49BwTR21SVjD+2QHP9gBV/nWsTHzNB/heRwXtHyA7lLQiDZDQ1TiNh/B/TZXKAZrHYyTt+cvBUrzYw=="], + "size-limit": ["size-limit@13.1.1", "", { "dependencies": { "bytes-iec": "^3.1.1", "lilconfig": "^3.1.3", "nanospinner": "^1.2.2" }, "bin": { "size-limit": "bin.js" } }, "sha512-Yl3CuQFSB3TSirrg9jyD/ahEUa6+aoMWj4UuucW36XunEsBfYhRACZRQr7vaUKoLkS88Njw0czdpDQFfbvCc+Q=="], "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], @@ -753,7 +753,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], "yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], @@ -787,8 +787,6 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "ajv-draft-04/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -815,8 +813,6 @@ "@rushstack/node-core-library/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "ajv-draft-04/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], } } diff --git a/examples/register-and-annotate.ts b/examples/register-and-annotate.ts index 098565a..452596a 100644 --- a/examples/register-and-annotate.ts +++ b/examples/register-and-annotate.ts @@ -8,7 +8,7 @@ import { Tag, TagsStore, bytesToHex, encodeCbor, taggedValue } from "@blockchain import { diagnostic } from "@blockchaincommons/dcbor/diagnostic"; import { ALL_TAGS, LEGACY_TAGS, TAG_ENVELOPE, TAG_LEAF, registerTags } from "../src/index"; -// dcbor's standard tags first (date, bignums), then all 75 of this package. +// dcbor's standard tags first (date), then all 75 of this package. const store = new TagsStore(); registerTags(store); diff --git a/package.json b/package.json index fd0776b..0505297 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockchaincommons/tags", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "type": "module", "sideEffects": false, "description": "Blockchain Commons CBOR Tags Registry for TypeScript", @@ -100,6 +100,6 @@ "vitest": "^5.0.0" }, "dependencies": { - "@blockchaincommons/dcbor": "^1.0.0-beta.1" + "@blockchaincommons/dcbor": "^1.0.0-beta.3" } } diff --git a/scripts/generate-vectors.ts b/scripts/generate-vectors.ts index 55d8108..3db967c 100644 --- a/scripts/generate-vectors.ts +++ b/scripts/generate-vectors.ts @@ -1,16 +1,44 @@ /** * Golden vector generator. `bun scripts/generate-vectors.ts`. - * Writes tests/vectors/vectors.json from the WORKING TREE. Regenerating is a - * deliberate, reviewed act: a changed value or name is a wire change. + * Writes tests/vectors/vectors.json from the WORKING TREE: the tag table and + * registry, plus the semantic blocks (order, summarizers, names, conflicts, + * rendering, the bignum recipe). Also writes the harness's negative fixture, + * tests/rust-validation/fixtures/swapped-identifiers.json, whose two X25519 + * constant labels are swapped so the identifier check is proven to bite. + * Regenerating is a deliberate, reviewed act: a changed value or name is a + * wire change. */ -import { writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import * as src from "../src/index.ts"; -import { redesignedVectors } from "../tests/vectors/table.ts"; +import { redesignedVectors, type Vectors } from "../tests/vectors/table.ts"; import { makeStoreFor } from "../tests/vectors/store.ts"; +import { semanticsVectors } from "../tests/vectors/semantics.ts"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); -const vectors = redesignedVectors(src, await makeStoreFor(src)); +const base = redesignedVectors(src, await makeStoreFor(src)); +const vectors: Vectors = { ...base, ...semanticsVectors(src) }; writeFileSync(join(root, "tests/vectors/vectors.json"), JSON.stringify(vectors, null, 1) + "\n"); -console.log(`wrote ${vectors.count} tags, ${vectors.registry.length} registry probes`); + +const SWAP: Record = { + TAG_X25519_PRIVATE_KEY: "TAG_X25519_PUBLIC_KEY", + TAG_X25519_PUBLIC_KEY: "TAG_X25519_PRIVATE_KEY", +}; +const swapped: Vectors = { + count: base.count, + table: base.table.map((t) => ({ ...t, const: SWAP[t.const] ?? t.const })), + registry: base.registry, +}; +const fixtures = join(root, "tests/rust-validation/fixtures"); +mkdirSync(fixtures, { recursive: true }); +writeFileSync(join(fixtures, "swapped-identifiers.json"), JSON.stringify(swapped, null, 1) + "\n"); + +const s = vectors; +console.log( + `wrote ${s.count} tags, ${s.registry.length} registry probes, ${s.order?.length ?? 0} order, ` + + `${s.summarizers?.length ?? 0} summarizers, ${s.names?.length ?? 0} names, ` + + `${s.conflicts?.length ?? 0} conflicts, ${s.format?.length ?? 0} format, ` + + `bignum block ${(s.bignum?.registry.length ?? 0) + (s.bignum?.summarizers.length ?? 0) + (s.bignum?.names.length ?? 0) + (s.bignum?.conflicts.length ?? 0)}; ` + + `swapped-identifiers fixture`, +); diff --git a/src/register.ts b/src/register.ts index d69570d..c36226a 100644 --- a/src/register.ts +++ b/src/register.ts @@ -8,17 +8,24 @@ import { ALL_TAGS } from "./tags.js"; /** * Register dcbor's standard tags and every tag in {@link ALL_TAGS} into - * `store` (default: the global store). + * `store` (default: the global store), in the reference's order: `date` + * (tag 1) with its summarizer, then the 75 tags of this package. * - * Idempotent: the store compares names, so a value already registered under - * the same name is a no-op, which is what makes repeated calls safe. + * Idempotent: a value already registered under the same name is a no-op, + * and a name already registered under another value moves to this + * package's value, as the reference's `insert_all` does. Tags 2 and 3 stay + * unnamed, as in the reference's default build; for the `num-bigint` + * registry call `registerStandardTags(store, { bignum: true })` before this + * function. * * @param store - The store to register into; defaults to dcbor's global store. - * @throws {Error} dcbor's store throws a bare `Error` when a value is - * already registered under a *different* name - * (`Attempt to register tag: 200 'foo' with different name: 'envelope'`). + * @throws {CborError} Code `Custom` (dcbor's store) when a value is already + * registered under a different name; the message is the reference's panic + * text, e.g. `Attempt to register tag: 200 'foo' with different name: 'envelope'`. + * Tags registered earlier in the same call stay registered, and the rejected + * entry is unchanged. */ export function registerTags(store: TagsStore = getGlobalTagsStore()): void { registerStandardTags(store); - store.registerAll([...ALL_TAGS]); + store.registerAll(ALL_TAGS); } diff --git a/src/tags.ts b/src/tags.ts index d1806bb..38410bc 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -12,27 +12,25 @@ * @see https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md * @module tags */ -import { - Tag, - TAG_ENCODED_CBOR as IANA_ENCODED_CBOR, - TAG_URI as IANA_URI, - TAG_UUID as IANA_UUID, -} from "@blockchaincommons/dcbor"; +import { Tag } from "@blockchaincommons/dcbor"; /** - * One immutable tag. dcbor's `Tag.from` returns a plain object whose - * `readonly` is a compile-time promise only; the names here are wire, - * so nothing may rewrite them process-wide. + * One immutable tag. dcbor's `Tag.from` returns a frozen object (since + * 1.0.0-beta.3, as the reference's `Tag` is a value); the freeze here is + * kept as defence in depth so the constants stay frozen on any dcbor the + * floor admits. The names are wire, so nothing may rewrite them + * process-wide. */ const tag = (value: number, name: string): Tag => Object.freeze(Tag.from(value, name)); -// IANA standard tags this stack uses. dcbor owns the numbers (it defines -// them unnamed); this registry owns the names. +// IANA standard tags this stack uses. The numbers are written here, as the +// Rust reference writes them in `bc-tags` (dcbor defines only the date and +// bignum tags); this registry owns the names. /** #6.32: URI (RFC 8949 §3.4.5.3), named `url` in this stack. */ -export const TAG_URI: Tag = tag(IANA_URI, "url"); +export const TAG_URI: Tag = tag(32, "url"); /** #6.37: binary UUID (RFC 4122). */ -export const TAG_UUID: Tag = tag(IANA_UUID, "uuid"); +export const TAG_UUID: Tag = tag(37, "uuid"); // Core Envelope tags. #6.24 was used as the leaf header by an earlier spec // (incorrectly: RFC 8949 §3.4.5.1 requires a byte string); #6.201 replaced @@ -40,7 +38,7 @@ export const TAG_UUID: Tag = tag(IANA_UUID, "uuid"); // Blockchain Commons tags in IANA's "Specification Required" range. /** #6.24: encoded CBOR data item; the pre-#6.201 Envelope leaf header, accepted on decode only. */ -export const TAG_ENCODED_CBOR: Tag = tag(IANA_ENCODED_CBOR, "encoded-cbor"); +export const TAG_ENCODED_CBOR: Tag = tag(24, "encoded-cbor"); /** #6.200: Gordian Envelope. */ export const TAG_ENVELOPE: Tag = tag(200, "envelope"); /** #6.201: dCBOR data item; the Envelope leaf case. */ diff --git a/tests/dist-packaging.test.ts b/tests/dist-packaging.test.ts index 84aebc0..9a48c1b 100644 --- a/tests/dist-packaging.test.ts +++ b/tests/dist-packaging.test.ts @@ -70,4 +70,24 @@ describe.skipIf(!built)("dist packaging", () => { .sort(); expect(names(cjs)).toEqual(names(esm)); }); + + // dcbor keeps its global store on `globalThis`, so the CommonJS build of + // this package and the ESM build of dcbor resolve one store, as the + // reference has one `GLOBAL_TAGS` per process (N12). + it("registerTags through the CJS entry names the ESM global store", async () => { + const require_ = createRequire(import.meta.url); + let cjs: { registerTags(): void }; + try { + cjs = require_(join(dist, "index.cjs")) as { registerTags(): void }; + } catch (error) { + console.warn(`CJS entry could not be loaded in this environment: ${String(error)}`); + return; + } + const dcbor = (await import("@blockchaincommons/dcbor")) as { + getGlobalTagsStore(): { nameForValue(value: number): string }; + }; + cjs.registerTags(); + expect(dcbor.getGlobalTagsStore().nameForValue(200)).toBe("envelope"); + expect(dcbor.getGlobalTagsStore().nameForValue(1347571542)).toBe("provenance"); + }); }); diff --git a/tests/global-store.test.ts b/tests/global-store.test.ts new file mode 100644 index 0000000..d64af36 --- /dev/null +++ b/tests/global-store.test.ts @@ -0,0 +1,42 @@ +/** + * The global store after a conflict (N3) and non-store arguments (N5). In + * their own file so the global store these tests dirty is isolated from + * every other suite. + */ +import { CborError, Tag, TagsStore, getGlobalTagsStore } from "@blockchaincommons/dcbor"; +import { registerTags } from "../src"; + +describe("global store contract (Rust parity)", () => { + it("N3: a conflict on the global store throws CborError Custom and leaves the store usable", () => { + const global = getGlobalTagsStore(); + expect(global.tagForValue(200)).toBeUndefined(); + global.register(Tag.from(200, "not-envelope")); + + let err: unknown; + try { + registerTags(); + } catch (e) { + err = e; + } + expect(CborError.isCborError(err) && err.code === "Custom").toBe(true); + expect((err as Error).message).toBe( + "Attempt to register tag: 200 'not-envelope' with different name: 'envelope'", + ); + + // The reference's `register_tags()` poisons its global Mutex here and every + // later access panics; the TypeScript global store keeps answering. + expect(getGlobalTagsStore().nameForValue(200)).toBe("not-envelope"); + expect(getGlobalTagsStore().nameForValue(32)).toBe("url"); + const fresh = new TagsStore(); + registerTags(fresh); + expect(fresh.nameForValue(200)).toBe("envelope"); + }); + + it("N5: a non-store argument throws a TypeError before anything is registered", () => { + const before = getGlobalTagsStore().tagForValue(201); + expect(() => registerTags(null as never)).toThrow(TypeError); + expect(() => registerTags({} as never)).toThrow(TypeError); + expect(() => registerTags(42 as never)).toThrow(TypeError); + expect(getGlobalTagsStore().tagForValue(201)).toBe(before); + }); +}); diff --git a/tests/golden-vectors.test.ts b/tests/golden-vectors.test.ts index 3252bf3..7bc74de 100644 --- a/tests/golden-vectors.test.ts +++ b/tests/golden-vectors.test.ts @@ -1,5 +1,5 @@ /** - * Golden vector suite: the committed table and registry. + * Golden vector suite: the committed table, registry and semantic blocks. * Changes only through `bun run vectors:generate`. */ import { readFileSync } from "node:fs"; @@ -8,15 +8,19 @@ import { fileURLToPath } from "node:url"; import * as tags from "../src"; import { redesignedVectors, type Vectors } from "./vectors/table"; import { makeStoreFor } from "./vectors/store"; +import { semanticsVectors } from "./vectors/semantics"; const here = dirname(fileURLToPath(import.meta.url)); const frozen = JSON.parse(readFileSync(join(here, "vectors/vectors.json"), "utf8")) as Vectors; const current = redesignedVectors(tags, await makeStoreFor(tags)); +const semantics = semanticsVectors(tags); describe("golden vectors (frozen)", () => { it("fixture is self-consistent and non-trivial", () => { expect(frozen.table.length).toBe(frozen.count); expect(frozen.count).toBeGreaterThanOrEqual(70); + expect(frozen.order?.length).toBe(frozen.count); + expect(frozen.format?.length).toBeGreaterThan(0); }); it("table is identical", () => { expect(current.table).toEqual(frozen.table); @@ -24,4 +28,22 @@ describe("golden vectors (frozen)", () => { it("registry is identical", () => { expect(current.registry).toEqual(frozen.registry); }); + it("registration order is identical", () => { + expect(semantics.order).toEqual(frozen.order); + }); + it("summarizer presence is identical", () => { + expect(semantics.summarizers).toEqual(frozen.summarizers); + }); + it("dcbor names are identical", () => { + expect(semantics.names).toEqual(frozen.names); + }); + it("conflict outcomes are identical", () => { + expect(semantics.conflicts).toEqual(frozen.conflicts); + }); + it("renderings through the registry are identical", () => { + expect(semantics.format).toEqual(frozen.format); + }); + it("the bignum recipe's registry is identical", () => { + expect(semantics.bignum).toEqual(frozen.bignum); + }); }); diff --git a/tests/rust-validation/Cargo.lock b/tests/rust-validation/Cargo.lock index d7e5f3d..b5cdedf 100644 --- a/tests/rust-validation/Cargo.lock +++ b/tests/rust-validation/Cargo.lock @@ -83,6 +83,7 @@ dependencies = [ "chrono", "half", "hex", + "num-bigint", "paste", "thiserror", "unicode-normalization", @@ -194,6 +195,25 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -328,6 +348,7 @@ version = "0.1.0" dependencies = [ "bc-tags", "dcbor", + "hex", "serde", "serde_json", ] diff --git a/tests/rust-validation/Cargo.toml b/tests/rust-validation/Cargo.toml index 7f45def..172baac 100644 --- a/tests/rust-validation/Cargo.toml +++ b/tests/rust-validation/Cargo.toml @@ -3,11 +3,24 @@ name = "tags-validation" version = "0.1.0" edition = "2021" publish = false +description = "Cross-validates @blockchaincommons/tags vectors against the Rust reference (bc-tags-rust over dcbor)." [dependencies] bc-tags = "=0.12.0" -# TypeScript always registers bignum tags. Enable the corresponding Rust -# feature for comparison; see RUST_DIVERGENCES.md. Without it, probes 2 and 3 differ. -dcbor = "0.25" +# Default features = the reference build: `bc-tags` depends on dcbor without +# `num-bigint`, so tags 2 and 3 stay unnamed. `--features bignum` validates the +# documented opt-in recipe only. +dcbor = "=0.25.2" serde = { version = "1", features = ["derive"] } serde_json = "1" +hex = "0.4" + +[build-dependencies] +# build.rs generates the identifier table from ../vectors/vectors.json. +serde_json = "1" + +[features] +# `cargo run --release --offline --features bignum -- ../vectors/vectors.json`: +# the reference with `num-bigint`, which names tags 2/3 and installs their +# summarizers; validates the `registerStandardTags(store, { bignum: true })` recipe. +bignum = ["dcbor/num-bigint"] diff --git a/tests/rust-validation/README.md b/tests/rust-validation/README.md index ea9fa7f..6ad8678 100644 --- a/tests/rust-validation/README.md +++ b/tests/rust-validation/README.md @@ -1,16 +1,65 @@ # Rust reference cross-validation -Replays `tests/vectors/vectors.json` against `bc-tags = 0.12.0`. +Replays `tests/vectors/vectors.json` against `bc-tags = 0.12.0` over +`dcbor = 0.25.2`, in the two builds that matter: ```sh cd tests/rust-validation -cargo run --release -- ../vectors/vectors.json +cargo run --release --offline -- ../vectors/vectors.json +cargo run --release --offline --features bignum -- ../vectors/vectors.json ``` -Checks, for every TypeScript tag constant, that `register_tags_in` on a -fresh `dcbor::TagsStore` maps the value to the same name and the name to the -same value; that every registry probe (`name_for_value`) matches; and that -the number of constants equals the number of `const_cbor_tag!` lines in the -crate (75), so a tag added on one side only cannot go unnoticed. Exit 0 iff -everything matches. Not wired into CI (needs a Rust toolchain); run manually -before any release. +The default build is the reference every Rust consumer uses: `bc-tags` +depends on dcbor without its `num-bigint` feature, so tags 2 and 3 are +unnamed and have no summarizers. The `bignum` build validates only the +documented opt-in recipe (`registerStandardTags(store, { bignum: true })` +before `registerTags(store)`), through the vectors' `bignum` block. Rows +that describe one build are skipped by the other and counted in its result +line. + +## What is checked + +Against the pinned crate, on a fresh store filled by `register_tags_in`: + +- every constant **by identifier**, value and name: `build.rs` compiles a + table of `bc_tags::TAG_` / `TAG_NAME_` from the vectors' `table` + (`LEGACY_TAGS.SEED_V1` ↔ `TAG_SEED_V1`), so an identifier the crate lacks + fails the build, and `RUST_TAG_COUNT` (75, `grep -c 'const_cbor_tag!'`) + pins the other direction; +- `tag_for_value` and `tag_for_name` for every table entry; +- `name_for_value` for every table value plus 1, 2, 3, 100 and 999999 + (`registry`); +- the registration order (`order`): the store is pre-filled with every value + under a bogus name and `register_tags_in` panics its way through, one tag + per call; +- which tags carry summarizers (`summarizers`); +- `tag_for_name` for dcbor's own names and an unknown one (`names`); +- the panic text of a conflicting registration, and the store after a + registration that re-points a name (`conflicts`; the post-panic store is + not compared, see `RUST_DIVERGENCES.md` §1.1); +- annotated, summarised and hex-annotated rendering of tagged items through + the registry (`format`). + +Exit 0 iff every row matches. The result line names every block: + +``` +75 tags, 75 identifiers, 80 registry probes, 75 order, 6 summarizers, 4 names, 6 conflicts, 6 format - 0 MISMATCH (bignum block: 9 skipped) +75 tags, 75 identifiers, 78 registry probes, 75 order, 4 summarizers, 2 names, 5 conflicts, 6 format, bignum block 9 - 0 MISMATCH (default-only: 7 skipped) +``` + +## Negative fixture + +`fixtures/swapped-identifiers.json` is the vectors' `table` with the +`TAG_X25519_PRIVATE_KEY` and `TAG_X25519_PUBLIC_KEY` labels swapped. It must +exit 1 with exactly two `MISMATCH ident` lines; CI checks that it does. + +```sh +cargo run --release --offline -- fixtures/swapped-identifiers.json +``` + +## Maintenance + +`bun run vectors:generate` rewrites the vectors and the fixture from the +working tree. At a pin bump, update `bc-tags`/`dcbor` in `Cargo.toml`, +`RUST_TAG_COUNT` in `src/main.rs`, and `.github/versions.yml`. The CI job +`rust-validation` runs both builds and the fixture with `--locked`. diff --git a/tests/rust-validation/build.rs b/tests/rust-validation/build.rs new file mode 100644 index 0000000..aedd17a --- /dev/null +++ b/tests/rust-validation/build.rs @@ -0,0 +1,29 @@ +//! Generates `$OUT_DIR/idents.rs` from `../vectors/vectors.json`: one +//! `(identifier, bc_tags::TAG_X, bc_tags::TAG_NAME_X)` row per table entry. +//! +//! The TypeScript export name is the Rust identifier (`TAG_SEED` ↔ +//! `bc_tags::TAG_SEED`); a legacy entry grouped under `LEGACY_TAGS` +//! (`SEED_V1`) is the flat `bc_tags::TAG_SEED_V1`. An identifier the crate +//! does not define fails to compile, so a constant that exists on one side +//! only cannot pass. +use std::{env, fs, path::Path}; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../vectors/vectors.json"); + let json = fs::read_to_string("../vectors/vectors.json").expect("read ../vectors/vectors.json"); + let file: serde_json::Value = serde_json::from_str(&json).expect("parse ../vectors/vectors.json"); + let mut out = String::from( + "/// Generated by build.rs from ../vectors/vectors.json: (identifier, value, name).\n\ + pub const IDENTS: &[(&str, u64, &str)] = &[\n", + ); + for entry in file["table"].as_array().expect("table") { + let c = entry["const"].as_str().expect("const"); + let ident = if c.starts_with("TAG_") { c.to_string() } else { format!("TAG_{c}") }; + let name_ident = format!("TAG_NAME_{}", &ident[4..]); + out.push_str(&format!(" ({ident:?}, bc_tags::{ident}, bc_tags::{name_ident}),\n")); + } + out.push_str("];\n"); + let dest = Path::new(&env::var("OUT_DIR").expect("OUT_DIR")).join("idents.rs"); + fs::write(dest, out).expect("write idents.rs"); +} diff --git a/tests/rust-validation/fixtures/swapped-identifiers.json b/tests/rust-validation/fixtures/swapped-identifiers.json new file mode 100644 index 0000000..68fa378 --- /dev/null +++ b/tests/rust-validation/fixtures/swapped-identifiers.json @@ -0,0 +1,702 @@ +{ + "count": 75, + "table": [ + { + "const": "TAG_ENCODED_CBOR", + "value": 24, + "name": "encoded-cbor" + }, + { + "const": "TAG_URI", + "value": 32, + "name": "url" + }, + { + "const": "TAG_UUID", + "value": 37, + "name": "uuid" + }, + { + "const": "TAG_ENVELOPE", + "value": 200, + "name": "envelope" + }, + { + "const": "TAG_LEAF", + "value": 201, + "name": "leaf" + }, + { + "const": "TAG_JSON", + "value": 262, + "name": "json" + }, + { + "const": "SEED_V1", + "value": 300, + "name": "crypto-seed" + }, + { + "const": "HDKEY_V1", + "value": 303, + "name": "crypto-hdkey" + }, + { + "const": "DERIVATION_PATH_V1", + "value": 304, + "name": "crypto-keypath" + }, + { + "const": "USE_INFO_V1", + "value": 305, + "name": "crypto-coin-info" + }, + { + "const": "EC_KEY_V1", + "value": 306, + "name": "crypto-eckey" + }, + { + "const": "OUTPUT_DESCRIPTOR_V1", + "value": 307, + "name": "crypto-output" + }, + { + "const": "SSKR_SHARE_V1", + "value": 309, + "name": "crypto-sskr" + }, + { + "const": "PSBT_V1", + "value": 310, + "name": "crypto-psbt" + }, + { + "const": "ACCOUNT_V1", + "value": 311, + "name": "crypto-account" + }, + { + "const": "TAG_OUTPUT_SCRIPT_HASH", + "value": 400, + "name": "output-script-hash" + }, + { + "const": "TAG_OUTPUT_WITNESS_SCRIPT_HASH", + "value": 401, + "name": "output-witness-script-hash" + }, + { + "const": "TAG_OUTPUT_PUBLIC_KEY", + "value": 402, + "name": "output-public-key" + }, + { + "const": "TAG_OUTPUT_PUBLIC_KEY_HASH", + "value": 403, + "name": "output-public-key-hash" + }, + { + "const": "TAG_OUTPUT_WITNESS_PUBLIC_KEY_HASH", + "value": 404, + "name": "output-witness-public-key-hash" + }, + { + "const": "TAG_OUTPUT_COMBO", + "value": 405, + "name": "output-combo" + }, + { + "const": "TAG_OUTPUT_MULTISIG", + "value": 406, + "name": "output-multisig" + }, + { + "const": "TAG_OUTPUT_SORTED_MULTISIG", + "value": 407, + "name": "output-sorted-multisig" + }, + { + "const": "TAG_OUTPUT_RAW_SCRIPT", + "value": 408, + "name": "output-raw-script" + }, + { + "const": "TAG_OUTPUT_TAPROOT", + "value": 409, + "name": "output-taproot" + }, + { + "const": "TAG_OUTPUT_COSIGNER", + "value": 410, + "name": "output-cosigner" + }, + { + "const": "TAG_KNOWN_VALUE", + "value": 40000, + "name": "known-value" + }, + { + "const": "TAG_DIGEST", + "value": 40001, + "name": "digest" + }, + { + "const": "TAG_ENCRYPTED", + "value": 40002, + "name": "encrypted" + }, + { + "const": "TAG_COMPRESSED", + "value": 40003, + "name": "compressed" + }, + { + "const": "TAG_REQUEST", + "value": 40004, + "name": "request" + }, + { + "const": "TAG_RESPONSE", + "value": 40005, + "name": "response" + }, + { + "const": "TAG_FUNCTION", + "value": 40006, + "name": "function" + }, + { + "const": "TAG_PARAMETER", + "value": 40007, + "name": "parameter" + }, + { + "const": "TAG_PLACEHOLDER", + "value": 40008, + "name": "placeholder" + }, + { + "const": "TAG_REPLACEMENT", + "value": 40009, + "name": "replacement" + }, + { + "const": "TAG_X25519_PUBLIC_KEY", + "value": 40010, + "name": "agreement-private-key" + }, + { + "const": "TAG_X25519_PRIVATE_KEY", + "value": 40011, + "name": "agreement-public-key" + }, + { + "const": "TAG_ARID", + "value": 40012, + "name": "arid" + }, + { + "const": "TAG_PRIVATE_KEYS", + "value": 40013, + "name": "crypto-prvkeys" + }, + { + "const": "TAG_NONCE", + "value": 40014, + "name": "nonce" + }, + { + "const": "TAG_PASSWORD", + "value": 40015, + "name": "password" + }, + { + "const": "TAG_PRIVATE_KEY_BASE", + "value": 40016, + "name": "crypto-prvkey-base" + }, + { + "const": "TAG_PUBLIC_KEYS", + "value": 40017, + "name": "crypto-pubkeys" + }, + { + "const": "TAG_SALT", + "value": 40018, + "name": "salt" + }, + { + "const": "TAG_SEALED_MESSAGE", + "value": 40019, + "name": "crypto-sealed" + }, + { + "const": "TAG_SIGNATURE", + "value": 40020, + "name": "signature" + }, + { + "const": "TAG_SIGNING_PRIVATE_KEY", + "value": 40021, + "name": "signing-private-key" + }, + { + "const": "TAG_SIGNING_PUBLIC_KEY", + "value": 40022, + "name": "signing-public-key" + }, + { + "const": "TAG_SYMMETRIC_KEY", + "value": 40023, + "name": "crypto-key" + }, + { + "const": "TAG_XID", + "value": 40024, + "name": "xid" + }, + { + "const": "TAG_REFERENCE", + "value": 40025, + "name": "reference" + }, + { + "const": "TAG_EVENT", + "value": 40026, + "name": "event" + }, + { + "const": "TAG_ENCRYPTED_KEY", + "value": 40027, + "name": "encrypted-key" + }, + { + "const": "TAG_MLKEM_PRIVATE_KEY", + "value": 40100, + "name": "mlkem-private-key" + }, + { + "const": "TAG_MLKEM_PUBLIC_KEY", + "value": 40101, + "name": "mlkem-public-key" + }, + { + "const": "TAG_MLKEM_CIPHERTEXT", + "value": 40102, + "name": "mlkem-ciphertext" + }, + { + "const": "TAG_MLDSA_PRIVATE_KEY", + "value": 40103, + "name": "mldsa-private-key" + }, + { + "const": "TAG_MLDSA_PUBLIC_KEY", + "value": 40104, + "name": "mldsa-public-key" + }, + { + "const": "TAG_MLDSA_SIGNATURE", + "value": 40105, + "name": "mldsa-signature" + }, + { + "const": "TAG_SEED", + "value": 40300, + "name": "seed" + }, + { + "const": "TAG_HDKEY", + "value": 40303, + "name": "hdkey" + }, + { + "const": "TAG_DERIVATION_PATH", + "value": 40304, + "name": "keypath" + }, + { + "const": "TAG_USE_INFO", + "value": 40305, + "name": "coin-info" + }, + { + "const": "TAG_EC_KEY", + "value": 40306, + "name": "eckey" + }, + { + "const": "TAG_ADDRESS", + "value": 40307, + "name": "address" + }, + { + "const": "TAG_OUTPUT_DESCRIPTOR", + "value": 40308, + "name": "output-descriptor" + }, + { + "const": "TAG_SSKR_SHARE", + "value": 40309, + "name": "sskr" + }, + { + "const": "TAG_PSBT", + "value": 40310, + "name": "psbt" + }, + { + "const": "TAG_ACCOUNT_DESCRIPTOR", + "value": 40311, + "name": "account-descriptor" + }, + { + "const": "TAG_SSH_TEXT_PRIVATE_KEY", + "value": 40800, + "name": "ssh-private" + }, + { + "const": "TAG_SSH_TEXT_PUBLIC_KEY", + "value": 40801, + "name": "ssh-public" + }, + { + "const": "TAG_SSH_TEXT_SIGNATURE", + "value": 40802, + "name": "ssh-signature" + }, + { + "const": "TAG_SSH_TEXT_CERTIFICATE", + "value": 40803, + "name": "ssh-certificate" + }, + { + "const": "TAG_PROVENANCE_MARK", + "value": 1347571542, + "name": "provenance" + } + ], + "registry": [ + [ + 1, + "date" + ], + [ + 2, + "2" + ], + [ + 3, + "3" + ], + [ + 24, + "encoded-cbor" + ], + [ + 32, + "url" + ], + [ + 37, + "uuid" + ], + [ + 100, + "100" + ], + [ + 200, + "envelope" + ], + [ + 201, + "leaf" + ], + [ + 262, + "json" + ], + [ + 300, + "crypto-seed" + ], + [ + 303, + "crypto-hdkey" + ], + [ + 304, + "crypto-keypath" + ], + [ + 305, + "crypto-coin-info" + ], + [ + 306, + "crypto-eckey" + ], + [ + 307, + "crypto-output" + ], + [ + 309, + "crypto-sskr" + ], + [ + 310, + "crypto-psbt" + ], + [ + 311, + "crypto-account" + ], + [ + 400, + "output-script-hash" + ], + [ + 401, + "output-witness-script-hash" + ], + [ + 402, + "output-public-key" + ], + [ + 403, + "output-public-key-hash" + ], + [ + 404, + "output-witness-public-key-hash" + ], + [ + 405, + "output-combo" + ], + [ + 406, + "output-multisig" + ], + [ + 407, + "output-sorted-multisig" + ], + [ + 408, + "output-raw-script" + ], + [ + 409, + "output-taproot" + ], + [ + 410, + "output-cosigner" + ], + [ + 40000, + "known-value" + ], + [ + 40001, + "digest" + ], + [ + 40002, + "encrypted" + ], + [ + 40003, + "compressed" + ], + [ + 40004, + "request" + ], + [ + 40005, + "response" + ], + [ + 40006, + "function" + ], + [ + 40007, + "parameter" + ], + [ + 40008, + "placeholder" + ], + [ + 40009, + "replacement" + ], + [ + 40010, + "agreement-private-key" + ], + [ + 40011, + "agreement-public-key" + ], + [ + 40012, + "arid" + ], + [ + 40013, + "crypto-prvkeys" + ], + [ + 40014, + "nonce" + ], + [ + 40015, + "password" + ], + [ + 40016, + "crypto-prvkey-base" + ], + [ + 40017, + "crypto-pubkeys" + ], + [ + 40018, + "salt" + ], + [ + 40019, + "crypto-sealed" + ], + [ + 40020, + "signature" + ], + [ + 40021, + "signing-private-key" + ], + [ + 40022, + "signing-public-key" + ], + [ + 40023, + "crypto-key" + ], + [ + 40024, + "xid" + ], + [ + 40025, + "reference" + ], + [ + 40026, + "event" + ], + [ + 40027, + "encrypted-key" + ], + [ + 40100, + "mlkem-private-key" + ], + [ + 40101, + "mlkem-public-key" + ], + [ + 40102, + "mlkem-ciphertext" + ], + [ + 40103, + "mldsa-private-key" + ], + [ + 40104, + "mldsa-public-key" + ], + [ + 40105, + "mldsa-signature" + ], + [ + 40300, + "seed" + ], + [ + 40303, + "hdkey" + ], + [ + 40304, + "keypath" + ], + [ + 40305, + "coin-info" + ], + [ + 40306, + "eckey" + ], + [ + 40307, + "address" + ], + [ + 40308, + "output-descriptor" + ], + [ + 40309, + "sskr" + ], + [ + 40310, + "psbt" + ], + [ + 40311, + "account-descriptor" + ], + [ + 40800, + "ssh-private" + ], + [ + 40801, + "ssh-public" + ], + [ + 40802, + "ssh-signature" + ], + [ + 40803, + "ssh-certificate" + ], + [ + 999999, + "999999" + ], + [ + 1347571542, + "provenance" + ] + ] +} diff --git a/tests/rust-validation/src/main.rs b/tests/rust-validation/src/main.rs index 1152847..9da9c57 100644 --- a/tests/rust-validation/src/main.rs +++ b/tests/rust-validation/src/main.rs @@ -1,44 +1,362 @@ -//! Replays tests/vectors/vectors.json against bc-tags 0.12.0. +//! Replays tests/vectors/vectors.json against bc-tags 0.12.0 over dcbor 0.25.2. //! -//! cargo run --release -- ../vectors/vectors.json -use dcbor::{TagsStore, TagsStoreTrait}; +//! cargo run --release --offline -- ../vectors/vectors.json +//! cargo run --release --offline --features bignum -- ../vectors/vectors.json +//! +//! The default build is the reference every Rust consumer uses (`bc-tags` +//! depends on dcbor without `num-bigint`). The `bignum` build validates the +//! documented opt-in recipe (`registerStandardTags(store, { bignum: true })` +//! before `registerTags`): rows that hold only in the other build are skipped +//! and counted in the result line. +//! +//! Checks: every constant by identifier, value and name (the table is +//! compiled against the crate by build.rs); `name_for_value` probes; +//! `tag_for_name` probes; summarizer presence; the registration order; the +//! panic text of conflicting registrations and the store after registrations +//! that re-point a name; annotated, summarised and hex-annotated rendering +//! through the registry. Exit 0 iff nothing mismatches. +use dcbor::{DiagFormatOpts, HexFormatOpts, Tag, TagsStore, TagsStoreOpt, TagsStoreTrait, CBOR}; use serde::Deserialize; +use std::any::Any; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +// `IDENTS`: one `(identifier, bc_tags::TAG_X, bc_tags::TAG_NAME_X)` row per +// table entry of ../vectors/vectors.json. A TypeScript identifier the crate +// lacks does not compile; the Rust→TypeScript direction is `RUST_TAG_COUNT`. +include!(concat!(env!("OUT_DIR"), "/idents.rs")); -/// `grep -c 'const_cbor_tag!' bc-tags-0.12.0/src/tags_registry.rs`. Rust's -/// store cannot be enumerated, so the count pins "no tag exists on one side -/// only" together with the per-entry checks below. +/// `grep -c 'const_cbor_tag!' bc-tags-0.12.0/src/tags_registry.rs`. The +/// crate's store cannot be enumerated, so the count pins "no tag exists on +/// the Rust side only"; the identifier table pins the other direction. const RUST_TAG_COUNT: usize = 75; +const BIGNUM: bool = cfg!(feature = "bignum"); #[derive(Deserialize)] -struct File { count: usize, table: Vec, registry: Vec<(u64, String)> } +struct File { + count: usize, + table: Vec, + registry: Vec<(u64, String)>, + #[serde(default)] + order: Vec, + #[serde(default)] + summarizers: Vec<(u64, bool)>, + #[serde(default)] + names: Vec<(String, Option)>, + #[serde(default)] + conflicts: Vec, + #[serde(default)] + format: Vec, + #[serde(default)] + bignum: Option, +} +#[derive(Deserialize)] +struct Entry { + r#const: String, + value: u64, + name: String, +} +#[derive(Deserialize)] +struct Conflict { + pre: Vec<(u64, String)>, + message: Option, + #[serde(default)] + after: Option, +} +#[derive(Deserialize)] +struct After { + #[serde(rename = "tagForName", default)] + tag_for_name: Vec<(String, Option)>, + #[serde(rename = "nameForValue", default)] + name_for_value: Vec<(u64, String)>, +} +#[derive(Deserialize)] +struct Format { + hex: String, + annotate: String, + summarize: String, + #[serde(rename = "hexAnnotated")] + hex_annotated: String, +} #[derive(Deserialize)] -struct Entry { r#const: String, value: u64, name: String } +struct Bignum { + registry: Vec<(u64, String)>, + summarizers: Vec<(u64, bool)>, + names: Vec<(String, Option)>, + conflicts: Vec, +} -fn main() { - let path = std::env::args().nth(1).expect("path"); - let file: File = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap(); +/// Tallies for one block: rows checked, rows skipped (build-scoped), mismatches. +#[derive(Default)] +struct Tally { + checked: usize, + skipped: usize, + mismatch: usize, +} +impl Tally { + fn hit(&mut self, mismatch: Option) { + self.checked += 1; + if let Some(text) = mismatch { + self.mismatch += 1; + eprintln!("MISMATCH {text}"); + } + } +} + +fn ident_of(c: &str) -> String { + if c.starts_with("TAG_") { + c.to_string() + } else { + format!("TAG_{c}") + } +} + +fn panic_message(payload: &Box) -> String { + payload + .downcast_ref::() + .cloned() + .or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_string())) + .unwrap_or_else(|| "".to_string()) +} + +/// Runs `f` with panics caught and the panic hook silenced. +fn quietly(f: impl FnOnce() -> T) -> Result { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + let result = catch_unwind(AssertUnwindSafe(f)).map_err(|payload| panic_message(&payload)); + std::panic::set_hook(previous); + result +} + +fn reference_store() -> TagsStore { let mut store = TagsStore::default(); bc_tags::register_tags_in(&mut store); + store +} + +/// Rows that describe only the default build (tags 2 and 3 unnamed). +fn default_only_value(v: u64) -> bool { + BIGNUM && (v == 2 || v == 3) +} +fn default_only_name(n: &str) -> bool { + BIGNUM && (n == "positive-bignum" || n == "negative-bignum") +} +fn default_only_conflict(c: &Conflict) -> bool { + BIGNUM && c.pre.iter().any(|(v, _)| *v == 2 || *v == 3) +} + +fn check_identifiers(table: &[Entry], tally: &mut Tally) { + for e in table { + let ident = ident_of(&e.r#const); + tally.hit(match IDENTS.iter().find(|(i, _, _)| *i == ident) { + Some((_, v, n)) if *v == e.value && *n == e.name => None, + Some((_, v, n)) => Some(format!( + "ident {ident}: rust ({v}, {n:?}) ts ({}, {:?})", + e.value, e.name + )), + None => Some(format!( + "ident {ident}: not in the compiled identifier table (it is built from ../vectors/vectors.json)" + )), + }); + } + if IDENTS.len() != table.len() { + tally.mismatch += 1; + eprintln!("MISMATCH ident count: compiled {} ts {}", IDENTS.len(), table.len()); + } +} + +fn check_table(store: &TagsStore, table: &[Entry], tally: &mut Tally) { + for e in table { + let by_value = match store.tag_for_value(e.value).and_then(|t| t.name()) { + Some(n) if n == e.name => None, + got => Some(format!("{} ({}): rust {:?} ts {:?}", e.r#const, e.value, got, e.name)), + }; + let by_name = match store.tag_for_name(&e.name).map(|t| t.value()) { + Some(v) if v == e.value => None, + got => Some(format!("name {} : rust {:?} ts {}", e.name, got, e.value)), + }; + tally.hit(by_value.or(by_name)); + } +} + +fn check_registry(store: &TagsStore, rows: &[(u64, String)], skip: bool, tally: &mut Tally) { + for (v, n) in rows { + if skip && default_only_value(*v) { + tally.skipped += 1; + continue; + } + let got = store.name_for_value(*v); + tally.hit((got != *n).then(|| format!("registry {v}: rust {got:?} ts {n:?}"))); + } +} - let mut mismatch = 0; - for e in &file.table { - match store.tag_for_value(e.value).and_then(|t| t.name()) { - Some(n) if n == e.name => {} - got => { mismatch += 1; eprintln!("MISMATCH {} ({}): rust {:?} ts {:?}", e.r#const, e.value, got, e.name); } +fn check_summarizers(store: &TagsStore, rows: &[(u64, bool)], skip: bool, tally: &mut Tally) { + for (v, want) in rows { + if skip && default_only_value(*v) { + tally.skipped += 1; + continue; } - match store.tag_for_name(&e.name).map(|t| t.value()) { - Some(v) if v == e.value => {} - got => { mismatch += 1; eprintln!("MISMATCH name {} : rust {:?} ts {}", e.name, got, e.value); } + let got = store.summarizer(*v).is_some(); + tally.hit((got != *want).then(|| format!("summarizer {v}: rust {got} ts {want}"))); + } +} + +fn check_names(store: &TagsStore, rows: &[(String, Option)], skip: bool, tally: &mut Tally) { + for (n, want) in rows { + if skip && default_only_name(n) { + tally.skipped += 1; + continue; } + let got = store.tag_for_name(n).map(|t| t.value()); + tally.hit((got != *want).then(|| format!("name {n:?}: rust {got:?} ts {want:?}"))); } - for (v, n) in &file.registry { - let got = store.name_for_value(*v); - if &got != n { mismatch += 1; eprintln!("MISMATCH registry {v}: rust {got:?} ts {n:?}"); } +} + +/// Pre-fills a store with every table value under a bogus name, then lets +/// `register_tags_in` panic its way through: each panic names the next value +/// in registration order and leaves that value's entry corrected (the +/// reference inserts before it compares names), so the next call gets one +/// tag further. +fn check_order(table: &[Entry], expected: &[u64], tally: &mut Tally) { + if expected.is_empty() { + return; + } + let mut store = TagsStore::default(); + for e in table { + store.insert(Tag::new(e.value, format!("x{}", e.value))); + } + let mut observed = Vec::new(); + for _ in 0..100 { + match quietly(|| bc_tags::register_tags_in(&mut store)) { + Ok(()) => break, + Err(message) => { + // "Attempt to register tag: {value} '{old}' with different name: '{new}'" + match message.split_whitespace().nth(4).and_then(|w| w.parse::().ok()) { + Some(v) => observed.push(v), + None => { + tally.mismatch += 1; + eprintln!("MISMATCH order: unexpected panic {message:?}"); + return; + } + } + } + } + } + let mismatch = (observed != expected).then(|| format!("order: rust {observed:?} ts {expected:?}")); + tally.checked += expected.len(); + if let Some(text) = mismatch { + tally.mismatch += 1; + eprintln!("MISMATCH {text}"); + } +} + +fn run_conflict(row: &Conflict) -> Option { + let mut store = TagsStore::default(); + for (v, n) in &row.pre { + store.insert(Tag::new(*v, n.clone())); + } + let message = quietly(|| bc_tags::register_tags_in(&mut store)).err(); + if message != row.message { + return Some(format!("conflict {:?}: rust {:?} ts {:?}", row.pre, message, row.message)); } + // The post-panic store is not compared (RUST_DIVERGENCES.md §1.1). + if row.message.is_none() { + if let Some(after) = &row.after { + for (n, want) in &after.tag_for_name { + let got = store.tag_for_name(n).map(|t| t.value()); + if got != *want { + return Some(format!("conflict {:?} tagForName {n:?}: rust {got:?} ts {want:?}", row.pre)); + } + } + for (v, want) in &after.name_for_value { + let got = store.name_for_value(*v); + if got != *want { + return Some(format!("conflict {:?} nameForValue {v}: rust {got:?} ts {want:?}", row.pre)); + } + } + } + } + None +} + +fn check_conflicts(rows: &[Conflict], skip: bool, tally: &mut Tally) { + for row in rows { + if skip && default_only_conflict(row) { + tally.skipped += 1; + continue; + } + tally.hit(run_conflict(row)); + } +} + +fn check_format(store: &TagsStore, rows: &[Format], tally: &mut Tally) { + for row in rows { + let value = match hex::decode(&row.hex).ok().and_then(|b| CBOR::try_from_data(b).ok()) { + Some(v) => v, + None => { + tally.hit(Some(format!("format {}: input does not decode", row.hex))); + continue; + } + }; + let opt = || TagsStoreOpt::Custom(store); + let rendered = [ + ("annotate", value.diagnostic_opt(&DiagFormatOpts::default().annotate(true).tags(opt())), &row.annotate), + ("summarize", value.diagnostic_opt(&DiagFormatOpts::default().summarize(true).tags(opt())), &row.summarize), + ("hexAnnotated", value.hex_opt(&HexFormatOpts::default().annotate(true).context(opt())), &row.hex_annotated), + ]; + let mismatch = rendered + .iter() + .find(|(_, got, want)| got != *want) + .map(|(field, got, want)| format!("format {} {field}: rust {got:?} ts {want:?}", row.hex)); + tally.hit(mismatch); + } +} + +fn main() { + let path = std::env::args().nth(1).expect("path to vectors.json"); + let file: File = serde_json::from_str(&std::fs::read_to_string(&path).expect("read vectors")).expect("parse vectors"); + let store = reference_store(); + + let (mut tags, mut idents, mut registry, mut order, mut summarizers, mut names, mut conflicts, mut format) = + (Tally::default(), Tally::default(), Tally::default(), Tally::default(), Tally::default(), Tally::default(), Tally::default(), Tally::default()); + + check_table(&store, &file.table, &mut tags); if file.count != RUST_TAG_COUNT || file.table.len() != RUST_TAG_COUNT { - mismatch += 1; + tags.mismatch += 1; eprintln!("MISMATCH count: rust {RUST_TAG_COUNT} ts {}", file.table.len()); } - println!("{} tags, {} registry probes - {} MISMATCH", file.table.len(), file.registry.len(), mismatch); + check_identifiers(&file.table, &mut idents); + check_registry(&store, &file.registry, true, &mut registry); + check_order(&file.table, &file.order, &mut order); + check_summarizers(&store, &file.summarizers, true, &mut summarizers); + check_names(&store, &file.names, true, &mut names); + check_conflicts(&file.conflicts, true, &mut conflicts); + check_format(&store, &file.format, &mut format); + + // The bignum block describes the `num-bigint` registry: checked by the + // bignum build, counted as skipped by the default build. + let mut bignum = Tally::default(); + if let Some(block) = &file.bignum { + if BIGNUM { + check_registry(&store, &block.registry, false, &mut bignum); + check_summarizers(&store, &block.summarizers, false, &mut bignum); + check_names(&store, &block.names, false, &mut bignum); + check_conflicts(&block.conflicts, false, &mut bignum); + } else { + bignum.skipped = block.registry.len() + block.summarizers.len() + block.names.len() + block.conflicts.len(); + } + } + + let all = [&tags, &idents, ®istry, &order, &summarizers, &names, &conflicts, &format, &bignum]; + let mismatch: usize = all.iter().map(|t| t.mismatch).sum(); + let default_only: usize = [®istry, &summarizers, &names, &conflicts].iter().map(|t| t.skipped).sum(); + let head = format!( + "{} tags, {} identifiers, {} registry probes, {} order, {} summarizers, {} names, {} conflicts, {} format", + file.table.len(), idents.checked, registry.checked, order.checked, summarizers.checked, names.checked, conflicts.checked, format.checked + ); + if BIGNUM { + println!("{head}, bignum block {} - {mismatch} MISMATCH (default-only: {default_only} skipped)", bignum.checked); + } else { + println!("{head} - {mismatch} MISMATCH (bignum block: {} skipped)", bignum.skipped); + } std::process::exit(if mismatch == 0 { 0 } else { 1 }); } diff --git a/tests/tags.test.ts b/tests/tags.test.ts index d2b5c1c..7e442b3 100644 --- a/tests/tags.test.ts +++ b/tests/tags.test.ts @@ -1,5 +1,11 @@ import * as tags from "../src/index"; -import { getGlobalTagsStore, TagsStore } from "@blockchaincommons/dcbor"; +import { + CborError, + Tag, + getGlobalTagsStore, + registerStandardTags, + TagsStore, +} from "@blockchaincommons/dcbor"; describe("Tags Registry", () => { describe("Core Envelope Tags", () => { @@ -376,3 +382,119 @@ describe("Tags Registry", () => { }); }); }); + +/** + * What `registerTags` guarantees on a pre-populated store, each assertion + * matching an executed outcome of `bc_tags::register_tags_in` (the Rust + * side is replayed by tests/rust-validation). + */ +describe("registration contract (Rust parity)", () => { + const values = (store: TagsStore, probes: number[]): string[] => + probes.map((v) => store.nameForValue(v)); + const PROBES = [...new Set([...tags.ALL_TAGS.map((t) => Number(t.value)), 1, 2, 3, 100, 999999])]; + + const registerOver = (pre: [number, string][]): { store: TagsStore; error: unknown } => { + const store = new TagsStore(); + for (const [value, name] of pre) store.register(Tag.from(value, name)); + let error: unknown; + try { + tags.registerTags(store); + } catch (e) { + error = e; + } + return { store, error }; + }; + + it("a conflicting value throws dcbor's CborError Custom with the reference's panic text (N2)", () => { + const { store, error } = registerOver([[200, "not-envelope"]]); + expect(CborError.isCborError(error) && error.code === "Custom").toBe(true); + expect((error as Error).message).toBe( + "Attempt to register tag: 200 'not-envelope' with different name: 'envelope'", + ); + // The rejected entry is unchanged and the name map untouched; tags + // registered earlier in the same call stay, later ones are absent. + expect(store.nameForValue(200)).toBe("not-envelope"); + expect(store.tagForName("envelope")).toBeUndefined(); + expect(store.nameForValue(32)).toBe("url"); + expect(store.nameForValue(24)).toBe("encoded-cbor"); + expect(store.nameForValue(201)).toBe("201"); + }); + + it("tag 1 under another name throws the same way, before its summarizer is set", () => { + const { store, error } = registerOver([[1, "other"]]); + expect(CborError.isCborError(error) && error.code === "Custom").toBe(true); + expect((error as Error).message).toBe( + "Attempt to register tag: 1 'other' with different name: 'date'", + ); + expect(store.summarizer(1)).toBeUndefined(); + }); + + it("a name registered under another value moves to the registered value, dcbor's date included (N11)", () => { + const envelope = registerOver([[777, "envelope"]]); + expect(envelope.error).toBeUndefined(); + expect(envelope.store.tagForName("envelope")?.value).toBe(200); + expect(envelope.store.nameForValue(777)).toBe("envelope"); + + const date = registerOver([ + [1, "date"], + [99, "date"], + ]); + expect(date.error).toBeUndefined(); + expect(date.store.tagForName("date")?.value).toBe(1); + expect(date.store.nameForValue(99)).toBe("date"); + }); + + it("the default registry leaves tags 2 and 3 unnamed and unsummarized (N1 floor guard)", () => { + const store = new TagsStore(); + tags.registerTags(store); + expect(store.summarizer(1)).toBeDefined(); + expect(store.summarizer(2)).toBeUndefined(); + expect(store.summarizer(3)).toBeUndefined(); + expect(store.nameForValue(2)).toBe("2"); + expect(store.nameForValue(3)).toBe("3"); + expect(store.tagForName("positive-bignum")).toBeUndefined(); + expect(store.tagForName("negative-bignum")).toBeUndefined(); + }); + + it("the bignum recipe gives one registry in either order; the reference order registers 1, 2, 3 first", () => { + const recipe = new TagsStore(); + registerStandardTags(recipe, { bignum: true }); + tags.registerTags(recipe); + const reversed = new TagsStore(); + tags.registerTags(reversed); + registerStandardTags(reversed, { bignum: true }); + expect(values(recipe, PROBES)).toEqual(values(reversed, PROBES)); + expect(recipe.nameForValue(2)).toBe("positive-bignum"); + expect(recipe.nameForValue(3)).toBe("negative-bignum"); + for (const store of [recipe, reversed]) { + expect([1, 2, 3].map((v) => store.summarizer(v) !== undefined)).toEqual([true, true, true]); + } + + class RecordingStore extends TagsStore { + readonly calls: number[] = []; + override register(tag: Tag): void { + this.calls.push(Number(tag.value)); + super.register(tag); + } + } + const recording = new RecordingStore(); + registerStandardTags(recording, { bignum: true }); + tags.registerTags(recording); + // First registrations in the reference's sequence; a repeat (date again, + // through registerTags) is a same-name no-op. + expect([...new Set(recording.calls)]).toEqual([ + 1, + 2, + 3, + ...tags.ALL_TAGS.map((t) => Number(t.value)), + ]); + }); + + it("the registry holds frozen tags, and this package's constants by identity", () => { + const store = new TagsStore(); + tags.registerTags(store); + expect(Object.isFrozen(store.tagForValue(1))).toBe(true); + expect(store.tagForValue(200)).toBe(tags.TAG_ENVELOPE); + expect(store.tagForValue(300)).toBe(tags.LEGACY_TAGS.SEED_V1); + }); +}); diff --git a/tests/vectors/semantics.ts b/tests/vectors/semantics.ts new file mode 100644 index 0000000..2461a29 --- /dev/null +++ b/tests/vectors/semantics.ts @@ -0,0 +1,167 @@ +/** + * The semantic vector blocks: what a consumer observes through + * `registerTags` beyond the table and the registry - the registration + * order, which tags carry summarizers, dcbor's own names, the outcome of + * registering over a pre-populated store, rendering through the registry, + * and the documented bignum recipe. Every block is produced from the + * working tree and replayed against the Rust reference by + * `tests/rust-validation`. + */ +import { + CborError, + Tag, + TagsStore, + decodeCbor, + hexToBytes, + registerStandardTags, +} from "@blockchaincommons/dcbor"; +import { diagnostic, hexAnnotated } from "@blockchaincommons/dcbor/diagnostic"; +import type { BignumBlock, ConflictRow, FormatRow } from "./table"; + +/** What the working tree must provide. */ +export interface TagsModule { + registerTags(store?: TagsStore): void; + ALL_TAGS: readonly Tag[]; +} + +export interface SemanticsVectors { + order: number[]; + summarizers: [number, boolean][]; + names: [string, number | null][]; + conflicts: ConflictRow[]; + format: FormatRow[]; + bignum: BignumBlock; +} + +/** A registration scenario: what is pre-registered, and what is read back on success. */ +interface Scenario { + pre: [number, string][]; + tagForName: string[]; + nameForValue: number[]; +} + +/** `summarizer(value)` presence probes: dcbor's standard tags, an unregistered value, a package tag, a large value. */ +const SUMMARIZER_PROBES: number[] = [1, 2, 3, 100, 200, 999999]; +/** `tagForName` probes: dcbor's standard names and an unknown one. */ +const NAME_PROBES: string[] = ["date", "positive-bignum", "negative-bignum", "no-such-tag"]; +/** Tagged items rendered through the registry: nested package tags, a legacy tag, the provenance tag, an IANA tag, an unregistered tag. */ +const FORMAT_HEX: string[] = [ + "d8c8d8c96648656c6c6f2e", + "d99c4001", + "d9012c41ab", + "da50524f5641ab", + "d8206968747470733a2f2f78", + "da000f423f01", +]; +const CONFLICT_SCENARIOS: Scenario[] = [ + { pre: [[200, "not-envelope"]], tagForName: [], nameForValue: [] }, + { pre: [[1, "other"]], tagForName: [], nameForValue: [] }, + { + pre: [ + [300, "x"], + [40300, "y"], + ], + tagForName: [], + nameForValue: [], + }, + { pre: [[777, "envelope"]], tagForName: ["envelope"], nameForValue: [777] }, + { + pre: [ + [1, "date"], + [99, "date"], + ], + tagForName: ["date"], + nameForValue: [99], + }, + { pre: [[2, "two"]], tagForName: ["two"], nameForValue: [2] }, +]; +const BIGNUM_CONFLICT_SCENARIOS: Scenario[] = [ + { + pre: [ + [2, "positive-bignum"], + [98, "positive-bignum"], + ], + tagForName: ["positive-bignum"], + nameForValue: [], + }, +]; + +const valueOf = (tag: Tag | undefined): number | null => + tag === undefined ? null : Number(tag.value); + +/** A store that records every `register` call, to observe the registration order. */ +class RecordingStore extends TagsStore { + readonly calls: number[] = []; + override register(tag: Tag): void { + this.calls.push(Number(tag.value)); + super.register(tag); + } +} + +function runScenario(scenario: Scenario, register: (store: TagsStore) => void): ConflictRow { + const store = new TagsStore(); + for (const [value, name] of scenario.pre) store.register(Tag.from(value, name)); + try { + register(store); + } catch (e) { + if (!CborError.isCborError(e) || e.code !== "Custom") throw e; + return { pre: scenario.pre, message: e.message }; + } + return { + pre: scenario.pre, + message: null, + after: { + tagForName: scenario.tagForName.map((name) => [name, valueOf(store.tagForName(name))]), + nameForValue: scenario.nameForValue.map((value) => [value, store.nameForValue(value)]), + }, + }; +} + +/** The semantic blocks for the working tree `m`. */ +export function semanticsVectors(m: TagsModule): SemanticsVectors { + // Order: every `register` call `registerTags` makes, minus dcbor's own standard tags. + const recording = new RecordingStore(); + m.registerTags(recording); + const own = new Set(m.ALL_TAGS.map((t) => Number(t.value))); + const order = recording.calls.filter((v) => own.has(v)); + + const store = new TagsStore(); + m.registerTags(store); + const summarizers: [number, boolean][] = SUMMARIZER_PROBES.map((v) => [ + v, + store.summarizer(v) !== undefined, + ]); + const names: [string, number | null][] = NAME_PROBES.map((n) => [ + n, + valueOf(store.tagForName(n)), + ]); + const conflicts = CONFLICT_SCENARIOS.map((s) => runScenario(s, (t) => m.registerTags(t))); + const format: FormatRow[] = FORMAT_HEX.map((hex) => { + const value = decodeCbor(hexToBytes(hex)); + return { + hex, + annotate: diagnostic(value, { annotate: true, tags: store }), + summarize: diagnostic(value, { summarize: true, tags: store }), + hexAnnotated: hexAnnotated(value, { tagsStore: store }), + }; + }); + + // The documented `num-bigint` recipe, in the reference's registration order. + const recipe = (t: TagsStore): void => { + registerStandardTags(t, { bignum: true }); + m.registerTags(t); + }; + const bignumStore = new TagsStore(); + recipe(bignumStore); + const bignum: BignumBlock = { + registry: [1, 2, 3].map((v) => [v, bignumStore.nameForValue(v)]), + summarizers: [1, 2, 3].map((v) => [v, bignumStore.summarizer(v) !== undefined]), + names: ["positive-bignum", "negative-bignum"].map((n) => [ + n, + valueOf(bignumStore.tagForName(n)), + ]), + conflicts: BIGNUM_CONFLICT_SCENARIOS.map((s) => runScenario(s, recipe)), + }; + + return { order, summarizers, names, conflicts, format, bignum }; +} diff --git a/tests/vectors/table.ts b/tests/vectors/table.ts index 981f9b0..eadf4e2 100644 --- a/tests/vectors/table.ts +++ b/tests/vectors/table.ts @@ -1,8 +1,9 @@ /** * The vector shape for this package: the full tag table and the registry a - * fresh `registerTags()` produces. Works over any module shape, pre- or - * post-redesign, so the golden, differential and generator scripts share - * one definition. + * fresh `registerTags()` produces, plus the semantic blocks of + * `./semantics.ts` (identifiers are checked by the harness from `table`). + * Works over any module shape, pre- or post-redesign, so the golden, + * differential and generator scripts share one definition. */ export interface TableEntry { /** Exported constant name (`SEED_V1` even when grouped under `LEGACY_TAGS`). */ @@ -10,11 +11,63 @@ export interface TableEntry { value: number; name: string; } + +/** One registration scenario: a pre-populated store, then `registerTags`. */ +export interface ConflictRow { + /** `[value, name]` registrations made on a fresh store before `registerTags`. */ + pre: [number, string][]; + /** + * The `CborError` message `registerTags` throws (the reference's panic + * text), or `null` when the registration succeeds. + */ + message: string | null; + /** + * Lookups after a registration that succeeded. Absent when it threw: the + * post-conflict store is not a contract (RUST_DIVERGENCES.md §1.1). + */ + after?: { + tagForName: [string, number | null][]; + nameForValue: [number, string][]; + }; +} + +/** Renderings of one tagged item through a store `registerTags` filled. */ +export interface FormatRow { + hex: string; + /** `diagnostic(value, { annotate: true, tags: store })` */ + annotate: string; + /** `diagnostic(value, { summarize: true, tags: store })` */ + summarize: string; + /** `hexAnnotated(value, { tagsStore: store })` */ + hexAnnotated: string; +} + +/** + * The registry produced by the documented `num-bigint` recipe + * (`registerStandardTags(store, { bignum: true })`, then `registerTags`). + * Checked by the harness's `--features bignum` build only. + */ +export interface BignumBlock { + registry: [number, string][]; + summarizers: [number, boolean][]; + names: [string, number | null][]; + conflicts: ConflictRow[]; +} + export interface Vectors { count: number; table: TableEntry[]; /** `[value, nameForValue(value)]` after `registerTags()` on a fresh store. */ registry: [number, string][]; + /** The values `registerTags` registers, in call order, without dcbor's own. */ + order?: number[]; + /** `[value, summarizer(value) !== undefined]` after `registerTags()`. */ + summarizers?: [number, boolean][]; + /** `[name, tagForName(name)?.value ?? null]` after `registerTags()`. */ + names?: [string, number | null][]; + conflicts?: ConflictRow[]; + format?: FormatRow[]; + bignum?: BignumBlock; } interface TagLike { diff --git a/tests/vectors/vectors.json b/tests/vectors/vectors.json index 25e1f25..5923145 100644 --- a/tests/vectors/vectors.json +++ b/tests/vectors/vectors.json @@ -698,5 +698,334 @@ 1347571542, "provenance" ] - ] + ], + "order": [ + 32, + 37, + 24, + 200, + 201, + 262, + 40000, + 40001, + 40002, + 40003, + 40004, + 40005, + 40006, + 40007, + 40008, + 40009, + 40026, + 300, + 306, + 309, + 40300, + 40306, + 40309, + 40010, + 40011, + 40012, + 40013, + 40014, + 40015, + 40016, + 40017, + 40018, + 40019, + 40020, + 40021, + 40022, + 40023, + 40024, + 40025, + 40027, + 40100, + 40101, + 40102, + 40103, + 40104, + 40105, + 303, + 304, + 305, + 307, + 310, + 311, + 40303, + 40304, + 40305, + 40307, + 40308, + 40310, + 40311, + 40800, + 40801, + 40802, + 40803, + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 1347571542 + ], + "summarizers": [ + [ + 1, + true + ], + [ + 2, + false + ], + [ + 3, + false + ], + [ + 100, + false + ], + [ + 200, + false + ], + [ + 999999, + false + ] + ], + "names": [ + [ + "date", + 1 + ], + [ + "positive-bignum", + null + ], + [ + "negative-bignum", + null + ], + [ + "no-such-tag", + null + ] + ], + "conflicts": [ + { + "pre": [ + [ + 200, + "not-envelope" + ] + ], + "message": "Attempt to register tag: 200 'not-envelope' with different name: 'envelope'" + }, + { + "pre": [ + [ + 1, + "other" + ] + ], + "message": "Attempt to register tag: 1 'other' with different name: 'date'" + }, + { + "pre": [ + [ + 300, + "x" + ], + [ + 40300, + "y" + ] + ], + "message": "Attempt to register tag: 300 'x' with different name: 'crypto-seed'" + }, + { + "pre": [ + [ + 777, + "envelope" + ] + ], + "message": null, + "after": { + "tagForName": [ + [ + "envelope", + 200 + ] + ], + "nameForValue": [ + [ + 777, + "envelope" + ] + ] + } + }, + { + "pre": [ + [ + 1, + "date" + ], + [ + 99, + "date" + ] + ], + "message": null, + "after": { + "tagForName": [ + [ + "date", + 1 + ] + ], + "nameForValue": [ + [ + 99, + "date" + ] + ] + } + }, + { + "pre": [ + [ + 2, + "two" + ] + ], + "message": null, + "after": { + "tagForName": [ + [ + "two", + 2 + ] + ], + "nameForValue": [ + [ + 2, + "two" + ] + ] + } + } + ], + "format": [ + { + "hex": "d8c8d8c96648656c6c6f2e", + "annotate": "200( / envelope /\n 201(\"Hello.\") / leaf /\n)", + "summarize": "200(201(\"Hello.\"))", + "hexAnnotated": "d8 c8 # tag(200) envelope\n d8 c9 # tag(201) leaf\n 66 # text(6)\n 48656c6c6f2e # \"Hello.\"" + }, + { + "hex": "d99c4001", + "annotate": "40000(1) / known-value /", + "summarize": "40000(1)", + "hexAnnotated": "d9 9c40 # tag(40000) known-value\n 01 # unsigned(1)" + }, + { + "hex": "d9012c41ab", + "annotate": "300(h'ab') / crypto-seed /", + "summarize": "300(h'ab')", + "hexAnnotated": "d9 012c # tag(300) crypto-seed\n 41 # bytes(1)\n ab" + }, + { + "hex": "da50524f5641ab", + "annotate": "1347571542(h'ab') / provenance /", + "summarize": "1347571542(h'ab')", + "hexAnnotated": "da 50524f56 # tag(1347571542) provenance\n 41 # bytes(1)\n ab" + }, + { + "hex": "d8206968747470733a2f2f78", + "annotate": "32(\"https://x\") / url /", + "summarize": "32(\"https://x\")", + "hexAnnotated": "d8 20 # tag(32) url\n 69 # text(9)\n 68747470733a2f2f78 # \"https://x\"" + }, + { + "hex": "da000f423f01", + "annotate": "999999(1)", + "summarize": "999999(1)", + "hexAnnotated": "da 000f423f # tag(999999)\n 01 # unsigned(1)" + } + ], + "bignum": { + "registry": [ + [ + 1, + "date" + ], + [ + 2, + "positive-bignum" + ], + [ + 3, + "negative-bignum" + ] + ], + "summarizers": [ + [ + 1, + true + ], + [ + 2, + true + ], + [ + 3, + true + ] + ], + "names": [ + [ + "positive-bignum", + 2 + ], + [ + "negative-bignum", + 3 + ] + ], + "conflicts": [ + { + "pre": [ + [ + 2, + "positive-bignum" + ], + [ + 98, + "positive-bignum" + ] + ], + "message": null, + "after": { + "tagForName": [ + [ + "positive-bignum", + 2 + ] + ], + "nameForValue": [] + } + } + ] + } } From 43455a6859fe8e0d6a7d459b6d88988b185df6e2 Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Mon, 14 Sep 2026 17:13:49 -0300 Subject: [PATCH 3/5] Changes --- RUST_DIVERGENCES.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/RUST_DIVERGENCES.md b/RUST_DIVERGENCES.md index 127f528..de7b0a8 100644 --- a/RUST_DIVERGENCES.md +++ b/RUST_DIVERGENCES.md @@ -26,43 +26,9 @@ Against the pinned crate, the harness checks: `fixtures/swapped-identifiers.json` must fail with two identifier mismatches. -No tag number, name, order, message or encoded byte differs. - ## 1. True behavioral divergences (same input, different outcome) -### 1.1 Conflicting registration - -When a store already holds one of these values under a different name: -- `bc_tags::register_tags_in` panics in dcbor's `TagsStore::insert`. -- `registerTags` throws dcbor's `CborError` with code `Custom`. - -Both fail at the same tag, in the same order, with the same text (`Attempt to register tag: 200 'not-envelope' with different name: 'envelope'`); this is a harness vector. Tags registered earlier in the same call stay registered on both sides. - -The store left behind is not a contract: -- **The conflicting value's entry.** The reference has already replaced it when it panics, and its name map is not updated. TypeScript leaves the entry unchanged, so a caught error has no effect on the entry it rejects. TypeScript could copy the reference's half-written state, but it would then leave `nameForValue` and `tagForName` disagreeing after an ordinary, recoverable error; the reference state is observable only through `catch_unwind`. -- **The global store.** A panic in `register_tags()` poisons the reference's global store, and every later access panics. The TypeScript global store stays usable. - -A name already registered under a different value moves back to the registered value on both sides, without an error. This includes dcbor's `date`, and with the bignum recipe `positive-bignum`/`negative-bignum`. The unnamed and empty-name registration errors cannot be reached through this package; see dcbor's `RUST_DIVERGENCES.md` §1.2. - -## 2. JS-only input domain (no Rust analog exists) - -- **Non-store arguments.** `registerTags(null)`, `registerTags({})` and other values that are not a `TagsStore` throw a `TypeError` from the first store call, before anything is registered. `undefined` selects the global store. -- **Mutating a tag.** Every constant, `LEGACY_TAGS` and `ALL_TAGS` are frozen, and dcbor stores and returns frozen tags. An assignment throws `TypeError` in strict-mode code (every ES module) and does nothing in sloppy-mode scripts, so a registered name cannot be changed through a `Tag`. - -## 3. Mapping equivalences (JS-specific inputs validated via their byte-target) - -- **Constants.** Rust's `TAG_: u64` and `TAG_NAME_: &str` are one frozen dcbor `Tag` here: use `.value` and `.name`. Equality is by value on both sides (`Tag.equals`, Rust's `PartialEq`), never by object identity. -- **Legacy tags.** Rust's flat `TAG_SEED_V1` … `TAG_ACCOUNT_V1` are `LEGACY_TAGS.SEED_V1` … `LEGACY_TAGS.ACCOUNT_V1`. -- **`ALL_TAGS`.** The reference's private registration vector, public and frozen here, in the same order. -- **IANA tags.** URI (32), UUID (37) and encoded CBOR (24) are written in this package, as Rust's `bc-tags` writes them; dcbor defines only the date and bignum tags on both sides. -- **Registration.** `registerTags(store?)` is `register_tags_in(&mut store)` with a store and `register_tags()` without one; repeating it is a no-op. -- **Global store.** `registerTags()` fills dcbor's single process-wide store. The ESM and CommonJS builds share it, as Rust has one `GLOBAL_TAGS` per semver-compatible dcbor. -- **No dcbor re-export.** Rust's `bc_tags` re-exports `dcbor::prelude::*`. Import `TagsStore`, `Tag` and the formatters from `@blockchaincommons/dcbor`. -- **Standard tags 2 and 3.** `registerTags` registers dcbor's standard tags as the reference's default build does: - - `date` (1) with its summarizer; - - tags 2 and 3 unnamed, with no summarizers, because `bc-tags` depends on dcbor without `num-bigint`. - - For the `num-bigint` registry (`positive-bignum`/`negative-bignum`, `bignum(…)` summaries), call `registerStandardTags(store, { bignum: true })` **before** `registerTags(store)`. That is the reference's registration order. The reverse order gives the same registry on a store without conflicts. The harness's `--features bignum` run validates this recipe. It needs `@blockchaincommons/dcbor` ≥ the declared floor. +None. No tag number, name, registration order, conflict message, lookup or encoded byte differs from the reference for any input both sides can hold. A conflicting registration throws dcbor's `CborError` (code `Custom`) where the reference panics, at the same tag with the same text. ## Maintenance From 17c02dd728d5bc227d460dfe30f7b090d30bf85e Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Mon, 14 Sep 2026 18:13:41 -0300 Subject: [PATCH 4/5] Improvements to docs --- .github/workflows/ci.yml | 3 +-- CHANGELOG.md | 56 +++++----------------------------------- MIGRATION.md | 10 ------- README.md | 3 +-- RUST_DIVERGENCES.md | 41 ----------------------------- 5 files changed, 8 insertions(+), 105 deletions(-) delete mode 100644 RUST_DIVERGENCES.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b02ee3d..2f3ad00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,7 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - # The runner image ships a stable Rust toolchain. The default build is - # the reference every Rust consumer uses (dcbor without num-bigint). + # The runner image ships a stable Rust toolchain. - name: Validate against bc-tags (default build) working-directory: tests/rust-validation run: cargo run --release --locked -- ../vectors/vectors.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9667c..56db976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,9 @@ # Changelog -## Unreleased +## 1.0.0-beta.3 - 2026-09-14 Requires `@blockchaincommons/dcbor` ^1.0.0-beta.3. The registry produced by -`registerTags` matches `bc_tags::register_tags_in` probe for probe, and the -Rust harness now proves it for identifiers, order, summarizers, names, -conflicts and rendering in both dcbor builds. +`registerTags` matches `bc_tags::register_tags_in` probe for probe. ### Changed @@ -17,14 +15,6 @@ conflicts and rendering in both dcbor builds. snapshot are regenerated and the differential pins the flip. For the `num-bigint` registry call `registerStandardTags(store, { bignum: true })` **before** `registerTags(store)`, the reference's registration order. -- **The dcbor floor is `^1.0.0-beta.3`, and `bun.lock` resolves it.** The - committed lock used to pin dcbor 1.0.0-beta.1, which names tags 2 and 3, - so a standalone install tested a registry that contradicts the snapshot. - dcbor 1.0.0-beta.3 is the release whose `registerStandardTags` registers - unconditionally (a name registered under another value moves back to the - standard value, as the reference's `insert_all` does), whose stored and - created tags are frozen, and whose global store is one per process across - the ESM and CommonJS builds; this package's registry depends on all three. - **The IANA tag numbers are written in this package.** dcbor 1.0.0-beta.3 no longer exports numeric `TAG_URI`, `TAG_UUID` and `TAG_ENCODED_CBOR`, so 32, 37 and 24 are literals here, as `bc-tags` writes them. Values and names are @@ -32,47 +22,13 @@ conflicts and rendering in both dcbor builds. - `registerTags` registers `ALL_TAGS` directly (dcbor's `registerAll` takes any iterable); no copy is made. -### Added - -- Rust harness (`tests/rust-validation`): every constant is checked **by - identifier** against a table `build.rs` compiles from the vectors - (`TAG_SEED` ↔ `bc_tags::TAG_SEED`/`TAG_NAME_SEED`, `LEGACY_TAGS.SEED_V1` ↔ - `TAG_SEED_V1`; an identifier the crate lacks fails the build), plus new - vector blocks for summarizer presence, dcbor's names, the registration - order, conflicting-registration messages and the store after a - re-pointed name, and annotated, summarised and hex-annotated rendering - through the registry. Two builds: the default (the reference every Rust - consumer uses) and `--features bignum` for the documented recipe, each - skipping the rows that describe the other. `fixtures/swapped-identifiers.json` - is a committed negative fixture that must fail with two identifier - mismatches. A `rust-validation` CI job runs both builds and the fixture. -- Registration-contract tests: the conflict text and the store after it, - name re-pointing including dcbor's `date`, the default registry's unnamed - 2 and 3, the bignum recipe in both orders and its register sequence, frozen - stored tags held by identity, the global store after a conflict, non-store - arguments, and `registerTags` through the CommonJS entry naming the ESM - global store. - ### Fixed - Documentation: a conflicting registration throws dcbor's `CborError` (code - `Custom`) with the reference's panic text, not a bare `Error` (JSDoc, API - report, README); `registerTags` registers the `date` tag only, not the - bignum tags (MIGRATION, example); `RUST_DIVERGENCES.md` rewritten with the - harness result lines, the kept post-conflict differences and the mapping - table. - -### Consumers - -- Raise `@blockchaincommons/dcbor` to `^1.0.0-beta.3` and this package to the - release carrying this entry, then regenerate lockfiles. The published - `@blockchaincommons/tags` 1.0.0-beta.2 over dcbor 1.0.0-beta.1 still names - tags 2 and 3. -- Only `TAG_*` names are exported (never `PROVENANCE_MARK`, `XID` or - `KNOWN_VALUE`); a Rust harness that enables `dcbor/num-bigint` validates a - registry no Blockchain Commons crate builds. + `Custom`) with the reference's panic text, not a bare `Error`; + `registerTags` registers the `date` tag only, not the bignum tags. -## 1.0.0-beta.2 +## 1.0.0-beta.2 - 2026-09-12 Documents the registry behavior and updates repository tooling. Public tag values, names, and registration logic are unchanged. @@ -83,6 +39,6 @@ names, and registration logic are unchanged. TypeScript always registers tags 2 and 3; Rust requires `num-bigint`. The Rust harness enables that feature and matches 75 tags and 80 probes. -## 1.0.0-beta.1 +## 1.0.0-beta.1 - 2026-09-09 Initial beta implementation. diff --git a/MIGRATION.md b/MIGRATION.md index fdc67df..1eac506 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -77,13 +77,3 @@ Tags compare by value: `TAG_ENVELOPE === Tag.from(200, "envelope")` is Every constant is frozen. `(TAG_ENVELOPE as any).name = "x"` used to succeed in `@bcts/tags` and silently rename the wire tag for every later `registerTags()`; it now throws a `TypeError`. - -## 6. Node and TypeScript floors - -Node **22.12** and TypeScript **5.7**. The IIFE / global-script build is -gone; use the ESM or CJS entry. - -## 7. What did not change - -- Every tag value and name, and the registration order. -- `registerTags()` with no argument. diff --git a/README.md b/README.md index d3eb923..1744e75 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,13 @@ Runnable examples live in the [`examples/`](https://github.com/BlockchainCommons ### Version History -- **Unreleased** - Requires `@blockchaincommons/dcbor` ^1.0.0-beta.3, so tags 2 and 3 are unnamed as in the reference's default build; the Rust harness checks identifiers, registration order, summarizers, dcbor's names, conflicts and rendering in both dcbor builds; registration-contract tests; conflicts documented as dcbor's `CborError`. +- **1.0.0-beta.3 (September 14, 2026)** - Requires `@blockchaincommons/dcbor` ^1.0.0-beta.3, so tags 2 and 3 are unnamed; conflicts documented as dcbor's `CborError`. - **1.0.0-beta.2 (September 12, 2026)** - Documents the bignum-registration difference from Rust builds without `num-bigint`. - **1.0.0-beta.1 (September 9, 2026)** - Initial beta implementation. ### Roadmap - Continued testing and auditing on the path from beta to a stable **1.0.0** release. -- Continued parity with the Rust reference implementation as it evolves (see [`RUST_DIVERGENCES.md`](./RUST_DIVERGENCES.md)). ### Dependencies diff --git a/RUST_DIVERGENCES.md b/RUST_DIVERGENCES.md deleted file mode 100644 index de7b0a8..0000000 --- a/RUST_DIVERGENCES.md +++ /dev/null @@ -1,41 +0,0 @@ -# Compatibility with the Rust reference - -`@blockchaincommons/tags` is a port of [bc-tags-rust](https://github.com/BlockchainCommons/bc-tags-rust) (crate `bc-tags`), **pinned at `bc-tags = 0.12.0`**, commit [`fc30b65`](https://github.com/BlockchainCommons/bc-tags-rust/commit/fc30b65c82eeca3124288fb9096c31a5631adc87), over `dcbor = 0.25.2`. The version and commit are recorded in [`.github/versions.yml`](./.github/versions.yml). The committed vectors (`tests/vectors/vectors.json`) are cross-validated by `tests/rust-validation/`. The harness runs against the build every Rust consumer uses (dcbor default features, no `num-bigint`), and again with `--features bignum` to validate the documented bignum recipe. - -```sh -cd tests/rust-validation -cargo run --release --offline -- ../vectors/vectors.json -cargo run --release --offline --features bignum -- ../vectors/vectors.json -``` - -Result on 2026-09-14: - -``` -75 tags, 75 identifiers, 80 registry probes, 75 order, 6 summarizers, 4 names, 6 conflicts, 6 format - 0 MISMATCH (bignum block: 9 skipped) -75 tags, 75 identifiers, 78 registry probes, 75 order, 4 summarizers, 2 names, 5 conflicts, 6 format, bignum block 9 - 0 MISMATCH (default-only: 7 skipped) -``` - -Against the pinned crate, the harness checks: -- every constant **by identifier** (`TAG_SEED` ↔ `bc_tags::TAG_SEED`/`TAG_NAME_SEED`; `LEGACY_TAGS.SEED_V1` ↔ `TAG_SEED_V1`), value and name, plus the crate's constant count; -- `name_for_value` for every tag value plus 1, 2, 3, 100 and 999999; -- `tag_for_name` for every tag name plus `date`, `positive-bignum`, `negative-bignum`; -- which tags have summarizers; -- the registration order; -- the panic text of conflicting registrations, and the store after registrations that re-point a name; -- annotated, summarised and hex-annotated rendering of tagged items through the registry. - -`fixtures/swapped-identifiers.json` must fail with two identifier mismatches. - -## 1. True behavioral divergences (same input, different outcome) - -None. No tag number, name, registration order, conflict message, lookup or encoded byte differs from the reference for any input both sides can hold. A conflicting registration throws dcbor's `CborError` (code `Custom`) where the reference panics, at the same tag with the same text. - -## Maintenance - -When the reference changes: -- Review its diff and update `.github/versions.yml`. -- Update the harness pins (`bc-tags`, `dcbor`) and `RUST_TAG_COUNT` (`grep -c 'const_cbor_tag!'`). The identifier rows are generated from the vectors. -- Regenerate vectors (`bun run vectors:generate`, which also rewrites the swapped-identifier fixture). -- Run the package tests and both harness builds, and update the result lines above. - -Keep the default build as the reference, since that is how `bc-tags` is built; the `bignum` build validates only the recipe. Keep the `@blockchaincommons/dcbor` floor at a release whose standard-tag registration and global store match this record. Record any newly observed difference here with its input and both outcomes. From 7f4d2cb6e08697186420b1c4fe67daec9f761223 Mon Sep 17 00:00:00 2001 From: Leonardo Custodio Date: Mon, 14 Sep 2026 18:15:50 -0300 Subject: [PATCH 5/5] Fix ci --- bun.lock | 15 +++++---------- package.json | 1 + scripts/api-report.ts | 5 +++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 70f8271..935e09a 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/parser": "^8.70.0", "@vitest/coverage-v8": "^5.0.0", + "ajv": "^8.20.0", "eslint": "^10.10.0", "prettier": "3.9.6", "publint": "^0.3.24", @@ -363,7 +364,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -535,7 +536,7 @@ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], @@ -779,15 +780,13 @@ "@microsoft/tsdoc-config/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "@rushstack/node-core-library/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "@rushstack/node-core-library/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -809,10 +808,6 @@ "vitest/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], - "@microsoft/tsdoc-config/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@rushstack/node-core-library/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], } } diff --git a/package.json b/package.json index 0505297..c3fcd9e 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/parser": "^8.70.0", "@vitest/coverage-v8": "^5.0.0", + "ajv": "^8.20.0", "eslint": "^10.10.0", "prettier": "3.9.6", "publint": "^0.3.24", diff --git a/scripts/api-report.ts b/scripts/api-report.ts index 0914c41..6ddd735 100644 --- a/scripts/api-report.ts +++ b/scripts/api-report.ts @@ -10,6 +10,11 @@ * (api/.api.md) is the reviewable record of the public surface - * "API deliberately unstable, wire frozen" is enforced by making every * surface change a visible diff here and in api/index.d.mts. + * + * `ajv` is a direct devDependency for this script's sake: api-extractor's + * `ajv-draft-04` wants ajv 8 as an optional peer, which bun does not nest, so + * without the direct dependency eslint's ajv 6 is what gets hoisted and the + * report fails with "Cannot find module 'ajv/dist/core'". */ import { copyFileSync, existsSync, readFileSync, rmSync } from "node:fs";