From ed2d2f9be05ae177df2d45331f1f5f05d5169443 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Thu, 21 May 2026 01:26:58 +0200 Subject: [PATCH] docs(concepts): add Wiki retrieval pillar page (#191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(concepts): add Wiki retrieval pillar page Adds a dedicated concept page covering the wiki side of the context layer: the page contract, the hybrid retrieval pipeline (lexical, semantic, token lanes fused by RRF), the refs/sl_refs/[[wikilink]] graph, validation that keeps edges live, and where ingest sources pages. Wired into concepts nav and cross-linked from the-context-layer to mirror the existing Semantic querying link. * test: derive release versions in tests instead of hardcoding 0.1.0-rc.1 After @semantic-release/git started committing version bumps back to the branch, the 0.4.0 release rewrote package.json, packages/cli/package.json, and release-policy.json — but the script and CLI tests still pinned the pre-bump strings (0.0.0-private, 0.1.0-rc.1, 0.1.0rc1), so every new branch off main failed TypeScript checks and Coverage. Drive the version off the existing source of truth instead: read @ktx/cli/package.json via createRequire in the CLI tests, and reuse the already-imported PUBLIC_NPM_PACKAGE_VERSION / RUNTIME_WHEEL_PACKAGE_VERSION constants in the script tests. The two assertions that pinned those constants to specific values become semver shape checks. --- docs-site/content/docs/concepts/meta.json | 2 +- .../docs/concepts/the-context-layer.mdx | 4 + .../content/docs/concepts/wiki-retrieval.mdx | 280 ++++++++++++++++++ packages/cli/src/admin-reindex.test.ts | 7 +- packages/cli/src/index.test.ts | 46 +-- scripts/build-public-npm-package.test.mjs | 15 +- scripts/build-python-runtime-wheel.test.mjs | 4 +- .../local-embeddings-runtime-smoke.test.mjs | 13 +- scripts/package-artifacts.test.mjs | 43 +-- scripts/release-readiness.test.mjs | 6 +- 10 files changed, 365 insertions(+), 55 deletions(-) create mode 100644 docs-site/content/docs/concepts/wiki-retrieval.mdx diff --git a/docs-site/content/docs/concepts/meta.json b/docs-site/content/docs/concepts/meta.json index 72c0a407..e82721b7 100644 --- a/docs-site/content/docs/concepts/meta.json +++ b/docs-site/content/docs/concepts/meta.json @@ -1,5 +1,5 @@ { "title": "Concepts", "defaultOpen": true, - "pages": ["the-context-layer", "semantic-layer-internals", "context-as-code"] + "pages": ["the-context-layer", "semantic-layer-internals", "wiki-retrieval", "context-as-code"] } diff --git a/docs-site/content/docs/concepts/the-context-layer.mdx b/docs-site/content/docs/concepts/the-context-layer.mdx index af2a6bdb..b88546c2 100644 --- a/docs-site/content/docs/concepts/the-context-layer.mdx +++ b/docs-site/content/docs/concepts/the-context-layer.mdx @@ -195,6 +195,10 @@ wiki pages are written and prunes `sl_refs` during ingest when their target sources are deleted or their measures are renamed - so a stale page can never quietly route an agent to a definition that no longer exists. +For how the hybrid search pipeline ranks pages, how `[[wikilinks]]` extend +the graph, and how ingest authors pages from evidence, read +[Wiki retrieval](/docs/concepts/wiki-retrieval). + The split between the two pillars is sharp: | Put it in YAML | Put it in Markdown | diff --git a/docs-site/content/docs/concepts/wiki-retrieval.mdx b/docs-site/content/docs/concepts/wiki-retrieval.mdx new file mode 100644 index 00000000..32e1bf61 --- /dev/null +++ b/docs-site/content/docs/concepts/wiki-retrieval.mdx @@ -0,0 +1,280 @@ +--- +title: Wiki retrieval +description: How ktx ranks wiki pages with hybrid search, links them into a graph, and keeps both sides anchored to evidence. +--- + +The wiki is the prose half of the context layer. Agents reach it two ways: +they search for a page, then follow references inside the pages they +already opened. This page covers how both work. + +- The wiki page contract that retrieval and validation depend on. +- The hybrid search pipeline that turns a question into ranked pages. +- The reference graph agents traverse without rerunning search. +- How pages get authored from evidence, and how broken edges get pruned. + +## The wiki page contract + +A wiki page is a Markdown file with a YAML frontmatter block. Frontmatter +carries metadata; the prose below it is free-form. Keys are flat tokens +(`revenue`, `mart_account_segments`), not paths, so every page is +addressable as `[[key]]` from any other page. + +```markdown +# wiki/global/revenue.md +--- +summary: Paid order value after refunds +tags: [finance, orders] +sl_refs: [warehouse.orders] +refs: [segment-classification] +usage_mode: auto +--- + +Revenue is paid order amount after refund adjustments. + +Use `orders.total_revenue` for recognized order value and +`orders.order_count` for paid order volume. +``` + +| Field | Purpose | +|-------|---------| +| `summary` | One-line description shown in search results and the agent's knowledge index | +| `tags` | Topic labels mixed into the search text and used for filtering | +| `refs` | Outgoing edges to other wiki pages by key | +| `sl_refs` | Outgoing edges to semantic-layer sources by `connection.source` name | +| `usage_mode` | `always`, `auto`, or `never` - whether the agent must, may, or must not surface this page | +| `source` | Where the page came from when authored by ingest (e.g. `historic-sql`, `dbt`) | +| `usage` | Stats attached to historic-SQL pattern pages: executions, distinct users, runtime percentiles, error rate | + +Pages live under two scopes. `wiki/global/*.md` is the team's shared +context; `wiki/user//*.md` is per-agent scratch space that shadows +global pages with the same key. + +## What retrieval does + +A wiki search runs the same ordered steps every time. + +1. **Normalize the query.** Lowercase, tokenize, deduplicate terms. +2. **Score in three lanes.** Lexical (SQLite FTS5 bm25), semantic + (cosine similarity over embeddings), and token (term-overlap fallback) + each rank every page independently. +3. **Fuse with Reciprocal Rank Fusion.** Each lane contributes + `weight / (60 + rank)` to a candidate's score. Lanes that fail or + skip are dropped, not zeroed. +4. **Order and trim.** Sort by fused score, then by how many lanes + matched, then by id for stable tie-breaks. Return the top `limit` + results with their summaries. +5. **Hydrate on demand.** The agent calls `wiki_read` to load full + bodies for the few pages that look relevant. + +
+
+

+ {"Hybrid retrieval"} +

+

+ {"Three lanes, one ranking"} +

+
+ +
+
+
+

{"lexical"}

+

{"sqlite fts5 / bm25"}

+

+ {"Matches stems and phrases. Strong on the exact terms the team already uses."} +

+

+ {"weight "}{"1.5"} +

+
+ +
+

{"semantic"}

+

{"cosine over embeddings"}

+

+ {"Catches synonyms and paraphrases the lexical lane misses."} +

+

+ {"weight "}{"2"} +

+
+ +
+

{"token"}

+

{"term-overlap fallback"}

+

+ {"Always available, so short queries still produce candidates."} +

+

+ {"weight "}{"0.75"} +

+
+
+ +
+

+ {"Reciprocal Rank Fusion"} +

+

+ {"score = Σ weight / (60 + rank)"} +

+

+ {"Pages that rank well in multiple lanes outscore pages that rank well in only one."} +

+
+
+ +
+ {"Defaults are tunable. "} + {"Lane weights and the RRF constant K are configuration, not assumptions."} +
+
+ +The text each lane scores is built deterministically: page key, summary, +body, and tags concatenated in that order. A precise summary and the +right tags make a page reachable before its body matches anything. + +## The page graph + +Two frontmatter fields and one inline syntax turn the wiki into a graph +the agent traverses without re-running search. + +| Edge | Source | Target | +|------|--------|--------| +| `sl_refs: [warehouse.orders]` | Frontmatter | Semantic source by name | +| `refs: [segment-classification]` | Frontmatter | Another wiki page by key | +| `[[segment-classification]]` | Inline in body | Another wiki page by key | + +`refs` stays in the prose layer; `sl_refs` crosses into the executable +half of the context layer. Inline `[[wikilinks]]` are extracted from +page bodies at validation time and treated as declared `refs`. + +
+
+

+ {"Anatomy of a traversal"} +

+

+ {"Edges to prose, edges to SQL"} +

+
+ +
+
+
+

+ {"wiki/global/revenue.md"} +

+

{"revenue"}

+

+ {"declares"} +

+
    +
  • {"sl_refs"}: warehouse.orders
  • +
  • {"refs"}: segment-classification
  • +
+
+
+

+ {"wiki/global/segment-classification.md"} +

+

{"segment-classification"}

+

+ {"declares"} +

+
    +
  • {"sl_refs"}: warehouse.customers
  • +
+
+
+ +
+
{"revenue → warehouse.orders · sl_refs"}
+
{"revenue → segment-classification · refs"}
+
+ +
+
+

+ {"semantic-layer/warehouse/orders.yaml"} +

+

{"warehouse.orders"}

+

{"grain: order_id · measure: total_revenue"}

+
+
+

+ {"semantic-layer/warehouse/customers.yaml"} +

+

{"warehouse.customers"}

+

{"grain: customer_id · dim: segment"}

+
+
+
+ +
+ {"Green nodes are wiki pages; blue nodes are semantic sources."} +
+
+ +## Keeping the graph live + +A page that references a deleted source is worse than no reference at +all - it sends the agent confidently to a definition that no longer +exists. **ktx** prevents that with three layered checks: + +- **At write time.** Every `refs` entry and `[[wikilink]]` is validated + against the pages visible in the current scope. A write that targets + a missing page is rejected before any file changes. +- **At ingest time.** Adapters prune `sl_refs` when the target source + is deleted, mark stale pattern pages with `stale_since`, and set + `archived_since` on retired pages instead of removing them silently. +- **At session end.** Every page touched by an ingest run is re-scanned + for references that resolved at write time but no longer point at + a live target. Dangling pairs are reported so the next iteration can + fix them. + +## Where the pages come from + +**ktx** writes wiki pages from evidence, not free invention. Each input +contributes a different kind of page, and accepted edits feed the next +ingest as input. + +| Evidence | What it produces | +|----------|------------------| +| Schema scans | One page per material table, with grain, columns, and known constraints | +| Query history | Pattern pages with `usage` frontmatter for executions, distinct users, runtime percentiles, and error rate | +| dbt manifests | Pages per model, exposure, and test, with `sl_refs` to the matching semantic source | +| MetricFlow, Looker, Metabase | Pages per metric, explore, or saved question, linked back to the source artifact | +| Notion, docs, analyst notes | Pages preserving business definitions, policies, and incident write-ups | +| Agent and analyst edits | First-class input to the next ingest, not a fork | + +Provenance stays with the page. Ingested pages keep HTML comments like +`` inline, so a reviewer can +walk from the prose back to the artifact that produced it. + +## Agent usage notes + +Point an agent at this page when it needs to explain why a wiki search +returned the pages it did, why a write was rejected, or how the wiki +stays in step with the semantic layer. + +| Agent task | Relevant section | Next page | +|------------|------------------|-----------| +| Explain why two searches return different pages for the same query | What retrieval does | [ktx wiki](/docs/cli-reference/ktx-wiki) | +| Decide whether to add a `refs` or `sl_refs` entry | The page graph | [Writing Context](/docs/guides/writing-context) | +| Repair a wiki write rejected for missing references | Keeping the graph live | [Writing Context](/docs/guides/writing-context) | +| Describe how historic SQL becomes a wiki page | Where the pages come from | [Building Context](/docs/guides/building-context) | +| Explain raw-source provenance comments | Where the pages come from | [Context as Code](/docs/concepts/context-as-code) | diff --git a/packages/cli/src/admin-reindex.test.ts b/packages/cli/src/admin-reindex.test.ts index 54ac5e7a..0f14486a 100644 --- a/packages/cli/src/admin-reindex.test.ts +++ b/packages/cli/src/admin-reindex.test.ts @@ -1,8 +1,13 @@ +import { createRequire } from 'node:module'; + import type { ReindexSummary } from '@ktx/context/index-sync'; import { describe, expect, it, vi } from 'vitest'; import { renderReindexJson, renderReindexPlain, reindexHasErrors } from './admin-reindex.js'; import { runKtxCli } from './index.js'; +const cliVersion = (createRequire(import.meta.url)('@ktx/cli/package.json') as { version: string }) + .version; + function makeIo(options: { stdoutIsTTY?: boolean } = {}) { let stdout = ''; let stderr = ''; @@ -137,7 +142,7 @@ describe('admin reindex Commander routing', () => { force: true, json: true, output: 'plain', - cliVersion: '0.0.0-private', + cliVersion, }, io.io, ); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 53a7b593..41cdaf39 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -19,6 +19,9 @@ import { const require = createRequire(import.meta.url); +const cliPackageJson = require('@ktx/cli/package.json') as { name: string; version: string }; +const cliVersion = cliPackageJson.version; + function makeIo(options: { stdoutIsTty?: boolean } = {}) { let stdout = ''; let stderr = ''; @@ -45,7 +48,7 @@ describe('getKtxCliPackageInfo', () => { it('identifies the CLI package and its context dependency', () => { expect(getKtxCliPackageInfo()).toEqual({ name: '@ktx/cli', - version: '0.0.0-private', + version: cliVersion, contextPackageName: '@ktx/context', }); }); @@ -55,8 +58,9 @@ describe('getKtxCliPackageInfo', () => { expect(packageJson).toMatchObject({ name: '@ktx/cli', - version: '0.0.0-private', + version: cliVersion, }); + expect(cliVersion).toMatch(/^\d+\.\d+\.\d+/); }); it('normalizes public package metadata from package.json contents', () => { @@ -114,7 +118,7 @@ describe('runKtxCli', () => { await expect(runKtxCli(['--version'], testIo.io)).resolves.toBe(0); - expect(testIo.stdout()).toBe('@ktx/cli 0.0.0-private\n'); + expect(testIo.stdout()).toBe(`@ktx/cli ${cliVersion}\n`); expect(testIo.stderr()).toBe(''); }); @@ -304,7 +308,7 @@ describe('runKtxCli', () => { 1, { command: 'install', - cliVersion: '0.0.0-private', + cliVersion, feature: 'local-embeddings', force: true, }, @@ -314,7 +318,7 @@ describe('runKtxCli', () => { 2, { command: 'start', - cliVersion: '0.0.0-private', + cliVersion, projectDir: expect.any(String), feature: 'local-embeddings', force: true, @@ -325,7 +329,7 @@ describe('runKtxCli', () => { 3, { command: 'stop', - cliVersion: '0.0.0-private', + cliVersion, projectDir: expect.any(String), all: false, }, @@ -335,7 +339,7 @@ describe('runKtxCli', () => { 4, { command: 'stop', - cliVersion: '0.0.0-private', + cliVersion, projectDir: expect.any(String), all: true, }, @@ -345,7 +349,7 @@ describe('runKtxCli', () => { 5, { command: 'status', - cliVersion: '0.0.0-private', + cliVersion, json: true, }, statusIo.io, @@ -418,7 +422,7 @@ describe('runKtxCli', () => { expect.objectContaining({ command: 'query', projectDir: tempDir, - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'prompt', query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }), }), @@ -433,7 +437,7 @@ describe('runKtxCli', () => { ).resolves.toBe(0); expect(sl).toHaveBeenLastCalledWith( expect.objectContaining({ - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'auto', }), autoIo.io, @@ -449,7 +453,7 @@ describe('runKtxCli', () => { ).resolves.toBe(0); expect(sl).toHaveBeenLastCalledWith( expect.objectContaining({ - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'never', }), noInputIo.io, @@ -585,7 +589,7 @@ describe('runKtxCli', () => { skipAgents: false, inputMode: 'auto', yes: false, - cliVersion: '0.0.0-private', + cliVersion, skipLlm: false, skipEmbeddings: false, databaseSchemas: [], @@ -715,7 +719,7 @@ describe('runKtxCli', () => { inputMode: 'disabled', depth: 'fast', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'never', }, testIo.io, @@ -742,7 +746,7 @@ describe('runKtxCli', () => { inputMode: 'auto', depth: 'deep', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'prompt', }, testIo.io, @@ -819,7 +823,7 @@ describe('runKtxCli', () => { json: false, inputMode: 'disabled', queryHistory: 'default', - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'never', }, testIo.io, @@ -1124,7 +1128,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion, anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret llmModel: 'claude-sonnet-4-6', skipLlm: false, @@ -1163,7 +1167,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion, llmBackend: 'vertex', vertexProject: 'local-gcp-project', vertexLocation: 'us-east5', @@ -1200,7 +1204,7 @@ describe('runKtxCli', () => { command: 'run', projectDir: tempDir, inputMode: 'disabled', - cliVersion: '0.0.0-private', + cliVersion, llmBackend: 'claude-code', llmModel: 'opus', skipLlm: false, @@ -1308,7 +1312,7 @@ describe('runKtxCli', () => { projectDir: '/tmp/project', inputMode: 'disabled', yes: true, - cliVersion: '0.0.0-private', + cliVersion, skipLlm: true, skipEmbeddings: true, databaseDrivers: ['postgres'], @@ -1649,7 +1653,7 @@ describe('runKtxCli', () => { queryFile: '/tmp/query.json', execute: false, format: 'json', - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'auto', }, autoIo.io, @@ -1663,7 +1667,7 @@ describe('runKtxCli', () => { queryFile: '/tmp/query.json', execute: false, format: 'json', - cliVersion: '0.0.0-private', + cliVersion, runtimeInstallPolicy: 'never', }, neverIo.io, diff --git a/scripts/build-public-npm-package.test.mjs b/scripts/build-public-npm-package.test.mjs index c78ae164..b69d7437 100644 --- a/scripts/build-public-npm-package.test.mjs +++ b/scripts/build-public-npm-package.test.mjs @@ -139,12 +139,15 @@ async function writeWorkspaceFixture(root) { } describe('publicNpmPackageLayout', () => { - it('uses the first public npm release version for the tarball name', () => { + it('uses the public npm release version for the tarball name', () => { const layout = publicNpmPackageLayout('/repo/ktx'); - assert.equal(PUBLIC_NPM_PACKAGE_VERSION, '0.1.0-rc.1'); - assert.equal(publicNpmPackageTarballName(), 'kaelio-ktx-0.1.0-rc.1.tgz'); - assert.equal(layout.tarballPath, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0-rc.1.tgz'); + assert.match(PUBLIC_NPM_PACKAGE_VERSION, /^\d+\.\d+\.\d+/); + assert.equal(publicNpmPackageTarballName(), `kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`); + assert.equal( + layout.tarballPath, + `/repo/ktx/dist/artifacts/npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, + ); }); }); @@ -211,7 +214,7 @@ describe('publicNpmPackageJson', () => { ); assert.equal(packageJson.name, PUBLIC_NPM_PACKAGE_NAME); - assert.equal(packageJson.version, '0.1.0-rc.1'); + assert.equal(packageJson.version, PUBLIC_NPM_PACKAGE_VERSION); assert.equal(packageJson.private, false); assert.deepEqual(packageJson.bin, { ktx: './dist/bin.js' }); assert.deepEqual(packageJson.dependencies, { commander: '14.0.3' }); @@ -275,7 +278,7 @@ describe('publicNpmPackCommand', () => { '--config.node-linker=hoisted', 'pack', '--out', - '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0-rc.1.tgz', + `/repo/ktx/dist/artifacts/npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, ], cwd: '/repo/ktx/dist/public-npm-package', }); diff --git a/scripts/build-python-runtime-wheel.test.mjs b/scripts/build-python-runtime-wheel.test.mjs index 7fc3923b..bf25af9a 100644 --- a/scripts/build-python-runtime-wheel.test.mjs +++ b/scripts/build-python-runtime-wheel.test.mjs @@ -52,7 +52,7 @@ describe('runtimeWheelPyproject', () => { const pyproject = runtimeWheelPyproject(); assert.match(pyproject, /name = "kaelio-ktx"/); - assert.match(pyproject, /version = "0\.1\.0rc1"/); + assert.match(pyproject, new RegExp(`version = "${RUNTIME_WHEEL_PACKAGE_VERSION.replace(/\./g, '\\.')}"`)); assert.match(pyproject, /ktx-daemon = "ktx_daemon\.__main__:main"/); assert.match(pyproject, /packages = \["semantic_layer", "ktx_daemon"\]/); assert.match(pyproject, /\[project\.optional-dependencies\]/); @@ -110,6 +110,6 @@ describe('runtimeWheelBuildCommand', () => { cwd: '/repo/ktx', }); assert.equal(RUNTIME_WHEEL_DISTRIBUTION_NAME, 'kaelio-ktx'); - assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0rc1'); + assert.match(RUNTIME_WHEEL_PACKAGE_VERSION, /^\d+\.\d+\.\d+/); }); }); diff --git a/scripts/local-embeddings-runtime-smoke.test.mjs b/scripts/local-embeddings-runtime-smoke.test.mjs index ca14e0df..3da1a83e 100644 --- a/scripts/local-embeddings-runtime-smoke.test.mjs +++ b/scripts/local-embeddings-runtime-smoke.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { describe, it } from 'node:test'; +import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs'; import { buildLocalEmbeddingsSmokeEnv, expectedPublicKtxVersionPattern, @@ -12,6 +13,9 @@ import { validateEmbeddingResponse, } from './local-embeddings-runtime-smoke.mjs'; +const PUBLIC_TARBALL_NAME = `kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`; +const OTHER_PUBLIC_TARBALL_NAME = 'kaelio-ktx-9.9.9.tgz'; + describe('localEmbeddingsSmokeOptIn', () => { it('skips unless the smoke is explicitly enabled', () => { assert.deepEqual(localEmbeddingsSmokeOptIn({}, []), { @@ -35,10 +39,7 @@ describe('localEmbeddingsSmokeOptIn', () => { describe('publicKtxTarballName', () => { it('selects the public @kaelio/ktx tarball name', () => { - assert.equal( - publicKtxTarballName(['kaelio-ktx-0.1.0-rc.1.tgz', 'ignore-me.tgz']), - 'kaelio-ktx-0.1.0-rc.1.tgz', - ); + assert.equal(publicKtxTarballName([PUBLIC_TARBALL_NAME, 'ignore-me.tgz']), PUBLIC_TARBALL_NAME); }); it('fails when the public package tarball is missing', () => { @@ -50,7 +51,7 @@ describe('publicKtxTarballName', () => { it('fails when multiple public package tarballs are present', () => { assert.throws( - () => publicKtxTarballName(['kaelio-ktx-0.1.0-rc.1.tgz', 'kaelio-ktx-0.2.0.tgz']), + () => publicKtxTarballName([PUBLIC_TARBALL_NAME, OTHER_PUBLIC_TARBALL_NAME]), /Expected exactly one @kaelio\/ktx tarball/, ); }); @@ -60,7 +61,7 @@ describe('expectedPublicKtxVersionPattern', () => { it('matches the public package version and rejects the private workspace version', () => { const pattern = expectedPublicKtxVersionPattern(); - assert.match('@kaelio/ktx 0.1.0-rc.1\n', pattern); + assert.match(`@kaelio/ktx ${PUBLIC_NPM_PACKAGE_VERSION}\n`, pattern); assert.doesNotMatch('@kaelio/ktx 0.0.0-private\n', pattern); }); }); diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 0104b836..ea078a23 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -72,6 +72,10 @@ async function writeReleaseMetadataInputs(root) { } } +function runtimeWheelFilename(version = RUNTIME_WHEEL_PACKAGE_VERSION) { + return `kaelio_ktx-${version}-py3-none-any.whl`; +} + async function writeUploadableArtifactFixtures(layout) { await mkdir(layout.npmDir, { recursive: true }); await mkdir(layout.pythonDir, { recursive: true }); @@ -82,7 +86,7 @@ async function writeUploadableArtifactFixtures(layout) { `${packageInfo.name}-tarball`, ]), [ - join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), + join(layout.pythonDir, runtimeWheelFilename()), 'kaelio-ktx-runtime-wheel', ], ]); @@ -99,7 +103,10 @@ describe('packageArtifactLayout', () => { assert.equal(layout.artifactDir, '/repo/ktx/dist/artifacts'); assert.equal(layout.npmDir, '/repo/ktx/dist/artifacts/npm'); assert.equal(layout.pythonDir, '/repo/ktx/dist/artifacts/python'); - assert.equal(layout.cliTarball, '/repo/ktx/dist/artifacts/npm/kaelio-ktx-0.1.0-rc.1.tgz'); + assert.equal( + layout.cliTarball, + `/repo/ktx/dist/artifacts/npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, + ); assert.deepEqual(Object.keys(layout.npmTarballs), ['@kaelio/ktx']); }); }); @@ -131,7 +138,7 @@ describe('packageReleaseMetadata', () => { ecosystem: 'npm', packageName: '@kaelio/ktx', packageRoot: 'packages/cli', - packageVersion: '0.1.0-rc.1', + packageVersion: PUBLIC_NPM_PACKAGE_VERSION, private: false, releaseMode: 'ci-artifact-only', }, @@ -139,7 +146,7 @@ describe('packageReleaseMetadata', () => { ecosystem: 'python', packageName: 'kaelio-ktx', packageRoot: 'python/runtime-wheel', - packageVersion: '0.1.0rc1', + packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION, private: false, releaseMode: 'ci-artifact-only', }, @@ -154,10 +161,10 @@ describe('findPythonArtifacts', () => { it('finds the bundled runtime wheel only', async () => { const root = await mkdtemp(join(tmpdir(), 'ktx-artifacts-test-')); try { - await writeFile(join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), ''); + await writeFile(join(root, runtimeWheelFilename()), ''); assert.deepEqual(await findPythonArtifacts(root), { - runtimeWheel: join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), + runtimeWheel: join(root, runtimeWheelFilename()), }); } finally { await rm(root, { recursive: true, force: true }); @@ -197,7 +204,7 @@ describe('artifact manifest', () => { ecosystem: 'npm', packageName: '@kaelio/ktx', packageRoot: 'packages/cli', - packageVersion: '0.1.0-rc.1', + packageVersion: PUBLIC_NPM_PACKAGE_VERSION, private: false, releaseMode: 'ci-artifact-only', }, @@ -210,7 +217,7 @@ describe('artifact manifest', () => { ecosystem: 'python', packageName: 'kaelio-ktx', packageRoot: 'python/runtime-wheel', - packageVersion: '0.1.0rc1', + packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION, private: false, releaseMode: 'ci-artifact-only', }, @@ -232,8 +239,8 @@ describe('artifact manifest', () => { artifactKind: 'tarball', ecosystem: 'npm', packageName: '@kaelio/ktx', - packageVersion: '0.1.0-rc.1', - path: 'npm/kaelio-ktx-0.1.0-rc.1.tgz', + packageVersion: PUBLIC_NPM_PACKAGE_VERSION, + path: `npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, }, ], ); @@ -252,13 +259,15 @@ describe('artifact manifest', () => { artifactKind: 'wheel', ecosystem: 'python', packageName: 'kaelio-ktx', - packageVersion: '0.1.0rc1', - path: 'python/kaelio_ktx-0.1.0rc1-py3-none-any.whl', + packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION, + path: `python/${runtimeWheelFilename()}`, }, ], ); - const npmEntry = manifest.files.find((file) => file.path === 'npm/kaelio-ktx-0.1.0-rc.1.tgz'); + const npmEntry = manifest.files.find( + (file) => file.path === `npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, + ); assert.ok(npmEntry); assert.equal(npmEntry.bytes, Buffer.byteLength('@kaelio/ktx-tarball')); assert.equal(npmEntry.sha256, createHash('sha256').update('@kaelio/ktx-tarball').digest('hex')); @@ -362,17 +371,17 @@ describe('copyRuntimeWheelAssets', () => { try { await mkdir(layout.pythonDir, { recursive: true }); await writeFile( - join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), + join(layout.pythonDir, runtimeWheelFilename()), 'kaelio-ktx-runtime-wheel', ); const assets = await copyRuntimeWheelAssets(layout, { - runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), + runtimeWheel: join(layout.pythonDir, runtimeWheelFilename()), }); assert.equal( assets.wheelPath, - join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), + join(root, 'packages', 'cli', 'assets', 'python', runtimeWheelFilename()), ); assert.equal( assets.manifestPath, @@ -385,7 +394,7 @@ describe('copyRuntimeWheelAssets', () => { normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME, version: RUNTIME_WHEEL_PACKAGE_VERSION, wheel: { - file: 'kaelio_ktx-0.1.0rc1-py3-none-any.whl', + file: runtimeWheelFilename(), sha256: createHash('sha256') .update('kaelio-ktx-runtime-wheel') .digest('hex'), diff --git a/scripts/release-readiness.test.mjs b/scripts/release-readiness.test.mjs index 84170761..820d72ea 100644 --- a/scripts/release-readiness.test.mjs +++ b/scripts/release-readiness.test.mjs @@ -11,6 +11,7 @@ import { writeArtifactManifest, } from './package-artifacts.mjs'; import { PUBLIC_NPM_PACKAGE_VERSION } from './build-public-npm-package.mjs'; +import { RUNTIME_WHEEL_PACKAGE_VERSION } from './build-python-runtime-wheel.mjs'; import { readReleasePolicy, releasePolicyPath, releaseReadinessReport } from './release-readiness.mjs'; async function writeJson(path, value) { @@ -37,7 +38,10 @@ async function writeUploadableArtifactFixtures(layout) { layout.npmTarballs[packageInfo.name], `${packageInfo.name}-tarball`, ]), - [join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'], + [ + join(layout.pythonDir, `kaelio_ktx-${RUNTIME_WHEEL_PACKAGE_VERSION}-py3-none-any.whl`), + 'kaelio-ktx-runtime-wheel', + ], ]); for (const [path, contents] of fileContents) {