From 51eb2c8c16425e14f3d06c81bcabddaf86674e51 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 31 Aug 2026 14:23:49 -0300 Subject: [PATCH 1/3] refactor(tests): read the Splice pin from the scaffolded template The pinned Splice version was copied by hand into the smoke test, the runtime-plan fixture and the config-validation fixture, so every upgrade had to move four literals in lockstep or a test would fail for the wrong reason. templates/canton-barebones.config.json is the only copy that reaches a user, so it becomes the single source: the two tests that care import it directly, and the runtime-plan fixture drops to a placeholder because nothing there asserts IMAGE_TAG. Using the real template as the config-validation baseline also makes "accepts the scaffolded default" a check on what actually ships. The hand-written copy it replaces had already drifted on the SV UI flags. --- .changeset/cyan-walls-go.md | 2 ++ scripts/config-validation.test.js | 23 ++++------------------- scripts/runtime-plan.test.js | 2 +- scripts/smoke.js | 7 +++++-- 4 files changed, 12 insertions(+), 22 deletions(-) create mode 100644 .changeset/cyan-walls-go.md diff --git a/.changeset/cyan-walls-go.md b/.changeset/cyan-walls-go.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/cyan-walls-go.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/scripts/config-validation.test.js b/scripts/config-validation.test.js index ff2de8e..6185a07 100644 --- a/scripts/config-validation.test.js +++ b/scripts/config-validation.test.js @@ -2,6 +2,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { parseComposeProjectName, parseConfig } from '../src/config.js'; +import scaffoldedConfig from '../templates/canton-barebones.config.json' with { type: 'json' }; // Deep-clones a config object so each case can mutate a copy without affecting // the shared valid baseline. @@ -22,25 +23,9 @@ function assertRejects(raw, expectedFragment) { ); } -// The scaffolded default shipped by `init`: version 1, a pinned Splice source, -// persistent volumes, app-provider off, app-user enabled headless (backend on, -// UIs off), every SV web UI flag present (the SV backend itself is not in the config — it -// is required infrastructure that always runs), and all network tools off. This -// is the baseline; every negative case below clones it and breaks a single rule, -// so a failure points to one validation concern. -const validConfig = { - version: 1, - splice: { repo: 'canton-network/splice', tag: '0.6.11' }, - composeProjectName: 'canton-barebones', - dockerNetwork: 'cantonBarebones', - persistence: { mode: 'persistent' }, - validators: { - appProvider: { enabled: false, ui: false }, - appUser: { enabled: true, ui: false }, - }, - sv: { scanUI: true, svUI: true, walletUI: true }, - networkTools: { console: false, multiSync: false, swaggerUI: false }, -}; +// Baseline for every case below: the real template `init` scaffolds, so the +// happy path asserts on the config users actually get. +const validConfig = scaffoldedConfig; // A richer config: app-user enabled with its UIs on, and every network tool on. // Exercises the enabled+ui combination and the tool flags together. diff --git a/scripts/runtime-plan.test.js b/scripts/runtime-plan.test.js index 1ab15f7..411ed26 100644 --- a/scripts/runtime-plan.test.js +++ b/scripts/runtime-plan.test.js @@ -15,7 +15,7 @@ import { deriveRuntimePlan, writeLocalnetEnv } from '../src/compose.js'; // disable flags one at a time from a fully-on baseline. function baseConfig(generatedDir) { return { - imageTag: '0.6.11', + imageTag: 'test-image-tag', // placeholder: no case here asserts IMAGE_TAG composeProjectName: 'canton-barebones', dockerNetwork: 'cantonBarebones', localnetDir: '/tmp/localnet', diff --git a/scripts/smoke.js b/scripts/smoke.js index ff1dfda..ff688d6 100644 --- a/scripts/smoke.js +++ b/scripts/smoke.js @@ -3,6 +3,7 @@ import fs from 'node:fs'; import { loadConfig } from '../src/config.js'; import { deriveRuntimePlan, runDockerCompose, writeLocalnetEnv } from '../src/compose.js'; +import scaffoldedConfig from '../templates/canton-barebones.config.json' with { type: 'json' }; const config = loadConfig(); const runtimeEnvPath = writeLocalnetEnv(config); @@ -13,8 +14,10 @@ const localnetOverride = fs.readFileSync(config.localnetOverridePath, 'utf8'); // It requires Docker and a Splice checkout, so it runs under "test:e2e", not the // unit test suite. assert.equal(config.imageTag.length > 0, true); -assert.equal(config.splice.repo, 'canton-network/splice'); -assert.equal(config.splice.tag, '0.6.11'); +// "test:e2e" runs `init` first, so the loaded config is the scaffolded template: +// the pin reaching the project intact is what these two assert. +assert.equal(config.splice.repo, scaffoldedConfig.splice.repo); +assert.equal(config.splice.tag, scaffoldedConfig.splice.tag); assert.equal(config.persistence.mode, 'persistent'); assert.deepEqual(config.validators, { appProvider: { enabled: false, ui: false }, From 9ec929c9afac0a0bf2bc08e6e4193ccf2c2ac757 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 31 Aug 2026 15:07:22 -0300 Subject: [PATCH 2/3] feat(validate): check the pinned Splice against what the overrides name The wrapper runs Splice's own compose.yaml with our overrides layered on top, and those overrides reach into Splice's model by literal name: a service to pin to 0 replicas, a volume target to shadow. Compose merges by name and accepts a name that matches nothing, so a Splice release that renamed a service left the override applying to a service it invented, and the stack came up with a web UI the user had turned off. A moved nginx route target is worse: the mount stops replacing Splice's, nginx keeps the routes of a headless validator, and it dies on the missing upstream at start with an error that points nowhere near us. `validate` now compares the checkout against the services, profiles and nginx mount targets the wrapper names, and fails saying which one moved. The names are read from the files that emit them, so a check cannot drift from what we hand to Docker, and nothing else about Splice is asserted. Splice spells a validator's mount target around its *_PROFILE value, so app-user.c${APP_USER_PROFILE}f.template only reads "conf" while that validator is on. The comparison uses that "on" spelling, since it is the one the override shadows. --- .changeset/splice-contract-check.md | 9 ++ bin/canton-barebones.js | 23 ++++- package-lock.json | 8 +- package.json | 1 + scripts/smoke.js | 5 ++ scripts/splice-contract.test.js | 132 ++++++++++++++++++++++++++++ src/splice-contract.js | 80 +++++++++++++++++ 7 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 .changeset/splice-contract-check.md create mode 100644 scripts/splice-contract.test.js create mode 100644 src/splice-contract.js diff --git a/.changeset/splice-contract-check.md b/.changeset/splice-contract-check.md new file mode 100644 index 0000000..edddeda --- /dev/null +++ b/.changeset/splice-contract-check.md @@ -0,0 +1,9 @@ +--- +'@bootnodedev/canton-barebones': minor +--- + +`validate` now checks that the pinned Splice release still defines everything the +wrapper's compose overrides address by name, and fails naming the mismatch. This +turns a Splice upgrade from a guess into a check: raise `splice.tag`, run +`validate`, and a renamed service or a moved nginx route template is reported +instead of silently producing a stack that starts and misbehaves. diff --git a/bin/canton-barebones.js b/bin/canton-barebones.js index 3bfcf18..e28c9bf 100755 --- a/bin/canton-barebones.js +++ b/bin/canton-barebones.js @@ -15,6 +15,7 @@ import { stopStackByProjectName, writeLocalnetEnv, } from '../src/compose.js'; +import { checkSpliceContract } from '../src/splice-contract.js'; import { isJsonMode, printError, printResult, setJsonMode } from '../src/output.js'; // Prints the CLI contract without requiring Docker or a valid LocalNet checkout. @@ -24,7 +25,7 @@ function usage() { Commands: init Scaffold config and compose overrides into the project setup Fetch the pinned Splice LocalNet source - validate Validate config and LocalNet paths + validate Validate config, LocalNet paths and Splice compatibility compose Run docker compose with the configured LocalNet files start Start the stack stop Stop the stack and keep volumes @@ -86,12 +87,28 @@ function main() { return; case 'validate': { const runtimeEnvPath = writeLocalnetEnv(config); + // Fail before reporting the plan: a plan is meaningless if the pinned + // Splice no longer defines what the overrides address (see + // src/splice-contract.js), and the mismatch is far easier to act on here + // than as a Docker error, or a silent no-op, at start time. + const mismatches = checkSpliceContract(config); + if (mismatches.length > 0) { + throw new Error( + `The pinned Splice ${config.splice.repo}@${config.splice.tag} is not compatible with this version of canton-barebones:\n` + + mismatches.map(mismatch => ` - ${mismatch}`).join('\n') + ); + } // The resolved runtime plan is included so a consumer can see exactly what // the config will launch (profiles, headless validators, participant env) // without having to start the stack. printResult( { - splice: { repo: config.splice.repo, tag: config.splice.tag, imageTag: config.imageTag }, + splice: { + repo: config.splice.repo, + tag: config.splice.tag, + imageTag: config.imageTag, + compatible: true, + }, config: { version: config.version, composeProjectName: config.composeProjectName, @@ -111,7 +128,7 @@ function main() { }, () => { console.log(`Config OK: ${config.configPath}`); - console.log(`Splice: ${config.splice.repo}@${config.splice.tag}`); + console.log(`Splice: ${config.splice.repo}@${config.splice.tag} (compatible)`); console.log(`LocalNet: ${config.localnetDir}`); console.log(`Runtime env: ${runtimeEnvPath}`); console.log(`LocalNet override: ${config.localnetOverridePath}`); diff --git a/package-lock.json b/package-lock.json index 9b71c18..6fa6b6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,15 @@ { "name": "@bootnodedev/canton-barebones", - "version": "0.2.2", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bootnodedev/canton-barebones", - "version": "0.2.2", + "version": "0.3.0", + "license": "Apache-2.0", "dependencies": { + "js-yaml": "^4.1.0", "zod": "^4.4.3" }, "bin": { @@ -428,7 +430,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-union": { @@ -761,7 +762,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, "funding": [ { "type": "github", diff --git a/package.json b/package.json index e84a979..d95091c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "node": ">=22" }, "dependencies": { + "js-yaml": "^4.1.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/scripts/smoke.js b/scripts/smoke.js index ff688d6..bcdd794 100644 --- a/scripts/smoke.js +++ b/scripts/smoke.js @@ -3,6 +3,7 @@ import fs from 'node:fs'; import { loadConfig } from '../src/config.js'; import { deriveRuntimePlan, runDockerCompose, writeLocalnetEnv } from '../src/compose.js'; +import { checkSpliceContract } from '../src/splice-contract.js'; import scaffoldedConfig from '../templates/canton-barebones.config.json' with { type: 'json' }; const config = loadConfig(); @@ -45,6 +46,10 @@ assert.match(localnetOverride, /max-size: "25m"/); assert.match(localnetOverride, /max-file: "3"/); assert.match(localnetOverride, /LOG_LEVEL_STDOUT: "\$\{LOG_LEVEL:-INFO\}"/); +// Against the real downloaded checkout, not a fixture: the pinned release must +// still define every service, profile and mount target the overrides name. +assert.deepEqual(checkSpliceContract(config), []); + runDockerCompose(config, ['config', '--quiet'], { stdio: 'pipe' }); console.log('Smoke test OK'); diff --git a/scripts/splice-contract.test.js b/scripts/splice-contract.test.js new file mode 100644 index 0000000..ae6d138 --- /dev/null +++ b/scripts/splice-contract.test.js @@ -0,0 +1,132 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { findContractMismatches } from '../src/splice-contract.js'; + +// A fake Splice compose.yaml, cut down to only the parts the check reads. It +// copies the real shape at the pinned release: +// - `nginx` and the three SV web UI services, because those are the services +// templates/runtime-overrides.yaml addresses by name. +// - nginx's route-template mounts written the way Splice writes them, with the +// validator's *_PROFILE value spliced into the middle of the filename, so +// `app-user.c${APP_USER_PROFILE}f.template` spells "conf" only while that +// validator is on. Our override shadows that "on" spelling. +// - one profile per service, covering the names the wrapper can select. +function spliceModel() { + return { + services: { + nginx: { + volumes: [ + '${LOCALNET_DIR}/conf/nginx/app-provider.conf:/etc/nginx/templates/app-provider.c${APP_PROVIDER_PROFILE}f.template', + '${LOCALNET_DIR}/conf/nginx/app-user.conf:/etc/nginx/templates/app-user.c${APP_USER_PROFILE}f.template', + ], + }, + 'scan-web-ui': { profiles: ['sv'] }, + 'sv-web-ui': { profiles: ['sv'] }, + 'wallet-web-ui-sv': { profiles: ['sv'] }, + 'wallet-web-ui-app-user': { profiles: ['app-user'] }, + }, + }; +} + +// A fake templates/runtime-overrides.yaml: the services it pins to 0 replicas, +// and the two nginx mount targets it shadows. Its targets carry no variable, +// which is the point of the comparison: the override only replaces Splice's +// mount while Splice's own target resolves to this same string. +function overrideModel() { + return { + services: { + nginx: { + volumes: [ + '${APP_PROVIDER_NGINX_ROUTES}:/etc/nginx/templates/app-provider.conf.template', + '${APP_USER_NGINX_ROUTES}:/etc/nginx/templates/app-user.conf.template', + ], + }, + 'scan-web-ui': { deploy: { replicas: '${SCAN_WEB_UI_REPLICAS}' } }, + 'sv-web-ui': { deploy: { replicas: '${SV_WEB_UI_REPLICAS}' } }, + 'wallet-web-ui-sv': { deploy: { replicas: '${WALLET_WEB_UI_SV_REPLICAS}' } }, + }, + }; +} + +// The profiles the wrapper passes to `--profile`, a subset of what the model +// above declares, so the compatible case has nothing to report. +const profiles = ['sv', 'app-user']; + +// Runs the check on the compatible pair above, with `overrides` replacing one +// input so each case states only what it breaks. +function check(overrides = {}) { + return findContractMismatches({ + spliceModel: spliceModel(), + overrideModel: overrideModel(), + profiles, + ...overrides, + }); +} + +// Scenario: what the wrapper names in Splice's compose model must still be there. +// Compose merges by name and silently accepts a name that matches nothing, so +// each case below is a real breakage that would otherwise surface as a stack +// that starts and misbehaves rather than as an error. +describe('findContractMismatches', () => { + // The pinned release defines every service, profile and mount target the + // wrapper addresses, so a compatible Splice must report nothing at all. + it('reports nothing when the pinned Splice still defines everything', () => { + assert.deepEqual(check(), []); + }); + + // Splice renaming `scan-web-ui` is the silent failure this check exists for: + // the override's 0-replica pin would apply to a service Compose invents from + // the override alone, so the real scan UI keeps starting even when the user + // turned it off in their config. + it('reports a service the override targets but Splice renamed', () => { + const model = spliceModel(); + model.services['scan-ui'] = model.services['scan-web-ui']; + delete model.services['scan-web-ui']; + const mismatches = check({ spliceModel: model }); + assert.equal(mismatches.length, 1); + assert.match(mismatches[0], /service "scan-web-ui"/); + }); + + // A profile the wrapper still passes to `--profile` but Splice dropped: the + // selection would bring up nothing instead of that validator's UI bundle. + it('reports a profile the wrapper selects but Splice dropped', () => { + const model = spliceModel(); + delete model.services['wallet-web-ui-app-user'].profiles; + const mismatches = check({ spliceModel: model }); + assert.equal(mismatches.length, 1); + assert.match(mismatches[0], /profile "app-user"/); + }); + + // Splice moving a validator's route template out from under our mount. Compose + // deduplicates volumes by target, so a target that no longer matches adds a + // second mount instead of replacing Splice's: nginx would keep the real routes + // of a headless validator and die on its missing UI containers at startup. + it('reports an nginx mount target Splice no longer uses', () => { + const model = spliceModel(); + model.services.nginx.volumes = [ + '${LOCALNET_DIR}/conf/nginx/app-user.conf:/etc/nginx/routes/app-user.c${APP_USER_PROFILE}f.template', + ]; + const mismatches = check({ spliceModel: model }); + assert.equal(mismatches.length, 2); + for (const mismatch of mismatches) { + assert.match(mismatch, /nginx mount target/); + } + }); + + // The "off" spelling must not count as a match. Splice's target only reads + // ".conf.template" while the validator is on; with APP_USER_PROFILE=off it + // reads ".coff.template", which nginx renders but never includes. If the check + // compared the raw string it would accept a Splice that no longer offers the + // target we actually shadow. + it('matches Splice targets on their profile-on spelling only', () => { + const model = spliceModel(); + model.services.nginx.volumes = [ + '${LOCALNET_DIR}/conf/nginx/app-provider.conf:/etc/nginx/templates/app-provider.c${APP_PROVIDER_PROFILE}f.template', + '${LOCALNET_DIR}/conf/nginx/app-user.conf:/etc/nginx/templates/app-user.coff.template', + ]; + const mismatches = check({ spliceModel: model }); + assert.equal(mismatches.length, 1); + assert.match(mismatches[0], /app-user\.conf\.template/); + }); +}); diff --git a/src/splice-contract.js b/src/splice-contract.js new file mode 100644 index 0000000..b4eff20 --- /dev/null +++ b/src/splice-contract.js @@ -0,0 +1,80 @@ +// Checks that the pinned Splice release still defines everything this wrapper +// addresses by name in templates/runtime-overrides.yaml. +// +// Docker Compose merges by name and never complains about a name that matches +// nothing, so a Splice release that renames a service does not fail: the +// override lands on nothing and the stack comes up misbehaving. These checks +// turn that silence into an error. +// +// The names are read from the files that emit them rather than restated here, so +// a check cannot drift from what we hand to Docker. Nothing else about Splice is +// asserted; it is free to move anything we do not name. +import fs from 'node:fs'; +import path from 'node:path'; +import yaml from 'js-yaml'; + +import { allLocalnetProfiles } from './compose.js'; +import { resolveFromPackage } from './paths.js'; + +// The target of a short-form `volumes:` entry ("source:target[:mode]"). +function mountTarget(entry) { + return String(entry).split(':')[1] ?? ''; +} + +// Splice spells a validator's mount target around its *_PROFILE value, so +// app-user.c${APP_USER_PROFILE}f.template reads "conf" only while that validator +// is on, and something nginx ignores when it is off. Our override shadows the +// "on" spelling, so that is the spelling these comparisons use. +function withProfilesOn(target) { + return target.replaceAll(/\$\{[A-Z_]+_PROFILE\}/g, 'on'); +} + +// Reports one message per mismatch, empty when the release is compatible. Takes +// parsed models so it can be exercised without a checkout on disk. +export function findContractMismatches({ spliceModel, overrideModel, profiles }) { + const spliceServices = spliceModel.services ?? {}; + const mismatches = []; + + // Without the real service, the override's replicas pin and nginx alias apply + // to a service Compose invents from the override alone and never starts. + for (const name of Object.keys(overrideModel.services ?? {})) { + if (!(name in spliceServices)) { + mismatches.push(`service "${name}" is overridden by this wrapper but the pinned Splice does not define it`); + } + } + + // A `--profile` selection that matches nothing starts nothing. + const spliceProfiles = new Set(Object.values(spliceServices).flatMap(service => service?.profiles ?? [])); + for (const profile of profiles) { + if (!spliceProfiles.has(profile)) { + mismatches.push(`profile "${profile}" is selectable by this wrapper but the pinned Splice does not define it`); + } + } + + // Compose deduplicates volumes by target, which is how the override replaces a + // validator's nginx routes. A target that no longer matches adds a second mount + // instead, leaving nginx with the routes of a validator whose UIs are not + // running: the startup failure the override exists to prevent. + const spliceTargets = new Set( + (spliceServices.nginx?.volumes ?? []).map(entry => withProfilesOn(mountTarget(entry))) + ); + for (const entry of overrideModel.services?.nginx?.volumes ?? []) { + const target = mountTarget(entry); + if (!spliceTargets.has(target)) { + mismatches.push(`nginx mount target "${target}" is overridden by this wrapper but the pinned Splice does not mount anything there`); + } + } + + return mismatches; +} + +// Reads the pinned checkout and the wrapper's own override, then compares them. +// No variable interpolation is applied: every name compared is a YAML literal. +export function checkSpliceContract(config) { + const parse = filePath => yaml.load(fs.readFileSync(filePath, 'utf8')) ?? {}; + return findContractMismatches({ + spliceModel: parse(path.resolve(config.localnetDir, 'compose.yaml')), + overrideModel: parse(resolveFromPackage('templates/runtime-overrides.yaml')), + profiles: allLocalnetProfiles, + }); +} From ae9c38b42fcf8238c52147965af24a3bd391d512 Mon Sep 17 00:00:00 2001 From: nicosampler Date: Mon, 31 Aug 2026 16:13:32 -0300 Subject: [PATCH 3/3] docs: document how to upgrade Splice Nothing told a user how to move to a new Splice release. Point splice.tag at it, run validate, then start. --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fe68ff5..b5983af 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ A Canton network here has three kinds of pieces: ```jsonc { "version": 1, - "splice": { "repo": "canton-network/splice", "tag": "0.6.11" }, // which Splice version to download + "splice": { "repo": "canton-network/splice", "tag": "0.6.11" }, // which Splice version to download (see Upgrading Splice) "composeProjectName": "canton-barebones", // Docker Compose project name "dockerNetwork": "cantonBarebones", // Docker network name "persistence": { "mode": "persistent" }, // "persistent" keeps volumes; "ephemeral" wipes on reset @@ -205,7 +205,7 @@ The binary is `canton-barebones `; the `npm run ` scripts wrap | ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------ | | `init [--force]` | Scaffold the config and compose override into the project | Writes `canton-barebones.config.json` and `splice-localnet-overrides.yaml` (existing files are skipped unless `--force`) | no | | `setup` | Download the pinned Splice LocalNet source | Writes `.generated/splice/…` on first run | no | -| `validate` | Validate the config and resolved Splice paths | Writes `.generated/localnet.env`; downloads Splice on first run | no | +| `validate` | Validate the config, the resolved Splice paths, and that the pinned Splice still fits this wrapper | Writes `.generated/localnet.env`; downloads Splice on first run | no | | `start` | Start the stack (`docker compose up -d`) | Starts containers, creates volumes and the Docker network | yes | | `stop` | Stop containers, keep volumes (`docker compose down`) | Removes containers; data volumes are preserved | yes | | `reset` | Stop containers and remove volumes (`docker compose down -v`) | **Deletes all stack data** | yes | @@ -246,6 +246,15 @@ Non-obvious behaviors worth knowing before automating against the stack: - **UIs go through nginx**, published on ports `2000`/`3000`/`4000`, not as per-UI host ports. `*.localhost` hostnames resolve to `127.0.0.1` automatically. - **Config changes apply on the next `start`** — nothing reacts to the file while the stack is running. +## Upgrading Splice + +Point `splice.tag` in `canton-barebones.config.json` at the new version, then: + +```bash +canton-barebones validate # checks that the new Splice works with canton-barebones +canton-barebones start # if validate passed +``` + ## Troubleshooting | Symptom | Cause | Fix | @@ -256,6 +265,7 @@ Non-obvious behaviors worth knowing before automating against the stack: | Port already in use (e.g. `2000`, `4903`, `5432`) | Another stack (or the same one) is already up on that port | `stop` the running stack, or free the port | | Containers are up but a participant does not respond | A bound port is not the same as a live backend | Check `readyz` (see [Verifying the stack](#verifying-the-stack)) | | Stale or corrupted state after config churn | Volumes hold old data | `reset` to wipe volumes, then `start` | +| `The pinned Splice …@X.Y.Z is not compatible …` | That Splice release moved something this wrapper addresses by name | Pin a known-good tag, or open an issue (see [Upgrading Splice](#upgrading-splice)) | | Anything under `.generated/` looks wrong | It is disposable | Delete `.generated/` — it is rebuilt on the next `start` | ## Development