Merge pull request #22 from Kaelio/andreybavt/fix-metabase-readiness

fix(cli): report metabase ingest readiness
This commit is contained in:
Andrey Avtomonov 2026-05-12 13:01:19 +02:00 committed by GitHub
commit da108e556c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 238 additions and 62 deletions

View file

@ -43,6 +43,7 @@ SQLite.
Install the CLI and run the setup wizard:
```bash
npm install @kaelio/ktx
npm install -g @kaelio/ktx
ktx setup
```
@ -70,6 +71,40 @@ KTX context built: yes
Agent integration ready: yes (claude-code:project)
```
Run the packaged demo without installing globally:
```bash
npx @kaelio/ktx setup demo --no-input
npx @kaelio/ktx setup demo inspect
```
The default demo uses packaged sample data and prebuilt context. It does not
require API keys, network access, or an LLM provider.
Generate SQL from a semantic-layer source:
```bash
npx @kaelio/ktx sl query --project-dir "$PROJECT_DIR" \
--connection-id warehouse \
--measure accounts.account_count \
--dimension accounts.segment \
--format sql
```
List and test a configured warehouse connection:
```bash
ktx connection list --project-dir "$PROJECT_DIR"
ktx connection test warehouse --project-dir "$PROJECT_DIR"
```
The connection test prints the configured driver and discovered table count:
```text
Driver: sqlite
Tables: 1
```
## What's in a project
```
@ -97,6 +132,47 @@ Semantic sources and knowledge pages are committed to git. The `.ktx/` directory
holds ephemeral state and is git-ignored — delete it and KTX rebuilds on the
next run.
### Scan the demo warehouse
Scan artifacts are written under
`raw-sources/warehouse/live-database/<syncId>/` in the project directory.
```bash
SCAN_OUTPUT="$(ktx scan warehouse --project-dir "$PROJECT_DIR")"
printf '%s\n' "$SCAN_OUTPUT"
SCAN_RUN_ID="$(printf '%s\n' "$SCAN_OUTPUT" | awk '/^Run: / { print $2 }')"
ktx scan status --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
ktx scan report --project-dir "$PROJECT_DIR" "$SCAN_RUN_ID"
```
For non-SQLite drivers, prefer credential references such as `--url env:NAME`
or `--url file:PATH` over literal credential URLs.
## Managed Python runtime
KTX installs its Python runtime only when a Python-backed command needs it.
The runtime lives outside the npm cache, is versioned by the installed CLI
version, and is managed by `ktx runtime` commands.
KTX requires `uv` on `PATH` to create the managed runtime. Install `uv` with
your system package manager or the official installer before running Python-
backed KTX commands. KTX doesn't download `uv` automatically; run
`ktx runtime doctor` if runtime installation fails:
```bash
ktx runtime install --yes
ktx runtime status
ktx runtime doctor
ktx runtime start
ktx runtime stop
ktx runtime prune --dry-run
ktx runtime prune --yes
```
The release artifact manifest contains the public npm tarball and the bundled `kaelio-ktx`
runtime wheel. The `python/ktx-sl` and `python/ktx-daemon` directories remain
source packages for development, not public release artifacts.
## Serve agents
KTX integrates with coding agents through CLI skills, an MCP server, or both.
@ -126,6 +202,11 @@ This exposes tools for connections, knowledge search, semantic-layer sources,
validation, queries, ingestion, and replay. The `--semantic-compute` flag starts
the managed Python runtime for query planning automatically.
The standalone MCP server exposes `connection_list`, `knowledge_search`,
`knowledge_read`, `knowledge_write`, `sl_list_sources`, `sl_read_source`,
`sl_write_source`, `sl_validate`, `sl_query`, `ingest_trigger`,
`ingest_status`, `ingest_report`, and `ingest_replay`.
Supported agents: Claude Code, Codex, Cursor, OpenCode, and any agent that
reads `.agents/` skills or MCP configuration.
@ -136,7 +217,13 @@ reads `.agents/` skills or MCP configuration.
| `packages/cli` | CLI entry point |
| `packages/context` | Core context engine |
| `packages/llm` | LLM and embedding providers |
| `packages/connector-*` | Database connectors (Postgres, Snowflake, BigQuery, ClickHouse, MySQL, SQL Server, SQLite) |
| `packages/connector-bigquery` | BigQuery scan connector |
| `packages/connector-clickhouse` | ClickHouse scan connector |
| `packages/connector-mysql` | MySQL scan connector |
| `packages/connector-postgres` | Postgres scan connector |
| `packages/connector-snowflake` | Snowflake scan connector |
| `packages/connector-sqlite` | SQLite scan connector |
| `packages/connector-sqlserver` | SQL Server scan connector |
| `python/ktx-sl` | Semantic-layer query planning |
| `python/ktx-daemon` | Portable compute service |

