2026-05-10 23:12:26 +02:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
import { execFile } from 'node:child_process';
|
|
|
|
|
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
|
|
|
import { tmpdir } from 'node:os';
|
2026-05-11 15:50:34 +02:00
|
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
import {
|
|
|
|
|
RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
|
|
|
|
RUNTIME_WHEEL_NORMALIZED_NAME,
|
|
|
|
|
RUNTIME_WHEEL_PACKAGE_VERSION,
|
|
|
|
|
} from './build-python-runtime-wheel.mjs';
|
|
|
|
|
import {
|
|
|
|
|
PUBLIC_NPM_PACKAGE_NAME,
|
|
|
|
|
publicNpmPackageTarballName,
|
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
|
|
|
publicNpmPackageVersion,
|
|
|
|
|
} from './public-npm-release-metadata.mjs';
|
2026-05-11 15:50:34 +02:00
|
|
|
|
|
|
|
|
export {
|
|
|
|
|
RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
|
|
|
|
RUNTIME_WHEEL_NORMALIZED_NAME,
|
|
|
|
|
RUNTIME_WHEEL_PACKAGE_VERSION,
|
|
|
|
|
};
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
export const NPM_ARTIFACT_PACKAGES = [{ name: PUBLIC_NPM_PACKAGE_NAME, packageRoot: 'packages/cli' }];
|
|
|
|
|
|
|
|
|
|
export const CLI_PYTHON_ASSET_MANIFEST = 'manifest.json';
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
function pnpmCommand(args) {
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
return {
|
|
|
|
|
command: 'cmd.exe',
|
|
|
|
|
args: ['/d', '/s', '/c', 'pnpm', ...args],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
command: 'pnpm',
|
|
|
|
|
args,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
function scriptRootDir() {
|
|
|
|
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 12:06:34 +02:00
|
|
|
function npmPackageTarballName(packageName, version) {
|
2026-05-11 15:50:34 +02:00
|
|
|
if (packageName !== PUBLIC_NPM_PACKAGE_NAME) {
|
|
|
|
|
throw new Error(`Unsupported npm artifact package: ${packageName}`);
|
|
|
|
|
}
|
2026-05-16 12:06:34 +02:00
|
|
|
return publicNpmPackageTarballName(version);
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 12:06:34 +02:00
|
|
|
function npmPackageTarballs(npmDir, version) {
|
2026-05-10 23:12:26 +02:00
|
|
|
return Object.fromEntries(
|
2026-05-16 12:06:34 +02:00
|
|
|
NPM_ARTIFACT_PACKAGES.map((packageInfo) => [
|
|
|
|
|
packageInfo.name,
|
|
|
|
|
join(npmDir, npmPackageTarballName(packageInfo.name, version)),
|
|
|
|
|
]),
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 12:06:34 +02:00
|
|
|
export function packageArtifactLayout(rootDir = scriptRootDir(), version = publicNpmPackageVersion(rootDir)) {
|
2026-05-10 23:12:26 +02:00
|
|
|
const artifactDir = join(rootDir, 'dist', 'artifacts');
|
|
|
|
|
const npmDir = join(artifactDir, 'npm');
|
|
|
|
|
const pythonDir = join(artifactDir, 'python');
|
2026-05-16 12:06:34 +02:00
|
|
|
const npmTarballs = npmPackageTarballs(npmDir, version);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rootDir,
|
|
|
|
|
artifactDir,
|
|
|
|
|
npmDir,
|
|
|
|
|
pythonDir,
|
|
|
|
|
npmTarballs,
|
2026-05-11 15:50:34 +02:00
|
|
|
contextTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME],
|
|
|
|
|
cliTarball: npmTarballs[PUBLIC_NPM_PACKAGE_NAME],
|
|
|
|
|
connectorTarballs: {},
|
2026-05-10 23:12:26 +02:00
|
|
|
manifestPath: join(artifactDir, 'manifest.json'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildArtifactCommands(layout) {
|
|
|
|
|
return [
|
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
|
|
|
{
|
2026-05-26 03:16:53 -07:00
|
|
|
...pnpmCommand(['--filter', PUBLIC_NPM_PACKAGE_NAME, 'run', 'build']),
|
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
|
|
|
cwd: layout.rootDir,
|
|
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
{
|
2026-05-11 15:50:34 +02:00
|
|
|
command: process.execPath,
|
|
|
|
|
args: ['scripts/build-python-runtime-wheel.mjs'],
|
2026-05-10 23:12:26 +02:00
|
|
|
cwd: layout.rootDir,
|
|
|
|
|
},
|
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
|
|
|
{
|
2026-05-26 03:16:53 -07:00
|
|
|
...pnpmCommand(['pack', '--out', layout.cliTarball]),
|
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
|
|
|
cwd: join(layout.rootDir, 'packages', 'cli'),
|
|
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pathExists(path) {
|
|
|
|
|
try {
|
|
|
|
|
await access(path);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function assertPathExists(path, label) {
|
|
|
|
|
if (!(await pathExists(path))) {
|
|
|
|
|
throw new Error(`Missing ${label}: ${path}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePythonDistributionName(name) {
|
|
|
|
|
return name.replaceAll('-', '_');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
function findOne(files, distributionName, suffix, label, pythonDir, version) {
|
2026-05-10 23:12:26 +02:00
|
|
|
const normalized = normalizePythonDistributionName(distributionName);
|
2026-05-11 15:50:34 +02:00
|
|
|
const found = files.find((file) => file.startsWith(`${normalized}-${version}`) && file.endsWith(suffix));
|
2026-05-10 23:12:26 +02:00
|
|
|
if (!found) {
|
|
|
|
|
throw new Error(`Missing Python artifact: ${label}`);
|
|
|
|
|
}
|
|
|
|
|
return join(pythonDir, found);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function findPythonArtifacts(pythonDir) {
|
|
|
|
|
const files = await readdir(pythonDir);
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-11 15:50:34 +02:00
|
|
|
runtimeWheel: findOne(
|
|
|
|
|
files,
|
|
|
|
|
RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
|
|
|
|
'.whl',
|
2026-05-20 01:36:54 +02:00
|
|
|
'kaelio-ktx runtime wheel',
|
2026-05-11 15:50:34 +02:00
|
|
|
pythonDir,
|
|
|
|
|
RUNTIME_WHEEL_PACKAGE_VERSION,
|
|
|
|
|
),
|
2026-05-10 23:12:26 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function artifactManifestPath(layout) {
|
|
|
|
|
return layout.manifestPath ?? join(layout.artifactDir, 'manifest.json');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readJson(path) {
|
|
|
|
|
return JSON.parse(await readFile(path, 'utf-8'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function releaseMetadataEntry({ ecosystem, packageName, packageRoot, packageVersion, privatePackage }) {
|
|
|
|
|
return {
|
|
|
|
|
ecosystem,
|
|
|
|
|
packageName,
|
|
|
|
|
packageRoot,
|
|
|
|
|
packageVersion,
|
|
|
|
|
private: privatePackage,
|
|
|
|
|
releaseMode: 'ci-artifact-only',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 12:06:34 +02:00
|
|
|
async function readNpmPackageMetadata(rootDir, packageInfo, version) {
|
2026-05-10 23:12:26 +02:00
|
|
|
const packageJson = await readJson(join(rootDir, packageInfo.packageRoot, 'package.json'));
|
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
|
|
|
if (packageJson.name !== packageInfo.name) {
|
2026-05-10 23:12:26 +02:00
|
|
|
throw new Error(
|
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
|
|
|
`Unexpected package name in ${packageInfo.packageRoot}/package.json: expected ${packageInfo.name}, got ${packageJson.name}`,
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return releaseMetadataEntry({
|
|
|
|
|
ecosystem: 'npm',
|
2026-05-11 15:50:34 +02:00
|
|
|
packageName: packageInfo.name,
|
2026-05-10 23:12:26 +02:00
|
|
|
packageRoot: packageInfo.packageRoot,
|
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
|
|
|
packageVersion: version,
|
|
|
|
|
privatePackage: false,
|
2026-05-10 23:12:26 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 12:06:34 +02:00
|
|
|
export async function packageReleaseMetadata(rootDir = scriptRootDir(), version = publicNpmPackageVersion(rootDir)) {
|
2026-05-10 23:12:26 +02:00
|
|
|
const npmPackages = await Promise.all(
|
2026-05-16 12:06:34 +02:00
|
|
|
NPM_ARTIFACT_PACKAGES.map((packageInfo) => readNpmPackageMetadata(rootDir, packageInfo, version)),
|
2026-05-10 23:12:26 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...npmPackages,
|
|
|
|
|
releaseMetadataEntry({
|
|
|
|
|
ecosystem: 'python',
|
2026-05-11 15:50:34 +02:00
|
|
|
packageName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
|
|
|
|
packageRoot: 'python/runtime-wheel',
|
|
|
|
|
packageVersion: RUNTIME_WHEEL_PACKAGE_VERSION,
|
2026-05-10 23:12:26 +02:00
|
|
|
privatePackage: false,
|
|
|
|
|
}),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function packageMetadataByName(packages) {
|
|
|
|
|
return new Map(packages.map((metadata) => [metadata.packageName, metadata]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requirePackageMetadata(packagesByName, packageName) {
|
|
|
|
|
const metadata = packagesByName.get(packageName);
|
|
|
|
|
if (!metadata) {
|
|
|
|
|
throw new Error(`Missing package release metadata for ${packageName}`);
|
|
|
|
|
}
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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',
|
2026-05-11 15:50:34 +02:00
|
|
|
artifactPath: pythonArtifacts.runtimeWheel,
|
|
|
|
|
metadata: requirePackageMetadata(packagesByName, RUNTIME_WHEEL_DISTRIBUTION_NAME),
|
2026-05-10 23:12:26 +02:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function artifactRelativePath(layout, artifactPath) {
|
|
|
|
|
return relative(layout.artifactDir, artifactPath).split(sep).join('/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatJson(value) {
|
|
|
|
|
return JSON.stringify(value, null, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertJsonEqual(actual, expected, label) {
|
|
|
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
|
|
|
throw new Error(`${label} do not match\nExpected:\n${formatJson(expected)}\nActual:\n${formatJson(actual)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPlainObject(value) {
|
|
|
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertString(value, label) {
|
|
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
throw new Error(`${label} must be a string`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function artifactPathFromManifest(layout, manifestPath) {
|
|
|
|
|
assertString(manifestPath, 'Artifact manifest file path');
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
manifestPath.length === 0 ||
|
|
|
|
|
manifestPath.startsWith('/') ||
|
|
|
|
|
manifestPath.includes('\\') ||
|
|
|
|
|
manifestPath.split('/').some((part) => part.length === 0 || part === '..')
|
|
|
|
|
) {
|
|
|
|
|
throw new Error(`Unsafe artifact manifest path: ${manifestPath}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedPath = resolve(layout.artifactDir, manifestPath);
|
|
|
|
|
const relativePath = relative(layout.artifactDir, resolvedPath);
|
|
|
|
|
if (relativePath.startsWith('..') || isAbsolute(relativePath)) {
|
|
|
|
|
throw new Error(`Unsafe artifact manifest path: ${manifestPath}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resolvedPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortedManifestFiles(files) {
|
|
|
|
|
return [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertManifestShape(manifest) {
|
|
|
|
|
if (!isPlainObject(manifest)) {
|
|
|
|
|
throw new Error('Artifact manifest must be a JSON object');
|
|
|
|
|
}
|
|
|
|
|
if (manifest.schemaVersion !== 2) {
|
|
|
|
|
throw new Error(`Unsupported artifact manifest schemaVersion: ${manifest.schemaVersion}`);
|
|
|
|
|
}
|
|
|
|
|
assertString(manifest.generatedAt, 'Artifact manifest generatedAt');
|
|
|
|
|
if (Number.isNaN(Date.parse(manifest.generatedAt))) {
|
|
|
|
|
throw new Error(`Artifact manifest generatedAt is not an ISO timestamp: ${manifest.generatedAt}`);
|
|
|
|
|
}
|
|
|
|
|
if (manifest.sourceRevision !== null && typeof manifest.sourceRevision !== 'string') {
|
|
|
|
|
throw new Error('Artifact manifest sourceRevision must be a string or null');
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(manifest.packages)) {
|
|
|
|
|
throw new Error('Artifact manifest packages must be an array');
|
|
|
|
|
}
|
|
|
|
|
if (!Array.isArray(manifest.files)) {
|
|
|
|
|
throw new Error('Artifact manifest files must be an array');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function artifactManifestEntry(layout, record) {
|
|
|
|
|
const contents = await readFile(record.artifactPath);
|
|
|
|
|
return {
|
|
|
|
|
path: artifactRelativePath(layout, record.artifactPath),
|
|
|
|
|
ecosystem: record.metadata.ecosystem,
|
|
|
|
|
artifactKind: record.artifactKind,
|
|
|
|
|
packageName: record.metadata.packageName,
|
|
|
|
|
packageVersion: record.metadata.packageVersion,
|
|
|
|
|
bytes: contents.byteLength,
|
|
|
|
|
sha256: createHash('sha256').update(contents).digest('hex'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function buildArtifactManifest(layout, generatedAt = new Date(), options = {}) {
|
|
|
|
|
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
|
|
|
|
const packages = await packageReleaseMetadata(layout.rootDir);
|
|
|
|
|
const artifactRecords = artifactPackageRecords(layout, pythonArtifacts, packages);
|
|
|
|
|
const files = await Promise.all(artifactRecords.map((record) => artifactManifestEntry(layout, record)));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
generatedAt: generatedAt.toISOString(),
|
|
|
|
|
sourceRevision: options.sourceRevision ?? process.env.GITHUB_SHA ?? null,
|
|
|
|
|
packages,
|
|
|
|
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function writeArtifactManifest(layout, generatedAt = new Date(), options = {}) {
|
|
|
|
|
const manifest = await buildArtifactManifest(layout, generatedAt, options);
|
|
|
|
|
await writeFile(artifactManifestPath(layout), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
|
return manifest;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function verifyArtifactManifest(layout, options = {}) {
|
|
|
|
|
const manifest = await readJson(artifactManifestPath(layout));
|
|
|
|
|
assertManifestShape(manifest);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const expectedSourceRevision = options.expectedSourceRevision ?? process.env.KTX_EXPECTED_SOURCE_REVISION;
|
2026-05-10 23:12:26 +02:00
|
|
|
if (expectedSourceRevision !== undefined && manifest.sourceRevision !== expectedSourceRevision) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Artifact manifest sourceRevision mismatch: expected ${expectedSourceRevision}, got ${manifest.sourceRevision}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expectedPackages = await packageReleaseMetadata(layout.rootDir);
|
|
|
|
|
assertJsonEqual(manifest.packages, expectedPackages, 'Artifact manifest packages');
|
|
|
|
|
|
|
|
|
|
for (const file of manifest.files) {
|
|
|
|
|
if (!isPlainObject(file)) {
|
|
|
|
|
throw new Error('Artifact manifest file entries must be JSON objects');
|
|
|
|
|
}
|
|
|
|
|
artifactPathFromManifest(layout, file.path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
|
|
|
|
const expectedFiles = await Promise.all(
|
|
|
|
|
artifactPackageRecords(layout, pythonArtifacts, expectedPackages).map((record) => artifactManifestEntry(layout, record)),
|
|
|
|
|
);
|
|
|
|
|
assertJsonEqual(
|
|
|
|
|
sortedManifestFiles(manifest.files),
|
|
|
|
|
sortedManifestFiles(expectedFiles),
|
|
|
|
|
'Artifact manifest files do not match artifact contents',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return manifest;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
function runtimeWheelAssetName(runtimeWheelPath) {
|
|
|
|
|
return runtimeWheelPath.split(sep).at(-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function copyRuntimeWheelAssets(layout, pythonArtifacts) {
|
|
|
|
|
const assetDir = join(layout.rootDir, 'packages', 'cli', 'assets', 'python');
|
|
|
|
|
const wheelFile = runtimeWheelAssetName(pythonArtifacts.runtimeWheel);
|
|
|
|
|
if (!wheelFile) {
|
|
|
|
|
throw new Error(`Unable to determine runtime wheel filename: ${pythonArtifacts.runtimeWheel}`);
|
|
|
|
|
}
|
|
|
|
|
const wheelContents = await readFile(pythonArtifacts.runtimeWheel);
|
|
|
|
|
await rm(assetDir, { recursive: true, force: true });
|
|
|
|
|
await mkdir(assetDir, { recursive: true });
|
|
|
|
|
const wheelPath = join(assetDir, wheelFile);
|
|
|
|
|
const manifestPath = join(assetDir, CLI_PYTHON_ASSET_MANIFEST);
|
|
|
|
|
await writeFile(wheelPath, wheelContents);
|
|
|
|
|
await writeFile(
|
|
|
|
|
manifestPath,
|
|
|
|
|
`${JSON.stringify(
|
|
|
|
|
{
|
|
|
|
|
schemaVersion: 1,
|
|
|
|
|
distributionName: RUNTIME_WHEEL_DISTRIBUTION_NAME,
|
|
|
|
|
normalizedName: RUNTIME_WHEEL_NORMALIZED_NAME,
|
|
|
|
|
version: RUNTIME_WHEEL_PACKAGE_VERSION,
|
|
|
|
|
wheel: {
|
|
|
|
|
file: wheelFile,
|
|
|
|
|
sha256: createHash('sha256').update(wheelContents).digest('hex'),
|
|
|
|
|
bytes: wheelContents.byteLength,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
)}\n`,
|
|
|
|
|
);
|
|
|
|
|
return { assetDir, wheelPath, manifestPath };
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runCommand(command, args, options = {}) {
|
|
|
|
|
const cwd = options.cwd ?? process.cwd();
|
|
|
|
|
process.stdout.write(`$ ${command} ${args.join(' ')}\n`);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolvePromise, reject) => {
|
|
|
|
|
const child = execFile(
|
|
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
{
|
|
|
|
|
cwd,
|
|
|
|
|
env: { ...process.env, ...options.env },
|
|
|
|
|
maxBuffer: 1024 * 1024 * 20,
|
|
|
|
|
},
|
|
|
|
|
(error, stdout, stderr) => {
|
|
|
|
|
if (stdout) {
|
|
|
|
|
process.stdout.write(stdout);
|
|
|
|
|
}
|
|
|
|
|
if (stderr) {
|
|
|
|
|
process.stderr.write(stderr);
|
|
|
|
|
}
|
|
|
|
|
if (error) {
|
|
|
|
|
reject(error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resolvePromise({ stdout, stderr });
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (options.input !== undefined) {
|
|
|
|
|
child.stdin?.end(options.input);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function npmSmokePackageJson(layout) {
|
|
|
|
|
return {
|
2026-05-10 23:51:24 +02:00
|
|
|
name: 'ktx-artifact-npm-smoke',
|
2026-05-10 23:12:26 +02:00
|
|
|
version: '0.0.0',
|
|
|
|
|
private: true,
|
|
|
|
|
type: 'module',
|
|
|
|
|
dependencies: {
|
2026-05-11 15:50:34 +02:00
|
|
|
'@kaelio/ktx': `file:${layout.cliTarball}`,
|
|
|
|
|
},
|
2026-05-10 23:12:26 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 01:15:35 +02:00
|
|
|
export function npmSmokePnpmWorkspaceYaml() {
|
|
|
|
|
return ['packages:', ' - "."', 'allowBuilds:', ' better-sqlite3: true', ''].join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
export function npmVerifySource() {
|
|
|
|
|
return `
|
2026-05-11 15:50:34 +02:00
|
|
|
const cli = await import('@kaelio/ktx');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
if (cli.getKtxCliPackageInfo().name !== '@kaelio/ktx') {
|
|
|
|
|
throw new Error('Unexpected @kaelio/ktx package info');
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
2026-05-11 15:50:34 +02:00
|
|
|
if (typeof cli.runKtxCli !== 'function') {
|
|
|
|
|
throw new Error('Missing runKtxCli export');
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function npmRuntimeSmokeSource() {
|
|
|
|
|
return `
|
|
|
|
|
import assert from 'node:assert/strict';
|
2026-05-11 15:50:34 +02:00
|
|
|
import { execFile } from 'node:child_process';
|
2026-05-19 15:52:30 +02:00
|
|
|
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
|
|
import { createRequire } from 'node:module';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { tmpdir } from 'node:os';
|
2026-05-11 15:50:34 +02:00
|
|
|
import { join } from 'node:path';
|
2026-05-13 13:33:28 +02:00
|
|
|
import { DatabaseSync } from 'node:sqlite';
|
2026-05-26 03:16:53 -07:00
|
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { promisify } from 'node:util';
|
|
|
|
|
|
|
|
|
|
const execFileAsync = promisify(execFile);
|
2026-05-19 15:52:30 +02:00
|
|
|
const require = createRequire(import.meta.url);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
function pnpmCommand(args) {
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'pnpm', ...args] };
|
|
|
|
|
}
|
|
|
|
|
return { command: 'pnpm', args };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
async function run(command, args, options = {}) {
|
|
|
|
|
process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n');
|
|
|
|
|
try {
|
|
|
|
|
const result = await execFileAsync(command, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
encoding: 'utf8',
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
});
|
|
|
|
|
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
code: typeof error.code === 'number' ? error.code : 1,
|
|
|
|
|
stdout: error.stdout ?? '',
|
|
|
|
|
stderr: error.stderr ?? error.message,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireSuccess(label, result) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
result.code,
|
|
|
|
|
0,
|
|
|
|
|
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(result.stderr, '', label + ' wrote unexpected stderr');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 01:43:06 +02:00
|
|
|
function requireSuccessWithProjectStderr(label, result, projectDir) {
|
2026-05-12 15:31:41 +02:00
|
|
|
assert.equal(
|
|
|
|
|
result.code,
|
|
|
|
|
0,
|
|
|
|
|
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 01:43:06 +02:00
|
|
|
function requireExitCodeWithProjectStderr(label, result, projectDir, expectedCode) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
result.code,
|
|
|
|
|
expectedCode,
|
|
|
|
|
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
|
|
|
|
);
|
|
|
|
|
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
function requireOutput(label, result, text) {
|
|
|
|
|
assert.match(result.stdout, text, label + ' output did not match ' + text);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:52:30 +02:00
|
|
|
function escapeRegExp(value) {
|
|
|
|
|
return value.replace(/[|\\\\{}()[\\]^$+*?.]/g, '\\\\$&');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installedPackageVersionPattern() {
|
|
|
|
|
const packageJson = JSON.parse(await readFile(require.resolve('@kaelio/ktx/package.json'), 'utf8'));
|
|
|
|
|
return new RegExp('^' + escapeRegExp(packageJson.name) + ' ' + escapeRegExp(packageJson.version) + '$', 'm');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
function parseJsonResult(label, result) {
|
|
|
|
|
requireSuccess(label, result);
|
|
|
|
|
return JSON.parse(result.stdout);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 12:28:24 +02:00
|
|
|
function parseJsonResultWithExitCode(label, result, expectedCode) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
result.code,
|
|
|
|
|
expectedCode,
|
|
|
|
|
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
|
|
|
|
);
|
|
|
|
|
return JSON.parse(result.stdout);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
function requireIncludes(values, expected, label) {
|
|
|
|
|
assert.ok(Array.isArray(values), label + ' must be an array');
|
|
|
|
|
assert.ok(values.includes(expected), label + ' did not include ' + expected + ': ' + values.join(', '));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
async function rmWithRetry(path) {
|
|
|
|
|
for (let attempt = 0; ; attempt += 1) {
|
|
|
|
|
try {
|
|
|
|
|
await rm(path, { recursive: true, force: true });
|
|
|
|
|
return;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const code = typeof error?.code === 'string' ? error.code : '';
|
|
|
|
|
if (attempt >= 4 || !['EBUSY', 'ENOTEMPTY', 'EPERM'].includes(code)) {
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
await delay(500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
async function writeSqliteWarehouse(projectDir) {
|
2026-05-13 13:33:28 +02:00
|
|
|
const database = new DatabaseSync(join(projectDir, 'warehouse.db'));
|
2026-05-11 15:50:34 +02:00
|
|
|
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();
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const root = await mkdtemp(join(tmpdir(), 'ktx-installed-cli-smoke-'));
|
2026-05-11 15:50:34 +02:00
|
|
|
const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
|
|
|
|
|
process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime');
|
|
|
|
|
let daemonStarted = false;
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
|
|
|
|
const projectDir = join(root, 'project');
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const version = await run(...Object.values(pnpmCommand(['exec', 'ktx', '--version'])));
|
2026-05-11 15:50:34 +02:00
|
|
|
requireSuccess('ktx public package version', version);
|
2026-05-19 15:52:30 +02:00
|
|
|
requireOutput('ktx public package version', version, await installedPackageVersionPattern());
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-13 12:28:24 +02:00
|
|
|
const runtimeStatusBefore = parseJsonResultWithExitCode(
|
2026-05-20 01:36:54 +02:00
|
|
|
'ktx admin runtime status missing',
|
2026-05-26 03:16:53 -07:00
|
|
|
await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status', '--json']))),
|
2026-05-13 12:28:24 +02:00
|
|
|
1,
|
2026-05-11 15:50:34 +02:00
|
|
|
);
|
|
|
|
|
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');
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const init = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'setup',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
projectDir,
|
|
|
|
|
'--no-input',
|
|
|
|
|
'--yes',
|
|
|
|
|
'--skip-llm',
|
|
|
|
|
'--skip-embeddings',
|
|
|
|
|
'--skip-databases',
|
|
|
|
|
'--skip-sources',
|
|
|
|
|
'--skip-agents',
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-13 09:16:35 -07:00
|
|
|
requireSuccess('ktx setup', init);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const emptyProjectDir = join(root, 'empty-project');
|
2026-05-26 03:16:53 -07:00
|
|
|
const emptyInit = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'setup',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
emptyProjectDir,
|
|
|
|
|
'--no-input',
|
|
|
|
|
'--yes',
|
|
|
|
|
'--skip-llm',
|
|
|
|
|
'--skip-embeddings',
|
|
|
|
|
'--skip-databases',
|
|
|
|
|
'--skip-sources',
|
|
|
|
|
'--skip-agents',
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-13 09:16:35 -07:00
|
|
|
requireSuccess('ktx setup empty project', emptyInit);
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(
|
2026-05-10 23:51:24 +02:00
|
|
|
join(projectDir, 'ktx.yaml'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'connections:',
|
|
|
|
|
' warehouse:',
|
|
|
|
|
' driver: sqlite',
|
|
|
|
|
' path: warehouse.db',
|
|
|
|
|
'storage:',
|
|
|
|
|
' state: sqlite',
|
|
|
|
|
' search: sqlite-fts5',
|
|
|
|
|
'scan:',
|
|
|
|
|
' enrichment:',
|
|
|
|
|
' mode: deterministic',
|
|
|
|
|
'',
|
|
|
|
|
].join('\\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
await writeSqliteWarehouse(projectDir);
|
|
|
|
|
|
2026-05-13 16:05:58 +02:00
|
|
|
await mkdir(join(projectDir, 'wiki', 'global'), { recursive: true });
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(
|
2026-05-13 16:05:58 +02:00
|
|
|
join(projectDir, 'wiki', 'global', 'revenue.md'),
|
2026-05-10 23:12:26 +02:00
|
|
|
[
|
|
|
|
|
'---',
|
|
|
|
|
'summary: Paid order value',
|
|
|
|
|
'tags:',
|
|
|
|
|
' - finance',
|
|
|
|
|
'refs: []',
|
|
|
|
|
'sl_refs: []',
|
|
|
|
|
'usage_mode: auto',
|
|
|
|
|
'---',
|
|
|
|
|
'',
|
|
|
|
|
'Revenue is the sum of paid order amounts.',
|
|
|
|
|
'',
|
|
|
|
|
].join('\\n'),
|
|
|
|
|
'utf-8',
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const wikiSearch = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'wiki',
|
|
|
|
|
'revenue',
|
|
|
|
|
'--json',
|
|
|
|
|
'--limit',
|
|
|
|
|
'5',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
projectDir,
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-13 13:01:56 +02:00
|
|
|
const wikiSearchJson = parseJsonResult('ktx wiki search', wikiSearch);
|
|
|
|
|
assert.equal(wikiSearchJson.kind, 'list');
|
|
|
|
|
assert.equal(wikiSearchJson.data.items.length, 1);
|
|
|
|
|
assert.equal(wikiSearchJson.data.items[0].key, 'revenue');
|
2026-05-13 16:05:58 +02:00
|
|
|
assert.equal(wikiSearchJson.data.items[0].path, 'wiki/global/revenue.md');
|
2026-05-13 13:01:56 +02:00
|
|
|
assert.equal(typeof wikiSearchJson.data.items[0].score, 'number');
|
|
|
|
|
requireIncludes(wikiSearchJson.data.items[0].matchReasons, 'lexical', 'wiki search match reasons');
|
|
|
|
|
process.stdout.write('ktx wiki search hybrid metadata verified\\n');
|
2026-05-10 23:51:24 +02:00
|
|
|
await access(join(projectDir, '.ktx', 'db.sqlite'));
|
2026-05-13 16:05:58 +02:00
|
|
|
process.stdout.write('SQLite wiki index: ' + join(projectDir, '.ktx', 'db.sqlite') + '\\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
const slYaml = [
|
|
|
|
|
'name: orders',
|
|
|
|
|
'table: orders',
|
|
|
|
|
'grain:',
|
|
|
|
|
' - id',
|
|
|
|
|
'columns:',
|
|
|
|
|
' - name: id',
|
|
|
|
|
' type: number',
|
|
|
|
|
' - name: amount',
|
|
|
|
|
' type: number',
|
|
|
|
|
'measures:',
|
|
|
|
|
' - name: order_count',
|
|
|
|
|
' expr: count(*)',
|
|
|
|
|
'joins: []',
|
|
|
|
|
'',
|
|
|
|
|
].join('\\n');
|
|
|
|
|
|
|
|
|
|
await mkdir(join(projectDir, 'semantic-layer', 'warehouse'), { recursive: true });
|
|
|
|
|
await writeFile(join(projectDir, 'semantic-layer', 'warehouse', 'orders.yaml'), slYaml, 'utf-8');
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const slSearch = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'sl',
|
|
|
|
|
'orders',
|
|
|
|
|
'--json',
|
|
|
|
|
'--connection-id',
|
|
|
|
|
'warehouse',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
projectDir,
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-13 15:41:10 +02:00
|
|
|
const slSearchJson = parseJsonResult('ktx sl search', slSearch);
|
2026-05-13 13:01:56 +02:00
|
|
|
assert.equal(slSearchJson.kind, 'list');
|
|
|
|
|
assert.equal(slSearchJson.data.items.length, 1);
|
|
|
|
|
assert.equal(slSearchJson.data.items[0].connectionId, 'warehouse');
|
|
|
|
|
assert.equal(slSearchJson.data.items[0].name, 'orders');
|
|
|
|
|
assert.equal(typeof slSearchJson.data.items[0].score, 'number');
|
|
|
|
|
requireIncludes(slSearchJson.data.items[0].matchReasons, 'lexical', 'sl search match reasons');
|
2026-05-13 15:41:10 +02:00
|
|
|
process.stdout.write('ktx sl search hybrid metadata verified\\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const slQuery = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'sl',
|
|
|
|
|
'query',
|
|
|
|
|
'--connection-id',
|
|
|
|
|
'warehouse',
|
|
|
|
|
'--measure',
|
|
|
|
|
'orders.order_count',
|
|
|
|
|
'--format',
|
|
|
|
|
'json',
|
|
|
|
|
'--yes',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
projectDir,
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-11 15:50:34 +02:00
|
|
|
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/);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-11 15:50:34 +02:00
|
|
|
const runtimeStatusAfter = parseJsonResult(
|
2026-05-20 01:36:54 +02:00
|
|
|
'ktx admin runtime status ready',
|
2026-05-26 03:16:53 -07:00
|
|
|
await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status', '--json']))),
|
2026-05-11 15:50:34 +02:00
|
|
|
);
|
|
|
|
|
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');
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const sqliteSlQuery = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand([
|
|
|
|
|
'exec',
|
|
|
|
|
'ktx',
|
|
|
|
|
'sl',
|
|
|
|
|
'query',
|
|
|
|
|
'--connection-id',
|
|
|
|
|
'warehouse',
|
|
|
|
|
'--measure',
|
|
|
|
|
'orders.order_count',
|
|
|
|
|
'--format',
|
|
|
|
|
'json',
|
|
|
|
|
'--execute',
|
|
|
|
|
'--max-rows',
|
|
|
|
|
'100',
|
|
|
|
|
'--yes',
|
|
|
|
|
'--project-dir',
|
|
|
|
|
projectDir,
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-11 15:50:34 +02:00
|
|
|
requireSuccess('ktx sl query sqlite execute', sqliteSlQuery);
|
|
|
|
|
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"dialect": "sqlite"/);
|
|
|
|
|
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"mode": "executed"/);
|
|
|
|
|
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"driver": "sqlite"/);
|
|
|
|
|
requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/);
|
|
|
|
|
process.stdout.write('ktx sl query sqlite execute verified\\n');
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const runtimeDoctor = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status'])));
|
2026-05-20 01:36:54 +02:00
|
|
|
requireSuccess('ktx admin runtime status', runtimeDoctor);
|
|
|
|
|
requireOutput('ktx admin runtime status', runtimeDoctor, /KTX Python runtime/);
|
|
|
|
|
requireOutput('ktx admin runtime status', runtimeDoctor, /status: ready/);
|
|
|
|
|
process.stdout.write('ktx admin runtime status verified\\n');
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const runtimeStart = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'start'])));
|
2026-05-20 01:36:54 +02:00
|
|
|
requireSuccess('ktx admin runtime start', runtimeStart);
|
2026-05-11 15:50:34 +02:00
|
|
|
daemonStarted = true;
|
refactor(release): drop release-policy.json runtime dep and next branch (#180)
* chore: standardize daemon naming on "KTX daemon"
Replace inconsistent names ("KTX Python daemon", "KTX local embeddings
daemon", "KTX managed daemon", "Python daemon") with the single name
"KTX daemon" in CLI output, errors, command descriptions, test
assertions, smoke scripts, docs, AGENTS.md, issue templates, and
codecov flags. The daemon is a portable compute server with endpoints
for SQL analysis, semantic layer, LookML, database introspection, and
embeddings; the previous labels misrepresented it as embeddings-only or
exposed implementation details ("Python", "managed").
The "KTX Python runtime" concept (installed interpreter + packages) is
deliberately left as-is — it is a separate concept from the daemon
process.
* refactor(release): drop release-policy.json runtime dep and next branch
Strips the release-policy.json fallback from release-version.ts so the CLI
reads its version straight from packages/cli/package.json. dev → 0.0.0-private,
installed @kaelio/ktx → the real semver baked into the published package.json.
KtxCliPackageInfo collapses to { name, version, contextPackageName }; /health
no longer depends on version files surviving past a CI run.
Replaces the dual-branch (main + next) semantic-release model with a single-
branch model on main. rcs and stables interleave on the same branch via
{ name: 'main', prerelease: 'rc', channel: 'next' } / ['main']. Drops
@semantic-release/git and @semantic-release/changelog (nothing is committed
back to the repo on any channel) and the workflow's "Prepare next prerelease
branch" step plus the KTX_PRERELEASE_BRANCH plumbing. The git tag plus the
published npm artifact carry the version forward.
Updates docs/release.md, removes the two now-unused devDeps, regenerates
pnpm-lock.yaml. 611/611 @ktx/cli tests, 173/173 script tests, type-check,
biome, knip all clean.
* fix(release): don't throw on non-main branches at config-load time
knip loads .releaserc.cjs on every PR run, where GITHUB_REF_NAME is the
merge ref (e.g. 180/merge). The previous version of releaseBranches threw
immediately when the branch wasn't main, which made knip fail to evaluate
the config and then mis-flag @semantic-release/exec as an unused dep.
semantic-release already refuses to publish when the current branch doesn't
match a configured release branch, so the explicit throw was redundant.
Drop it (and the unused currentBranch helper) and replace the
"rejects releases from non-main" assertion with one that exercises a CI-
shaped GITHUB_REF_NAME and confirms the config loads.
2026-05-20 13:53:14 +02:00
|
|
|
requireOutput('ktx admin runtime start', runtimeStart, /Started KTX daemon/);
|
2026-05-20 01:36:54 +02:00
|
|
|
requireOutput('ktx admin runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/);
|
|
|
|
|
requireOutput('ktx admin runtime start', runtimeStart, /features: core/);
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const runtimeStartReuse = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'start'])));
|
2026-05-20 01:36:54 +02:00
|
|
|
requireSuccess('ktx admin runtime start reuse', runtimeStartReuse);
|
refactor(release): drop release-policy.json runtime dep and next branch (#180)
* chore: standardize daemon naming on "KTX daemon"
Replace inconsistent names ("KTX Python daemon", "KTX local embeddings
daemon", "KTX managed daemon", "Python daemon") with the single name
"KTX daemon" in CLI output, errors, command descriptions, test
assertions, smoke scripts, docs, AGENTS.md, issue templates, and
codecov flags. The daemon is a portable compute server with endpoints
for SQL analysis, semantic layer, LookML, database introspection, and
embeddings; the previous labels misrepresented it as embeddings-only or
exposed implementation details ("Python", "managed").
The "KTX Python runtime" concept (installed interpreter + packages) is
deliberately left as-is — it is a separate concept from the daemon
process.
* refactor(release): drop release-policy.json runtime dep and next branch
Strips the release-policy.json fallback from release-version.ts so the CLI
reads its version straight from packages/cli/package.json. dev → 0.0.0-private,
installed @kaelio/ktx → the real semver baked into the published package.json.
KtxCliPackageInfo collapses to { name, version, contextPackageName }; /health
no longer depends on version files surviving past a CI run.
Replaces the dual-branch (main + next) semantic-release model with a single-
branch model on main. rcs and stables interleave on the same branch via
{ name: 'main', prerelease: 'rc', channel: 'next' } / ['main']. Drops
@semantic-release/git and @semantic-release/changelog (nothing is committed
back to the repo on any channel) and the workflow's "Prepare next prerelease
branch" step plus the KTX_PRERELEASE_BRANCH plumbing. The git tag plus the
published npm artifact carry the version forward.
Updates docs/release.md, removes the two now-unused devDeps, regenerates
pnpm-lock.yaml. 611/611 @ktx/cli tests, 173/173 script tests, type-check,
biome, knip all clean.
* fix(release): don't throw on non-main branches at config-load time
knip loads .releaserc.cjs on every PR run, where GITHUB_REF_NAME is the
merge ref (e.g. 180/merge). The previous version of releaseBranches threw
immediately when the branch wasn't main, which made knip fail to evaluate
the config and then mis-flag @semantic-release/exec as an unused dep.
semantic-release already refuses to publish when the current branch doesn't
match a configured release branch, so the explicit throw was redundant.
Drop it (and the unused currentBranch helper) and replace the
"rejects releases from non-main" assertion with one that exercises a CI-
shaped GITHUB_REF_NAME and confirms the config loads.
2026-05-20 13:53:14 +02:00
|
|
|
requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /Using existing KTX daemon/);
|
2026-05-20 01:36:54 +02:00
|
|
|
requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /features: core/);
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const runtimeStop = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'stop'])));
|
2026-05-20 01:36:54 +02:00
|
|
|
requireSuccess('ktx admin runtime stop', runtimeStop);
|
2026-05-11 15:50:34 +02:00
|
|
|
daemonStarted = false;
|
refactor(release): drop release-policy.json runtime dep and next branch (#180)
* chore: standardize daemon naming on "KTX daemon"
Replace inconsistent names ("KTX Python daemon", "KTX local embeddings
daemon", "KTX managed daemon", "Python daemon") with the single name
"KTX daemon" in CLI output, errors, command descriptions, test
assertions, smoke scripts, docs, AGENTS.md, issue templates, and
codecov flags. The daemon is a portable compute server with endpoints
for SQL analysis, semantic layer, LookML, database introspection, and
embeddings; the previous labels misrepresented it as embeddings-only or
exposed implementation details ("Python", "managed").
The "KTX Python runtime" concept (installed interpreter + packages) is
deliberately left as-is — it is a separate concept from the daemon
process.
* refactor(release): drop release-policy.json runtime dep and next branch
Strips the release-policy.json fallback from release-version.ts so the CLI
reads its version straight from packages/cli/package.json. dev → 0.0.0-private,
installed @kaelio/ktx → the real semver baked into the published package.json.
KtxCliPackageInfo collapses to { name, version, contextPackageName }; /health
no longer depends on version files surviving past a CI run.
Replaces the dual-branch (main + next) semantic-release model with a single-
branch model on main. rcs and stables interleave on the same branch via
{ name: 'main', prerelease: 'rc', channel: 'next' } / ['main']. Drops
@semantic-release/git and @semantic-release/changelog (nothing is committed
back to the repo on any channel) and the workflow's "Prepare next prerelease
branch" step plus the KTX_PRERELEASE_BRANCH plumbing. The git tag plus the
published npm artifact carry the version forward.
Updates docs/release.md, removes the two now-unused devDeps, regenerates
pnpm-lock.yaml. 611/611 @ktx/cli tests, 173/173 script tests, type-check,
biome, knip all clean.
* fix(release): don't throw on non-main branches at config-load time
knip loads .releaserc.cjs on every PR run, where GITHUB_REF_NAME is the
merge ref (e.g. 180/merge). The previous version of releaseBranches threw
immediately when the branch wasn't main, which made knip fail to evaluate
the config and then mis-flag @semantic-release/exec as an unused dep.
semantic-release already refuses to publish when the current branch doesn't
match a configured release branch, so the explicit throw was redundant.
Drop it (and the unused currentBranch helper) and replace the
"rejects releases from non-main" assertion with one that exercises a CI-
shaped GITHUB_REF_NAME and confirms the config loads.
2026-05-20 13:53:14 +02:00
|
|
|
requireOutput('ktx admin runtime stop', runtimeStop, /Stopped KTX daemon/);
|
2026-05-20 01:36:54 +02:00
|
|
|
process.stdout.write('ktx admin runtime daemon lifecycle verified\\n');
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const structuralScan = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand(['exec', 'ktx', 'ingest', 'warehouse', '--project-dir', projectDir, '--fast', '--no-input']),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-14 01:43:06 +02:00
|
|
|
requireSuccessWithProjectStderr('ktx ingest fast', structuralScan, projectDir);
|
|
|
|
|
requireOutput('ktx ingest fast', structuralScan, /Ingest finished/);
|
|
|
|
|
requireOutput('ktx ingest fast', structuralScan, /Database schema/);
|
|
|
|
|
requireOutput('ktx ingest fast', structuralScan, /warehouse\\s+done/);
|
2026-05-10 23:12:26 +02:00
|
|
|
await access(join(projectDir, 'semantic-layer', 'warehouse', '_schema', 'public.yaml'));
|
2026-05-14 01:43:06 +02:00
|
|
|
process.stdout.write('ktx ingest fast verified\\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const enrichedScan = await run(
|
|
|
|
|
...Object.values(
|
|
|
|
|
pnpmCommand(['exec', 'ktx', 'ingest', 'warehouse', '--project-dir', projectDir, '--deep', '--no-input']),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-14 01:43:06 +02:00
|
|
|
requireExitCodeWithProjectStderr('ktx ingest deep readiness guard', enrichedScan, projectDir, 1);
|
|
|
|
|
requireOutput('ktx ingest deep readiness guard', enrichedScan, /Ingest finished with partial failures/);
|
|
|
|
|
requireOutput('ktx ingest deep readiness guard', enrichedScan, /requires deep ingest readiness/);
|
|
|
|
|
process.stdout.write('ktx ingest deep readiness guard verified\\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
await access(join(projectDir, '.ktx', 'db.sqlite'));
|
2026-05-14 01:43:06 +02:00
|
|
|
process.stdout.write('ktx ingest state verified\\n');
|
2026-05-10 23:12:26 +02:00
|
|
|
} finally {
|
2026-05-11 15:50:34 +02:00
|
|
|
if (daemonStarted) {
|
2026-05-26 03:16:53 -07:00
|
|
|
await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'stop'])));
|
|
|
|
|
await delay(500);
|
2026-05-11 15:50:34 +02:00
|
|
|
}
|
|
|
|
|
if (previousRuntimeRoot === undefined) {
|
|
|
|
|
delete process.env.KTX_RUNTIME_ROOT;
|
|
|
|
|
} else {
|
|
|
|
|
process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
|
|
|
|
|
}
|
2026-05-26 03:16:53 -07:00
|
|
|
await rmWithRetry(root);
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
export function npmCliSmokeSource() {
|
2026-05-10 23:12:26 +02:00
|
|
|
return `
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { execFile } from 'node:child_process';
|
2026-05-11 15:50:34 +02:00
|
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
2026-05-10 23:12:26 +02:00
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
import { promisify } from 'node:util';
|
|
|
|
|
|
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
function pnpmCommand(args) {
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'pnpm', ...args] };
|
|
|
|
|
}
|
|
|
|
|
return { command: 'pnpm', args };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
async function run(command, args, options = {}) {
|
|
|
|
|
process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n');
|
|
|
|
|
try {
|
|
|
|
|
const result = await execFileAsync(command, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env ?? process.env,
|
|
|
|
|
encoding: 'utf8',
|
|
|
|
|
timeout: 45_000,
|
|
|
|
|
});
|
|
|
|
|
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
code: typeof error.code === 'number' ? error.code : 1,
|
|
|
|
|
stdout: error.stdout ?? '',
|
|
|
|
|
stderr: error.stderr ?? error.message,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireSuccess(label, result) {
|
|
|
|
|
assert.equal(
|
|
|
|
|
result.code,
|
|
|
|
|
0,
|
|
|
|
|
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireStdout(label, result, pattern) {
|
|
|
|
|
assert.match(result.stdout, pattern, label + ' stdout did not match ' + pattern);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
const root = await mkdtemp(join(tmpdir(), 'ktx-cli-smoke-'));
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
2026-05-11 15:50:34 +02:00
|
|
|
const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8'));
|
|
|
|
|
assert.deepEqual(Object.keys(packageJson.dependencies), ['@kaelio/ktx']);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const help = await run(...Object.values(pnpmCommand(['exec', 'ktx', '--help'])));
|
2026-05-10 23:51:24 +02:00
|
|
|
requireSuccess('ktx --help', help);
|
|
|
|
|
requireStdout('ktx --help', help, /Usage: ktx/);
|
|
|
|
|
requireStdout('ktx --help', help, /setup/);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const setupHelp = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'setup', '--help'])));
|
2026-05-13 00:38:26 +02:00
|
|
|
requireSuccess('ktx setup --help', setupHelp);
|
|
|
|
|
requireStdout('ktx setup --help', setupHelp, /Usage: ktx setup/);
|
|
|
|
|
requireStdout('ktx setup --help', setupHelp, /--no-input/);
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
const doctor = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'status', '--verbose', '--no-input'])));
|
2026-05-12 23:51:46 +02:00
|
|
|
assert.ok([0, 1].includes(doctor.code), 'ktx status setup exit code must be 0 or 1');
|
2026-05-14 01:13:31 +02:00
|
|
|
requireStdout('ktx status setup', doctor, /KTX status/);
|
|
|
|
|
requireStdout('ktx status setup', doctor, /No project here yet\\./);
|
2026-05-14 01:43:06 +02:00
|
|
|
requireStdout('ktx status setup', doctor, /ktx setup/);
|
2026-05-14 01:27:31 +02:00
|
|
|
requireStdout('ktx status setup', doctor, /Node 22\\+/);
|
2026-05-12 23:51:46 +02:00
|
|
|
assert.equal(doctor.stderr, '', 'ktx status setup wrote unexpected stderr');
|
2026-05-10 23:12:26 +02:00
|
|
|
} finally {
|
|
|
|
|
await rm(root, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function buildArtifacts(layout) {
|
|
|
|
|
await rm(layout.artifactDir, { recursive: true, force: true });
|
|
|
|
|
await mkdir(layout.npmDir, { recursive: true });
|
|
|
|
|
await mkdir(layout.pythonDir, { recursive: true });
|
|
|
|
|
|
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
|
|
|
const [npmBuildCommand, wheelCommand, packCommand] = buildArtifactCommands(layout);
|
2026-05-11 15:50:34 +02:00
|
|
|
|
2026-05-15 12:06:37 +02:00
|
|
|
await runCommand(npmBuildCommand.command, npmBuildCommand.args, { cwd: npmBuildCommand.cwd });
|
|
|
|
|
await runCommand(wheelCommand.command, wheelCommand.args, { cwd: wheelCommand.cwd });
|
2026-05-11 15:50:34 +02:00
|
|
|
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
|
|
|
|
await copyRuntimeWheelAssets(layout, pythonArtifacts);
|
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
|
|
|
await runCommand(packCommand.command, packCommand.args, { cwd: packCommand.cwd });
|
2026-05-10 23:12:26 +02:00
|
|
|
|
|
|
|
|
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
|
|
|
|
|
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
|
|
|
|
|
}
|
|
|
|
|
await writeArtifactManifest(layout);
|
|
|
|
|
await assertPathExists(artifactManifestPath(layout), 'artifact manifest');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 01:04:44 +02:00
|
|
|
async function buildRuntimeWheelAssets(layout) {
|
|
|
|
|
await rm(layout.pythonDir, { recursive: true, force: true });
|
|
|
|
|
await mkdir(layout.pythonDir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
const [, wheelCommand] = buildArtifactCommands(layout);
|
|
|
|
|
await runCommand(wheelCommand.command, wheelCommand.args, { cwd: wheelCommand.cwd });
|
|
|
|
|
const pythonArtifacts = await findPythonArtifacts(layout.pythonDir);
|
|
|
|
|
await copyRuntimeWheelAssets(layout, pythonArtifacts);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 23:12:26 +02:00
|
|
|
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`,
|
|
|
|
|
);
|
2026-05-13 01:15:35 +02:00
|
|
|
await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
|
2026-05-10 23:12:26 +02:00
|
|
|
await writeFile(join(projectDir, 'verify-npm.mjs'), npmVerifySource());
|
|
|
|
|
await writeFile(join(projectDir, 'verify-installed-cli.mjs'), npmRuntimeSmokeSource());
|
2026-05-13 00:38:26 +02:00
|
|
|
await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource());
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
{
|
|
|
|
|
const pnpmInstall = pnpmCommand(['install']);
|
|
|
|
|
await runCommand(pnpmInstall.command, pnpmInstall.args, { cwd: projectDir });
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
const pnpmRebuild = pnpmCommand(['rebuild', 'better-sqlite3']);
|
|
|
|
|
await runCommand(pnpmRebuild.command, pnpmRebuild.args, { cwd: projectDir });
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir });
|
2026-05-26 03:16:53 -07:00
|
|
|
{
|
|
|
|
|
const pnpmExecVersion = pnpmCommand(['exec', 'ktx', '--version']);
|
|
|
|
|
await runCommand(pnpmExecVersion.command, pnpmExecVersion.args, { cwd: projectDir });
|
|
|
|
|
}
|
2026-05-11 15:50:34 +02:00
|
|
|
await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir });
|
2026-05-13 00:38:26 +02:00
|
|
|
await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
async function verifyNpmCliArtifacts(layout, tmpRoot) {
|
2026-05-10 23:12:26 +02:00
|
|
|
for (const packageInfo of NPM_ARTIFACT_PACKAGES) {
|
|
|
|
|
await assertPathExists(layout.npmTarballs[packageInfo.name], `${packageInfo.name} tarball`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
const projectDir = join(tmpRoot, 'npm-cli-clean-install');
|
2026-05-10 23:12:26 +02:00
|
|
|
await mkdir(projectDir, { recursive: true });
|
|
|
|
|
await writeFile(join(projectDir, 'package.json'), `${JSON.stringify(npmSmokePackageJson(layout), null, 2)}\n`);
|
2026-05-13 01:15:35 +02:00
|
|
|
await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml());
|
2026-05-13 00:38:26 +02:00
|
|
|
await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource());
|
2026-05-10 23:12:26 +02:00
|
|
|
|
2026-05-26 03:16:53 -07:00
|
|
|
{
|
|
|
|
|
const pnpmInstall = pnpmCommand(['install']);
|
|
|
|
|
await runCommand(pnpmInstall.command, pnpmInstall.args, { cwd: projectDir });
|
|
|
|
|
}
|
2026-05-13 00:38:26 +02:00
|
|
|
await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir });
|
2026-05-10 23:12:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verifyArtifacts(layout) {
|
|
|
|
|
await verifyArtifactManifest(layout);
|
|
|
|
|
|
2026-05-10 23:51:24 +02:00
|
|
|
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-artifacts-'));
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
|
|
|
|
await verifyNpmArtifacts(layout, tmpRoot);
|
|
|
|
|
} finally {
|
|
|
|
|
await rm(tmpRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
async function verifyCliArtifacts(layout) {
|
2026-05-10 23:12:26 +02:00
|
|
|
await verifyArtifactManifest(layout);
|
|
|
|
|
|
2026-05-13 00:38:26 +02:00
|
|
|
const tmpRoot = await mkdtemp(join(tmpdir(), 'ktx-cli-artifacts-'));
|
2026-05-10 23:12:26 +02:00
|
|
|
try {
|
2026-05-13 00:38:26 +02:00
|
|
|
await verifyNpmCliArtifacts(layout, tmpRoot);
|
2026-05-10 23:12:26 +02:00
|
|
|
} finally {
|
|
|
|
|
await rm(tmpRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
const command = process.argv[2] ?? 'check';
|
|
|
|
|
const layout = packageArtifactLayout();
|
|
|
|
|
|
|
|
|
|
if (command === 'build') {
|
|
|
|
|
await buildArtifacts(layout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-17 01:04:44 +02:00
|
|
|
if (command === 'build-runtime') {
|
|
|
|
|
await buildRuntimeWheelAssets(layout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-10 23:12:26 +02:00
|
|
|
if (command === 'verify') {
|
|
|
|
|
await verifyArtifacts(layout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (command === 'verify-demo') {
|
2026-05-13 00:38:26 +02:00
|
|
|
await verifyCliArtifacts(layout);
|
2026-05-10 23:12:26 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (command === 'verify-manifest') {
|
|
|
|
|
await verifyArtifactManifest(layout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (command === 'check') {
|
|
|
|
|
await buildArtifacts(layout);
|
|
|
|
|
await verifyArtifacts(layout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(`Unknown package artifact command: ${command}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
|
|
|
try {
|
|
|
|
|
await main();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
|
|
|
|
process.exitCode = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|