From fef3166c94d647e78238c30f275c670b299268c4 Mon Sep 17 00:00:00 2001 From: Kendrik Felty Date: Fri, 24 Jul 2026 21:26:05 -0500 Subject: [PATCH 1/4] test: cover mixed search lifecycle recovery --- scripts/packed-process-smoke.mjs | 289 +++++++++++++++++++++++++++++++ src/taproot-operation.ts | 132 ++++++++++---- test/search.integration.test.ts | 65 +++++++ 3 files changed, 449 insertions(+), 37 deletions(-) diff --git a/scripts/packed-process-smoke.mjs b/scripts/packed-process-smoke.mjs index 04d45fa..b23137d 100644 --- a/scripts/packed-process-smoke.mjs +++ b/scripts/packed-process-smoke.mjs @@ -1305,6 +1305,295 @@ export async function createWorkshopSemanticConfigurationDependencies( ), JSON.stringify(statementSearches), ); + const transientResourceText = 'Transient mixed lifecycle resource payload.'; + const transientResourceBytes = Buffer.from(transientResourceText, 'utf8'); + const transientResource = await successfulToolCall( + origin, + token, + 2300, + 'create_resource', + { + idempotencyKey: 'packed-mixed-resource-v1', + data: { + id: 'resource:packed-mixed-lifecycle', + itemId: 'Q1', + title: 'Packed mixed lifecycle resource', + payload: { kind: 'inline-text', text: transientResourceText }, + mediaType: 'text/plain', + language: 'en', + integrity: { + algorithm: 'sha256', + digest: createHash('sha256') + .update(transientResourceBytes) + .digest('hex'), + byteLength: transientResourceBytes.byteLength, + }, + }, + }, + ); + const transientResourceUpdated = await successfulToolCall( + origin, + token, + 2301, + 'update_resource', + { + resourceId: transientResource.id, + expectedRevision: transientResource.revision, + idempotencyKey: 'packed-mixed-resource-v2', + patch: { title: 'Packed mixed lifecycle resource revised' }, + }, + ); + const transientAnnotation = await successfulToolCall( + origin, + token, + 2302, + 'create_annotation', + { + idempotencyKey: 'packed-mixed-annotation-v1', + data: { + id: 'annotation:packed-mixed-lifecycle', + body: { + kind: 'text', + text: 'Packed mixed lifecycle annotation.', + mediaType: 'text/plain', + language: 'en', + }, + target: { + kind: 'resource', + sourceId: transientResource.id, + selector: { type: 'FragmentSelector', value: 'line=1' }, + }, + motivation: 'commenting', + }, + }, + ); + const transientAnnotationUpdated = await successfulToolCall( + origin, + token, + 2303, + 'update_annotation', + { + annotationId: transientAnnotation.id, + expectedRevision: transientAnnotation.revision, + idempotencyKey: 'packed-mixed-annotation-v2', + patch: { + body: { + kind: 'text', + text: 'Packed mixed lifecycle annotation revised.', + mediaType: 'text/plain', + language: 'en', + }, + }, + }, + ); + const transientMemoryInput = { + slug: 'packed-mixed-lifecycle-memory', + idempotencyKey: 'packed-mixed-memory-v1', + description: 'Packed mixed lifecycle memory', + content: 'Packed mixed lifecycle memory revision one.', + }; + const transientMemory = await successfulToolCall( + origin, + token, + 2304, + 'upsert_memory', + transientMemoryInput, + ); + const beforeMemoryReplay = await successfulToolCall( + origin, + token, + 2305, + 'search_admin', + { action: 'health' }, + ); + assert.deepEqual( + await successfulToolCall( + origin, + token, + 2306, + 'upsert_memory', + transientMemoryInput, + ), + transientMemory, + ); + const afterMemoryReplay = await successfulToolCall( + origin, + token, + 2307, + 'search_admin', + { action: 'health' }, + ); + assert.equal( + afterMemoryReplay.materialization.sourceHighWatermark, + beforeMemoryReplay.materialization.sourceHighWatermark, + ); + const transientMemoryUpdated = await successfulToolCall( + origin, + token, + 2308, + 'upsert_memory', + { + slug: transientMemory.slug, + expectedRevision: transientMemory.revision, + idempotencyKey: 'packed-mixed-memory-v2', + description: transientMemory.description, + content: 'Packed mixed lifecycle memory revision two.', + }, + ); + const transientPrompt = await successfulToolCall( + origin, + token, + 2309, + 'create_prompt', + { + idempotencyKey: 'packed-mixed-prompt-v1', + name: 'packed-mixed-lifecycle-prompt', + promptText: 'Packed mixed lifecycle prompt revision one.', + }, + ); + const transientPromptUpdated = await successfulToolCall( + origin, + token, + 2310, + 'update_prompt', + { + promptId: transientPrompt.id, + expectedRevision: transientPrompt.revision, + idempotencyKey: 'packed-mixed-prompt-v2', + patch: { promptText: 'Packed mixed lifecycle prompt revision two.' }, + }, + ); + const transientTask = await successfulToolCall( + origin, + token, + 2311, + 'create_task', + { + idempotencyKey: 'packed-mixed-task-v1', + description: 'Archive the packed mixed lifecycle task', + prompt: 'Archive after the lifecycle projection is committed.', + }, + ); + await successfulToolCall(origin, token, 2312, 'archive_task', { + taskId: transientTask.id, + expectedRevision: transientTask.revision, + }); + await successfulToolCall(origin, token, 2313, 'delete_annotation', { + annotationId: transientAnnotation.id, + expectedRevision: transientAnnotationUpdated.revision, + idempotencyKey: 'packed-mixed-annotation-delete-v3', + }); + await successfulToolCall(origin, token, 2314, 'delete_resource', { + resourceId: transientResource.id, + expectedRevision: transientResourceUpdated.revision, + idempotencyKey: 'packed-mixed-resource-delete-v3', + }); + await successfulToolCall(origin, token, 2315, 'delete_memory', { + slug: transientMemory.slug, + expectedRevision: transientMemoryUpdated.revision, + idempotencyKey: 'packed-mixed-memory-delete-v3', + }); + await successfulToolCall(origin, token, 2316, 'delete_prompt', { + promptId: transientPrompt.id, + expectedRevision: transientPromptUpdated.revision, + idempotencyKey: 'packed-mixed-prompt-delete-v3', + }); + const mixedLagged = await successfulToolCall( + origin, + token, + 2317, + 'search_admin', + { action: 'health' }, + ); + assert.equal(mixedLagged.materialization.deadJobs, 0); + assert.ok( + mixedLagged.materialization.sourceHighWatermark > + mixedLagged.materialization.activeAppliedWatermark, + JSON.stringify(mixedLagged), + ); + for (let page = 0; page < 10; page += 1) { + await successfulToolCall(origin, token, 2318 + page, 'search_admin', { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }); + } + const mixedCaughtUp = await successfulToolCall( + origin, + token, + 2328, + 'search_admin', + { action: 'health' }, + ); + assert.deepEqual( + { + pendingJobs: mixedCaughtUp.materialization.pendingJobs, + leasedJobs: mixedCaughtUp.materialization.leasedJobs, + deadJobs: mixedCaughtUp.materialization.deadJobs, + staleHeads: mixedCaughtUp.materialization.staleHeads, + activeAppliedWatermark: + mixedCaughtUp.materialization.activeAppliedWatermark, + }, + { + pendingJobs: 0, + leasedJobs: 0, + deadJobs: 0, + staleHeads: 0, + activeAppliedWatermark: mixedCaughtUp.materialization.sourceHighWatermark, + }, + ); + await successfulToolCall(origin, token, 2329, 'search_admin', { + action: 'rebuild', + }); + const duplicateRebuild = await mcp(origin, token, 2330, 'tools/call', { + name: 'search_admin', + arguments: { action: 'rebuild' }, + }); + assert.equal( + toolErrorCode(duplicateRebuild), + 'conflict', + JSON.stringify(duplicateRebuild), + ); + for (let page = 0; page < 10; page += 1) { + await successfulToolCall(origin, token, 2331 + page, 'search_admin', { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }); + } + await successfulToolCall(origin, token, 2341, 'search_admin', { + action: 'activate', + }); + const mixedActivated = await successfulToolCall( + origin, + token, + 2342, + 'search_admin', + { action: 'health' }, + ); + assert.deepEqual( + { + status: mixedActivated.materialization.status, + shadowCorpusGeneration: + mixedActivated.materialization.shadowCorpusGeneration, + pendingJobs: mixedActivated.materialization.pendingJobs, + leasedJobs: mixedActivated.materialization.leasedJobs, + deadJobs: mixedActivated.materialization.deadJobs, + staleHeads: mixedActivated.materialization.staleHeads, + activeAppliedWatermark: + mixedActivated.materialization.activeAppliedWatermark, + }, + { + status: 'healthy', + shadowCorpusGeneration: null, + pendingJobs: 0, + leasedJobs: 0, + deadJobs: 0, + staleHeads: 0, + activeAppliedWatermark: + mixedActivated.materialization.sourceHighWatermark, + }, + ); const qdrantConfiguration = await successfulToolCall( origin, token, diff --git a/src/taproot-operation.ts b/src/taproot-operation.ts index d2df7c7..92cb79c 100644 --- a/src/taproot-operation.ts +++ b/src/taproot-operation.ts @@ -416,47 +416,64 @@ async function searchAdministration( : { state: 'unconfigured' }, }; case 'materialize': { - const receipt = await runBoundedMaterialization( - runtime.materialization, - context, - { - limit, - maxRebuildRoots: optionalInteger(values.maxRebuildRoots, limit), - ...(typeof values.maxChunkBytes === 'number' - ? { maxChunkBytes: values.maxChunkBytes } - : {}), - }, - ); - const health = truthfulMaterializationHealth( - await runtime.materialization.health(context), - ); - return { - claimed: receipt.claimed, - completed: receipt.completed, - superseded: receipt.superseded, - deferred: receipt.deferred, - dead: receipt.dead, - rebuildRootsEnumerated: receipt.rebuildRootsEnumerated, - job: { - state: 'completed', - requestedLimit: limit, - }, - result: { ...receipt }, - health, - }; + try { + const receipt = await runBoundedMaterialization( + runtime.materialization, + context, + { + limit, + maxRebuildRoots: optionalInteger(values.maxRebuildRoots, limit), + ...(typeof values.maxChunkBytes === 'number' + ? { maxChunkBytes: values.maxChunkBytes } + : {}), + }, + ); + const health = truthfulMaterializationHealth( + await runtime.materialization.health(context), + ); + return { + claimed: receipt.claimed, + completed: receipt.completed, + superseded: receipt.superseded, + deferred: receipt.deferred, + dead: receipt.dead, + rebuildRootsEnumerated: receipt.rebuildRootsEnumerated, + job: { + state: 'completed', + requestedLimit: limit, + }, + result: { ...receipt }, + health, + }; + } catch (error) { + return mapSearchAdministrationError(error, action); + } } case 'retry-dead': - return { - retried: await runtime.materialization.retryDead(context, { limit }), - }; + try { + return { + retried: await runtime.materialization.retryDead(context, { limit }), + }; + } catch (error) { + return mapSearchAdministrationError(error, action); + } case 'rebuild': - return { - generation: await runtime.materialization.startShadowRebuild(context), - }; + try { + return { + generation: await runtime.materialization.startShadowRebuild(context), + }; + } catch (error) { + return mapSearchAdministrationError(error, action); + } case 'activate': - return { - generation: await runtime.materialization.activateReadyShadow(context), - }; + try { + return { + generation: + await runtime.materialization.activateReadyShadow(context), + }; + } catch (error) { + return mapSearchAdministrationError(error, action); + } } const semantic = options.semantic; if (!semantic) throw unavailable('Semantic search is not configured'); @@ -789,3 +806,44 @@ function invalid(message: string): WorkshopError { function unavailable(message: string): WorkshopError { return new WorkshopError('dependency_unavailable', message, 503); } + +function mapSearchAdministrationError(error: unknown, action: string): never { + if (error instanceof WorkshopError) throw error; + const candidate = error as { + name?: unknown; + code?: unknown; + message?: unknown; + }; + const name = typeof candidate?.name === 'string' ? candidate.name : ''; + const code = typeof candidate?.code === 'string' ? candidate.code : ''; + const message = + typeof candidate?.message === 'string' ? candidate.message : ''; + if (name === 'AuthorizationDeniedError' || code === 'AUTHORIZATION_DENIED') + throw new WorkshopError('forbidden', 'Authorization denied', 403); + if (action === 'rebuild' && message === 'search rebuild is already active') + throw new WorkshopError( + 'conflict', + 'Search rebuild is already active', + 409, + ); + if (action === 'activate' && message === 'search rebuild is not active') + throw new WorkshopError('conflict', 'Search rebuild is not active', 409); + if (action === 'activate' && message === 'search rebuild is not ready') + throw new WorkshopError('conflict', 'Search rebuild is not ready', 409); + if (name === 'InvalidAuthorizationError' || code === 'INVALID_AUTHORIZATION') + throw new WorkshopError( + 'validation_failed', + 'Search administration request is invalid', + 400, + ); + throw new WorkshopError( + 'dependency_unavailable', + 'Search administration owner is unavailable', + 503, + { + action, + ownerErrorName: name.slice(0, 128) || 'unknown', + ownerErrorCode: code.slice(0, 128) || 'unknown', + }, + ); +} diff --git a/test/search.integration.test.ts b/test/search.integration.test.ts index 94e2d44..397881d 100644 --- a/test/search.integration.test.ts +++ b/test/search.integration.test.ts @@ -1300,6 +1300,17 @@ describe('Workshop external search producers', () => { operationContext, ), ).resolves.toHaveProperty('materialization'); + await expect( + operationPort.call( + 'search_admin', + { action: 'activate' }, + operationContext, + ), + ).rejects.toMatchObject({ + code: 'conflict', + status: 409, + message: 'Search rebuild is not active', + }); await expect( operationPort.call( 'search_admin', @@ -1374,6 +1385,60 @@ describe('Workshop external search producers', () => { ) ).results, ).toEqual([]); + await expect( + search.materialization.health(context), + ).resolves.toMatchObject({ + staleHeads: 0, + pendingJobs: 0, + leasedJobs: 0, + deadJobs: 0, + }); + await expect( + operationPort.call( + 'search_admin', + { action: 'rebuild' }, + operationContext, + ), + ).resolves.toMatchObject({ generation: expect.any(Number) }); + await expect( + operationPort.call( + 'search_admin', + { action: 'rebuild' }, + operationContext, + ), + ).rejects.toMatchObject({ + code: 'conflict', + status: 409, + message: 'Search rebuild is already active', + }); + for (let page = 0; page < 10; page += 1) { + await operationPort.call( + 'search_admin', + { + action: 'materialize', + limit: 100, + options: { maxRebuildRoots: 100 }, + }, + operationContext, + ); + } + await expect( + operationPort.call( + 'search_admin', + { action: 'activate' }, + operationContext, + ), + ).resolves.toMatchObject({ generation: expect.any(Number) }); + await expect( + search.materialization.health(context), + ).resolves.toMatchObject({ + status: 'healthy', + staleHeads: 0, + pendingJobs: 0, + leasedJobs: 0, + deadJobs: 0, + shadowCorpusGeneration: null, + }); } finally { await db.close(); } From a4f1677ce6f260a937329439232b977c4821b861 Mon Sep 17 00:00:00 2001 From: Kendrik Felty Date: Fri, 24 Jul 2026 21:27:50 -0500 Subject: [PATCH 2/4] fix: propagate external producer deletions --- src/server/canonical-producer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/canonical-producer.ts b/src/server/canonical-producer.ts index 8d18555..ad03e01 100644 --- a/src/server/canonical-producer.ts +++ b/src/server/canonical-producer.ts @@ -97,6 +97,7 @@ export function createWorkshopCanonicalOwnerAdapterV1( const participant = await registered.issuer.seal({ sourceId: input.sourceId, sourceRevision: String(input.sourceRevision), + operation: input.operation, predecessor: input.predecessor, ownerMutationToken: token, }); From c3034a03a8fa2912cdfcd2a735e8f8466b85d865 Mon Sep 17 00:00:00 2001 From: Kendrik Felty Date: Fri, 24 Jul 2026 22:21:02 -0500 Subject: [PATCH 3/4] chore: bind corrected Taproot candidate --- docs/peer-artifacts-v1.json | 28 ++++++++++++++-------------- package-lock.json | 2 +- scripts/packed-process-smoke.mjs | 1 + 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/peer-artifacts-v1.json b/docs/peer-artifacts-v1.json index ff57cce..d6995cf 100644 --- a/docs/peer-artifacts-v1.json +++ b/docs/peer-artifacts-v1.json @@ -17,32 +17,32 @@ "name": "taproot", "destinationDirectory": "taproot-artifacts", "repository": "https://github.com/gnolith/taproot.git", - "commit": "98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac", - "tree": "b3f11979405f1ecfca881367ce1ee40218d83d71", + "commit": "fa74e4550301fd575049d7c3703e39086ea93200", + "tree": "8b23cf40b26dda6421dd52999607d0cc89c89bf9", "filename": "gnolith-taproot-0.5.0.tgz", - "bytes": 348272, - "sha1": "6a1e6d54293d57067ee37e3a3aae447e04127023", - "sha256": "030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818", - "integrity": "sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg==", + "bytes": 349163, + "sha1": "cefab7e92d8af14938550a887dc0c2cdfdaeba35", + "sha256": "8d7b42bd3c9a283f7cbcfa8579ae71296b6e571e1813998b4c460f6db1e0e2c4", + "integrity": "sha512-bL5bUcqvjxO57o3e+A2Ffn9G9dZLH/i1Ltau5Mc3TcBIZRsxI4bhKuBbieOQEitG4ABktilOsqgIMG5F9joCzQ==", "canonicalEvidence": { - "hostedRunId": "30130270744", - "hostedArtifactId": "8610844408", - "hostedArtifactName": "taproot-release-candidate-98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac", - "expiresAt": "2026-10-22", + "hostedRunId": "30141709949", + "hostedArtifactId": "8614684487", + "hostedArtifactName": "taproot-release-candidate-fa74e4550301fd575049d7c3703e39086ea93200", + "expiresAt": "2026-10-23", "sbom": { "filename": "gnolith-taproot-0.5.0.cdx.json", - "bytes": 734126, - "sha256": "cee47a765e917037ce0c3d0c28e481d0a48c204f729e9a28dff730df6bf5f3fc" + "bytes": 741220, + "sha256": "c49c69b55db703a4284e60bc6d6e54c26044b391ff516f196306d21d8a8e47c9" }, "provenance": { "filename": "gnolith-taproot-0.5.0.provenance.json", "bytes": 1150, - "sha256": "678f76bbef3433ba9b7f206052af68575033334c05c28cc7413d6fb68f8915fa" + "sha256": "03d3dc4cdcdc29e70971055d52bb375ab15171a3ae3375b8ae36d4b2b3eee6cb" }, "inventory": { "filename": "SHA256SUMS", "bytes": 293, - "sha256": "116e33d84b5bf8578e46d8c106cf3c4ffa33b8346368d504c6114e6a532e189b" + "sha256": "adc2ef99ca68523c7ee7f4cdb90ee35a254fa2b2b3eff85d7799b3e05b5dcf2c" } } } diff --git a/package-lock.json b/package-lock.json index 386d33e..22b98dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5807,7 +5807,7 @@ "node_modules/@gnolith/taproot": { "version": "0.5.0", "resolved": "file:../taproot-artifacts/gnolith-taproot-0.5.0.tgz", - "integrity": "sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg==", + "integrity": "sha512-bL5bUcqvjxO57o3e+A2Ffn9G9dZLH/i1Ltau5Mc3TcBIZRsxI4bhKuBbieOQEitG4ABktilOsqgIMG5F9joCzQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/scripts/packed-process-smoke.mjs b/scripts/packed-process-smoke.mjs index b23137d..4e58086 100644 --- a/scripts/packed-process-smoke.mjs +++ b/scripts/packed-process-smoke.mjs @@ -1823,6 +1823,7 @@ export async function createWorkshopSemanticConfigurationDependencies( [ 'installation:packed\u0000workspace:packed\u0000principal:packed\u0000backup-fixture-second-task-v1', 'installation:packed\u0000workspace:packed\u0000principal:packed\u0000backup-fixture-task-v1', + 'installation:packed\u0000workspace:packed\u0000principal:packed\u0000packed-mixed-task-v1', ], ); process.stdout.write( From 3d356d422bc743e855d9b69b03403ca88defce99 Mon Sep 17 00:00:00 2001 From: Kendrik Felty Date: Fri, 24 Jul 2026 22:39:01 -0500 Subject: [PATCH 4/4] test: pin corrected Taproot evidence --- scripts/node-sqlite-parity.mjs | 2 +- scripts/readiness-check.mjs | 6 +++--- scripts/release-block.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/node-sqlite-parity.mjs b/scripts/node-sqlite-parity.mjs index e87adff..8735701 100644 --- a/scripts/node-sqlite-parity.mjs +++ b/scripts/node-sqlite-parity.mjs @@ -18,7 +18,7 @@ try { 'TAPROOT_TARBALL', resolve('..', 'taproot-artifacts', 'gnolith-taproot-0.5.0.tgz'), process.env.TAPROOT_TARBALL_SHA256 ?? - '030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818', + '8d7b42bd3c9a283f7cbcfa8579ae71296b6e571e1813998b4c460f6db1e0e2c4', ); const [packed] = JSON.parse( execFileSync( diff --git a/scripts/readiness-check.mjs b/scripts/readiness-check.mjs index 2addaa8..8d2f0ab 100644 --- a/scripts/readiness-check.mjs +++ b/scripts/readiness-check.mjs @@ -77,15 +77,15 @@ assert.deepEqual( ); assert.equal( peerArtifacts.peers[1].commit, - '98c7bc6e66b39d4bba0862a7e5dcb3a58a7882ac', + 'fa74e4550301fd575049d7c3703e39086ea93200', ); assert.equal( peerArtifacts.peers[1].sha256, - '030e0bc4b58873b1ba362cc7ea0a232b769fcd5163ce7679d397c47b8f15b818', + '8d7b42bd3c9a283f7cbcfa8579ae71296b6e571e1813998b4c460f6db1e0e2c4', ); assert.equal( peerArtifacts.peers[1].canonicalEvidence.sbom.sha256, - 'cee47a765e917037ce0c3d0c28e481d0a48c204f729e9a28dff730df6bf5f3fc', + 'c49c69b55db703a4284e60bc6d6e54c26044b391ff516f196306d21d8a8e47c9', ); assert.ok(existsSync('schemas/workshop-config-v1.json')); console.log('repository readiness invariants passed'); diff --git a/scripts/release-block.mjs b/scripts/release-block.mjs index f00722c..3cea3bc 100644 --- a/scripts/release-block.mjs +++ b/scripts/release-block.mjs @@ -14,7 +14,7 @@ const exactIntegrity = 'sha512-yYxbrUNnu74zBaxHoywGlgeG2LFz4HMzi2RLcsq83/JVEIkwbWvWZ8tuLpxFYxTAgTXG1/FHddgEJStRupe54A=='; const candidateVersion = '0.5.0'; const candidateIntegrity = - 'sha512-uVA0FBUyQA3SwNGtxk93mHeoZibzzddHvbmSJ8krZKWekLbHSGNDtYBCmtPTpMZPhlrWMJYYBO+7NjpFy1Tkbg=='; + 'sha512-bL5bUcqvjxO57o3e+A2Ffn9G9dZLH/i1Ltau5Mc3TcBIZRsxI4bhKuBbieOQEitG4ABktilOsqgIMG5F9joCzQ=='; export function verifyInstalledTaprootCandidate() { const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));