mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
Merge remote-tracking branch 'origin/main' into fix-sl-query-source-column-type
# Conflicts: # packages/context/skills/metabase_ingest/SKILL.md # packages/context/skills/sl_capture/SKILL.md
This commit is contained in:
commit
cd49d5d4ae
168 changed files with 3567 additions and 1621 deletions
|
|
@ -12,7 +12,7 @@ refs:
|
|||
|
||||
## Customer Update Communication Standard
|
||||
|
||||
**Source:** Notion — People & Operating Norms, last edited 2026-05-07
|
||||
**Source:** Notion - People & Operating Norms, last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -12,23 +12,23 @@ 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)
|
||||
|
||||
---
|
||||
|
||||
## Policy
|
||||
|
||||
Every new hire must understand **four things by end of week one**. The manager — not People Ops — is responsible for supplying this context.
|
||||
Every new hire must understand **four things by end of week one**. The manager - not People Ops - is responsible for supplying this context.
|
||||
|
||||
## Required Week-One Knowledge
|
||||
|
||||
| # | What the new hire must understand |
|
||||
|---|---|
|
||||
| 1 | **What Orbit sells** — the core procurement workflow product and value proposition |
|
||||
| 2 | **Why procurement workflow gets messy inside a customer** — the pain points that make Orbit necessary |
|
||||
| 3 | **Which team handles which part of the customer lifecycle** — team lanes and ownership boundaries |
|
||||
| 4 | **What their first useful project is** — a concrete, scoped piece of work they can contribute to immediately |
|
||||
| 1 | **What Orbit sells** - the core procurement workflow product and value proposition |
|
||||
| 2 | **Why procurement workflow gets messy inside a customer** - the pain points that make Orbit necessary |
|
||||
| 3 | **Which team handles which part of the customer lifecycle** - team lanes and ownership boundaries |
|
||||
| 4 | **What their first useful project is** - a concrete, scoped piece of work they can contribute to immediately |
|
||||
|
||||
## Ownership
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ tables:
|
|||
# Activation KPI Glossary
|
||||
|
||||
**Owner team:** Growth
|
||||
**Source:** Notion — Orbit Demo Home / Data Team - Onboarding / Activation KPI Glossary, last edited 2026-05-07
|
||||
**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.
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ A customer is **activated** when **all three** of the following happen **within
|
|||
| 2. Email Verified | `customer.email_verified_at` is not null | `orbit_analytics.customer` |
|
||||
| 3. First Project | At least one row in `orbit_analytics.project` for the customer | `orbit_analytics.project` |
|
||||
| 4. Team Invite | At least one row in `orbit_analytics.invite` for the customer | `orbit_analytics.invite` |
|
||||
| 5. Activated | All of (2), (3), and (4) within 14 days of (1) | — |
|
||||
| 5. Activated | All of (2), (3), and (4) within 14 days of (1) | - |
|
||||
|
||||
## Conversion-Rate KPIs
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ A customer is **activated** when **all three** of the following happen **within
|
|||
| **D14 Activation Rate** | `activated_customers_within_14_days / signups_in_cohort` |
|
||||
| **Time-to-Activate** | `median(activated_at - created_at)` in hours |
|
||||
|
||||
Growth conversations typically use D7 and D14 Activation Rate. Product and CS may ask about individual funnel steps — confirm whether they mean the full activation definition or only one stage.
|
||||
Growth conversations typically use D7 and D14 Activation Rate. Product and CS may ask about individual funnel steps - confirm whether they mean the full activation definition or only one stage.
|
||||
|
||||
## Source Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ sl_refs:
|
|||
- mart_account_activity
|
||||
---
|
||||
|
||||
# Activation Policy Change — January 2026
|
||||
# Activation Policy Change - January 2026
|
||||
|
||||
**Governed metric key:** `activated_accounts`
|
||||
**Owner team:** growth
|
||||
|
|
@ -23,8 +23,8 @@ sl_refs:
|
|||
|
||||
The activation workflow changed on **2026-01-15**. All activation events are tagged with `policy_version`:
|
||||
|
||||
- `pre_2026_01_15` — events before the workflow update
|
||||
- `post_2026_01_15` — events after the workflow update
|
||||
- `pre_2026_01_15` - events before the workflow update
|
||||
- `post_2026_01_15` - events after the workflow update
|
||||
|
||||
## Activation Event Types
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ sl_refs:
|
|||
- mart_account_segments
|
||||
---
|
||||
|
||||
# ARR — Contract-First Definition
|
||||
# ARR - Contract-First Definition
|
||||
|
||||
**Governed metric key:** `arr`
|
||||
**Owner team:** finance
|
||||
|
|
@ -30,10 +30,10 @@ The dbt test on `mart_arr_daily.arr_cents` asserts the value equals **1,874,200,
|
|||
|
||||
## Intermediate model
|
||||
|
||||
`int_active_contract_arr` — active contract ARR as of 2026-03-31 (grain: `contract_id`).
|
||||
`int_active_contract_arr` - active contract ARR as of 2026-03-31 (grain: `contract_id`).
|
||||
|
||||
## Related
|
||||
|
||||
- `stg_contracts` — contract records (status: draft, active, cancelled, expired)
|
||||
- `stg_subscriptions` — fallback ARR source (status: active, cancelled, past_due, trialing)
|
||||
- `mart_arr_daily` — board-prep daily ARR mart
|
||||
- `stg_contracts` - contract records (status: draft, active, cancelled, expired)
|
||||
- `stg_subscriptions` - fallback ARR source (status: active, cancelled, past_due, trialing)
|
||||
- `mart_arr_daily` - board-prep daily ARR mart
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ refs:
|
|||
|
||||
# Orbit Company Overview
|
||||
|
||||
**Source:** Notion — Orbit Demo Home / Company Overview + Orbit Demo Home (root), last edited 2026-05-07
|
||||
**Source:** Notion - Orbit Demo Home / Company Overview + Orbit Demo Home (root), last edited 2026-05-07
|
||||
|
||||
## What Orbit Sells
|
||||
|
||||
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.
|
||||
**Daily users:** department admins, office managers, IT leads, legal ops partners — anyone who has to get a vendor through the building.
|
||||
**Daily users:** department admins, office managers, IT leads, legal ops partners - anyone who has to get a vendor through the building.
|
||||
|
||||
## Product Workflow
|
||||
|
||||
|
|
|
|||
|
|
@ -21,18 +21,18 @@ sl_refs:
|
|||
|
||||
## Risk Levels
|
||||
|
||||
`low`, `medium`, `high` — derived from two signal types:
|
||||
`low`, `medium`, `high` - derived from two signal types:
|
||||
|
||||
1. **Support ticket signals** (`stg_support_tickets`): open or pending tickets with severity `high` or `critical` increase risk.
|
||||
2. **Procurement activity signals** (`stg_purchase_requests`, `stg_purchase_orders`): recent qualifying procurement actions reduce risk.
|
||||
|
||||
## Intermediate Model
|
||||
|
||||
`int_customer_health_signals` — combines open critical ticket count and recent procurement action count per account.
|
||||
`int_customer_health_signals` - combines open critical ticket count and recent procurement action count per account.
|
||||
|
||||
## Mart
|
||||
|
||||
`mart_customer_health` — account-grain risk mart as of **2026-03-31**.
|
||||
`mart_customer_health` - account-grain risk mart as of **2026-03-31**.
|
||||
|
||||
- `account_id`: dbt not_null, unique
|
||||
- `risk_level`: dbt accepted_values [low, medium, high]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ refs:
|
|||
|
||||
## Customer Stakeholder Needs by Role
|
||||
|
||||
**Source:** Notion — Product & Customers, last edited 2026-05-07
|
||||
**Source:** Notion - Product & Customers, last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ These are recurring, role-specific customer needs observed across accounts. Use
|
|||
| Role | Primary Need | Implication |
|
||||
|---|---|---|
|
||||
| **Finance** | Committed spend visibility earlier in the procurement cycle | Surface budget commitments at request approval, not at PO creation |
|
||||
| **Department leaders** | Request speed — faster time from request to approval | Reduce approval routing friction; minimize back-and-forth |
|
||||
| **Department leaders** | Request speed - faster time from request to approval | Reduce approval routing friction; minimize back-and-forth |
|
||||
| **Procurement** | Supplier file complete before the first invoice | Supplier onboarding must be finished before PO is issued, not after |
|
||||
| **Legal** | Fewer emergency reviews | Route contracts with legal implications earlier; avoid last-minute escalations |
|
||||
| **Customer Success (internal)** | Renewal risk visible before the account is already annoyed | CS needs leading indicators of dissatisfaction, not lagging ones |
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ tables:
|
|||
|
||||
**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
|
||||
**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.
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ Use this when a question needs customer identity, plan tier, signup timing, rece
|
|||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `id` | number | Primary key, surrogate key |
|
||||
| `email` | string | Login email, unique — **do not use as join key** |
|
||||
| `email` | string | Login email, unique - **do not use as join key** |
|
||||
| `name` | string | Display name |
|
||||
| `country` | string | ISO 3166-1 alpha-2 code |
|
||||
| `plan_tier` | string | One of `free`, `pro`, `enterprise` |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ refs:
|
|||
|
||||
## How We Work
|
||||
|
||||
**Source:** Notion — Orbit Demo Home / How We Work, last edited 2026-05-07
|
||||
**Source:** Notion - Orbit Demo Home / How We Work, last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ refs:
|
|||
|---|---|
|
||||
| **Monday** | Commitments and dependency checks |
|
||||
| **Tuesday – Thursday** | Customer calls, product work, implementation, and building |
|
||||
| **Friday** | Closing loops — review what shipped, what slipped, and write down any decisions |
|
||||
| **Friday** | Closing loops - review what shipped, what slipped, and write down any decisions |
|
||||
|
||||
Use this rhythm when scheduling work, meetings, or reviews. Do not schedule decision-making meetings on Fridays; use Friday to record decisions already made.
|
||||
|
||||
|
|
@ -64,11 +64,11 @@ These are explicitly codified rules Orbit has identified as recurring failure mo
|
|||
- **Escalations are coordination tools, not indicators of individual failure.** Escalating is the correct behavior when a problem exceeds the current team's ability to resolve it alone.
|
||||
- When escalating, the person escalating must:
|
||||
1. Bring in the right people (those with authority or context to unblock).
|
||||
2. Summarize current state clearly — what has been tried, what is blocked, and why.
|
||||
2. Summarize current state clearly - what has been tried, what is blocked, and why.
|
||||
3. Name the customer impact explicitly.
|
||||
4. Keep updates moving until the risk is resolved or a workaround is established.
|
||||
- Escalations that stall because no one owns the next update are a process failure, not a customer failure.
|
||||
- An escalation is closed when the risk is resolved or a documented workaround is in place — not when the immediate noise stops.
|
||||
- An escalation is closed when the risk is resolved or a documented workaround is in place - not when the immediate noise stops.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ refs:
|
|||
|
||||
## Known Product Gaps and Friction Points
|
||||
|
||||
**Source:** Notion — Product & Customers (Notes from Recent Customer Calls), last edited 2026-05-07
|
||||
**Source:** Notion - Product & Customers (Notes from Recent Customer Calls), last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_account_activity`)
|
||||
|
||||
- `avg_pre_policy_activation_rate` — `avg(pre_policy_30_day_activation_rate)`
|
||||
- `avg_post_policy_activation_rate` — `avg(post_policy_30_day_activation_rate)`
|
||||
- `avg_pre_policy_activation_rate` - `avg(pre_policy_30_day_activation_rate)`
|
||||
- `avg_post_policy_activation_rate` - `avg(post_policy_30_day_activation_rate)`
|
||||
|
||||
## Common query patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_account_segments`)
|
||||
|
||||
- `account_count` — `count(*)`
|
||||
- `total_contract_arr_cents` — `sum(contract_arr_cents)`
|
||||
- `active_contract_arr_cents` — `sum(contract_arr_cents)` where `contract_status = 'active'`
|
||||
- `active_contract_arr_millions` — active ARR in $M
|
||||
- `account_count` - `count(*)`
|
||||
- `total_contract_arr_cents` - `sum(contract_arr_cents)`
|
||||
- `active_contract_arr_cents` - `sum(contract_arr_cents)` where `contract_status = 'active'`
|
||||
- `active_contract_arr_millions` - active ARR in $M
|
||||
|
||||
## Common query patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_arr_daily`)
|
||||
|
||||
- `total_arr_cents` — `sum(arr_cents)`
|
||||
- `arr_millions` — `round(sum(arr_cents) / 100000000.0, 3)` — ARR in $M
|
||||
- `total_arr_cents` - `sum(arr_cents)`
|
||||
- `arr_millions` - `round(sum(arr_cents) / 100000000.0, 3)` - ARR in $M
|
||||
|
||||
## Common query patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_nrr_quarterly`)
|
||||
|
||||
- `avg_nrr` — `avg(net_revenue_retention)` across all rows
|
||||
- `avg_nrr_enterprise` — `avg(net_revenue_retention)` filtered to `segment = 'enterprise'`
|
||||
- `avg_nrr` - `avg(net_revenue_retention)` across all rows
|
||||
- `avg_nrr_enterprise` - `avg(net_revenue_retention)` filtered to `segment = 'enterprise'`
|
||||
- `total_expansion_arr_cents`, `total_contraction_arr_cents`, `total_churned_arr_cents`
|
||||
|
||||
## Common query patterns
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_procurement_activity`)
|
||||
|
||||
- `total_active_requesters` — `sum(active_requesters)`
|
||||
- `active_requesters_200k_threshold` — `sum(active_requesters)` where `contract_arr_threshold_cents = 20000000`
|
||||
- `total_active_requesters` - `sum(active_requesters)`
|
||||
- `active_requesters_200k_threshold` - `sum(active_requesters)` where `contract_arr_threshold_cents = 20000000`
|
||||
|
||||
## Common query patterns
|
||||
|
||||
|
|
@ -44,4 +44,4 @@ 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.
|
||||
- Always filter by `contract_arr_threshold_cents` - the table contains rows for multiple threshold values.
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ tables:
|
|||
|
||||
## Key measures (SL: `mart_revenue_daily`)
|
||||
|
||||
- `total_gross_revenue_cents` — `sum(gross_revenue_cents)`
|
||||
- `total_credits_cents` — `sum(credits_cents)`
|
||||
- `total_refunds_cents` — `sum(refunds_cents)`
|
||||
- `total_net_revenue_cents` — `sum(net_revenue_cents)`
|
||||
- `net_revenue_millions` — `round(sum(net_revenue_cents) / 100000000.0, 3)`
|
||||
- `gross_revenue_millions` — `round(sum(gross_revenue_cents) / 100000000.0, 3)`
|
||||
- `total_gross_revenue_cents` - `sum(gross_revenue_cents)`
|
||||
- `total_credits_cents` - `sum(credits_cents)`
|
||||
- `total_refunds_cents` - `sum(refunds_cents)`
|
||||
- `total_net_revenue_cents` - `sum(net_revenue_cents)`
|
||||
- `net_revenue_millions` - `round(sum(net_revenue_cents) / 100000000.0, 3)`
|
||||
- `gross_revenue_millions` - `round(sum(gross_revenue_cents) / 100000000.0, 3)`
|
||||
|
||||
## Common query patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ sl_refs:
|
|||
- mart_nrr_quarterly
|
||||
---
|
||||
|
||||
# Orbit Metabase SQL Library — Patterns & Conventions
|
||||
# Orbit Metabase SQL Library - Patterns & Conventions
|
||||
|
||||
Collection **7 "SQL Library"** (parent: Orbit Showcase, collection 5) contains reference queries that demonstrate how to write Metabase native SQL against the Orbit analytics marts. Cards here are intentionally illustrative; several have `dashboardCount: 0` and are not embedded in live dashboards.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ sl_refs:
|
|||
- mart_nrr_quarterly
|
||||
---
|
||||
|
||||
# NRR — Discount Expiration Treatment
|
||||
# NRR - Discount Expiration Treatment
|
||||
|
||||
**Governed metric key:** `net_revenue_retention`
|
||||
**Owner team:** analytics
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ sl_refs:
|
|||
- mart_procurement_activity
|
||||
---
|
||||
|
||||
# Procurement — Qualifying Actions & Weekly Active Requesters
|
||||
# Procurement - Qualifying Actions & Weekly Active Requesters
|
||||
|
||||
**Governed metric key:** `weekly_active_requesters`
|
||||
**Owner team:** product
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ refs:
|
|||
|
||||
## Orbit Product Design Principles
|
||||
|
||||
**Source:** Notion — Product & Customers, last edited 2026-05-07
|
||||
**Source:** Notion - Product & Customers, last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ refs:
|
|||
|
||||
## Product Review Checklist
|
||||
|
||||
**Source:** Notion — Product & Customers, last edited 2026-05-07
|
||||
**Source:** Notion - Product & Customers, last edited 2026-05-07
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ sl_refs:
|
|||
- mart_revenue_daily
|
||||
---
|
||||
|
||||
# Revenue — Gross-to-Net Reconciliation
|
||||
# Revenue - Gross-to-Net Reconciliation
|
||||
|
||||
**Governed metric key:** `net_revenue`
|
||||
**Owner team:** finance
|
||||
|
|
@ -25,7 +25,7 @@ sl_refs:
|
|||
net_revenue = gross_revenue - credits - refunds
|
||||
```
|
||||
|
||||
All amounts are in **cents** (USD only — `stg_invoices.currency` is asserted to be `USD`).
|
||||
All amounts are in **cents** (USD only - `stg_invoices.currency` is asserted to be `USD`).
|
||||
|
||||
## Components
|
||||
|
||||
|
|
@ -38,12 +38,12 @@ All amounts are in **cents** (USD only — `stg_invoices.currency` is asserted t
|
|||
|
||||
## Intermediate model
|
||||
|
||||
`int_revenue_components` — daily gross, credit, refund, and net revenue components.
|
||||
`int_revenue_components` - daily gross, credit, refund, and net revenue components.
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- `reconciliation_check` must be `true` on every row of `mart_revenue_daily`.
|
||||
- `assert_february_2026_net_revenue` — a dbt singular test covering February 2026 net revenue total.
|
||||
- `assert_february_2026_net_revenue` - a dbt singular test covering February 2026 net revenue total.
|
||||
|
||||
## Line Item Types (`stg_invoice_line_items`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
|
@ -27,7 +27,7 @@ Sales Ops must complete the handoff **before the first implementation call**. Cu
|
|||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| Current plan | Starter / Growth / Enterprise — use canonical plan name |
|
||||
| Current plan | Starter / Growth / Enterprise - use canonical plan name |
|
||||
| Account segment | self_serve / commercial / enterprise (see `orbit-plan-segment-normalization`) |
|
||||
| Contract shape | Term, ARR, any discounts or custom terms |
|
||||
| Renewal contact | Named person on the customer side responsible for renewal |
|
||||
|
|
@ -38,7 +38,7 @@ Sales Ops must complete the handoff **before the first implementation call**. Cu
|
|||
|
||||
- **Sales Ops** is responsible for populating and delivering the handoff before the first implementation call.
|
||||
- **Customer Success** is responsible for flagging missing fields to Sales Ops before the call, not during or after.
|
||||
- If a field is unknown at handoff time, Sales Ops must note it explicitly as "unknown — to be resolved by [date]" rather than leaving it blank.
|
||||
- If a field is unknown at handoff time, Sales Ops must note it explicitly as "unknown - to be resolved by [date]" rather than leaving it blank.
|
||||
|
||||
## Common Failure Mode
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ Handoffs that omit contract shape or renewal contact force CS to re-engage Sales
|
|||
- Enterprise accounts with parent/child account structures require extra care during handoff.
|
||||
- Small assumptions made during handoff in these accounts tend to produce large downstream problems (billing mismatches, approval routing failures, supplier onboarding gaps).
|
||||
- When the account has parent/child complexity, Sales Ops must explicitly flag it in the handoff and document the account hierarchy before the first implementation call.
|
||||
- CS should treat any undocumented parent/child relationship as a blocker — do not proceed with implementation setup until the structure is confirmed.
|
||||
- CS should treat any undocumented parent/child relationship as a blocker - do not proceed with implementation setup until the structure is confirmed.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export interface KtxCliCommandContext {
|
|||
deps: KtxCliDeps;
|
||||
packageInfo: KtxCliPackageInfo;
|
||||
setExitCode: (code: number) => void;
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
runInit: (args: { projectDir: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
writeDebug?: (command: string, commandContext: CommandWithGlobalOptions) => void;
|
||||
}
|
||||
|
||||
|
|
@ -32,14 +32,14 @@ export interface OutputModeOptions {
|
|||
}
|
||||
|
||||
interface KtxCommanderProgramOptions {
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
runInit: (args: { projectDir: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
}
|
||||
|
||||
export interface BuildKtxProgramOptions {
|
||||
io: KtxCliIo;
|
||||
deps: KtxCliDeps;
|
||||
packageInfo: KtxCliPackageInfo;
|
||||
runInit: (args: { projectDir: string; projectName?: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
runInit: (args: { projectDir: string; force: boolean }, io: KtxCliIo) => Promise<number>;
|
||||
setExitCode?: (code: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +58,8 @@ type CommandPathNode = CommandWithGlobalOptions & {
|
|||
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']);
|
||||
const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx dev init']);
|
||||
const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']);
|
||||
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
||||
const GLOBAL_OPTIONS_WITHOUT_VALUE = new Set(['--debug', '--help', '-h', '--version', '-v']);
|
||||
|
||||
class KtxProjectMissingAbortError extends Error {
|
||||
readonly isKtxProjectMissingAbort = true;
|
||||
|
|
@ -72,24 +74,6 @@ function isKtxProjectMissingAbortError(error: unknown): error is KtxProjectMissi
|
|||
(typeof error === 'object' && error !== null && (error as { isKtxProjectMissingAbort?: unknown }).isKtxProjectMissingAbort === true)
|
||||
);
|
||||
}
|
||||
const REMOVED_COMMAND_PATHS = new Set([
|
||||
'scan',
|
||||
'wiki read',
|
||||
'wiki write',
|
||||
]);
|
||||
const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']);
|
||||
const OPTIONS_WITH_VALUE = new Set([
|
||||
'--project-dir',
|
||||
'--query-history-window-days',
|
||||
'--user-id',
|
||||
'--limit',
|
||||
'--format',
|
||||
'--connection-id',
|
||||
'--source-name',
|
||||
'--query-file',
|
||||
'--max-rows',
|
||||
]);
|
||||
|
||||
export interface CommandWithGlobalOptions {
|
||||
opts: () => object;
|
||||
optsWithGlobals?: () => object;
|
||||
|
|
@ -336,43 +320,32 @@ function formatCliError(error: unknown): string {
|
|||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function commandPathFromArgv(argv: string[]): string[] {
|
||||
const path: string[] = [];
|
||||
for (let index = 0; index < argv.length && path.length < 2; index += 1) {
|
||||
function firstTopLevelCommandToken(argv: string[]): string | null {
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (arg === '--') {
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
if ((path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE).has(arg)) {
|
||||
if (GLOBAL_OPTIONS_WITH_VALUE.has(arg)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const optionsWithValue = path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE;
|
||||
if ([...optionsWithValue].some((option) => arg.startsWith(`${option}=`))) {
|
||||
if ([...GLOBAL_OPTIONS_WITH_VALUE].some((option) => arg.startsWith(`${option}=`))) {
|
||||
continue;
|
||||
}
|
||||
if (path.length === 0 && arg === '--debug') {
|
||||
if (GLOBAL_OPTIONS_WITHOUT_VALUE.has(arg) || arg.startsWith('-')) {
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('-')) {
|
||||
continue;
|
||||
}
|
||||
path.push(arg);
|
||||
return arg;
|
||||
}
|
||||
return path;
|
||||
return null;
|
||||
}
|
||||
|
||||
function removedCommandName(argv: string[]): string | null {
|
||||
const path = commandPathFromArgv(argv);
|
||||
if (path.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathKey = path.join(' ');
|
||||
return REMOVED_COMMAND_PATHS.has(pathKey) ? path.at(-1) ?? null : null;
|
||||
function isKnownTopLevelCommand(program: Command, commandName: string): boolean {
|
||||
return program.commands.some((command) => command.name() === commandName || command.aliases().includes(commandName));
|
||||
}
|
||||
|
||||
async function runBareInteractiveCommand(
|
||||
|
|
@ -489,9 +462,9 @@ export async function runCommanderKtxCli(
|
|||
return 0;
|
||||
}
|
||||
|
||||
const removedCommand = removedCommandName(argv);
|
||||
if (removedCommand) {
|
||||
io.stderr.write(`error: unknown command '${removedCommand}'\n`);
|
||||
const topLevelCommand = firstTopLevelCommandToken(argv);
|
||||
if (topLevelCommand && !isKnownTopLevelCommand(program, topLevelCommand)) {
|
||||
io.stderr.write(`error: unknown command '${topLevelCommand}'\n`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,14 +59,10 @@ export function packageInfoFromJson(packageJson: unknown): KtxCliPackageInfo {
|
|||
};
|
||||
}
|
||||
|
||||
async function runInit(
|
||||
args: { projectDir: string; projectName?: string; force: boolean },
|
||||
io: KtxCliIo,
|
||||
): Promise<number> {
|
||||
async function runInit(args: { projectDir: string; force: boolean }, io: KtxCliIo): Promise<number> {
|
||||
const { initKtxProject } = await import('@ktx/context/project');
|
||||
const result = await initKtxProject({
|
||||
projectDir: args.projectDir,
|
||||
projectName: args.projectName,
|
||||
force: args.force,
|
||||
});
|
||||
|
||||
|
|
@ -77,7 +73,7 @@ async function runInit(
|
|||
}
|
||||
|
||||
export async function runInitForCommander(
|
||||
args: { projectDir: string; projectName?: string; force: boolean },
|
||||
args: { projectDir: string; force: boolean },
|
||||
io: KtxCliIo,
|
||||
): Promise<number> {
|
||||
return await runInit(args, io);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('lists configured connections without resolving secrets', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
warehouse: { driver: 'postgres', url: 'env:DATABASE_URL' },
|
||||
docs: { driver: 'notion', auth_token_ref: 'env:NOTION_TOKEN', crawl_mode: 'all_accessible' },
|
||||
|
|
@ -100,7 +100,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('prints an empty-state message that points at setup instead of removed connection add', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKtxConnection({ command: 'list', projectDir }, io.io)).resolves.toBe(0);
|
||||
|
|
@ -111,7 +111,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a native connection by calling connector.testConnection (not introspect)', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
warehouse: { driver: 'sqlite' },
|
||||
});
|
||||
|
|
@ -136,7 +136,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('reports the connector error and still cleans up when native testConnection fails', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
warehouse: { driver: 'sqlite' },
|
||||
});
|
||||
|
|
@ -155,7 +155,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a configured Metabase connection through the Metabase runtime client', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
prod_metabase: {
|
||||
driver: 'metabase',
|
||||
|
|
@ -201,7 +201,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a Looker connection through the Looker client', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
bi_looker: {
|
||||
driver: 'looker',
|
||||
|
|
@ -230,7 +230,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('falls back to userId when Looker metadata has no display name', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
bi_looker: {
|
||||
driver: 'looker',
|
||||
|
|
@ -255,7 +255,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('reports the Looker error when testConnection fails', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
bi_looker: {
|
||||
driver: 'looker',
|
||||
|
|
@ -277,7 +277,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a Notion connection by retrieving the bot user', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
docs: {
|
||||
driver: 'notion',
|
||||
|
|
@ -302,7 +302,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('falls back to bot id when Notion bot has no name', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
docs: {
|
||||
driver: 'notion',
|
||||
|
|
@ -323,7 +323,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a dbt connection via testRepoConnection (success)', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
process.env.DBT_TOKEN = 'gh_token_abc'; // pragma: allowlist secret
|
||||
await writeConnections(projectDir, {
|
||||
'dbt-main': {
|
||||
|
|
@ -354,7 +354,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('reports the git error when testRepoConnection fails for dbt', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
'dbt-main': {
|
||||
driver: 'dbt',
|
||||
|
|
@ -377,7 +377,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a LookML connection via testRepoConnection with camelCase repoUrl', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
lookml_main: {
|
||||
driver: 'lookml',
|
||||
|
|
@ -400,7 +400,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('tests a MetricFlow connection via the nested metricflow block', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
mf_main: {
|
||||
driver: 'metricflow',
|
||||
|
|
@ -422,7 +422,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('--all: prints a single coherent list with one row per connection', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
warehouse: { driver: 'sqlite' },
|
||||
docs: { driver: 'notion', auth_token: 'secret_token', crawl_mode: 'all_accessible' }, // pragma: allowlist secret
|
||||
|
|
@ -450,7 +450,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('--all: marks failing connections, keeps passing ones, and returns non-zero', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeConnections(projectDir, {
|
||||
warehouse: { driver: 'sqlite' },
|
||||
broken: { driver: 'sqlite' },
|
||||
|
|
@ -476,7 +476,7 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('--all: shows an empty-state message when no connections are configured', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
const io = makeIo();
|
||||
|
||||
await expect(runKtxConnection({ command: 'test-all', projectDir }, io.io)).resolves.toBe(0);
|
||||
|
|
@ -488,16 +488,18 @@ describe('runKtxConnection', () => {
|
|||
|
||||
it('rejects unknown drivers with a helpful error', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await writeConnections(projectDir, {
|
||||
mystery: { driver: 'duckdb' },
|
||||
});
|
||||
await initKtxProject({ projectDir });
|
||||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
'connections:\n mystery:\n driver: duckdb\n',
|
||||
'utf-8',
|
||||
);
|
||||
const io = makeIo();
|
||||
|
||||
await expect(
|
||||
runKtxConnection({ command: 'test', projectDir, connectionId: 'mystery' }, io.io),
|
||||
).resolves.toBe(1);
|
||||
expect(io.stderr()).toContain('uses driver "duckdb"');
|
||||
expect(io.stderr()).toContain('Supported:');
|
||||
expect(io.stderr()).toContain('connections.mystery.driver');
|
||||
expect(io.stderr()).toContain('postgres');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function projectWithConnections(connections: KtxProjectConfig['connections']): K
|
|||
return {
|
||||
projectDir: '/tmp/project',
|
||||
config: {
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
...buildDefaultKtxProjectConfig(),
|
||||
connections,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ export function defaultDemoProjectDir(): string {
|
|||
|
||||
function demoConfig(databasePath: string): string {
|
||||
return [
|
||||
'project: ktx-demo-orbit',
|
||||
'connections:',
|
||||
` ${DEMO_CONNECTION_ID}:`,
|
||||
' driver: sqlite',
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ describe('dev Commander tree', () => {
|
|||
const testIo = makeIo();
|
||||
|
||||
try {
|
||||
await expect(runKtxCli(['dev', 'init', projectDir, '--name', 'warehouse'], testIo.io)).resolves.toBe(0);
|
||||
await expect(runKtxCli(['dev', 'init', projectDir], testIo.io)).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.toContain('project: warehouse');
|
||||
await expect(readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).resolves.not.toContain('project:');
|
||||
expect(testIo.stderr()).toBe('');
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
|
|
@ -92,7 +92,7 @@ describe('dev Commander tree', () => {
|
|||
|
||||
try {
|
||||
await expect(
|
||||
runKtxCli(['--project-dir', projectDir, 'dev', 'init', '--name', 'global-init'], testIo.io),
|
||||
runKtxCli(['--project-dir', projectDir, 'dev', 'init'], testIo.io),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(testIo.stdout()).toContain(`Initialized KTX project at ${projectDir}`);
|
||||
|
|
|
|||
|
|
@ -25,19 +25,17 @@ export function registerDevCommands(program: Command, context: KtxCliCommandCont
|
|||
.command('init')
|
||||
.description('Initialize a Git-backed KTX project directory for maintenance scripts')
|
||||
.argument('[directory]', 'Project directory')
|
||||
.option('--name <name>', 'Project name written to ktx.yaml')
|
||||
.option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
|
||||
.action(
|
||||
async (
|
||||
projectDir: string | undefined,
|
||||
commandOptions: { name?: string; force?: boolean },
|
||||
commandOptions: { force?: boolean },
|
||||
command: CommandWithGlobalOptions,
|
||||
) => {
|
||||
context.setExitCode(
|
||||
await context.runInit(
|
||||
{
|
||||
projectDir: projectDir ? resolve(projectDir) : resolveCommandProjectDir(command),
|
||||
...(commandOptions.name ? { projectName: commandOptions.name } : {}),
|
||||
force: commandOptions.force === true,
|
||||
},
|
||||
context.io,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { basename, join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
formatDoctorReport,
|
||||
|
|
@ -64,6 +64,11 @@ describe('formatDoctorReport', () => {
|
|||
expect(output).toContain('Node 22+ · pnpm 10.20+');
|
||||
expect(output).not.toContain('v22.16.0');
|
||||
expect(output).toContain('Everything ready.');
|
||||
expect(output).toContain('ktx status --json');
|
||||
expect(output).toContain('ktx sl list');
|
||||
expect(output).toContain('ktx wiki list');
|
||||
expect(output).not.toContain('ktx scan');
|
||||
expect(output).not.toContain('ktx sl ask');
|
||||
});
|
||||
|
||||
it('shows the underlying detail for a single-check group on the group line', () => {
|
||||
|
|
@ -328,7 +333,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'storrage:',
|
||||
' state: sqlite',
|
||||
'ingest:',
|
||||
|
|
@ -359,7 +363,7 @@ describe('runKtxDoctor', () => {
|
|||
it('emits structured JSON when ktx.yaml fails Zod validation', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
['project: warehouse', 'storrage: {}', ''].join('\n'),
|
||||
['storrage: {}', ''].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const testIo = makeIo();
|
||||
|
|
@ -387,7 +391,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -418,7 +421,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -452,7 +454,7 @@ describe('runKtxDoctor', () => {
|
|||
|
||||
const out = testIo.stdout();
|
||||
expect(out).toContain('KTX status');
|
||||
expect(out).toContain('· warehouse');
|
||||
expect(out).toContain(`· ${basename(tempDir)}`);
|
||||
expect(out).toContain('Connections (1)');
|
||||
expect(out).toContain('LLM');
|
||||
expect(out).toContain('anthropic');
|
||||
|
|
@ -465,10 +467,10 @@ describe('runKtxDoctor', () => {
|
|||
it('includes Postgres query-history readiness in project doctor output', async () => {
|
||||
process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret
|
||||
process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret
|
||||
process.env.WAREHOUSE_DATABASE_URL = 'postgresql://reader@example.test/warehouse';
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -520,15 +522,20 @@ describe('runKtxDoctor', () => {
|
|||
expect(out).toContain('pg_stat_statements ready (PostgreSQL 16.4)');
|
||||
expect(out).toContain('info: pg_stat_statements.max is 1000');
|
||||
expect(out).not.toContain('Update the Postgres parameter group or config');
|
||||
expect(out).toContain('ktx status --json');
|
||||
expect(out).toContain('ktx sl list');
|
||||
expect(out).toContain('ktx wiki list');
|
||||
expect(out).not.toContain('ktx scan');
|
||||
expect(out).not.toContain('ktx sl ask');
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.WAREHOUSE_DATABASE_URL;
|
||||
});
|
||||
|
||||
it('returns blocked verdict when LLM is not configured', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -548,6 +555,7 @@ describe('runKtxDoctor', () => {
|
|||
).resolves.toBe(1);
|
||||
|
||||
expect(testIo.stdout()).toContain('no LLM configured');
|
||||
expect(testIo.stdout()).not.toContain('ktx ask');
|
||||
expect(testIo.stdout()).toContain('ktx setup');
|
||||
});
|
||||
|
||||
|
|
@ -558,7 +566,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -620,7 +627,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -660,7 +666,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -695,7 +700,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -724,7 +728,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'storrage:',
|
||||
' state: sqlite',
|
||||
'ingest:',
|
||||
|
|
@ -752,7 +755,7 @@ describe('runKtxDoctor', () => {
|
|||
it('emits structured JSON issues when validation fails', async () => {
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
['project: warehouse', 'storrage: {}', ''].join('\n'),
|
||||
['storrage: {}', ''].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const testIo = makeIo();
|
||||
|
|
@ -788,7 +791,6 @@ describe('runKtxDoctor', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { join, resolve } from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
import type { KtxConfigIssue } from '@ktx/context/project';
|
||||
import { KTX_NEXT_STEP_DIRECT_COMMANDS } from './next-steps.js';
|
||||
import type { BuildProjectStatusOptions } from './status-project.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
|
@ -287,7 +288,7 @@ interface RenderOptions {
|
|||
command?: 'setup' | 'project';
|
||||
}
|
||||
|
||||
const NEXT_STEPS_PROJECT = ['ktx scan', 'ktx wiki', 'ktx sl ask "…"'];
|
||||
const NEXT_STEPS_PROJECT = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command);
|
||||
|
||||
export function formatDoctorReport(report: DoctorReport, options: Partial<RenderOptions> = {}): string {
|
||||
const opts: RenderOptions = {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-cli-'));
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: cli-dispatch-fixture\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), '{}\n', 'utf-8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -502,7 +502,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
it('keeps representative JSON command stdout parseable', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
const commands = [
|
||||
['--project-dir', projectDir, 'status', '--json'],
|
||||
['--project-dir', projectDir, 'sl', 'list', '--json'],
|
||||
|
|
@ -580,7 +580,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
try {
|
||||
delete process.env.KTX_PROJECT_DIR;
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
process.chdir(tempDir);
|
||||
|
||||
await expect(runKtxCli([], testIo.io, { setup })).resolves.toBe(0);
|
||||
|
|
@ -1482,7 +1482,7 @@ describe('runKtxCli', () => {
|
|||
|
||||
it('dispatches public connection subcommands through the existing connection implementation', async () => {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'ktx-connection-dispatch-'));
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: connection-dispatch\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), '{}\n', 'utf-8');
|
||||
const connection = vi.fn(async () => 0);
|
||||
|
||||
await expect(
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ export async function writeWarehouseConfig(projectDir: string): Promise<void> {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' prod-metabase:',
|
||||
' driver: metabase',
|
||||
' api_url: https://metabase.example.test',
|
||||
' warehouse_a:',
|
||||
' driver: postgres',
|
||||
'ingest:',
|
||||
|
|
@ -126,7 +126,6 @@ export async function writeMetabaseConfig(projectDir: string): Promise<void> {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -488,7 +487,6 @@ export async function runPublicMetabaseSyncModeCase(tempDir: string, input: Sync
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
`project: metabase-sync-mode-${input.name}`,
|
||||
'connections:',
|
||||
' prod-metabase:',
|
||||
' driver: metabase',
|
||||
|
|
|
|||
|
|
@ -633,7 +633,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: metabase-cli',
|
||||
'connections:',
|
||||
' prod-metabase:',
|
||||
' driver: metabase',
|
||||
|
|
@ -1099,7 +1098,7 @@ describe('runKtxIngest', () => {
|
|||
|
||||
it('passes managed daemon options to adapters and pull-config options when no explicit daemon URL is set', async () => {
|
||||
const projectDir = join(tempDir, 'managed-daemon-ingest-project');
|
||||
await initKtxProject({ projectDir, projectName: 'managed-daemon-ingest-project' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeWarehouseConfig(projectDir);
|
||||
const createdAdapters: SourceAdapter[] = [
|
||||
{ source: 'fake', skillNames: [], detect: async () => true, chunk: async () => ({ workUnits: [] }) },
|
||||
|
|
@ -1159,7 +1158,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: historic-sql-project',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1224,7 +1222,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: historic-sql-progress-project',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1353,7 +1350,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: historic-sql-step-progress-project',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1446,7 +1442,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: historic-sql-concurrent-progress-project',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1596,7 +1591,6 @@ describe('runKtxIngest', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: looker-cli',
|
||||
'connections:',
|
||||
' prod-looker:',
|
||||
' driver: looker',
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ export interface PrintListArgs<Row> {
|
|||
io: KtxCliIo;
|
||||
}
|
||||
|
||||
export interface KtxJsonResultEnvelope<T> {
|
||||
interface KtxJsonResultEnvelope<T> {
|
||||
kind: string;
|
||||
data: T;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function writeJsonResult<T>(io: KtxCliIo, envelope: KtxJsonResultEnvelope<T>): void {
|
||||
function writeJsonResult<T>(io: KtxCliIo, envelope: KtxJsonResultEnvelope<T>): void {
|
||||
io.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKtxProject } from '@ktx/context/project';
|
||||
import { initKtxProject, loadKtxProject } from '@ktx/context/project';
|
||||
import type { KtxEmbeddingPort } from '@ktx/context';
|
||||
import { writeLocalKnowledgePage } from '@ktx/context/wiki';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { runKtxKnowledge } from './knowledge.js';
|
||||
|
||||
|
|
@ -40,6 +41,28 @@ class FakeEmbeddingPort implements KtxEmbeddingPort {
|
|||
}
|
||||
}
|
||||
|
||||
interface WikiPageFixture {
|
||||
key?: string;
|
||||
summary?: string;
|
||||
content?: string;
|
||||
tags?: string[];
|
||||
slRefs?: string[];
|
||||
}
|
||||
|
||||
async function seedWikiPage(projectDir: string, fixture: WikiPageFixture = {}): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await writeLocalKnowledgePage(project, {
|
||||
key: fixture.key ?? 'metrics-revenue',
|
||||
scope: 'GLOBAL',
|
||||
userId: 'local',
|
||||
summary: fixture.summary ?? 'Revenue',
|
||||
content: fixture.content ?? 'Revenue is paid order value.',
|
||||
tags: fixture.tags ?? ['finance'],
|
||||
refs: [],
|
||||
slRefs: fixture.slRefs ?? ['orders'],
|
||||
});
|
||||
}
|
||||
|
||||
describe('runKtxKnowledge', () => {
|
||||
let tempDir: string;
|
||||
|
||||
|
|
@ -51,36 +74,10 @@ describe('runKtxKnowledge', () => {
|
|||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('writes, reads, lists, and searches wiki pages', async () => {
|
||||
it('lists and searches wiki pages', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
const writeIo = makeIo();
|
||||
await expect(
|
||||
runKtxKnowledge(
|
||||
{
|
||||
command: 'write',
|
||||
projectDir,
|
||||
key: 'metrics-revenue',
|
||||
scope: 'GLOBAL',
|
||||
userId: 'local',
|
||||
summary: 'Revenue',
|
||||
content: 'Revenue is paid order value.',
|
||||
tags: ['finance'],
|
||||
refs: [],
|
||||
slRefs: ['orders'],
|
||||
},
|
||||
writeIo.io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
expect(writeIo.stdout()).toContain('Wrote wiki/global/metrics-revenue.md');
|
||||
|
||||
const readIo = makeIo();
|
||||
await expect(
|
||||
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local' }, readIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(readIo.stdout()).toContain('# metrics-revenue');
|
||||
expect(readIo.stdout()).toContain('Revenue is paid order value.');
|
||||
await initKtxProject({ projectDir });
|
||||
await seedWikiPage(projectDir);
|
||||
|
||||
const listIo = makeIo();
|
||||
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0);
|
||||
|
|
@ -93,27 +90,10 @@ describe('runKtxKnowledge', () => {
|
|||
expect(searchIo.stdout()).toContain('metrics-revenue');
|
||||
});
|
||||
|
||||
it('prints wiki list, search, and read as public JSON envelopes', async () => {
|
||||
it('prints wiki list and search as public JSON envelopes', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
await expect(
|
||||
runKtxKnowledge(
|
||||
{
|
||||
command: 'write',
|
||||
projectDir,
|
||||
key: 'metrics-revenue',
|
||||
scope: 'GLOBAL',
|
||||
userId: 'local',
|
||||
summary: 'Revenue',
|
||||
content: 'Revenue is paid order value.',
|
||||
tags: ['finance'],
|
||||
refs: [],
|
||||
slRefs: ['orders'],
|
||||
},
|
||||
makeIo().io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await initKtxProject({ projectDir });
|
||||
await seedWikiPage(projectDir);
|
||||
|
||||
const listIo = makeIo();
|
||||
await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe(
|
||||
|
|
@ -137,53 +117,11 @@ describe('runKtxKnowledge', () => {
|
|||
data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] },
|
||||
meta: { command: 'wiki search' },
|
||||
});
|
||||
|
||||
const readIo = makeIo();
|
||||
await expect(
|
||||
runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local', json: true }, readIo.io),
|
||||
).resolves.toBe(0);
|
||||
expect(JSON.parse(readIo.stdout())).toMatchObject({
|
||||
kind: 'wiki.page',
|
||||
data: {
|
||||
key: 'metrics-revenue',
|
||||
summary: 'Revenue',
|
||||
content: 'Revenue is paid order value.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects slash-delimited write keys with a flat-key suggestion', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
const writeIo = makeIo();
|
||||
await expect(
|
||||
runKtxKnowledge(
|
||||
{
|
||||
command: 'write',
|
||||
projectDir,
|
||||
key: 'orbit/company-overview',
|
||||
scope: 'GLOBAL',
|
||||
userId: 'local',
|
||||
summary: 'Orbit',
|
||||
content: 'Orbit overview.',
|
||||
tags: [],
|
||||
refs: [],
|
||||
slRefs: [],
|
||||
},
|
||||
writeIo.io,
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(writeIo.stderr()).toContain(
|
||||
'Invalid wiki key "orbit/company-overview". Wiki keys must be flat; use "orbit-company-overview".',
|
||||
);
|
||||
expect(writeIo.stdout()).toBe('');
|
||||
});
|
||||
|
||||
it('explains empty search results for a project without wiki pages', async () => {
|
||||
const projectDir = join(tempDir, 'empty-project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
|
|
@ -197,25 +135,14 @@ describe('runKtxKnowledge', () => {
|
|||
|
||||
it('uses configured embeddings for semantic wiki search', async () => {
|
||||
const projectDir = join(tempDir, 'semantic-project');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
|
||||
await expect(
|
||||
runKtxKnowledge(
|
||||
{
|
||||
command: 'write',
|
||||
projectDir,
|
||||
key: 'active-contract-arr-open-tickets',
|
||||
scope: 'GLOBAL',
|
||||
userId: 'local',
|
||||
summary: 'Active Contract ARR Ranked by Open Support Ticket Count',
|
||||
content: 'Accounts ranked by annual recurring contract value and support ticket load.',
|
||||
tags: ['historic-sql'],
|
||||
refs: [],
|
||||
slRefs: [],
|
||||
},
|
||||
makeIo().io,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
await initKtxProject({ projectDir });
|
||||
await seedWikiPage(projectDir, {
|
||||
key: 'active-contract-arr-open-tickets',
|
||||
summary: 'Active Contract ARR Ranked by Open Support Ticket Count',
|
||||
content: 'Accounts ranked by annual recurring contract value and support ticket load.',
|
||||
tags: ['historic-sql'],
|
||||
slRefs: [],
|
||||
});
|
||||
|
||||
const searchIo = makeIo();
|
||||
await expect(
|
||||
|
|
|
|||
|
|
@ -5,20 +5,16 @@ import {
|
|||
} from '@ktx/context';
|
||||
import { loadKtxProject } from '@ktx/context/project';
|
||||
import {
|
||||
type LocalKnowledgeScope,
|
||||
type LocalKnowledgeSearchResult,
|
||||
type LocalKnowledgeSummary,
|
||||
listLocalKnowledgePages,
|
||||
readLocalKnowledgePage,
|
||||
searchLocalKnowledgePages,
|
||||
writeLocalKnowledgePage,
|
||||
} from '@ktx/context/wiki';
|
||||
import { resolveOutputMode } from './io/mode.js';
|
||||
import { printList, type PrintListColumn, writeJsonResult } from './io/print-list.js';
|
||||
import { printList, type PrintListColumn } from './io/print-list.js';
|
||||
|
||||
export type KtxKnowledgeArgs =
|
||||
| { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean }
|
||||
| { command: 'read'; projectDir: string; key: string; userId: string; json?: boolean }
|
||||
| {
|
||||
command: 'search';
|
||||
projectDir: string;
|
||||
|
|
@ -27,18 +23,6 @@ export type KtxKnowledgeArgs =
|
|||
output?: string;
|
||||
json?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
| {
|
||||
command: 'write';
|
||||
projectDir: string;
|
||||
key: string;
|
||||
scope: LocalKnowledgeScope;
|
||||
userId: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
refs: string[];
|
||||
slRefs: string[];
|
||||
};
|
||||
|
||||
type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
|
||||
|
|
@ -104,25 +88,6 @@ export async function runKtxKnowledge(
|
|||
});
|
||||
return 0;
|
||||
}
|
||||
if (args.command === 'read') {
|
||||
const page = await readLocalKnowledgePage(project, { key: args.key, userId: args.userId });
|
||||
if (!page) {
|
||||
throw new Error(`Wiki page "${args.key}" was not found`);
|
||||
}
|
||||
if (args.json) {
|
||||
writeJsonResult(io, {
|
||||
kind: 'wiki.page',
|
||||
data: page,
|
||||
meta: { command: 'wiki read' },
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
io.stdout.write(`# ${page.key}\n\n`);
|
||||
io.stdout.write(`Scope: ${page.scope}\n`);
|
||||
io.stdout.write(`Summary: ${page.summary}\n\n`);
|
||||
io.stdout.write(`${page.content}\n`);
|
||||
return 0;
|
||||
}
|
||||
if (args.command === 'search') {
|
||||
const results = await searchLocalKnowledgePages(project, {
|
||||
query: args.query,
|
||||
|
|
@ -153,18 +118,6 @@ export async function runKtxKnowledge(
|
|||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
const write = await writeLocalKnowledgePage(project, {
|
||||
key: args.key,
|
||||
scope: args.scope,
|
||||
userId: args.userId,
|
||||
summary: args.summary,
|
||||
content: args.content,
|
||||
tags: args.tags,
|
||||
refs: args.refs,
|
||||
slRefs: args.slRefs,
|
||||
});
|
||||
io.stdout.write(`Wrote ${write.path}\n`);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ describe('CLI local ingest adapters', () => {
|
|||
await writeProject(
|
||||
tempDir,
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -71,7 +70,6 @@ describe('CLI local ingest adapters', () => {
|
|||
await writeProject(
|
||||
tempDir,
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -103,7 +101,6 @@ describe('CLI local ingest adapters', () => {
|
|||
await writeProject(
|
||||
tempDir,
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' bq:',
|
||||
' driver: bigquery',
|
||||
|
|
@ -136,7 +133,6 @@ describe('CLI local ingest adapters', () => {
|
|||
await writeProject(
|
||||
tempDir,
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' sf:',
|
||||
' driver: snowflake',
|
||||
|
|
@ -172,7 +168,6 @@ describe('CLI local ingest adapters', () => {
|
|||
await writeProject(
|
||||
tempDir,
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' bq:',
|
||||
' driver: bigquery',
|
||||
|
|
|
|||
|
|
@ -39,11 +39,10 @@ describe('createKtxCliScanConnector', () => {
|
|||
});
|
||||
|
||||
it('creates a native sqlite connector from standalone config', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -61,11 +60,10 @@ describe('createKtxCliScanConnector', () => {
|
|||
});
|
||||
|
||||
it('passes canonical BigQuery YAML scan limits through to the connector', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: bigquery',
|
||||
|
|
@ -94,12 +92,11 @@ describe('createKtxCliScanConnector', () => {
|
|||
expect(bigQueryMock.constructorInputs[0]).not.toHaveProperty('maxBytesBilled');
|
||||
});
|
||||
|
||||
it('throws for structural daemon-only fallback configs', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
it('rejects daemon-only fallback driver configs at config parse time', async () => {
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: duckdb',
|
||||
|
|
@ -108,19 +105,17 @@ describe('createKtxCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" uses driver "duckdb", which has no native standalone KTX scan connector',
|
||||
await expect(loadKtxProject({ projectDir: tempDir })).rejects.toThrow(
|
||||
/connections\.warehouse\.driver:.*Invalid discriminator value/,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a clear error when the connection block has no driver field', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
it('rejects connection blocks with no driver field at config parse time', async () => {
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' type: postgres',
|
||||
|
|
@ -129,10 +124,9 @@ describe('createKtxCliScanConnector', () => {
|
|||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
const project = await loadKtxProject({ projectDir: tempDir });
|
||||
|
||||
await expect(createKtxCliScanConnector(project, 'warehouse')).rejects.toThrow(
|
||||
'Connection "warehouse" has no `driver` field in ktx.yaml',
|
||||
await expect(loadKtxProject({ projectDir: tempDir })).rejects.toThrow(
|
||||
/connections\.warehouse\.driver:.*Invalid discriminator value/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { runKtxCli, type KtxCliDeps } from './index.js';
|
|||
|
||||
async function makeFixtureProject(prefix: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
await writeFile(join(dir, 'ktx.yaml'), 'project: project-dir-fixture\n', 'utf-8');
|
||||
await writeFile(join(dir, 'ktx.yaml'), '{}\n', 'utf-8');
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ describe('project directory defaults', () => {
|
|||
const projectDir = join(root, 'warehouse');
|
||||
const nestedDir = join(projectDir, 'nested', 'deeper');
|
||||
await mkdir(nestedDir, { recursive: true });
|
||||
await writeFile(join(projectDir, 'ktx.yaml'), 'project: warehouse\n', 'utf-8');
|
||||
await writeFile(join(projectDir, 'ktx.yaml'), '{}\n', 'utf-8');
|
||||
const expectedProjectDir = await realpath(projectDir);
|
||||
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ describe('resolveKtxProjectDir', () => {
|
|||
const project = join(tempDir, 'warehouse');
|
||||
const nested = join(project, 'nested', 'deeper');
|
||||
await mkdir(nested, { recursive: true });
|
||||
await writeFile(join(project, 'ktx.yaml'), 'project: warehouse\n', 'utf-8');
|
||||
await writeFile(join(project, 'ktx.yaml'), '{}\n', 'utf-8');
|
||||
|
||||
expect(resolveKtxProjectDir({ env: {}, cwd: nested })).toBe(resolve(project));
|
||||
expect(findNearestKtxProjectDir(nested)).toBe(resolve(project));
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function projectWithConnections(connections: KtxProjectConfig['connections']): K
|
|||
return {
|
||||
projectDir: '/tmp/project',
|
||||
config: {
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
...buildDefaultKtxProjectConfig(),
|
||||
connections,
|
||||
},
|
||||
};
|
||||
|
|
@ -51,7 +51,7 @@ function deepReadyProject(
|
|||
connections: KtxProjectConfig['connections'],
|
||||
relationshipsEnabled = true,
|
||||
): KtxPublicIngestProject {
|
||||
const config = buildDefaultKtxProjectConfig('warehouse');
|
||||
const config = buildDefaultKtxProjectConfig();
|
||||
return {
|
||||
projectDir: '/tmp/project',
|
||||
config: {
|
||||
|
|
@ -85,7 +85,7 @@ describe('buildPublicIngestPlan', () => {
|
|||
it('plans warehouse connections as scan targets and source connections as source ingest targets', () => {
|
||||
const project = projectWithConnections({
|
||||
warehouse: { driver: 'postgres' },
|
||||
prod_metabase: { driver: 'metabase' },
|
||||
prod_metabase: { driver: 'metabase', api_url: 'https://metabase.example.com' },
|
||||
docs: { driver: 'notion' },
|
||||
});
|
||||
|
||||
|
|
@ -745,7 +745,7 @@ describe('runKtxPublicIngest', () => {
|
|||
const io = makeIo();
|
||||
const project = projectWithConnections({
|
||||
warehouse: { driver: 'postgres' },
|
||||
prod_metabase: { driver: 'metabase' },
|
||||
prod_metabase: { driver: 'metabase', api_url: 'https://metabase.example.com' },
|
||||
});
|
||||
const runScan = vi.fn(async () => 1);
|
||||
const runIngest = vi.fn(async () => 0);
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('runs structural scans and prints a dev-friendly plain summary', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(
|
||||
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
|
||||
runId: 'scan-run-1',
|
||||
|
|
@ -377,7 +377,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('passes managed daemon options to local ingest adapters when no explicit daemon URL is set', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const createLocalIngestAdapters = vi.fn(() => []);
|
||||
const runLocalScan = vi.fn(
|
||||
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
|
||||
|
|
@ -421,7 +421,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('explains warnings, capability gaps, and relationships in human scan summaries', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(
|
||||
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
|
||||
runId: 'scan-run-1',
|
||||
|
|
@ -472,7 +472,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('prints review-only relationship summaries and validation capability warnings', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const reviewOnlyReport: KtxScanReport = {
|
||||
...reportWithAttention,
|
||||
capabilityGaps: [],
|
||||
|
|
@ -525,7 +525,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('passes a scan progress port and prints TTY progress messages', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
|
||||
await input.progress?.update(0.15, 'Inspecting database schema');
|
||||
await input.progress?.update(0.55, 'Semantic layer comparison found 5 changes across 18 tables');
|
||||
|
|
@ -572,7 +572,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('uses injected structured progress without requiring TTY progress output', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const progressEvents: Array<{ progress: number; message?: string; transient?: boolean }> = [];
|
||||
const structuredProgress = {
|
||||
async update(progress: number, message?: string, options?: { transient?: boolean }) {
|
||||
|
|
@ -674,7 +674,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('flushes transient TTY progress messages before printing scan failures', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
|
||||
await input.progress?.update(0.42, 'Generating descriptions 3/35 tables', { transient: true });
|
||||
throw new Error('scan failed');
|
||||
|
|
@ -711,7 +711,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('does not print live progress messages for non-TTY output', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(async (input: RunLocalScanOptions): Promise<LocalScanRunResult> => {
|
||||
await input.progress?.update(0.15, 'Inspecting database schema');
|
||||
return {
|
||||
|
|
@ -747,7 +747,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('uses terminal-aware visual styling only for TTY output', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(
|
||||
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
|
||||
runId: 'scan-run-1',
|
||||
|
|
@ -807,7 +807,7 @@ describe('runKtxScan', () => {
|
|||
});
|
||||
|
||||
it('honors NO_COLOR for TTY scan summaries', async () => {
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
const runLocalScan = vi.fn(
|
||||
async (_input: RunLocalScanOptions): Promise<LocalScanRunResult> => ({
|
||||
runId: 'scan-run-1',
|
||||
|
|
@ -853,11 +853,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('passes native CLI adapters into local scan runs for mysql configs', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: mysql',
|
||||
|
|
@ -901,11 +900,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('creates a native connector for standalone relationship scans', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-relationships-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -955,11 +953,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('routes standalone postgres scans through the native connector before daemon fallback', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-postgres-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1021,11 +1018,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('passes native CLI adapters into local scan runs for clickhouse configs', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-clickhouse-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: clickhouse',
|
||||
|
|
@ -1072,11 +1068,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('passes native CLI adapters into local scan runs for sqlserver configs', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-sqlserver-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlserver',
|
||||
|
|
@ -1138,11 +1133,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('passes native CLI adapters into local scan runs for bigquery configs', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-bigquery-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: bigquery',
|
||||
|
|
@ -1203,11 +1197,10 @@ describe('runKtxScan', () => {
|
|||
|
||||
it('passes native CLI adapters into local scan runs for snowflake configs', async () => {
|
||||
const tempProject = await mkdtemp(join(tmpdir(), 'ktx-scan-cli-native-snowflake-'));
|
||||
await initKtxProject({ projectDir: tempProject, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempProject });
|
||||
await writeFile(
|
||||
join(tempProject, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: snowflake',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ describe('setup agents', () => {
|
|||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-agents-'));
|
||||
await mkdir(join(tempDir, '.ktx', 'agents'), { recursive: true });
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ type ReadyProjectOverrides = Omit<Partial<KtxProjectConfig>, 'ingest' | 'llm' |
|
|||
};
|
||||
|
||||
async function writeReadyProject(projectDir: string, overrides: ReadyProjectOverrides = {}) {
|
||||
const defaults = buildDefaultKtxProjectConfig('revenue');
|
||||
const defaults = buildDefaultKtxProjectConfig();
|
||||
const readyConfig: KtxProjectConfig = {
|
||||
...defaults,
|
||||
setup: { database_connection_ids: ['warehouse'] },
|
||||
|
|
@ -595,7 +595,6 @@ describe('setup context build state', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'connections: {}',
|
||||
'llm:',
|
||||
' provider:',
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe('setup databases step', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-databases-'));
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -242,7 +242,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -575,7 +574,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -622,7 +620,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -770,7 +767,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -815,7 +811,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -864,7 +859,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -936,7 +930,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1010,7 +1003,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1079,7 +1071,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1146,7 +1137,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1646,7 +1636,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1939,7 +1928,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -2019,7 +2007,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' analytics:',
|
||||
' driver: bigquery',
|
||||
|
|
@ -2074,7 +2061,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -2123,7 +2109,6 @@ describe('setup databases step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ describe('setup embeddings step', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-embeddings-'));
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -446,11 +446,10 @@ describe('setup embeddings step', () => {
|
|||
|
||||
it('preserves already completed embeddings setup when no embedding args request changes', async () => {
|
||||
await mkdir(join(tempDir, '.ktx'), { recursive: true });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse', force: true });
|
||||
await initKtxProject({ projectDir: tempDir, force: true });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ describe('setup Anthropic model step', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-models-'));
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir: tempDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -1049,11 +1049,10 @@ describe('setup Anthropic model step', () => {
|
|||
|
||||
it('preserves already completed llm setup when no model args request changes', async () => {
|
||||
await mkdir(join(tempDir, '.ktx'), { recursive: true });
|
||||
await initKtxProject({ projectDir: tempDir, projectName: 'warehouse', force: true });
|
||||
await initKtxProject({ projectDir: tempDir, force: true });
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
|
|
@ -1099,7 +1098,6 @@ describe('setup Anthropic model step', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
|
|
|
|||
|
|
@ -76,11 +76,10 @@ describe('setup project step', () => {
|
|||
|
||||
it('loads an existing project with --existing and drops config setup progress', async () => {
|
||||
const projectDir = join(tempDir, 'warehouse');
|
||||
await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
await initKtxProject({ projectDir });
|
||||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -196,7 +195,7 @@ describe('setup project step', () => {
|
|||
expect.objectContaining({ message: `Create KTX project at ${projectDir}?` }),
|
||||
);
|
||||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
expect(result.status === 'ready' ? result.project.config.project : '').toBe('ktx-project');
|
||||
expect(result.status === 'ready' ? result.project.configPath : '').toBe(join(projectDir, 'ktx.yaml'));
|
||||
expect(testIo.stdout()).toContain(`│ KTX will create:\n│ ${projectDir}`);
|
||||
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { join, resolve } from 'node:path';
|
||||
import {
|
||||
initKtxProject,
|
||||
type KtxLocalProject,
|
||||
|
|
@ -156,7 +156,7 @@ async function persistProjectStep(project: KtxLocalProject): Promise<KtxLocalPro
|
|||
|
||||
async function createProject(projectDir: string, deps: KtxSetupProjectDeps): Promise<KtxLocalProject> {
|
||||
const initProject = deps.initProject ?? initKtxProject;
|
||||
const initialized = await initProject({ projectDir, projectName: basename(projectDir) || 'ktx-project' });
|
||||
const initialized = await initProject({ projectDir });
|
||||
return await persistProjectStep(initialized);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ describe('setup sources step', () => {
|
|||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-sources-'));
|
||||
projectDir = join(tempDir, 'project');
|
||||
await initKtxProject({ projectDir, projectName: 'sources' });
|
||||
await initKtxProject({ projectDir });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -1024,6 +1024,8 @@ describe('setup sources step', () => {
|
|||
databaseMappings: { '1': 'warehouse' },
|
||||
syncEnabled: { '1': true },
|
||||
syncMode: 'ALL',
|
||||
selections: { collections: [], items: [] },
|
||||
defaultTagNames: [],
|
||||
},
|
||||
},
|
||||
deps: {
|
||||
|
|
@ -1181,6 +1183,8 @@ describe('setup sources step', () => {
|
|||
databaseMappings: { '1': 'warehouse' },
|
||||
syncEnabled: { '1': true },
|
||||
syncMode: 'ALL',
|
||||
selections: { collections: [], items: [] },
|
||||
defaultTagNames: [],
|
||||
},
|
||||
});
|
||||
const testPrompts = prompts({
|
||||
|
|
|
|||
|
|
@ -451,6 +451,8 @@ function buildMetabaseConnection(args: KtxSetupSourcesArgs): KtxProjectConnectio
|
|||
databaseMappings: { [String(args.metabaseDatabaseId)]: args.sourceWarehouseConnectionId },
|
||||
syncEnabled: { [String(args.metabaseDatabaseId)]: true },
|
||||
syncMode: 'ALL',
|
||||
selections: { collections: [], items: [] },
|
||||
defaultTagNames: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
|
|
@ -109,7 +108,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'llm:',
|
||||
' provider:',
|
||||
...fixture.providerLines,
|
||||
|
|
@ -129,7 +127,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -162,7 +159,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -183,7 +179,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -206,7 +201,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections:',
|
||||
|
|
@ -230,7 +224,7 @@ describe('setup status', () => {
|
|||
|
||||
it('reports agent status from the install manifest', async () => {
|
||||
await mkdir(join(tempDir, '.ktx', 'agents'), { recursive: true });
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
await writeFile(
|
||||
join(tempDir, '.ktx/agents/install-manifest.json'),
|
||||
JSON.stringify(
|
||||
|
|
@ -256,7 +250,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -309,7 +302,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -319,7 +311,7 @@ describe('setup status', () => {
|
|||
' url: env:DATABASE_URL',
|
||||
' metabase:',
|
||||
' driver: metabase',
|
||||
' url: env:METABASE_URL',
|
||||
' api_url: https://metabase.example.test',
|
||||
' api_key_ref: env:METABASE_API_KEY',
|
||||
' warehouse_connection_id: warehouse',
|
||||
'llm:',
|
||||
|
|
@ -370,7 +362,7 @@ describe('setup status', () => {
|
|||
});
|
||||
|
||||
it('prints the readiness checklist for an existing project', async () => {
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
|
||||
const rendered = formatKtxSetupStatus(await readKtxSetupStatus(tempDir));
|
||||
|
||||
|
|
@ -503,7 +495,7 @@ describe('setup status', () => {
|
|||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
|
|
@ -589,7 +581,7 @@ describe('setup status', () => {
|
|||
});
|
||||
|
||||
it('lets Back from new project creation return to the first setup intent menu', async () => {
|
||||
const existingConfig = 'project: revenue\nconnections: {}\n';
|
||||
const existingConfig = 'connections: {}\n';
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), existingConfig, 'utf-8');
|
||||
|
||||
const entryChoices = ['new-project', 'exit'];
|
||||
|
|
@ -645,7 +637,7 @@ describe('setup status', () => {
|
|||
const existingProjectDir = join(tempDir, 'existing');
|
||||
const newProjectDir = join(tempDir, 'fresh');
|
||||
await mkdir(existingProjectDir, { recursive: true });
|
||||
const existingConfig = 'project: revenue\nconnections: {}\n';
|
||||
const existingConfig = 'connections: {}\n';
|
||||
await writeFile(join(existingProjectDir, 'ktx.yaml'), existingConfig, 'utf-8');
|
||||
|
||||
const projectChoices = ['custom', 'create'];
|
||||
|
|
@ -722,7 +714,7 @@ describe('setup status', () => {
|
|||
const existingProjectDir = join(tempDir, 'existing');
|
||||
const newProjectDir = join(tempDir, 'fresh');
|
||||
await mkdir(existingProjectDir, { recursive: true });
|
||||
await writeFile(join(existingProjectDir, 'ktx.yaml'), 'project: revenue\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(existingProjectDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
|
||||
const projectChoices = ['custom', 'create'];
|
||||
const projectPrompts = {
|
||||
|
|
@ -1147,7 +1139,7 @@ describe('setup status', () => {
|
|||
});
|
||||
|
||||
it('lets Back from the first setup step return to the entry menu instead of exiting', async () => {
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'project: test\nconnections: {}\n', 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), 'connections: {}\n', 'utf-8');
|
||||
const testIo = makeIo();
|
||||
|
||||
const entryChoices = ['setup', 'exit'];
|
||||
|
|
@ -1254,7 +1246,7 @@ describe('setup status', () => {
|
|||
it('runs sources after database setup', async () => {
|
||||
const calls: string[] = [];
|
||||
const io = makeIo();
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['project: revenue', 'connections: {}', ''].join('\n'), 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
|
|
@ -1315,7 +1307,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -1374,7 +1365,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
|
|
@ -1430,7 +1420,7 @@ describe('setup status', () => {
|
|||
it('runs context after sources and before agents in full setup', async () => {
|
||||
const calls: string[] = [];
|
||||
const io = makeIo();
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['project: revenue', 'connections: {}', ''].join('\n'), 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
|
|
@ -1543,7 +1533,7 @@ describe('setup status', () => {
|
|||
it('runs agent setup after context succeeds in --agents mode', async () => {
|
||||
const calls: string[] = [];
|
||||
const io = makeIo();
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['project: revenue', 'connections: {}', ''].join('\n'), 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
|
|
@ -1596,7 +1586,7 @@ describe('setup status', () => {
|
|||
projectDir: tempDir,
|
||||
installs: [{ target: 'codex' as const, scope: 'project' as const, mode: 'cli' as const }],
|
||||
}));
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['project: revenue', 'connections: {}', ''].join('\n'), 'utf-8');
|
||||
await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
|
|
@ -1633,7 +1623,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids:',
|
||||
' - warehouse',
|
||||
|
|
@ -1671,7 +1660,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
|
|
@ -1778,7 +1766,6 @@ describe('setup status', () => {
|
|||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: revenue',
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { getLatestLocalIngestStatus, savedMemoryCountsForReport } from '@ktx/context/ingest';
|
||||
import {
|
||||
ktxLocalStateDbPath,
|
||||
|
|
@ -317,7 +317,7 @@ export async function readKtxSetupStatus(projectDir: string): Promise<KtxSetupSt
|
|||
})) ?? [];
|
||||
|
||||
return {
|
||||
project: { path: resolvedProjectDir, ready: true, name: project.config.project },
|
||||
project: { path: resolvedProjectDir, ready: true, name: basename(project.projectDir) || project.projectDir },
|
||||
llm,
|
||||
embeddings,
|
||||
databases: databaseIds.map((connectionId) => ({
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ async function seedSlSource(input: {
|
|||
sourceName?: string;
|
||||
yaml?: string;
|
||||
}): Promise<void> {
|
||||
const project = await initKtxProject({ projectDir: input.projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir: input.projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
`semantic-layer/${input.connectionId ?? 'warehouse'}/${input.sourceName ?? 'orders'}.yaml`,
|
||||
input.yaml ?? ORDERS_YAML,
|
||||
|
|
@ -139,7 +139,7 @@ describe('runKtxSl', () => {
|
|||
|
||||
it('fails validation when a table-backed source declares columns absent from a matching warehouse manifest', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/postgres-warehouse/_schema/orbit_analytics.yaml',
|
||||
`tables:
|
||||
|
|
@ -189,7 +189,7 @@ joins: []
|
|||
|
||||
it('runs sl query and prints SQL output', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
project.config.connections.warehouse = { driver: 'postgres' };
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/orders.yaml',
|
||||
|
|
@ -246,7 +246,7 @@ joins: []
|
|||
|
||||
it('runs sl query from a JSON query file', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
project.config.connections.warehouse = { driver: 'postgres' };
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/orders.yaml',
|
||||
|
|
@ -313,7 +313,7 @@ joins: []
|
|||
|
||||
it('creates default sl query compute through the managed runtime helper', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
project.config.connections.warehouse = { driver: 'postgres' };
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/orders.yaml',
|
||||
|
|
@ -374,7 +374,7 @@ joins: []
|
|||
|
||||
it('executes sl query through the injected query executor', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
project.config.connections.warehouse = { driver: 'postgres', url: 'postgres://example/db' };
|
||||
await project.fileStore.writeFile(
|
||||
'semantic-layer/warehouse/orders.yaml',
|
||||
|
|
@ -459,7 +459,7 @@ joins: []
|
|||
|
||||
it('executes sl query against a local SQLite connection through the default executor', async () => {
|
||||
const projectDir = join(tempDir, 'project');
|
||||
const project = await initKtxProject({ projectDir, projectName: 'warehouse' });
|
||||
const project = await initKtxProject({ projectDir });
|
||||
const dbPath = join(projectDir, 'warehouse.db');
|
||||
const db = new Database(dbPath);
|
||||
db.exec(`
|
||||
|
|
@ -475,7 +475,6 @@ joins: []
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ async function writeSqliteScanConfig(projectDir: string, dbPath: string, enrich
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: warehouse',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: sqlite',
|
||||
|
|
@ -218,7 +217,6 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
await writeFile(
|
||||
join(projectDir, 'ktx.yaml'),
|
||||
[
|
||||
'project: gateway-smoke',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: gateway',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { basename } from 'node:path';
|
||||
import type {
|
||||
KtxConfigIssue,
|
||||
KtxLocalProject,
|
||||
|
|
@ -8,6 +9,7 @@ import type {
|
|||
} from '@ktx/context/project';
|
||||
import type { PostgresPgssProbeResult } from '@ktx/context/ingest';
|
||||
import type { DoctorCheck } from './doctor.js';
|
||||
import { KTX_NEXT_STEP_DIRECT_COMMANDS } from './next-steps.js';
|
||||
|
||||
type ProjectStatusLevel = 'ok' | 'warn' | 'fail';
|
||||
type ProjectVerdict = 'ready' | 'partial' | 'blocked';
|
||||
|
|
@ -68,6 +70,8 @@ interface WarningItem {
|
|||
fix?: string;
|
||||
}
|
||||
|
||||
const PROJECT_READY_COMMANDS = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command);
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -131,7 +135,7 @@ function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): Ll
|
|||
backend,
|
||||
model,
|
||||
status: 'fail',
|
||||
detail: 'no LLM configured — ktx ask will not work',
|
||||
detail: 'no LLM configured; research agent will not run',
|
||||
fix: 'Run: ktx setup (choose an LLM provider)',
|
||||
};
|
||||
}
|
||||
|
|
@ -570,7 +574,7 @@ function buildVerdict(
|
|||
if (llm.status === 'fail') {
|
||||
return {
|
||||
verdict: 'blocked',
|
||||
reason: 'LLM not configured — `ktx ask` will not work.',
|
||||
reason: 'LLM not configured; research agent will not run.',
|
||||
nextActions: ['ktx setup'],
|
||||
};
|
||||
}
|
||||
|
|
@ -604,7 +608,7 @@ function buildVerdict(
|
|||
return {
|
||||
verdict: 'ready',
|
||||
reason: 'Ready.',
|
||||
nextActions: ['ktx scan', 'ktx wiki', 'ktx sl ask "…"'],
|
||||
nextActions: [...PROJECT_READY_COMMANDS],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -650,7 +654,7 @@ export async function buildProjectStatus(project: KtxLocalProject, options: Buil
|
|||
const { verdict, reason, nextActions } = buildVerdict(llm, embeddings, connections, queryHistory, warnings);
|
||||
|
||||
return {
|
||||
projectName: config.project,
|
||||
projectName: basename(project.projectDir) || project.projectDir,
|
||||
projectDir: project.projectDir,
|
||||
config: configStatus,
|
||||
llm,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue