mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
feat: npm-managed Python runtime for @kaelio/ktx (#7)
* docs: add npm managed python runtime design * build: add bundled python runtime wheel builder * build: make local embedding dependencies optional * build: bundle python runtime wheel in cli artifacts * build: track bundled python runtime release artifact * test: verify bundled python runtime wheel * docs: add plan for bundled python runtime wheel * test: cover managed python runtime lifecycle * feat: add managed python runtime installer * feat: add runtime command runner * feat: expose runtime management commands * test: verify managed python runtime commands * docs: add plan for managed python runtime installer * feat: add managed python command helper * feat: use managed runtime for sl query compute * feat: route sl query managed runtime policy * docs: add plan for managed runtime sl query integration * feat: add managed runtime daemon metadata * feat: manage python daemon lifecycle * feat: add runtime daemon start stop commands * fix: verify managed runtime daemon lifecycle * docs: add plan for managed runtime daemon lifecycle * feat: add managed local embeddings config marker * feat: add managed local embeddings daemon helper * feat: use managed runtime for local embedding setup * feat: pass managed runtime policy through setup * docs: add plan for managed local embeddings runtime * feat: read CLI package metadata dynamically * feat: assemble public kaelio ktx npm package * feat: release one public kaelio ktx npm artifact * test: cover public kaelio ktx package invocations * chore: verify public kaelio ktx package artifacts * docs: add plan for public kaelio ktx npm package * test: verify managed runtime in public package smoke * test: finalize managed runtime release smoke * docs: add plan for managed runtime release smoke * test: specify local embeddings release smoke * feat: add local embeddings runtime smoke * chore: register local embeddings smoke * fix: verify local embeddings smoke * fix: restore artifact smoke python env helper * docs: add plan for managed local embeddings release smoke * refactor: share managed runtime install policy parsing * feat: use managed runtime for agent semantic queries * feat: use managed runtime for MCP semantic compute * docs: add plan for managed agent and MCP semantic runtime * feat(cli): add managed daemon HTTP helpers * feat(cli): route local adapters through managed daemon * feat(cli): use managed daemon for ingest helpers * feat(cli): pass managed daemon options to scan * feat(context): pass MCP ingest pull config options * feat(cli): pass managed daemon options to serve ingest * test: verify managed local ingest daemon runtime * docs: add plan for managed local ingest daemon runtime * docs: align managed runtime examples * docs: add plan for managed runtime docs cleanup * test: cover published package runtime smoke commands * test: validate published package smoke outputs * docs: add plan for published package runtime smoke * build: stamp public npm package version * release: add npm public release policy * release: add guarded npm publish script * release: document public npm release handoff * docs: add plan for public npm release handoff * test: cover managed runtime prune in package smoke * docs: document managed runtime prune * docs: add plan for managed runtime prune smoke and docs * chore: encode uv runtime prerequisite policy * fix: clarify missing uv runtime error * docs: document uv runtime prerequisite * docs: add plan for uv runtime prerequisite contract * refactor: limit release artifacts to public package runtime * chore: align release policy with bundled runtime wheel * docs: describe single public runtime artifact surface * test: verify single public runtime artifact contract * docs: add plan for single public runtime artifact cleanup * fix: align local embeddings smoke with public version * docs: add plan for local embeddings smoke public version * release: soft-launch as @kaelio/ktx@0.1.0-rc.0 on next tag Publish target moves to the pre-release version 0.1.0-rc.0 under the next dist-tag so npm install @kaelio/ktx (which resolves to latest) does not pick up the soft-launch build. Users opt in via @kaelio/ktx@next. * Fix release script boundary checks * Remove PostHog from public package bundle
This commit is contained in:
parent
075764fe77
commit
9dad936ac7
99 changed files with 25375 additions and 1538 deletions
1144
docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md
Normal file
1144
docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,856 @@
|
|||
# Managed Local Embeddings Release Smoke Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add an opt-in release smoke that proves the public `@kaelio/ktx`
|
||||
package can install `local-embeddings`, start the managed daemon, compute a real
|
||||
local embedding, and persist the managed embedding marker through setup.
|
||||
|
||||
**Architecture:** Keep the default `artifacts:verify` path lightweight. Add a
|
||||
separate Node smoke script with an explicit opt-in gate, source-level tests, and
|
||||
a package script that a release job can run only when large Python and model
|
||||
downloads are acceptable.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv, KTX managed Python
|
||||
runtime assets, FastAPI embedding endpoint, sentence-transformers.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plans are based on that spec and are already implemented in this
|
||||
worktree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `scripts/build-python-runtime-wheel.mjs` and matching tests.
|
||||
- `packages/cli/src/managed-python-runtime.ts`, `runtime.ts`, and
|
||||
`commands/runtime-commands.ts`.
|
||||
- `packages/cli/src/managed-python-command.ts` and `ktx sl query` runtime
|
||||
install policy flags.
|
||||
- `packages/cli/src/managed-python-daemon.ts` and `ktx runtime start` /
|
||||
`ktx runtime stop`.
|
||||
- `packages/cli/src/managed-local-embeddings.ts`,
|
||||
`packages/context/src/llm/local-config.ts`, and setup embedding wiring.
|
||||
- `scripts/build-public-npm-package.mjs`, `release-policy.json` listing
|
||||
`@kaelio/ktx`, and public-package smoke command construction.
|
||||
- `scripts/package-artifacts.mjs` installed CLI smoke that isolates
|
||||
`KTX_RUNTIME_ROOT`, lazily installs the core runtime, runs `ktx sl query`,
|
||||
checks runtime status and doctor output, and starts, reuses, and stops the
|
||||
core daemon.
|
||||
|
||||
The remaining spec gap is the release-check item that permits local embeddings
|
||||
coverage in a separate job or opt-in check. The default release artifact smoke
|
||||
must not download `sentence-transformers`, `torch`, or the
|
||||
`all-MiniLM-L6-v2` model.
|
||||
|
||||
## File structure
|
||||
|
||||
- Create `scripts/local-embeddings-runtime-smoke.mjs`: an opt-in smoke script
|
||||
that consumes the built public npm tarball, installs it in a temporary pnpm
|
||||
project, isolates all runtime and model caches, installs the
|
||||
`local-embeddings` feature, starts the managed daemon, computes one real
|
||||
embedding, runs setup with local embeddings, verifies the managed config
|
||||
marker, and stops the daemon.
|
||||
- Create `scripts/local-embeddings-runtime-smoke.test.mjs`: fast source-level
|
||||
tests for opt-in gating, public tarball selection, cache isolation, command
|
||||
construction, daemon URL parsing, embedding response validation, and package
|
||||
script registration.
|
||||
- Modify `package.json`: add `release:local-embeddings-smoke` without adding
|
||||
it to default `check`, `test`, `artifacts:verify`, or release readiness.
|
||||
|
||||
### Task 1: Add failing local embeddings smoke tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test file**
|
||||
|
||||
Create `scripts/local-embeddings-runtime-smoke.test.mjs` with this content:
|
||||
|
||||
```javascript
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
buildLocalEmbeddingsSmokeEnv,
|
||||
localEmbeddingsSmokeCommands,
|
||||
localEmbeddingsSmokeOptIn,
|
||||
parseDaemonBaseUrl,
|
||||
publicKtxTarballName,
|
||||
validateEmbeddingResponse,
|
||||
} from './local-embeddings-runtime-smoke.mjs';
|
||||
|
||||
describe('localEmbeddingsSmokeOptIn', () => {
|
||||
it('skips unless the smoke is explicitly enabled', () => {
|
||||
assert.deepEqual(localEmbeddingsSmokeOptIn({}, []), {
|
||||
run: false,
|
||||
message: 'Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.',
|
||||
});
|
||||
});
|
||||
|
||||
it('runs when the environment opt-in is set', () => {
|
||||
assert.deepEqual(localEmbeddingsSmokeOptIn({ KTX_RUN_LOCAL_EMBEDDINGS_SMOKE: '1' }, []), {
|
||||
run: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('runs when --force is present', () => {
|
||||
assert.deepEqual(localEmbeddingsSmokeOptIn({}, ['--force']), {
|
||||
run: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('publicKtxTarballName', () => {
|
||||
it('selects the public @kaelio/ktx tarball name', () => {
|
||||
assert.equal(
|
||||
publicKtxTarballName(['kaelio-ktx-0.0.0-private.tgz', 'ignore-me.tgz']),
|
||||
'kaelio-ktx-0.0.0-private.tgz',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when the public package tarball is missing', () => {
|
||||
assert.throws(
|
||||
() => publicKtxTarballName(['ktx-cli-0.0.0-private.tgz']),
|
||||
/Expected exactly one @kaelio\/ktx tarball/,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when multiple public package tarballs are present', () => {
|
||||
assert.throws(
|
||||
() => publicKtxTarballName(['kaelio-ktx-0.1.0.tgz', 'kaelio-ktx-0.2.0.tgz']),
|
||||
/Expected exactly one @kaelio\/ktx tarball/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildLocalEmbeddingsSmokeEnv', () => {
|
||||
it('isolates the runtime root and model caches inside the smoke root', () => {
|
||||
const env = buildLocalEmbeddingsSmokeEnv('/tmp/ktx-local-embedding-smoke', {
|
||||
PATH: '/usr/bin',
|
||||
});
|
||||
|
||||
assert.equal(env.PATH, '/usr/bin');
|
||||
assert.equal(env.KTX_RUN_LOCAL_EMBEDDINGS_SMOKE, '1');
|
||||
assert.equal(env.KTX_RUNTIME_ROOT, '/tmp/ktx-local-embedding-smoke/managed-runtime');
|
||||
assert.equal(env.HF_HOME, '/tmp/ktx-local-embedding-smoke/hf-home');
|
||||
assert.equal(env.TRANSFORMERS_CACHE, '/tmp/ktx-local-embedding-smoke/transformers-cache');
|
||||
assert.equal(env.SENTENCE_TRANSFORMERS_HOME, '/tmp/ktx-local-embedding-smoke/sentence-transformers-home');
|
||||
assert.equal(env.TORCH_HOME, '/tmp/ktx-local-embedding-smoke/torch-home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('localEmbeddingsSmokeCommands', () => {
|
||||
it('describes the installed-package commands needed for the smoke', () => {
|
||||
const commands = localEmbeddingsSmokeCommands({
|
||||
projectDir: '/tmp/ktx-local-embedding-smoke/project',
|
||||
});
|
||||
|
||||
assert.deepEqual(commands.map((command) => command.label), [
|
||||
'ktx public package version',
|
||||
'ktx runtime status missing',
|
||||
'ktx runtime install local embeddings',
|
||||
'ktx runtime status local embeddings ready',
|
||||
'ktx runtime start local embeddings',
|
||||
'ktx setup local embeddings',
|
||||
'ktx runtime stop local embeddings',
|
||||
]);
|
||||
assert.deepEqual(commands[2], {
|
||||
label: 'ktx runtime install local embeddings',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
|
||||
timeoutMs: 1_200_000,
|
||||
});
|
||||
assert.deepEqual(commands[4], {
|
||||
label: 'ktx runtime start local embeddings',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
|
||||
timeoutMs: 300_000,
|
||||
});
|
||||
assert.deepEqual(commands[5].args, [
|
||||
'exec',
|
||||
'ktx',
|
||||
'setup',
|
||||
'--project-dir',
|
||||
'/tmp/ktx-local-embedding-smoke/project',
|
||||
'--new',
|
||||
'--no-input',
|
||||
'--yes',
|
||||
'--skip-llm',
|
||||
'--embedding-backend',
|
||||
'sentence-transformers',
|
||||
'--skip-databases',
|
||||
'--skip-sources',
|
||||
'--skip-agents',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDaemonBaseUrl', () => {
|
||||
it('extracts the daemon URL from runtime start output', () => {
|
||||
assert.equal(
|
||||
parseDaemonBaseUrl('Started KTX Python daemon\nurl: http://127.0.0.1:61234\nfeatures: local-embeddings\n'),
|
||||
'http://127.0.0.1:61234',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects output without a daemon URL', () => {
|
||||
assert.throws(() => parseDaemonBaseUrl('Started KTX Python daemon\n'), /Daemon URL was not printed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEmbeddingResponse', () => {
|
||||
it('accepts a finite embedding vector with the expected dimensions', () => {
|
||||
validateEmbeddingResponse({ embedding: [0.1, -0.2, 0.3] }, 3);
|
||||
});
|
||||
|
||||
it('rejects a vector with the wrong dimensions', () => {
|
||||
assert.throws(
|
||||
() => validateEmbeddingResponse({ embedding: [0.1, 0.2] }, 3),
|
||||
/Expected embedding dimension 3, got 2/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects non-finite embedding values', () => {
|
||||
assert.throws(
|
||||
() => validateEmbeddingResponse({ embedding: [0.1, Number.NaN, 0.3] }, 3),
|
||||
/Embedding value at index 1 is not a finite number/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('package script', () => {
|
||||
it('registers the opt-in local embeddings smoke command', async () => {
|
||||
const packageJson = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
||||
|
||||
assert.equal(
|
||||
packageJson.scripts['release:local-embeddings-smoke'],
|
||||
'node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in',
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL with an import error for
|
||||
`./local-embeddings-runtime-smoke.mjs`.
|
||||
|
||||
- [ ] **Step 3: Commit the failing tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
git commit -m "test: specify local embeddings release smoke"
|
||||
```
|
||||
|
||||
### Task 2: Implement the opt-in smoke script
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `scripts/local-embeddings-runtime-smoke.mjs`
|
||||
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Create the smoke script**
|
||||
|
||||
Create `scripts/local-embeddings-runtime-smoke.mjs` with this content:
|
||||
|
||||
```javascript
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_ROOT_DIR = resolve(SCRIPT_DIR, '..');
|
||||
const PUBLIC_NPM_ARTIFACT_DIR = join('dist', 'artifacts', 'npm');
|
||||
const OPT_IN_MESSAGE =
|
||||
'Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.';
|
||||
|
||||
export function localEmbeddingsSmokeOptIn(env = process.env, args = process.argv.slice(2)) {
|
||||
if (env.KTX_RUN_LOCAL_EMBEDDINGS_SMOKE === '1' || args.includes('--force')) {
|
||||
return { run: true };
|
||||
}
|
||||
return { run: false, message: OPT_IN_MESSAGE };
|
||||
}
|
||||
|
||||
export function publicKtxTarballName(files) {
|
||||
const matches = files.filter((file) => /^kaelio-ktx-.+\.tgz$/.test(file)).sort();
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one @kaelio/ktx tarball in ${PUBLIC_NPM_ARTIFACT_DIR}, found ${matches.length}: ${
|
||||
matches.join(', ') || 'none'
|
||||
}. Run pnpm run artifacts:build first.`,
|
||||
);
|
||||
}
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
export async function selectPublicKtxTarball(rootDir = DEFAULT_ROOT_DIR) {
|
||||
const npmArtifactDir = join(rootDir, PUBLIC_NPM_ARTIFACT_DIR);
|
||||
const files = await readdir(npmArtifactDir);
|
||||
return join(npmArtifactDir, publicKtxTarballName(files));
|
||||
}
|
||||
|
||||
export function buildLocalEmbeddingsSmokeEnv(root, baseEnv = process.env) {
|
||||
return {
|
||||
...baseEnv,
|
||||
KTX_RUN_LOCAL_EMBEDDINGS_SMOKE: '1',
|
||||
KTX_RUNTIME_ROOT: join(root, 'managed-runtime'),
|
||||
HF_HOME: join(root, 'hf-home'),
|
||||
TRANSFORMERS_CACHE: join(root, 'transformers-cache'),
|
||||
SENTENCE_TRANSFORMERS_HOME: join(root, 'sentence-transformers-home'),
|
||||
TORCH_HOME: join(root, 'torch-home'),
|
||||
};
|
||||
}
|
||||
|
||||
export function localEmbeddingsSmokeCommands(input) {
|
||||
return [
|
||||
{
|
||||
label: 'ktx public package version',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', '--version'],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx runtime status missing',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx runtime install local embeddings',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'install', '--feature', 'local-embeddings', '--yes'],
|
||||
timeoutMs: 1_200_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx runtime status local embeddings ready',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'status', '--json'],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx runtime start local embeddings',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'start', '--feature', 'local-embeddings'],
|
||||
timeoutMs: 300_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx setup local embeddings',
|
||||
command: 'pnpm',
|
||||
args: [
|
||||
'exec',
|
||||
'ktx',
|
||||
'setup',
|
||||
'--project-dir',
|
||||
input.projectDir,
|
||||
'--new',
|
||||
'--no-input',
|
||||
'--yes',
|
||||
'--skip-llm',
|
||||
'--embedding-backend',
|
||||
'sentence-transformers',
|
||||
'--skip-databases',
|
||||
'--skip-sources',
|
||||
'--skip-agents',
|
||||
],
|
||||
timeoutMs: 900_000,
|
||||
},
|
||||
{
|
||||
label: 'ktx runtime stop local embeddings',
|
||||
command: 'pnpm',
|
||||
args: ['exec', 'ktx', 'runtime', 'stop'],
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function parseDaemonBaseUrl(stdout) {
|
||||
const match = stdout.match(/^url: (http:\/\/127\.0\.0\.1:\d+)$/m);
|
||||
if (!match) {
|
||||
throw new Error(`Daemon URL was not printed by runtime start:\n${stdout}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function validateEmbeddingResponse(raw, expectedDimensions) {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
throw new Error('Embedding response must be a JSON object');
|
||||
}
|
||||
const embedding = raw.embedding;
|
||||
if (!Array.isArray(embedding)) {
|
||||
throw new Error('Embedding response must include an embedding array');
|
||||
}
|
||||
if (embedding.length !== expectedDimensions) {
|
||||
throw new Error(`Expected embedding dimension ${expectedDimensions}, got ${embedding.length}`);
|
||||
}
|
||||
for (const [index, value] of embedding.entries()) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
throw new Error(`Embedding value at index ${index} is not a finite number`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run(command, args, options = {}) {
|
||||
process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
|
||||
try {
|
||||
const result = await execFileAsync(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: { ...process.env, ...options.env },
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 1024 * 1024 * 20,
|
||||
timeout: options.timeoutMs ?? 120_000,
|
||||
});
|
||||
if (result.stdout) {
|
||||
process.stdout.write(result.stdout);
|
||||
}
|
||||
if (result.stderr) {
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
||||
} catch (error) {
|
||||
const stdout = typeof error.stdout === 'string' ? error.stdout : '';
|
||||
const stderr = typeof error.stderr === 'string' ? error.stderr : error.message;
|
||||
if (stdout) {
|
||||
process.stdout.write(stdout);
|
||||
}
|
||||
if (stderr) {
|
||||
process.stderr.write(stderr);
|
||||
}
|
||||
return {
|
||||
code: typeof error.code === 'number' ? error.code : 1,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function requireSuccess(label, result, options = {}) {
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`${label} failed with code ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
||||
}
|
||||
if (options.stderrPattern && !options.stderrPattern.test(result.stderr)) {
|
||||
throw new Error(`${label} stderr did not match ${options.stderrPattern}\nstderr:\n${result.stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonStdout(label, result) {
|
||||
requireSuccess(label, result);
|
||||
try {
|
||||
return JSON.parse(result.stdout);
|
||||
} catch (error) {
|
||||
throw new Error(`${label} did not write JSON stdout: ${error.message}\nstdout:\n${result.stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireOutput(label, result, pattern) {
|
||||
if (!pattern.test(result.stdout)) {
|
||||
throw new Error(`${label} stdout did not match ${pattern}\nstdout:\n${result.stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function postJson(baseUrl, path, payload, timeoutMs) {
|
||||
const response = await fetch(new URL(path, baseUrl), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`POST ${path} failed with ${response.status}: ${text}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(`POST ${path} returned non-JSON response: ${error.message}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSmokePackage(projectDir, tarballPath) {
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(projectDir, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'ktx-local-embeddings-runtime-smoke',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
'@kaelio/ktx': `file:${tarballPath}`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runLocalEmbeddingsRuntimeSmoke(options = {}) {
|
||||
const rootDir = options.rootDir ?? DEFAULT_ROOT_DIR;
|
||||
const tarballPath = options.tarballPath ?? (await selectPublicKtxTarball(rootDir));
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-local-embeddings-smoke-'));
|
||||
const keepTemp = options.keepTemp ?? process.env.KTX_KEEP_LOCAL_EMBEDDINGS_SMOKE === '1';
|
||||
const installDir = join(root, 'installed-package');
|
||||
const projectDir = join(root, 'project');
|
||||
const smokeEnv = buildLocalEmbeddingsSmokeEnv(root);
|
||||
const commands = localEmbeddingsSmokeCommands({ projectDir });
|
||||
let daemonStarted = false;
|
||||
|
||||
try {
|
||||
await writeSmokePackage(installDir, tarballPath);
|
||||
requireSuccess(
|
||||
'pnpm install public package',
|
||||
await run('pnpm', ['install', '--ignore-scripts=false'], {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: 300_000,
|
||||
}),
|
||||
);
|
||||
|
||||
const version = await run(commands[0].command, commands[0].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[0].timeoutMs,
|
||||
});
|
||||
requireSuccess(commands[0].label, version);
|
||||
requireOutput(commands[0].label, version, /@kaelio\/ktx 0\.0\.0-private/);
|
||||
|
||||
const missingStatus = parseJsonStdout(
|
||||
commands[1].label,
|
||||
await run(commands[1].command, commands[1].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[1].timeoutMs,
|
||||
}),
|
||||
);
|
||||
if (missingStatus.kind !== 'missing') {
|
||||
throw new Error(`Expected missing runtime before install, got ${JSON.stringify(missingStatus)}`);
|
||||
}
|
||||
|
||||
const install = await run(commands[2].command, commands[2].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[2].timeoutMs,
|
||||
});
|
||||
requireSuccess(commands[2].label, install);
|
||||
requireOutput(commands[2].label, install, /Installed KTX Python runtime/);
|
||||
requireOutput(commands[2].label, install, /features: core, local-embeddings/);
|
||||
|
||||
const readyStatus = parseJsonStdout(
|
||||
commands[3].label,
|
||||
await run(commands[3].command, commands[3].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[3].timeoutMs,
|
||||
}),
|
||||
);
|
||||
if (readyStatus.kind !== 'ready') {
|
||||
throw new Error(`Expected ready runtime after install, got ${JSON.stringify(readyStatus)}`);
|
||||
}
|
||||
if (!readyStatus.manifest?.features?.includes('local-embeddings')) {
|
||||
throw new Error(`Runtime manifest did not include local-embeddings: ${JSON.stringify(readyStatus.manifest)}`);
|
||||
}
|
||||
|
||||
const start = await run(commands[4].command, commands[4].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[4].timeoutMs,
|
||||
});
|
||||
requireSuccess(commands[4].label, start);
|
||||
daemonStarted = true;
|
||||
const baseUrl = parseDaemonBaseUrl(start.stdout);
|
||||
|
||||
const embeddingResponse = await postJson(
|
||||
baseUrl,
|
||||
'/embeddings/compute',
|
||||
{ text: 'KTX local embeddings release smoke' },
|
||||
900_000,
|
||||
);
|
||||
validateEmbeddingResponse(embeddingResponse, 384);
|
||||
process.stdout.write('KTX local embeddings daemon computed a 384-dimensional embedding\n');
|
||||
|
||||
const setup = await run(commands[5].command, commands[5].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[5].timeoutMs,
|
||||
});
|
||||
requireSuccess(commands[5].label, setup);
|
||||
requireOutput(commands[5].label, setup, /Embeddings ready: yes \(all-MiniLM-L6-v2\)/);
|
||||
|
||||
const config = await readFile(join(projectDir, 'ktx.yaml'), 'utf8');
|
||||
if (!config.includes('base_url: managed:local-embeddings')) {
|
||||
throw new Error(`ktx.yaml did not contain managed local embeddings marker:\n${config}`);
|
||||
}
|
||||
process.stdout.write('KTX setup persisted managed local embeddings marker\n');
|
||||
|
||||
const stop = await run(commands[6].command, commands[6].args, {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: commands[6].timeoutMs,
|
||||
});
|
||||
requireSuccess(commands[6].label, stop);
|
||||
daemonStarted = false;
|
||||
requireOutput(commands[6].label, stop, /Stopped KTX Python daemon/);
|
||||
|
||||
process.stdout.write('KTX local embeddings runtime smoke verified\n');
|
||||
} finally {
|
||||
if (daemonStarted) {
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop'], {
|
||||
cwd: installDir,
|
||||
env: smokeEnv,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
}
|
||||
if (!keepTemp) {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
} else {
|
||||
process.stdout.write(`Kept local embeddings smoke root: ${root}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const optIn = localEmbeddingsSmokeOptIn(process.env, args);
|
||||
if (!optIn.run) {
|
||||
process.stdout.write(`Skipping KTX local embeddings runtime smoke. ${optIn.message}\n`);
|
||||
if (args.includes('--require-opt-in')) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await runLocalEmbeddingsRuntimeSmoke();
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the smoke test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL only in the package script test because
|
||||
`release:local-embeddings-smoke` is not registered yet.
|
||||
|
||||
- [ ] **Step 3: Commit the smoke script**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/local-embeddings-runtime-smoke.mjs
|
||||
git commit -m "feat: add local embeddings runtime smoke"
|
||||
```
|
||||
|
||||
### Task 3: Register the opt-in package script
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `package.json`
|
||||
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add the package script**
|
||||
|
||||
In `package.json`, add this script immediately after
|
||||
`"release:published-smoke"`:
|
||||
|
||||
```json
|
||||
"release:local-embeddings-smoke": "node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in",
|
||||
```
|
||||
|
||||
The surrounding `scripts` section must contain this sequence after the edit:
|
||||
|
||||
```json
|
||||
"release:published-smoke": "node scripts/published-package-smoke.mjs --require-config",
|
||||
"release:local-embeddings-smoke": "node scripts/local-embeddings-runtime-smoke.mjs --require-opt-in",
|
||||
"release:readiness": "node scripts/release-readiness.mjs",
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Verify the script stays opt-in**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:local-embeddings-smoke
|
||||
```
|
||||
|
||||
Expected: FAIL with:
|
||||
|
||||
```text
|
||||
Skipping KTX local embeddings runtime smoke. Set KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 or pass --force to run the local embeddings smoke.
|
||||
```
|
||||
|
||||
The command must exit non-zero because `--require-opt-in` is present. This
|
||||
protects local and CI runs from downloading large dependencies by accident.
|
||||
|
||||
- [ ] **Step 4: Commit the package script**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add package.json
|
||||
git commit -m "chore: register local embeddings smoke"
|
||||
```
|
||||
|
||||
### Task 4: Verify the opt-in smoke path
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `scripts/local-embeddings-runtime-smoke.mjs`
|
||||
- Verify: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
- Verify: `package.json`
|
||||
|
||||
- [ ] **Step 1: Run fast script tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. Existing package artifact tests must still prove that the
|
||||
default npm artifact smoke does not prepare an external Python environment or
|
||||
run local embeddings downloads.
|
||||
|
||||
- [ ] **Step 2: Build release artifacts for the smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run artifacts:build
|
||||
```
|
||||
|
||||
Expected: PASS and `dist/artifacts/npm/` contains exactly one
|
||||
`kaelio-ktx-*.tgz` tarball.
|
||||
|
||||
- [ ] **Step 3: Run the opt-in local embeddings smoke**
|
||||
|
||||
Run this only in an environment where downloading `sentence-transformers`,
|
||||
`torch`, and `all-MiniLM-L6-v2` is acceptable:
|
||||
|
||||
```bash
|
||||
KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 pnpm run release:local-embeddings-smoke
|
||||
```
|
||||
|
||||
Expected: PASS with output containing:
|
||||
|
||||
```text
|
||||
KTX local embeddings daemon computed a 384-dimensional embedding
|
||||
KTX setup persisted managed local embeddings marker
|
||||
KTX local embeddings runtime smoke verified
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run release readiness**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:readiness
|
||||
```
|
||||
|
||||
Expected: PASS. The readiness report must not require
|
||||
`release:local-embeddings-smoke`; that smoke remains a separately triggered
|
||||
release job.
|
||||
|
||||
- [ ] **Step 5: Run pre-commit for changed files when configured**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run --files scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs package.json
|
||||
```
|
||||
|
||||
Expected: PASS. If pre-commit is unavailable in the environment, record the
|
||||
tooling failure and keep the previous verification output.
|
||||
|
||||
- [ ] **Step 6: Commit verification fixes if needed**
|
||||
|
||||
If verification required edits, run:
|
||||
|
||||
```bash
|
||||
git add scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs package.json
|
||||
git commit -m "fix: verify local embeddings smoke"
|
||||
```
|
||||
|
||||
Skip this commit when no files changed after the previous commits.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `node --test scripts/local-embeddings-runtime-smoke.test.mjs` passes.
|
||||
- `pnpm run release:local-embeddings-smoke` fails fast without the opt-in
|
||||
environment variable and prints the exact opt-in guidance.
|
||||
- `KTX_RUN_LOCAL_EMBEDDINGS_SMOKE=1 pnpm run release:local-embeddings-smoke`
|
||||
installs the public `@kaelio/ktx` tarball into a clean project, isolates
|
||||
`KTX_RUNTIME_ROOT` and model caches, installs `local-embeddings`, starts the
|
||||
managed daemon, computes a 384-dimensional embedding through
|
||||
`/embeddings/compute`, runs setup with `--embedding-backend
|
||||
sentence-transformers`, verifies `base_url: managed:local-embeddings` in
|
||||
`ktx.yaml`, and stops the daemon.
|
||||
- The default `pnpm run artifacts:verify`, `pnpm run release:readiness`, and
|
||||
`pnpm run check` paths do not run the local embeddings smoke.
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: this plan covers the remaining release-check item for local
|
||||
embeddings in a separate job or opt-in check. Earlier implemented plans cover
|
||||
the bundled wheel, managed runtime installer, `sl query` command integration,
|
||||
daemon lifecycle, managed local embeddings runtime behavior, public npm
|
||||
package assembly, and default core runtime release smoke.
|
||||
- Placeholder scan: no steps contain placeholder implementation language.
|
||||
- Type consistency: runtime feature names are consistently `core` and
|
||||
`local-embeddings`; the public npm package name is `@kaelio/ktx`; the opt-in
|
||||
environment variable is `KTX_RUN_LOCAL_EMBEDDINGS_SMOKE`; the managed local
|
||||
embedding marker remains `managed:local-embeddings`.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,239 @@
|
|||
# Managed Local Embeddings Smoke Public Version Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the opt-in local embeddings release smoke validate the public
|
||||
`@kaelio/ktx` package version instead of the private workspace version.
|
||||
|
||||
**Architecture:** Reuse the public package constants from
|
||||
`scripts/build-public-npm-package.mjs` inside the local embeddings smoke. Add a
|
||||
small exported RegExp helper so the unit test can lock the version expectation
|
||||
without running the expensive model-download smoke.
|
||||
|
||||
**Tech Stack:** Node.js ESM scripts, `node:test`, pnpm release scripts.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
The npm-managed Python runtime spec is
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
The current branch already contains implementation commits for each existing
|
||||
plan derived from that spec.
|
||||
|
||||
Implemented spec-derived plans:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
|
||||
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-uv-prerequisite-contract.md`
|
||||
- `docs/superpowers/plans/2026-05-11-single-public-runtime-artifact-cleanup.md`
|
||||
|
||||
The remaining gap is in
|
||||
`scripts/local-embeddings-runtime-smoke.mjs`. The script selects and installs a
|
||||
public tarball named `kaelio-ktx-*.tgz` and writes a smoke package dependency on
|
||||
`@kaelio/ktx`, but line 267 still expects `@kaelio/ktx 0.0.0-private`. The
|
||||
public package builder defines `PUBLIC_NPM_PACKAGE_VERSION = '0.1.0'`, and the
|
||||
main packed-package smoke already expects `@kaelio/ktx 0.1.0`.
|
||||
|
||||
## File Structure
|
||||
|
||||
This change keeps the release version source of truth in one script and reuses
|
||||
it from the opt-in smoke.
|
||||
|
||||
- Modify `scripts/local-embeddings-runtime-smoke.mjs`: import the public package
|
||||
constants, export `expectedPublicKtxVersionPattern()`, and use that pattern
|
||||
for the smoke version assertion.
|
||||
- Modify `scripts/local-embeddings-runtime-smoke.test.mjs`: import
|
||||
`expectedPublicKtxVersionPattern()` and assert that it accepts
|
||||
`@kaelio/ktx 0.1.0` and rejects `@kaelio/ktx 0.0.0-private`.
|
||||
|
||||
### Task 1: Align the local embeddings smoke version assertion
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/local-embeddings-runtime-smoke.mjs:1-267`
|
||||
- Modify: `scripts/local-embeddings-runtime-smoke.test.mjs:5-118`
|
||||
- Test: `scripts/local-embeddings-runtime-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing version-pattern test**
|
||||
|
||||
In `scripts/local-embeddings-runtime-smoke.test.mjs`, update the import block
|
||||
to include `expectedPublicKtxVersionPattern`:
|
||||
|
||||
```js
|
||||
import {
|
||||
buildLocalEmbeddingsSmokeEnv,
|
||||
expectedPublicKtxVersionPattern,
|
||||
localEmbeddingsSmokeCommands,
|
||||
localEmbeddingsSmokeOptIn,
|
||||
parseDaemonBaseUrl,
|
||||
publicKtxTarballName,
|
||||
validateEmbeddingResponse,
|
||||
} from './local-embeddings-runtime-smoke.mjs';
|
||||
```
|
||||
|
||||
Then add this test after the `publicKtxTarballName` describe block:
|
||||
|
||||
```js
|
||||
describe('expectedPublicKtxVersionPattern', () => {
|
||||
it('matches the public package version and rejects the private workspace version', () => {
|
||||
const pattern = expectedPublicKtxVersionPattern();
|
||||
|
||||
assert.match('@kaelio/ktx 0.1.0\n', pattern);
|
||||
assert.doesNotMatch('@kaelio/ktx 0.0.0-private\n', pattern);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL with an ESM export error that says
|
||||
`expectedPublicKtxVersionPattern` is not exported from
|
||||
`./local-embeddings-runtime-smoke.mjs`.
|
||||
|
||||
- [ ] **Step 3: Import the public package constants**
|
||||
|
||||
In `scripts/local-embeddings-runtime-smoke.mjs`, add this import after the
|
||||
existing Node imports:
|
||||
|
||||
```js
|
||||
import {
|
||||
PUBLIC_NPM_PACKAGE_NAME,
|
||||
PUBLIC_NPM_PACKAGE_VERSION,
|
||||
} from './build-public-npm-package.mjs';
|
||||
```
|
||||
|
||||
The top of the file becomes:
|
||||
|
||||
```js
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import {
|
||||
PUBLIC_NPM_PACKAGE_NAME,
|
||||
PUBLIC_NPM_PACKAGE_VERSION,
|
||||
} from './build-public-npm-package.mjs';
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the version-pattern helper**
|
||||
|
||||
In `scripts/local-embeddings-runtime-smoke.mjs`, add these functions after the
|
||||
`OPT_IN_MESSAGE` constant:
|
||||
|
||||
```js
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function expectedPublicKtxVersionPattern() {
|
||||
return new RegExp(
|
||||
`${escapeRegExp(PUBLIC_NPM_PACKAGE_NAME)} ${escapeRegExp(PUBLIC_NPM_PACKAGE_VERSION)}`,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Use the helper in the smoke**
|
||||
|
||||
In `scripts/local-embeddings-runtime-smoke.mjs`, replace this line:
|
||||
|
||||
```js
|
||||
requireOutput(commands[0].label, version, /@kaelio\/ktx 0\.0\.0-private/);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```js
|
||||
requireOutput(commands[0].label, version, expectedPublicKtxVersionPattern());
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the focused test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The new test proves the smoke accepts `@kaelio/ktx 0.1.0` and
|
||||
rejects `@kaelio/ktx 0.0.0-private`.
|
||||
|
||||
- [ ] **Step 7: Run related release-script tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. These tests cover the public package constants, tarball name,
|
||||
artifact smoke source, and local embeddings smoke helpers.
|
||||
|
||||
- [ ] **Step 8: Run a stale-expectation search**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "@kaelio/ktx 0\\.0\\.0-private|0\\\\\\.0\\\\\\.0-private" scripts/local-embeddings-runtime-smoke.mjs
|
||||
```
|
||||
|
||||
Expected: no output. The opt-in local embeddings smoke no longer contains the
|
||||
private package version expectation. The test file still uses
|
||||
`@kaelio/ktx 0.0.0-private` as a negative fixture.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/local-embeddings-runtime-smoke.mjs scripts/local-embeddings-runtime-smoke.test.mjs
|
||||
git commit -m "fix: align local embeddings smoke with public version"
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Run these checks before marking the plan complete:
|
||||
|
||||
```bash
|
||||
node --test scripts/local-embeddings-runtime-smoke.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs
|
||||
rg -n "@kaelio/ktx 0\\.0\\.0-private|0\\\\\\.0\\\\\\.0-private" scripts/local-embeddings-runtime-smoke.mjs
|
||||
```
|
||||
|
||||
Expected results:
|
||||
|
||||
- `node --test ...` exits with code 0.
|
||||
- `rg ...` prints no matches.
|
||||
- No Python files changed, so the repository Python pre-commit requirement does
|
||||
not apply.
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: this plan fixes the opt-in local embeddings release smoke from
|
||||
the npm-managed runtime spec so it validates the public npm package produced
|
||||
by the current release artifact flow.
|
||||
- Placeholder scan: the plan contains concrete file paths, code blocks,
|
||||
commands, and expected outcomes.
|
||||
- Type consistency: the helper name is consistently
|
||||
`expectedPublicKtxVersionPattern`, and it uses
|
||||
`PUBLIC_NPM_PACKAGE_NAME` plus `PUBLIC_NPM_PACKAGE_VERSION` from the public
|
||||
package builder.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,935 @@
|
|||
# Managed Python Runtime Command Integration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `ktx sl query` use the KTX-managed bundled Python runtime
|
||||
instead of relying on a user-provided `python -m ktx_daemon`.
|
||||
|
||||
**Architecture:** Add a small CLI helper that resolves the managed runtime,
|
||||
installs the `core` feature when policy permits it, and creates the existing
|
||||
`@ktx/context/daemon` one-shot semantic-layer compute port with the managed
|
||||
`ktx-daemon` executable. Wire `ktx sl query` to pass an explicit runtime
|
||||
install policy from `--yes`, `--no-input`, or the default interactive mode.
|
||||
|
||||
**Tech Stack:** TypeScript, Commander, Vitest, `@clack/prompts`,
|
||||
`@ktx/context/daemon`, existing KTX managed runtime installer.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
Existing plans based on the spec:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md` is
|
||||
implemented. The worktree contains
|
||||
`scripts/build-python-runtime-wheel.mjs`,
|
||||
`scripts/build-python-runtime-wheel.test.mjs`, runtime-wheel packaging in
|
||||
`scripts/package-artifacts.mjs`, release-policy coverage, and matching
|
||||
artifact tests. The targeted verification passes:
|
||||
`node --test scripts/build-python-runtime-wheel.test.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs`.
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md` is
|
||||
implemented. The worktree contains
|
||||
`packages/cli/src/managed-python-runtime.ts`,
|
||||
`packages/cli/src/runtime.ts`,
|
||||
`packages/cli/src/commands/runtime-commands.ts`, CLI registration, and
|
||||
matching Vitest coverage. The targeted CLI verification passes:
|
||||
`pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts src/runtime.test.ts src/index.test.ts`.
|
||||
|
||||
Spec requirements still outside this plan:
|
||||
|
||||
- `ktx runtime start` and `ktx runtime stop`.
|
||||
- Managed HTTP daemon state, health checks, reuse, and stale daemon repair.
|
||||
- Lazy `local-embeddings` installation and local embedding daemon reuse.
|
||||
- Public npm package rename from `@ktx/cli` to `@kaelio/ktx`.
|
||||
|
||||
This plan implements the next runnable user path: `ktx sl query` installs or
|
||||
uses the managed `core` Python runtime according to the command's input policy.
|
||||
|
||||
## File structure
|
||||
|
||||
- Create `packages/cli/src/managed-python-command.ts`: CLI helper for managed
|
||||
runtime policy, optional prompt, runtime install, and managed semantic-layer
|
||||
compute port creation.
|
||||
- Create `packages/cli/src/managed-python-command.test.ts`: unit tests for
|
||||
ready runtime reuse, `--no-input` failure, `--yes` installation, and
|
||||
interactive prompt acceptance.
|
||||
- Modify `packages/cli/src/sl.ts`: extend `KtxSlArgs` with CLI version and
|
||||
runtime install policy for `query`, and use the managed helper when no test
|
||||
compute port is injected.
|
||||
- Modify `packages/cli/src/sl.test.ts`: update existing `query` arguments and
|
||||
assert `runKtxSl` delegates default compute creation to the managed helper.
|
||||
- Modify `packages/cli/src/commands/sl-commands.ts`: add `--yes` and
|
||||
`--no-input` to `sl query`, derive the runtime install policy, and pass the
|
||||
CLI package version.
|
||||
- Modify `packages/cli/src/command-schemas.ts`: validate `cliVersion` and
|
||||
`runtimeInstallPolicy` on parsed `sl query` arguments.
|
||||
- Modify `packages/cli/src/index.test.ts`: assert Commander routes the new
|
||||
`sl query` runtime policy flags.
|
||||
|
||||
### Task 1: Add failing managed Python command helper tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/managed-python-command.test.ts`
|
||||
- Test: `packages/cli/src/managed-python-command.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test file**
|
||||
|
||||
Create `packages/cli/src/managed-python-command.test.ts` with this content:
|
||||
|
||||
```typescript
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
createManagedPythonSemanticLayerComputePort,
|
||||
managedRuntimeInstallCommand,
|
||||
} from './managed-python-command.js';
|
||||
import type {
|
||||
InstalledKtxRuntimeManifest,
|
||||
KtxRuntimeFeature,
|
||||
ManagedPythonRuntimeInstallResult,
|
||||
ManagedPythonRuntimeLayout,
|
||||
ManagedPythonRuntimeStatus,
|
||||
} from './managed-python-runtime.js';
|
||||
|
||||
function makeIo() {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
return {
|
||||
io: {
|
||||
stdout: {
|
||||
write: (chunk: string) => {
|
||||
stdout += chunk;
|
||||
},
|
||||
},
|
||||
stderr: {
|
||||
write: (chunk: string) => {
|
||||
stderr += chunk;
|
||||
},
|
||||
},
|
||||
},
|
||||
stdout: () => stdout,
|
||||
stderr: () => stderr,
|
||||
};
|
||||
}
|
||||
|
||||
function layout(): ManagedPythonRuntimeLayout {
|
||||
return {
|
||||
cliVersion: '0.2.0',
|
||||
runtimeRoot: '/runtime',
|
||||
versionDir: '/runtime/0.2.0',
|
||||
venvDir: '/runtime/0.2.0/.venv',
|
||||
manifestPath: '/runtime/0.2.0/manifest.json',
|
||||
installLogPath: '/runtime/0.2.0/install.log',
|
||||
assetDir: '/assets/python',
|
||||
assetManifestPath: '/assets/python/manifest.json',
|
||||
pythonPath: '/runtime/0.2.0/.venv/bin/python',
|
||||
daemonPath: '/runtime/0.2.0/.venv/bin/ktx-daemon',
|
||||
};
|
||||
}
|
||||
|
||||
function manifest(features: KtxRuntimeFeature[] = ['core']): InstalledKtxRuntimeManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
cliVersion: '0.2.0',
|
||||
installedAt: '2026-05-11T00:00:00.000Z',
|
||||
asset: {
|
||||
schemaVersion: 1,
|
||||
distributionName: 'kaelio-ktx',
|
||||
normalizedName: 'kaelio_ktx',
|
||||
version: '0.2.0',
|
||||
wheel: {
|
||||
file: 'kaelio_ktx-0.2.0-py3-none-any.whl',
|
||||
sha256: 'a'.repeat(64),
|
||||
bytes: 123,
|
||||
},
|
||||
},
|
||||
features,
|
||||
python: {
|
||||
executable: '/runtime/0.2.0/.venv/bin/python',
|
||||
daemonExecutable: '/runtime/0.2.0/.venv/bin/ktx-daemon',
|
||||
},
|
||||
installLog: '/runtime/0.2.0/install.log',
|
||||
};
|
||||
}
|
||||
|
||||
function readyStatus(features: KtxRuntimeFeature[] = ['core']): ManagedPythonRuntimeStatus {
|
||||
return {
|
||||
kind: 'ready',
|
||||
detail: 'Runtime ready at /runtime/0.2.0',
|
||||
layout: layout(),
|
||||
manifest: manifest(features),
|
||||
};
|
||||
}
|
||||
|
||||
function missingStatus(): ManagedPythonRuntimeStatus {
|
||||
return {
|
||||
kind: 'missing',
|
||||
detail: 'No runtime manifest at /runtime/0.2.0/manifest.json',
|
||||
layout: layout(),
|
||||
};
|
||||
}
|
||||
|
||||
function installResult(features: KtxRuntimeFeature[] = ['core']): ManagedPythonRuntimeInstallResult {
|
||||
const installedManifest = manifest(features);
|
||||
return {
|
||||
status: 'installed',
|
||||
layout: layout(),
|
||||
asset: {
|
||||
manifest: installedManifest.asset,
|
||||
wheelPath: '/assets/python/kaelio_ktx-0.2.0-py3-none-any.whl',
|
||||
},
|
||||
manifest: installedManifest,
|
||||
};
|
||||
}
|
||||
|
||||
describe('managedRuntimeInstallCommand', () => {
|
||||
it('prints the exact command for each managed runtime feature', () => {
|
||||
expect(managedRuntimeInstallCommand('core')).toBe('ktx runtime install --yes');
|
||||
expect(managedRuntimeInstallCommand('local-embeddings')).toBe(
|
||||
'ktx runtime install --feature local-embeddings --yes',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createManagedPythonSemanticLayerComputePort', () => {
|
||||
it('uses the managed ktx-daemon executable when the runtime is ready', async () => {
|
||||
const io = makeIo();
|
||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||
const createPythonCompute = vi.fn(() => compute);
|
||||
|
||||
await expect(
|
||||
createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'never',
|
||||
io: io.io,
|
||||
readStatus: vi.fn(async () => readyStatus()),
|
||||
installRuntime: vi.fn(),
|
||||
createPythonCompute,
|
||||
}),
|
||||
).resolves.toBe(compute);
|
||||
|
||||
expect(createPythonCompute).toHaveBeenCalledWith({
|
||||
command: '/runtime/0.2.0/.venv/bin/ktx-daemon',
|
||||
args: [],
|
||||
});
|
||||
expect(io.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('fails with a preparation command when input is disabled and the runtime is missing', async () => {
|
||||
const io = makeIo();
|
||||
const installRuntime = vi.fn();
|
||||
|
||||
await expect(
|
||||
createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'never',
|
||||
io: io.io,
|
||||
readStatus: vi.fn(async () => missingStatus()),
|
||||
installRuntime,
|
||||
}),
|
||||
).rejects.toThrow('KTX Python runtime is required for this command. Run: ktx runtime install --yes');
|
||||
|
||||
expect(installRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('installs the core runtime without prompting when policy is auto', async () => {
|
||||
const io = makeIo();
|
||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||
const createPythonCompute = vi.fn(() => compute);
|
||||
const installRuntime = vi.fn(async () => installResult());
|
||||
|
||||
await expect(
|
||||
createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'auto',
|
||||
io: io.io,
|
||||
readStatus: vi.fn(async () => missingStatus()),
|
||||
installRuntime,
|
||||
createPythonCompute,
|
||||
}),
|
||||
).resolves.toBe(compute);
|
||||
|
||||
expect(installRuntime).toHaveBeenCalledWith({
|
||||
cliVersion: '0.2.0',
|
||||
features: ['core'],
|
||||
force: false,
|
||||
});
|
||||
expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv');
|
||||
expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
|
||||
});
|
||||
|
||||
it('prompts before installing when policy is prompt', async () => {
|
||||
const io = makeIo();
|
||||
const confirmInstall = vi.fn(async () => true);
|
||||
const installRuntime = vi.fn(async () => installResult());
|
||||
|
||||
await createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'prompt',
|
||||
io: io.io,
|
||||
readStatus: vi.fn(async () => missingStatus()),
|
||||
installRuntime,
|
||||
createPythonCompute: vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() })),
|
||||
confirmInstall,
|
||||
});
|
||||
|
||||
expect(confirmInstall).toHaveBeenCalledWith(
|
||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
);
|
||||
expect(installRuntime).toHaveBeenCalledWith({
|
||||
cliVersion: '0.2.0',
|
||||
features: ['core'],
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL with an import error for
|
||||
`./managed-python-command.js`.
|
||||
|
||||
### Task 2: Implement the managed Python command helper
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/managed-python-command.ts`
|
||||
- Test: `packages/cli/src/managed-python-command.test.ts`
|
||||
|
||||
- [ ] **Step 1: Create the helper**
|
||||
|
||||
Create `packages/cli/src/managed-python-command.ts` with this content:
|
||||
|
||||
```typescript
|
||||
import { cancel, confirm, isCancel } from '@clack/prompts';
|
||||
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import {
|
||||
installManagedPythonRuntime,
|
||||
readManagedPythonRuntimeStatus,
|
||||
type InstalledKtxRuntimeManifest,
|
||||
type KtxRuntimeFeature,
|
||||
type ManagedPythonRuntimeInstallOptions,
|
||||
type ManagedPythonRuntimeInstallResult,
|
||||
type ManagedPythonRuntimeLayout,
|
||||
type ManagedPythonRuntimeLayoutOptions,
|
||||
type ManagedPythonRuntimeStatus,
|
||||
} from './managed-python-runtime.js';
|
||||
|
||||
export type KtxManagedPythonInstallPolicy = 'prompt' | 'auto' | 'never';
|
||||
|
||||
export interface ManagedPythonCommandRuntime {
|
||||
layout: ManagedPythonRuntimeLayout;
|
||||
manifest: InstalledKtxRuntimeManifest;
|
||||
}
|
||||
|
||||
export interface ManagedPythonCommandDeps {
|
||||
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
||||
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
||||
confirmInstall?: (message: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps {
|
||||
cliVersion: string;
|
||||
installPolicy: KtxManagedPythonInstallPolicy;
|
||||
io: KtxCliIo;
|
||||
feature?: KtxRuntimeFeature;
|
||||
}
|
||||
|
||||
export interface ManagedPythonSemanticLayerComputeOptions extends ManagedPythonCommandOptions {
|
||||
createPythonCompute?: typeof createPythonSemanticLayerComputePort;
|
||||
}
|
||||
|
||||
export function managedRuntimeInstallCommand(feature: KtxRuntimeFeature): string {
|
||||
return feature === 'local-embeddings'
|
||||
? 'ktx runtime install --feature local-embeddings --yes'
|
||||
: 'ktx runtime install --yes';
|
||||
}
|
||||
|
||||
function installPrompt(feature: KtxRuntimeFeature): string {
|
||||
const label = feature === 'local-embeddings' ? 'local embeddings Python runtime' : 'core Python runtime';
|
||||
return `KTX needs to install the ${label}. This downloads Python dependencies with uv. Continue?`;
|
||||
}
|
||||
|
||||
function runtimeRequiredMessage(feature: KtxRuntimeFeature): string {
|
||||
return `KTX Python runtime is required for this command. Run: ${managedRuntimeInstallCommand(feature)}`;
|
||||
}
|
||||
|
||||
function hasFeature(manifest: InstalledKtxRuntimeManifest, feature: KtxRuntimeFeature): boolean {
|
||||
return manifest.features.includes(feature);
|
||||
}
|
||||
|
||||
async function defaultConfirmInstall(message: string): Promise<boolean> {
|
||||
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
||||
return false;
|
||||
}
|
||||
const response = await confirm({ message, initialValue: true });
|
||||
if (isCancel(response)) {
|
||||
cancel('Runtime installation cancelled.');
|
||||
return false;
|
||||
}
|
||||
return response === true;
|
||||
}
|
||||
|
||||
export async function ensureManagedPythonCommandRuntime(
|
||||
options: ManagedPythonCommandOptions,
|
||||
): Promise<ManagedPythonCommandRuntime> {
|
||||
const feature = options.feature ?? 'core';
|
||||
const readStatus = options.readStatus ?? readManagedPythonRuntimeStatus;
|
||||
const installRuntime = options.installRuntime ?? installManagedPythonRuntime;
|
||||
const status = await readStatus({ cliVersion: options.cliVersion });
|
||||
|
||||
if (status.kind === 'ready' && status.manifest && hasFeature(status.manifest, feature)) {
|
||||
return { layout: status.layout, manifest: status.manifest };
|
||||
}
|
||||
|
||||
if (options.installPolicy === 'never') {
|
||||
throw new Error(runtimeRequiredMessage(feature));
|
||||
}
|
||||
|
||||
if (options.installPolicy === 'prompt') {
|
||||
const confirmInstall = options.confirmInstall ?? defaultConfirmInstall;
|
||||
const confirmed = await confirmInstall(installPrompt(feature));
|
||||
if (!confirmed) {
|
||||
throw new Error(`KTX Python runtime installation was cancelled. Run: ${managedRuntimeInstallCommand(feature)}`);
|
||||
}
|
||||
}
|
||||
|
||||
options.io.stderr.write(`Installing KTX Python runtime (${feature}) with uv...\n`);
|
||||
const installed = await installRuntime({
|
||||
cliVersion: options.cliVersion,
|
||||
features: [feature],
|
||||
force: false,
|
||||
});
|
||||
options.io.stderr.write(`KTX Python runtime ready: ${installed.layout.versionDir}\n`);
|
||||
return { layout: installed.layout, manifest: installed.manifest };
|
||||
}
|
||||
|
||||
export async function createManagedPythonSemanticLayerComputePort(
|
||||
options: ManagedPythonSemanticLayerComputeOptions,
|
||||
): Promise<KtxSemanticLayerComputePort> {
|
||||
const runtime = await ensureManagedPythonCommandRuntime({
|
||||
cliVersion: options.cliVersion,
|
||||
installPolicy: options.installPolicy,
|
||||
io: options.io,
|
||||
feature: 'core',
|
||||
...(options.readStatus ? { readStatus: options.readStatus } : {}),
|
||||
...(options.installRuntime ? { installRuntime: options.installRuntime } : {}),
|
||||
...(options.confirmInstall ? { confirmInstall: options.confirmInstall } : {}),
|
||||
});
|
||||
const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort;
|
||||
return createPythonCompute({
|
||||
command: runtime.manifest.python.daemonExecutable,
|
||||
args: [],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the helper test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts
|
||||
git commit -m "feat: add managed python command helper"
|
||||
```
|
||||
|
||||
Expected: commit succeeds.
|
||||
|
||||
### Task 3: Add failing `runKtxSl` managed runtime tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/sl.test.ts`
|
||||
- Test: `packages/cli/src/sl.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add runtime fields to existing `query` test args**
|
||||
|
||||
In each existing `runKtxSl` call whose argument object has
|
||||
`command: 'query'`, add these properties:
|
||||
|
||||
```typescript
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
```
|
||||
|
||||
For example, the first `query` argument object becomes:
|
||||
|
||||
```typescript
|
||||
{
|
||||
command: 'query',
|
||||
projectDir: '/tmp/project',
|
||||
connectionId: 'warehouse',
|
||||
query: { measures: ['orders.order_count'], dimensions: [] },
|
||||
format: 'sql',
|
||||
execute: false,
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the managed helper delegation test**
|
||||
|
||||
In `packages/cli/src/sl.test.ts`, add this test inside
|
||||
`describe('runKtxSl', () => { ... })` after the existing
|
||||
`runs sl query and prints SQL output` test:
|
||||
|
||||
```typescript
|
||||
it('creates default sl query compute through the managed runtime helper', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
project.config.connections.warehouse = { driver: 'postgres', readonly: true };
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/orders.yaml',
|
||||
`name: orders
|
||||
table: public.orders
|
||||
grain: [id]
|
||||
columns:
|
||||
- name: id
|
||||
type: number
|
||||
measures:
|
||||
- name: order_count
|
||||
expr: count(*)
|
||||
joins: []
|
||||
`,
|
||||
'ktx',
|
||||
'ktx@example.com',
|
||||
'Add orders source',
|
||||
);
|
||||
|
||||
const stdout = { write: vi.fn() };
|
||||
const stderr = { write: vi.fn() };
|
||||
const compute = {
|
||||
query: vi.fn(async () => ({
|
||||
sql: 'select count(*) as order_count from public.orders',
|
||||
dialect: 'postgres',
|
||||
columns: [{ name: 'orders.order_count' }],
|
||||
plan: {},
|
||||
})),
|
||||
validateSources: vi.fn(),
|
||||
generateSources: vi.fn(),
|
||||
};
|
||||
const createManagedSemanticLayerCompute = vi.fn(async () => compute);
|
||||
|
||||
await expect(
|
||||
runKtxSl(
|
||||
{
|
||||
command: 'query',
|
||||
projectDir,
|
||||
connectionId: 'warehouse',
|
||||
query: { measures: ['orders.order_count'], dimensions: [] },
|
||||
format: 'sql',
|
||||
execute: false,
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
},
|
||||
{ stdout, stderr },
|
||||
{ createManagedSemanticLayerCompute },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(createManagedSemanticLayerCompute).toHaveBeenCalledWith({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'auto',
|
||||
io: { stdout, stderr },
|
||||
});
|
||||
expect(stdout.write).toHaveBeenCalledWith('select count(*) as order_count from public.orders\n');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the failing `sl` test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/sl.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL with a TypeScript/Vitest error because `runKtxSl` does not
|
||||
accept `createManagedSemanticLayerCompute` yet.
|
||||
|
||||
### Task 4: Wire `runKtxSl` to the managed helper
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/sl.ts`
|
||||
- Test: `packages/cli/src/sl.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add the managed helper imports**
|
||||
|
||||
In `packages/cli/src/sl.ts`, add this import after the existing imports:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createManagedPythonSemanticLayerComputePort,
|
||||
type KtxManagedPythonInstallPolicy,
|
||||
} from './managed-python-command.js';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend the `query` args type**
|
||||
|
||||
In the `KtxSlArgs` union, replace the current `query` object type with this
|
||||
shape:
|
||||
|
||||
```typescript
|
||||
| {
|
||||
command: 'query';
|
||||
projectDir: string;
|
||||
connectionId?: string;
|
||||
query: SemanticLayerQueryInput;
|
||||
format: SlQueryFormat;
|
||||
execute: boolean;
|
||||
maxRows?: number;
|
||||
cliVersion: string;
|
||||
runtimeInstallPolicy: KtxManagedPythonInstallPolicy;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend `KtxSlDeps`**
|
||||
|
||||
In `packages/cli/src/sl.ts`, replace `KtxSlDeps` with this interface:
|
||||
|
||||
```typescript
|
||||
interface KtxSlDeps {
|
||||
loadProject?: typeof loadKtxProject;
|
||||
createSemanticLayerCompute?: () => KtxSemanticLayerComputePort;
|
||||
createManagedSemanticLayerCompute?: (options: {
|
||||
cliVersion: string;
|
||||
installPolicy: KtxManagedPythonInstallPolicy;
|
||||
io: KtxSlIo;
|
||||
}) => Promise<KtxSemanticLayerComputePort>;
|
||||
createQueryExecutor?: () => KtxSqlQueryExecutorPort;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Use the managed helper in the `query` branch**
|
||||
|
||||
In the `args.command === 'query'` branch, replace:
|
||||
|
||||
```typescript
|
||||
const compute = (deps.createSemanticLayerCompute ?? createPythonSemanticLayerComputePort)();
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```typescript
|
||||
const compute = deps.createSemanticLayerCompute
|
||||
? deps.createSemanticLayerCompute()
|
||||
: await (deps.createManagedSemanticLayerCompute ?? createManagedPythonSemanticLayerComputePort)({
|
||||
cliVersion: args.cliVersion,
|
||||
installPolicy: args.runtimeInstallPolicy,
|
||||
io,
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the `sl` test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/sl.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/sl.ts packages/cli/src/sl.test.ts
|
||||
git commit -m "feat: use managed runtime for sl query compute"
|
||||
```
|
||||
|
||||
Expected: commit succeeds.
|
||||
|
||||
### Task 5: Add failing Commander routing tests for `sl query`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/index.test.ts`
|
||||
- Test: `packages/cli/src/index.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add routing tests**
|
||||
|
||||
In `packages/cli/src/index.test.ts`, add this test near the other command
|
||||
routing tests:
|
||||
|
||||
```typescript
|
||||
it('routes sl query managed runtime install policies', async () => {
|
||||
const sl = vi.fn(async () => 0);
|
||||
|
||||
const promptIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count'], promptIo.io, { sl }),
|
||||
).resolves.toBe(0);
|
||||
expect(sl).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
command: 'query',
|
||||
projectDir: tempDir,
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'prompt',
|
||||
query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }),
|
||||
}),
|
||||
promptIo.io,
|
||||
);
|
||||
|
||||
const autoIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes'], autoIo.io, {
|
||||
sl,
|
||||
}),
|
||||
).resolves.toBe(0);
|
||||
expect(sl).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
}),
|
||||
autoIo.io,
|
||||
);
|
||||
|
||||
const noInputIo = makeIo();
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--no-input'],
|
||||
noInputIo.io,
|
||||
{ sl },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
expect(sl).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
cliVersion: '0.0.0-private',
|
||||
runtimeInstallPolicy: 'never',
|
||||
}),
|
||||
noInputIo.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects conflicting sl query runtime install flags', async () => {
|
||||
const io = makeIo();
|
||||
const sl = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes', '--no-input'],
|
||||
io.io,
|
||||
{ sl },
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(sl).not.toHaveBeenCalled();
|
||||
expect(io.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the failing routing tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/index.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because `sl query` does not accept `--yes` or `--no-input`
|
||||
and does not pass runtime policy fields yet.
|
||||
|
||||
### Task 6: Wire `sl query` flags and schema validation
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/commands/sl-commands.ts`
|
||||
- Modify: `packages/cli/src/command-schemas.ts`
|
||||
- Test: `packages/cli/src/index.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add the runtime policy type import**
|
||||
|
||||
In `packages/cli/src/commands/sl-commands.ts`, add this import:
|
||||
|
||||
```typescript
|
||||
import type { KtxManagedPythonInstallPolicy } from '../managed-python-command.js';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the runtime policy parser**
|
||||
|
||||
In `packages/cli/src/commands/sl-commands.ts`, add this function near the
|
||||
other option parsers:
|
||||
|
||||
```typescript
|
||||
function runtimeInstallPolicy(options: { yes?: boolean; input?: boolean }): KtxManagedPythonInstallPolicy {
|
||||
if (options.yes === true && options.input === false) {
|
||||
throw new Error('Choose only one runtime install mode: --yes or --no-input');
|
||||
}
|
||||
if (options.yes === true) {
|
||||
return 'auto';
|
||||
}
|
||||
return options.input === false ? 'never' : 'prompt';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the command options**
|
||||
|
||||
In the `sl.command('query')` option chain, add these options after
|
||||
`.option('--execute', 'Execute the compiled query', false)`:
|
||||
|
||||
```typescript
|
||||
.option('--yes', 'Install the managed Python runtime without prompting when required', false)
|
||||
.option('--no-input', 'Disable interactive managed runtime installation')
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Pass runtime fields into `slQueryCommandSchema.parse`**
|
||||
|
||||
In the `sl.command('query')` action, add these properties to the parsed object:
|
||||
|
||||
```typescript
|
||||
cliVersion: context.packageInfo.version,
|
||||
runtimeInstallPolicy: runtimeInstallPolicy(options),
|
||||
```
|
||||
|
||||
The parsed object must include these fields next to `execute` and `format`:
|
||||
|
||||
```typescript
|
||||
const args = slQueryCommandSchema.parse({
|
||||
command: 'query',
|
||||
projectDir: resolveCommandProjectDir(command),
|
||||
connectionId: options.connectionId,
|
||||
query: {
|
||||
measures: options.measure,
|
||||
dimensions: options.dimension,
|
||||
...(options.filter.length > 0 ? { filters: options.filter } : {}),
|
||||
...(options.segment.length > 0 ? { segments: options.segment } : {}),
|
||||
...(options.orderBy.length > 0 ? { order_by: options.orderBy } : {}),
|
||||
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
||||
...(options.includeEmpty === true ? { include_empty: true } : {}),
|
||||
},
|
||||
format: options.format,
|
||||
execute: options.execute === true,
|
||||
cliVersion: context.packageInfo.version,
|
||||
runtimeInstallPolicy: runtimeInstallPolicy(options),
|
||||
...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Extend the command schema**
|
||||
|
||||
In `packages/cli/src/command-schemas.ts`, add these fields to
|
||||
`slQueryCommandSchema` after `execute: z.boolean()`:
|
||||
|
||||
```typescript
|
||||
cliVersion: z.string().min(1),
|
||||
runtimeInstallPolicy: z.enum(['prompt', 'auto', 'never']),
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the routing tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/index.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts
|
||||
git commit -m "feat: route sl query managed runtime policy"
|
||||
```
|
||||
|
||||
Expected: commit succeeds.
|
||||
|
||||
### Task 7: Verify the full changed surface
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `packages/cli/src/managed-python-command.test.ts`
|
||||
- Verify: `packages/cli/src/sl.test.ts`
|
||||
- Verify: `packages/cli/src/index.test.ts`
|
||||
- Verify: `packages/cli/src/managed-python-command.ts`
|
||||
- Verify: `packages/cli/src/sl.ts`
|
||||
- Verify: `packages/cli/src/commands/sl-commands.ts`
|
||||
- Verify: `packages/cli/src/command-schemas.ts`
|
||||
|
||||
- [ ] **Step 1: Run focused CLI tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-command.test.ts src/sl.test.ts src/index.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run CLI type checking**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run type-check
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run pre-commit for changed TypeScript files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run --files packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts packages/cli/src/sl.ts packages/cli/src/sl.test.ts packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS. If pre-commit is unavailable because the local `uv` version
|
||||
does not satisfy `pyproject.toml`, record the version mismatch and run the
|
||||
focused CLI tests plus type checking from Steps 1 and 2.
|
||||
|
||||
- [ ] **Step 4: Commit verification fixes when needed**
|
||||
|
||||
If Step 1, Step 2, or Step 3 changes files through formatting hooks, run:
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/managed-python-command.ts packages/cli/src/managed-python-command.test.ts packages/cli/src/sl.ts packages/cli/src/sl.test.ts packages/cli/src/commands/sl-commands.ts packages/cli/src/command-schemas.ts packages/cli/src/index.test.ts
|
||||
git commit -m "test: verify managed runtime sl query integration"
|
||||
```
|
||||
|
||||
Expected: commit succeeds only when verification changed files. If no files
|
||||
changed, leave the branch with the commits from Tasks 2, 4, and 6.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
When this plan is complete:
|
||||
|
||||
- `ktx sl query` uses the managed runtime's installed `ktx-daemon` executable
|
||||
for semantic-layer compilation when no test compute dependency is injected.
|
||||
- `ktx sl query --yes` installs the `core` runtime feature without prompting
|
||||
when the managed runtime is missing.
|
||||
- `ktx sl query --no-input` fails with
|
||||
`KTX Python runtime is required for this command. Run: ktx runtime install --yes`
|
||||
when the managed runtime is missing.
|
||||
- `ktx sl query` prompts before first managed runtime installation in an
|
||||
interactive terminal.
|
||||
- Existing injected-compute tests still bypass runtime installation.
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,585 @@
|
|||
# Managed Python Runtime Release Smoke Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the public `@kaelio/ktx` artifact smoke prove that the npm
|
||||
package installs and uses its own managed Python runtime without an externally
|
||||
prepared Python environment.
|
||||
|
||||
**Architecture:** Keep the release smoke black-box: install the packed public
|
||||
npm tarball into a clean project, isolate `KTX_RUNTIME_ROOT`, and exercise the
|
||||
installed `ktx` binary. The first `ktx sl query --yes` performs the lazy core
|
||||
runtime install from bundled package assets, then the smoke verifies
|
||||
`runtime status`, `runtime doctor`, daemon start/reuse, and daemon stop.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv, KTX CLI managed
|
||||
Python runtime assets.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
Existing plans based on the spec:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
|
||||
All six are implemented in this worktree. Evidence found before writing this
|
||||
plan includes:
|
||||
|
||||
- `scripts/build-python-runtime-wheel.mjs` and
|
||||
`scripts/build-python-runtime-wheel.test.mjs`.
|
||||
- `packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl` and
|
||||
`packages/cli/assets/python/manifest.json`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`,
|
||||
`packages/cli/src/runtime.ts`, and
|
||||
`packages/cli/src/commands/runtime-commands.ts`.
|
||||
- `packages/cli/src/managed-python-command.ts` and `ktx sl query` runtime
|
||||
install policy flags.
|
||||
- `packages/cli/src/managed-python-daemon.ts`, daemon state paths, and
|
||||
`ktx runtime start` / `ktx runtime stop`.
|
||||
- `packages/cli/src/managed-local-embeddings.ts`,
|
||||
`packages/context/src/llm/local-config.ts` managed marker constants, and
|
||||
setup wiring in `packages/cli/src/setup-embeddings.ts`.
|
||||
- `scripts/build-public-npm-package.mjs`,
|
||||
`scripts/build-public-npm-package.test.mjs`, `release-policy.json` listing
|
||||
`@kaelio/ktx`, and published smoke command construction for the required
|
||||
`@kaelio/ktx` invocation modes.
|
||||
|
||||
The remaining release-smoke gap is in `scripts/package-artifacts.mjs`:
|
||||
|
||||
- `verifyNpmArtifacts()` creates a smoke `.venv`, installs the built Python
|
||||
runtime wheel into it, and runs installed CLI smoke scripts with that venv at
|
||||
the front of `PATH`.
|
||||
- The installed CLI smoke does run `ktx sl query --yes`, but it does not
|
||||
isolate `KTX_RUNTIME_ROOT`, does not assert that the first query installed
|
||||
the managed runtime from bundled npm assets, and does not exercise
|
||||
`ktx runtime status`, `doctor`, `start`, reuse, and `stop`.
|
||||
|
||||
This plan closes that release-flow gap without changing the separate Python
|
||||
artifact smoke. `verifyPythonArtifacts()` must continue to install the built
|
||||
Python wheel directly because it verifies the Python artifact itself.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `scripts/package-artifacts.test.mjs`: remove the npm-smoke venv test,
|
||||
add a source-level guard that npm artifact verification does not prepare an
|
||||
external Python venv, and assert that the installed CLI smoke exercises the
|
||||
managed runtime lifecycle.
|
||||
- Modify `scripts/package-artifacts.mjs`: remove npm-smoke Python venv PATH
|
||||
setup, isolate `KTX_RUNTIME_ROOT` inside `npmRuntimeSmokeSource()`, assert
|
||||
first-run lazy install, and add runtime status/doctor/start/reuse/stop smoke
|
||||
commands.
|
||||
|
||||
### Task 1: Add failing release-smoke tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/package-artifacts.test.mjs`
|
||||
- Test: `scripts/package-artifacts.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Remove the stale npm-smoke venv import**
|
||||
|
||||
In `scripts/package-artifacts.test.mjs`, delete `npmSmokePythonEnv` from the
|
||||
import list. The surrounding import block must contain this sequence after the
|
||||
edit:
|
||||
|
||||
```javascript
|
||||
npmDemoSmokeSource,
|
||||
npmRuntimeSmokeSource,
|
||||
npmSmokePackageJson,
|
||||
npmVerifySource,
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the npm-smoke venv test with a source guard**
|
||||
|
||||
Delete this entire test block:
|
||||
|
||||
```javascript
|
||||
describe('npmSmokePythonEnv', () => {
|
||||
it('prepends the npm smoke virtualenv bin directory to PATH', () => {
|
||||
const env = npmSmokePythonEnv('/tmp/ktx-npm-smoke', { PATH: '/usr/bin' });
|
||||
|
||||
assert.match(env.PATH, /^\/tmp\/ktx-npm-smoke\/\.venv\/(bin|Scripts)/);
|
||||
assert.match(env.PATH, /\/usr\/bin$/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Insert this block in the same location:
|
||||
|
||||
```javascript
|
||||
describe('verifyNpmArtifacts', () => {
|
||||
it('does not prepare an external Python environment for the npm smoke', async () => {
|
||||
const source = await readFile(new URL('./package-artifacts.mjs', import.meta.url), 'utf8');
|
||||
const start = source.indexOf('async function verifyNpmArtifacts');
|
||||
const end = source.indexOf('async function verifyNpmDemoArtifacts');
|
||||
assert.ok(start > 0, 'verifyNpmArtifacts function must exist');
|
||||
assert.ok(end > start, 'verifyNpmDemoArtifacts must follow verifyNpmArtifacts');
|
||||
|
||||
const body = source.slice(start, end);
|
||||
assert.doesNotMatch(body, /uv', \['venv', '\.venv'\]/);
|
||||
assert.doesNotMatch(body, /pythonArtifactInstallArgs/);
|
||||
assert.doesNotMatch(body, /npmSmokePythonEnv/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend the installed CLI smoke assertions**
|
||||
|
||||
In the `it('runs installed CLI commands through the public package runtime',
|
||||
...)` test, add these assertions after the existing
|
||||
`assert.match(source, /ktx sl query sqlite execute/);` assertion:
|
||||
|
||||
```javascript
|
||||
assert.match(source, /import Database from 'better-sqlite3'/);
|
||||
assert.doesNotMatch(source, /run\('python'/);
|
||||
assert.match(source, /KTX_RUNTIME_ROOT/);
|
||||
assert.match(source, /managed-runtime/);
|
||||
assert.match(source, /ktx runtime status missing/);
|
||||
assert.match(source, /runtimeStatusBefore\.kind, 'missing'/);
|
||||
assert.match(source, /Installing KTX Python runtime \(core\) with uv/);
|
||||
assert.match(source, /KTX Python runtime ready:/);
|
||||
assert.match(source, /ktx runtime status ready/);
|
||||
assert.match(source, /runtimeStatusAfter\.kind, 'ready'/);
|
||||
assert.match(source, /runtimeStatusAfter\.manifest\.features/);
|
||||
assert.match(source, /ktx runtime doctor/);
|
||||
assert.match(source, /PASS Managed Python runtime/);
|
||||
assert.match(source, /ktx runtime start/);
|
||||
assert.match(source, /ktx runtime start reuse/);
|
||||
assert.match(source, /Using existing KTX Python daemon/);
|
||||
assert.match(source, /ktx runtime stop/);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the failing package artifact tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL. The guard fails because `verifyNpmArtifacts()` still creates
|
||||
the npm-smoke `.venv`, and the installed CLI smoke assertions fail because
|
||||
`npmRuntimeSmokeSource()` does not yet isolate or verify the managed runtime.
|
||||
|
||||
### Task 2: Make the npm smoke use only the managed runtime
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/package-artifacts.mjs`
|
||||
- Modify: `scripts/package-artifacts.test.mjs`
|
||||
- Test: `scripts/package-artifacts.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Remove the npm-smoke PATH helper**
|
||||
|
||||
In `scripts/package-artifacts.mjs`, change the path import from:
|
||||
|
||||
```javascript
|
||||
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```javascript
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
```
|
||||
|
||||
Then delete this exported helper:
|
||||
|
||||
```javascript
|
||||
export function npmSmokePythonEnv(projectDir, baseEnv = process.env) {
|
||||
const binDir = process.platform === 'win32' ? join(projectDir, '.venv', 'Scripts') : join(projectDir, '.venv', 'bin');
|
||||
const existingPath = baseEnv.PATH ?? '';
|
||||
|
||||
return Object.assign({}, baseEnv, {
|
||||
PATH: existingPath ? `${binDir}${delimiter}${existingPath}` : binDir,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add runtime-smoke helpers to `npmRuntimeSmokeSource()`**
|
||||
|
||||
Inside the template string returned by `npmRuntimeSmokeSource()`, add this
|
||||
helper immediately after `requireSuccess()`:
|
||||
|
||||
```javascript
|
||||
function requireSuccessWithStderr(label, result, stderrPattern) {
|
||||
assert.equal(
|
||||
result.code,
|
||||
0,
|
||||
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
||||
);
|
||||
assert.match(result.stderr, stderrPattern, label + ' stderr did not match ' + stderrPattern);
|
||||
}
|
||||
```
|
||||
|
||||
Then replace the smoke root setup:
|
||||
|
||||
```javascript
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
|
||||
try {
|
||||
const projectDir = join(root, 'project');
|
||||
const sourceDir = join(root, 'source');
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```javascript
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
|
||||
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
|
||||
process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime');
|
||||
let daemonStarted = false;
|
||||
try {
|
||||
const projectDir = join(root, 'project');
|
||||
const sourceDir = join(root, 'source');
|
||||
```
|
||||
|
||||
Finally replace the existing `finally` block at the end of
|
||||
`npmRuntimeSmokeSource()`:
|
||||
|
||||
```javascript
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```javascript
|
||||
} finally {
|
||||
if (daemonStarted) {
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
|
||||
}
|
||||
if (previousRuntimeRoot === undefined) {
|
||||
delete process.env.KTX_RUNTIME_ROOT;
|
||||
} else {
|
||||
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create the sqlite smoke warehouse without Python**
|
||||
|
||||
Inside the template string returned by `npmRuntimeSmokeSource()`, add this
|
||||
import after the `assert` import:
|
||||
|
||||
```javascript
|
||||
import Database from 'better-sqlite3';
|
||||
```
|
||||
|
||||
Then replace the current `writeSqliteWarehouse()` function:
|
||||
|
||||
```javascript
|
||||
async function writeSqliteWarehouse(projectDir) {
|
||||
const createDb = await run('python', [
|
||||
'-c',
|
||||
[
|
||||
'import sqlite3',
|
||||
'import sys',
|
||||
'db_path = sys.argv[1]',
|
||||
'conn = sqlite3.connect(db_path)',
|
||||
'conn.executescript("""',
|
||||
'DROP TABLE IF EXISTS orders;',
|
||||
'CREATE TABLE orders (',
|
||||
' id INTEGER PRIMARY KEY,',
|
||||
' status TEXT NOT NULL,',
|
||||
' amount INTEGER NOT NULL',
|
||||
');',
|
||||
"INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);",
|
||||
'""")',
|
||||
'conn.close()',
|
||||
].join('\\n'),
|
||||
join(projectDir, 'warehouse.db'),
|
||||
]);
|
||||
requireSuccess('create sqlite warehouse', createDb);
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```javascript
|
||||
async function writeSqliteWarehouse(projectDir) {
|
||||
const database = new Database(join(projectDir, 'warehouse.db'));
|
||||
try {
|
||||
database.exec(`
|
||||
DROP TABLE IF EXISTS orders;
|
||||
CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY,
|
||||
status TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10);
|
||||
`);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Assert the isolated runtime is initially missing**
|
||||
|
||||
In `npmRuntimeSmokeSource()`, insert this block immediately after the public
|
||||
package version assertion:
|
||||
|
||||
```javascript
|
||||
const runtimeStatusBefore = parseJsonResult(
|
||||
'ktx runtime status missing',
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
|
||||
);
|
||||
assert.equal(runtimeStatusBefore.kind, 'missing');
|
||||
assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
|
||||
process.stdout.write('ktx managed runtime starts missing in isolated root\\n');
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Assert first `sl query --yes` performs lazy managed install**
|
||||
|
||||
In `npmRuntimeSmokeSource()`, replace the current `slQuery` verification block:
|
||||
|
||||
```javascript
|
||||
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--measure',
|
||||
'orders.order_count',
|
||||
'--format',
|
||||
'json',
|
||||
'--yes',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
]);
|
||||
requireSuccess('ktx sl query', slQuery);
|
||||
requireOutput('ktx sl query', slQuery, /"mode": "compile_only"/);
|
||||
requireOutput('ktx sl query', slQuery, /orders/);
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```javascript
|
||||
const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query',
|
||||
'--connection-id',
|
||||
'warehouse',
|
||||
'--measure',
|
||||
'orders.order_count',
|
||||
'--format',
|
||||
'json',
|
||||
'--yes',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
]);
|
||||
requireSuccessWithStderr(
|
||||
'ktx sl query first managed runtime install',
|
||||
slQuery,
|
||||
/Installing KTX Python runtime \(core\) with uv[\s\S]*KTX Python runtime ready:/,
|
||||
);
|
||||
requireOutput('ktx sl query first managed runtime install', slQuery, /"mode": "compile_only"/);
|
||||
requireOutput('ktx sl query first managed runtime install', slQuery, /orders/);
|
||||
|
||||
const runtimeStatusAfter = parseJsonResult(
|
||||
'ktx runtime status ready',
|
||||
await run('pnpm', ['exec', 'ktx', 'runtime', 'status', '--json']),
|
||||
);
|
||||
assert.equal(runtimeStatusAfter.kind, 'ready');
|
||||
assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']);
|
||||
assert.equal(runtimeStatusAfter.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT);
|
||||
process.stdout.write('ktx managed runtime lazy install verified\\n');
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add runtime doctor and daemon lifecycle smoke**
|
||||
|
||||
In `npmRuntimeSmokeSource()`, insert this block immediately after the
|
||||
`sqliteSlQuery` verification block:
|
||||
|
||||
```javascript
|
||||
const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'runtime', 'doctor']);
|
||||
requireSuccess('ktx runtime doctor', runtimeDoctor);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS uv/);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Bundled Python wheel/);
|
||||
requireOutput('ktx runtime doctor', runtimeDoctor, /PASS Managed Python runtime/);
|
||||
process.stdout.write('ktx runtime doctor verified\\n');
|
||||
|
||||
const runtimeStart = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
|
||||
requireSuccess('ktx runtime start', runtimeStart);
|
||||
daemonStarted = true;
|
||||
requireOutput('ktx runtime start', runtimeStart, /Started KTX Python daemon/);
|
||||
requireOutput('ktx runtime start', runtimeStart, /url: http:\/\/127\.0\.0\.1:\d+/);
|
||||
requireOutput('ktx runtime start', runtimeStart, /features: core/);
|
||||
|
||||
const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'runtime', 'start']);
|
||||
requireSuccess('ktx runtime start reuse', runtimeStartReuse);
|
||||
requireOutput('ktx runtime start reuse', runtimeStartReuse, /Using existing KTX Python daemon/);
|
||||
requireOutput('ktx runtime start reuse', runtimeStartReuse, /features: core/);
|
||||
|
||||
const runtimeStop = await run('pnpm', ['exec', 'ktx', 'runtime', 'stop']);
|
||||
requireSuccess('ktx runtime stop', runtimeStop);
|
||||
daemonStarted = false;
|
||||
requireOutput('ktx runtime stop', runtimeStop, /Stopped KTX Python daemon/);
|
||||
process.stdout.write('ktx runtime daemon lifecycle verified\\n');
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Remove npm-smoke Python preparation from artifact verification**
|
||||
|
||||
In `scripts/package-artifacts.mjs`, replace `verifyNpmArtifacts()` with this
|
||||
implementation:
|
||||
|
||||
```javascript
|
||||
async function verifyNpmArtifacts(layout, tmpRoot) {
|
||||
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
|
||||
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
|
||||
}
|
||||
|
||||
const projectDir = join(tmpRoot, 'npm-clean-install');
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
await writeFile(
|
||||
join(projectDir, 'package.json'),
|
||||
`${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`,
|
||||
);
|
||||
await writeFile(join(projectDir, 'verify-npm.mjs'), npmVerifySource());
|
||||
await writeFile(join(projectDir, 'verify-installed-cli.mjs'), npmRuntimeSmokeSource());
|
||||
await writeFile(join(projectDir, 'verify-installed-demo.mjs'), npmDemoSmokeSource());
|
||||
|
||||
await runCommand('pnpm', ['install'], { cwd: projectDir });
|
||||
await runCommand('pnpm', ['rebuild', 'better-sqlite3'], { cwd: projectDir });
|
||||
await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir });
|
||||
await runCommand('pnpm', ['exec', 'ktx', '--version'], { cwd: projectDir });
|
||||
await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir });
|
||||
await runCommand('node', ['verify-installed-demo.mjs'], { cwd: projectDir });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run the focused package artifact tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 9: Commit the release-smoke implementation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
|
||||
git commit -m "test: verify managed runtime in public package smoke"
|
||||
```
|
||||
|
||||
### Task 3: Verify the release-smoke surface
|
||||
|
||||
**Files:**
|
||||
|
||||
- Test: `scripts/package-artifacts.test.mjs`
|
||||
- Test: `scripts/package-artifacts.mjs`
|
||||
|
||||
- [ ] **Step 1: Run script unit tests that cover artifact packaging**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/build-python-runtime-wheel.test.mjs scripts/build-public-npm-package.test.mjs scripts/package-artifacts.test.mjs scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run the public package artifact smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run artifacts:verify
|
||||
```
|
||||
|
||||
Expected: PASS. The `verify-installed-cli.mjs` output must include:
|
||||
|
||||
```text
|
||||
ktx managed runtime starts missing in isolated root
|
||||
ktx managed runtime lazy install verified
|
||||
ktx runtime doctor verified
|
||||
ktx runtime daemon lifecycle verified
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run release readiness**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:readiness
|
||||
```
|
||||
|
||||
Expected: PASS. The report must still list `@kaelio/ktx` as the only npm
|
||||
package and must still report registry publishing as disabled by
|
||||
`release-policy.json`.
|
||||
|
||||
- [ ] **Step 4: Run pre-commit for changed files**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
if [ -d .venv ]; then source .venv/bin/activate; fi
|
||||
uv run pre-commit run --files scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. If pre-commit cannot run because the local environment lacks a
|
||||
compatible hook version, record the exact failure and keep the passing
|
||||
`node --test` and artifact smoke results.
|
||||
|
||||
- [ ] **Step 5: Commit verification fixes if needed**
|
||||
|
||||
If Step 1, Step 2, Step 3, or Step 4 required edits, run:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
|
||||
git commit -m "test: finalize managed runtime release smoke"
|
||||
```
|
||||
|
||||
If no files changed after Task 2, do not create an empty commit.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `verifyNpmArtifacts()` no longer creates a Python `.venv`, no longer calls
|
||||
`pythonArtifactInstallArgs()`, and no longer runs npm smoke scripts with a
|
||||
custom Python venv at the front of `PATH`.
|
||||
- The installed public npm smoke creates its sqlite warehouse with
|
||||
`better-sqlite3` and does not shell out to `python`.
|
||||
- The installed public npm smoke sets an isolated `KTX_RUNTIME_ROOT` and
|
||||
confirms that `ktx runtime status --json` starts as `missing`.
|
||||
- The first installed `ktx sl query --yes` installs the `core` managed Python
|
||||
runtime from bundled npm package assets and still returns compile-only SQL.
|
||||
- A second semantic query executes against sqlite using the installed managed
|
||||
runtime.
|
||||
- `ktx runtime doctor` passes after lazy install.
|
||||
- `ktx runtime start` starts a core daemon, a second `ktx runtime start` reuses
|
||||
the daemon, and `ktx runtime stop` stops it.
|
||||
- The separate Python artifact verification still installs and tests the
|
||||
Python wheel directly.
|
||||
- Focused script tests, `pnpm run artifacts:verify`, release readiness, and
|
||||
pre-commit pass or have explicitly recorded environment blockers.
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: the previous six plans cover the bundled wheel, runtime
|
||||
installer, `sl query` command integration, daemon lifecycle, local embeddings,
|
||||
and public npm package surface. This plan covers release-flow checks for clean
|
||||
install of the packed npm package, first-run managed runtime install from the
|
||||
bundled wheel, one-shot semantic-layer query through the managed runtime,
|
||||
runtime status and doctor output, and daemon start/reuse/stop.
|
||||
- Remaining intentional gap: optional `local-embeddings` smoke remains outside
|
||||
the default release artifact smoke because the spec permits it in a separate
|
||||
job or opt-in check and the dependency downloads are large.
|
||||
- Placeholder scan: no steps contain placeholder implementation language.
|
||||
- Type consistency: runtime feature names remain `core` and
|
||||
`local-embeddings`; the public npm package name remains `@kaelio/ktx`; the
|
||||
runtime root environment variable is `KTX_RUNTIME_ROOT`.
|
||||
|
|
@ -0,0 +1,657 @@
|
|||
# Managed Runtime Docs and Postgres Smoke Cleanup Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Remove the remaining manual Python service guidance from the Postgres
|
||||
historic SQL smoke and update public docs so the npm-managed Python runtime is
|
||||
the documented path.
|
||||
|
||||
**Architecture:** Keep the existing managed-runtime code unchanged. Add source
|
||||
and docs guards first, then make the Postgres historic smoke use the
|
||||
CLI-managed core daemon through `createKtxCliLocalIngestAdapters()`, and update
|
||||
the README files that still describe internal package artifacts, manual
|
||||
`ktx-daemon` startup, or `python-service/`.
|
||||
|
||||
**Tech Stack:** Bash, Node 22 ESM, `node:test`, Markdown, pnpm, uv, KTX CLI
|
||||
managed Python runtime.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plans are based on that spec and are already implemented in this
|
||||
worktree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `scripts/build-python-runtime-wheel.mjs` and
|
||||
`packages/cli/assets/python/manifest.json`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`,
|
||||
`packages/cli/src/runtime.ts`, and
|
||||
`packages/cli/src/commands/runtime-commands.ts`.
|
||||
- `packages/cli/src/managed-python-command.ts` and managed `ktx sl query`
|
||||
runtime policy flags.
|
||||
- `packages/cli/src/managed-python-daemon.ts` and `ktx runtime start` /
|
||||
`ktx runtime stop`.
|
||||
- `packages/cli/src/managed-local-embeddings.ts` and local embeddings setup
|
||||
wiring.
|
||||
- `scripts/build-public-npm-package.mjs`, release policy updates, release
|
||||
smoke coverage, and opt-in local embeddings smoke coverage.
|
||||
- `packages/cli/src/agent-runtime.ts` and `packages/cli/src/serve.ts` now
|
||||
create managed semantic-layer compute when no explicit semantic HTTP URL is
|
||||
provided.
|
||||
- `packages/cli/src/managed-python-http.ts`,
|
||||
`packages/cli/src/local-adapters.ts`, `packages/cli/src/ingest.ts`,
|
||||
`packages/cli/src/scan.ts`, and `packages/cli/src/serve.ts` wire local ingest
|
||||
helper paths to the managed core daemon.
|
||||
|
||||
The remaining drift is documentation and one example smoke script:
|
||||
|
||||
- `examples/postgres-historic/scripts/smoke.sh` still checks for
|
||||
`python-service/.venv`, starts `uvicorn app.main:app`, and exports
|
||||
`KTX_SQL_ANALYSIS_URL`.
|
||||
- `examples/postgres-historic/README.md` still documents
|
||||
`python-service/.venv` or `KTX_SQL_ANALYSIS_URL` as a prerequisite.
|
||||
- `examples/package-artifacts/README.md` still says the npm smoke installs
|
||||
generated `@ktx/context` and `@ktx/cli` tarballs.
|
||||
- `README.md` still presents source-tree `pnpm run ktx -- ...` commands as the
|
||||
quick start and tells users to start `ktx-daemon` manually for MCP.
|
||||
|
||||
This plan closes that drift. It does not rename internal workspace packages and
|
||||
does not remove explicit daemon URL override behavior from production code.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `scripts/examples-docs.test.mjs`: add regression coverage for managed
|
||||
runtime docs, public npm package docs, and the Postgres smoke script.
|
||||
- Modify `examples/postgres-historic/scripts/smoke.sh`: remove
|
||||
`python-service/` startup and pass managed daemon options into stage-only
|
||||
historic SQL ingest.
|
||||
- Modify `examples/postgres-historic/README.md`: document the managed runtime
|
||||
and remove old SQL-analysis service instructions.
|
||||
- Modify `examples/package-artifacts/README.md`: describe the single public
|
||||
`@kaelio/ktx` npm artifact and managed runtime smoke.
|
||||
- Modify `README.md`: make public `@kaelio/ktx` invocation modes and managed
|
||||
runtime commands visible while keeping source-tree development commands in
|
||||
the development section.
|
||||
|
||||
### Task 1: Add failing docs and smoke guards
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/examples-docs.test.mjs`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add public runtime README assertions**
|
||||
|
||||
In `scripts/examples-docs.test.mjs`, insert this test after the existing
|
||||
`walks through ktx connection list and ktx connection test in the README
|
||||
quickstart` test:
|
||||
|
||||
```javascript
|
||||
it('documents public npm and managed runtime usage in the README', async () => {
|
||||
const rootReadme = await readText('README.md');
|
||||
|
||||
assert.match(rootReadme, /npx @kaelio\/ktx setup demo --no-input/);
|
||||
assert.match(rootReadme, /npx @kaelio\/ktx sl query/);
|
||||
assert.match(rootReadme, /npm install @kaelio\/ktx/);
|
||||
assert.match(rootReadme, /npm install -g @kaelio\/ktx/);
|
||||
assert.match(rootReadme, /ktx runtime install/);
|
||||
assert.match(rootReadme, /ktx runtime status/);
|
||||
assert.match(rootReadme, /ktx runtime doctor/);
|
||||
assert.match(rootReadme, /ktx runtime start/);
|
||||
assert.match(rootReadme, /ktx runtime stop/);
|
||||
assert.match(rootReadme, /ktx serve --mcp stdio/);
|
||||
assert.doesNotMatch(rootReadme, /uv run ktx-daemon serve-http/);
|
||||
assert.doesNotMatch(rootReadme, /--semantic-compute-url http:\/\/127\.0\.0\.1:8765/);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add package artifact README assertions**
|
||||
|
||||
In `scripts/examples-docs.test.mjs`, insert this test after the new public
|
||||
runtime README test:
|
||||
|
||||
```javascript
|
||||
it('documents the public package artifact smoke shape', async () => {
|
||||
const readme = await readText('examples/package-artifacts/README.md');
|
||||
|
||||
assert.match(readme, /@kaelio\/ktx/);
|
||||
assert.match(readme, /managed Python runtime/);
|
||||
assert.match(readme, /ktx runtime status/);
|
||||
assert.match(readme, /ktx runtime doctor/);
|
||||
assert.doesNotMatch(readme, /@ktx\/context/);
|
||||
assert.doesNotMatch(readme, /@ktx\/cli/);
|
||||
assert.doesNotMatch(readme, /python -m ktx_daemon semantic-validate/);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend Postgres smoke assertions**
|
||||
|
||||
In the existing `documents the Postgres historic SQL smoke example` test in
|
||||
`scripts/examples-docs.test.mjs`, add these assertions after
|
||||
`assert.match(smoke, /pg_stat_statements_reset/);`:
|
||||
|
||||
```javascript
|
||||
assert.match(smoke, /KTX_RUNTIME_ROOT/);
|
||||
assert.match(smoke, /managedDaemon/);
|
||||
assert.match(smoke, /installPolicy: 'auto'/);
|
||||
assert.match(smoke, /getKtxCliPackageInfo/);
|
||||
assert.doesNotMatch(smoke, /python-service/);
|
||||
assert.doesNotMatch(smoke, /PYTHON_SERVICE/);
|
||||
assert.doesNotMatch(smoke, /uvicorn app\.main:app/);
|
||||
assert.doesNotMatch(smoke, /export KTX_SQL_ANALYSIS_URL/);
|
||||
assert.doesNotMatch(readme, /python-service/);
|
||||
assert.doesNotMatch(readme, /KTX_SQL_ANALYSIS_URL/);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the docs test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL. The failure includes missing `@kaelio/ktx` README matches and
|
||||
the existing `python-service` / `KTX_SQL_ANALYSIS_URL` references in the
|
||||
Postgres smoke files.
|
||||
|
||||
### Task 2: Move the Postgres historic smoke to the managed runtime
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `examples/postgres-historic/scripts/smoke.sh`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Remove Python service process state**
|
||||
|
||||
In `examples/postgres-historic/scripts/smoke.sh`, replace the variable block:
|
||||
|
||||
```bash
|
||||
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
|
||||
PYTHON_SERVICE_LOG="$PROJECT_PARENT/python-service.log"
|
||||
PYTHON_SERVICE_PID=""
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```bash
|
||||
KTX_BIN="$KTX_ROOT/packages/cli/dist/bin.js"
|
||||
export KTX_RUNTIME_ROOT="$PROJECT_PARENT/managed-runtime"
|
||||
unset KTX_DAEMON_URL
|
||||
unset KTX_SQL_ANALYSIS_URL
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace cleanup**
|
||||
|
||||
In `examples/postgres-historic/scripts/smoke.sh`, replace the `cleanup()`
|
||||
function with:
|
||||
|
||||
```bash
|
||||
cleanup() {
|
||||
if [[ -f "$KTX_BIN" ]]; then
|
||||
node "$KTX_BIN" runtime stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ "${KTX_POSTGRES_HISTORIC_KEEP_DOCKER:-0}" != "1" ]]; then
|
||||
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Delete the old SQL analysis service starter**
|
||||
|
||||
Delete the entire `start_sql_analysis_if_needed()` function from
|
||||
`examples/postgres-historic/scripts/smoke.sh`. The deleted function begins with
|
||||
this line:
|
||||
|
||||
```bash
|
||||
start_sql_analysis_if_needed() {
|
||||
```
|
||||
|
||||
and ends with this line:
|
||||
|
||||
```bash
|
||||
}
|
||||
```
|
||||
|
||||
immediately before the `latest_manifest()` function.
|
||||
|
||||
- [ ] **Step 4: Pass managed daemon options to stage-only ingest**
|
||||
|
||||
In the Node heredoc inside `run_historic_stage_only()`, replace this block:
|
||||
|
||||
```javascript
|
||||
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
|
||||
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const adapters = createKtxCliLocalIngestAdapters(project, { historicSqlConnectionId: 'warehouse' });
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```javascript
|
||||
const { createKtxCliLocalIngestAdapters } = await import(join(ktxRoot, 'packages/cli/dist/local-adapters.js'));
|
||||
const { getKtxCliPackageInfo } = await import(join(ktxRoot, 'packages/cli/dist/index.js'));
|
||||
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const cliVersion = getKtxCliPackageInfo().version;
|
||||
const managedRuntimeIo = { stdout: process.stdout, stderr: process.stderr };
|
||||
const adapters = createKtxCliLocalIngestAdapters(project, {
|
||||
historicSqlConnectionId: 'warehouse',
|
||||
managedDaemon: {
|
||||
cliVersion,
|
||||
installPolicy: 'auto',
|
||||
io: managedRuntimeIo,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Remove the old starter call**
|
||||
|
||||
Delete this line from the bottom half of
|
||||
`examples/postgres-historic/scripts/smoke.sh`:
|
||||
|
||||
```bash
|
||||
start_sql_analysis_if_needed
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the docs test to verify the script guards pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL remains because README files have not been updated yet. The
|
||||
Postgres smoke script assertions now pass.
|
||||
|
||||
### Task 3: Update Postgres historic and artifact docs
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `examples/postgres-historic/README.md`
|
||||
- Modify: `examples/package-artifacts/README.md`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Replace Postgres prerequisites**
|
||||
|
||||
In `examples/postgres-historic/README.md`, replace the `## Prerequisites`
|
||||
section with:
|
||||
|
||||
```markdown
|
||||
## Prerequisites
|
||||
|
||||
- Docker with Compose v2
|
||||
- Node and pnpm matching the KTX workspace
|
||||
- `uv` on `PATH` so the KTX-managed Python runtime can install the bundled
|
||||
runtime wheel
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the smoke run description**
|
||||
|
||||
In `examples/postgres-historic/README.md`, replace the paragraph after the
|
||||
`examples/postgres-historic/scripts/smoke.sh` command with:
|
||||
|
||||
```markdown
|
||||
The smoke creates a temporary KTX project, isolates the managed Python runtime
|
||||
under the temporary project parent, starts Postgres on `127.0.0.1:55432`, and
|
||||
uses this connection URL:
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the full ingest command**
|
||||
|
||||
In `examples/postgres-historic/README.md`, replace the manual ingest command:
|
||||
|
||||
```bash
|
||||
node packages/cli/dist/bin.js --project-dir /tmp/ktx-postgres-historic dev ingest run \
|
||||
--connection-id warehouse \
|
||||
--adapter historic-sql \
|
||||
--plain \
|
||||
--no-input
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```bash
|
||||
pnpm run ktx -- dev ingest run --project-dir /tmp/ktx-postgres-historic \
|
||||
--connection-id warehouse \
|
||||
--adapter historic-sql \
|
||||
--plain \
|
||||
--yes \
|
||||
--no-input
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace SQL-analysis troubleshooting**
|
||||
|
||||
In `examples/postgres-historic/README.md`, replace the final troubleshooting
|
||||
bullet:
|
||||
|
||||
```markdown
|
||||
- SQL-analysis failures: set `KTX_SQL_ANALYSIS_URL` to the running service URL
|
||||
or create `python-service/.venv` before running `scripts/smoke.sh`.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
- SQL-analysis failures: run `pnpm run ktx -- runtime doctor` from the KTX
|
||||
repository root and confirm `uv`, the bundled Python wheel, and the managed
|
||||
runtime all pass.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Replace package artifact README body**
|
||||
|
||||
Replace the full contents of `examples/package-artifacts/README.md` with:
|
||||
|
||||
````markdown
|
||||
# Package artifact smoke checks
|
||||
|
||||
The package artifact smoke checks create temporary projects instead of storing
|
||||
sample projects in this directory. Run the checks from `ktx/`:
|
||||
|
||||
```bash
|
||||
pnpm run artifacts:check
|
||||
```
|
||||
|
||||
The npm smoke project installs the generated public `@kaelio/ktx` tarball,
|
||||
imports the package entry point, and runs installed `ktx` commands against a
|
||||
generated local project.
|
||||
|
||||
The managed runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
|
||||
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
|
||||
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
|
||||
daemon, and stops it.
|
||||
|
||||
The Python smoke project still installs the Python artifacts directly because
|
||||
it verifies the standalone Python distributions that feed the bundled runtime
|
||||
wheel.
|
||||
````
|
||||
|
||||
- [ ] **Step 6: Run the docs test to verify these docs pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL remains because `README.md` still lacks the public npm managed
|
||||
runtime documentation. The Postgres and package artifact assertions now pass.
|
||||
|
||||
### Task 4: Update the root README public runtime path
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `README.md`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Replace quick start**
|
||||
|
||||
In `README.md`, replace the `## Quick start` section through the end of the
|
||||
full-demo paragraph with:
|
||||
|
||||
````markdown
|
||||
## Quick start
|
||||
|
||||
Run the pre-seeded demo through the public npm package:
|
||||
|
||||
```bash
|
||||
npx @kaelio/ktx setup demo --no-input
|
||||
npx @kaelio/ktx setup demo inspect
|
||||
```
|
||||
|
||||
The default demo uses packaged sample data and prebuilt context. It does not
|
||||
require API keys, network access, or an LLM provider.
|
||||
|
||||
To replay the packaged ingest run, use:
|
||||
|
||||
```bash
|
||||
npx @kaelio/ktx setup demo --mode replay --no-input
|
||||
```
|
||||
|
||||
To run the full agentic demo with an LLM provider, set a provider key for the
|
||||
current process:
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=$YOUR_ANTHROPIC_API_KEY \
|
||||
npx @kaelio/ktx setup demo --mode full --no-input
|
||||
```
|
||||
|
||||
Interactive full-demo setup can prompt for a provider key without writing the
|
||||
key to `ktx.yaml`.
|
||||
|
||||
You can also install the CLI in a project or globally:
|
||||
|
||||
```bash
|
||||
npm install @kaelio/ktx
|
||||
npx ktx --help
|
||||
npm install -g @kaelio/ktx
|
||||
ktx --help
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 2: Replace local project setup command**
|
||||
|
||||
In the `## Build a local project` section of `README.md`, replace:
|
||||
|
||||
```bash
|
||||
uv sync --all-packages
|
||||
source .venv/bin/activate
|
||||
|
||||
PROJECT_DIR="$(mktemp -d)/ktx-demo"
|
||||
pnpm run ktx -- init "$PROJECT_DIR" --name ktx-demo
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```bash
|
||||
npm install @kaelio/ktx
|
||||
PROJECT_DIR="$(mktemp -d)/ktx-demo"
|
||||
npx ktx init "$PROJECT_DIR" --name ktx-demo
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Replace README command prefixes**
|
||||
|
||||
In `README.md`, replace the source-tree command prefix `pnpm run ktx --` with
|
||||
`npx ktx` in all user workflow commands under `## Build a local project`,
|
||||
`### Scan the demo warehouse`, and `## Serve MCP`. Keep `pnpm run ktx --` in
|
||||
the `## Development` section.
|
||||
|
||||
For example, this command:
|
||||
|
||||
```bash
|
||||
pnpm run ktx -- sl query --project-dir "$PROJECT_DIR" \
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```bash
|
||||
npx ktx sl query --project-dir "$PROJECT_DIR" \
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add managed runtime section**
|
||||
|
||||
Insert this section after the scan walkthrough in `README.md`:
|
||||
|
||||
````markdown
|
||||
## Managed Python runtime
|
||||
|
||||
KTX installs its Python runtime only when a Python-backed command needs it.
|
||||
The runtime lives outside the npm cache, is versioned by the installed CLI
|
||||
version, and is managed by `ktx runtime` commands:
|
||||
|
||||
```bash
|
||||
npx ktx runtime install --yes
|
||||
npx ktx runtime status
|
||||
npx ktx runtime doctor
|
||||
npx ktx runtime start
|
||||
npx ktx runtime stop
|
||||
```
|
||||
|
||||
Commands such as `npx @kaelio/ktx sl query ... --yes` can install the core
|
||||
runtime lazily from the bundled wheel. Local embeddings remain lazy; prepare
|
||||
them only when you select local `sentence-transformers` embeddings:
|
||||
|
||||
```bash
|
||||
npx ktx runtime install --feature local-embeddings --yes
|
||||
npx ktx runtime start --feature local-embeddings
|
||||
```
|
||||
````
|
||||
|
||||
- [ ] **Step 5: Replace Serve MCP section**
|
||||
|
||||
In `README.md`, replace the full `## Serve MCP` section with:
|
||||
|
||||
````markdown
|
||||
## Serve MCP
|
||||
|
||||
Start the stdio MCP server from the project directory:
|
||||
|
||||
```bash
|
||||
npx ktx serve --mcp stdio --project-dir "$PROJECT_DIR" \
|
||||
--user-id local \
|
||||
--semantic-compute \
|
||||
--execute-queries \
|
||||
--yes
|
||||
```
|
||||
|
||||
The `--semantic-compute` flag uses the managed Python runtime when no explicit
|
||||
semantic compute URL is provided. KTX starts or reuses the managed runtime as
|
||||
needed.
|
||||
|
||||
The MCP server exposes `connection_list`, `knowledge_search`,
|
||||
`knowledge_read`, `knowledge_write`, `sl_list_sources`, `sl_read_source`,
|
||||
`sl_write_source`, `sl_validate`, `sl_query`, `ingest_trigger`,
|
||||
`ingest_status`, `ingest_report`, and `ingest_replay`.
|
||||
````
|
||||
|
||||
- [ ] **Step 6: Update release status wording**
|
||||
|
||||
In `README.md`, replace this sentence in `## Release status`:
|
||||
|
||||
```markdown
|
||||
This repository is prepared for source publication. Package publishing is still
|
||||
disabled by `release-policy.json`; registry names, public versions, package
|
||||
visibility, and provenance policy must be chosen before publishing artifacts to
|
||||
npm or Python package indexes.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
This repository builds a single public npm artifact named `@kaelio/ktx`.
|
||||
Package publishing is still disabled by `release-policy.json`; registry
|
||||
credentials, public versions, release tags, and provenance policy must be
|
||||
chosen before publishing artifacts to npm or Python package indexes.
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Run the docs test to verify the README passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 5: Final verification and commit
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `scripts/examples-docs.test.mjs`
|
||||
- Verify: `examples/postgres-historic/scripts/smoke.sh`
|
||||
- Verify: `examples/postgres-historic/README.md`
|
||||
- Verify: `examples/package-artifacts/README.md`
|
||||
- Verify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Run the script test suite affected by docs**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs scripts/check-boundaries.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run the boundary check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node scripts/check-boundaries.mjs
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
ktx boundary check passed
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Search for removed external runtime references**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "python-service|uvicorn app\\.main:app|export KTX_SQL_ANALYSIS_URL|uv run ktx-daemon serve-http|@ktx/context.*@ktx/cli" README.md examples/postgres-historic/README.md examples/postgres-historic/scripts/smoke.sh examples/package-artifacts/README.md
|
||||
```
|
||||
|
||||
Expected: no matches.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/examples-docs.test.mjs \
|
||||
examples/postgres-historic/scripts/smoke.sh \
|
||||
examples/postgres-historic/README.md \
|
||||
examples/package-artifacts/README.md \
|
||||
README.md
|
||||
git commit -m "docs: align managed runtime examples"
|
||||
```
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The Postgres historic SQL smoke no longer references `python-service/`,
|
||||
`uvicorn app.main:app`, or `export KTX_SQL_ANALYSIS_URL`.
|
||||
- The stage-only Postgres historic smoke uses `createKtxCliLocalIngestAdapters`
|
||||
with managed daemon options and `installPolicy: 'auto'`.
|
||||
- The root README documents `npx @kaelio/ktx`, local `npx ktx`, global `ktx`,
|
||||
`ktx runtime ...`, and MCP `--semantic-compute --yes` managed-runtime usage.
|
||||
- Package artifact docs describe the single public `@kaelio/ktx` tarball and
|
||||
the managed runtime smoke.
|
||||
- `node --test scripts/examples-docs.test.mjs scripts/check-boundaries.test.mjs`
|
||||
passes.
|
||||
- `node scripts/check-boundaries.mjs` passes.
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: This plan covers the remaining user-facing drift from the
|
||||
npm-managed runtime spec by removing manual Python service guidance,
|
||||
documenting public `@kaelio/ktx` invocation modes, and making the Postgres
|
||||
example smoke use the managed core daemon.
|
||||
- Placeholder scan: The plan contains exact files, edits, commands, expected
|
||||
outcomes, and commit instructions.
|
||||
- Type consistency: The plan uses the existing `managedDaemon` option shape
|
||||
from `packages/cli/src/local-adapters.ts` and the existing
|
||||
`installPolicy: 'auto'` value from `packages/cli/src/managed-python-command.ts`.
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
# Managed Runtime Prune Smoke and Docs Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Prove and document `ktx runtime prune` as part of the npm-managed
|
||||
Python runtime release contract.
|
||||
|
||||
**Architecture:** The prune command already exists in the CLI runtime layer, so
|
||||
this plan adds black-box package smoke coverage and public documentation only.
|
||||
The smoke creates an isolated stale versioned runtime directory, previews it,
|
||||
verifies confirmation is required, and removes it through the installed
|
||||
`@kaelio/ktx` package.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, Markdown, KTX CLI
|
||||
managed Python runtime.
|
||||
|
||||
---
|
||||
|
||||
## Current state
|
||||
|
||||
This plan follows
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plan files are based on that spec and are implemented in the
|
||||
current tree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
|
||||
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `packages/cli/assets/python/manifest.json` and
|
||||
`packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`, including
|
||||
`installManagedPythonRuntime()`, `doctorManagedPythonRuntime()`, and
|
||||
`pruneManagedPythonRuntimes()`.
|
||||
- `packages/cli/src/runtime.ts`, including the `install`, `status`,
|
||||
`doctor`, `start`, `stop`, and `prune` runtime command runner branches.
|
||||
- `packages/cli/src/commands/runtime-commands.ts`, including the
|
||||
`runtime prune --dry-run` and `runtime prune --yes` Commander wiring.
|
||||
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
|
||||
`scripts/published-package-smoke.mjs`, `scripts/local-embeddings-runtime-smoke.mjs`,
|
||||
`scripts/publish-public-npm-package.mjs`, `release-policy.json`, and
|
||||
`.github/workflows/release.yml`.
|
||||
- `README.md` and `examples/package-artifacts/README.md` document the managed
|
||||
runtime but do not mention `ktx runtime prune`.
|
||||
|
||||
The remaining gap is narrow: the spec lists `ktx runtime prune` as part of the
|
||||
runtime management command family, but public docs and installed package smoke
|
||||
coverage only prove `install`, `status`, `doctor`, `start`, and `stop`.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `scripts/package-artifacts.test.mjs`: assert that the generated
|
||||
installed npm smoke covers `ktx runtime prune --dry-run`, confirmation
|
||||
failure, and confirmed deletion.
|
||||
- Modify `scripts/package-artifacts.mjs`: extend `npmRuntimeSmokeSource()` to
|
||||
create a stale runtime directory and exercise `ktx runtime prune`.
|
||||
- Modify `scripts/examples-docs.test.mjs`: require public docs to mention
|
||||
`ktx runtime prune --dry-run` and `ktx runtime prune --yes`.
|
||||
- Modify `README.md`: add prune commands and one sentence describing preview
|
||||
and confirmed deletion.
|
||||
- Modify `examples/package-artifacts/README.md`: describe prune coverage in the
|
||||
package artifact smoke.
|
||||
|
||||
### Task 1: Add installed package prune smoke coverage
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/package-artifacts.test.mjs`
|
||||
- Modify: `scripts/package-artifacts.mjs`
|
||||
|
||||
- [ ] **Step 1: Add failing smoke-source assertions**
|
||||
|
||||
In `scripts/package-artifacts.test.mjs`, inside
|
||||
`it('runs installed CLI commands through the public package runtime', () => {`
|
||||
and immediately after the existing assertions for `ktx runtime stop`, add:
|
||||
|
||||
```javascript
|
||||
assert.match(source, /ktx runtime prune dry run/);
|
||||
assert.match(source, /0\.0\.0/);
|
||||
assert.match(source, /ktx runtime prune needs confirmation/);
|
||||
assert.match(source, /Refusing to prune without --yes/);
|
||||
assert.match(source, /ktx runtime prune confirmed/);
|
||||
assert.match(source, /Removed stale KTX Python runtimes/);
|
||||
assert.match(source, /assert\.rejects\(\(\) => access\(staleRuntimeDir\)\)/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the package artifact test and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL in the installed CLI smoke source test because
|
||||
`npmRuntimeSmokeSource()` does not yet contain the prune labels, confirmation
|
||||
guard, or stale runtime removal assertion.
|
||||
|
||||
- [ ] **Step 3: Extend the generated installed CLI smoke**
|
||||
|
||||
In `scripts/package-artifacts.mjs`, inside `npmRuntimeSmokeSource()`, add this
|
||||
block immediately after:
|
||||
|
||||
```javascript
|
||||
process.stdout.write('ktx runtime daemon lifecycle verified\n');
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```javascript
|
||||
const staleRuntimeDir = join(process.env.KTX_RUNTIME_ROOT, '0.0.0');
|
||||
await mkdir(staleRuntimeDir, { recursive: true });
|
||||
|
||||
const runtimePruneDryRun = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--dry-run']);
|
||||
requireSuccess('ktx runtime prune dry run', runtimePruneDryRun);
|
||||
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /Stale KTX Python runtimes/);
|
||||
requireOutput('ktx runtime prune dry run', runtimePruneDryRun, /0\.0\.0/);
|
||||
await access(staleRuntimeDir);
|
||||
|
||||
const runtimePruneNeedsConfirmation = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune']);
|
||||
assert.equal(runtimePruneNeedsConfirmation.code, 1, 'ktx runtime prune without --yes must fail');
|
||||
assert.equal(runtimePruneNeedsConfirmation.stdout, '', 'ktx runtime prune confirmation failure wrote stdout');
|
||||
assert.match(runtimePruneNeedsConfirmation.stderr, /Refusing to prune without --yes/);
|
||||
|
||||
const runtimePruneConfirmed = await run('pnpm', ['exec', 'ktx', 'runtime', 'prune', '--yes']);
|
||||
requireSuccess('ktx runtime prune confirmed', runtimePruneConfirmed);
|
||||
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /Removed stale KTX Python runtimes/);
|
||||
requireOutput('ktx runtime prune confirmed', runtimePruneConfirmed, /0\.0\.0/);
|
||||
await assert.rejects(() => access(staleRuntimeDir));
|
||||
process.stdout.write('ktx runtime prune verified\n');
|
||||
```
|
||||
|
||||
No import changes are needed because the generated smoke already imports
|
||||
`assert`, `access`, `mkdir`, and `join`.
|
||||
|
||||
- [ ] **Step 4: Run the package artifact test and verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The source assertions now find prune dry-run coverage,
|
||||
confirmation failure coverage, confirmed prune coverage, and stale directory
|
||||
deletion verification.
|
||||
|
||||
- [ ] **Step 5: Commit the smoke coverage**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
|
||||
git commit -m "test: cover managed runtime prune in package smoke"
|
||||
```
|
||||
|
||||
### Task 2: Document runtime prune in public docs
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/examples-docs.test.mjs`
|
||||
- Modify: `README.md`
|
||||
- Modify: `examples/package-artifacts/README.md`
|
||||
|
||||
- [ ] **Step 1: Add failing docs assertions**
|
||||
|
||||
In `scripts/examples-docs.test.mjs`, inside
|
||||
`it('documents public npm and managed runtime usage in the README', async () => {`
|
||||
and immediately after:
|
||||
|
||||
```javascript
|
||||
assert.match(rootReadme, /ktx runtime stop/);
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```javascript
|
||||
assert.match(rootReadme, /ktx runtime prune --dry-run/);
|
||||
assert.match(rootReadme, /ktx runtime prune --yes/);
|
||||
```
|
||||
|
||||
In the same file, inside
|
||||
`it('documents the public package artifact smoke shape', async () => {` and
|
||||
immediately after:
|
||||
|
||||
```javascript
|
||||
assert.match(readme, /ktx runtime doctor/);
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```javascript
|
||||
assert.match(readme, /ktx runtime prune --dry-run/);
|
||||
assert.match(readme, /ktx runtime prune --yes/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the docs test and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL because `README.md` and
|
||||
`examples/package-artifacts/README.md` do not yet mention `ktx runtime prune`.
|
||||
|
||||
- [ ] **Step 3: Update the root README runtime section**
|
||||
|
||||
In `README.md`, in the `## Managed Python runtime` command block, replace:
|
||||
|
||||
```bash
|
||||
npx ktx runtime install --yes
|
||||
npx ktx runtime status
|
||||
npx ktx runtime doctor
|
||||
npx ktx runtime start
|
||||
npx ktx runtime stop
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```bash
|
||||
npx ktx runtime install --yes
|
||||
npx ktx runtime status
|
||||
npx ktx runtime doctor
|
||||
npx ktx runtime start
|
||||
npx ktx runtime stop
|
||||
npx ktx runtime prune --dry-run
|
||||
npx ktx runtime prune --yes
|
||||
```
|
||||
|
||||
Immediately after that command block, add:
|
||||
|
||||
```markdown
|
||||
Use `runtime prune --dry-run` to preview stale runtime directories from older
|
||||
CLI versions. Add `--yes` to remove those stale directories after daemon
|
||||
processes are stopped.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update package artifact smoke docs**
|
||||
|
||||
In `examples/package-artifacts/README.md`, replace:
|
||||
|
||||
```markdown
|
||||
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
|
||||
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
|
||||
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
|
||||
daemon, and stops it.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
|
||||
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
|
||||
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
|
||||
daemon, stops it, previews a stale runtime with `ktx runtime prune --dry-run`,
|
||||
verifies confirmation is required, and removes the stale runtime with
|
||||
`ktx runtime prune --yes`.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the docs test and verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The public README and package artifact README now document
|
||||
runtime prune alongside the other managed runtime commands.
|
||||
|
||||
- [ ] **Step 6: Commit the docs coverage**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
|
||||
git commit -m "docs: document managed runtime prune"
|
||||
```
|
||||
|
||||
### Task 3: Verify the completed prune release surface
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `scripts/package-artifacts.mjs`
|
||||
- Verify: `scripts/package-artifacts.test.mjs`
|
||||
- Verify: `scripts/examples-docs.test.mjs`
|
||||
- Verify: `README.md`
|
||||
- Verify: `examples/package-artifacts/README.md`
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The source-level tests cover generated package smoke behavior
|
||||
and docs assertions.
|
||||
|
||||
- [ ] **Step 2: Run the installed package artifact smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run artifacts:check
|
||||
```
|
||||
|
||||
Expected: PASS. The generated installed CLI smoke prints:
|
||||
|
||||
```text
|
||||
ktx runtime prune verified
|
||||
```
|
||||
|
||||
and removes the temporary `0.0.0` directory from the isolated
|
||||
`KTX_RUNTIME_ROOT`.
|
||||
|
||||
- [ ] **Step 3: Inspect git status**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: only the five planned files are modified before the final commit, or
|
||||
no modified files remain after the task commits.
|
||||
|
||||
- [ ] **Step 4: Commit verification fixes if needed**
|
||||
|
||||
If verification required small corrections, commit only those intended files:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
|
||||
git commit -m "test: verify managed runtime prune release surface"
|
||||
```
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- The generated installed npm package smoke creates a stale versioned runtime
|
||||
directory under the isolated `KTX_RUNTIME_ROOT`.
|
||||
- `ktx runtime prune --dry-run` lists the stale runtime and leaves it on disk.
|
||||
- `ktx runtime prune` without `--yes` exits nonzero and prints the existing
|
||||
confirmation guidance.
|
||||
- `ktx runtime prune --yes` removes the stale runtime directory.
|
||||
- `README.md` lists `ktx runtime prune --dry-run` and
|
||||
`ktx runtime prune --yes` with the other managed runtime commands.
|
||||
- `examples/package-artifacts/README.md` describes prune coverage in the
|
||||
package artifact smoke.
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: this plan covers the remaining visible gap for the runtime
|
||||
management command family in the npm-managed Python runtime spec. The prune
|
||||
implementation already exists, and this plan adds release smoke and public
|
||||
docs coverage.
|
||||
- Placeholder scan: no placeholder steps, deferred implementation notes, or
|
||||
unspecified behavior gaps remain.
|
||||
- Type consistency: the plan uses existing labels and functions:
|
||||
`npmRuntimeSmokeSource()`, `requireSuccess()`, `requireOutput()`,
|
||||
`KTX_RUNTIME_ROOT`, `ktx runtime prune --dry-run`, and
|
||||
`ktx runtime prune --yes`.
|
||||
|
|
@ -0,0 +1,647 @@
|
|||
# Managed Runtime uv Prerequisite Contract Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Close the remaining npm-managed Python runtime open decision by
|
||||
making `uv` a documented, release-policy-checked prerequisite.
|
||||
|
||||
**Architecture:** Keep the runtime installer behavior simple: the CLI locates
|
||||
`uv` on `PATH` and prints a focused error when it is missing. Encode that
|
||||
decision in `release-policy.json`, validate it during release readiness, use one
|
||||
shared runtime error message, and document the prerequisite in public docs.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, TypeScript, Vitest, JSON
|
||||
release policy, Markdown.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plan files are based on that spec and are already implemented in
|
||||
this worktree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
|
||||
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `packages/cli/assets/python/manifest.json` and the bundled
|
||||
`kaelio_ktx-0.1.0-py3-none-any.whl`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`, including runtime roots,
|
||||
bundled wheel verification, install, status, doctor, and prune behavior.
|
||||
- `packages/cli/src/managed-python-command.ts`,
|
||||
`packages/cli/src/managed-python-daemon.ts`,
|
||||
`packages/cli/src/managed-local-embeddings.ts`, and
|
||||
`packages/cli/src/managed-python-http.ts`.
|
||||
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
|
||||
`scripts/published-package-smoke.mjs`,
|
||||
`scripts/local-embeddings-runtime-smoke.mjs`, and
|
||||
`scripts/publish-public-npm-package.mjs`.
|
||||
- `release-policy.json` is already in `npm-public-release-ready` mode for
|
||||
`@kaelio/ktx` `0.1.0` and keeps Python package publishing disabled.
|
||||
- `README.md` and `examples/package-artifacts/README.md` document the managed
|
||||
runtime command family, including `runtime prune`.
|
||||
|
||||
The remaining spec gap is the open decision in
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`:
|
||||
|
||||
```text
|
||||
KTX still needs a final decision on whether uv is a hard prerequisite or a
|
||||
bootstrap dependency that KTX downloads automatically.
|
||||
```
|
||||
|
||||
This plan chooses the hard-prerequisite path for the first public release. KTX
|
||||
will not download `uv` automatically in this release.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `release-policy.json`: add a `runtimeInstaller` policy section that
|
||||
records the hard `uv` prerequisite decision.
|
||||
- Modify `scripts/release-readiness.mjs`: validate the runtime installer
|
||||
policy, include it in readiness reports, and print it in text output.
|
||||
- Modify `scripts/release-readiness.test.mjs`: cover the accepted policy and
|
||||
rejection paths for missing or bootstrap-style `uv` policies.
|
||||
- Modify `packages/cli/src/managed-python-runtime.ts`: export one shared
|
||||
missing-`uv` message and use it for install and doctor output.
|
||||
- Modify `packages/cli/src/managed-python-runtime.test.ts`: cover install and
|
||||
doctor behavior when `uv` is missing.
|
||||
- Modify `scripts/examples-docs.test.mjs`: require public docs to state the
|
||||
hard `uv` prerequisite.
|
||||
- Modify `README.md`: document that `uv` must be on `PATH` and KTX does not
|
||||
download it automatically.
|
||||
- Modify `examples/package-artifacts/README.md`: document the artifact smoke
|
||||
`uv` prerequisite.
|
||||
|
||||
### Task 1: Encode the runtime installer policy
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `release-policy.json`
|
||||
- Modify: `scripts/release-readiness.test.mjs`
|
||||
- Modify: `scripts/release-readiness.mjs`
|
||||
- Test: `scripts/release-readiness.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add failing release policy tests**
|
||||
|
||||
In `scripts/release-readiness.test.mjs`, inside the `releasePolicy()` helper
|
||||
return value, add the `runtimeInstaller` object immediately after
|
||||
`publishedPackageSmoke`:
|
||||
|
||||
```javascript
|
||||
runtimeInstaller: {
|
||||
uvStrategy: 'path-prerequisite',
|
||||
bootstrapUv: false,
|
||||
missingUvBehavior: 'focused-error',
|
||||
},
|
||||
```
|
||||
|
||||
In the three `assert.deepEqual(report, { ... })` expectations, add this field
|
||||
immediately after `publishedPackageSmokeGate`:
|
||||
|
||||
```javascript
|
||||
runtimeInstaller: {
|
||||
uvStrategy: 'path-prerequisite',
|
||||
bootstrapUv: false,
|
||||
missingUvBehavior: 'focused-error',
|
||||
},
|
||||
```
|
||||
|
||||
Add these tests immediately after the
|
||||
`it('accepts the npm public release ready policy', async () => { ... })` block:
|
||||
|
||||
```javascript
|
||||
it('rejects npm public release ready mode without a runtime installer policy', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-missing-test-'));
|
||||
try {
|
||||
await writeReadyFixture(root, {
|
||||
policy: releasePolicy({
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
npm: {
|
||||
publish: true,
|
||||
registry: null,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
},
|
||||
publishedPackageSmoke: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
registry: null,
|
||||
},
|
||||
runtimeInstaller: undefined,
|
||||
requiredBeforePublishing: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => releaseReadinessReport(root),
|
||||
/Release policy runtimeInstaller must be a JSON object/,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects uv bootstrap download policy for the first public npm release', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-runtime-policy-bootstrap-test-'));
|
||||
try {
|
||||
await writeReadyFixture(root, {
|
||||
policy: releasePolicy({
|
||||
releaseMode: 'npm-public-release-ready',
|
||||
npm: {
|
||||
publish: true,
|
||||
registry: null,
|
||||
access: 'public',
|
||||
tag: 'latest',
|
||||
},
|
||||
publishedPackageSmoke: {
|
||||
packageName: '@kaelio/ktx',
|
||||
version: PUBLIC_NPM_PACKAGE_VERSION,
|
||||
registry: null,
|
||||
},
|
||||
runtimeInstaller: {
|
||||
uvStrategy: 'bootstrap-download',
|
||||
bootstrapUv: true,
|
||||
missingUvBehavior: 'download',
|
||||
},
|
||||
requiredBeforePublishing: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => releaseReadinessReport(root),
|
||||
/Release policy runtimeInstaller\.uvStrategy must be path-prerequisite/,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the release readiness tests and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/release-readiness.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL because `releaseReadinessReport()` does not include
|
||||
`runtimeInstaller`, and `validateReleasePolicy()` does not validate the new
|
||||
policy section.
|
||||
|
||||
- [ ] **Step 3: Validate the runtime installer policy**
|
||||
|
||||
In `scripts/release-readiness.mjs`, add this function immediately after the
|
||||
`assertRequiredBeforePublishing(policy)` function definition:
|
||||
|
||||
```javascript
|
||||
function assertRuntimeInstallerPolicy(policy) {
|
||||
assertPlainObject(policy.runtimeInstaller, 'Release policy runtimeInstaller');
|
||||
assertString(policy.runtimeInstaller.uvStrategy, 'Release policy runtimeInstaller.uvStrategy');
|
||||
assertBoolean(policy.runtimeInstaller.bootstrapUv, 'Release policy runtimeInstaller.bootstrapUv');
|
||||
assertString(
|
||||
policy.runtimeInstaller.missingUvBehavior,
|
||||
'Release policy runtimeInstaller.missingUvBehavior',
|
||||
);
|
||||
|
||||
if (policy.runtimeInstaller.uvStrategy !== 'path-prerequisite') {
|
||||
throw new Error('Release policy runtimeInstaller.uvStrategy must be path-prerequisite');
|
||||
}
|
||||
if (policy.runtimeInstaller.bootstrapUv !== false) {
|
||||
throw new Error('Release policy runtimeInstaller.bootstrapUv must be false');
|
||||
}
|
||||
if (policy.runtimeInstaller.missingUvBehavior !== 'focused-error') {
|
||||
throw new Error('Release policy runtimeInstaller.missingUvBehavior must be focused-error');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In `validateReleasePolicy(policy)`, add this call immediately after
|
||||
`assertRequiredBeforePublishing(policy);`:
|
||||
|
||||
```javascript
|
||||
assertRuntimeInstallerPolicy(policy);
|
||||
```
|
||||
|
||||
In `releaseReadinessReport(rootDir = scriptRootDir())`, add
|
||||
`runtimeInstaller` to the returned object immediately after
|
||||
`publishedPackageSmokeGate`:
|
||||
|
||||
```javascript
|
||||
runtimeInstaller: policy.runtimeInstaller,
|
||||
```
|
||||
|
||||
In `main()`, add these lines immediately after the published package smoke
|
||||
registry line:
|
||||
|
||||
```javascript
|
||||
process.stdout.write(`Runtime uv strategy: ${report.runtimeInstaller.uvStrategy}\n`);
|
||||
process.stdout.write(
|
||||
`Runtime uv bootstrap: ${report.runtimeInstaller.bootstrapUv ? 'enabled' : 'disabled'}\n`,
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Encode the policy in `release-policy.json`**
|
||||
|
||||
Replace `release-policy.json` with this exact content:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"releaseMode": "npm-public-release-ready",
|
||||
"npm": {
|
||||
"publish": true,
|
||||
"registry": null,
|
||||
"access": "public",
|
||||
"tag": "latest",
|
||||
"packages": ["@kaelio/ktx"]
|
||||
},
|
||||
"python": {
|
||||
"publish": false,
|
||||
"repository": null,
|
||||
"packages": ["ktx-sl", "ktx-daemon", "kaelio-ktx"]
|
||||
},
|
||||
"publishedPackageSmoke": {
|
||||
"packageName": "@kaelio/ktx",
|
||||
"version": "0.1.0",
|
||||
"registry": null
|
||||
},
|
||||
"runtimeInstaller": {
|
||||
"uvStrategy": "path-prerequisite",
|
||||
"bootstrapUv": false,
|
||||
"missingUvBehavior": "focused-error"
|
||||
},
|
||||
"requiredBeforePublishing": []
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the release readiness tests and verify success**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/release-readiness.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit the release policy contract**
|
||||
|
||||
```bash
|
||||
git add release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs
|
||||
git commit -m "chore: encode uv runtime prerequisite policy"
|
||||
```
|
||||
|
||||
### Task 2: Centralize missing-uv runtime output
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/managed-python-runtime.test.ts`
|
||||
- Modify: `packages/cli/src/managed-python-runtime.ts`
|
||||
- Test: `packages/cli/src/managed-python-runtime.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add failing missing-uv runtime tests**
|
||||
|
||||
In `packages/cli/src/managed-python-runtime.test.ts`, add
|
||||
`MISSING_UV_RUNTIME_INSTALL_MESSAGE` to the import from
|
||||
`./managed-python-runtime.js`:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
MISSING_UV_RUNTIME_INSTALL_MESSAGE,
|
||||
doctorManagedPythonRuntime,
|
||||
installManagedPythonRuntime,
|
||||
managedPythonRuntimeLayout,
|
||||
pruneManagedPythonRuntimes,
|
||||
readManagedPythonRuntimeStatus,
|
||||
verifyRuntimeAsset,
|
||||
type ManagedPythonRuntimeExec,
|
||||
} from './managed-python-runtime.js';
|
||||
```
|
||||
|
||||
Inside `describe('installManagedPythonRuntime', () => { ... })`, add this test
|
||||
after the local embeddings test:
|
||||
|
||||
```typescript
|
||||
it('fails with the hard-prerequisite message when uv is missing', async () => {
|
||||
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
||||
const commands: Array<{ command: string; args: string[] }> = [];
|
||||
const exec: ManagedPythonRuntimeExec = vi.fn(async (command, args) => {
|
||||
commands.push({ command, args });
|
||||
throw new Error('spawn uv ENOENT');
|
||||
});
|
||||
|
||||
await expect(
|
||||
installManagedPythonRuntime({
|
||||
cliVersion: '0.2.0',
|
||||
runtimeRoot: join(tempDir, 'runtime'),
|
||||
assetDir,
|
||||
features: ['core'],
|
||||
exec,
|
||||
}),
|
||||
).rejects.toThrow(MISSING_UV_RUNTIME_INSTALL_MESSAGE);
|
||||
|
||||
expect(commands).toEqual([{ command: 'uv', args: ['--version'] }]);
|
||||
});
|
||||
```
|
||||
|
||||
Inside `describe('doctorManagedPythonRuntime', () => { ... })`, add this test
|
||||
after the existing doctor test:
|
||||
|
||||
```typescript
|
||||
it('reports uv as a hard prerequisite when uv is missing', async () => {
|
||||
const { assetDir } = await writeAsset(tempDir, 'core-wheel');
|
||||
const exec: ManagedPythonRuntimeExec = vi.fn(async () => {
|
||||
throw new Error('spawn uv ENOENT');
|
||||
});
|
||||
|
||||
const checks = await doctorManagedPythonRuntime({
|
||||
cliVersion: '0.2.0',
|
||||
runtimeRoot: join(tempDir, 'runtime'),
|
||||
assetDir,
|
||||
exec,
|
||||
});
|
||||
|
||||
expect(checks[0]).toEqual({
|
||||
id: 'uv',
|
||||
label: 'uv',
|
||||
status: 'fail',
|
||||
detail: MISSING_UV_RUNTIME_INSTALL_MESSAGE,
|
||||
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the runtime tests and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
|
||||
```
|
||||
|
||||
Expected: FAIL because the shared message constant does not exist and the
|
||||
doctor fix text still uses the older message.
|
||||
|
||||
- [ ] **Step 3: Add the shared missing-uv message**
|
||||
|
||||
In `packages/cli/src/managed-python-runtime.ts`, add this export immediately
|
||||
after the `ManagedPythonRuntimePruneResult` interface:
|
||||
|
||||
```typescript
|
||||
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE =
|
||||
'uv is required to install the KTX Python runtime. KTX does not download uv automatically. Install uv, make sure it is on PATH, and retry: ktx runtime install --yes';
|
||||
```
|
||||
|
||||
Replace the body of the `catch` block in `ensureUv()` with:
|
||||
|
||||
```typescript
|
||||
throw new Error(MISSING_UV_RUNTIME_INSTALL_MESSAGE);
|
||||
```
|
||||
|
||||
In `doctorManagedPythonRuntime()`, replace the `fix` value for the `uv` check
|
||||
with:
|
||||
|
||||
```typescript
|
||||
fix: 'Install uv, make sure it is on PATH, and run: ktx runtime install --yes',
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the runtime tests and verify success**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit the runtime output contract**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts
|
||||
git commit -m "fix: clarify missing uv runtime error"
|
||||
```
|
||||
|
||||
### Task 3: Document the hard uv prerequisite
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/examples-docs.test.mjs`
|
||||
- Modify: `README.md`
|
||||
- Modify: `examples/package-artifacts/README.md`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add failing docs assertions**
|
||||
|
||||
In `scripts/examples-docs.test.mjs`, inside
|
||||
`it('documents public npm and managed runtime usage in the README', ... )`, add
|
||||
these assertions immediately after the existing `ktx runtime prune --yes`
|
||||
assertion:
|
||||
|
||||
```javascript
|
||||
assert.match(rootReadme, /KTX requires `uv` on `PATH`/);
|
||||
assert.match(rootReadme, /KTX doesn't download `uv` automatically/);
|
||||
```
|
||||
|
||||
Inside `it('documents the public package artifact smoke shape', ... )`, add
|
||||
this assertion immediately after the `managed Python runtime` assertion:
|
||||
|
||||
```javascript
|
||||
assert.match(readme, /requires `uv` on `PATH`/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the docs test and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL because the README files do not state the hard `uv`
|
||||
prerequisite.
|
||||
|
||||
- [ ] **Step 3: Update the root README runtime section**
|
||||
|
||||
In `README.md`, in the `## Managed Python runtime` section, replace this
|
||||
paragraph:
|
||||
|
||||
```markdown
|
||||
KTX installs its Python runtime only when a Python-backed command needs it.
|
||||
The runtime lives outside the npm cache, is versioned by the installed CLI
|
||||
version, and is managed by `ktx runtime` commands:
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```markdown
|
||||
KTX installs its Python runtime only when a Python-backed command needs it.
|
||||
The runtime lives outside the npm cache, is versioned by the installed CLI
|
||||
version, and is managed by `ktx runtime` commands.
|
||||
|
||||
KTX requires `uv` on `PATH` to create the managed runtime. Install `uv` with
|
||||
your system package manager or the official installer before running Python-
|
||||
backed KTX commands. KTX doesn't download `uv` automatically; run
|
||||
`ktx runtime doctor` if runtime installation fails:
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update the package artifact smoke README**
|
||||
|
||||
In `examples/package-artifacts/README.md`, replace this paragraph:
|
||||
|
||||
```markdown
|
||||
The managed Python runtime smoke isolates `KTX_RUNTIME_ROOT`, verifies
|
||||
`ktx runtime status`, runs `ktx sl query --yes` to install the core runtime from
|
||||
the bundled wheel, checks `ktx runtime doctor`, starts and reuses the managed
|
||||
daemon, stops it, previews a stale runtime with `ktx runtime prune --dry-run`,
|
||||
verifies confirmation is required, and removes the stale runtime with
|
||||
`ktx runtime prune --yes`.
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```markdown
|
||||
The managed Python runtime smoke requires `uv` on `PATH`, isolates
|
||||
`KTX_RUNTIME_ROOT`, verifies `ktx runtime status`, runs `ktx sl query --yes` to
|
||||
install the core runtime from the bundled wheel, checks `ktx runtime doctor`,
|
||||
starts and reuses the managed daemon, stops it, previews a stale runtime with
|
||||
`ktx runtime prune --dry-run`, verifies confirmation is required, and removes
|
||||
the stale runtime with `ktx runtime prune --yes`.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the docs test and verify success**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit the public docs update**
|
||||
|
||||
```bash
|
||||
git add README.md examples/package-artifacts/README.md scripts/examples-docs.test.mjs
|
||||
git commit -m "docs: document uv runtime prerequisite"
|
||||
```
|
||||
|
||||
### Task 4: Verify the completed contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `release-policy.json`
|
||||
- Verify: `scripts/release-readiness.mjs`
|
||||
- Verify: `scripts/release-readiness.test.mjs`
|
||||
- Verify: `packages/cli/src/managed-python-runtime.ts`
|
||||
- Verify: `packages/cli/src/managed-python-runtime.test.ts`
|
||||
- Verify: `scripts/examples-docs.test.mjs`
|
||||
- Verify: `README.md`
|
||||
- Verify: `examples/package-artifacts/README.md`
|
||||
|
||||
- [ ] **Step 1: Run focused verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs
|
||||
pnpm --filter @ktx/cli run test -- src/managed-python-runtime.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Verify release readiness text output**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:readiness
|
||||
```
|
||||
|
||||
Expected output includes:
|
||||
|
||||
```text
|
||||
KTX release mode: npm-public-release-ready
|
||||
Runtime uv strategy: path-prerequisite
|
||||
Runtime uv bootstrap: disabled
|
||||
NPM publish target: @kaelio/ktx@0.1.0 (latest)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify no pre-commit config is required**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg --files -g '.pre-commit-config.yaml' -g 'pre-commit-config.yaml'
|
||||
```
|
||||
|
||||
Expected: no output and exit code 1. No Python files changed, so the repository
|
||||
Python pre-commit requirement does not apply.
|
||||
|
||||
- [ ] **Step 4: Review the final diff**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --stat
|
||||
git diff -- release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
|
||||
```
|
||||
|
||||
Expected: only the runtime installer policy, missing-`uv` message/tests, and
|
||||
public docs changed.
|
||||
|
||||
- [ ] **Step 5: Commit final verification notes if needed**
|
||||
|
||||
If Task 4 produces only verification output and no file changes, skip this
|
||||
step. If a correction was made during verification, commit it:
|
||||
|
||||
```bash
|
||||
git add release-policy.json scripts/release-readiness.mjs scripts/release-readiness.test.mjs packages/cli/src/managed-python-runtime.ts packages/cli/src/managed-python-runtime.test.ts scripts/examples-docs.test.mjs README.md examples/package-artifacts/README.md
|
||||
git commit -m "chore: finish uv prerequisite release contract"
|
||||
```
|
||||
|
||||
## Self-review
|
||||
|
||||
Spec coverage:
|
||||
|
||||
- The earlier implemented plans cover the single public npm package, bundled
|
||||
Python wheel, managed runtime installer, runtime commands, daemon lifecycle,
|
||||
local embeddings, Python-backed command integration, release smoke, published
|
||||
smoke, docs cleanup, release handoff, and prune coverage.
|
||||
- This plan closes the spec's remaining `uv` open decision by choosing
|
||||
`path-prerequisite`, recording that decision in release policy, validating it
|
||||
in release readiness, using one CLI error message, and documenting it.
|
||||
- The plan keeps Python package publication disabled and keeps KTX-owned Python
|
||||
code bundled in the npm package.
|
||||
|
||||
Placeholder scan:
|
||||
|
||||
- No task contains deferred implementation markers.
|
||||
- Each code-changing step names exact files and includes the concrete code to
|
||||
add or replace.
|
||||
|
||||
Type consistency:
|
||||
|
||||
- The release policy field is consistently named `runtimeInstaller`.
|
||||
- The chosen strategy is consistently `path-prerequisite`.
|
||||
- The shared CLI message constant is consistently
|
||||
`MISSING_UV_RUNTIME_INSTALL_MESSAGE`.
|
||||
1904
docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md
Normal file
1904
docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md
Normal file
File diff suppressed because it is too large
Load diff
1332
docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md
Normal file
1332
docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,602 @@
|
|||
# Published Package Managed Runtime Smoke Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make the post-publication smoke prove that the published
|
||||
`@kaelio/ktx` package uses the same isolated managed Python runtime across
|
||||
`npx @kaelio/ktx`, local `npx ktx`, and global `ktx` invocation modes.
|
||||
|
||||
**Architecture:** Keep the smoke black-box and network-gated. Strengthen the
|
||||
command builder so every Python-backed published-package command receives the
|
||||
same temporary `KTX_RUNTIME_ROOT`, then run a real semantic-layer query through
|
||||
the direct `npx`, local install, and global install paths instead of checking
|
||||
only `--version` for local and global binaries.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, npx, KTX managed Python
|
||||
runtime, published `@kaelio/ktx` package smoke.
|
||||
|
||||
---
|
||||
|
||||
## Existing status
|
||||
|
||||
This plan is based on
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plans are based on that spec and are implemented in this
|
||||
worktree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `scripts/build-python-runtime-wheel.mjs` and
|
||||
`packages/cli/assets/python/manifest.json`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`,
|
||||
`packages/cli/src/runtime.ts`,
|
||||
`packages/cli/src/commands/runtime-commands.ts`,
|
||||
`packages/cli/src/managed-python-command.ts`,
|
||||
`packages/cli/src/managed-python-daemon.ts`,
|
||||
`packages/cli/src/managed-local-embeddings.ts`, and
|
||||
`packages/cli/src/managed-python-http.ts`.
|
||||
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
|
||||
`scripts/local-embeddings-runtime-smoke.mjs`, and
|
||||
`scripts/published-package-smoke.mjs`.
|
||||
- `packages/cli/src/agent-runtime.ts`, `packages/cli/src/serve.ts`,
|
||||
`packages/cli/src/ingest.ts`, and `packages/cli/src/scan.ts` thread managed
|
||||
runtime policy through the Python-backed CLI paths.
|
||||
- `examples/postgres-historic/scripts/smoke.sh`,
|
||||
`examples/postgres-historic/README.md`,
|
||||
`examples/package-artifacts/README.md`, and `README.md` now document the
|
||||
managed runtime instead of a manual `python-service/` process.
|
||||
|
||||
The remaining release-confidence gap is in the post-publication smoke:
|
||||
|
||||
- `scripts/published-package-smoke-config.mjs` runs `npx @kaelio/ktx setup
|
||||
demo` and `npx @kaelio/ktx sl query ... --yes`, but it does not isolate
|
||||
`KTX_RUNTIME_ROOT` for those commands.
|
||||
- The same smoke installs `@kaelio/ktx` locally and globally, but local and
|
||||
global verification only run `--version`.
|
||||
- The design spec requires the direct `npx @kaelio/ktx`, local `npx ktx`, and
|
||||
global `ktx` modes to work for real KTX commands. A semantic-layer query is
|
||||
the smallest Python-backed command that proves the bundled managed runtime is
|
||||
usable in each mode.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `scripts/published-package-smoke.test.mjs`: expect a shared
|
||||
`KTX_RUNTIME_ROOT` in the published smoke commands, expect local and global
|
||||
semantic query commands, and cover label classification used by the runner.
|
||||
- Modify `scripts/published-package-smoke-config.mjs`: derive a temporary
|
||||
runtime root from the smoke project directory, merge it with registry
|
||||
environment settings, and add local and global `sl query` commands.
|
||||
- Modify `scripts/published-package-smoke.mjs`: validate the renamed version
|
||||
labels and semantic query labels when the smoke runs.
|
||||
|
||||
### Task 1: Isolate runtime roots and add real local/global command coverage
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/published-package-smoke.test.mjs`
|
||||
- Modify: `scripts/published-package-smoke-config.mjs`
|
||||
- Test: `scripts/published-package-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing command-list test**
|
||||
|
||||
In `scripts/published-package-smoke.test.mjs`, replace the existing
|
||||
`it('builds the full public package smoke command list', ...)` block with this
|
||||
test:
|
||||
|
||||
```javascript
|
||||
it('builds the full public package smoke command list', () => {
|
||||
assert.deepEqual(
|
||||
buildPublishedPackageSmokeCommands(
|
||||
config,
|
||||
'/tmp/ktx-smoke/demo',
|
||||
'/tmp/ktx-smoke/managed-runtime',
|
||||
),
|
||||
[
|
||||
{
|
||||
label: 'published package npx version',
|
||||
command: 'npx',
|
||||
args: ['--yes', '@kaelio/ktx@latest', '--version'],
|
||||
env: { npm_config_registry: 'https://registry.npmjs.org/' },
|
||||
},
|
||||
{
|
||||
label: 'published package setup demo',
|
||||
command: 'npx',
|
||||
args: [
|
||||
'--yes',
|
||||
'@kaelio/ktx@latest',
|
||||
'setup',
|
||||
'demo',
|
||||
'--project-dir',
|
||||
'/tmp/ktx-smoke/demo',
|
||||
'--no-input',
|
||||
'--plain',
|
||||
],
|
||||
env: {
|
||||
npm_config_registry: 'https://registry.npmjs.org/',
|
||||
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'published package npx sl query',
|
||||
command: 'npx',
|
||||
args: [
|
||||
'--yes',
|
||||
'@kaelio/ktx@latest',
|
||||
'sl',
|
||||
'query',
|
||||
'--project-dir',
|
||||
'/tmp/ktx-smoke/demo',
|
||||
'--connection-id',
|
||||
'orbit_demo',
|
||||
'--measure',
|
||||
'contracts.contract_count',
|
||||
'--format',
|
||||
'sql',
|
||||
'--yes',
|
||||
],
|
||||
env: {
|
||||
npm_config_registry: 'https://registry.npmjs.org/',
|
||||
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'published package local install',
|
||||
command: 'pnpm',
|
||||
args: ['add', '@kaelio/ktx@latest'],
|
||||
env: { npm_config_registry: 'https://registry.npmjs.org/' },
|
||||
},
|
||||
{
|
||||
label: 'published package local version',
|
||||
command: 'npx',
|
||||
args: ['ktx', '--version'],
|
||||
env: { npm_config_registry: 'https://registry.npmjs.org/' },
|
||||
},
|
||||
{
|
||||
label: 'published package local sl query',
|
||||
command: 'npx',
|
||||
args: [
|
||||
'ktx',
|
||||
'sl',
|
||||
'query',
|
||||
'--project-dir',
|
||||
'/tmp/ktx-smoke/demo',
|
||||
'--connection-id',
|
||||
'orbit_demo',
|
||||
'--measure',
|
||||
'contracts.contract_count',
|
||||
'--format',
|
||||
'sql',
|
||||
'--yes',
|
||||
],
|
||||
env: {
|
||||
npm_config_registry: 'https://registry.npmjs.org/',
|
||||
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'published package global install',
|
||||
command: 'pnpm',
|
||||
args: ['add', '--global', '@kaelio/ktx@latest'],
|
||||
env: { npm_config_registry: 'https://registry.npmjs.org/' },
|
||||
},
|
||||
{
|
||||
label: 'published package global version',
|
||||
command: 'ktx',
|
||||
args: ['--version'],
|
||||
env: { npm_config_registry: 'https://registry.npmjs.org/' },
|
||||
},
|
||||
{
|
||||
label: 'published package global sl query',
|
||||
command: 'ktx',
|
||||
args: [
|
||||
'sl',
|
||||
'query',
|
||||
'--project-dir',
|
||||
'/tmp/ktx-smoke/demo',
|
||||
'--connection-id',
|
||||
'orbit_demo',
|
||||
'--measure',
|
||||
'contracts.contract_count',
|
||||
'--format',
|
||||
'sql',
|
||||
'--yes',
|
||||
],
|
||||
env: {
|
||||
npm_config_registry: 'https://registry.npmjs.org/',
|
||||
KTX_RUNTIME_ROOT: '/tmp/ktx-smoke/managed-runtime',
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/published-package-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL with an `AssertionError` showing that the actual command list
|
||||
still uses `published package version`, lacks `KTX_RUNTIME_ROOT`, and lacks the
|
||||
local/global `sl query` commands.
|
||||
|
||||
- [ ] **Step 3: Implement the command builder changes**
|
||||
|
||||
In `scripts/published-package-smoke-config.mjs`, add this import before the
|
||||
existing `node:assert/strict` import:
|
||||
|
||||
```javascript
|
||||
import { dirname, join } from 'node:path';
|
||||
```
|
||||
|
||||
In the same file, add these helper functions after
|
||||
`assertHttpRegistry(registry, label)`:
|
||||
|
||||
```javascript
|
||||
function registryEnv(config) {
|
||||
return config.registry ? { npm_config_registry: config.registry } : {};
|
||||
}
|
||||
|
||||
function runtimeCommandEnv(config, runtimeRoot) {
|
||||
return { ...registryEnv(config), KTX_RUNTIME_ROOT: runtimeRoot };
|
||||
}
|
||||
|
||||
function semanticQueryArgs(projectDir) {
|
||||
return [
|
||||
'sl',
|
||||
'query',
|
||||
'--project-dir',
|
||||
projectDir,
|
||||
'--connection-id',
|
||||
'orbit_demo',
|
||||
'--measure',
|
||||
'contracts.contract_count',
|
||||
'--format',
|
||||
'sql',
|
||||
'--yes',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Replace `buildPublishedPackageNpxCommand()` and
|
||||
`buildPublishedPackageSmokeCommands()` with this implementation:
|
||||
|
||||
```javascript
|
||||
export function buildPublishedPackageNpxCommand(config, args, label = 'published package command', extraEnv = {}) {
|
||||
return {
|
||||
label,
|
||||
command: 'npx',
|
||||
args: ['--yes', publishedPackageSpec(config), ...args],
|
||||
env: { ...registryEnv(config), ...extraEnv },
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPublishedPackageSmokeCommands(
|
||||
config,
|
||||
projectDir,
|
||||
runtimeRoot = join(dirname(projectDir), 'managed-runtime'),
|
||||
) {
|
||||
const runtimeEnv = runtimeCommandEnv(config, runtimeRoot);
|
||||
const packageEnv = registryEnv(config);
|
||||
const queryArgs = semanticQueryArgs(projectDir);
|
||||
|
||||
return [
|
||||
buildPublishedPackageNpxCommand(config, ['--version'], 'published package npx version'),
|
||||
buildPublishedPackageNpxCommand(
|
||||
config,
|
||||
['setup', 'demo', '--project-dir', projectDir, '--no-input', '--plain'],
|
||||
'published package setup demo',
|
||||
{ KTX_RUNTIME_ROOT: runtimeRoot },
|
||||
),
|
||||
buildPublishedPackageNpxCommand(config, queryArgs, 'published package npx sl query', {
|
||||
KTX_RUNTIME_ROOT: runtimeRoot,
|
||||
}),
|
||||
{
|
||||
label: 'published package local install',
|
||||
command: 'pnpm',
|
||||
args: ['add', publishedPackageSpec(config)],
|
||||
env: packageEnv,
|
||||
},
|
||||
{
|
||||
label: 'published package local version',
|
||||
command: 'npx',
|
||||
args: ['ktx', '--version'],
|
||||
env: packageEnv,
|
||||
},
|
||||
{
|
||||
label: 'published package local sl query',
|
||||
command: 'npx',
|
||||
args: ['ktx', ...queryArgs],
|
||||
env: runtimeEnv,
|
||||
},
|
||||
{
|
||||
label: 'published package global install',
|
||||
command: 'pnpm',
|
||||
args: ['add', '--global', publishedPackageSpec(config)],
|
||||
env: packageEnv,
|
||||
},
|
||||
{
|
||||
label: 'published package global version',
|
||||
command: 'ktx',
|
||||
args: ['--version'],
|
||||
env: packageEnv,
|
||||
},
|
||||
{
|
||||
label: 'published package global sl query',
|
||||
command: 'ktx',
|
||||
args: queryArgs,
|
||||
env: runtimeEnv,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the command-list test and verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/published-package-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS for the command construction tests, with remaining failures only
|
||||
if the runner label validation test from Task 2 has already been added.
|
||||
|
||||
- [ ] **Step 5: Commit the command-builder change**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/published-package-smoke-config.mjs scripts/published-package-smoke.test.mjs
|
||||
git commit -m "test: cover published package runtime smoke commands"
|
||||
```
|
||||
|
||||
### Task 2: Validate smoke runner labels for the new command list
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/published-package-smoke.test.mjs`
|
||||
- Modify: `scripts/published-package-smoke.mjs`
|
||||
- Test: `scripts/published-package-smoke.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing label classification test**
|
||||
|
||||
In `scripts/published-package-smoke.test.mjs`, replace the import from
|
||||
`./published-package-smoke.mjs` with this import:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
buildPublishedPackageNpxCommand,
|
||||
buildPublishedPackageSmokeCommands,
|
||||
isPublishedPackageSemanticQueryLabel,
|
||||
isPublishedPackageVersionLabel,
|
||||
publishedPackageSpec,
|
||||
readPublishedPackageSmokeConfig,
|
||||
} from './published-package-smoke.mjs';
|
||||
```
|
||||
|
||||
Add this test after the `describe('published package smoke command
|
||||
construction', ...)` block:
|
||||
|
||||
```javascript
|
||||
describe('published package smoke output validation labels', () => {
|
||||
it('classifies version and semantic query commands', () => {
|
||||
assert.equal(isPublishedPackageVersionLabel('published package npx version'), true);
|
||||
assert.equal(isPublishedPackageVersionLabel('published package local version'), true);
|
||||
assert.equal(isPublishedPackageVersionLabel('published package global version'), true);
|
||||
assert.equal(isPublishedPackageVersionLabel('published package setup demo'), false);
|
||||
|
||||
assert.equal(isPublishedPackageSemanticQueryLabel('published package npx sl query'), true);
|
||||
assert.equal(isPublishedPackageSemanticQueryLabel('published package local sl query'), true);
|
||||
assert.equal(isPublishedPackageSemanticQueryLabel('published package global sl query'), true);
|
||||
assert.equal(isPublishedPackageSemanticQueryLabel('published package local install'), false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test and verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/published-package-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL with an import error because
|
||||
`isPublishedPackageSemanticQueryLabel` and `isPublishedPackageVersionLabel` are
|
||||
not exported yet.
|
||||
|
||||
- [ ] **Step 3: Implement label classification and runner validation**
|
||||
|
||||
In `scripts/published-package-smoke.mjs`, add these constants and exports after
|
||||
`const SMOKE_TIMEOUT_MS = 180_000;`:
|
||||
|
||||
```javascript
|
||||
const VERSION_LABELS = new Set([
|
||||
'published package npx version',
|
||||
'published package local version',
|
||||
'published package global version',
|
||||
]);
|
||||
|
||||
const SEMANTIC_QUERY_LABELS = new Set([
|
||||
'published package npx sl query',
|
||||
'published package local sl query',
|
||||
'published package global sl query',
|
||||
]);
|
||||
|
||||
export function isPublishedPackageVersionLabel(label) {
|
||||
return VERSION_LABELS.has(label);
|
||||
}
|
||||
|
||||
export function isPublishedPackageSemanticQueryLabel(label) {
|
||||
return SEMANTIC_QUERY_LABELS.has(label);
|
||||
}
|
||||
```
|
||||
|
||||
In `runPublishedPackageSmoke(config)`, replace this block:
|
||||
|
||||
```javascript
|
||||
if (
|
||||
command.label === 'published package version' ||
|
||||
command.label === 'published package local binary' ||
|
||||
command.label === 'published package global binary'
|
||||
) {
|
||||
assert.match(result.stdout, /@kaelio\/ktx /);
|
||||
}
|
||||
if (command.label === 'published package sl query') {
|
||||
assert.match(result.stdout, /SELECT/i);
|
||||
assert.match(result.stdout, /contracts/i);
|
||||
}
|
||||
```
|
||||
|
||||
with this block:
|
||||
|
||||
```javascript
|
||||
if (isPublishedPackageVersionLabel(command.label)) {
|
||||
assert.match(result.stdout, /@kaelio\/ktx /);
|
||||
}
|
||||
if (isPublishedPackageSemanticQueryLabel(command.label)) {
|
||||
assert.match(result.stdout, /SELECT/i);
|
||||
assert.match(result.stdout, /contracts/i);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the label tests and verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/published-package-smoke.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit the runner-label change**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs
|
||||
git commit -m "test: validate published package smoke outputs"
|
||||
```
|
||||
|
||||
### Task 3: Verify release-script compatibility
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `scripts/published-package-smoke-config.mjs`
|
||||
- Verify: `scripts/published-package-smoke.mjs`
|
||||
- Verify: `scripts/published-package-smoke.test.mjs`
|
||||
- Verify: `scripts/release-readiness.test.mjs`
|
||||
- Verify: `package.json`
|
||||
|
||||
- [ ] **Step 1: Run the focused Node tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The release-readiness tests must continue to report the
|
||||
published package smoke gate without executing the network smoke.
|
||||
|
||||
- [ ] **Step 2: Run release readiness**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:readiness
|
||||
```
|
||||
|
||||
Expected: PASS and output containing these lines:
|
||||
|
||||
```text
|
||||
Release mode: ci-artifact-only
|
||||
NPM publish enabled: false
|
||||
Published package smoke: pending
|
||||
Published package smoke script: pnpm run release:published-smoke
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Confirm the network smoke stays explicit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n '"release:published-smoke": "node scripts/published-package-smoke.mjs --require-config"' package.json
|
||||
```
|
||||
|
||||
Expected: PASS with one match in `package.json`. Do not run
|
||||
`pnpm run release:published-smoke` in normal CI before the package is published
|
||||
to the configured registry.
|
||||
|
||||
- [ ] **Step 4: Check pre-commit availability**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
test ! -f .pre-commit-config.yaml
|
||||
```
|
||||
|
||||
Expected: PASS in the current worktree. If a pre-commit config exists when this
|
||||
plan is executed, run this instead after activating `.venv`:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
uv run pre-commit run --files scripts/published-package-smoke-config.mjs scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit verification-only fixes if needed**
|
||||
|
||||
If Step 1 or Step 2 required additional source changes, commit them with:
|
||||
|
||||
```bash
|
||||
git add scripts/published-package-smoke-config.mjs scripts/published-package-smoke.mjs scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs package.json
|
||||
git commit -m "chore: verify published package runtime smoke"
|
||||
```
|
||||
|
||||
If no files changed after Task 2, do not create an empty commit.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `buildPublishedPackageSmokeCommands()` derives
|
||||
`<smoke root>/managed-runtime` from the demo project directory by default.
|
||||
- Direct `npx @kaelio/ktx`, local `npx ktx`, and global `ktx` semantic query
|
||||
commands all receive the same `KTX_RUNTIME_ROOT`.
|
||||
- Local and global post-publication smoke coverage runs `sl query ... --yes`,
|
||||
not only `--version`.
|
||||
- `runPublishedPackageSmoke()` validates version output for all version labels
|
||||
and validates generated SQL output for all semantic query labels.
|
||||
- `node --test scripts/published-package-smoke.test.mjs scripts/release-readiness.test.mjs`
|
||||
passes.
|
||||
- `pnpm run release:readiness` still reports the published-package smoke as a
|
||||
pending explicit release gate while registry publishing is disabled.
|
||||
|
||||
## Self-review notes
|
||||
|
||||
- Spec coverage: this plan covers the remaining invocation-mode confidence gap
|
||||
from the spec by proving the published package uses an isolated managed
|
||||
runtime across direct `npx`, local binary, and global binary paths.
|
||||
- Placeholder scan: the plan contains concrete file paths, exact code blocks,
|
||||
exact commands, and exact expected outcomes.
|
||||
- Type consistency: the command label strings are consistent across tests,
|
||||
command construction, and smoke-runner output validation.
|
||||
|
|
@ -0,0 +1,978 @@
|
|||
# Single Public Runtime Artifact Cleanup Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||||
> checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make release artifacts match the npm-managed Python runtime design:
|
||||
one public `@kaelio/ktx` npm tarball plus one bundled `kaelio-ktx` runtime
|
||||
wheel, with no standalone `ktx-sl` or `ktx-daemon` release artifacts.
|
||||
|
||||
**Architecture:** Keep `python/ktx-sl` and `python/ktx-daemon` as source
|
||||
packages used to assemble the bundled runtime wheel. Remove direct standalone
|
||||
Python wheel and source-distribution builds from the release artifact path,
|
||||
manifest, readiness policy, and artifact smoke docs. The packed npm package
|
||||
remains the only user-visible package; Python-backed verification continues
|
||||
through the managed runtime installed from the bundled wheel.
|
||||
|
||||
**Tech Stack:** Node 22 ESM scripts, `node:test`, pnpm, uv-built bundled
|
||||
runtime wheel, JSON release policy, Markdown.
|
||||
|
||||
---
|
||||
|
||||
## Current state
|
||||
|
||||
This plan follows
|
||||
`docs/superpowers/specs/2026-05-11-npm-managed-python-runtime-design.md`.
|
||||
|
||||
The following plan files are based on that spec and are implemented in the
|
||||
current tree:
|
||||
|
||||
- `docs/superpowers/plans/2026-05-11-bundled-python-runtime-wheel.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-installer.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-command-integration.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-daemon-lifecycle.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-kaelio-ktx-npm-package.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-python-runtime-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-embeddings-release-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-agent-mcp-semantic-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-local-ingest-daemon-runtime.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-docs-and-postgres-smoke-cleanup.md`
|
||||
- `docs/superpowers/plans/2026-05-11-published-package-managed-runtime-smoke.md`
|
||||
- `docs/superpowers/plans/2026-05-11-public-npm-release-handoff.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-prune-smoke-and-docs.md`
|
||||
- `docs/superpowers/plans/2026-05-11-managed-runtime-uv-prerequisite-contract.md`
|
||||
|
||||
Implementation evidence found before writing this plan includes:
|
||||
|
||||
- `packages/cli/assets/python/manifest.json` and
|
||||
`packages/cli/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl`.
|
||||
- `packages/cli/src/managed-python-runtime.ts`,
|
||||
`packages/cli/src/managed-python-command.ts`,
|
||||
`packages/cli/src/managed-python-daemon.ts`,
|
||||
`packages/cli/src/managed-local-embeddings.ts`,
|
||||
`packages/cli/src/managed-python-http.ts`, and `packages/cli/src/runtime.ts`.
|
||||
- `scripts/build-public-npm-package.mjs`, `scripts/package-artifacts.mjs`,
|
||||
`scripts/published-package-smoke.mjs`,
|
||||
`scripts/local-embeddings-runtime-smoke.mjs`,
|
||||
`scripts/publish-public-npm-package.mjs`, and
|
||||
`.github/workflows/release.yml`.
|
||||
- `release-policy.json` is in `npm-public-release-ready` mode, publishes
|
||||
`@kaelio/ktx`, disables Python package publishing, and encodes the hard
|
||||
`uv` prerequisite.
|
||||
- `README.md` and `examples/package-artifacts/README.md` document public npm
|
||||
usage, managed runtime commands, `runtime prune`, and the `uv` prerequisite.
|
||||
|
||||
The remaining mismatch is in the artifact release surface:
|
||||
|
||||
- `scripts/package-artifacts.mjs` still runs `uv build --package ktx-sl` and
|
||||
`uv build --package ktx-daemon`.
|
||||
- `scripts/package-artifacts.mjs` still adds `ktx-sl` and `ktx-daemon` wheel
|
||||
and source-distribution files to the artifact manifest.
|
||||
- `scripts/package-artifacts.mjs` still runs a direct Python clean-install
|
||||
smoke, even though the npm artifact smoke already proves Python-backed
|
||||
commands through the managed runtime.
|
||||
- `release-policy.json` still lists `ktx-sl` and `ktx-daemon` under
|
||||
`python.packages`.
|
||||
- `examples/package-artifacts/README.md` says the Python smoke installs
|
||||
standalone Python artifacts directly.
|
||||
|
||||
This plan removes those release artifacts. It does not delete the Python source
|
||||
packages because the bundled runtime wheel builder still copies from
|
||||
`python/ktx-sl/semantic_layer` and `python/ktx-daemon/src/ktx_daemon`.
|
||||
|
||||
## File structure
|
||||
|
||||
- Modify `scripts/package-artifacts.test.mjs`: make artifact tests expect only
|
||||
`@kaelio/ktx` plus the `kaelio-ktx` bundled runtime wheel, and add a guard
|
||||
that direct standalone Python artifact smoke code is gone.
|
||||
- Modify `scripts/package-artifacts.mjs`: stop building standalone Python
|
||||
artifacts, stop looking for their wheel and source-distribution files, remove
|
||||
their release metadata, and remove the direct Python artifact verification
|
||||
path.
|
||||
- Modify `scripts/release-readiness.test.mjs`: update release policy fixtures
|
||||
and readiness reports so the only Python release metadata is `kaelio-ktx`.
|
||||
- Modify `release-policy.json`: set `python.packages` to `["kaelio-ktx"]`.
|
||||
- Modify `scripts/examples-docs.test.mjs`: require docs to describe the single
|
||||
npm tarball plus runtime wheel artifact shape and reject the old direct
|
||||
Python-artifact smoke wording.
|
||||
- Modify `README.md`: clarify that `python/ktx-sl` and `python/ktx-daemon` are
|
||||
source packages, not release artifacts for the first npm release.
|
||||
- Modify `examples/package-artifacts/README.md`: replace the stale standalone
|
||||
Python smoke paragraph with the managed-runtime artifact contract.
|
||||
|
||||
### Task 1: Make package artifact tests expect one runtime wheel
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/package-artifacts.test.mjs`
|
||||
- Test: `scripts/package-artifacts.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Update package artifact imports**
|
||||
|
||||
In `scripts/package-artifacts.test.mjs`, replace the import from
|
||||
`./package-artifacts.mjs` with this import:
|
||||
|
||||
```javascript
|
||||
import {
|
||||
CLI_PYTHON_ASSET_MANIFEST,
|
||||
INTERNAL_NPM_WORKSPACE_PACKAGES,
|
||||
RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
||||
RUNTIME_WHEEL_NORMALIZED_NAME,
|
||||
RUNTIME_WHEEL_PACKAGE_VERSION,
|
||||
artifactManifestPath,
|
||||
buildArtifactCommands,
|
||||
copyRuntimeWheelAssets,
|
||||
findPythonArtifacts,
|
||||
NPM_ARTIFACT_PACKAGES,
|
||||
npmDemoSmokeSource,
|
||||
npmRuntimeSmokeSource,
|
||||
npmSmokePackageJson,
|
||||
npmVerifySource,
|
||||
packageArtifactLayout,
|
||||
packageReleaseMetadata,
|
||||
verifyArtifactManifest,
|
||||
writeArtifactManifest,
|
||||
} from './package-artifacts.mjs';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove standalone Python fixture setup**
|
||||
|
||||
In `scripts/package-artifacts.test.mjs`, replace `writeReleaseMetadataInputs`
|
||||
with this function:
|
||||
|
||||
```javascript
|
||||
async function writeReleaseMetadataInputs(root) {
|
||||
for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) {
|
||||
await mkdir(join(root, packageInfo.packageRoot), { recursive: true });
|
||||
await writeJson(join(root, packageInfo.packageRoot, 'package.json'), {
|
||||
name: packageInfo.name,
|
||||
version: '0.0.0-private',
|
||||
private: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `writeUploadableArtifactFixtures` with this function:
|
||||
|
||||
```javascript
|
||||
async function writeUploadableArtifactFixtures(layout) {
|
||||
await mkdir(layout.npmDir, { recursive: true });
|
||||
await mkdir(layout.pythonDir, { recursive: true });
|
||||
|
||||
const fileContents = new Map([
|
||||
...NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
|
||||
layout.npmTarballs[packageInfo.name],
|
||||
`${packageInfo.name}-tarball`,
|
||||
]),
|
||||
[
|
||||
join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
||||
'kaelio-ktx-runtime-wheel',
|
||||
],
|
||||
]);
|
||||
|
||||
for (const [path, contents] of fileContents) {
|
||||
await writeFile(path, contents);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Change build command expectations**
|
||||
|
||||
In the `buildArtifactCommands` test, replace the body with this code:
|
||||
|
||||
```javascript
|
||||
it('builds TypeScript packages and the runtime wheel before packing npm artifacts', () => {
|
||||
const layout = packageArtifactLayout('/repo/ktx');
|
||||
const commands = buildArtifactCommands(layout);
|
||||
|
||||
assert.deepEqual(
|
||||
commands.slice(0, NPM_BUILD_PACKAGE_ORDER.length).map((command) => [command.command, command.args]),
|
||||
NPM_BUILD_PACKAGE_ORDER.map((packageName) => ['pnpm', ['--filter', packageName, 'run', 'build']]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
commands.slice(NPM_BUILD_PACKAGE_ORDER.length, NPM_BUILD_PACKAGE_ORDER.length + 1).map((command) => [
|
||||
command.command,
|
||||
command.args,
|
||||
]),
|
||||
[[process.execPath, ['scripts/build-python-runtime-wheel.mjs']]],
|
||||
);
|
||||
assert.deepEqual(
|
||||
commands.slice(NPM_BUILD_PACKAGE_ORDER.length + 1).map((command) => [command.command, command.args]),
|
||||
[[process.execPath, ['scripts/build-public-npm-package.mjs']]],
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Change release metadata expectations**
|
||||
|
||||
In the `packageReleaseMetadata` test, replace the expected array with this
|
||||
array:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(await packageReleaseMetadata(root), [
|
||||
{
|
||||
ecosystem: 'npm',
|
||||
packageName: '@kaelio/ktx',
|
||||
packageRoot: 'packages/cli',
|
||||
packageVersion: '0.1.0',
|
||||
private: false,
|
||||
releaseMode: 'ci-artifact-only',
|
||||
},
|
||||
{
|
||||
ecosystem: 'python',
|
||||
packageName: 'kaelio-ktx',
|
||||
packageRoot: 'python/runtime-wheel',
|
||||
packageVersion: '0.1.0',
|
||||
private: false,
|
||||
releaseMode: 'ci-artifact-only',
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Change Python artifact discovery expectations**
|
||||
|
||||
Replace the `findPythonArtifacts` success test with this test:
|
||||
|
||||
```javascript
|
||||
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'), '');
|
||||
|
||||
assert.deepEqual(await findPythonArtifacts(root), {
|
||||
runtimeWheel: join(root, 'kaelio_ktx-0.1.0-py3-none-any.whl'),
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Change artifact manifest expectations**
|
||||
|
||||
Inside the artifact manifest test, replace the Python package assertion with:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(
|
||||
manifest.packages.filter((entry) => entry.ecosystem === 'python'),
|
||||
[
|
||||
{
|
||||
ecosystem: 'python',
|
||||
packageName: 'kaelio-ktx',
|
||||
packageRoot: 'python/runtime-wheel',
|
||||
packageVersion: '0.1.0',
|
||||
private: false,
|
||||
releaseMode: 'ci-artifact-only',
|
||||
},
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
Replace the Python file assertion with:
|
||||
|
||||
```javascript
|
||||
assert.deepEqual(
|
||||
manifest.files
|
||||
.filter((file) => file.ecosystem === 'python')
|
||||
.map((file) => ({
|
||||
artifactKind: file.artifactKind,
|
||||
ecosystem: file.ecosystem,
|
||||
packageName: file.packageName,
|
||||
packageVersion: file.packageVersion,
|
||||
path: file.path,
|
||||
})),
|
||||
[
|
||||
{
|
||||
artifactKind: 'wheel',
|
||||
ecosystem: 'python',
|
||||
packageName: 'kaelio-ktx',
|
||||
packageVersion: '0.1.0',
|
||||
path: 'python/kaelio_ktx-0.1.0-py3-none-any.whl',
|
||||
},
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
In the `verifyArtifactManifest` success test, replace the file-count assertion
|
||||
with:
|
||||
|
||||
```javascript
|
||||
assert.equal(manifest.files.length, NPM_ARTIFACT_PACKAGES.length + 1);
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Replace direct Python smoke tests with a dead-code guard**
|
||||
|
||||
Remove the whole `describe('pythonArtifactInstallArgs', ...)` block.
|
||||
|
||||
In `describe('verification snippets', ...)`, remove the test named
|
||||
`asserts the Python modules that clean installs must expose`.
|
||||
|
||||
Add this test after the `verifyNpmArtifacts` test:
|
||||
|
||||
```javascript
|
||||
describe('standalone Python artifact cleanup', () => {
|
||||
it('does not build or verify standalone Python package artifacts', async () => {
|
||||
const source = await readFile(new URL('./package-artifacts.mjs', import.meta.url), 'utf8');
|
||||
|
||||
assert.doesNotMatch(source, /uv', \['build', '--package', 'ktx-sl'/);
|
||||
assert.doesNotMatch(source, /uv', \['build', '--package', 'ktx-daemon'/);
|
||||
assert.doesNotMatch(source, /async function verifyPythonArtifacts/);
|
||||
assert.doesNotMatch(source, /pythonArtifactInstallArgs/);
|
||||
assert.doesNotMatch(source, /pythonVerifySource/);
|
||||
assert.doesNotMatch(source, /ktx_sl-0\.1\.0/);
|
||||
assert.doesNotMatch(source, /ktx_daemon-0\.1\.0/);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run package artifact tests and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL. The failures mention the extra `ktx-sl` and `ktx-daemon`
|
||||
artifact commands, metadata entries, manifest files, or direct Python smoke
|
||||
helpers.
|
||||
|
||||
### Task 2: Remove standalone Python artifacts from package artifacts
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/package-artifacts.mjs`
|
||||
- Test: `scripts/package-artifacts.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Remove dead constants and imports**
|
||||
|
||||
In `scripts/package-artifacts.mjs`, replace the `node:path` import with this
|
||||
import:
|
||||
|
||||
```javascript
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
```
|
||||
|
||||
Remove these constants:
|
||||
|
||||
```javascript
|
||||
const PACKAGE_VERSION = '0.0.0-private';
|
||||
const PYTHON_PACKAGE_VERSION = '0.1.0';
|
||||
```
|
||||
|
||||
Remove the whole `ordersSource` constant block.
|
||||
|
||||
- [ ] **Step 2: Make npm artifact names public-package only**
|
||||
|
||||
Replace `npmPackageTarballName` with this function:
|
||||
|
||||
```javascript
|
||||
function npmPackageTarballName(packageName) {
|
||||
if (packageName !== PUBLIC_NPM_PACKAGE_NAME) {
|
||||
throw new Error(`Unsupported npm artifact package: ${packageName}`);
|
||||
}
|
||||
return publicNpmPackageTarballName(PUBLIC_NPM_PACKAGE_VERSION);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Remove standalone Python build commands**
|
||||
|
||||
Replace `buildArtifactCommands` with this function:
|
||||
|
||||
```javascript
|
||||
export function buildArtifactCommands(layout) {
|
||||
const packagesByName = new Map(INTERNAL_NPM_WORKSPACE_PACKAGES.map((packageInfo) => [packageInfo.name, packageInfo]));
|
||||
const npmBuildCommands = NPM_ARTIFACT_BUILD_ORDER.map((packageName) => {
|
||||
const packageInfo = packagesByName.get(packageName);
|
||||
if (!packageInfo) {
|
||||
throw new Error(`Unknown npm artifact build package: ${packageName}`);
|
||||
}
|
||||
return {
|
||||
command: 'pnpm',
|
||||
args: ['--filter', packageInfo.name, 'run', 'build'],
|
||||
cwd: layout.rootDir,
|
||||
};
|
||||
});
|
||||
const publicPackageCommand = {
|
||||
command: process.execPath,
|
||||
args: ['scripts/build-public-npm-package.mjs'],
|
||||
cwd: layout.rootDir,
|
||||
};
|
||||
|
||||
return [
|
||||
...npmBuildCommands,
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ['scripts/build-python-runtime-wheel.mjs'],
|
||||
cwd: layout.rootDir,
|
||||
},
|
||||
publicPackageCommand,
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Discover only the bundled runtime wheel**
|
||||
|
||||
Replace `findOne` and `findPythonArtifacts` with these functions:
|
||||
|
||||
```javascript
|
||||
function findOne(files, distributionName, suffix, label, pythonDir, version) {
|
||||
const normalized = normalizePythonDistributionName(distributionName);
|
||||
const found = files.find((file) => file.startsWith(`${normalized}-${version}`) && file.endsWith(suffix));
|
||||
if (!found) {
|
||||
throw new Error(`Missing Python artifact: ${label}`);
|
||||
}
|
||||
return join(pythonDir, found);
|
||||
}
|
||||
|
||||
export async function findPythonArtifacts(pythonDir) {
|
||||
const files = await readdir(pythonDir);
|
||||
|
||||
return {
|
||||
runtimeWheel: findOne(
|
||||
files,
|
||||
RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
||||
'.whl',
|
||||
'kaelio-ktx runtime wheel',
|
||||
pythonDir,
|
||||
RUNTIME_WHEEL_PACKAGE_VERSION,
|
||||
),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Emit release metadata only for npm and runtime wheel**
|
||||
|
||||
Replace `packageReleaseMetadata` with this function:
|
||||
|
||||
```javascript
|
||||
export async function packageReleaseMetadata(rootDir = scriptRootDir()) {
|
||||
const npmPackages = await Promise.all(
|
||||
NPM_ARTIFACT_PACKAGES.map((packageInfo) => readNpmPackageMetadata(rootDir, packageInfo)),
|
||||
);
|
||||
|
||||
return [
|
||||
...npmPackages,
|
||||
releaseMetadataEntry({
|
||||
ecosystem: 'python',
|
||||
packageName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
||||
packageRoot: 'python/runtime-wheel',
|
||||
packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION,
|
||||
privatePackage: false,
|
||||
}),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Remove dead TOML metadata helpers**
|
||||
|
||||
Delete these helper functions from `scripts/package-artifacts.mjs` because
|
||||
release metadata no longer reads standalone Python `pyproject.toml` files:
|
||||
|
||||
```javascript
|
||||
function readProjectBlock(toml, sourcePath) {
|
||||
const lines = toml.split(/\r?\n/);
|
||||
const block = [];
|
||||
let inProject = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^\[project\]\s*$/.test(line)) {
|
||||
inProject = true;
|
||||
continue;
|
||||
}
|
||||
if (inProject && /^\[.*\]\s*$/.test(line)) {
|
||||
break;
|
||||
}
|
||||
if (inProject) {
|
||||
block.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inProject) {
|
||||
throw new Error(`Missing [project] table in ${sourcePath}`);
|
||||
}
|
||||
return block.join('\n');
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
function readTomlStringField(projectBlock, fieldName, sourcePath) {
|
||||
const match = projectBlock.match(new RegExp(`^${fieldName}\\s*=\\s*"([^"]+)"\\s*$`, 'm'));
|
||||
if (!match) {
|
||||
throw new Error(`Missing project.${fieldName} in ${sourcePath}`);
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
async function readPyprojectMetadata(path) {
|
||||
const toml = await readFile(path, 'utf-8');
|
||||
const projectBlock = readProjectBlock(toml, path);
|
||||
return {
|
||||
name: readTomlStringField(projectBlock, 'name', path),
|
||||
version: readTomlStringField(projectBlock, 'version', path),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Emit manifest records only for npm and runtime wheel**
|
||||
|
||||
Replace `artifactPackageRecords` with this function:
|
||||
|
||||
```javascript
|
||||
function artifactPackageRecords(layout, pythonArtifacts, packages) {
|
||||
const packagesByName = packageMetadataByName(packages);
|
||||
const npmRecords = NPM_ARTIFACT_PACKAGES.map((packageInfo) => ({
|
||||
artifactKind: 'tarball',
|
||||
artifactPath: layout.npmTarballs[packageInfo.name],
|
||||
metadata: requirePackageMetadata(packagesByName, packageInfo.name),
|
||||
}));
|
||||
|
||||
return [
|
||||
...npmRecords,
|
||||
{
|
||||
artifactKind: 'wheel',
|
||||
artifactPath: pythonArtifacts.runtimeWheel,
|
||||
metadata: requirePackageMetadata(packagesByName, RUNTIME_WHEEL_DISTRIBUTION_NAME),
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Remove direct Python artifact verification helpers**
|
||||
|
||||
Delete these exports and functions from `scripts/package-artifacts.mjs`:
|
||||
|
||||
```javascript
|
||||
export function pythonArtifactInstallArgs(python, pythonArtifacts) {
|
||||
return ['pip', 'install', '--python', python, pythonArtifacts.runtimeWheel];
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
export function pythonVerifySource() {
|
||||
return `
|
||||
import importlib.metadata
|
||||
|
||||
import semantic_layer
|
||||
import ktx_daemon
|
||||
|
||||
assert importlib.metadata.version("kaelio-ktx") == "0.1.0"
|
||||
assert semantic_layer is not None
|
||||
assert ktx_daemon.PACKAGE_NAME == "ktx-daemon"
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
function pythonExecutable(projectDir) {
|
||||
if (process.platform === 'win32') {
|
||||
return join(projectDir, '.venv', 'Scripts', 'python.exe');
|
||||
}
|
||||
return join(projectDir, '.venv', 'bin', 'python');
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
export function npmSmokePythonEnv(projectDir, baseEnv = process.env) {
|
||||
const binDir = process.platform === 'win32' ? join(projectDir, '.venv', 'Scripts') : join(projectDir, '.venv', 'bin');
|
||||
const existingPath = baseEnv.PATH ?? '';
|
||||
|
||||
return {
|
||||
...baseEnv,
|
||||
PATH: existingPath ? `${binDir}${delimiter}${existingPath}` : binDir,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
async function verifyPythonArtifacts(layout, tmpRoot) {
|
||||
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
||||
|
||||
const projectDir = join(tmpRoot, 'python-clean-install');
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
const python = pythonExecutable(projectDir);
|
||||
await writeFile(join(projectDir, 'verify_python.py'), pythonVerifySource());
|
||||
|
||||
await runCommand('uv', ['venv', '.venv'], { cwd: projectDir });
|
||||
await runCommand('uv', pythonArtifactInstallArgs(python, pythonArtifacts), {
|
||||
cwd: projectDir,
|
||||
});
|
||||
await runCommand(python, ['verify_python.py'], { cwd: projectDir });
|
||||
await runCommand(python, ['-m', 'ktx_daemon', 'semantic-validate'], {
|
||||
cwd: projectDir,
|
||||
input: `${JSON.stringify({ sources: [ordersSource], dialect: 'postgres' })}\n`,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Verify artifacts through npm only**
|
||||
|
||||
Replace `verifyArtifacts` with this function:
|
||||
|
||||
```javascript
|
||||
async function verifyArtifacts(layout) {
|
||||
await verifyArtifactManifest(layout);
|
||||
|
||||
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-artifacts-'));
|
||||
try {
|
||||
await verifyNpmArtifacts(layout, tmpRoot);
|
||||
} finally {
|
||||
await rm(tmpRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 10: Run package artifact tests and verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The output includes `# fail 0`.
|
||||
|
||||
- [ ] **Step 11: Commit package artifact cleanup**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs
|
||||
git commit -m "refactor: limit release artifacts to public package runtime"
|
||||
```
|
||||
|
||||
### Task 3: Align release policy and readiness reports
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `release-policy.json`
|
||||
- Modify: `scripts/release-readiness.test.mjs`
|
||||
- Test: `scripts/release-readiness.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Update release readiness fixtures**
|
||||
|
||||
In `scripts/release-readiness.test.mjs`, replace
|
||||
`writeReleaseMetadataInputs` with:
|
||||
|
||||
```javascript
|
||||
async function writeReleaseMetadataInputs(root) {
|
||||
for (const packageInfo of INTERNAL_NPM_WORKSPACE_PACKAGES) {
|
||||
await mkdir(join(root, packageInfo.packageRoot), { recursive: true });
|
||||
await writeJson(join(root, packageInfo.packageRoot, 'package.json'), {
|
||||
name: packageInfo.name,
|
||||
version: '0.0.0-private',
|
||||
private: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `writeUploadableArtifactFixtures` with:
|
||||
|
||||
```javascript
|
||||
async function writeUploadableArtifactFixtures(layout) {
|
||||
await mkdir(layout.npmDir, { recursive: true });
|
||||
await mkdir(layout.pythonDir, { recursive: true });
|
||||
|
||||
const fileContents = new Map([
|
||||
...NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
|
||||
layout.npmTarballs[packageInfo.name],
|
||||
`${packageInfo.name}-tarball`,
|
||||
]),
|
||||
[join(layout.pythonDir, 'kaelio_ktx-0.1.0-py3-none-any.whl'), 'kaelio-ktx-runtime-wheel'],
|
||||
]);
|
||||
|
||||
for (const [path, contents] of fileContents) {
|
||||
await writeFile(path, contents);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In `releasePolicy`, replace the `python` object with:
|
||||
|
||||
```javascript
|
||||
python: {
|
||||
publish: false,
|
||||
repository: null,
|
||||
packages: ['kaelio-ktx'],
|
||||
...pythonOverrides,
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update readiness report expectations**
|
||||
|
||||
In `scripts/release-readiness.test.mjs`, replace every expected
|
||||
`packageNames` array with:
|
||||
|
||||
```javascript
|
||||
packageNames: ['@kaelio/ktx', 'kaelio-ktx'],
|
||||
```
|
||||
|
||||
There are three report assertions to update:
|
||||
|
||||
- `accepts the current ci-artifact-only policy, package metadata, and artifact manifest`
|
||||
- `reports required published package smoke when release mode requires it`
|
||||
- `accepts the npm public release ready policy`
|
||||
|
||||
- [ ] **Step 3: Update checked release policy**
|
||||
|
||||
In `release-policy.json`, replace the `python.packages` value with:
|
||||
|
||||
```json
|
||||
"packages": ["kaelio-ktx"]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run readiness tests and verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/release-readiness.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The output includes `# fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit release policy cleanup**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add release-policy.json scripts/release-readiness.test.mjs
|
||||
git commit -m "chore: align release policy with bundled runtime wheel"
|
||||
```
|
||||
|
||||
### Task 4: Document the single release artifact surface
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/examples-docs.test.mjs`
|
||||
- Modify: `README.md`
|
||||
- Modify: `examples/package-artifacts/README.md`
|
||||
- Test: `scripts/examples-docs.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add failing docs assertions**
|
||||
|
||||
In `scripts/examples-docs.test.mjs`, inside
|
||||
`it('documents the public package artifact smoke shape', ...)`, add these
|
||||
assertions after the existing `assert.match(readme, /managed Python runtime/);`
|
||||
line:
|
||||
|
||||
```javascript
|
||||
assert.match(readme, /public `@kaelio\/ktx` npm tarball and the bundled `kaelio-ktx` runtime wheel/);
|
||||
assert.match(readme, /does not install standalone Python packages directly/);
|
||||
assert.doesNotMatch(readme, /standalone Python distributions/);
|
||||
assert.doesNotMatch(readme, /installs the Python artifacts directly/);
|
||||
```
|
||||
|
||||
In `it('documents public npm and managed runtime usage in the README', ...)`,
|
||||
add these assertions after the existing `uv` assertions:
|
||||
|
||||
```javascript
|
||||
assert.match(rootReadme, /release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx` runtime wheel/);
|
||||
assert.match(rootReadme, /source packages for development, not public release artifacts/);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run docs tests and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: FAIL. The failure mentions the missing single-artifact wording in
|
||||
`README.md` or `examples/package-artifacts/README.md`.
|
||||
|
||||
- [ ] **Step 3: Update the package artifact example README**
|
||||
|
||||
In `examples/package-artifacts/README.md`, replace:
|
||||
|
||||
```markdown
|
||||
The Python smoke project still installs the Python artifacts directly because
|
||||
it verifies the standalone Python distributions that feed the bundled runtime
|
||||
wheel.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
The artifact manifest contains the public `@kaelio/ktx` npm tarball and the
|
||||
bundled `kaelio-ktx` runtime wheel. The smoke does not install standalone
|
||||
Python packages directly; Python-backed behavior is verified through the
|
||||
managed runtime installed from the npm package.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update the root README release status**
|
||||
|
||||
In `README.md`, in the `## Release status` section, replace this paragraph:
|
||||
|
||||
```markdown
|
||||
This repository builds one public npm artifact named `@kaelio/ktx`. The first
|
||||
public npm handoff is policy-gated through `release-policy.json`, which keeps
|
||||
Python package publishing disabled because KTX-owned Python code ships inside
|
||||
the npm package as a bundled wheel.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```markdown
|
||||
This repository builds one public npm artifact named `@kaelio/ktx`. The release
|
||||
artifact manifest contains the public npm tarball and the bundled `kaelio-ktx`
|
||||
runtime wheel. The first public npm handoff is policy-gated through
|
||||
`release-policy.json`, which keeps Python package publishing disabled because
|
||||
KTX-owned Python code ships inside the npm package as a bundled wheel. The
|
||||
`python/ktx-sl` and `python/ktx-daemon` directories remain source packages for
|
||||
development, not public release artifacts.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run docs tests and verify pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The output includes `# fail 0`.
|
||||
|
||||
- [ ] **Step 6: Commit docs cleanup**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add README.md examples/package-artifacts/README.md scripts/examples-docs.test.mjs
|
||||
git commit -m "docs: describe single public runtime artifact surface"
|
||||
```
|
||||
|
||||
### Task 5: Verify the cleaned release artifact contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `scripts/package-artifacts.mjs`
|
||||
- Verify: `scripts/package-artifacts.test.mjs`
|
||||
- Verify: `scripts/release-readiness.test.mjs`
|
||||
- Verify: `scripts/examples-docs.test.mjs`
|
||||
- Verify: `release-policy.json`
|
||||
- Verify: `README.md`
|
||||
- Verify: `examples/package-artifacts/README.md`
|
||||
|
||||
- [ ] **Step 1: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs
|
||||
```
|
||||
|
||||
Expected: PASS. The output includes `# fail 0`.
|
||||
|
||||
- [ ] **Step 2: Verify stale artifact strings are gone from production/docs files**
|
||||
|
||||
Run (scans only production and docs files, not test files — test files keep guard assertions that reference the removed strings):
|
||||
|
||||
```bash
|
||||
rg -n "uv', \\['build', '--package', 'ktx-sl'|uv', \\['build', '--package', 'ktx-daemon'|ktx_sl-0\\.1\\.0|ktx_daemon-0\\.1\\.0|pythonArtifactInstallArgs|pythonVerifySource|verifyPythonArtifacts|standalone Python distributions|installs the Python artifacts directly" scripts/package-artifacts.mjs scripts/release-readiness.mjs README.md examples/package-artifacts/README.md release-policy.json
|
||||
```
|
||||
|
||||
Expected: no matches.
|
||||
|
||||
- [ ] **Step 3: Verify release readiness against the current artifact manifest**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm run release:readiness -- --json
|
||||
```
|
||||
|
||||
Expected: PASS when `dist/artifacts/manifest.json` has been rebuilt after this
|
||||
change. The JSON output contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"releaseMode": "npm-public-release-ready",
|
||||
"packageNames": ["@kaelio/ktx", "kaelio-ktx"],
|
||||
"pythonPublishEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
If this command fails because the local artifact manifest was generated before
|
||||
the cleanup, run:
|
||||
|
||||
```bash
|
||||
pnpm run artifacts:check
|
||||
pnpm run release:readiness -- --json
|
||||
```
|
||||
|
||||
Expected: both commands pass. The rebuilt manifest contains only
|
||||
`npm/kaelio-ktx-0.1.0.tgz` and
|
||||
`python/kaelio_ktx-0.1.0-py3-none-any.whl` under `files`.
|
||||
|
||||
- [ ] **Step 4: Run pre-commit on changed files when configured**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run --files scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs release-policy.json README.md examples/package-artifacts/README.md
|
||||
```
|
||||
|
||||
Expected: PASS. If pre-commit is not installed or no pre-commit config exists,
|
||||
record the exact error and keep the focused Node test output from Step 1.
|
||||
|
||||
- [ ] **Step 5: Commit final verification fixes if needed**
|
||||
|
||||
If Step 1, Step 2, Step 3, or Step 4 required code or docs fixes, commit them:
|
||||
|
||||
```bash
|
||||
git add scripts/package-artifacts.mjs scripts/package-artifacts.test.mjs scripts/release-readiness.test.mjs scripts/examples-docs.test.mjs release-policy.json README.md examples/package-artifacts/README.md
|
||||
git commit -m "test: verify single public runtime artifact contract"
|
||||
```
|
||||
|
||||
If no fixes were required after the previous commits, do not create an empty
|
||||
commit.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `scripts/package-artifacts.mjs` builds TypeScript packages, builds the
|
||||
bundled `kaelio-ktx` runtime wheel, copies it into CLI assets, and packs the
|
||||
public `@kaelio/ktx` npm tarball.
|
||||
- `scripts/package-artifacts.mjs` no longer builds `ktx-sl` or `ktx-daemon`
|
||||
standalone wheel or source-distribution artifacts.
|
||||
- Artifact manifests contain release metadata for `@kaelio/ktx` and
|
||||
`kaelio-ktx` only.
|
||||
- `release-policy.json` lists only `@kaelio/ktx` under `npm.packages` and only
|
||||
`kaelio-ktx` under `python.packages`.
|
||||
- The artifact smoke verifies Python-backed behavior through the installed
|
||||
public npm package and managed runtime, not by installing standalone Python
|
||||
artifacts directly.
|
||||
- Public docs state that `python/ktx-sl` and `python/ktx-daemon` remain source
|
||||
packages for development, not public release artifacts.
|
||||
|
||||
## Self-review
|
||||
|
||||
Spec coverage:
|
||||
|
||||
- The plan preserves the single public npm package requirement.
|
||||
- The plan preserves the bundled KTX-owned Python wheel requirement.
|
||||
- The plan keeps Python package publishing disabled.
|
||||
- The plan removes the only remaining artifact path that treated KTX-owned
|
||||
Python source packages as standalone release artifacts.
|
||||
|
||||
Placeholder scan:
|
||||
|
||||
- No steps contain placeholder implementation text.
|
||||
- Every code-changing step names exact files and provides concrete replacement
|
||||
snippets.
|
||||
|
||||
Type and name consistency:
|
||||
|
||||
- Public npm package name remains `@kaelio/ktx`.
|
||||
- Bundled runtime distribution name remains `kaelio-ktx`.
|
||||
- Runtime wheel filename remains `kaelio_ktx-0.1.0-py3-none-any.whl`.
|
||||
- Removed standalone Python artifact names are consistently `ktx-sl` and
|
||||
`ktx-daemon`.
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
# npm-managed Python runtime design
|
||||
|
||||
This spec defines how KTX ships as one visible npm package while still using
|
||||
Python for sqlglot, semantic-layer planning, database-agent compute, and local
|
||||
embeddings. The goal is a user experience where users install or run only
|
||||
`@kaelio/ktx`, and KTX manages its Python runtime automatically when a command
|
||||
needs it.
|
||||
|
||||
## Goals
|
||||
|
||||
KTX must be usable through the npm package `@kaelio/ktx` with a `ktx` binary.
|
||||
Users can run KTX without learning about the Python packages that power parts of
|
||||
the system.
|
||||
|
||||
The first release must support these invocation modes:
|
||||
|
||||
- `npx @kaelio/ktx setup demo`
|
||||
- `npx @kaelio/ktx sl query ...`
|
||||
- `npm install @kaelio/ktx`, followed by `npx ktx ...`
|
||||
- `npm install -g @kaelio/ktx`, followed by `ktx ...`
|
||||
|
||||
KTX-owned Python code must ship inside the npm package as a bundled wheel. KTX
|
||||
doesn't need to publish its own Python code to PyPI for this release.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This release does not need to provide a public TypeScript SDK split across
|
||||
multiple npm packages. The internal workspace package layout can remain useful
|
||||
for development, but the public npm surface is a single package.
|
||||
|
||||
This release does not need a fully offline install. KTX's own Python wheel is
|
||||
bundled, but third-party Python dependencies can come from PyPI through `uv`.
|
||||
|
||||
This release does not install local embedding dependencies by default. Local
|
||||
embeddings remain lazy because `sentence-transformers`, `torch`, and model
|
||||
downloads are large.
|
||||
|
||||
## Package model
|
||||
|
||||
KTX publishes one public npm package:
|
||||
|
||||
```text
|
||||
@kaelio/ktx
|
||||
```
|
||||
|
||||
That package exposes one binary:
|
||||
|
||||
```json
|
||||
{
|
||||
"bin": {
|
||||
"ktx": "./dist/bin.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The npm package includes these assets:
|
||||
|
||||
- Bundled JavaScript CLI output.
|
||||
- Packaged demo assets.
|
||||
- One KTX-owned Python wheel, for example
|
||||
`python/kaelio_ktx-0.1.0-py3-none-any.whl`.
|
||||
- A wheel checksum or runtime manifest that lets the CLI verify the bundled
|
||||
Python payload before installation.
|
||||
|
||||
The Python wheel contains the current `semantic_layer` and `ktx_daemon`
|
||||
modules. It exposes at least the `ktx-daemon` console script.
|
||||
|
||||
## Runtime installation
|
||||
|
||||
KTX creates a managed Python runtime only when a command needs Python-backed
|
||||
behavior. The runtime lives outside the npm cache so it survives `npx` runs.
|
||||
|
||||
The runtime root is platform-specific:
|
||||
|
||||
- macOS: `~/Library/Application Support/kaelio/ktx/runtime`
|
||||
- Linux: `${XDG_DATA_HOME:-~/.local/share}/kaelio/ktx/runtime`
|
||||
- Windows: `%LOCALAPPDATA%/Kaelio/KTX/runtime`
|
||||
|
||||
The runtime is versioned by the npm package version. A versioned runtime avoids
|
||||
mixing JavaScript and Python code from incompatible releases.
|
||||
|
||||
The installer performs these steps:
|
||||
|
||||
1. Locate `uv`.
|
||||
2. Create a virtual environment under the versioned runtime directory.
|
||||
3. Install the bundled KTX wheel into that environment.
|
||||
4. Write a runtime manifest with the CLI version, wheel checksum, Python
|
||||
executable, daemon executable, and installed feature set.
|
||||
|
||||
For lightweight Python support, the install command uses the bundled wheel's
|
||||
default dependency set. For local embeddings, the installer adds the embeddings
|
||||
extra only when selected:
|
||||
|
||||
```bash
|
||||
uv pip install "/path/to/kaelio_ktx-0.1.0-py3-none-any.whl"
|
||||
uv pip install "/path/to/kaelio_ktx-0.1.0-py3-none-any.whl[local-embeddings]"
|
||||
```
|
||||
|
||||
## Feature installation levels
|
||||
|
||||
KTX manages Python runtime features in levels so first use stays fast.
|
||||
|
||||
`core` includes:
|
||||
|
||||
- `sqlglot`
|
||||
- `pydantic`
|
||||
- `pyyaml`
|
||||
- `fastapi`
|
||||
- `uvicorn`
|
||||
- lightweight daemon dependencies
|
||||
|
||||
`local-embeddings` adds:
|
||||
|
||||
- `sentence-transformers`
|
||||
- `torch`
|
||||
- model download support for `all-MiniLM-L6-v2`
|
||||
|
||||
Commands that only need semantic-layer SQL generation require `core`.
|
||||
Commands that need local embeddings require `local-embeddings`.
|
||||
|
||||
## Command behavior
|
||||
|
||||
Pure TypeScript commands run without the managed Python runtime.
|
||||
|
||||
Python-backed one-shot operations use the managed `ktx-daemon` executable
|
||||
directly. Examples include semantic query compilation, semantic validation,
|
||||
semantic source generation, and sqlglot-backed table identifier parsing.
|
||||
|
||||
Repeated or expensive operations use a managed HTTP daemon. Local embeddings use
|
||||
the daemon because loading the model for every one-shot process is too slow.
|
||||
|
||||
KTX provides runtime management commands:
|
||||
|
||||
```bash
|
||||
ktx runtime install
|
||||
ktx runtime status
|
||||
ktx runtime start
|
||||
ktx runtime stop
|
||||
ktx runtime doctor
|
||||
ktx runtime prune
|
||||
```
|
||||
|
||||
Normal commands can install the runtime lazily. Runtime commands make that
|
||||
behavior inspectable and debuggable.
|
||||
|
||||
## Daemon lifecycle
|
||||
|
||||
The daemon binds to `127.0.0.1` on an available random port. KTX writes daemon
|
||||
state to the runtime manifest or an adjacent state file:
|
||||
|
||||
```json
|
||||
{
|
||||
"pid": 12345,
|
||||
"port": 58731,
|
||||
"version": "0.1.0",
|
||||
"features": ["core", "local-embeddings"],
|
||||
"startedAt": "2026-05-11T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Before reusing a daemon, KTX checks that the process is alive, the port responds
|
||||
to `/health`, and the daemon version matches the CLI version. If any check
|
||||
fails, KTX treats the daemon as stale and starts a new one.
|
||||
|
||||
KTX uses one-shot Python for short operations by default. It starts the daemon
|
||||
only when a command benefits from process reuse.
|
||||
|
||||
## Interactive and CI behavior
|
||||
|
||||
In an interactive terminal, KTX prompts before installing the managed runtime
|
||||
for the first time. The prompt states that Python dependencies will be
|
||||
downloaded.
|
||||
|
||||
With `--yes`, KTX installs the required runtime features without prompting.
|
||||
|
||||
With `--no-input`, KTX fails if a required runtime feature is missing and no
|
||||
explicit auto-install flag is present. The error prints the exact command to
|
||||
prepare the runtime.
|
||||
|
||||
For local embeddings, KTX prompts separately because the dependency and model
|
||||
downloads are larger than the core runtime.
|
||||
|
||||
## Error handling
|
||||
|
||||
If `uv` is missing, KTX prints a focused error that explains how to install it
|
||||
and how to retry. A later release can add a bundled or downloaded `uv` strategy.
|
||||
|
||||
If Python runtime installation fails, KTX preserves install logs in the runtime
|
||||
directory and prints the log path.
|
||||
|
||||
If the daemon fails to start, KTX prints the captured daemon stdout and stderr
|
||||
path. It falls back to one-shot mode only when the requested operation supports
|
||||
one-shot execution.
|
||||
|
||||
If JavaScript and Python versions don't match, KTX reinstalls the managed
|
||||
runtime for the current npm package version.
|
||||
|
||||
## Release flow
|
||||
|
||||
The release builds the Python wheel before packing npm artifacts. The npm pack
|
||||
step includes the wheel as an asset.
|
||||
|
||||
Release checks must cover:
|
||||
|
||||
1. Clean install of the packed npm package.
|
||||
2. `npx` execution of the packed package.
|
||||
3. First-run managed runtime install from the bundled wheel.
|
||||
4. One-shot semantic-layer query through the managed runtime.
|
||||
5. Runtime status and doctor output.
|
||||
6. Daemon start, health check, reuse, and stop.
|
||||
7. Optional local embeddings smoke in a separate job or opt-in check.
|
||||
|
||||
## Open decisions
|
||||
|
||||
KTX still needs a final decision on whether `uv` is a hard prerequisite or a
|
||||
bootstrap dependency that KTX downloads automatically.
|
||||
|
||||
KTX also needs the final Python distribution name. This spec uses
|
||||
`kaelio-ktx` as the distribution name and `kaelio_ktx` in wheel filenames.
|
||||
|
||||
## Success criteria
|
||||
|
||||
Users can run `npx @kaelio/ktx ...` and complete Python-backed KTX operations
|
||||
without manually installing a KTX Python package.
|
||||
|
||||
Users who install `@kaelio/ktx` locally can run `npx ktx ...` through the local
|
||||
project's npm binary resolution.
|
||||
|
||||
The first Python-backed command installs only the core runtime. Local embedding
|
||||
dependencies install only after the user selects local embeddings or explicitly
|
||||
requests the `local-embeddings` runtime feature.
|
||||
|
||||
KTX can diagnose and repair stale or mismatched managed runtimes without asking
|
||||
users to delete directories manually.
|
||||
Loading…
Add table
Add a link
Reference in a new issue