2026-05-11 15:50:34 +02:00
import { execFile } from 'node:child_process' ;
import { createHash } from 'node:crypto' ;
2026-05-13 12:28:24 +02:00
import { access , appendFile , mkdir , readFile , rm , writeFile } from 'node:fs/promises' ;
2026-05-11 15:50:34 +02:00
import { homedir } from 'node:os' ;
import { basename , join } from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import { promisify } from 'node:util' ;
2026-05-19 18:18:38 +02:00
import { strFromU8 , unzipSync } from 'fflate' ;
2026-05-11 15:50:34 +02:00
import { z } from 'zod' ;
const execFileAsync = promisify ( execFile ) ;
export const runtimeFeatureSchema = z . enum ( [ 'core' , 'local-embeddings' ] ) ;
export type KtxRuntimeFeature = z . infer < typeof runtimeFeatureSchema > ;
const runtimeAssetManifestSchema = z . object ( {
schemaVersion : z.literal ( 1 ) ,
distributionName : z.literal ( 'kaelio-ktx' ) ,
normalizedName : z.literal ( 'kaelio_ktx' ) ,
version : z.string ( ) . min ( 1 ) ,
wheel : z.object ( {
file : z.string ( ) . min ( 1 ) ,
sha256 : z.string ( ) . regex ( /^[a-f0-9]{64}$/ ) ,
bytes : z.number ( ) . int ( ) . nonnegative ( ) ,
} ) ,
} ) ;
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
type KtxRuntimeAssetManifest = z . infer < typeof runtimeAssetManifestSchema > ;
2026-05-11 15:50:34 +02:00
const installedRuntimeManifestSchema = z . object ( {
schemaVersion : z.literal ( 1 ) ,
cliVersion : z.string ( ) . min ( 1 ) ,
installedAt : z.string ( ) . min ( 1 ) ,
asset : runtimeAssetManifestSchema ,
features : z.array ( runtimeFeatureSchema ) . min ( 1 ) ,
python : z.object ( {
executable : z.string ( ) . min ( 1 ) ,
daemonExecutable : z.string ( ) . min ( 1 ) ,
} ) ,
installLog : z.string ( ) . min ( 1 ) ,
} ) ;
export type InstalledKtxRuntimeManifest = z . infer < typeof installedRuntimeManifestSchema > ;
export interface ManagedPythonRuntimeLayoutOptions {
cliVersion : string ;
platform? : NodeJS.Platform ;
env? : NodeJS.ProcessEnv ;
homeDir? : string ;
runtimeRoot? : string ;
assetDir? : string ;
}
export interface ManagedPythonRuntimeLayout {
cliVersion : string ;
runtimeRoot : string ;
versionDir : string ;
venvDir : string ;
manifestPath : string ;
installLogPath : string ;
assetDir : string ;
assetManifestPath : string ;
pythonPath : string ;
daemonPath : string ;
2026-05-14 14:35:55 +02:00
}
export interface ManagedPythonDaemonLayoutOptions extends ManagedPythonRuntimeLayoutOptions {
projectDir : string ;
}
export interface ManagedPythonDaemonLayout extends ManagedPythonRuntimeLayout {
projectDir : string ;
daemonStateDir : string ;
2026-05-11 15:50:34 +02:00
daemonStatePath : string ;
daemonStdoutPath : string ;
daemonStderrPath : string ;
}
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
/** @internal */
2026-05-11 15:50:34 +02:00
export interface ManagedRuntimeAsset {
manifest : KtxRuntimeAssetManifest ;
wheelPath : string ;
2026-05-19 18:18:38 +02:00
requiresPython : {
specifier : string ;
minimumVersion : string ;
} ;
2026-05-11 15:50:34 +02:00
}
export type ManagedPythonRuntimeExec = (
command : string ,
args : string [ ] ,
options ? : { cwd? : string ; env? : NodeJS.ProcessEnv } ,
) = > Promise < { stdout : string ; stderr : string } > ;
export interface ManagedPythonRuntimeInstallOptions extends ManagedPythonRuntimeLayoutOptions {
features : KtxRuntimeFeature [ ] ;
force? : boolean ;
exec? : ManagedPythonRuntimeExec ;
}
export interface ManagedPythonRuntimeInstallResult {
status : 'ready' | 'installed' ;
layout : ManagedPythonRuntimeLayout ;
asset : ManagedRuntimeAsset ;
manifest : InstalledKtxRuntimeManifest ;
}
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
type ManagedPythonRuntimeStatusKind = 'missing' | 'ready' | 'mismatched' | 'broken' ;
2026-05-11 15:50:34 +02:00
export interface ManagedPythonRuntimeStatus {
kind : ManagedPythonRuntimeStatusKind ;
detail : string ;
layout : ManagedPythonRuntimeLayout ;
manifest? : InstalledKtxRuntimeManifest ;
}
export interface ManagedPythonRuntimeDoctorCheck {
id : 'uv' | 'asset' | 'runtime' ;
label : string ;
status : 'pass' | 'fail' ;
detail : string ;
fix? : string ;
}
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
/** @internal */
2026-05-11 15:50:34 +02:00
export const MISSING_UV_RUNTIME_INSTALL_MESSAGE =
2026-05-20 01:36:54 +02:00
'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 admin runtime install --yes' ;
2026-05-11 15:50:34 +02:00
function defaultAssetDir ( ) : string {
return fileURLToPath ( new URL ( '../assets/python/' , import . meta . url ) ) ;
}
2026-05-14 14:35:55 +02:00
function runtimeRootFor ( input : { env : NodeJS.ProcessEnv ; homeDir : string } ) : string {
2026-05-11 15:50:34 +02:00
if ( input . env . KTX_RUNTIME_ROOT ) {
return input . env . KTX_RUNTIME_ROOT ;
}
2026-05-14 14:35:55 +02:00
return join ( input . homeDir , '.ktx' , 'runtime' ) ;
2026-05-11 15:50:34 +02:00
}
function executablePath ( venvDir : string , platform : NodeJS.Platform , name : string ) : string {
if ( platform === 'win32' ) {
return join ( venvDir , 'Scripts' , ` ${ name } .exe ` ) ;
}
return join ( venvDir , 'bin' , name ) ;
}
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
/** @internal */
2026-05-11 15:50:34 +02:00
export function managedPythonRuntimeLayout ( options : ManagedPythonRuntimeLayoutOptions ) : ManagedPythonRuntimeLayout {
const platform = options . platform ? ? process . platform ;
const env = options . env ? ? process . env ;
const homeDir = options . homeDir ? ? homedir ( ) ;
2026-05-14 14:35:55 +02:00
const runtimeRoot = options . runtimeRoot ? ? runtimeRootFor ( { env , homeDir } ) ;
2026-05-11 15:50:34 +02:00
const versionDir = join ( runtimeRoot , options . cliVersion ) ;
const venvDir = join ( versionDir , '.venv' ) ;
const assetDir = options . assetDir ? ? defaultAssetDir ( ) ;
return {
cliVersion : options.cliVersion ,
runtimeRoot ,
versionDir ,
venvDir ,
manifestPath : join ( versionDir , 'manifest.json' ) ,
installLogPath : join ( versionDir , 'install.log' ) ,
assetDir ,
assetManifestPath : join ( assetDir , 'manifest.json' ) ,
pythonPath : executablePath ( venvDir , platform , 'python' ) ,
daemonPath : executablePath ( venvDir , platform , 'ktx-daemon' ) ,
2026-05-14 14:35:55 +02:00
} ;
}
export function managedPythonDaemonLayout ( options : ManagedPythonDaemonLayoutOptions ) : ManagedPythonDaemonLayout {
const runtime = managedPythonRuntimeLayout ( options ) ;
const daemonStateDir = join ( options . projectDir , '.ktx' , 'runtime' ) ;
return {
. . . runtime ,
projectDir : options.projectDir ,
daemonStateDir ,
daemonStatePath : join ( daemonStateDir , 'daemon.json' ) ,
daemonStdoutPath : join ( daemonStateDir , 'daemon.stdout.log' ) ,
daemonStderrPath : join ( daemonStateDir , 'daemon.stderr.log' ) ,
2026-05-11 15:50:34 +02:00
} ;
}
async function pathExists ( path : string ) : Promise < boolean > {
try {
await access ( path ) ;
return true ;
} catch {
return false ;
}
}
function assertSafeWheelFilename ( file : string ) : void {
if ( file !== basename ( file ) || file . includes ( '/' ) || file . includes ( '\\' ) ) {
throw new Error ( ` Unsafe runtime wheel filename in bundled manifest: ${ file } ` ) ;
}
}
async function readJsonFile ( path : string ) : Promise < unknown > {
return JSON . parse ( await readFile ( path , 'utf8' ) ) as unknown ;
}
2026-05-12 10:26:05 +02:00
function isErrnoException ( error : unknown , code : string ) : boolean {
return typeof error === 'object' && error !== null && 'code' in error && error . code === code ;
}
2026-05-19 18:18:38 +02:00
function parseRequiresPythonFromWheel ( input : { wheelPath : string ; contents : Buffer } ) : ManagedRuntimeAsset [ 'requiresPython' ] {
let files : Record < string , Uint8Array > ;
try {
files = unzipSync ( new Uint8Array ( input . contents ) ) ;
} catch ( error ) {
throw new Error (
` Unable to read bundled Python runtime wheel metadata: ${ error instanceof Error ? error.message : String ( error ) } ` ,
) ;
}
const metadataEntry = Object . entries ( files ) . find ( ( [ path ] ) = > path . endsWith ( '.dist-info/METADATA' ) ) ;
if ( ! metadataEntry ) {
throw new Error ( ` Bundled Python runtime wheel metadata is missing: ${ input . wheelPath } ` ) ;
}
const metadata = strFromU8 ( metadataEntry [ 1 ] ) ;
const requiresPython = metadata
. split ( /\r?\n/ )
. map ( ( line ) = > line . match ( /^Requires-Python:\s*(.+)\s*$/i ) ? . [ 1 ] ? . trim ( ) )
. find ( ( value ) : value is string = > typeof value === 'string' && value . length > 0 ) ;
if ( ! requiresPython ) {
throw new Error ( 'Bundled Python runtime wheel metadata is missing Requires-Python' ) ;
}
const minimumMatch = requiresPython . match ( /(?:^|[,\s])>=\s*([0-9]+)\.([0-9]+)(?:\.[0-9]+)?\b/ ) ;
if ( ! minimumMatch ) {
throw new Error ( ` Unsupported bundled Python runtime Requires-Python: ${ requiresPython } ` ) ;
}
return {
specifier : requiresPython ,
minimumVersion : ` ${ minimumMatch [ 1 ] } . ${ minimumMatch [ 2 ] } ` ,
} ;
}
chore(workspace): gate dead-code with knip production mode (#196)
* refactor(workspace): relocate @ktx/llm source into packages/cli/src/llm
* refactor(workspace): rewrite @ktx/llm imports to relative paths
* refactor(workspace): fold internal packages into cli
* chore(workspace): gate dead-code with knip production mode
Turn on production-mode knip plus an autofix run in pre-commit and the
`pnpm dead-code` script, document the `/** @internal */` convention for
test-only exports in AGENTS.md, annotate test-only exports across the
CLI with that JSDoc, and drop dead exports/wrappers the new gate
surfaced (e.g. `cli-project.ts`, `lookerRuntimeSourceToFileAdapterSource`,
`createLocalScanEnrichmentProvidersFromConfig`,
`PGLITE_OWNER_PROCESS_BACKEND_CAPABILITIES`, stale type re-exports).
Replace the loose `ignoreIssues` allowlist in `knip.json` with explicit
production entries so cross-package barrel leaks are caught.
* refactor(cli): delete internal barrel index.ts files
The 34 `index.ts` re-export barrels inside `packages/cli/src/` were
holdovers from the pre-fold multi-workspace structure. Post-fold-in they
served no production purpose: external consumers go through the single
package main entry, and in-repo callers mostly imported through them
only because the path was short. Internally, knip flagged most barrel
re-exports as production-dead (only reached via tests).
This change:
- Deletes every internal barrel except `packages/cli/src/index.ts`
(the published package entry).
- Rewrites ~270 source/test files to import each name directly from
the file that defines it.
- Moves `tools/warehouse-verification/index.ts` to
`create-warehouse-verification-tools.ts` (the function it defined
locally) and updates its single consumer.
- Renames `search/backend-conformance.ts` → `.test-utils.ts` to match
the existing test-helper file convention.
- Deletes 13 dead test-only chains (dbt-descriptions/*,
live-database/extracted-schema, live-database/structural-sync,
relationship-* feedback/review chain) plus their tests and a
cascading orphan integration test.
- Updates test mocks that pointed at deleted barrel paths
(notion-client, connector barrels in scan/local-scan-connectors
tests) to mock the source files instead.
- Points the maintainer benchmark script
(`scripts/relationship-benchmark-report.mjs`) at source files
instead of `dist/context/scan/index.js`.
- Drops the barrel `!` entries from `knip.json`; adds explicit
production entries only for the benchmark code reached via dist by
the maintainer script.
Net: 413 files changed, ~1.2k insertions, ~9.4k deletions.
`pnpm run dead-code` (Biome + knip default + knip production) and
`pnpm run type-check` are clean; 2277 tests pass.
* refactor(workspace): rename @ktx/cli to @kaelio/ktx and pack it directly
Promote the CLI workspace package to the public name `@kaelio/ktx` and
drop the separate `scripts/build-public-npm-package.mjs` wrapper. The
CLI package is now publishable in place (`publishConfig.access: public`,
`provenance: true`), so artifact packing uses `pnpm pack` against
`packages/cli/` instead of assembling a parallel package tree.
Updates all workspace filter invocations, docs, tests, and release
readiness checks to reference the new package name, and folds the
tarball-name helper into `scripts/public-npm-release-metadata.mjs`.
* docs: align "agent clients" and "data agents" terminology
Replace "client agents" with "agent clients" and "database agents" with
"data agents" across AGENTS.md, README.md, the docs-site copy, and the
matching setup-agents test description, matching the canonical
vocabulary in docs/terminology.md.
Also moves packages/cli/tsconfig.json's tsBuildInfoFile from
node_modules/.cache/ to dist/.tsbuildinfo so incremental builds survive
node_modules reinstalls.
* refactor(release): single source of truth for package version
Make packages/cli/package.json the single source of truth for the
@kaelio/ktx version. publicNpmPackageVersion() now reads it directly,
so artifact filenames, release-readiness checks, and the Python wheel
version all derive from one field. The duplicate
release-policy.json.publicNpmPackageVersion is removed.
Previously the two fields could drift: tarballs were named
kaelio-ktx-0.4.1.tgz while internally containing
@kaelio/ktx@0.0.0-private.
- update-public-release-version.mjs rewrites both Python pyproject.toml
files (ktx-daemon, ktx-sl) alongside the npm package.jsons,
normalizing the version for PEP 440 (e.g. 0.1.0-rc.2 -> 0.1.0rc2).
- semantic-release-config.cjs adds the two pyproject.toml files to
@semantic-release/git assets so the release commit back to main
carries every version source in lockstep.
- The six "?? '0.0.0-private'" fallback literals across the CLI are
replaced with "?? getKtxCliPackageInfo().version", and
createDefaultKtxMcpServer makes its version arg required.
- docs/release.md describes the actual commit-back model: the dev tree
always reflects the most recent release; no sentinel pin to
maintain.
Verified: pnpm run artifacts:build now produces
kaelio-ktx-0.4.1.tgz and kaelio_ktx-0.4.1-py3-none-any.whl with
@kaelio/ktx@0.4.1 inside. Full type-check, dead-code, and
2287 vitests + 173 script tests pass.
* refactor(cli): inject embedding provider resolution and detect sentence-transformers runtime
Make resolveProjectEmbeddingProvider and runtimeIo injectable in ingest and
scan command entrypoints so tests can stub them, and teach
resolvePublicIngestRuntimeRequirements to flag the local-embeddings runtime
feature when ktx.yaml selects sentence-transformers.
* chore(cli): mark buildLocalStatsStatus and LocalStatsStatus as @internal
Both symbols are consumed only by status-project.test.ts. Annotating with
/** @internal */ keeps knip's production-mode check clean without changing
runtime behavior.
* fix(cli): use real package metadata in print-command-tree
The stubbed package name embedded a forbidden product identifier that
tripped the boundary check in CI. Read the metadata from package.json
instead — keeps the rendered tree unchanged and removes a duplicate
source of truth.
* feat(cli): show embedding coverage in `ktx status`, drop duplicate disk counts
Inline `(N embedded)` next to the Wiki scope counts and Semantic-layer
source counts, computed with `SUM(embedding_json IS NOT NULL)` over
`knowledge_pages` and `local_sl_sources`. Rename the "Knowledge" label to
"Wiki" (canonical per `docs/terminology.md`) and rename the matching
`localStats.knowledgePages` field to `localStats.wikiPages`.
Drop `wiki=N md` and `semantic-layer=N yaml` from the Disk row — those
duplicated the per-surface rows above. Disk now reports only actual byte
usage (db, cache, raw-sources). The unused `wikiGlobalMarkdownCount` /
`semanticLayerYamlCount` fields, the `isMarkdownEntry` / `isYamlEntry`
helpers, and the `filter` arg on `summarizeDir` are removed.
2026-05-21 15:28:58 +02:00
/** @internal */
2026-05-11 15:50:34 +02:00
export async function verifyRuntimeAsset ( input : { assetDir : string } ) : Promise < ManagedRuntimeAsset > {
const manifestPath = join ( input . assetDir , 'manifest.json' ) ;
2026-05-12 10:26:05 +02:00
let manifestData : unknown ;
try {
manifestData = await readJsonFile ( manifestPath ) ;
} catch ( error ) {
if ( isErrnoException ( error , 'ENOENT' ) ) {
throw new Error (
[
` Missing bundled Python runtime manifest: ${ manifestPath } ` ,
'In a source checkout, build the local runtime assets with: pnpm run artifacts:build' ,
'Then retry the runtime-backed KTX command.' ,
] . join ( '\n' ) ,
) ;
}
throw error ;
}
const manifest = runtimeAssetManifestSchema . parse ( manifestData ) ;
2026-05-11 15:50:34 +02:00
assertSafeWheelFilename ( manifest . wheel . file ) ;
const wheelPath = join ( input . assetDir , manifest . wheel . file ) ;
const wheel = await readFile ( wheelPath ) ;
const sha256 = createHash ( 'sha256' ) . update ( wheel ) . digest ( 'hex' ) ;
if ( sha256 !== manifest . wheel . sha256 || wheel . byteLength !== manifest . wheel . bytes ) {
throw new Error ( ` Bundled Python runtime wheel checksum mismatch: ${ wheelPath } ` ) ;
}
2026-05-19 18:18:38 +02:00
return { manifest , wheelPath , requiresPython : parseRequiresPythonFromWheel ( { wheelPath , contents : wheel } ) } ;
2026-05-11 15:50:34 +02:00
}
function normalizeFeatures ( features : KtxRuntimeFeature [ ] ) : KtxRuntimeFeature [ ] {
const requested = new Set < KtxRuntimeFeature > ( [ 'core' , . . . features ] ) ;
return runtimeFeatureSchema . options . filter ( ( feature ) = > requested . has ( feature ) ) ;
}
async function readInstalledManifest ( path : string ) : Promise < InstalledKtxRuntimeManifest | undefined > {
if ( ! ( await pathExists ( path ) ) ) {
return undefined ;
}
return installedRuntimeManifestSchema . parse ( await readJsonFile ( path ) ) ;
}
function hasFeatures ( manifest : InstalledKtxRuntimeManifest , features : KtxRuntimeFeature [ ] ) : boolean {
return normalizeFeatures ( features ) . every ( ( feature ) = > manifest . features . includes ( feature ) ) ;
}
async function defaultExec (
command : string ,
args : string [ ] ,
options : { cwd? : string ; env? : NodeJS.ProcessEnv } = { } ,
) : Promise < { stdout : string ; stderr : string } > {
const result = await execFileAsync ( command , args , {
cwd : options.cwd ,
env : options.env ,
encoding : 'utf8' ,
maxBuffer : 1024 * 1024 * 20 ,
} ) ;
return { stdout : result.stdout , stderr : result.stderr } ;
}
function errorOutput ( error : unknown ) : { stdout : string ; stderr : string } {
const value = error as { stdout? : unknown ; stderr? : unknown } ;
return {
stdout : typeof value . stdout === 'string' ? value . stdout : '' ,
stderr : typeof value . stderr === 'string' ? value . stderr : '' ,
} ;
}
2026-05-19 18:18:38 +02:00
function installFailureMessage ( input : { logPath : string ; stdout : string ; stderr : string } ) : string {
const output = [ input . stderr . trim ( ) , input . stdout . trim ( ) ] . filter ( ( part ) = > part . length > 0 ) . join ( '\n' ) ;
if ( ! output ) {
return ` Python runtime install failed. Install log: ${ input . logPath } ` ;
}
return ` Python runtime install failed. \ n ${ output } \ nInstall log: ${ input . logPath } ` ;
}
2026-05-11 15:50:34 +02:00
async function runLogged ( input : {
exec : ManagedPythonRuntimeExec ;
logPath : string ;
command : string ;
args : string [ ] ;
cwd? : string ;
2026-05-12 10:26:05 +02:00
env? : NodeJS.ProcessEnv ;
2026-05-11 15:50:34 +02:00
} ) : Promise < { stdout : string ; stderr : string } > {
await appendFile ( input . logPath , ` $ ${ input . command } ${ input . args . join ( ' ' ) } \ n ` ) ;
try {
2026-05-12 10:26:05 +02:00
const result = await input . exec ( input . command , input . args , { cwd : input.cwd , env : input.env } ) ;
2026-05-11 15:50:34 +02:00
if ( result . stdout ) {
await appendFile ( input . logPath , result . stdout . endsWith ( '\n' ) ? result . stdout : ` ${ result . stdout } \ n ` ) ;
}
if ( result . stderr ) {
await appendFile ( input . logPath , result . stderr . endsWith ( '\n' ) ? result . stderr : ` ${ result . stderr } \ n ` ) ;
}
return result ;
} catch ( error ) {
const output = errorOutput ( error ) ;
if ( output . stdout ) {
await appendFile ( input . logPath , output . stdout . endsWith ( '\n' ) ? output . stdout : ` ${ output . stdout } \ n ` ) ;
}
if ( output . stderr ) {
await appendFile ( input . logPath , output . stderr . endsWith ( '\n' ) ? output . stderr : ` ${ output . stderr } \ n ` ) ;
}
2026-05-19 18:18:38 +02:00
throw new Error ( installFailureMessage ( { logPath : input.logPath , stdout : output.stdout , stderr : output.stderr } ) ) ;
2026-05-11 15:50:34 +02:00
}
}
2026-05-12 10:26:05 +02:00
function managedRuntimeUvEnv ( baseEnv : NodeJS.ProcessEnv ) : NodeJS . ProcessEnv {
return { . . . baseEnv , UV_NO_CONFIG : '1' } ;
}
async function ensureUv ( exec : ManagedPythonRuntimeExec , env? : NodeJS.ProcessEnv ) : Promise < string > {
2026-05-11 15:50:34 +02:00
try {
2026-05-12 10:26:05 +02:00
const result = await exec ( 'uv' , [ '--version' ] , { env } ) ;
2026-05-11 15:50:34 +02:00
return result . stdout . trim ( ) || 'uv available' ;
} catch {
throw new Error ( MISSING_UV_RUNTIME_INSTALL_MESSAGE ) ;
}
}
export async function installManagedPythonRuntime (
options : ManagedPythonRuntimeInstallOptions ,
) : Promise < ManagedPythonRuntimeInstallResult > {
const layout = managedPythonRuntimeLayout ( options ) ;
const exec = options . exec ? ? defaultExec ;
const features = normalizeFeatures ( options . features ) ;
const asset = await verifyRuntimeAsset ( { assetDir : layout.assetDir } ) ;
2026-05-12 10:26:05 +02:00
const uvEnv = managedRuntimeUvEnv ( options . env ? ? process . env ) ;
2026-05-11 15:50:34 +02:00
const existing = await readInstalledManifest ( layout . manifestPath ) ;
if (
options . force !== true &&
existing &&
existing . cliVersion === options . cliVersion &&
existing . asset . wheel . sha256 === asset . manifest . wheel . sha256 &&
hasFeatures ( existing , features ) &&
( await pathExists ( existing . python . executable ) ) &&
( await pathExists ( existing . python . daemonExecutable ) )
) {
return { status : 'ready' , layout , asset , manifest : existing } ;
}
await rm ( layout . versionDir , { recursive : true , force : true } ) ;
await mkdir ( layout . versionDir , { recursive : true } ) ;
await writeFile ( layout . installLogPath , '' ) ;
2026-05-12 10:26:05 +02:00
await ensureUv ( exec , uvEnv ) ;
await runLogged ( {
exec ,
logPath : layout.installLogPath ,
command : 'uv' ,
2026-05-19 18:18:38 +02:00
args : [ 'python' , 'install' , asset . requiresPython . minimumVersion ] ,
env : uvEnv ,
} ) ;
await runLogged ( {
exec ,
logPath : layout.installLogPath ,
command : 'uv' ,
args : [ 'venv' , '--python' , asset . requiresPython . minimumVersion , layout . venvDir ] ,
2026-05-12 10:26:05 +02:00
env : uvEnv ,
} ) ;
2026-05-11 15:50:34 +02:00
const wheelSpec = features . includes ( 'local-embeddings' ) ? ` ${ asset . wheelPath } [local-embeddings] ` : asset . wheelPath ;
await runLogged ( {
exec ,
logPath : layout.installLogPath ,
command : 'uv' ,
args : [ 'pip' , 'install' , '--python' , layout . pythonPath , wheelSpec ] ,
2026-05-12 10:26:05 +02:00
env : uvEnv ,
2026-05-11 15:50:34 +02:00
} ) ;
const manifest : InstalledKtxRuntimeManifest = {
schemaVersion : 1 ,
cliVersion : options.cliVersion ,
installedAt : new Date ( ) . toISOString ( ) ,
asset : asset.manifest ,
features ,
python : {
executable : layout.pythonPath ,
daemonExecutable : layout.daemonPath ,
} ,
installLog : layout.installLogPath ,
} ;
await writeFile ( layout . manifestPath , ` ${ JSON . stringify ( manifest , null , 2 ) } \ n ` ) ;
return { status : 'installed' , layout , asset , manifest } ;
}
export async function readManagedPythonRuntimeStatus (
options : ManagedPythonRuntimeLayoutOptions ,
) : Promise < ManagedPythonRuntimeStatus > {
const layout = managedPythonRuntimeLayout ( options ) ;
let manifest : InstalledKtxRuntimeManifest | undefined ;
try {
manifest = await readInstalledManifest ( layout . manifestPath ) ;
} catch ( error ) {
return {
kind : 'broken' ,
detail : ` Runtime manifest is invalid: ${ error instanceof Error ? error.message : String ( error ) } ` ,
layout ,
} ;
}
if ( ! manifest ) {
return { kind : 'missing' , detail : ` No runtime manifest at ${ layout . manifestPath } ` , layout } ;
}
if ( manifest . cliVersion !== options . cliVersion ) {
return {
kind : 'mismatched' ,
detail : ` Runtime is for CLI ${ manifest . cliVersion } , current CLI is ${ options . cliVersion } ` ,
layout ,
manifest ,
} ;
}
if ( ! ( await pathExists ( manifest . python . executable ) ) ) {
return { kind : 'broken' , detail : ` Missing Python executable: ${ manifest . python . executable } ` , layout , manifest } ;
}
if ( ! ( await pathExists ( manifest . python . daemonExecutable ) ) ) {
return { kind : 'broken' , detail : ` Missing ktx-daemon executable: ${ manifest . python . daemonExecutable } ` , layout , manifest } ;
}
return { kind : 'ready' , detail : ` Runtime ready at ${ layout . versionDir } ` , layout , manifest } ;
}
function check (
status : ManagedPythonRuntimeDoctorCheck [ 'status' ] ,
input : Omit < ManagedPythonRuntimeDoctorCheck , 'status' > ,
) : ManagedPythonRuntimeDoctorCheck {
return { status , . . . input } ;
}
export async function doctorManagedPythonRuntime (
options : ManagedPythonRuntimeLayoutOptions & { exec? : ManagedPythonRuntimeExec } ,
) : Promise < ManagedPythonRuntimeDoctorCheck [ ] > {
const exec = options . exec ? ? defaultExec ;
const checks : ManagedPythonRuntimeDoctorCheck [ ] = [ ] ;
try {
2026-05-12 10:26:05 +02:00
const version = await ensureUv ( exec , managedRuntimeUvEnv ( options . env ? ? process . env ) ) ;
2026-05-11 15:50:34 +02:00
checks . push ( check ( 'pass' , { id : 'uv' , label : 'uv' , detail : version } ) ) ;
} catch ( error ) {
checks . push (
check ( 'fail' , {
id : 'uv' ,
label : 'uv' ,
detail : error instanceof Error ? error.message : String ( error ) ,
2026-05-20 01:36:54 +02:00
fix : 'Install uv, make sure it is on PATH, and run: ktx admin runtime install --yes' ,
2026-05-11 15:50:34 +02:00
} ) ,
) ;
}
try {
const asset = await verifyRuntimeAsset ( { assetDir : managedPythonRuntimeLayout ( options ) . assetDir } ) ;
checks . push ( check ( 'pass' , { id : 'asset' , label : 'Bundled Python wheel' , detail : asset.wheelPath } ) ) ;
} catch ( error ) {
checks . push (
check ( 'fail' , {
id : 'asset' ,
label : 'Bundled Python wheel' ,
detail : error instanceof Error ? error.message : String ( error ) ,
fix : 'Run: pnpm run artifacts:check' ,
} ) ,
) ;
}
const status = await readManagedPythonRuntimeStatus ( options ) ;
checks . push (
check ( status . kind === 'ready' ? 'pass' : 'fail' , {
id : 'runtime' ,
label : 'Managed Python runtime' ,
detail : status.detail ,
2026-05-20 01:36:54 +02:00
. . . ( status . kind === 'ready' ? { } : { fix : 'Run: ktx admin runtime install --yes' } ) ,
2026-05-11 15:50:34 +02:00
} ) ,
) ;
return checks ;
}