@candid-core/cli
The same compiler as WebAssembly: a JavaScript-only on-ramp for Node and the browser.
@candid-core/cli 0.4.0 is on the npm registry under the
latest tag, published from this repository by its dispatch-only release workflow,
beside the earlier 0.1.0, 0.2.0 and 0.3.0 and, under the beta tag,
0.2.0-beta.1.
The registry also holds 0.0.0-bootstrap under the bootstrap tag, which
exists only so a trusted publisher could be attached to the name; do not install it. The
candid-core binary does the same compilation from Rust.
This page describes the package the repository builds, published on 2026-10-27 as 0.4.0 under
the npm latest dist-tag, with @candid-core/schema 0.5.0, which it peers exactly. The previous
stable release, 0.3.0, lacks what this page marks as new in 0.4.0: didToDocAttributes and each entry’s identities in gen --json. It also names every use of a blob, record {} or variant {} after the first alias of that shape (the code generator), and writes envelopes whose producer is candid-core 0.1.0-beta.3, where 0.4.0's is meant 0.1.0, the compiler crate published as candid-core before its rename, so 0.4.0's gen --check reports every envelope 0.3.0 wrote as drifted. Migrating from 0.4.0 says what the upgrade means.
The stable release before that, 0.2.0, also lacks what this page marks as new in 0.3.0: the
project and check commands, with
projectDid and checkCompatible, and generated modules that declare each type under its Candid name and each
Actor method with its mode. Migrating from 0.3.0 says what that upgrade means, and the
package changelog records it under 0.3.0.
The stable release before those, 0.1.0, has the old behaviour: it
emits modules in the old layout with an object-shaped principal type, exits 1 with
ts_generation_refused for an interface with one declaration it cannot represent, and
returns no omitted list. Migrating from 0.2.0 shows what each break means, with before and after code,
and each is recorded in the package changelog under 0.2.0 and 0.2.0-beta.1. The beta dist-tag still names 0.2.0-beta.1, which lacks the changes recorded under 0.2.0 and every stable release since.
# 0.4.0, the release this page describes (see the note above)
npx @candid-core/[email protected] gen ./service.did -o ./generated
Here is what the package is and what it does. Everything candid-core does to a
.did file normally happens in Rust. @candid-core/cli is that same Rust code
— the compiler's compiler feature surface plus the
candid-core-ts TypeScript generator — compiled once to
WebAssembly and wrapped in a small Node CLI and an ES module. A JavaScript project can therefore go
from a Candid interface to typed, validated schemas with npm as its only toolchain.
It is not a reimplementation, and that is the point of its test suites: the same crates are compiled
into one wasm artifact, and CI compares the artifact's output byte-for-byte against the reviewed
goldens and against a document the native binary actually produced. The
crate that produces
it sets publish = false and is its own workspace root, so it never appears in the
published Rust crate's dependency graph; the npm package is the deliverable.
What is in the package#
| Field | Value |
|---|---|
| Name and version | @candid-core/cli, 0.4.0 — published on npm under latest |
| Binary | candid-core-cli → ./bin/cli.js |
| Library entry point |
"." → ./lib/index.js, under a types condition.
./package.json is the only other entry in the map
|
| Module format | "type": "module" — ESM only, no CommonJS require path |
| Runtime dependencies |
None. @candid-core/schema is declared as an optional peer at exactly
0.5.0 (new in 0.4.0) — every module the CLI
emits imports from it, but a consumer calling only didToContract never loads it.
The modules this page's generator emits compile against 0.4.0 and
0.5.0 alike and need the WithMode export that 0.3.0 lacks; while the two
packages move in lockstep every schema release is paired with a CLI release that names it
exactly. The published 0.3.0 declares exactly 0.4.0,
0.2.0 exactly 0.3.0,
0.2.0-beta.1 exactly 0.3.0-beta.1,
and 0.1.0 still declares ^0.2.0. The one
devDependency, playwright-core, drives the browser test
|
| Types |
A hand-written lib/index.d.ts, reached through the types
condition. The wasm-pack generated declarations are deliberately not shipped:
they name BufferSource, URL, Response and
WebAssembly, which do not resolve without the DOM library
|
| Licence | Apache-2.0 |
The command#
gen, and these options, nothing else (the usage's first line; project and check, new in 0.3.0, are under Projection and compatibility):
usage: candid-core-cli gen <service.did>... [-o <dir>] [--json] [--check]
The first stable release, 0.1.0, takes exactly one entry and neither flag:
gen <service.did> [-o <dir>]. Several entries, --json and
--check, described in their own section
below, shipped in 0.2.0-beta.1 and are in every stable release since 0.2.0. Everything else on this page describes both.
It writes two files and prints the identities:
-
<stem>.ts— the generated@candid-core/schemamodule: one reviewed type alias and one schema builder per declaration. -
<stem>.envelope.json— the one-documentContractEnvelope: the canonical Contract plus its field-name table under theorg.candid-core.field-names/v1extension, ready to hand whole toschemaFromContract. -
the content-addressed identities, on stdout. The
interface:line appears only when the interface identity is present, which means only when the source has an actor.
Run over the repository's two-line basic.did fixture — the same one shown on
the candid-core binary page — with -o generated, stdout is
four lines and the exit status is 0:
contract: candid-core:contract:v1:sha256:0b553cb65eb436a7ac5d35869d0016d043867bab0a97e844350ae72a5b4aea7d
interface: candid-core:interface:v1:sha256:99f400820c0ca2fb7c32cc3ab1df6d6c000bc614cffe0e350b6a9a576da18d48
wrote generated/basic.ts
wrote generated/basic.envelope.json
Those are the real identities for that fixture, and they are the same addresses the Rust binary
computes — a test compares the emitted envelope against the native binary's committed output byte for
byte. The stem comes from the entry file's name, so service.did would produce
service.ts and service.envelope.json. -o defaults to
"." — the current working directory, not the entry file's directory — and the output
directory is created if it does not exist.
How imports are found#
The wasm side never touches a filesystem, so the Node host does all the I/O. gen walks
the entry file's directory recursively, collects every .did beneath it keyed by
its /-separated relative path, and hands that map to the compiler as one in-memory
bundle. Relative imports then resolve inside the bundle with no filesystem access from wasm. The
consequence to plan for: unrelated sibling .did files under the same directory are pulled
in too. The parity tests copy each fixture into an otherwise-empty temporary directory precisely so
the walk cannot smuggle unrelated sources into provenance.
The walk is bounded before anything is read. File count, per-file size (through stat) and
aggregate size are checked against the compiler's own default limits — 256 sources, 1 MiB per file,
8 MiB in total — so an oversized tree fails with a structured diagnostic rather than by exhausting the
JavaScript heap:
{
"ok": false,
"diagnostics": [
{
"code": "resource_limit_exceeded",
"phase": "load",
"severity": "error",
"message": "huge.did is 1048577 bytes, over the 1048576-byte source limit",
"resource_limit": { "resource": "source_bytes", "limit": 1048576, "observed": 1048577 }
}
]
}
Inside a file, the compiler refuses a run of more than 256 consecutive comments between two tokens,
line and block comments alike, with resource_limit_exceeded and resource
source_nesting (limit 256, observed 257): the upstream tokenizer spends a
stack frame on each comment it skips, so the run is counted first. This is new in
0.2.0; earlier releases compiled such a run up to a stack-dependent length and then
failed (internal_error in 0.2.0-beta.1, an uncaught
RangeError in 0.1.0). A license header written as hundreds of //
lines is the likely case: write it as one /* */ block, which counts as one comment.
The CLI has no flag to raise the limit. Limits lists it with the
others.
Exit codes#
They follow the native binary's convention exactly.
| Exit | When | Where the output goes |
|---|---|---|
0 |
Both files written, including a module that omits declarations | Identities and two wrote lines on stdout; one warning: omitted … line per omission on stderr |
1 |
Compile or generation failure | The { "ok": false, "diagnostics": […] } document on stdout |
1 |
Entry file missing or unreadable, or a determinism mismatch | A plain message on stderr — the two paths that print no JSON |
1 |
With several entries, any one entry failed; with --check, a file is missing or has drifted |
As above, per entry; with --json, everything is in the one document on stdout |
64 |
Usage error, including two entries that would write the same output stem | The usage line on stderr, nothing on stdout |
That determinism path is unusual enough to name. Every generation is run twice and the results
compared; on any byte mismatch the tool prints
determinism check failed: two <label> runs disagreed; refusing to write and exits
without writing anything. Determinism is enforced at run time, not only asserted in CI.
Several entries, --json and --check#
Since 0.2.0-beta.1 (0.1.0 takes one entry and neither flag). Each entry generates its own
<stem>.ts and <stem>.envelope.json into the one
-o, byte for byte what a run of that entry alone writes, and a failing entry does not
stop the others: every entry is attempted, and the exit status is 1 if any failed.
Omissions never fail an entry. Two rules are recommendations the maintainer may still overturn: an
entry's bundle is the .did files beneath its own directory, with the limits above applied
per entry (entries sharing a directory share one read of it); and two entries with the same stem,
compared case-insensitively, are a usage error before any work. An output that already holds the
generated bytes is not rewritten, and is reported unchanged. The CLI never reads stdin
and never writes outside -o.
--check generates in memory and compares byte for byte with the files on disk. It writes
nothing, not even the output directory, and exits 1 if any file is missing or differs,
which makes it the CI gate for committed output.
--json prints exactly one document on stdout, with stderr empty; combine it with
--check. Each compiled entry carries its content-addressed identities. A usage error
prints no document.
0.3.0 and earlier leave the identities out of the --json document; read them
from the envelope's contract.identities there. 0.4.0, published on
2026-10-27, adds each entry's identities. Every other
field below is the same in 0.3.0.
{
"schemaVersion": 1,
"ok": true,
"check": false,
"entries": [
{
"entry": "a/service.did",
"status": "written",
"module": "generated/service.ts",
"envelope": "generated/service.envelope.json",
"identities": {
"contract_id": "candid-core:contract:v1:sha256:…",
"interface_id": "candid-core:interface:v1:sha256:…"
},
"omitted": [],
"diagnostics": []
}
],
"drift": []
}
status is written, unchanged, drifted
(--check only) or failed. identities is
{ contract_id, interface_id }, equal to the envelope's contract.identities under
the key names project and check use; interface_id is absent
when the source has no actor, and a failed entry has no identities. omitted is the list
didToModule returns, and diagnostics is the compiler's own, unchanged in
shape; ok is true exactly when the exit status is 0; drift lists
the paths --check found missing or different. schemaVersion changes only when
an existing field's meaning or shape does. The types ship as CliReport,
CliEntryReport and CliIdentities.
The library API#
Six exports, and the whole surface: didToContract, didToModule,
didToDocAttributes and init here, and projectDid and
checkCompatible under
Projection and compatibility
(didToDocAttributes is new in 0.4.0;
projectDid and checkCompatible are new in 0.3.0;
0.2.0 has didToContract, didToModule and init).
didToContract, didToModule and didToDocAttributes each take one JSON-serializable request;
projectDid and checkCompatible take two arguments, described in that section.
Every operation returns one parsed JSON document, and nothing is thrown for a data error.
export async function didToContract(sources)
Compiles Candid sources into a one-document ContractEnvelope carrying the
org.candid-core.field-names/v1 extension — the same document the native
compiler's compile <path> --envelope emits. sources is either a
string of Candid text or { entry, files: { name: text } } for a bundle. On success it
returns the envelope itself, so you test "contract" in result; on failure it
returns { ok: false, diagnostics }.
export async function didToModule(sources)
Generates the @candid-core/schema TypeScript module for the same two source shapes.
Returns { ok: true, module, omitted }, where module is the generated
text and omitted lists what it left out (empty when nothing; see
Omissions), or { ok: false, diagnostics }.
export async function didToDocAttributes(sources)
New in 0.4.0. Compiles the same two source shapes and reads the
@name(key = value, …) attributes in their doc comments. Returns
{ ok: true, contract, source_info, doc_attributes } — the Contract, the SourceInfo
sidecar, which no other function of the package exposes, and every documented item's prose and
attributes (see Doc attributes) — or
{ ok: false, diagnostics }.
export function init(input)
Initializes the embedded wasm module, memoized, so calling it again is a no-op. Under Node with no
argument it reads the .wasm bytes from inside the package; in a browser with no
argument the artifact is fetched relative to the module, and a browser may instead pass its own
BufferSource, URL or Response. The functions above await it
internally, so calling it yourself is optional.
didToModule returns { ok: true, module, omitted }. didToContract does
not carry an ok key on success — it returns the envelope document, and you
check for "contract" in result. This mirrors the native binary, where
compile --envelope prints the envelope rather than an ok-wrapped response.
Only failures share one shape.
The examples below import from the published package; from a clone, import
crates/candid-core-wasm/npm/lib/index.js directly.
import { didToContract, didToModule } from "@candid-core/cli";
const envelope = await didToContract("service : { ping : () -> () };");
// → the ContractEnvelope document ("contract" in envelope), or
// { ok: false, diagnostics } with the compiler's diagnostics verbatim.
const generated = await didToModule({
entry: "main.did",
files: {
"main.did": 'import "types.did";\nservice : { get : () -> (Item) query };',
"types.did": "type Item = record { id : nat };",
},
});
// → { ok: true, module, omitted } with the generated TypeScript text and
// what it left out (empty here), or { ok: false, diagnostics }.
The envelope is designed to need no glue on the way out.
schemaFromContract accepts either a bare Contract or an envelope — it recognises the
envelope by its contract key — and consumes the field-name table itself, which is what
lets record and variant fields come back with their source spellings instead of numeric label hashes:
import { schemaFromContract } from "@candid-core/schema/contract";
const didText = "type Payload = record { owner : principal; amount : nat };";
const built = schemaFromContract(await didToContract(didText));Omissions#
A declaration the generator cannot represent costs only itself and what depends on it. It is left out
together with every declaration and actor method that references it — through nested
func and service types too, up to the containing declaration — and the
module is still a success: gen writes it and exits 0, printing one line per omission on
stderr, worded exactly as the module header lists it:
warning: omitted type Bad (reserved_field_name)
warning: omitted type Registry (references_omitted via Bad)
warning: omitted method bad (references_omitted via Bad)
didToModule returns the same list as omitted: an array of
{ kind, name, reason, via? }, declarations first, then methods, each sorted by name.
kind is "declaration" or "method" (a method of the actor's
service, dropped from both actor and Actor). reason is one of a
closed set — reserved_field_name, ambiguous_variant_arm,
reserved_export_name, invalid_declaration_name (Contract documents only) and
references_omitted — explained on the code generator page;
via, present only for references_omitted, names the omitted declaration
referenced. Everything the module still emits is exactly what it would be had the omitted
declarations never been written, and schemaFromContract omits the same entries from the
envelope.
Doc attributes#
didToDocAttributes and the types it returns are new in 0.4.0, published
on 2026-10-27; the package changelog records them under
0.4.0. 0.3.0 has no function that reads doc comments or exposes the
SourceInfo sidecar.
A doc comment line that starts with @, a lowercase name, then ( or nothing
is an attribute, and the rest of the comment stays prose. The syntax is specified in
docs/doc-attributes.md
(doc attributes v1):
service : {
/// Send tokens to another principal.
/// @intent(title = "Send", template = "Send {amount} to {to}")
/// @amount(at = "amount", decimals = query(decimals))
send : (record { to : principal; amount : nat }) -> ();
}import { didToDocAttributes } from "@candid-core/cli";
const didText = `service : {
/// Send tokens to another principal.
/// @amount(at = "amount", decimals = query(decimals))
send : (record { to : principal; amount : nat }) -> ();
}`;
const result = await didToDocAttributes(didText);
if (result.ok) {
for (const item of result.doc_attributes.items) {
// item.path is "actor.methods[0]"; item.prose is ["Send tokens to another principal."]
for (const attribute of item.attributes) console.log(item.path, attribute.name, attribute.args);
}
}
Each documented item — the actor, a declaration, a field or variant case, a method — is keyed by
its path (actor, type:<Name>, or the occurrence path SourceInfo records) and carries its prose lines and its attributes in order. An attribute is
{ name, doc_line, args }, and each argument value is tagged: text,
number (the digits as written, never a float), bool, ident,
call (query(decimals)), list or record. Once a line
is an attribute it must parse to its end, so a malformed one fails the call with
doc_attribute_malformed, and a repeated key with doc_attribute_duplicate_key;
each names its item's path. The compiler gives no name a meaning: the tool that reads the
attributes decides which names it knows. Comments never move contract_id or
interface_id.
Failures#
Data errors never throw and never half-succeed. Every failure is
{ ok: false, diagnostics: [...] }. Compiler diagnostics pass through verbatim with their
stable codes — did_parse_error, resolver refusals such as
did_source_scheme_mismatch, resource bounds. Two codes originate in this layer:
invalid_request (phase load) when the request document is neither of the two
accepted shapes, and ts_generation_refused (phase generate), kept for a
generator refusal of an invalid Contract graph — which a Contract compiled from Candid source never
is, so no .did reaches it: what cannot be represented is omitted instead. A matrix of nine malformed requests — including
"not json", "{}", and a request carrying both source and
entry — is pinned to return invalid_request rather than panicking. The codes
projectDid and checkCompatible add are listed with them below.
Projection and compatibility#
project, check, projectDid and checkCompatible
are new in 0.3.0, published on 2026-10-10; the package changelog records them under
0.3.0. The previous release, 0.2.0, has none of them: it exits 64 with
its usage for either command.
Two commands for an interface you do not own, such as a canister's published .did.
Both read files already on disk; neither fetches anything, so getting the live interface is the
caller's job.
candid-core-cli project <in.did> --methods <a,b,...> -o <out.did> [--json]
candid-core-cli check <written.did> --against <live.did> [--json]
project writes a self-contained .did holding only the named methods and
every declaration they reach, with their doc comments and argument names, and prints the interface
identities of the input and of the projection. The same input and the same set of names give the
same bytes. gen on the result emits an Actor listing only those methods,
each with its mode. Each distinct unknown name fails with one unknown_method, and the
failure's first diagnostic lists the service's methods in its notes, once; an empty
list fails with empty_method_list. The unknown-method diagnostics are bounded at 4 MiB
in all, each counted as the JSON writes it, the list of methods aside: past that bound the names
left are counted in one resource_limit_exceeded diagnostic
(projection_diagnostic_bytes) instead of reported. A projection must compile again, so it is at most 1 MiB, the compiler's bound on one
source; a larger one fails with resource_limit_exceeded
(projection_bytes) as soon as writing it reaches that bound. Every name, requested or
reached, is found by binary search among names sorted once. -o may not be, or become, a source of the input (the
file and every .did beneath its directory): naming one by any path or link, or a new
.did beneath that directory, fails with output_is_input and writes
nothing, so write the projection outside the input's directory. An output that cannot be written fails with output_write_failed.
check exits 0 when the live service is still a Candid subtype of the written one and 1
otherwise: every written method must exist in the live service with the same mode, arguments
contravariant and results covariant. Methods only the live service has are ignored. Each finding
names its method, a stable code (method_missing, mode_changed,
method_incompatible, the warning special_opt_rule, or
resource_limit_exceeded) and the path into the type where the check failed, such as
$results[0].Err.TooOld. A special_opt_rule warning is reported at every
path whose value decodes as null along which no pair of types repeats. A hand-written subset .did is as valid a written
side as a projection.
The check is bounded per method (384 pairs on one path, 1,000,000 steps, 1,000 reported warnings),
a step being one unit of work done: a pair of types visited, a field, arm, method or value examined
or passed over, or one path segment a warning or a failure copies, or a warning re-examines. A step
reads no name: service methods are paired by ranks computed once per check, and a field's name is
read by its position. So a step's cost does not grow with the length of the names: it does a bounded
amount of work, and its lookups in the maps of type pairs the walk keeps take time logarithmic in
their size. It is
bounded too, so that a service with many methods cannot multiply those bounds, as a whole: 10,000,000
steps, one more per method decided (check_total_steps), 10,000 special_opt_rule warnings
(check_total_warnings) and 4 MiB of reported diagnostics, each counted as the JSON
response writes it, keys and indentation included (check_output_bytes), measured only
up to the first string that passes that bound. Methods are checked in name order. When the check
reaches its step or warning bound, the method at which it did and every method after it fail closed:
each gets one resource_limit_exceeded error naming the bound, with its limit and
observed value, and nothing else. Those count against the 4 MiB like any other diagnostic: once a
method's diagnostics would pass it, that method and every method after it are counted in one last
resource_limit_exceeded error (check_output_bytes), which names no method
and holds the count in unreported_methods. Either way the check exits 1. Below these
bounds a method's findings do not depend on the other methods; once one is reached, they do. Without
them, a 10-level shared two-field type reused by 100 methods turned a 5.3 KB request into a 27.8 MB
response; with them it is 2.8 MB, and however many methods the written side has, a response is at
most 4 MiB and 1,016 bytes. So the walk's time grows with its steps (times that logarithm) and the
reported diagnostics' with their bytes; compiling both sides, within the compiler's own limits, and
ranking the method names are done once per check, before the walk.
The check is one Rust implementation, compiled into the same WebAssembly. Its tests compare it with
the subtype check of the pinned candid crate on hand-written cases and a seeded random
campaign, and with the subtype relation @candid-core/schema's decoder runs on a
reference. The one recorded disagreement is a candid crate defect: its coinductive memo
can keep a pair proven under an assumption it later retracted, and accept a pair that is not a
subtype. This check and the decoder refuse it.
In a browser#
The artifact is built for the web target, not nodejs, and one artifact serves
both hosts — under Node the wrapper supplies the bytes itself. The wasm side evaluates no code, opens
no network connection and touches no filesystem: it sees only the strings handed to it. The wrapper's
one node: import is a dynamic node:fs/promises inside init,
reached only when it detects Node and reads the packaged .wasm; a page never takes that
branch, which is what the headless-Chrome test below exercises. Candid imports are resolved from the
map you hand over, so a page that already fetched its .did sources can compile them in
place.
A headless-Chrome test in the repository serves the wasm and the schema runtime over HTTP and runs
this, asserting that the envelope-carried names really do become the schema's keys — a value keyed
owner/amount validates:
import init, { didToContract } from "/wasm/candid_core_wasm.js";
import { schemaFromContract } from "/schema/contract.js";
import { validate } from "/schema/validate.js";
await init();
const envelope = JSON.parse(didToContract(JSON.stringify({ source: didText })));
const built = schemaFromContract(envelope);
// built.schemas.Payload now validates { owner: <a principal>, amount: 5n }
Note the shapes there: importing the wasm module directly gives you the raw wasm-bindgen exports,
which are string in, string out. The whole wasm ABI is five functions wide in 0.4.0,
four in 0.3.0 and two in 0.2.0 (didToDocAttributes is new in
0.4.0, projectDid and checkCompatible in 0.3.0) —
didToContract(request: string): string,
didToModule(request: string): string,
didToDocAttributes(request: string): string,
projectDid(request: string): string and
checkCompatible(request: string): string — and every richer shape lives in the JSON
documents, which is what keeps the boundary reviewable. Importing from the package instead gives you
the wrapper that does the JSON encoding for you.
Each request is one JSON document. didToContract and didToModule take a
sources request: { "source": "<did text>" }, or
{ "entry": "<name>", "files": { "<name>": "<did text>" } } for a
bundle. projectDid takes a sources request with "methods": ["a", "b"]
added. checkCompatible takes { "written": <sources request>, "live":
<sources request> }. Each response is one JSON document too, the same documents the
wrapper returns parsed: the envelope from didToContract;
{ ok: true, module, omitted } from didToModule;
{ ok: true, did, methods, input, projection } from projectDid, where
input and projection each hold a contract_id and an
interface_id; and { ok: true, compatible, written, live, diagnostics }
from checkCompatible. Every failure is { ok: false, diagnostics }. A
checkCompatible failure caused by one side adds input, naming that side,
"written" or "live", when it does not compile or declares no service; a
malformed request carries no input.
Building it from a clone#
The wasm/ directory is a build output and is not checked in, so the build comes first.
The package's own build script is the exact command:
"scripts": {
"build": "cd .. && wasm-pack build --target web --out-dir npm/wasm --no-pack --release",
"test": "node --test test/*.test.js",
"test:browser": "node --test test/browser/*.test.js"
}-
Install the pinned wasm-pack and the wasm target#
bashcargo install wasm-pack --version 0.14.0 --locked rustup target add wasm32-unknown-unknown -
Build the artifact#
bashcd crates/candid-core-wasm/npm npm run build -
Run the CLI and its suite#
bashnpm ci node bin/cli.js gen ./service.did -o ./out npm testnode bin/cli.js gen …is exactly how the tests invoke it, so it is the verified local entry point.npm testruns the byte-for-byte parity suite.
CI runs the same build with -- --locked appended, under the exact-pinned release
toolchain rather than the rolling stable of ordinary jobs. wasm-opt is
deliberately disabled so the artifact's bytes are a pure function of the pinned rustc and
wasm-bindgen, with no binaryen download anywhere in the build path.
What the parity gate actually asserts#
The wasm CLI workflow runs three jobs, and it is worth being precise about what each one
establishes, because "there is a gate" and "there is a released package" are different claims.
| Job | What it asserts |
|---|---|
host |
Format, clippy with warnings denied, and the Rust parity tests: every golden fixture reproduces
its reviewed .ts module byte for byte; the emitted envelope equals the committed
fixture the native candid-core compile --envelope binary produced, byte for byte;
the field-name triples equal the generator's reviewed *.names.json goldens for five
fixtures; identical requests produce identical responses.
|
artifact |
The same comparisons again through the real WebAssembly under Node — the npm suite runs
bin/cli.js, which loads the compiled wasm — plus artifact determinism: hash the
build, cargo clean, delete the output, rebuild, and require the hashes to match.
|
browser |
didToContract inside a pinned headless Chrome build, feeding
schemaFromContract, with envelope-carried field names rendering as schema keys.
|
What that buys you is specific: for those fixtures, at that commit, the WebAssembly path and the
native Rust path emit the same bytes, and the artifact is reproducible from its pinned inputs. What it
does not buy you is a published version: the gates run on a commit and publish nothing, and a version
reaches the registry only when its release workflow is dispatched, as 0.1.0 was. Nor does
it buy a promise about versions beyond the tested set. The version pairing is a revision
pairing: the package embeds the compiler and generator from the exact commit a release is dispatched from, and the
changelog names the revisions per release. Read the changelog for the pairing rather than assuming a
crate version.
Until 1.0 any release may change the CLI grammar, the library API, and the request and response shapes — the same warning the Rust crate carries. Pin an exact version.
Next#
Schemas from a Contract document shows the TypeScript side consuming the envelope this package writes. The candid-core binary produces the same envelope document from Rust. For where each unit stands, see Status, versions and releases.