View file

@ -12,7 +12,7 @@ refs:
## New Hire Week-One Onboarding Policy
**Source:** Notion — People & Operating Norms, last edited 2026-05-07
**Source:** Notion — People & Operating Norms, last edited 2026-05-07
**Owner:** Manager (not People Ops)
---

View file

@ -20,7 +20,7 @@ tables:
# Activation KPI Glossary
**Owner team:** Growth
**Owner team:** Growth
**Source:** Notion — Orbit Demo Home / Data Team - Onboarding / Activation KPI Glossary, last edited 2026-05-07
Use this when a question is about signup-to-habit behavior. Orbit uses activation language across Growth, Product, and CS conversations.
@ -62,4 +62,3 @@ Growth conversations typically use D7 and D14 Activation Rate. Product and CS ma
## Relationship to Account-Level Activation
This glossary defines **customer-level** activation (signup-to-habit). The **account-level** activation workflow (requester login → first approved purchase request → account activated) is a separate concept tracked in `mart_account_activity` and governed by the January 2026 policy change. See `orbit-activation-policy-change-jan-2026` for that definition.

View file

@ -14,9 +14,9 @@ sl_refs:
# Activation Policy Change — January 2026
**Governed metric key:** `activated_accounts`
**Owner team:** growth
**Notion:** `notion://notion_page_activation_policy_decision#policy-change`
**Governed metric key:** `activated_accounts`
**Owner team:** growth
**Notion:** `notion://notion_page_activation_policy_decision#policy-change`
**Sources:** `mart_account_activity`, `int_activation_policy_windows`, `stg_activation_events`
## Policy Boundary

View file

@ -15,9 +15,9 @@ sl_refs:
# ARR — Contract-First Definition
**Governed metric key:** `arr`
**Owner team:** finance
**Notion:** `notion://notion_page_arr_contract_reporting#arr-contract-first`
**Governed metric key:** `arr`
**Owner team:** finance
**Notion:** `notion://notion_page_arr_contract_reporting#arr-contract-first`
**Source:** `mart_arr_daily` (grain: `metric_date`)
## Rule

View file

@ -21,7 +21,7 @@ refs:
Orbit sells procurement workflow and spend-control software. The core value proposition: route purchase requests, collect approvals, onboard suppliers, and issue purchase orders without turning every exception into a status hunt.
**Primary buyers:** Finance, Procurement, Business Operations.
**Primary buyers:** Finance, Procurement, Business Operations.
**Daily users:** department admins, office managers, IT leads, legal ops partners — anyone who has to get a vendor through the building.
## Product Workflow
@ -69,4 +69,3 @@ Orbit sells procurement workflow and spend-control software. The core value prop
- "Supplier onboarding is split across three teams."
- "Renewals are visible too late."
- "People keep asking Finance for status because there is nowhere better to look."

View file

