From dd997bf0766a135c740fd97d255170c678af457b Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Tue, 22 Sep 2026 18:51:47 -0700 Subject: [PATCH 1/3] fix(maintenance): collect complete CI samples and repair updater boundaries --- .github/dependabot.yml | 8 ++ .github/workflows/codegen.yml | 3 +- docs/architecture/wallet-utxo-lifecycle.md | 45 +++++-- docs/infrastructure/chaintracks-server.md | 14 ++- docs/reference/dependency-policy.md | 19 ++- .../helpers/air-gap/tests/decoder.test.ts | 16 ++- scripts/ci-performance.mjs | 114 ++++++++++++------ scripts/ci-performance.test.mjs | 99 +++++++++++++++ tools/codegen/pyproject.toml | 3 +- 9 files changed, 264 insertions(+), 57 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 51596fdae..7e6209c36 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -66,6 +66,14 @@ updates: - '*' multi-ecosystem-group: monthly-stack-maintenance rebase-strategy: auto + # The npm updater currently fetches unrelated ancestor pnpm support files + # and mistakes these standalone package-lock projects for sub-workspaces. + # Exclude only those two ancestor files here; the root entry owns them. + exclude-paths: + - '../../pnpm-lock.yaml' + - '../../pnpm-workspace.yaml' + - '../../../pnpm-lock.yaml' + - '../../../pnpm-workspace.yaml' cooldown: default-days: 7 ignore: diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 32bd95d17..340313697 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -52,7 +52,8 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - version-file: tools/codegen/pyproject.toml + # Keep CI reproducible while pyproject permits Dependabot's uv resolver. + version: '0.11.32' python-version: '3.12' enable-cache: true cache-dependency-glob: tools/codegen/uv.lock diff --git a/docs/architecture/wallet-utxo-lifecycle.md b/docs/architecture/wallet-utxo-lifecycle.md index 6047b9da8..75fd754ac 100644 --- a/docs/architecture/wallet-utxo-lifecycle.md +++ b/docs/architecture/wallet-utxo-lifecycle.md @@ -4,8 +4,8 @@ title: Wallet UTXO Lifecycle kind: meta domain: wallet version: 'n/a' -last_updated: '2026-08-13' -last_verified: '2026-08-13' +last_updated: '2026-09-23' +last_verified: '2026-09-23' review_cadence_days: 30 status: stable tags: ['architecture', 'BRC-100', 'wallet', 'utxo', 'storage'] @@ -24,6 +24,12 @@ It is written against two implementations, [`@bsv/wallet-toolbox`](../packages/w against it, and the places where either one deviates are collected in [Implementation differences](#implementation-differences). +The TypeScript paths and lifecycle were rechecked against TS Stack main +`57d72e24a565d090bd605cf9767785c51ea4f179` on 2026-09-23. The Go diagrams and +comparisons below preserve the **2026-08-13 review snapshot**; they are historical +findings, not claims about the latest Go release. Recheck the linked Go source +before using a difference as a current cross-implementation defect. + ## How to read the diagrams Each lane is a layer, and time runs downward. The lanes below appear in every diagram on @@ -105,14 +111,14 @@ sequenceDiagram end ``` -The write transaction opens at `storage/methods/createAction.ts:196` and every call from -`insertTransaction` onward is inside it. The transaction row is born `unsigned` -(`createAction.ts:690`). If anything downstream throws, the cleanup path drives it to -`failed` (`:309`) and records a forensic row (`:312`) rather than deleting evidence. +The write transaction in `storage/methods/createAction.ts` contains +`insertTransaction` and its subsequent writes. The transaction row is born +`unsigned`. If construction fails after recording the plan, the cleanup path +drives it to `failed` and records a forensic row rather than deleting evidence. -`markChangeInputsSpent` (`:1398`) is the moment funding becomes exclusive: it flips the +`markChangeInputsSpent` is the moment funding becomes exclusive: it flips the selected change outputs to `{spendable: false, spentBy: transactionId}` under the row -locks taken by `findFundingOutputsForUpdate` (`:1376`). +locks taken by `findFundingOutputsForUpdate`. ### Storage call ledger @@ -166,7 +172,7 @@ sequenceDiagram The `pendingSignActions` cache is process memory on the `Wallet` instance. A `reference` issued by one process cannot be signed by another, and cannot survive a restart — -`Wallet.ts:1056` throws `WERR_NOT_IMPLEMENTED` rather than attempting recovery. Go stores +`signer/methods/signAction.ts` throws `WERR_NOT_IMPLEMENTED` rather than attempting recovery. Go stores these in a pluggable repository instead; see [difference 6](#implementation-differences). ### processAction — commit and broadcast @@ -429,6 +435,14 @@ custom instructions and tags. ### abortAction +The diagram below describes ordinary actions. BRC-177 expiring `noSend` actions +have a separate durable lifecycle: aborting a released signed action requests +revocation instead of immediately freeing its inputs. `TaskNoSendExpiry` uses +positive chain and UTXO evidence to arbitrate target-versus-reclaim races; +reclaiming, reclaimed, broadcast, target-won and conflicted states retain their +specific guards. See `StorageProvider.abortAction` and `TaskNoSendExpiry` before +operating on expiring actions. + ```mermaid sequenceDiagram autonumber @@ -504,11 +518,14 @@ sequenceDiagram end ``` -Nineteen tasks ship in `src/monitor/tasks/`. Beyond those above, `TaskReorg` and +The task classes in `src/monitor/tasks/` are selected by the Monitor profile; +not every class is registered by default. Beyond those above, `TaskReorg` and `TaskNewHeader` handle chain reorganisation, `TaskCheckNoSends` settles `nosend` transactions, `TaskUnFail` retries operator-flagged failures, `TaskArcSSE` consumes broadcaster push events, `TaskPurge` and `TaskCleanupActionBatches` reclaim storage, and -`TaskSyncWhenIdle` replicates to backup stores. +`TaskSyncWhenIdle` can be registered to replicate to backup stores. The default +and multi-user profiles include `TaskNoSendExpiry`, `TaskReviewProvenTxs` and +`TaskReconcilePendingTransactions`; `TaskMineBlock` is mock-chain-only. ## The Go implementation @@ -771,8 +788,10 @@ list-outputs special operations. ### 11. Background convergence uses different mechanisms -TypeScript ships nineteen registered Monitor tasks; Go registers four. That comparison is -misleading on its own, because Go moves much of the same work off the scheduler: +The TypeScript default and multi-user profiles each schedule sixteen tasks plus +two housekeeping tasks, with a mock-only miner added for mock chains. The archived +Go review found four scheduled tasks. Counts alone do not describe convergence: +that Go implementation moved much of the work off the scheduler: - **Event consumers.** `pkg/monitor` runs an SSE broadcast-event pipeline with a persisted replay cursor (`arcade_sse_last_event_id`) plus reorg and new-tip consumers. Reorg diff --git a/docs/infrastructure/chaintracks-server.md b/docs/infrastructure/chaintracks-server.md index 2939d86fb..498b5c3aa 100644 --- a/docs/infrastructure/chaintracks-server.md +++ b/docs/infrastructure/chaintracks-server.md @@ -3,8 +3,8 @@ id: infra-chaintracks-server title: 'Chaintracks Server' kind: infra version: '1.2.0' -last_updated: '2026-08-12' -last_verified: '2026-08-12' +last_updated: '2026-09-23' +last_verified: '2026-09-23' review_cadence_days: 30 status: stable tags: [chaintracks, block-headers, spv, merkle, infrastructure] @@ -175,6 +175,16 @@ them as a fallback until the first new generation is complete. Roll back the service image without deleting this root. Older releases continue to see their flat files; the new content-addressed and generation directories are additive. +## Verification scope + +Rechecked on 2026-09-23 against TS Stack main +`57d72e24a565d090bd605cf9767785c51ea4f179`: v1/v2 route handlers, +`server.ts` readiness and upstream/worker limits, the edge policies, and +`BulkHeaderSnapshotPublisher` generation retention and atomic current pointer. +These are source contracts. Operators must separately verify the deployed image, +ready endpoints, durable storage and public routes during each rollout; a source +version or passing unit test does not prove a live service has been upgraded. + ## When to deploy this - Running `@bsv/wallet-toolbox`-based wallets in production (the toolbox calls Chaintracks for SPV) diff --git a/docs/reference/dependency-policy.md b/docs/reference/dependency-policy.md index 4ff051026..6da2a97cc 100644 --- a/docs/reference/dependency-policy.md +++ b/docs/reference/dependency-policy.md @@ -2,9 +2,9 @@ id: dependency-release-policy title: 'Dependency and Release Policy' kind: reference -version: '1.3.1' -last_updated: '2026-09-15' -last_verified: '2026-09-15' +version: '1.3.2' +last_updated: '2026-09-23' +last_verified: '2026-09-23' review_cadence_days: 30 status: stable tags: [reference, dependencies, security, releases] @@ -61,6 +61,19 @@ Security updates are grouped only within a package-manager ecosystem and are never delayed into the monthly cross-ecosystem version update. First-party `@bsv/*` versions remain owned by the release graph. +Standalone infrastructure uses npm `package-lock.json` files outside the pnpm +workspace. Its Dependabot entry excludes only the unrelated ancestor +`pnpm-lock.yaml` and `pnpm-workspace.yaml` support files, avoiding the updater's +sub-workspace misclassification; the root entry continues to own both files. +Every infrastructure manifest and npm lockfile remains monitored. Remove this +workaround when Dependabot respects standalone npm boundaries beneath a pnpm root. + +The Python code generator accepts uv >=0.11.32 so Dependabot can resolve its +locked dependency graph with the uv version supplied by GitHub. The codegen +workflow retains an explicit uv 0.11.32 pin, Python 3.12, `uv run --locked`, and +byte-for-byte generated-output verification. A resolver proposal does not +silently change the CI toolchain or authorize different generated types. + Major changes that alter a runtime, compiler, or persisted-data contract are held from the routine monthly PR until their focused migration is ready: diff --git a/packages/helpers/air-gap/tests/decoder.test.ts b/packages/helpers/air-gap/tests/decoder.test.ts index 04cbddc65..6f7a5281f 100644 --- a/packages/helpers/air-gap/tests/decoder.test.ts +++ b/packages/helpers/air-gap/tests/decoder.test.ts @@ -410,11 +410,23 @@ describe('AirGapDecoder', () => { const dec = new AirGapDecoder() expect(dec.accept(e.partAt(0)).have).toBe(1) const redundant: number[] = [] - for (let seq = 2; redundant.length < MAX_TRACKED_SEQS + 1; seq++) { + // Bound fixture generation too: a broken mapping must fail this test, + // not make the fixture search forever during mutation testing. + for ( + let seq = 2; + seq < MAX_TRACKED_SEQS * 16 && redundant.length < MAX_TRACKED_SEQS + 1; + seq++ + ) { const blocks = blocksForPart(seq, 2) if (blocks.length === 1 && blocks[0] === 0) redundant.push(seq) } - for (const seq of redundant) expect(dec.accept(e.partAt(seq)).ok).toBe(true) + expect(redundant).toHaveLength(MAX_TRACKED_SEQS + 1) + for (const seq of redundant) { + const accepted = dec.accept(e.partAt(seq)).ok + // Avoid allocating thousands of successful Jest matchers in every mutant. + // Keep the same literal-true assertion, with full diagnostics on failure. + if (accepted !== true) expect({ seq, accepted }).toEqual({ seq, accepted: true }) + } // The tracker is full; new and repeated sequence numbers are simply // re-processed as redundancy instead of being remembered, and the honest // part still completes the message. diff --git a/scripts/ci-performance.mjs b/scripts/ci-performance.mjs index 42963dd46..dbd79c266 100644 --- a/scripts/ci-performance.mjs +++ b/scripts/ci-performance.mjs @@ -7,7 +7,10 @@ import { pathToFileURL } from 'node:url' const DEFAULT_REPOSITORY = 'bsv-blockchain/ts-stack' const DEFAULT_SAMPLE_SIZE = 20 const FULL_SCOPE_MINIMUM_JOBS = 50 -const MAX_RUNS = 100 +const PAGE_SIZE = 100 +const MAX_CANDIDATE_PAGES = 10 + +class PerformanceCollectionError extends Error {} function secondsBetween(start, end) { if (!start || !end) return null @@ -322,54 +325,84 @@ async function githubJson(url, token) { Authorization: `Bearer ${token}`, 'User-Agent': 'ts-stack-ci-performance', 'X-GitHub-Api-Version': '2022-11-28' - } + }, + signal: AbortSignal.timeout(30_000) }) if (!response.ok) { - throw new Error(`GitHub API ${response.status} for ${url}: ${await response.text()}`) + throw new PerformanceCollectionError( + `GitHub API returned HTTP ${response.status} while collecting CI performance evidence.` + ) } return await response.json() } -async function collectReport({ +async function collectJobs(apiRoot, run, request) { + const jobs = [] + for (let page = 1; page <= 10; page++) { + const data = await request( + `${apiRoot}/actions/runs/${run.id}/jobs?per_page=${PAGE_SIZE}&page=${page}` + ) + const batch = data.jobs ?? [] + jobs.push(...batch) + if (batch.length < PAGE_SIZE || jobs.length >= data.total_count) return jobs + } + throw new PerformanceCollectionError( + 'CI run exceeds the bounded job-page limit; refusing partial timing evidence.' + ) +} + +export function sampleErrors(report) { + const expected = report.classification.sampleSizePerClass + return ['fullScope', 'targeted'].flatMap(name => { + const count = report.groups[name]?.runs.length ?? 0 + return count === expected + ? [] + : [`Only ${count} ${name} successful PR runs were available; expected ${expected}`] + }) +} + +export async function collectReport({ repository, workflow, token, sampleSize = DEFAULT_SAMPLE_SIZE, - minimumJobs = FULL_SCOPE_MINIMUM_JOBS + minimumJobs = FULL_SCOPE_MINIMUM_JOBS, + maximumPages = MAX_CANDIDATE_PAGES, + request = url => githubJson(url, token) }) { const apiRoot = `https://api.github.com/repos/${repository}` const encodedWorkflow = encodeURIComponent(workflow) - const runData = await githubJson( - `${apiRoot}/actions/workflows/${encodedWorkflow}/runs?event=pull_request&status=success&per_page=${MAX_RUNS}`, - token - ) - const candidates = (runData.workflow_runs ?? []).filter( - run => run.status === 'completed' && run.conclusion === 'success' - ) const groups = { fullScope: [], targeted: [] } - const batchSize = 8 - for (let index = 0; index < candidates.length; index += batchSize) { - const batch = candidates.slice(index, index + batchSize) - const measured = await Promise.all( - batch.map(async run => { - const data = await githubJson(`${run.jobs_url}?per_page=100`, token) - return measureRun(run, data.jobs ?? []) - }) + const seenHeads = new Set() + let candidatePages = 0 + let examinedRuns = 0 + const complete = () => Object.values(groups).every(runs => runs.length === sampleSize) + for (let page = 1; page <= maximumPages && !complete(); page++) { + const runData = await request( + `${apiRoot}/actions/workflows/${encodedWorkflow}/runs?event=pull_request&status=success&per_page=${PAGE_SIZE}&page=${page}` ) - for (const run of measured) { - const classification = classifyRun(run, minimumJobs) - if (groups[classification].length < sampleSize) groups[classification].push(run) - } - if (Object.values(groups).every(runs => runs.length === sampleSize)) break - } - for (const [name, runs] of Object.entries(groups)) { - if (runs.length !== sampleSize) { - throw new Error( - `Only ${runs.length} ${name} successful PR runs were available; expected ${sampleSize}` + candidatePages++ + const pageRuns = runData.workflow_runs ?? [] + const candidates = pageRuns.filter(run => { + if (run.status !== 'completed' || run.conclusion !== 'success' || seenHeads.has(run.head_sha)) + return false + seenHeads.add(run.head_sha) + return true + }) + for (let index = 0; index < candidates.length && !complete(); index += 8) { + const batch = candidates.slice(index, index + 8) + const measured = await Promise.all( + batch.map(async run => measureRun(run, await collectJobs(apiRoot, run, request))) ) + examinedRuns += measured.length + for (const run of measured) { + const classification = classifyRun(run, minimumJobs) + if (groups[classification].length < sampleSize) groups[classification].push(run) + } } + if (pageRuns.length < PAGE_SIZE) break } - return createReport({ + const report = createReport({ repository, workflow, collectedAt: new Date().toISOString(), @@ -377,6 +410,13 @@ async function collectReport({ minimumJobs, groups }) + report.collection = { + candidatePages, + maximumPages, + examinedRuns, + uniqueCandidateHeads: seenHeads.size + } + return report } function renderSummary(report, comparisons) { @@ -430,9 +470,11 @@ async function main(arguments_) { }) if (outputPath) await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) if (writeBaselinePath) { + const errors = [...sampleErrors(report), ...validateBaseline(createBaseline(report))] + if (errors.length > 0) throw new PerformanceCollectionError(errors.join('\n')) await writeFile(writeBaselinePath, `${JSON.stringify(createBaseline(report), null, 2)}\n`) } - let comparisons = [] + let comparisons = sampleErrors(report) if (baselinePath) { const baseline = JSON.parse(await readFile(baselinePath, 'utf8')) comparisons = compareToBaseline(report, baseline) @@ -442,14 +484,16 @@ async function main(arguments_) { if (process.env.GITHUB_STEP_SUMMARY) { await appendFile(process.env.GITHUB_STEP_SUMMARY, summary) } - if (comparisons.length > 0) throw new Error(comparisons.join('\n')) + if (comparisons.length > 0) throw new PerformanceCollectionError(comparisons.join('\n')) } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { try { await main(process.argv.slice(2)) - } catch { - console.error('CI performance command failed.') + } catch (error) { + console.error( + error instanceof PerformanceCollectionError ? error.message : 'CI performance command failed.' + ) process.exitCode = 1 } } diff --git a/scripts/ci-performance.test.mjs b/scripts/ci-performance.test.mjs index 06c1980ec..1a01999b1 100644 --- a/scripts/ci-performance.test.mjs +++ b/scripts/ci-performance.test.mjs @@ -3,6 +3,8 @@ import test from 'node:test' import { classifyRun, + collectReport, + sampleErrors, compareToBaseline, createBaseline, createReport, @@ -119,3 +121,100 @@ test('baseline comparison permits bounded noise and rejects median or p95 regres staleSummary.reference.fullScope.runs[0].durationSeconds += 1000 assert.match(validateBaseline(staleSummary).join('\n'), /summary must match/) }) + +function candidate(id, head = id) { + return { + id, + head_sha: String(head).padStart(40, '0'), + status: 'completed', + conclusion: 'success', + html_url: `https://github.com/bsv-blockchain/ts-stack/actions/runs/${id}`, + created_at: '2026-09-01T00:00:00Z', + updated_at: '2026-09-01T00:01:00Z' + } +} + +function jobs(count) { + return Array.from({ length: count }, () => ({ + name: 'Test', + conclusion: 'success', + started_at: '2026-09-01T00:00:01Z', + completed_at: '2026-09-01T00:00:59Z', + steps: [] + })) +} + +test('collector searches beyond 100 runs, deduplicates heads and paginates complete job evidence', async () => { + const requests = [] + const request = async url => { + requests.push(url) + const parsed = new URL(url) + const page = Number(parsed.searchParams.get('page')) + if (parsed.pathname.endsWith('/runs')) { + return { + workflow_runs: + page === 1 + ? Array.from({ length: 100 }, (_, index) => + candidate(index + 1, index === 1 ? 1 : index + 1) + ) + : [candidate(101), candidate(102)] + } + } + const id = Number(parsed.pathname.split('/').at(-2)) + const all = jobs(id > 100 ? 125 : 3) + return { total_count: all.length, jobs: all.slice((page - 1) * 100, page * 100) } + } + const result = await collectReport({ + repository: 'bsv-blockchain/ts-stack', + workflow: 'ci.yml', + sampleSize: 2, + request + }) + assert.deepEqual(sampleErrors(result), []) + assert.deepEqual( + result.groups.targeted.runs.map(run => run.id), + [1, 3] + ) + assert.deepEqual( + result.groups.fullScope.runs.map(run => run.jobCount), + [125, 125] + ) + assert.equal(result.collection.candidatePages, 2) + assert.ok(requests.some(url => url.includes('/101/jobs?per_page=100&page=2'))) + assert.equal( + new Set(Object.values(result.groups).flatMap(group => group.runs.map(run => run.headSha))).size, + 4 + ) +}) + +test('an exhausted search retains partial diagnostics but cannot pass the sample gate', async () => { + const result = await collectReport({ + repository: 'bsv-blockchain/ts-stack', + workflow: 'ci.yml', + sampleSize: 2, + request: async url => + url.includes('/workflows/') + ? { workflow_runs: [candidate(1)] } + : { jobs: jobs(3), total_count: 3 } + }) + assert.equal(result.collection.candidatePages, 1) + assert.equal(result.groups.targeted.runs.length, 1) + assert.match(sampleErrors(result).join('\n'), /Only 0 fullScope.*expected 2/) + assert.match(sampleErrors(result).join('\n'), /Only 1 targeted.*expected 2/) +}) + +test('collector bounds history scanning even if the API keeps returning pages', async () => { + const result = await collectReport({ + repository: 'bsv-blockchain/ts-stack', + workflow: 'ci.yml', + sampleSize: 2, + maximumPages: 2, + request: async url => + url.includes('/workflows/') + ? { workflow_runs: Array.from({ length: 100 }, () => candidate(1)) } + : { jobs: jobs(3), total_count: 3 } + }) + assert.equal(result.collection.candidatePages, 2) + assert.equal(result.collection.examinedRuns, 1) + assert.equal(sampleErrors(result).length, 2) +}) diff --git a/tools/codegen/pyproject.toml b/tools/codegen/pyproject.toml index 4c3581d6b..daedb8d39 100644 --- a/tools/codegen/pyproject.toml +++ b/tools/codegen/pyproject.toml @@ -8,5 +8,6 @@ dependencies = [ [tool.uv] package = false -required-version = "==0.11.32" +# CI pins uv exactly; allow the compatible resolver provided by Dependabot. +required-version = ">=0.11.32" exclude-newer = "1 week" From 7ba8695d8c057eb15484820bff31958e284d75de Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Tue, 22 Sep 2026 19:02:19 -0700 Subject: [PATCH 2/3] docs(ci): record performance sampling findings and test guarantees --- docs/reference/ci-performance.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/reference/ci-performance.md b/docs/reference/ci-performance.md index 964bebb91..12586d7dd 100644 --- a/docs/reference/ci-performance.md +++ b/docs/reference/ci-performance.md @@ -74,6 +74,28 @@ Review the 40 exact run links, classification threshold, sample summaries, workflow or runner changes, and the stated median/p95 budget. Never loosen a budget solely to make a red trend green. +## Sampling and investigation + +The collector searches up to ten pages of successful PR runs, deduplicates source +heads, and paginates each run's complete job list. It retains 20 full-scope and +20 targeted runs, including job and step timings. When history cannot supply the +required sample, it writes the partial report before failing; incomplete samples +cannot establish a new baseline. API requests and pagination are bounded. + +The September review found that the July baseline predates the optical-codec +mutation target and later QA additions. In the recovered 40-run sample, full-scope +median/p95 was 2,007/2,254 seconds and targeted median/p95 was 566/833 seconds. +The optical-codec mutation job dominated full runs (median 1,378 seconds), while +wallet coverage dominated the longer targeted runs. Those observations do not by +themselves justify raising the budget. Keep the historical baseline until a +reviewed workload comparison and measurements justify a replacement. + +The duplicate-tracking codec regression now bounds its fixture's search and +avoids constructing a successful Jest matcher for every redundant frame. It +still sends more than the actual tracking limit, checks every acceptance result, +repeats the final frame and verifies exact recovered bytes. Mutation and property +coverage, payload sizes, test counts and timeout budgets remain governed. + ## Complete release acceptance Dispatch `CI` manually on the reviewed main commit to select the entire governed From 2d0f9a82cbd4f83dc63d6e463fdf9ab76d7b7a93 Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Tue, 22 Sep 2026 19:10:59 -0700 Subject: [PATCH 3/3] fix(ci): declare conformance wallet build prerequisite --- conformance/runner/README.md | 7 ++++++- conformance/runner/ts/package.json | 1 + pnpm-lock.yaml | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/conformance/runner/README.md b/conformance/runner/README.md index 1128e9f46..de68e86c7 100644 --- a/conformance/runner/README.md +++ b/conformance/runner/README.md @@ -20,7 +20,12 @@ pnpm --filter @bsv/conformance-runner-ts test The structural runner's `build` command verifies its JavaScript syntax. The TypeScript runner is linted and checked in strict mode before executing its -tests. `test` executes the required vector suite. `validate` checks vector and +tests. Its workspace dependencies include wallet-toolbox because the wallet +dispatchers typecheck against its built declarations. For a clean targeted +build, run `pnpm --filter '@bsv/conformance-runner-ts...' --if-present build` +before the TypeScript runner's typecheck; the declared dependency graph builds +those prerequisites even when the changed package is air-gap or CHIRP. +`test` executes the required vector suite. `validate` checks vector and implementation metadata without executing the cases. Generated vectors live under `conformance/generated`; edit their source specifications and run the owned generator rather than editing generated output. diff --git a/conformance/runner/ts/package.json b/conformance/runner/ts/package.json index 92eb51f14..5e25c33d6 100644 --- a/conformance/runner/ts/package.json +++ b/conformance/runner/ts/package.json @@ -13,6 +13,7 @@ "@bsv/air-gap": "workspace:^", "@bsv/chirp": "workspace:^", "@bsv/sdk": "workspace:^", + "@bsv/wallet-toolbox": "workspace:^", "@jest/globals": "^30.4.1", "@types/node": "^26.1.2", "@typescript/native": "npm:typescript@7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6e24c1bd..c39ef3776 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: '@bsv/sdk': specifier: workspace:^ version: link:../../../packages/sdk + '@bsv/wallet-toolbox': + specifier: workspace:^ + version: link:../../../packages/wallet/wallet-toolbox '@jest/globals': specifier: ^30.4.1 version: 30.4.1