diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baa5587..6431b4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,12 @@ jobs: - name: Checkout development family sources outside runtime Cargo scope env: GH_TOKEN: ${{ github.token }} - run: node scripts/checkout-family-sources.mjs --development --parent "$GITHUB_WORKSPACE/.." + DEVELOPMENT_REF: ${{ github.head_ref || github.ref_name }} + run: >- + node scripts/checkout-family-sources.mjs + --development + --development-ref "$DEVELOPMENT_REF" + --parent "$GITHUB_WORKSPACE/.." - uses: dtolnay/rust-toolchain@1.95 with: { targets: "wasm32-unknown-unknown,wasm32-wasip1" } - uses: jetli/wasm-pack-action@v0.4.0 @@ -43,7 +48,7 @@ jobs: - name: Install build tool and local interop type package run: >- npm install --ignore-scripts --no-save node-gyp@13.0.1 - ../vinary-tree-interop/bindings/javascript + "@vinary-tree/vinary-tree-interop@file:../vinary-tree-interop/bindings/javascript" - name: Build and test integrated backends env: VINARY_TREE_INTEROP_ROOT: ${{ github.workspace }}/../vinary-tree-interop diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c18969..334107e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: shell: bash run: >- npm install --ignore-scripts --no-save node-gyp@13.0.1 - ../vinary-tree-interop/bindings/javascript + "@vinary-tree/vinary-tree-interop@file:../vinary-tree-interop/bindings/javascript" - name: Build relocatable SDK and native addon shell: bash env: @@ -109,7 +109,9 @@ jobs: with: { version: v0.13.1 } - run: bash .github/setup-binaryen.sh - name: Install local interop type package - run: npm install --ignore-scripts --no-save ../vinary-tree-interop/bindings/javascript + run: >- + npm install --ignore-scripts --no-save + "@vinary-tree/vinary-tree-interop@file:../vinary-tree-interop/bindings/javascript" - name: Build and test both WebAssembly hosts env: VINARY_TREE_INTEROP_ROOT: ${{ github.workspace }}/../vinary-tree-interop diff --git a/.gitignore b/.gitignore index 0da9f42..c06095a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # C8 property tests install fast-check into an isolated package so npm never has -# to resolve the unpublished @vinary-tree/interop sibling. The lockfile is +# to resolve the unpublished @vinary-tree/vinary-tree-interop sibling. The lockfile is # committed for reproducibility; the installed tree is not. test-property/node_modules/ node_modules/ diff --git a/README.md b/README.md index f33f6e5..090086b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Vinary Tree JavaScript runtime -`@vinary-tree/vinary-tree` is the single-instance JavaScript runtime for +`@vinary-tree/javascript-runtime` is the single-instance JavaScript runtime for libdictenstein, liblevenshtein, lling-llang, and duallity. It lets their project-specific facades exchange retained dictionaries and weighted finite-state transducers (WFSTs) without copying or loading incompatible native @@ -8,7 +8,7 @@ runtimes. | Release property | Value | |---|---| -| Candidate | `4.0.0-rc.4` | +| Candidate | `4.0.0-rc.5` | | npm dist-tag | `next` | | Node | 22.14 or newer | | Backends | Native N-API, browser WebAssembly, Node WASI | @@ -17,12 +17,12 @@ runtimes. ## Install the release candidate ```sh -npm install @vinary-tree/vinary-tree@next +npm install @vinary-tree/javascript-runtime@next ``` Use the package root for Node's native backend, -`@vinary-tree/vinary-tree/wasm` in a browser, or -`@vinary-tree/vinary-tree/wasi` when Node/WASI filesystem preopens are needed. +`@vinary-tree/javascript-runtime/wasm` in a browser, or +`@vinary-tree/javascript-runtime/wasi` when Node/WASI filesystem preopens are needed. Applications normally install a project facade rather than importing the shared runtime directly. @@ -32,7 +32,7 @@ Dictionaries follow the synchronous `Map` vocabulary while retaining explicit native lifetime control: ```js -import { libdictenstein, liblevenshtein } from "@vinary-tree/vinary-tree"; +import { libdictenstein, liblevenshtein } from "@vinary-tree/javascript-runtime"; using dictionary = libdictenstein.dynamicDawg("unicode"); dictionary.set("cat", 1n).set("cot", 2n).set("cut", null); @@ -60,9 +60,9 @@ Garbage collection is exceptional-path containment, not resource scheduling. | Import | Backend | Intended host | |---|---|---| -| `@vinary-tree/vinary-tree` | Prebuilt N-API addon | Node services and tools | -| `@vinary-tree/vinary-tree/wasm` | `wasm-bindgen` module | Browsers and web workers | -| `@vinary-tree/vinary-tree/wasi` | Explicit WASI linear-memory ABI | Node with preopened persistent storage | +| `@vinary-tree/javascript-runtime` | Prebuilt N-API addon | Node services and tools | +| `@vinary-tree/javascript-runtime/wasm` | `wasm-bindgen` module | Browsers and web workers | +| `@vinary-tree/javascript-runtime/wasi` | Explicit WASI linear-memory ABI | Node with preopened persistent storage | All three expose the same snapshot, collection, query, and WFST semantics. Resources carry an immutable runtime identity; passing a resource between @@ -93,6 +93,7 @@ directories. - [Architecture and invariants](docs/architecture.md) - [Testing strategy and property models](docs/testing.md) - [Release order, platform matrix, and rollback](docs/releasing.md) +- [npm coordinate migration and compatibility](docs/npm-coordinate-migration.md) - [History-preserving extraction provenance](docs/extraction-provenance.md) The project-specific packages own their idiomatic user APIs. This repository diff --git a/docs/diagrams/release-dependency-graph.puml b/docs/diagrams/release-dependency-graph.puml index fb7b0fa..8c53830 100644 --- a/docs/diagrams/release-dependency-graph.puml +++ b/docs/diagrams/release-dependency-graph.puml @@ -4,18 +4,18 @@ skinparam backgroundColor #FAFAFA skinparam shadowing false skinparam ArrowColor #455A64 skinparam defaultFontName DejaVu Sans -title Immutable 4.0.0-rc.4 JavaScript release dependency graph +title Immutable 4.0.0-rc.5 JavaScript release dependency graph rectangle "llattice\n0.1.0" as lattice #ECEFF1 -rectangle "vinary-tree-interop\n4.0.0-rc.4" as interop #FFF59D -rectangle "libdictenstein\n4.0.0-rc.4" as dict #C8E6C9 -rectangle "liblevenshtein\n4.0.0-rc.4" as lev #C8E6C9 -rectangle "lling-llang\n4.0.0-rc.4" as lling #C8E6C9 -rectangle "duallity\n4.0.0-rc.4" as dual #C8E6C9 +rectangle "@vinary-tree/vinary-tree-interop\n4.0.0-rc.5 · npm next" as interop #FFF59D +rectangle "libdictenstein\n4.0.0-rc.5" as dict #C8E6C9 +rectangle "liblevenshtein\n4.0.0-rc.5" as lev #C8E6C9 +rectangle "lling-llang\n4.0.0-rc.5" as lling #C8E6C9 +rectangle "duallity\n4.0.0-rc.5" as dual #C8E6C9 rectangle "platform native prebuilds\n+ browser WASM + WASI" as artifacts #B2DFDB -rectangle "@vinary-tree/vinary-tree\n4.0.0-rc.4 · npm next" as runtime #80CBC4 -rectangle "project npm facades\n4.0.0-rc.4 · npm next" as facades #C5CAE9 -rectangle "liblevenshtein compatibility facade\n4.0.0-rc.4 · npm next" as legacy #FFCCBC +rectangle "@vinary-tree/javascript-runtime\n4.0.0-rc.5 · npm next" as runtime #80CBC4 +rectangle "project npm facades\n4.0.0-rc.5 · npm next" as facades #C5CAE9 +rectangle "liblevenshtein compatibility facade\n4.0.0-rc.5 · npm next" as legacy #FFCCBC lattice --> dict interop --> dict diff --git a/docs/diagrams/release-dependency-graph.svg b/docs/diagrams/release-dependency-graph.svg index 80d660e..cf747cd 100644 --- a/docs/diagrams/release-dependency-graph.svg +++ b/docs/diagrams/release-dependency-graph.svg @@ -1 +1 @@ -Immutable 4.0.0-rc.4 JavaScript release dependency graphllattice0.1.0vinary-tree-interop4.0.0-rc.4libdictenstein4.0.0-rc.4liblevenshtein4.0.0-rc.4lling-llang4.0.0-rc.4duallity4.0.0-rc.4platform native prebuilds+ browser WASM + WASI@vinary-tree/vinary-tree4.0.0-rc.4 · npm nextproject npm facades4.0.0-rc.4 · npm nextliblevenshtein compatibility facade4.0.0-rc.4 · npm nextProtected publication environment;publish under next, then replace thescoped 0.0.0 bootstrap latest after smoke. \ No newline at end of file +Immutable 4.0.0-rc.5 JavaScript release dependency graphllattice0.1.0@vinary-tree/vinary-tree-interop4.0.0-rc.5 · npm nextlibdictenstein4.0.0-rc.5liblevenshtein4.0.0-rc.5lling-llang4.0.0-rc.5duallity4.0.0-rc.5platform native prebuilds+ browser WASM + WASI@vinary-tree/javascript-runtime4.0.0-rc.5 · npm nextproject npm facades4.0.0-rc.5 · npm nextliblevenshtein compatibility facade4.0.0-rc.5 · npm nextProtected publication environment;publish under next, then replace thescoped 0.0.0 bootstrap latest after smoke. \ No newline at end of file diff --git a/docs/diagrams/runtime-architecture.puml b/docs/diagrams/runtime-architecture.puml index 0d28335..11b6405 100644 --- a/docs/diagrams/runtime-architecture.puml +++ b/docs/diagrams/runtime-architecture.puml @@ -16,7 +16,7 @@ package "Project facades" #E8EAF6 { component "@vinary-tree/duallity" as dualFacade #C5CAE9 } -package "@vinary-tree/vinary-tree 4.0.0-rc.4" #E0F2F1 { +package "@vinary-tree/javascript-runtime 4.0.0-rc.5" #E0F2F1 { component "Runtime identity\n(resource compatibility gate)" as identity #80CBC4 component "Node N-API" as napi #B2DFDB component "Browser WASM" as wasm #B2DFDB @@ -26,7 +26,7 @@ package "@vinary-tree/vinary-tree 4.0.0-rc.4" #E0F2F1 { component "liblevenshtein" as lev #C8E6C9 component "lling-llang" as lling #C8E6C9 component "duallity" as dual #C8E6C9 - component "vinary-tree-interop\nretained resources" as interop #FFF59D + component "@vinary-tree/vinary-tree-interop\nretained resources" as interop #FFF59D } } diff --git a/docs/diagrams/runtime-architecture.svg b/docs/diagrams/runtime-architecture.svg index 4031d4d..3d5a897 100644 --- a/docs/diagrams/runtime-architecture.svg +++ b/docs/diagrams/runtime-architecture.svg @@ -1 +1 @@ -One JavaScript object graph, three physical backends, four project enginesProject facades@vinary-tree/vinary-tree 4.0.0-rc.4One selected backend instance@vinary-tree/libdictensteinliblevenshtein@vinary-tree/lling-llang@vinary-tree/duallityRuntime identity(resource compatibility gate)Node N-APIBrowser WASMNode WASIlibdictensteinliblevenshteinlling-llangduallityvinary-tree-interopretained resourcesApplicationO(1) in-process handoff:retain capability, do not serialize graphNodebrowserNode + filesystemproduces dictionaryconsumes dictionaryconsumes dictionaryproduces WFSTproduces/consumes WFST \ No newline at end of file +One JavaScript object graph, three physical backends, four project enginesProject facades@vinary-tree/javascript-runtime 4.0.0-rc.5One selected backend instance@vinary-tree/libdictensteinliblevenshtein@vinary-tree/lling-llang@vinary-tree/duallityRuntime identity(resource compatibility gate)Node N-APIBrowser WASMNode WASIlibdictensteinliblevenshteinlling-llangduallity@vinary-tree/vinary-tree-interopretained resourcesApplicationO(1) in-process handoff:retain capability, do not serialize graphNodebrowserNode + filesystemproduces dictionaryconsumes dictionaryconsumes dictionaryproduces WFSTproduces/consumes WFST \ No newline at end of file diff --git a/docs/npm-coordinate-migration.md b/docs/npm-coordinate-migration.md new file mode 100644 index 0000000..440ec50 --- /dev/null +++ b/docs/npm-coordinate-migration.md @@ -0,0 +1,78 @@ +# npm coordinate migration for the JavaScript runtime + +## Canonical public identity + +The standalone runtime repository publishes +`@vinary-tree/javascript-runtime`. The name states what the package is and +matches the source repository; it is not a family-wide umbrella package. The +runtime hosts native N-API, browser WebAssembly (WASM), and WebAssembly System +Interface (WASI) backends for the project-specific facades. + +RC4 was published under `@vinary-tree/vinary-tree`. That coordinate is an +immutable historical publication mistake. It does not rename the Vinary Tree +project, and it must not appear in new manifests, imports, examples, or release +graphs. + +| Role | Coordinate | Policy from RC5 onward | +|---|---|---| +| Canonical shared runtime | `@vinary-tree/javascript-runtime` | Publish and consume directly. | +| Canonical resource contracts | `@vinary-tree/vinary-tree-interop` | Exact RC dependency of the runtime. | +| Legacy RC4 runtime | `@vinary-tree/vinary-tree` | Preserve immutable bytes; deprecate only after canonical public-install verification. | + +## Single source of truth + +[`release/version.json`](../release/version.json) owns +`coordinates.npmPackage`, the private property-test package name, the RC +version, and exact upstream versions and source tags. The synchronizer derives +package manifests from those fields and rejects legacy, malformed, or drifted +coordinates. In particular, it rejects an accidental concatenation such as +`@vinary-tree/javascript-runtime-interop`; the runtime and interop package are +two independent coordinates joined by an exact dependency edge. + +## Safe migration algorithm + +The following literate procedure distinguishes immutable artifacts from +mutable registry pointers: + +```text +build native, WASM, and WASI artifacts from the exact RC5 source graph +pack the canonical runtime and inspect its package name and dependency keys +publish canonical RC5 with provenance under the `next` dist-tag +install from the public registry in an empty project +exercise all three backends and cross-project retained-resource handoff +promote canonical `latest` only after the full train passes +deprecate the legacy coordinate with a canonical replacement message +read back package metadata and dist-tags without local workspace overlays +``` + +If verification fails, leave the immutable version in place, keep or restore +the previous distribution tag, correct the source, and issue the next unused +candidate. Never overwrite a version or silently redirect module resolution. + +## Consumer migration + +Applications should normally install a project facade. Direct runtime users +replace both the package and any backend subpath: + +```sh +npm install @vinary-tree/javascript-runtime@next +``` + +```js +import runtime from "@vinary-tree/javascript-runtime"; +import wasmRuntime from "@vinary-tree/javascript-runtime/wasm"; +import wasiRuntime from "@vinary-tree/javascript-runtime/wasi"; +``` + +The runtime's public semantics do not change: one package instance owns the +runtime identity, and resources from another instance are rejected before +native dispatch. + +## Trust boundary + +The package is published through npm's repository-bound trusted publisher and +the protected `npm` environment. Public read-back must match the expected +repository, exact tag, version, coordinate, dependency coordinate, and +provenance before distribution tags or deprecations are changed. Authentication +material must never be written into package metadata, generated artifacts, +logs, or documentation. diff --git a/docs/releasing.md b/docs/releasing.md index 7e4b9cb..108fbe2 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,4 +1,4 @@ -# Releasing `@vinary-tree/vinary-tree` +# Releasing `@vinary-tree/javascript-runtime` The runtime is a downstream assembly artifact. It must never publish before the exact Rust crates and shared interop package it consumes are publicly @@ -9,11 +9,14 @@ resolvable. ## Release identity `release/version.json` is authoritative. For this train, every Rust and npm -coordinate is `4.0.0-rc.4`, and npm publication uses the `next` distribution +coordinate is `4.0.0-rc.5`, and npm publication uses the `next` distribution tag. npm's first-publication behavior assigned `latest` to the inert `0.0.0` bootstrap reservation. After the OIDC-published runtime passes installed native, WASM, and WASI smoke tests, retarget the new scoped package's `latest` -pointer to `4.0.0-rc.4`, remove `bootstrap`, and deprecate `0.0.0`. +pointer to `4.0.0-rc.5`, remove `bootstrap`, and deprecate `0.0.0`. +The [npm coordinate migration](npm-coordinate-migration.md) records the exact +identity correction and the compatibility policy for the immutable RC4 +artifacts. The runtime's exact Rust requirements deliberately reject a mixed family. The development overlay changes only source location; it does not relax versions. @@ -24,33 +27,10 @@ lockfile. Every component lock must remain byte-for-byte unchanged. ## Exact-tag workflow protocol -Pushing the annotated tag creates only the immutable source ref. Release +Pushing the annotated `v4.0.0-rc.5` tag creates only the immutable source ref. Release validation and publication are explicit manual dispatches so the complete cross-project tag graph and public prerequisites can be established first. -The canonical tag predates protected GitHub-release approval. Append-only -corrective source `v4.0.0-rc.4-release.1` added those authority boundaries. -Its validate-only matrix exposed a separate topology defect: family repositories -were cloned beneath the runtime checkout, so owner-crate Cargo builds inherited -the runtime's `[patch.crates-io]` overlay and rejected their otherwise unchanged -lockfiles. No package-registry job ran. - -Append-only corrective source `v4.0.0-rc.4-release.2` fixes the topology and -records every family checkout in `release/version.json` as an exact immutable -tag. Native, browser-WASM, WASI, and development integration jobs now place -family owners beside the runtime checkout. Both the checkout helper and local -layout validation reject nested owner roots. Package identity remains -`4.0.0-rc.4`; neither prior tag is moved. - -The exact-source gate then exposed a malformed CRLF blob in interop's Windows -Gradle launcher: a fresh checkout of the otherwise exact tag appeared locally -modified after Git applied the declared `text eol=crlf` filter. Interop -corrective source `v4.0.0-rc.4-release.3` stores canonical LF object data and -retains CRLF checkout semantics. Runtime corrective source -`v4.0.0-rc.4-release.3` changes only the immutable source map to consume that -interop correction. It preserves the package version, platform topology, and -all other owner refs from runtime release.2. - The release workflow has two fail-closed modes. `validate-only` builds all six native prebuilds plus browser WebAssembly and WASI, assembles the npm tarball, verifies its contents, and creates the checksummed GitHub prerelease. `npm` @@ -60,12 +40,12 @@ environment. ```bash gh workflow run release.yml \ --repo vinary-tree/javascript-runtime \ - --ref v4.0.0-rc.4-release.3 \ + --ref v4.0.0-rc.5 \ -f registry=validate-only gh workflow run release.yml \ --repo vinary-tree/javascript-runtime \ - --ref v4.0.0-rc.4-release.3 \ + --ref v4.0.0-rc.5 \ -f registry=npm ``` @@ -79,6 +59,31 @@ The `github-release` environment has the same required reviewer and `v*` tag policy as npm but stores no secret; it gates only the job-scoped `GITHUB_TOKEN` used to create the checksummed prerelease. +### RC4 historical source record + +The RC4 canonical tag predated protected GitHub-release approval. Append-only +corrective source `v4.0.0-rc.4-release.1` added those authority boundaries. +Its validate-only matrix exposed a separate topology defect: family repositories +were cloned beneath the runtime checkout, so owner-crate Cargo builds inherited +the runtime's `[patch.crates-io]` overlay and rejected their otherwise unchanged +lockfiles. No package-registry job ran. + +Append-only corrective source `v4.0.0-rc.4-release.2` fixed the topology and +recorded every family checkout in `release/version.json` as an exact immutable +tag. Native, browser-WASM, WASI, and development integration jobs place family +owners beside the runtime checkout. Both the checkout helper and local layout +validation reject nested owner roots. RC4 package identity remained immutable; +neither prior tag was moved. + +The exact-source gate then exposed a malformed CRLF blob in interop's Windows +Gradle launcher: a fresh checkout of the otherwise exact tag appeared locally +modified after Git applied the declared `text eol=crlf` filter. Interop +corrective source `v4.0.0-rc.4-release.3` stores canonical LF object data and +retains CRLF checkout semantics. Runtime corrective source +`v4.0.0-rc.4-release.3` changed only the immutable source map to consume that +interop correction. It preserved the package version, platform topology, and +all other owner refs from runtime release.2. + ## Required order 1. Publish independent leaf crate `llattice` at its own `0.1.0` version if the @@ -86,13 +91,14 @@ policy as npm but stores no secret; it gates only the job-scoped 2. Publish `vinary-tree-interop` and verify installation from each supported registry coordinate. 3. Publish `libdictenstein`, `liblevenshtein`, `lling-llang`, and `duallity` - crates at `4.0.0-rc.4` in dependency order. + crates at `4.0.0-rc.5` in dependency order. 4. Build the runtime's native prebuild matrix from the exact component tags in `release/version.json`; build browser WASM and WASI from the same source map. 5. Merge the platform artifacts, run package-content and installed-tarball - smoke tests, then publish `@vinary-tree/vinary-tree@4.0.0-rc.4` with `next`. + smoke tests, then publish `@vinary-tree/javascript-runtime@4.0.0-rc.5` with + `next`. 6. Publish project-specific npm facades against that exact runtime. -7. Publish the unscoped `liblevenshtein@4.0.0-rc.4` compatibility facade with +7. Publish the unscoped `liblevenshtein@4.0.0-rc.5` compatibility facade with `next`; do not change the legacy `latest` tag. ## Native platform matrix diff --git a/index.d.ts b/index.d.ts index 058a8d6..cfaa826 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ /// -import type { DictionaryResource, RuntimeIdentity, UnitDomain } from "@vinary-tree/interop"; +import type { DictionaryResource, RuntimeIdentity, UnitDomain } from "@vinary-tree/vinary-tree-interop"; export type Algorithm = "standard" | "transposition" | "merge-and-split" | "damerau-levenshtein"; export type QueryOrder = "traversal" | "distance-then-term"; diff --git a/native.cjs b/native.cjs index 44ec61f..3c64136 100644 --- a/native.cjs +++ b/native.cjs @@ -10,7 +10,7 @@ try { ffi = require(addon); } catch (cause) { throw new Error( - `@vinary-tree/vinary-tree has no usable native addon for ${platform}; ` + `@vinary-tree/javascript-runtime has no usable native addon for ${platform}; ` + "install a supported prebuilt package or build the addon from source", { cause }, ); diff --git a/native.mjs b/native.mjs index 23a301c..020bbe5 100644 --- a/native.mjs +++ b/native.mjs @@ -20,7 +20,7 @@ try { ffi = require(fileURLToPath(addon)); } catch (cause) { throw new Error( - `@vinary-tree/vinary-tree has no usable native addon for ${platform}; ` + `@vinary-tree/javascript-runtime has no usable native addon for ${platform}; ` + "install a supported prebuilt package or build the addon from source", { cause }, ); diff --git a/package.json b/package.json index 981d172..cf50389 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@vinary-tree/vinary-tree", - "version": "4.0.0-rc.4", + "name": "@vinary-tree/javascript-runtime", + "version": "4.0.0-rc.5", "description": "Single-instance JavaScript runtime for related Vinary Tree libraries", "author": { "name": "Dylon Edwards", @@ -17,7 +17,7 @@ "node": ">=22.14" }, "dependencies": { - "@vinary-tree/interop": "4.0.0-rc.4" + "@vinary-tree/vinary-tree-interop": "4.0.0-rc.5" }, "types": "./index.d.ts", "exports": { diff --git a/release/version.json b/release/version.json index 68f25b9..793db16 100644 --- a/release/version.json +++ b/release/version.json @@ -1,10 +1,14 @@ { "schemaVersion": 1, "component": "javascript-runtime", - "canonical": "4.0.0-rc.4", - "npm": "4.0.0-rc.4", + "canonical": "4.0.0-rc.5", + "coordinates": { + "npmPackage": "@vinary-tree/javascript-runtime", + "npmPropertyTestPackage": "@vinary-tree/javascript-runtime-property-tests" + }, + "npm": "4.0.0-rc.5", "distTag": "next", - "sourceTag": "v4.0.0-rc.4-release.3", + "sourceTag": "v4.0.0-rc.5", "nativePrebuilds": [ "linux-x64", "linux-arm64", @@ -14,20 +18,20 @@ "win32-arm64" ], "dependencies": { - "duallity": "4.0.0-rc.4", + "duallity": "4.0.0-rc.5", "llattice": "0.1.0", - "libdictenstein": "4.0.0-rc.4", - "liblevenshtein": "4.0.0-rc.4", - "lling-llang": "4.0.0-rc.4", - "vinary-tree-interop": "4.0.0-rc.4", - "@vinary-tree/interop": "4.0.0-rc.4" + "libdictenstein": "4.0.0-rc.5", + "liblevenshtein": "4.0.0-rc.5", + "lling-llang": "4.0.0-rc.5", + "vinary-tree-interop": "4.0.0-rc.5", + "@vinary-tree/vinary-tree-interop": "4.0.0-rc.5" }, "sourceRefs": { - "vinary-tree-interop": "v4.0.0-rc.4-release.3", + "vinary-tree-interop": "v4.0.0-rc.5", "llattice": "v0.1.0", - "libdictenstein": "v4.0.0-rc.4-release.1", - "liblevenshtein-rust": "v4.0.0-rc.4-release.3", - "lling-llang": "v4.0.0-rc.4-release.1", - "duallity": "v4.0.0-rc.4-release.1" + "libdictenstein": "v4.0.0-rc.5", + "liblevenshtein-rust": "v4.0.0-rc.5", + "lling-llang": "v4.0.0-rc.5", + "duallity": "v4.0.0-rc.5" } } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5333966..517b07c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -180,7 +180,7 @@ dependencies = [ [[package]] name = "duallity" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" dependencies = [ "libdictenstein", "liblevenshtein", @@ -274,7 +274,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdictenstein" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" dependencies = [ "arc-swap", "bincode-next", @@ -296,7 +296,7 @@ dependencies = [ [[package]] name = "liblevenshtein" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" dependencies = [ "ahash", "arc-swap", @@ -317,7 +317,7 @@ version = "0.1.0" [[package]] name = "lling-llang" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" dependencies = [ "ahash", "blake3", @@ -866,11 +866,11 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vinary-tree-interop" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" [[package]] name = "vinary-tree-js-runtime" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" dependencies = [ "console_error_panic_hook", "duallity", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 65e2b9b..5e59739 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vinary-tree-js-runtime" -version = "4.0.0-rc.4" +version = "4.0.0-rc.5" edition = "2021" rust-version = "1.95" license-file = "../LICENSE" @@ -12,11 +12,11 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -liblevenshtein = { version = "=4.0.0-rc.4", features = ["bindings-phonetic"] } -libdictenstein = { version = "=4.0.0-rc.4", features = ["bindings-core"] } -lling-llang = { version = "=4.0.0-rc.4", default-features = false, features = ["bindings-core"] } -duallity = { version = "=4.0.0-rc.4", features = ["bindings-core"] } -vinary-tree-interop = "=4.0.0-rc.4" +liblevenshtein = { version = "=4.0.0-rc.5", features = ["bindings-phonetic"] } +libdictenstein = { version = "=4.0.0-rc.5", features = ["bindings-core"] } +lling-llang = { version = "=4.0.0-rc.5", default-features = false, features = ["bindings-core"] } +duallity = { version = "=4.0.0-rc.5", features = ["bindings-core"] } +vinary-tree-interop = "=4.0.0-rc.5" wasm-bindgen = "=0.2.100" js-sys = "=0.3.77" console_error_panic_hook = "0.1" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 875570c..5f6d3b2 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,5 +1,5 @@ #![deny(missing_docs)] -//! Shared WebAssembly implementation behind `@vinary-tree/vinary-tree`. +//! Shared WebAssembly implementation behind `@vinary-tree/javascript-runtime`. #[cfg(all(feature = "browser", target_arch = "wasm32", target_os = "unknown"))] mod browser; diff --git a/scripts/checkout-family-sources.mjs b/scripts/checkout-family-sources.mjs index d21df4c..dc5eb56 100644 --- a/scripts/checkout-family-sources.mjs +++ b/scripts/checkout-family-sources.mjs @@ -7,15 +7,6 @@ import { fileURLToPath } from "node:url"; import { readReleaseModel, sourceOwners, validateSourceRefs } from "./release-source-refs.mjs"; const runtimeRoot = dirname(dirname(fileURLToPath(import.meta.url))); -const developmentRefs = Object.freeze({ - "vinary-tree-interop": "master", - llattice: "v0.1.0", - libdictenstein: "master", - "liblevenshtein-rust": "master", - "lling-llang": "release/4.0.0-rc.4", - duallity: "release/4.0.0-rc.4", -}); - export function validateCheckoutParent(value) { const parent = resolve(value); if (parent !== dirname(runtimeRoot)) { @@ -37,6 +28,37 @@ function output(command, args) { return result.stdout.trim(); } +export function selectDevelopmentRef(candidate) { + if (typeof candidate !== "string" || !candidate.startsWith("release/")) return "master"; + const components = candidate.split("/"); + const forbidden = /[\x00-\x20\x7f~^:?*\[\\]/; + if ( + candidate === "@" + || candidate.startsWith("-") + || candidate.endsWith(".") + || candidate.includes("..") + || candidate.includes("@{") + || forbidden.test(candidate) + || components.some((component) => ( + component.length === 0 + || component.startsWith(".") + || component.startsWith("-") + || component.endsWith(".lock") + )) + ) { + throw new Error(`invalid coordinated development ref: ${candidate}`); + } + return candidate; +} + +function developmentSourceRefs(candidate) { + const coordinated = selectDevelopmentRef(candidate); + return Object.freeze(Object.fromEntries(sourceOwners.map((owner) => [ + owner, + owner === "llattice" ? "v0.1.0" : coordinated, + ]))); +} + function checkout(parent, refs, immutable) { for (const owner of sourceOwners) { const target = resolve(parent, owner); @@ -63,6 +85,20 @@ function selfTest() { } if (!rejected) throw new Error(`unsafe checkout parent passed validation: ${forbidden}`); } + if (selectDevelopmentRef(undefined) !== "master") throw new Error("missing event ref must use master"); + if (selectDevelopmentRef("feature/local") !== "master") throw new Error("feature refs must use master siblings"); + if (selectDevelopmentRef("release/4.0.0-rc.5") !== "release/4.0.0-rc.5") { + throw new Error("coordinated release ref was not preserved"); + } + for (const malformed of ["release/-option", "release/../escape", "release/bad ref", "release/x.lock"]) { + let rejected = false; + try { + selectDevelopmentRef(malformed); + } catch { + rejected = true; + } + if (!rejected) throw new Error(`malformed coordinated ref passed validation: ${malformed}`); + } console.log("external family-checkout topology tests passed"); } @@ -83,6 +119,8 @@ if (argumentsByName.has("--self-test")) { } else { const parent = validateCheckoutParent(argumentsByName.get("--parent") ?? dirname(runtimeRoot)); const development = argumentsByName.has("--development"); - const refs = development ? developmentRefs : validateSourceRefs(readReleaseModel()); + const refs = development + ? developmentSourceRefs(argumentsByName.get("--development-ref")) + : validateSourceRefs(readReleaseModel()); checkout(parent, refs, !development); } diff --git a/scripts/release-source-refs.mjs b/scripts/release-source-refs.mjs index 449c078..5409125 100644 --- a/scripts/release-source-refs.mjs +++ b/scripts/release-source-refs.mjs @@ -60,31 +60,31 @@ export function readReleaseModel() { function selfTest() { const valid = { dependencies: { - "vinary-tree-interop": "4.0.0-rc.4", + "vinary-tree-interop": "4.0.0-rc.5", llattice: "0.1.0", - libdictenstein: "4.0.0-rc.4", - liblevenshtein: "4.0.0-rc.4", - "lling-llang": "4.0.0-rc.4", - duallity: "4.0.0-rc.4", + libdictenstein: "4.0.0-rc.5", + liblevenshtein: "4.0.0-rc.5", + "lling-llang": "4.0.0-rc.5", + duallity: "4.0.0-rc.5", }, sourceRefs: { - "vinary-tree-interop": "v4.0.0-rc.4-release.3", + "vinary-tree-interop": "v4.0.0-rc.5", llattice: "v0.1.0", - libdictenstein: "v4.0.0-rc.4-release.1", - "liblevenshtein-rust": "v4.0.0-rc.4-release.3", - "lling-llang": "v4.0.0-rc.4-release.1", - duallity: "v4.0.0-rc.4-release.1", + libdictenstein: "v4.0.0-rc.5", + "liblevenshtein-rust": "v4.0.0-rc.5", + "lling-llang": "v4.0.0-rc.5", + duallity: "v4.0.0-rc.5", }, }; validateSourceRefs(valid); const mutations = [ (model) => delete model.sourceRefs.duallity, - (model) => { model.sourceRefs.unexpected = "v4.0.0-rc.4"; }, + (model) => { model.sourceRefs.unexpected = "v4.0.0-rc.5"; }, (model) => { model.sourceRefs.libdictenstein = "master"; }, (model) => { model.sourceRefs.libdictenstein = "208d9cd6ccfc4993acddd3c166bb314049dfb258"; }, (model) => { model.sourceRefs.libdictenstein = "v4.0.0-rc.3"; }, - (model) => { model.sourceRefs.libdictenstein = "v4.0.0-rc.4-release.0"; }, - (model) => { model.sourceRefs.libdictenstein = "v4.0.0-rc.4-release.next"; }, + (model) => { model.sourceRefs.libdictenstein = "v4.0.0-rc.5-release.0"; }, + (model) => { model.sourceRefs.libdictenstein = "v4.0.0-rc.5-release.next"; }, ]; for (const mutate of mutations) { const malformed = structuredClone(valid); diff --git a/scripts/sync-release-version.mjs b/scripts/sync-release-version.mjs index 82a3556..fc0e34e 100644 --- a/scripts/sync-release-version.mjs +++ b/scripts/sync-release-version.mjs @@ -1,4 +1,4 @@ -import { readFileSync, writeFileSync } from "node:fs"; +import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { validateSourceRefs } from "./release-source-refs.mjs"; @@ -9,6 +9,20 @@ const write = process.argv.includes("--write"); if (!/^\d+\.\d+\.\d+-rc\.\d+$/.test(model.canonical)) { throw new Error(`canonical version is not a numbered RC: ${model.canonical}`); } +const npmPackage = model.coordinates?.npmPackage; +const npmPropertyTestPackage = model.coordinates?.npmPropertyTestPackage; +const legacyInteropPackage = ["@vinary-tree", "interop"].join("/"); +if (npmPackage !== "@vinary-tree/javascript-runtime") { + throw new Error( + "release/version.json coordinates.npmPackage must be @vinary-tree/javascript-runtime", + ); +} +if (npmPropertyTestPackage !== "@vinary-tree/javascript-runtime-property-tests") { + throw new Error( + "release/version.json coordinates.npmPropertyTestPackage must be " + + "@vinary-tree/javascript-runtime-property-tests", + ); +} const nativePrebuilds = [ "linux-x64", "linux-arm64", @@ -52,17 +66,25 @@ function rewriteCandidateTokens(paths) { const packageJson = updateJson("package.json", (value) => { if (write) { + value.name = npmPackage; value.version = model.npm; - value.dependencies["@vinary-tree/interop"] = model.dependencies["@vinary-tree/interop"]; + delete value.dependencies[legacyInteropPackage]; + value.dependencies["@vinary-tree/vinary-tree-interop"] = + model.dependencies["@vinary-tree/vinary-tree-interop"]; value.publishConfig.tag = model.distTag; } }); const propertyPackage = updateJson("test-property/package.json", (value) => { - if (write) value.version = model.canonical; + if (write) { + value.name = npmPropertyTestPackage; + value.version = model.canonical; + } }); const propertyLock = updateJson("test-property/package-lock.json", (value) => { if (write) { + value.name = npmPropertyTestPackage; value.version = model.canonical; + value.packages[""].name = npmPropertyTestPackage; value.packages[""].version = model.canonical; } }); @@ -105,7 +127,6 @@ if (write) writeFileSync(cargoLockPath, cargoLock); rewriteCandidateTokens([ ".github/workflows/ci.yml", "README.md", - "docs/releasing.md", "docs/diagrams/release-dependency-graph.puml", "docs/diagrams/runtime-architecture.puml", ]); @@ -114,7 +135,12 @@ const failures = []; const expect = (name, actual, wanted) => { if (actual !== wanted) failures.push(`${name}: expected ${wanted}, got ${actual}`); }; -expect("corrective source tag", model.sourceTag, `v${model.canonical}-release.3`); +const escapedCanonical = model.canonical.replaceAll(".", "\\."); +if (!new RegExp(`^v${escapedCanonical}(?:-release\\.[1-9][0-9]*)?$`).test(model.sourceTag)) { + failures.push( + `source tag must be v${model.canonical} or an append-only numbered correction, got ${model.sourceTag}`, + ); +} validateSourceRefs(model); const releaseWorkflow = readFileSync(join(root, ".github", "workflows", "release.yml"), "utf8"); for (const marker of [ @@ -124,11 +150,20 @@ for (const marker of [ ]) { if (!releaseWorkflow.includes(marker)) failures.push(`release workflow is missing ${marker}`); } +expect("npm package", packageJson.name, npmPackage); expect("npm", packageJson.version, model.npm); -expect("npm interop", packageJson.dependencies["@vinary-tree/interop"], model.dependencies["@vinary-tree/interop"]); +expect( + "npm interop", + packageJson.dependencies["@vinary-tree/vinary-tree-interop"], + model.dependencies["@vinary-tree/vinary-tree-interop"], +); +expect("legacy npm interop dependency", packageJson.dependencies[legacyInteropPackage], undefined); expect("npm dist-tag", packageJson.publishConfig.tag, model.distTag); +expect("property package name", propertyPackage.name, npmPropertyTestPackage); expect("property package", propertyPackage.version, model.canonical); +expect("property lock name", propertyLock.name, npmPropertyTestPackage); expect("property lock", propertyLock.version, model.canonical); +expect("property lock root name", propertyLock.packages[""].name, npmPropertyTestPackage); expect("property lock root", propertyLock.packages[""].version, model.canonical); expect("Rust package", cargo.match(/^version = "([^"]+)"/m)?.[1], model.canonical); for (const dependency of ["duallity", "libdictenstein", "liblevenshtein", "lling-llang"]) { @@ -152,5 +187,78 @@ for (const [name, version] of lockedReleasePackages) { version, ); } + +const ignoredCoordinateTrees = new Set([ + ".git", + ".build", + "build", + "dist", + "generated", + "native/prebuilds", + "node_modules", + "target", +]); +const coordinateMigrationRecord = "docs/npm-coordinate-migration.md"; +const forbiddenCoordinates = [ + ["legacy interop coordinate", legacyInteropPackage], + ["legacy runtime coordinate", ["@vinary-tree", "vinary-tree"].join("/")], + [ + "malformed runtime/interop composition", + ["@vinary-tree", "javascript-runtime-interop"].join("/"), + ], +]; + +function coordinateViolation(source) { + for (const [label, coordinate] of forbiddenCoordinates) { + const escaped = coordinate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`${escaped}(?![A-Za-z0-9._-])`).exec(source); + if (match !== null) return { label, index: match.index }; + } + return undefined; +} + +for (const coordinate of [ + "@vinary-tree/vinary-tree-interop", + "@vinary-tree/javascript-runtime", + "@vinary-tree/javascript-runtime/wasm", +]) { + expect(`coordinate gate accepts ${coordinate}`, coordinateViolation(coordinate), undefined); +} +for (const [label, coordinate] of forbiddenCoordinates) { + expect( + `coordinate gate rejects ${label}`, + coordinateViolation(`"${coordinate}@4.0.0-rc.4"`)?.label, + label, + ); +} + +function validateNpmCoordinates(directory = root, relativeDirectory = "") { + for (const entry of readdirSync(directory)) { + const relative = relativeDirectory === "" ? entry : `${relativeDirectory}/${entry}`; + if ([...ignoredCoordinateTrees].some((tree) => relative === tree || relative.startsWith(`${tree}/`))) { + continue; + } + const absolute = join(directory, entry); + if (statSync(absolute).isDirectory()) { + validateNpmCoordinates(absolute, relative); + continue; + } + if (relative === coordinateMigrationRecord) continue; + let source; + try { + source = readFileSync(absolute, "utf8"); + } catch { + continue; + } + if (source.includes("\0")) continue; + const violation = coordinateViolation(source); + if (violation !== undefined) { + const line = source.slice(0, violation.index).split("\n").length; + failures.push(`${relative}:${line}: ${violation.label} is forbidden`); + } + } +} + +validateNpmCoordinates(); if (failures.length > 0) throw new Error(failures.join("\n")); console.log(`release versions agree with ${model.canonical}`); diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index 294f32d..d087929 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -1,4 +1,14 @@ import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const release = JSON.parse(readFileSync(join(root, "release", "version.json"), "utf8")); +const packageName = release.coordinates?.npmPackage; +if (packageName !== "@vinary-tree/javascript-runtime") { + throw new Error("release manifest does not name the canonical JavaScript runtime package"); +} const result = spawnSync("npm", ["pack", "--dry-run", "--ignore-scripts", "--json"], { encoding: "utf8", @@ -11,7 +21,7 @@ const report = Array.isArray(parsed) ? parsed[0] : parsed.files ? parsed - : parsed["@vinary-tree/vinary-tree"] ?? Object.values(parsed)[0]; + : parsed[packageName] ?? Object.values(parsed)[0]; if (!report || !Array.isArray(report.files)) { throw new Error("npm pack returned no package file report"); } diff --git a/test-property/native.property.test.mjs b/test-property/native.property.test.mjs index 55bf3d0..2b85348 100644 --- a/test-property/native.property.test.mjs +++ b/test-property/native.property.test.mjs @@ -1,7 +1,7 @@ // C8 native property-based tests for the javascript-runtime facade (fast-check). // // This suite lives in an isolated package whose only dependency is fast-check, -// so `npm install` never has to resolve the unpublished @vinary-tree/interop +// so `npm install` never has to resolve the unpublished @vinary-tree/vinary-tree-interop // sibling that the umbrella package declares. It imports the native runtime by // relative path (`../native.mjs`), which loads the prebuilt N-API addon. // diff --git a/test-property/package-lock.json b/test-property/package-lock.json index 982c951..ccb68e0 100644 --- a/test-property/package-lock.json +++ b/test-property/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@vinary-tree/vinary-tree-property-tests", - "version": "4.0.0-rc.4", + "name": "@vinary-tree/javascript-runtime-property-tests", + "version": "4.0.0-rc.5", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@vinary-tree/vinary-tree-property-tests", - "version": "4.0.0-rc.4", + "name": "@vinary-tree/javascript-runtime-property-tests", + "version": "4.0.0-rc.5", "devDependencies": { "fast-check": "^4.0.0" } diff --git a/test-property/package.json b/test-property/package.json index 9a0b899..b0f1d4a 100644 --- a/test-property/package.json +++ b/test-property/package.json @@ -1,6 +1,6 @@ { - "name": "@vinary-tree/vinary-tree-property-tests", - "version": "4.0.0-rc.4", + "name": "@vinary-tree/javascript-runtime-property-tests", + "version": "4.0.0-rc.5", "private": true, "type": "module", "description": "C8 fast-check property tests for the native runtime in an isolated private package", diff --git a/wasi.d.ts b/wasi.d.ts index a61fff6..06ba575 100644 --- a/wasi.d.ts +++ b/wasi.d.ts @@ -1,4 +1,4 @@ -import type { RuntimeIdentity, UnitDomain } from "@vinary-tree/interop"; +import type { RuntimeIdentity, UnitDomain } from "@vinary-tree/vinary-tree-interop"; import type { Dictionary, QueryCursor, Algorithm, LlingLlangNamespace, DuallityNamespace, } from "./index.js";