@ -14,9 +14,9 @@ sl_refs:
# Customer Health Risk Definition
**Governed metric key:** `active_customers`
**Owner team:** customer_success
**Notion:** `notion://notion_page_customer_health_playbook#risk-definition`
**Governed metric key:** `active_customers`
**Owner team:** customer_success
**Notion:** `notion://notion_page_customer_health_playbook#risk-definition`
**Sources:** `mart_customer_health`, `int_customer_health_signals`
## Risk Levels

View file

@ -18,8 +18,8 @@ tables:
# Orbit Customers Source
**Table:** `orbit_analytics.customer`
**Grain:** one row per signed-up customer
**Table:** `orbit_analytics.customer`
**Grain:** one row per signed-up customer
**Source:** Notion — Orbit Demo Home / Data Team - Onboarding / Orbit Customers Source, last edited 2026-05-07
Use this when a question needs customer identity, plan tier, signup timing, recent activity, or the standard customer joins.
@ -58,4 +58,3 @@ Always join through `customer.id`. Do not join on `email`.
- **Timezone:** `created_at` and `last_seen_at` are UTC. Confirm whether a question expects UTC or a local business day before filtering.
- **Paying vs. all:** `free` customers must be excluded from paying-customer follow-ups. Use `paying_customer_count`, not `customer_count`.
- **plan_tier values:** `free`, `pro`, `enterprise`. Note: `pro_plus` is a legacy alias for `growth` in the account/contract layer (see `orbit-plan-segment-normalization`), but `plan_tier` on this table uses `pro` not `pro_plus`.

View file

@ -42,4 +42,3 @@ Declared in `models/exposures.yml`. All exposures are type `dashboard` with matu
- **Owner:** Growth (growth@orbit-demo.example.com)
- **Depends on:** `mart_account_activity`
- **Description:** Activation policy comparison around the January 2026 workflow update.

View file

@ -22,10 +22,10 @@ sl_refs:
# Orbit dbt Project Overview
**Project name:** `kaelio_demo`
**dbt version:** 1.0.0
**Profile target:** Postgres (`orbit_analytics` schema, `kaelio_demo` database)
**Raw source schema:** `orbit_raw`
**Project name:** `kaelio_demo`
**dbt version:** 1.0.0
**Profile target:** Postgres (`orbit_analytics` schema, `kaelio_demo` database)
**Raw source schema:** `orbit_raw`
**Analytics schema:** `orbit_analytics` (all models materialised as views by default)
## Model Layers
@ -52,4 +52,3 @@ sl_refs:
## Raw Source Tables (`orbit_raw` schema)
accounts, account_hierarchy, plans, contracts, subscriptions, contract_discount_terms, arr_movements, invoices, invoice_line_items, refunds, plan_segment_mapping, users, activation_events, sessions, purchase_requests, approval_events, suppliers, supplier_onboarding_events, purchase_orders, support_tickets, account_owners.

View file

@ -20,7 +20,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/106.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/107.json -->
**Table:** `orbit_analytics.mart_account_activity`
**Table:** `orbit_analytics.mart_account_activity`
**Grain:** one row per `policy_change_date`
## Columns
@ -47,4 +47,3 @@ tables:
- The January 2026 activation policy change (`policy_change_date = 2026-01-15`) is the primary boundary. `policy_version` in upstream events splits into `pre_2026_01_15` and `post_2026_01_15` cohorts.
- Rates are ratios (01); multiply by 100 for percentage display.
- See [orbit-activation-policy-change-jan-2026](orbit-activation-policy-change-jan-2026) for full policy context.

View file

@ -19,7 +19,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/69.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/100.json -->
**Table:** `orbit_analytics.mart_account_segments`
**Table:** `orbit_analytics.mart_account_segments`
**Grain:** one row per `account_id`
## Columns
@ -53,4 +53,3 @@ tables:
- `normalized_plan_code` maps `pro_plus``growth`. Always use `normalized_plan_code` for plan-based reporting. See [orbit-plan-segment-normalization](orbit-plan-segment-normalization).
- `segment` is derived from `canonical_plan_code × size_band` via `stg_plan_segment_mapping`.
- `contract_arr_cents` is the contract-first ARR value. See [orbit-arr-contract-first-definition](orbit-arr-contract-first-definition).

