fix: improve setup wizard behavior (#127)

* fix: improve setup wizard behavior

* fix: derive runtime versions from release metadata

* test: validate metabase source mapping requirements

* Fix boundary check release identifiers
This commit is contained in:
Andrey Avtomonov 2026-05-17 19:15:09 +02:00 committed by GitHub
parent 33a142f769
commit d1c84e5564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 671 additions and 90 deletions

View file

@ -6,11 +6,13 @@ import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
import { publicPythonRuntimePackageVersion } from './public-npm-release-metadata.mjs';
const execFileAsync = promisify(execFile);
export const RUNTIME_WHEEL_DISTRIBUTION_NAME = 'kaelio-ktx';
export const RUNTIME_WHEEL_NORMALIZED_NAME = 'kaelio_ktx';
export const RUNTIME_WHEEL_PACKAGE_VERSION = '0.1.0';
export const RUNTIME_WHEEL_PACKAGE_VERSION = publicPythonRuntimePackageVersion();
function scriptRootDir() {
return resolve(dirname(fileURLToPath(import.meta.url)), '..');

View file

@ -48,11 +48,11 @@ describe('runtimeWheelLayout', () => {
});
describe('runtimeWheelPyproject', () => {
it('describes one kaelio-ktx wheel with lazy local embeddings', () => {
it('describes one kaelio-ktx wheel with the release-derived Python version and lazy local embeddings', () => {
const pyproject = runtimeWheelPyproject();
assert.match(pyproject, /name = "kaelio-ktx"/);
assert.match(pyproject, /version = "0\.1\.0"/);
assert.match(pyproject, /version = "0\.1\.0rc1"/);
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.0');
assert.equal(RUNTIME_WHEEL_PACKAGE_VERSION, '0.1.0rc1');
});
});

View file

@ -8,7 +8,8 @@ const codeExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.
const runtimeAssetPatterns = [/^packages\/[^/]+\/prompts\/.+\.md$/, /^packages\/[^/]+\/skills\/.+\.md$/];
const identifierSkipPrefixes = ['docs/', 'docs-site/', 'examples/', 'python/ktx-sl/plans/', 'python/ktx-sl/openspec/'];
const identifierAllowPatterns = [
/^packages\/cli\/src\/(?:index|managed-local-embeddings|managed-python-command|managed-python-daemon|managed-python-runtime|runtime)(?:\.test)?\.ts$/,
/^packages\/cli\/src\/(?:index|managed-local-embeddings|managed-python-command|managed-python-daemon|managed-python-runtime|release-version|runtime)(?:\.test)?\.ts$/,
/^python\/ktx-daemon\/src\/ktx_daemon\/__init__\.py$/,
/^scripts\/(?:build-public-npm-package|build-python-runtime-wheel|local-embeddings-runtime-smoke|package-artifacts|public-npm-release-metadata|publish-public-npm-package|published-package-smoke|release-readiness)(?:\.test)?\.mjs$/,
];
const forbiddenIdentifierTerms = ['kae' + 'lio', 'Kae' + 'lio', 'KAE' + 'LIO_'];
@ -87,7 +88,10 @@ function scansForLlmBoundaries(relativePath) {
}
function isTestSource(relativePath) {
return /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativePath);
return (
/(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativePath) ||
/(?:^|\/)tests\/(?:.+\/)?(?:test_[^/]+|[^/]+_test)\.py$/.test(relativePath)
);
}
function scansForContextProductionLlmBoundaries(relativePath) {

View file

@ -70,6 +70,7 @@ describe('scanFileContent', () => {
assert.equal(scanFileContent('packages/cli/src/setup.test.ts', `project: ${name}-dev`).length, 0);
assert.equal(scanFileContent('packages/context/src/ingest/importer.test.ts', `email: system@${name}.dev`).length, 0);
assert.equal(scanFileContent('python/ktx-daemon/tests/test_package.py', `${name}-ktx`).length, 0);
});
it('allows public package identifiers in release packaging and managed runtime source', () => {
@ -79,7 +80,9 @@ describe('scanFileContent', () => {
assert.equal(scanFileContent('scripts/package-artifacts.test.mjs', `${name}-ktx`).length, 0);
assert.equal(scanFileContent('scripts/public-npm-release-metadata.mjs', `@${name}/ktx`).length, 0);
assert.equal(scanFileContent('scripts/publish-public-npm-package.test.mjs', `@${name}/ktx`).length, 0);
assert.equal(scanFileContent('packages/cli/src/release-version.ts', `@${name}/ktx`).length, 0);
assert.equal(scanFileContent('packages/cli/src/managed-python-runtime.ts', `${name}_ktx`).length, 0);
assert.equal(scanFileContent('python/ktx-daemon/src/ktx_daemon/__init__.py', `${name}-ktx`).length, 0);
});
it('allows clean source files and clean runtime prompt assets', () => {

View file

@ -82,7 +82,7 @@ async function writeUploadableArtifactFixtures(layout) {
`${packageInfo.name}-tarball`,
]),
[
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
'kaelio-ktx-runtime-wheel',
],
]);
@ -139,7 +139,7 @@ describe('packageReleaseMetadata', () => {
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageRoot: 'python/runtime-wheel',
packageVersion: '0.1.0',
packageVersion: '0.1.0rc1',
private: false,
releaseMode: 'ci-artifact-only',
},
@ -154,10 +154,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.0-py3-none-any.whl'), '');
await writeFile(join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), '');
assert.deepEqual(await findPythonArtifacts(root), {
runtimeWheel: join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
runtimeWheel: join(root, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
});
} finally {
await rm(root, { recursive: true, force: true });
@ -210,7 +210,7 @@ describe('artifact manifest', () => {
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageRoot: 'python/runtime-wheel',
packageVersion: '0.1.0',
packageVersion: '0.1.0rc1',
private: false,
releaseMode: 'ci-artifact-only',
},
@ -252,8 +252,8 @@ describe('artifact manifest', () => {
artifactKind: 'wheel',
ecosystem: 'python',
packageName: 'kaelio-ktx',
packageVersion: '0.1.0',
path: 'python/kaelio_ktx-0.1.0-py3-none-any.whl',
packageVersion: '0.1.0rc1',
path: 'python/kaelio_ktx-0.1.0rc1-py3-none-any.whl',
},
],
);
@ -362,17 +362,17 @@ describe('copyRuntimeWheelAssets', () => {
try {
await mkdir(layout.pythonDir, { recursive: true });
await writeFile(
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
'kaelio-ktx-runtime-wheel',
);
const assets = await copyRuntimeWheelAssets(layout, {
runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
runtimeWheel: join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
});
assert.equal(
assets.wheelPath,
join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0-py3-none-any.whl'),
join(root, 'packages', 'cli', 'assets', 'python', 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'),
);
assert.equal(
assets.manifestPath,
@ -385,7 +385,7 @@ describe('copyRuntimeWheelAssets', () => {
normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME,
version: RUNTIME_WHEEL_PACKAGE_VERSION,
wheel: {
file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
file: 'kaelio_ktx-0.1.0rc1-py3-none-any.whl',
sha256: createHash('sha256')
.update('kaelio-ktx-runtime-wheel')
.digest('hex'),

View file

@ -9,6 +9,8 @@ export const PUBLIC_NPM_RELEASE_TAGS = new Set(['latest', 'next']);
const SEMVER_PATTERN =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
const SEMVER_PARTS_PATTERN =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
function scriptRootDir() {
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
@ -29,6 +31,30 @@ export function assertPublicNpmPackageVersion(version) {
return version;
}
export function publicNpmPackageVersionToPythonVersion(version) {
const safeVersion = assertPublicNpmPackageVersion(version);
const match = SEMVER_PARTS_PATTERN.exec(safeVersion);
if (!match) {
throw new Error(`Invalid public npm package version: ${version}`);
}
const [, major, minor, patch, prerelease, buildMetadata] = match;
if (buildMetadata) {
throw new Error(`Unsupported public npm build metadata for Python runtime version: ${safeVersion}`);
}
const baseVersion = `${major}.${minor}.${patch}`;
if (!prerelease) {
return baseVersion;
}
const rcMatch = /^rc\.([1-9]\d*|0)$/.exec(prerelease);
if (!rcMatch) {
throw new Error(`Unsupported public npm prerelease for Python runtime version: ${safeVersion}`);
}
return `${baseVersion}rc${rcMatch[1]}`;
}
export function assertPublicNpmReleaseTag(tag) {
if (!PUBLIC_NPM_RELEASE_TAGS.has(tag)) {
throw new Error(`Invalid public npm release tag: ${tag}`);
@ -51,3 +77,7 @@ export function readPublicNpmReleaseMetadata(rootDir = scriptRootDir()) {
export function publicNpmPackageVersion(rootDir = scriptRootDir()) {
return readPublicNpmReleaseMetadata(rootDir).version;
}
export function publicPythonRuntimePackageVersion(rootDir = scriptRootDir()) {
return publicNpmPackageVersionToPythonVersion(publicNpmPackageVersion(rootDir));
}

View file

@ -0,0 +1,26 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { publicNpmPackageVersionToPythonVersion } from './public-npm-release-metadata.mjs';
describe('publicNpmPackageVersionToPythonVersion', () => {
it('keeps stable public npm versions unchanged for Python wheels', () => {
assert.equal(publicNpmPackageVersionToPythonVersion('1.2.3'), '1.2.3');
});
it('converts semantic-release rc versions to PEP 440 rc versions', () => {
assert.equal(publicNpmPackageVersionToPythonVersion('0.1.0-rc.1'), '0.1.0rc1');
assert.equal(publicNpmPackageVersionToPythonVersion('2.0.0-rc.12'), '2.0.0rc12');
});
it('rejects unsupported prerelease and build metadata forms', () => {
assert.throws(
() => publicNpmPackageVersionToPythonVersion('1.2.3-beta.1'),
/Unsupported public npm prerelease for Python runtime version/,
);
assert.throws(
() => publicNpmPackageVersionToPythonVersion('1.2.3+build.1'),
/Unsupported public npm build metadata for Python runtime version/,
);
});
});

View file

@ -37,7 +37,7 @@ async function writeUploadableArtifactFixtures(layout) {
layout.npmTarballs[packageInfo.name],
`${packageInfo.name}-tarball`,
]),
[join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
[join(layout.pythonDir, 'kaelio_ktx-0.1.0rc1-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
]);
for (const [path, contents] of fileContents) {