From c247e84b25681a40e878ac3290b9a0225e62e6d2 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:03:32 +0000 Subject: [PATCH 01/21] docs: document consuming the registry as an npm dependency --- ARCHITECTURE.md | 2 ++ CLAUDE.md | 26 ++++++++++++++++++- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ RUNBOOK.md | 6 +++++ SPEC.md | 29 ++++++++++++++++------ package.json | 2 +- 6 files changed, 121 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ae798fe..d4c546e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -369,6 +369,8 @@ overrides from `SEED_*`/`LEDGER_*` ([`RUNBOOK.md`](RUNBOOK.md)). | `npm test` | Build the production DAR, then run the `canton-token-forge-test` suite. | | `npm run test:coverage` | Same as `npm test` with a template-focused coverage report. | | `npm run smoke` | Compile a package that data-depends on nothing but the built DAR (`scripts/consumer-smoke.sh`); proves the release artifact is consumable on its own. | +| `npm run check:deps` | Fail if the root and `registry/` manifests disagree on any dependency. | +| `npm run smoke:registry` | Pack the npm package, install it into a scratch consumer, and run the bin; proves the published service is consumable on its own. | | `bash scripts/release-notes.sh ` | Emit the release body, with the consumer snippet extracted from `consumer-smoke/consumer/daml.yaml`. Refuses if `` does not name the checked-out commit, if the working tree is dirty, or if `deps/` carries no commit stamp (`npm run setup` writes it); `ALLOW_UNTAGGED=1` previews a body before the tag exists ([`RUNBOOK.md`](RUNBOOK.md#cutting-a-release)). | | `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. | | `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. | diff --git a/CLAUDE.md b/CLAUDE.md index 0b2f629..2623a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,6 +110,8 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice. | `npm test` | Build the `canton-token-forge` DAR, then run the `canton-token-forge-test` suite. | | `npm run test:coverage` | Same, with a coverage report focused on your templates. | | `npm run smoke` | Build the DAR, then compile a package that data-depends on nothing but it, proving the artifact is consumable on its own. | +| `npm run check:deps` | Fail if the root and `registry/` manifests disagree on any dependency. | +| `npm run smoke:registry` | Pack the npm package, install it into a scratch consumer, and run the bin; proves the published service is consumable on its own. | | `npm run clean` | Remove both `.daml` build dirs, the consumer smoke test's output, and `registry/dist`. | | `npm run setup` | Re-vendor deps + re-create the stable symlinks. | | `npm run sandbox` | Build the DAR and run a local Canton sandbox with the JSON Ledger API. | @@ -119,7 +121,9 @@ installing this repository needs no `dpm`, no JDK and no clone of Splice. `registry/` is a separate npm package with its own dependency tree; the root `npm install` does not populate `registry/node_modules`. Run its commands from -that directory. +that directory. The root manifest is also what builds and ships this service +as an installable package, and `npm run check:deps` is what keeps the two +dependency lists in step. | Command | Does | | --- | --- | @@ -281,6 +285,8 @@ Run before declaring work done: - `npm run smoke` - when you changed what the production DAR exposes: a renamed module or template, its dependencies, its interface instances, or its `build-options` +- `npm run smoke:registry` - when you changed the root manifest, the + registry's dependencies, or either ignore file Every pull request gets three comparisons whatever it touches, because the `daml` check runs them ahead of its toolchain install and outside its own @@ -299,6 +305,24 @@ first re-runs Splice's own suites, the second needs a live participant. The end-to-end suite is typechecked on that path even so, which is the point of typechecking it separately from the run. +A third check, `package`, gates on `package.json`, `package-lock.json`, +`registry/`, `scripts/check-registry-deps.mjs`, +`scripts/registry-install-smoke.sh`, `.gitignore`, `.npmrc` and +`.github/workflows/ci.yml` itself, so a pull request touching `registry/` runs +both the `registry` check and this one. It has four verification steps, not +three, and only three of them sit behind that gate: `npm run check:deps`; +`npm ci`, which runs `prepare` and so compiles `registry/src` against the root +dependency set rather than `registry/`'s own, catching a type package that +arrives there transitively and is declared nowhere; and `npm run +smoke:registry`. The fourth is ungated and runs on every trigger of the +workflow regardless of what changed, ahead of the other three exactly as the +`daml` check's own comparisons run ahead of its gate: a `git check-ignore` +table confirming that `registry/dist` build output stays out of the package +and that the rule ignoring it lives in the root `.gitignore` rather than a +nested one, since a nested rule would still pass the smoke test and the +manifest guard while silently no longer covering `registry/src` and +`registry/test`. + A pushed tag whose `v` is followed by a digit (`v[0-9]*`, so `vnext` and `vendor` trigger nothing) runs the `release` workflow instead: it refuses a tag `main` does not reach, builds from a clean checkout, runs the suite, diff --git a/README.md b/README.md index 99e31e9..90e8098 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,72 @@ in both directions, so a flag, target or path that changes on one side reds rather than ships. The release body carries the same blocks, generated straight from that file. +## Consuming the registry service + +The registry service is published from this repository as an npm package, +consumed from git at a tag rather than from the public registry: + +```json +{ + "dependencies": { + "@bootnodedev/canton-token-forge": "github:BootNodeDev/canton-token-forge#v0.2.0" + } +} +``` + +Two preconditions. This repository is private, so the install needs +credentials that can read it: a token-based HTTPS credential helper or an SSH +key for `github.com`, whichever your environment already uses for private git +dependencies. And the tag must name a commit whose DAR is uploaded to the +participant the service points at, since the template ids are checked at boot +and an id the participant cannot resolve stops it starting. + +Every `v[0-9]*` tag is both a DAR release and an npm package pin: see +"Consuming a release" above for the DAR itself. + +`npm install` builds `registry/src` through the package's `prepare` script and +links one bin, `canton-token-forge-registry`, unmodified. `pnpm install` +refuses by default: pnpm will not run a git-hosted package's build scripts +unless the consumer allowlists it, and `registry/dist` is gitignored, so +`prepare` is the only thing that produces the bin. Add the resolved git +specifier to `pnpm-workspace.yaml`, using whichever key your pnpm major reads: + +```yaml +# pnpm 11 and later +allowBuilds: + "@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#": true + +# pnpm 10 +onlyBuiltDependencies: + - "@bootnodedev/canton-token-forge@git+https://github.com/BootNodeDev/canton-token-forge.git#" +``` + +The key is the full resolved git specifier, not the bare package name, a +version range, or a wildcard: pnpm resolves the tag to its commit sha and +matches on that exact string, and its own refusal error prints the line to +paste. Since the sha is resolved from the tag, this entry changes whenever the +pin does. With it present, `pnpm exec canton-token-forge-registry` runs the +same as `npm`'s link. + +The service reads its whole configuration from the environment, plus a `.env` +loaded from the working directory it is started in. Required, eight: +`LEDGER_API_URL`, `LEDGER_API_TOKEN`, `ADMIN_PARTY`, and the five template ids +(`INSTRUMENT_CONFIG_TEMPLATE_ID`, `PREAPPROVAL_TEMPLATE_ID`, +`LOCKED_TOKEN_TEMPLATE_ID`, `TRANSFER_INSTRUCTION_TEMPLATE_ID`, +`ALLOCATION_TEMPLATE_ID`). Optional, four: `PORT`, `LEDGER_USER_ID`, +`SHUTDOWN_TIMEOUT_MS`, `DIRECT_TRANSFER_MARGIN_MS`. The package ships +`registry/.env.example` with the full list and what each variable is for, and +`npm run seed` against a local sandbox prints the block filled in with the real +admin party and the five template ids. `ADMIN_PARTY` and all five template ids +are checked against the participant at boot, so a value it cannot resolve +stops the service starting rather than failing later. + +What the package contains: `registry/dist`, `registry/openapi` and +`registry/.env.example`, plus `package.json`, `README.md` and `LICENSE` (24 +entries). The manifest that travels with it is this repository's own, so its +`dpm` and `daml/` scripts cannot run from an installed copy; `prepare` is the +only entry npm acts on. + ## Requirements - `dpm` (Digital Asset Package Manager) and a JDK 17+ on `PATH` diff --git a/RUNBOOK.md b/RUNBOOK.md index 57a8264..1e0e48e 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -311,6 +311,12 @@ reason the seed script looks the way it does. ## Cutting a release +Every `v[0-9]*` tag is a DAR release, including the tag a consumer's +`package.json` pins the npm package at (see the README's "Consuming the +registry service"). `daml/canton-token-forge/daml.yaml` stays at `0.0.1` +regardless of which tag is cut, so `v0.2.0`'s DAR asset is byte-identical to +`v0.1.0`'s and carries the same sha256. + `.github/workflows/release.yml` builds and publishes. It runs the full suite, checks the DAR is byte-reproducible, and compiles `consumer-smoke/` against the built artifact before anything is published. The release body, including the diff --git a/SPEC.md b/SPEC.md index ea885a1..22f52d7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -63,8 +63,11 @@ resulting exercise itself over the JSON Ledger API, forwarding the service's ### Size and status -976 lines of production Daml, 2508 lines of Daml tests, 1724 lines of TypeScript -service, 4343 lines of TypeScript tests. MIT licensed. Pre-release: the package +976 lines of production Daml, 2508 lines of Daml tests, 1739 lines of +TypeScript service, 4349 lines of TypeScript tests, each figure a +`find -name '*.daml'` (or `'*.ts'`) `| xargs wc -l` count over +`daml/canton-token-forge`, `daml/canton-token-forge-test`, `registry/src` and +`registry/test` respectively. MIT licensed. Pre-release: the Daml package version is `0.0.1`, and the build is published as release `v0.1.0` for downstream repositories to pin (the tag is deliberately decoupled from the package version), with no compatibility guarantee offered across releases. @@ -578,10 +581,15 @@ Stated plainly, because they are what an evaluation turns on. ship a new interface version that compiles green while the smoke package still names the old one, and compiling that package is what catches it. One that touches `registry/` runs that package's lint, its typechecks and - the registry unit suite as the `registry` check. The end-to-end suite - needs a live participant, so only its types are checked there and it is - never run. Off that path too is `npm run test:coverage`, which re-runs - Splice's own suites and which nothing runs automatically. A pull request + the registry unit suite as the `registry` check. The same change also runs + a third job, `package`, gated on `registry/` among other inputs: it compiles + `registry/src` a second time, against the root dependency set rather than + `registry/`'s own, by running `npm ci` (which triggers `prepare`), then packs + the npm package and installs it into a scratch consumer to run the bin, + neither of which the `registry` check's own install and build can see. The + end-to-end suite needs a live participant, so only its types are checked + there and it is never run. Off that path too is `npm run test:coverage`, + which re-runs Splice's own suites and which nothing runs automatically. A pull request in the `daml` check's scope compiles the smoke package, generates the release body, which is the check that compares the published snippet against the artifact, and asserts that every tracked manifest still pins @@ -590,9 +598,12 @@ Stated plainly, because they are what an evaluation turns on. not reach, and `release-notes.sh`'s tag guard, which the pull-request path waives with `ALLOW_UNTAGGED` so that a body can be generated for a ref that is not a tag. -- **Pre-release.** Version `0.0.1`, with the build published as release `v0.1.0` +- **Pre-release.** Version `0.2.0`, with the build published as release `v0.1.0` for downstream repositories to pin (the tag is deliberately decoupled from the - package version). No migration story and no compatibility guarantees. + package version), and the npm package itself consumed from git at `v0.2.0`. + The two tags exist and mean different things: `v0.1.0` names the DAR release, + `v0.2.0` the commit a consumer's `package.json` pins. No migration story and + no compatibility guarantees. --- @@ -624,5 +635,7 @@ scripts/ release-notes.sh Emit the release body from the smoke package sandbox.sh Local Canton sandbox with the JSON Ledger API seed.mjs Seed an admin, demo users and one instrument + check-registry-deps.mjs Fail when the root and registry manifests disagree + registry-install-smoke.sh Pack, install and run the npm package versions.env The single version knob: SPLICE_TAG ``` diff --git a/package.json b/package.json index db02882..952b85d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bootnodedev/canton-token-forge", "version": "0.2.0", - "description": "A reusable, multi-instrument CIP-0056 (CN Token Standard) compliant token for demos and sandboxes", + "description": "A reusable, multi-instrument CIP-0056 (CN Token Standard) compliant token for demos and sandboxes, plus its read-only registry HTTP service", "license": "MIT", "type": "module", "scripts": { From 6d29befe068630997510c774adbcf6b4580f13d7 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:34 +0000 Subject: [PATCH 02/21] docs: say the specifier pnpm prints has to be quoted before it parses --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90e8098..c179b30 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,10 @@ onlyBuiltDependencies: The key is the full resolved git specifier, not the bare package name, a version range, or a wildcard: pnpm resolves the tag to its commit sha and -matches on that exact string, and its own refusal error prints the line to -paste. Since the sha is resolved from the tag, this entry changes whenever the -pin does. With it present, `pnpm exec canton-token-forge-registry` runs the +matches on that exact string. Its own refusal error prints that specifier, but +unquoted: a YAML key cannot start with `@`, and the `#` before the sha would +open a comment, so quote it as the block above does. Since the sha is resolved +from the tag, this entry changes whenever the pin does. With it present, `pnpm exec canton-token-forge-registry` runs the same as `npm`'s link. The service reads its whole configuration from the environment, plus a `.env` From 8786fc38c4122ec2ef1ac18714c7dc81cf76a28a Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:39 +0000 Subject: [PATCH 03/21] docs: tie the boot check to the package name it resolves, not to the tag --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c179b30..1421688 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,10 @@ consumed from git at a tag rather than from the public registry: Two preconditions. This repository is private, so the install needs credentials that can read it: a token-based HTTPS credential helper or an SSH key for `github.com`, whichever your environment already uses for private git -dependencies. And the tag must name a commit whose DAR is uploaded to the -participant the service points at, since the template ids are checked at boot -and an id the participant cannot resolve stops it starting. +dependencies. And the participant the service points at must host the +`canton-token-forge` package the configured template ids name: they are checked +at boot and resolve by package name, not by package id, so any release's DAR +serves, and one the participant cannot resolve stops the service starting. Every `v[0-9]*` tag is both a DAR release and an npm package pin: see "Consuming a release" above for the DAR itself. From 49808696f6d62a0e9221d9320a7ddd240f4682ed Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:50 +0000 Subject: [PATCH 04/21] docs: name which package each pre-release version belongs to --- SPEC.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index 22f52d7..b353f7e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -598,9 +598,10 @@ Stated plainly, because they are what an evaluation turns on. not reach, and `release-notes.sh`'s tag guard, which the pull-request path waives with `ALLOW_UNTAGGED` so that a body can be generated for a ref that is not a tag. -- **Pre-release.** Version `0.2.0`, with the build published as release `v0.1.0` - for downstream repositories to pin (the tag is deliberately decoupled from the - package version), and the npm package itself consumed from git at `v0.2.0`. +- **Pre-release.** Version `0.2.0` of the npm package, `0.0.1` of the Daml + package, with the Daml build published as release `v0.1.0` for downstream + repositories to pin (that tag is deliberately decoupled from the Daml package + version), and the npm package consumed from git at `v0.2.0`. The two tags exist and mean different things: `v0.1.0` names the DAR release, `v0.2.0` the commit a consumer's `package.json` pins. No migration story and no compatibility guarantees. From e39048c22047b0b5cc1eb31b37507e38c39b57c1 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:18:54 +0000 Subject: [PATCH 05/21] docs: stop asserting a tag that has not been cut yet --- SPEC.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index b353f7e..11ea37f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -602,8 +602,8 @@ Stated plainly, because they are what an evaluation turns on. package, with the Daml build published as release `v0.1.0` for downstream repositories to pin (that tag is deliberately decoupled from the Daml package version), and the npm package consumed from git at `v0.2.0`. - The two tags exist and mean different things: `v0.1.0` names the DAR release, - `v0.2.0` the commit a consumer's `package.json` pins. No migration story and + The two tags mean different things: `v0.1.0` names the DAR release, `v0.2.0` + the commit a consumer's `package.json` pins. No migration story and no compatibility guarantees. --- From b9fd720f0416ca9017c42ba909e5bd6d7370e243 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:07 +0000 Subject: [PATCH 06/21] docs: scope the ungated comparison count to the check that runs them --- CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2623a36..dbc3c52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,9 +288,9 @@ Run before declaring work done: - `npm run smoke:registry` - when you changed the root manifest, the registry's dependencies, or either ignore file -Every pull request gets three comparisons whatever it touches, because the -`daml` check runs them ahead of its toolchain install and outside its own -scope gate: the strings that spell a template id, against `daml/`; the +Every pull request gets three comparisons in the `daml` check whatever it +touches, because that check runs them ahead of its toolchain install and +outside its own scope gate: the strings that spell a template id, against `daml/`; the consumer snippet in `README.md`, against `consumer-smoke/consumer/daml.yaml`; and every tracked `daml.yaml`, against both the SDK version the workflow installs and LF 2.1. One that touches From 8e4c2494881796b68a5f5e8e20337ab326357cea Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:12 +0000 Subject: [PATCH 07/21] docs: stop denying the root commands a registry change now runs --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dbc3c52..0c59743 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -298,7 +298,8 @@ installs and LF 2.1. One that touches inputs runs four of these five automatically in that same check, after those three steps: only `npm run test:coverage` does not. One that touches `registry/` runs that package's own lint, both typechecks, and unit suite as the -`registry` check, not the root commands above. `npm run test:coverage` and +`registry` check, none of the Daml root commands above, and the `package` +check below alongside it. `npm run test:coverage` and the registry's `npm run test:e2e` are both off the pull-request path: the first re-runs Splice's own suites, the second needs a live participant. The `release` workflow adds checks of its own that no pull request runs. The From 4965a15ad5b3b0f1066265cc61679f00b9aae3de Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:19:17 +0000 Subject: [PATCH 08/21] docs: list both new scripts in the structure tree, not only the table --- ARCHITECTURE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d4c546e..7cef2bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -80,6 +80,8 @@ scripts/ release-notes.sh Emit the release body for a tag, snippet extracted from consumer-smoke/consumer/daml.yaml sandbox.sh Build the DAR and run a local Canton sandbox with the JSON Ledger API seed.mjs Seed a running sandbox with an admin, demo users, and one InstrumentConfig + check-registry-deps.mjs Fail when the root and registry manifests disagree on a dependency + registry-install-smoke.sh Pack the npm package, install it into a scratch consumer, and run the bin deps/ Vendored Splice sources + built DARs (gitignored; never edit or commit) multi-package.yaml Wires the two daml/ packages into one workspace; consumer-smoke/ has its own versions.env Single version knob: SPLICE_TAG From 274c4108705e7866c674dd01013a83c464b16b6e Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:20:07 +0000 Subject: [PATCH 09/21] docs: say the DAR version is irrelevant rather than that any DAR serves --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1421688..a8085a5 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,9 @@ credentials that can read it: a token-based HTTPS credential helper or an SSH key for `github.com`, whichever your environment already uses for private git dependencies. And the participant the service points at must host the `canton-token-forge` package the configured template ids name: they are checked -at boot and resolve by package name, not by package id, so any release's DAR -serves, and one the participant cannot resolve stops the service starting. +at boot and resolve by package name, not by package id, so which release built +the DAR does not matter, and an id the participant cannot resolve stops the +service starting. Every `v[0-9]*` tag is both a DAR release and an npm package pin: see "Consuming a release" above for the DAR itself. From 427969ccbcf736b1201297569efe212de7657ef9 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:20:26 +0000 Subject: [PATCH 10/21] docs: rewrap the two lines the corrections left ragged --- CLAUDE.md | 5 +++-- README.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c59743..a70f5c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -290,8 +290,9 @@ Run before declaring work done: Every pull request gets three comparisons in the `daml` check whatever it touches, because that check runs them ahead of its toolchain install and -outside its own scope gate: the strings that spell a template id, against `daml/`; the -consumer snippet in `README.md`, against `consumer-smoke/consumer/daml.yaml`; +outside its own scope gate: the strings that spell a template id, against +`daml/`; the consumer snippet in `README.md`, against +`consumer-smoke/consumer/daml.yaml`; and every tracked `daml.yaml`, against both the SDK version the workflow installs and LF 2.1. One that touches `daml/`, `consumer-smoke/`, `scripts/consumer-smoke.sh` or the root build diff --git a/README.md b/README.md index a8085a5..ce7338e 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ version range, or a wildcard: pnpm resolves the tag to its commit sha and matches on that exact string. Its own refusal error prints that specifier, but unquoted: a YAML key cannot start with `@`, and the `#` before the sha would open a comment, so quote it as the block above does. Since the sha is resolved -from the tag, this entry changes whenever the pin does. With it present, `pnpm exec canton-token-forge-registry` runs the -same as `npm`'s link. +from the tag, this entry changes whenever the pin does. With it present, +`pnpm exec canton-token-forge-registry` runs the same as `npm`'s link. The service reads its whole configuration from the environment, plus a `.env` loaded from the working directory it is started in. Required, eight: From 6812b84b5968c75bf7b106cf1c04d28cfb25a66a Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:20 +0000 Subject: [PATCH 11/21] docs: say the ignore table reads git, and name the mutation it catches --- CLAUDE.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a70f5c4..9bac7f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -319,11 +319,18 @@ arrives there transitively and is declared nowhere; and `npm run smoke:registry`. The fourth is ungated and runs on every trigger of the workflow regardless of what changed, ahead of the other three exactly as the `daml` check's own comparisons run ahead of its gate: a `git check-ignore` -table confirming that `registry/dist` build output stays out of the package -and that the rule ignoring it lives in the root `.gitignore` rather than a -nested one, since a nested rule would still pass the smoke test and the -manifest guard while silently no longer covering `registry/src` and -`registry/test`. +table confirming that build output under `registry/` stays out of git, +`registry/dist` included, which must not be committed even though it is the +package's own payload, and that the rule ignoring it lives in the root +`.gitignore` rather than a nested one. Each half catches a mutation the other +cannot see. Narrowing the root rule to `registry/dist` passes the smoke test, +the manifest guard and the placement read alike, while build output under +`registry/src` and `registry/test` silently stops being ignored, so the table +reads what the rule covers. Moving the same pattern into +`registry/.gitignore` leaves that coverage intact but subtracts +`registry/dist` from the npm pack walk, emptying the package to 8 entries from +24; only the smoke test sees that, and only when the gate lets it run, so the +table reads where the rule lives as well. A pushed tag whose `v` is followed by a digit (`v[0-9]*`, so `vnext` and `vendor` trigger nothing) runs the `release` workflow instead: it refuses a From e814f42cadfc6caf1a111a0891d178050bd88a26 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 12/21] docs: send a manifest dependency change to the guard that compares them --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9bac7f7..7e7e06e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,6 +285,8 @@ Run before declaring work done: - `npm run smoke` - when you changed what the production DAR exposes: a renamed module or template, its dependencies, its interface instances, or its `build-options` +- `npm run check:deps` - when you changed a dependency in the root or the + `registry/` manifest; `npm run smoke:registry` never compares the two - `npm run smoke:registry` - when you changed the root manifest, the registry's dependencies, or either ignore file From 07b83ea6ef82a8b823f231408f4068cf79a45b16 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 13/21] docs: reflow the paragraph the previous rewrap left ragged --- CLAUDE.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7e7e06e..ec1282a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,17 +294,16 @@ Every pull request gets three comparisons in the `daml` check whatever it touches, because that check runs them ahead of its toolchain install and outside its own scope gate: the strings that spell a template id, against `daml/`; the consumer snippet in `README.md`, against -`consumer-smoke/consumer/daml.yaml`; -and every tracked `daml.yaml`, against both the SDK version the workflow -installs and LF 2.1. One that touches +`consumer-smoke/consumer/daml.yaml`; and every tracked `daml.yaml`, against +both the SDK version the workflow installs and LF 2.1. One that touches `daml/`, `consumer-smoke/`, `scripts/consumer-smoke.sh` or the root build inputs runs four of these five automatically in that same check, after those -three steps: only `npm run test:coverage` does not. One that touches `registry/` -runs that package's own lint, both typechecks, and unit suite as the -`registry` check, none of the Daml root commands above, and the `package` -check below alongside it. `npm run test:coverage` and -the registry's `npm run test:e2e` are both off the pull-request path: the -first re-runs Splice's own suites, the second needs a live participant. The +three steps: only `npm run test:coverage` does not. One that touches +`registry/` runs that package's own lint, both typechecks, and unit suite as +the `registry` check, none of the Daml root commands above, and the `package` +check below alongside it. `npm run test:coverage` and the registry's `npm run +test:e2e` are both off the pull-request path: the first re-runs Splice's own +suites, the second needs a live participant. The `release` workflow adds checks of its own that no pull request runs. The end-to-end suite is typechecked on that path even so, which is the point of typechecking it separately from the run. From 42c2513bdc15bc23f72c1b6bcd91b4b953168612 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 14/21] docs: stop offering a tag that is not cut and a pin that installs no service --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce7338e..fa5b76c 100644 --- a/README.md +++ b/README.md @@ -170,8 +170,14 @@ at boot and resolve by package name, not by package id, so which release built the DAR does not matter, and an id the participant cannot resolve stops the service starting. -Every `v[0-9]*` tag is both a DAR release and an npm package pin: see -"Consuming a release" above for the DAR itself. +`v0.2.0` is the first tag to carry the npm package, and the tag this section +is written against; until it is cut, pin the commit sha instead. Do not pin +`v0.1.0`: it predates the package, so its manifest is unscoped, declares no +`bin` and no `files`, and runs `scripts/fetch-dep.sh` as a `postinstall`, +which installs no service and clones Splice into your `node_modules`. From +`v0.2.0` on the tags are one namespace, every `v[0-9]*` tag being both a DAR +release and an npm package pin. See "Consuming a release" above for the DAR +itself. `npm install` builds `registry/src` through the package's `prepare` script and links one bin, `canton-token-forge-registry`, unmodified. `pnpm install` From 38f8ce589379c9d4bbbfd6f5941acf9f527aaf14 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 15/21] docs: blame the reserved @ rather than the # for that key not parsing --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa5b76c..6b5e7aa 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,12 @@ onlyBuiltDependencies: The key is the full resolved git specifier, not the bare package name, a version range, or a wildcard: pnpm resolves the tag to its commit sha and -matches on that exact string. Its own refusal error prints that specifier, but -unquoted: a YAML key cannot start with `@`, and the `#` before the sha would -open a comment, so quote it as the block above does. Since the sha is resolved +matches on that exact string. Its own refusal error prints that specifier +unquoted, and pasted that way it does not parse: `@` is a reserved indicator +in YAML, so a plain scalar cannot begin with one, and the parser answers `bad +indentation of a mapping entry`. Quote it as the block above does. The `#` is +harmless either way, since YAML opens a comment only at a `#` that follows +whitespace, and this one sits inside the scalar. Since the sha is resolved from the tag, this entry changes whenever the pin does. With it present, `pnpm exec canton-token-forge-registry` runs the same as `npm`'s link. From bc823cde8c42d61f23763f7daee818133025b3a2 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:25 +0000 Subject: [PATCH 16/21] docs: say the template ids need quoting and where seed can be run --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b5e7aa..9568ed1 100644 --- a/README.md +++ b/README.md @@ -214,11 +214,22 @@ loaded from the working directory it is started in. Required, eight: `LOCKED_TOKEN_TEMPLATE_ID`, `TRANSFER_INSTRUCTION_TEMPLATE_ID`, `ALLOCATION_TEMPLATE_ID`). Optional, four: `PORT`, `LEDGER_USER_ID`, `SHUTDOWN_TIMEOUT_MS`, `DIRECT_TRANSFER_MARGIN_MS`. The package ships -`registry/.env.example` with the full list and what each variable is for, and -`npm run seed` against a local sandbox prints the block filled in with the real -admin party and the five template ids. `ADMIN_PARTY` and all five template ids -are checked against the participant at boot, so a value it cannot resolve -stops the service starting rather than failing later. +`registry/.env.example` with the full list and what each variable is for. + +Quote all five template ids in a `.env` file. Every one of them begins with +`#`, which dotenv reads as the start of a comment, so an unquoted +`INSTRUMENT_CONFIG_TEMPLATE_ID=#canton-token-forge:...` parses to the empty +string and the boot rejects it as `missing required env var +INSTRUMENT_CONFIG_TEMPLATE_ID`, naming a variable that is in fact set. Values +passed through the environment rather than a file need no quoting beyond +whatever the shell wants. + +`npm run seed`, run from a clone of this repository against a local sandbox, +prints the whole block filled in and quoted with the real admin party and the +five template ids. `scripts/seed.mjs` is not part of the package, so that is a +step you take in a checkout, not in the consumer. `ADMIN_PARTY` and all five +template ids are checked against the participant at boot, so a value it cannot +resolve stops the service starting rather than failing later. What the package contains: `registry/dist`, `registry/openapi` and `registry/.env.example`, plus `package.json`, `README.md` and `LICENSE` (24 From dbc0707c3a06724c6ad3e2e55aeac50cd38b18fe Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 17/21] docs: derive DAR byte-identity from the sources rather than the version string --- RUNBOOK.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/RUNBOOK.md b/RUNBOOK.md index 1e0e48e..3420aad 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -314,8 +314,19 @@ reason the seed script looks the way it does. Every `v[0-9]*` tag is a DAR release, including the tag a consumer's `package.json` pins the npm package at (see the README's "Consuming the registry service"). `daml/canton-token-forge/daml.yaml` stays at `0.0.1` -regardless of which tag is cut, so `v0.2.0`'s DAR asset is byte-identical to -`v0.1.0`'s and carries the same sha256. +whatever tag is cut, so every release attaches an asset under the same name, +`canton-token-forge-0.0.1.dar`. Whether two releases' assets are the same +bytes does not follow from that version string: it follows from `daml/`, +`versions.env` and the SDK being unchanged between them. Nothing under those +paths has moved since `v0.1.0`, so a tag cut from this commit would carry the +sha256 the `v0.1.0` body records; read it off the workflow's own output rather +than assuming it, as steps 1 and 4 below do. + +The root `package.json`'s `version` is the npm package's, and nothing checks +it against the tag: `release.yml` reads only `daml/canton-token-forge/daml.yaml`. +Bring it in step with the tag you are about to cut, in a commit merged to +`main` before you tag, or consumers install a package whose manifest names a +version the pin does not. `.github/workflows/release.yml` builds and publishes. It runs the full suite, checks the DAR is byte-reproducible, and compiles `consumer-smoke/` against the From 240c6951cca65d1227fc5bdeee618adf5bf6adc5 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 18/21] docs: add the package.json version bump the release procedure never named --- RUNBOOK.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/RUNBOOK.md b/RUNBOOK.md index 3420aad..4f82145 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -374,7 +374,10 @@ CI just proved compile. ``` Then `npm run clean`, and delete that release and its tag. -2. Tag and push. This is the decision that matters: a downstream repository pins +2. Set the root `package.json`'s `version` to the tag without its `v`, and land + that on `main`. Nothing enforces this, and the tag is what a consumer's + `package.json` resolves to. +3. Tag and push. This is the decision that matters: a downstream repository pins it permanently. ```bash @@ -382,7 +385,7 @@ CI just proved compile. git push origin v0.1.0 ``` -3. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body +4. Confirm the release carries `canton-token-forge-0.0.1.dar` and that its body shows the sha256 and package-id. The download-and-compile from step 1 is worth repeating here, against the real tag. From 76a3c9a8a2c98d0bf029c3a80f2fc64252452b8c Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:31 +0000 Subject: [PATCH 19/21] docs: point the line-count recipe at the source dir, not the package root --- SPEC.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 11ea37f..2d694f7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -66,8 +66,12 @@ resulting exercise itself over the JSON Ledger API, forwarding the service's 976 lines of production Daml, 2508 lines of Daml tests, 1739 lines of TypeScript service, 4349 lines of TypeScript tests, each figure a `find -name '*.daml'` (or `'*.ts'`) `| xargs wc -l` count over -`daml/canton-token-forge`, `daml/canton-token-forge-test`, `registry/src` and -`registry/test` respectively. MIT licensed. Pre-release: the Daml package +`daml/canton-token-forge/daml`, `daml/canton-token-forge-test/daml`, +`registry/src` and `registry/test` respectively. The two Daml paths name the +source directory rather than the package root on purpose: a package root that +has been built also holds a `.daml/` build directory, whose name the `*.daml` +glob matches and whose regenerated data-dependency sources dwarf the figure +above. MIT licensed. Pre-release: the Daml package version is `0.0.1`, and the build is published as release `v0.1.0` for downstream repositories to pin (the tag is deliberately decoupled from the package version), with no compatibility guarantee offered across releases. From 3aad5df2f833d2e281d55f4a5b5eec18149a9fd3 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:57 +0000 Subject: [PATCH 20/21] docs: say which package step is ungated instead of gating the whole job --- SPEC.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/SPEC.md b/SPEC.md index 2d694f7..b2670b4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -586,22 +586,26 @@ Stated plainly, because they are what an evaluation turns on. still names the old one, and compiling that package is what catches it. One that touches `registry/` runs that package's lint, its typechecks and the registry unit suite as the `registry` check. The same change also runs - a third job, `package`, gated on `registry/` among other inputs: it compiles - `registry/src` a second time, against the root dependency set rather than - `registry/`'s own, by running `npm ci` (which triggers `prepare`), then packs - the npm package and installs it into a scratch consumer to run the bin, - neither of which the `registry` check's own install and build can see. The - end-to-end suite needs a live participant, so only its types are checked - there and it is never run. Off that path too is `npm run test:coverage`, - which re-runs Splice's own suites and which nothing runs automatically. A pull request - in the `daml` check's scope compiles the smoke package, generates the - release body, which is the check that compares the published snippet - against the artifact, and asserts that every tracked manifest still pins - the SDK and targets LF 2.1. The release workflow still carries the rebuild - that proves the DAR is byte-reproducible, the refusal of a tag `main` does - not reach, and `release-notes.sh`'s tag guard, which the pull-request path - waives with `ALLOW_UNTAGGED` so that a body can be generated for a ref that - is not a tag. + a third job, `package`. Three of its four steps are gated on `registry/` + among other inputs: they compare the root and `registry/` manifests, compile + `registry/src` a second time against the root dependency set rather than + `registry/`'s own by running `npm ci` (which triggers `prepare`), then pack + the npm package and install it into a scratch consumer to run the bin. The + `registry` check's own install and build see neither of those last two. The + fourth step is ungated and runs on every trigger of the workflow, a + docs-only pull request included: it reads the `.gitignore` rules that keep + build output under `registry/` out of git, and reads which file carries + them. The end-to-end suite needs a live participant, so only its types are + checked there and it is never run. Off that path too is `npm run + test:coverage`, which re-runs Splice's own suites and which nothing runs + automatically. A pull request in the `daml` check's scope compiles the smoke + package, generates the release body, which is the check that compares the + published snippet against the artifact, and asserts that every tracked + manifest still pins the SDK and targets LF 2.1. The release workflow still + carries the rebuild that proves the DAR is byte-reproducible, the refusal + of a tag `main` does not reach, and `release-notes.sh`'s tag guard, which + the pull-request path waives with `ALLOW_UNTAGGED` so that a body can be + generated for a ref that is not a tag. - **Pre-release.** Version `0.2.0` of the npm package, `0.0.1` of the Daml package, with the Daml build published as release `v0.1.0` for downstream repositories to pin (that tag is deliberately decoupled from the Daml package From 24437f238f4484f00b6669b871db2e90c6079526 Mon Sep 17 00:00:00 2001 From: Lisandro Corbalan Date: Thu, 3 Sep 2026 19:58:57 +0000 Subject: [PATCH 21/21] docs: describe the release tags as one namespace rather than a split --- SPEC.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index b2670b4..9a3ed3b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -607,12 +607,12 @@ Stated plainly, because they are what an evaluation turns on. the pull-request path waives with `ALLOW_UNTAGGED` so that a body can be generated for a ref that is not a tag. - **Pre-release.** Version `0.2.0` of the npm package, `0.0.1` of the Daml - package, with the Daml build published as release `v0.1.0` for downstream - repositories to pin (that tag is deliberately decoupled from the Daml package - version), and the npm package consumed from git at `v0.2.0`. - The two tags mean different things: `v0.1.0` names the DAR release, `v0.2.0` - the commit a consumer's `package.json` pins. No migration story and - no compatibility guarantees. + package, both deliberately decoupled from the release tags. Those tags are + one namespace rather than two: every `v[0-9]*` tag publishes the DAR as a + release asset, and from `v0.2.0` on the same tag is what a consumer's + `package.json` pins the npm package at. `v0.1.0` is the only tag cut so far + and predates the npm package, so it serves the DAR alone. No migration story + and no compatibility guarantees. ---