View file

@ -18,7 +18,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/56.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/96.json -->
**Table:** `orbit_analytics.mart_arr_daily`
**Table:** `orbit_analytics.mart_arr_daily`
**Grain:** one row per `metric_date`
## Columns
@ -44,4 +44,3 @@ tables:
- ARR is calculated contract-first: active contract ARR takes precedence over subscription ARR for any covered period. See [orbit-arr-contract-first-definition](orbit-arr-contract-first-definition).
- `display` is a formatted label for UI rendering; use `arr_cents` for all arithmetic.

View file

@ -20,7 +20,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/98.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/103.json -->
**Table:** `orbit_analytics.mart_nrr_quarterly`
**Table:** `orbit_analytics.mart_nrr_quarterly`
**Grain:** one row per `quarter_label` × `segment`
## Columns
@ -53,4 +53,3 @@ tables:
- `net_revenue_retention` is a ratio, not a percentage. Multiply by 100 for display.
- Contraction includes discount expirations (classified as contraction, not churn). See [orbit-nrr-discount-expiration-treatment](orbit-nrr-discount-expiration-treatment).
- Enterprise is the primary executive reporting segment.

View file

@ -18,7 +18,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/88.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/108.json -->
**Table:** `orbit_analytics.mart_procurement_activity`
**Table:** `orbit_analytics.mart_procurement_activity`
**Grain:** one row per `week_start_date` × `contract_arr_threshold_cents`
## Columns
@ -45,4 +45,3 @@ tables:
- `active_requesters` counts non-internal, non-test requesters on large active contracts. See [orbit-procurement-qualifying-actions](orbit-procurement-qualifying-actions).
- The standard threshold is `contract_arr_threshold_cents = 20000000` ($200k ARR).
- Always filter by `contract_arr_threshold_cents` — the table contains rows for multiple threshold values.

View file

@ -19,7 +19,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/105.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/115.json -->
**Table:** `orbit_analytics.mart_retention_movement_breakout`
**Table:** `orbit_analytics.mart_retention_movement_breakout`
**Grain:** one row per `quarter_label` × `segment` × `movement_type` × `movement_reason`
## Columns
@ -53,4 +53,3 @@ tables:
- Contraction includes discount expirations, classified as contraction (not churn), tracked via `movement_reason`. See [orbit-nrr-discount-expiration-treatment](orbit-nrr-discount-expiration-treatment).
- This table is the row-level source for `mart_nrr_quarterly` aggregations.
- Only one of `expansion_arr_cents`, `contraction_arr_cents`, `churned_arr_cents` is non-zero per row.

View file

@ -20,7 +20,7 @@ tables:
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/102.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/104.json -->
**Table:** `orbit_analytics.mart_revenue_daily`
**Table:** `orbit_analytics.mart_revenue_daily`
**Grain:** one row per `revenue_date`
## Columns
@ -54,4 +54,3 @@ tables:
- `reconciliation_check` must be `true` on every row. Any `false` row indicates a data quality issue.
- Gross-to-net reconciliation: gross revenue credits refunds = net revenue. See [orbit-revenue-gross-to-net-reconciliation](orbit-revenue-gross-to-net-reconciliation).
- All amounts are in cents; divide by 100 for USD, by 100,000,000 for $M.

View file

@ -69,4 +69,3 @@ Card 48 is the canonical reference; card 55 is a filtered variant for large-cont
| 53 | Enterprise NRR quarter breakout | mart_nrr_quarterly | 0 |
| 54 | February credits drilldown | mart_revenue_daily | 0 |
| 55 | Large contract requesters | mart_account_segments | 0 |

View file

