From d6e7963c45bf0983116172665474cd63819468ff Mon Sep 17 00:00:00 2001 From: Dylon Edwards Date: Wed, 26 Aug 2026 17:45:02 -0400 Subject: [PATCH 1/2] release(npm): name the shared runtime explicitly for 4.0.0-rc.5 1. Replace the ambiguous @vinary-tree/vinary-tree package identity with @vinary-tree/javascript-runtime across manifests, locks, declarations, loaders, tests, and diagrams. 2. Point the runtime at the canonical @vinary-tree/vinary-tree-interop dependency and synchronize Rust and npm versions with the coordinated release train. 3. Add source-reference, package-content, and coordinate-drift gates, including append-only corrective-tag validation. 4. Document the migration boundary and preserve native, browser-WASM, and WASI runtime behavior verified by package and property tests. --- .gitignore | 2 +- README.md | 19 ++-- docs/diagrams/release-dependency-graph.puml | 18 +-- docs/diagrams/release-dependency-graph.svg | 2 +- docs/diagrams/runtime-architecture.puml | 4 +- docs/diagrams/runtime-architecture.svg | 2 +- docs/npm-coordinate-migration.md | 78 +++++++++++++ docs/releasing.md | 70 ++++++------ index.d.ts | 2 +- native.cjs | 2 +- native.mjs | 2 +- package.json | 6 +- release/version.json | 32 +++--- rust/Cargo.lock | 12 +- rust/Cargo.toml | 12 +- rust/src/lib.rs | 2 +- scripts/checkout-family-sources.mjs | 4 +- scripts/release-source-refs.mjs | 26 ++--- scripts/sync-release-version.mjs | 120 +++++++++++++++++++- scripts/verify-package.mjs | 12 +- test-property/native.property.test.mjs | 2 +- test-property/package-lock.json | 8 +- test-property/package.json | 4 +- wasi.d.ts | 2 +- 24 files changed, 325 insertions(+), 118 deletions(-) create mode 100644 docs/npm-coordinate-migration.md 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..50e9fa8 100644 --- a/scripts/checkout-family-sources.mjs +++ b/scripts/checkout-family-sources.mjs @@ -12,8 +12,8 @@ const developmentRefs = Object.freeze({ llattice: "v0.1.0", libdictenstein: "master", "liblevenshtein-rust": "master", - "lling-llang": "release/4.0.0-rc.4", - duallity: "release/4.0.0-rc.4", + "lling-llang": "master", + duallity: "master", }); export function validateCheckoutParent(value) { 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"; From 6bf343ca5ed66b94dc04e74629af75f2a899f8fa Mon Sep 17 00:00:00 2001 From: Dylon Edwards Date: Thu, 27 Aug 2026 13:28:57 -0400 Subject: [PATCH 2/2] ci: resolve coordinated RC5 sources without registry fallback 1. Derive release sibling refs from the pull-request head while keeping ordinary development builds on master and llattice on its immutable v0.1.0 release. 2. Validate coordinated Git refs and exercise normal, release, and hostile candidates in the checkout helper self-test. 3. Install the canonical interop dependency with an explicit name-to-file specification so npm cannot fall back to the unpublished RC5 registry coordinate. 4. Pass GitHub event data through the environment instead of interpolating untrusted branch names into shell source. --- .github/workflows/ci.yml | 9 ++++- .github/workflows/release.yml | 6 ++- scripts/checkout-family-sources.mjs | 58 ++++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 14 deletions(-) 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/scripts/checkout-family-sources.mjs b/scripts/checkout-family-sources.mjs index 50e9fa8..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": "master", - duallity: "master", -}); - 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); }