@ -15,9 +15,9 @@ sl_refs:
# NRR — Discount Expiration Treatment
**Governed metric key:** `net_revenue_retention`
**Owner team:** analytics
**Notion:** `notion://notion_page_retention_policy_current#nrr-definition` and `#discount-expiration-treatment`
**Governed metric key:** `net_revenue_retention`
**Owner team:** analytics
**Notion:** `notion://notion_page_retention_policy_current#nrr-definition` and `#discount-expiration-treatment`
**Sources:** `mart_nrr_quarterly`, `mart_retention_movement_breakout`
## NRR Definition

View file

@ -14,9 +14,9 @@ sl_refs:
# Plan & Segment Normalization
**Governed metric key:** `segment`
**Owner team:** sales_ops
**Notion:** `notion://notion_page_sales_ops_segmentation#growth-plan-normalization`
**Governed metric key:** `segment`
**Owner team:** sales_ops
**Notion:** `notion://notion_page_sales_ops_segmentation#growth-plan-normalization`
**Sources:** `mart_account_segments`, `stg_plan_segment_mapping`, `stg_plans`
## Canonical Plan Codes

View file

@ -14,9 +14,9 @@ sl_refs:
# Procurement — Qualifying Actions & Weekly Active Requesters
**Governed metric key:** `weekly_active_requesters`
**Owner team:** product
**Notion:** `notion://notion_page_procurement_instrumentation#qualifying-procurement-actions`
**Governed metric key:** `weekly_active_requesters`
**Owner team:** product
**Notion:** `notion://notion_page_procurement_instrumentation#qualifying-procurement-actions`
**Sources:** `mart_procurement_activity`, `int_procurement_qualifying_actions`
## Qualifying Action Definition

View file

@ -14,9 +14,9 @@ sl_refs:
# Revenue — Gross-to-Net Reconciliation
**Governed metric key:** `net_revenue`
**Owner team:** finance
**Notion:** `notion://notion_page_revenue_reporting_policy#gross-to-net-reconciliation`
**Governed metric key:** `net_revenue`
**Owner team:** finance
**Notion:** `notion://notion_page_revenue_reporting_policy#gross-to-net-reconciliation`
**Source:** `mart_revenue_daily` (grain: `revenue_date`)
## Formula

View file

@ -14,7 +14,7 @@ refs:
## Sales Ops → Customer Success Implementation Handoff
**Source:** Notion — People & Operating Norms, last edited 2026-05-07
**Source:** Notion — People & Operating Norms, last edited 2026-05-07
**Owner:** Sales Ops (sender), Customer Success (receiver)
---

View file

@ -11,6 +11,9 @@ import type { renderMemoryFlowTui } from './memory-flow-tui.js';
import { KTX_NEXT_STEP_COMMANDS } from './next-steps.js';
import { resetVizFallbackWarningsForTest } from './viz-fallback.js';
const SEEDED_DEMO_SEMANTIC_SOURCE_COUNT = 46;
const SEEDED_DEMO_KNOWLEDGE_PAGE_COUNT = 28;
function makeIo(options: { isTTY?: boolean; columns?: number; rawMode?: boolean } = {}) {
let stdout = '';
let stderr = '';
@ -336,8 +339,14 @@ describe('runKtxDemo', () => {
notion: { pageCount: 8 },
},
generatedOutputs: {
semanticLayer: { manifestSourceCount: 46, fileCount: 46 },
knowledge: { manifestPageCount: 28, fileCount: 28 },
semanticLayer: {
manifestSourceCount: SEEDED_DEMO_SEMANTIC_SOURCE_COUNT,
fileCount: SEEDED_DEMO_SEMANTIC_SOURCE_COUNT,
},
knowledge: {
manifestPageCount: SEEDED_DEMO_KNOWLEDGE_PAGE_COUNT,
fileCount: SEEDED_DEMO_KNOWLEDGE_PAGE_COUNT,
},
links: { manifestLinkCount: 23, linkCount: 23 },
reports: { primaryPath: 'reports/seeded-demo-report.json', fileCount: 1 },
},
@ -636,10 +645,16 @@ describe('runKtxDemo', () => {
).resolves.toBe(0);
expect(seededIo.stdout()).toContain('Status: ready');
expect(seededIo.stdout()).toContain('Semantic-layer sources: 46 manifest, 46 files');
expect(seededIo.stdout()).toContain('Knowledge pages: 28 manifest, 28 files');
expect(seededIo.stdout()).toContain(
`Semantic-layer sources: ${SEEDED_DEMO_SEMANTIC_SOURCE_COUNT} manifest, ${SEEDED_DEMO_SEMANTIC_SOURCE_COUNT} files`,
);
expect(seededIo.stdout()).toContain(
`Knowledge pages: ${SEEDED_DEMO_KNOWLEDGE_PAGE_COUNT} manifest, ${SEEDED_DEMO_KNOWLEDGE_PAGE_COUNT} files`,
);
expect(seededIo.stdout()).not.toContain('Status: corrupt');
expect(seededIo.stdout()).not.toContain('Semantic-layer sources: 46 manifest, 0 files');
expect(seededIo.stdout()).not.toContain(
`Semantic-layer sources: ${SEEDED_DEMO_SEMANTIC_SOURCE_COUNT} manifest, 0 files`,
);
});
it('fails corrupted demo projects in no-input mode with reset guidance', async () => {

View file

@ -3,6 +3,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js';
import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js';
import { runDemoTour } from './setup-demo-tour.js';
import { readKtxSetupStatus, runKtxSetup } from './setup.js';
@ -311,6 +312,62 @@ describe('setup status', () => {
});
});
it('reports Vertex LLM and context ready after a successful Metabase ingest report', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
'project: revenue',
'setup:',
' database_connection_ids:',
' - warehouse',
' completed_steps:',
' - project',
' - databases',
' - sources',
'connections:',
' warehouse:',
' driver: postgres',
' url: env:DATABASE_URL',
' metabase:',
' driver: metabase',
' url: env:METABASE_URL',
' api_key_ref: env:METABASE_API_KEY',
' warehouse_connection_id: warehouse',
'llm:',
' provider:',
' backend: vertex',
' vertex:',
' project: kaelio-dev',
' location: us-east5',
' models:',
' default: claude-sonnet-4-6',
'ingest:',
' embeddings:',
' backend: deterministic',
' model: deterministic',
' dimensions: 8',
'',
].join('\n'),
'utf-8',
);
await persistLocalBundleReport(
tempDir,
localFakeBundleReport('metabase-job-1', {
connectionId: 'warehouse',
sourceKey: 'metabase',
}),
);
const status = await readKtxSetupStatus(tempDir);
const io = makeIo();
await expect(runKtxSetup({ command: 'status', projectDir: tempDir, json: false }, io.io)).resolves.toBe(0);
expect(status.llm).toMatchObject({ backend: 'vertex', ready: true, model: 'claude-sonnet-4-6' });
expect(status.context).toMatchObject({ ready: true, status: 'completed' });
expect(io.stdout()).toContain('LLM ready: yes (claude-sonnet-4-6)');
expect(io.stdout()).toContain('KTX context built: yes');
});
it('prints plain and JSON setup status', async () => {
const plainIo = makeIo();
const jsonIo = makeIo();

View file

@ -1,7 +1,8 @@
import { existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { cancel, isCancel, select } from '@clack/prompts';
import { loadKtxProject } from '@ktx/context/project';
import { getLatestLocalIngestStatus, savedMemoryCountsForReport } from '@ktx/context/ingest';
import { ktxLocalStateDbPath, loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
import type { KtxCliIo } from './cli-runtime.js';
import { formatSetupNextStepLines } from './next-steps.js';
import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js';
@ -248,6 +249,31 @@ function sourceConnections(config: Awaited<ReturnType<typeof loadKtxProject>>['c
.sort((left, right) => left.connectionId.localeCompare(right.connectionId));
}
type LocalIngestStatusReport = NonNullable<Awaited<ReturnType<typeof getLatestLocalIngestStatus>>>;
function reportHasSavedContext(report: LocalIngestStatusReport): boolean {
if (report.body.failedWorkUnits.length > 0) {
return false;
}
const counts = savedMemoryCountsForReport(report);
return counts.wikiCount > 0 || counts.slCount > 0;
}
async function readIngestContextStatus(project: KtxLocalProject): Promise<KtxSetupContextStatusSummary | null> {
if (!existsSync(ktxLocalStateDbPath(project))) {
return null;
}
const report = await getLatestLocalIngestStatus(project);
if (!report || !reportHasSavedContext(report)) {
return null;
}
return {
ready: true,
status: 'completed',
runId: report.runId,
};
}
export async function readKtxSetupStatus(projectDir: string): Promise<KtxSetupStatus> {
const resolvedProjectDir = resolve(projectDir);
if (!existsSync(join(resolvedProjectDir, 'ktx.yaml'))) {
@ -279,6 +305,10 @@ export async function readKtxSetupStatus(projectDir: string): Promise<KtxSetupSt
const completedSteps = project.config.setup?.completed_steps ?? [];
const contextState = await readKtxSetupContextState(resolvedProjectDir);
const setupContextStatus = setupContextStatusFromState(contextState, {
completedStep: completedSteps.includes('context'),
});
const ingestContextStatus = setupContextStatus.ready ? null : await readIngestContextStatus(project);
const databaseIds = project.config.setup?.database_connection_ids ?? Object.keys(project.config.connections);
const databasesComplete = completedSteps.includes('databases');
const manifest = await readKtxAgentInstallManifest(resolvedProjectDir);
@ -301,7 +331,7 @@ export async function readKtxSetupStatus(projectDir: string): Promise<KtxSetupSt
...source,
ready: completedSteps.includes('sources'),
})),
context: setupContextStatusFromState(contextState, { completedStep: completedSteps.includes('context') }),
context: ingestContextStatus ?? setupContextStatus,
agents,
};
}

View file

@ -368,7 +368,7 @@ describe('standalone built ktx CLI smoke', () => {
const knowledgeSearch = structuredContent<{
results: Array<{ key: string; summary: string; score: number }>;
totalFound: number;
}>(await client.callTool({ name: 'knowledge_search', arguments: { query: 'ARR contract', limit: 5 } }));
}>(await client.callTool({ name: 'knowledge_search', arguments: { query: 'ARR contract-first definition', limit: 10 } }));
expect(knowledgeSearch.totalFound).toBeGreaterThan(0);
expect(knowledgeSearch.results.map((result) => result.key)).toContain('orbit-arr-contract-first-definition');
@ -387,7 +387,7 @@ describe('standalone built ktx CLI smoke', () => {
const slRead = structuredContent<{ sourceName: string; yaml: string }>(
await client.callTool({
name: 'sl_read_source',
arguments: { connectionId: 'postgres-warehouse', sourceName: 'mart_arr_daily' },
arguments: { connectionId: 'dbt-main', sourceName: 'mart_arr_daily' },
}),
);
expect(slRead.sourceName).toBe('mart_arr_daily');
@ -397,7 +397,7 @@ describe('standalone built ktx CLI smoke', () => {
const slValidate = structuredContent<{ success: boolean; errors: string[]; warnings: string[] }>(
await client.callTool({
name: 'sl_validate',
arguments: { connectionId: 'postgres-warehouse', names: ['mart_arr_daily'] },
arguments: { connectionId: 'dbt-main', names: ['mart_arr_daily', 'stg_contracts'] },
}),
);
expect(slValidate.success).toBe(true);