From 6e2a6b76116e54a7e2374c65697aaf7cfce5e527 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 14:34:07 -0700 Subject: [PATCH 01/10] docs: add demo guided tour design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-05-11-demo-guided-tour-design.md | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-11-demo-guided-tour-design.md diff --git a/docs/superpowers/specs/2026-05-11-demo-guided-tour-design.md b/docs/superpowers/specs/2026-05-11-demo-guided-tour-design.md new file mode 100644 index 00000000..34585743 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-demo-guided-tour-design.md @@ -0,0 +1,252 @@ +# Demo Guided Tour — Design Spec + +## Problem + +The "Try KTX with packaged demo data" option in `ktx setup` is completely +disconnected from the real setup wizard. It bypasses all wizard steps, plays +an animated replay in a temp directory, and exits with no bridge to actually +using KTX. Users don't learn the real setup flow and hit a dead end. + +## Solution + +Redesign the demo option as a **guided tour** that walks the user through the +same setup wizard steps with pre-filled, read-only selections. The tour ends +with a real interactive agents step so the user can immediately use the demo +project with their coding agent. + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Implementation strategy | Demo mode flag on existing wizard steps | Maximum code reuse; wizard changes automatically apply to demo | +| LLM/embeddings steps | Skipped | Not relevant to pre-packaged demo data | +| Database selection | PostgreSQL (read-only card) | Pre-filled, matches demo dataset | +| Context sources | dbt, Metabase, Notion (read-only card) | Pre-filled, matches demo dataset | +| Context build | Replay through real progress visualization | Same spinners, progress bars, status icons as real build | +| Agents step | Real interactive step | User actually connects their agent | +| Project location | Temp directory (`/tmp/ktx-demo-{hex}`) | Frictionless, no directory prompt | +| Navigation | Enter to advance, Escape to go back | Consistent with rest of wizard | + +## Flow + +``` +Entry menu: "Try KTX with packaged demo data" + │ + ▼ +Create demo project in /tmp/ktx-demo-{hex} +Copy pre-packaged assets (demo DB, replay, context artifacts) + │ + ▼ +┌────────────────────────────────────────────────────────────────┐ +│ Demo banner (persistent, shown on every step) │ +│ │ +│ Demo mode — data has been pre-processed and KTX context is │ +│ already built. This walkthrough illustrates the setup steps. │ +│ Selections are pre-filled and read-only. │ +└────────────────────────────────────────────────────────────────┘ + │ + ▼ +Read-only card: Database connection + ▸ PostgreSQL (demo warehouse) + [Enter → next, Escape → back to entry menu] + │ + ▼ +Read-only card: Context sources + ▸ dbt + ▸ Metabase + ▸ Notion + [Enter → next, Escape → back to database card] + │ + ▼ +Context build replay + Same renderContextBuildView() / repainter as real wizard + Sources: demo-warehouse, dbt, metabase, notion + Replay at slightly faster-than-real pace + Completion summary: business areas, query definitions, knowledge pages + [Enter → next, Escape → back to sources card] + │ + ▼ +Transition message: + "Demo project is ready — let's connect your agent" + │ + ▼ +Interactive agents step (real runKtxSetupAgentsStep()) + User selects agent target, scope, install mode + [Normal interactive navigation; Escape goes back to replay summary] + │ + ▼ +Final summary: + ★ KTX demo is ready + Agent connected, project path shown + ⚠ Temp directory warning + Pointer to `ktx setup` for real data +``` + +## Step Details + +### Demo Banner + +Shown at the top of every read-only step. Uses clack box-drawing style: + +``` +┌ Demo mode — data has been pre-processed and KTX context is already built. +│ This walkthrough illustrates the setup steps. Selections are pre-filled and read-only. +``` + +### Read-Only Step Cards + +Rendered by a shared `renderDemoCard()` helper: + +```typescript +async function renderDemoCard( + title: string, + selections: string[], + io: KtxCliIo, +): Promise<'forward' | 'back'> +``` + +- Renders a clack-style box with title, bullet list of pre-filled selections, + and navigation hint ("Press Enter to continue, Escape to go back") +- Listens for raw keypresses: Enter → `'forward'`, Escape → `'back'` +- Uses same box-drawing characters and colors as `@clack/prompts` + +Card format: + +``` +┌ {title} +│ +│ ▸ {selection 1} +│ ▸ {selection 2} +│ ... +│ +│ Press Enter to continue, Escape to go back +└ +``` + +### Demo Step Sequence + +The demo reuses the main wizard's step loop with these steps: + +```typescript +const demoSteps = ['databases', 'sources', 'context', 'agents']; +``` + +Steps `databases` and `sources` dispatch to `renderDemoCard()` instead of +their real interactive functions when demo mode is active. Step `context` +dispatches to the replay visualization. Step `agents` runs the real +`runKtxSetupAgentsStep()`. + +Back navigation reuses `previousNavigableStepIndex()`. Escaping from the +first step (databases) returns to the entry menu. + +### Context Build Replay + +Uses the same rendering pipeline as the real context build: + +- `renderContextBuildView()` for the progress display +- `createRepainter()` for terminal repainting +- Same spinner frames, progress bars (`████░░░░`), status icons (`✓`, `⠹`, `○`) +- Same source grouping (Primary sources / Context sources) + +Sources shown: + +``` +Primary sources: + ✓ demo-warehouse completed · Xs + +Context sources: + ✓ dbt completed · Xs + ✓ metabase completed · Xs + ✓ notion completed · Xs +``` + +Replay timing: events from the pre-packaged replay file are played back at +a slightly faster pace than real-time (compressed to feel brisk but not +instant). + +Completion summary uses the existing format: + +``` +★ KTX finished ingesting your data + + ✓ Analyzed X business areas + ✓ Reconciled — 0 conflicts + + KTX created: + 📊 X query definitions + 📝 X knowledge pages + + Press Enter to continue, Escape to go back +``` + +The exact counts and artifact names come from the pre-packaged demo results +(to be provided by the user as improved demo data). + +### Agents Step Transition + +A brief message bridges from the read-only tour to the interactive step: + +``` +┌ Demo project is ready — let's connect your agent +│ +│ Your KTX context has been built with demo data. +│ Select an agent to start using it. +└ +``` + +Then `runKtxSetupAgentsStep()` runs with the demo project directory, +normal interactive prompts enabled. + +### Final Summary + +``` +★ KTX demo is ready + + Your agent is connected to a demo KTX project. + + ⚠ This project is in a temporary directory and will be + cleaned up by your system. To set up KTX with your own + data, run: ktx setup + + Project: /tmp/ktx-demo-a1b2c3 +``` + +If the user skips the agents step, replace the first line with manual +agent connection instructions (`ktx setup --agents --project-dir /tmp/...`). + +## Implementation Approach + +Thread a `demoMode` flag through the main setup loop in `setup.ts`. When +active: + +1. Skip `models` and `embeddings` steps entirely +2. Replace `databases` and `sources` step dispatch with `renderDemoCard()` +3. Replace `context` step dispatch with replay visualization +4. Run `agents` step normally +5. Show demo-specific completion summary instead of ready menu + +The `renderDemoCard()` helper is a new function in a new file +(e.g. `setup-demo-cards.ts`) that handles read-only card rendering and +keypress listening. + +The context build replay reuses existing `renderContextBuildView()` and +`createRepainter()` from `context-build-view.ts`, fed with events from +the pre-packaged replay file at an accelerated playback rate. + +## Files Changed + +| File | Change | +|------|--------| +| `packages/cli/src/setup.ts` | Add `demoMode` flag to setup loop; skip models/embeddings; dispatch to demo cards for databases/sources; show demo banner; demo completion summary | +| `packages/cli/src/setup-demo-cards.ts` | New file: `renderDemoCard()` helper, demo banner renderer, demo step definitions | +| `packages/cli/src/setup-context.ts` | Support replay mode for demo: feed pre-packaged events at accelerated pace through existing progress view | +| `packages/cli/src/demo.ts` | Remove or simplify `runKtxSetupDemoFromEntryMenu()` — now dispatches to the main setup loop with `demoMode: true` | +| `packages/cli/src/demo-assets.ts` | Update asset list if new demo data is provided; ensure demo project setup writes valid `ktx.yaml` for agent use | + +## Open Items + +- **Demo data**: User will provide improved pre-packaged results (Postgres, + dbt, Metabase, Notion). Current demo assets may need updating. +- **Replay speed**: Exact acceleration factor TBD — should feel brisk but + give users time to read source names and status transitions. Start with + ~2x real-time and adjust. From e52713ca1ece99ee690aeae72c3ae4726957d643 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 15:26:26 -0700 Subject: [PATCH 02/10] docs: add demo guided tour implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-05-11-demo-guided-tour.md | 813 ++++++++++++++++++ 1 file changed, 813 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-11-demo-guided-tour.md diff --git a/docs/superpowers/plans/2026-05-11-demo-guided-tour.md b/docs/superpowers/plans/2026-05-11-demo-guided-tour.md new file mode 100644 index 00000000..3204e111 --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-demo-guided-tour.md @@ -0,0 +1,813 @@ +# Demo Guided Tour Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the disconnected "Try KTX with packaged demo data" flow with a guided tour that walks users through the same setup wizard steps using pre-filled, read-only selections, then connects their agent to the populated demo project. + +**Architecture:** A new `setup-demo-tour.ts` module owns the demo tour flow. It renders read-only cards (database, sources), a simulated context build replay using the existing `renderContextBuildView` + `createRepainter` pipeline from `context-build-view.ts`, then hands off to the real `runKtxSetupAgentsStep`. The entry point in `setup.ts` (`runKtxSetupDemoFromEntryMenu`) is rewired to call this new module instead of `runKtxDemo`. + +**Tech Stack:** TypeScript (ESM), Node.js raw stdin for keypress handling, existing `@clack/prompts` visual patterns, vitest for tests. + +--- + +### Task 1: Create `setup-demo-tour.ts` with keypress utility and banner + +**Files:** +- Create: `packages/cli/src/setup-demo-tour.ts` +- Test: `packages/cli/src/setup-demo-tour.test.ts` + +- [ ] **Step 1: Write the failing test for `renderDemoBanner`** + +```typescript +// packages/cli/src/setup-demo-tour.test.ts +import { describe, expect, it } from 'vitest'; +import { renderDemoBanner } from './setup-demo-tour.js'; + +describe('renderDemoBanner', () => { + it('includes demo mode explanation', () => { + const output = renderDemoBanner(); + expect(output).toContain('Demo mode'); + expect(output).toContain('pre-processed'); + expect(output).toContain('read-only'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: FAIL — module not found + +- [ ] **Step 3: Implement `renderDemoBanner` and `waitForDemoNavigation`** + +```typescript +// packages/cli/src/setup-demo-tour.ts +import type { KtxCliIo } from './cli-runtime.js'; +import { KtxSetupExitError } from './setup-interrupt.js'; + +const ESC = String.fromCharCode(0x1b); + +function cyan(text: string): string { + return `${ESC}[36m${text}${ESC}[39m`; +} + +function dim(text: string): string { + return `${ESC}[2m${text}${ESC}[22m`; +} + +export function renderDemoBanner(): string { + const lines = [ + '', + `┌ ${cyan('Demo mode')} — data has been pre-processed and KTX context is already built.`, + `│ This walkthrough illustrates the setup steps. Selections are pre-filled and read-only.`, + '', + ]; + return lines.join('\n'); +} + +export async function waitForDemoNavigation( + stdin: NodeJS.ReadStream = process.stdin, +): Promise<'forward' | 'back'> { + return new Promise((resolve, reject) => { + const wasRaw = stdin.isRaw; + if (stdin.setRawMode) stdin.setRawMode(true); + stdin.resume(); + + const onData = (data: Buffer) => { + const key = data.toString(); + if (key === '\r' || key === '\n') { + cleanup(); + resolve('forward'); + } else if (key === '\x1b') { + cleanup(); + resolve('back'); + } else if (key === '\x03') { + cleanup(); + reject(new KtxSetupExitError()); + } + }; + + const cleanup = () => { + stdin.off('data', onData); + if (stdin.setRawMode) stdin.setRawMode(wasRaw ?? false); + }; + + stdin.on('data', onData); + }); +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts +git commit -m "feat(cli): add demo tour banner and keypress navigation utility" +``` + +--- + +### Task 2: Add `renderDemoCard` function + +**Files:** +- Modify: `packages/cli/src/setup-demo-tour.ts` +- Modify: `packages/cli/src/setup-demo-tour.test.ts` + +- [ ] **Step 1: Write the failing test for `renderDemoCard`** + +Append to the test file: + +```typescript +import { renderDemoCardContent } from './setup-demo-tour.js'; + +describe('renderDemoCardContent', () => { + it('renders a card with title and selections', () => { + const output = renderDemoCardContent('Database connection', ['PostgreSQL (demo warehouse)']); + expect(output).toContain('Database connection'); + expect(output).toContain('PostgreSQL (demo warehouse)'); + expect(output).toContain('Press Enter to continue'); + expect(output).toContain('Escape to go back'); + }); + + it('renders multiple selections', () => { + const output = renderDemoCardContent('Context sources', ['dbt', 'Metabase', 'Notion']); + expect(output).toContain('dbt'); + expect(output).toContain('Metabase'); + expect(output).toContain('Notion'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: FAIL — `renderDemoCardContent` not exported + +- [ ] **Step 3: Implement `renderDemoCardContent` and `renderDemoCard`** + +Add to `setup-demo-tour.ts`: + +```typescript +export function renderDemoCardContent(title: string, selections: string[]): string { + const lines = [ + `┌ ${title}`, + '│', + ...selections.map((s) => `│ ${cyan('▸')} ${s}`), + '│', + `│ ${dim('Press Enter to continue, Escape to go back')}`, + '└', + '', + ]; + return lines.join('\n'); +} + +export async function renderDemoCard( + title: string, + selections: string[], + io: KtxCliIo, + stdin?: NodeJS.ReadStream, + waitNav?: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'>, +): Promise<'forward' | 'back'> { + io.stdout.write(renderDemoBanner()); + io.stdout.write(renderDemoCardContent(title, selections)); + const nav = waitNav ?? waitForDemoNavigation; + return nav(stdin); +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts +git commit -m "feat(cli): add demo tour read-only card rendering" +``` + +--- + +### Task 3: Add demo context build replay animation + +**Files:** +- Modify: `packages/cli/src/setup-demo-tour.ts` +- Modify: `packages/cli/src/setup-demo-tour.test.ts` + +- [ ] **Step 1: Write the failing test for demo replay event sequence** + +Append to the test file: + +```typescript +import { buildDemoReplayTimeline, DEMO_REPLAY_TARGETS } from './setup-demo-tour.js'; + +describe('buildDemoReplayTimeline', () => { + it('produces events for all four demo targets', () => { + const events = buildDemoReplayTimeline(); + const connectionIds = new Set(events.map((e) => e.connectionId)); + expect(connectionIds).toEqual(new Set(['demo-warehouse', 'dbt', 'metabase', 'notion'])); + }); + + it('ends with all targets done', () => { + const events = buildDemoReplayTimeline(); + const lastByConnection = new Map(); + for (const e of events) { + lastByConnection.set(e.connectionId, e.status); + } + for (const status of lastByConnection.values()) { + expect(status).toBe('done'); + } + }); + + it('events are sorted by delayMs', () => { + const events = buildDemoReplayTimeline(); + for (let i = 1; i < events.length; i++) { + expect(events[i]!.delayMs).toBeGreaterThanOrEqual(events[i - 1]!.delayMs); + } + }); +}); + +describe('DEMO_REPLAY_TARGETS', () => { + it('has one primary source and three context sources', () => { + expect(DEMO_REPLAY_TARGETS.primarySources).toHaveLength(1); + expect(DEMO_REPLAY_TARGETS.contextSources).toHaveLength(3); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: FAIL — exports not found + +- [ ] **Step 3: Implement replay timeline and target definitions** + +Add to `setup-demo-tour.ts`: + +```typescript +import type { KtxPublicIngestPlanTarget } from './public-ingest.js'; +import type { ContextBuildTargetState, ContextBuildViewState } from './context-build-view.js'; + +export interface DemoReplayEvent { + delayMs: number; + connectionId: string; + status: 'running' | 'done'; + detailLine: string | null; + summaryText: string | null; +} + +function createDemoTarget(connectionId: string, operation: 'scan' | 'source-ingest', driver: string): KtxPublicIngestPlanTarget { + return { + connectionId, + driver, + operation, + debugCommand: `ktx ${operation === 'scan' ? 'scan' : 'ingest'} ${connectionId}`, + steps: operation === 'scan' ? ['scan'] : ['source-ingest'], + }; +} + +const primaryTarget = createDemoTarget('demo-warehouse', 'scan', 'postgres'); +const dbtTarget = createDemoTarget('dbt', 'source-ingest', 'dbt'); +const metabaseTarget = createDemoTarget('metabase', 'source-ingest', 'metabase'); +const notionTarget = createDemoTarget('notion', 'source-ingest', 'notion'); + +function createTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState { + return { + target, + status: 'queued', + detailLine: null, + summaryText: null, + startedAt: null, + elapsedMs: 0, + }; +} + +export const DEMO_REPLAY_TARGETS = { + primarySources: [primaryTarget], + contextSources: [dbtTarget, metabaseTarget, notionTarget], +}; + +export function buildDemoReplayTimeline(): DemoReplayEvent[] { + return [ + { delayMs: 0, connectionId: 'demo-warehouse', status: 'running', detailLine: 'scanning...', summaryText: null }, + { delayMs: 600, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] scanning...', summaryText: null }, + { delayMs: 1200, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: 'completed' }, + { delayMs: 1200, connectionId: 'dbt', status: 'running', detailLine: 'ingesting...', summaryText: null }, + { delayMs: 1800, connectionId: 'dbt', status: 'running', detailLine: '[60%] ingesting...', summaryText: null }, + { delayMs: 2200, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: 'completed' }, + { delayMs: 2200, connectionId: 'metabase', status: 'running', detailLine: 'ingesting...', summaryText: null }, + { delayMs: 2800, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: 'completed' }, + { delayMs: 2800, connectionId: 'notion', status: 'running', detailLine: 'ingesting...', summaryText: null }, + { delayMs: 3400, connectionId: 'notion', status: 'done', detailLine: null, summaryText: 'completed' }, + ]; +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 5: Implement `runDemoContextReplay` animation driver** + +Add to `setup-demo-tour.ts`: + +```typescript +import { renderContextBuildView, createRepainter } from './context-build-view.js'; + +export async function runDemoContextReplay( + io: KtxCliIo, + stdin?: NodeJS.ReadStream, +): Promise<'forward' | 'back'> { + const repainter = createRepainter(io); + const timeline = buildDemoReplayTimeline(); + + const state: ContextBuildViewState = { + primarySources: DEMO_REPLAY_TARGETS.primarySources.map((t) => createTargetState(t)), + contextSources: DEMO_REPLAY_TARGETS.contextSources.map((t) => createTargetState(t)), + frame: 0, + startedAt: Date.now(), + totalElapsedMs: 0, + }; + + const allTargets = [...state.primarySources, ...state.contextSources]; + const targetMap = new Map(allTargets.map((t) => [t.target.connectionId, t])); + let eventIndex = 0; + const startTime = Date.now(); + const FRAME_MS = 120; + + await new Promise((resolve) => { + const interval = setInterval(() => { + const elapsed = Date.now() - startTime; + state.frame += 1; + state.totalElapsedMs = elapsed; + + while (eventIndex < timeline.length && timeline[eventIndex]!.delayMs <= elapsed) { + const event = timeline[eventIndex]!; + const target = targetMap.get(event.connectionId); + if (target) { + target.status = event.status; + target.detailLine = event.detailLine; + target.summaryText = event.summaryText; + if (event.status === 'running' && target.startedAt === null) { + target.startedAt = Date.now(); + } + if (event.status === 'done') { + target.elapsedMs = target.startedAt ? Date.now() - target.startedAt : 0; + } + } + eventIndex += 1; + } + + for (const t of allTargets) { + if (t.status === 'running' && t.startedAt !== null) { + t.elapsedMs = Date.now() - t.startedAt; + } + } + + repainter.paint(renderContextBuildView(state, { styled: io.stdout.isTTY ?? false, showHint: false })); + + if (eventIndex >= timeline.length && allTargets.every((t) => t.status === 'done')) { + clearInterval(interval); + resolve(); + } + }, FRAME_MS); + }); + + io.stdout.write(renderDemoContextCompletionSummary()); + return waitForDemoNavigation(stdin); +} + +function renderDemoContextCompletionSummary(): string { + const lines = [ + '', + `${cyan('★')} KTX finished ingesting demo data`, + '', + ' Placeholder — final counts will come from pre-packaged demo results.', + '', + ` ${dim('Press Enter to continue, Escape to go back')}`, + '', + ]; + return lines.join('\n'); +} +``` + +Note: `renderDemoContextCompletionSummary` is a placeholder that will be updated when +the user provides the real pre-packaged demo data. The summary counts (business areas, +query definitions, knowledge pages) will be populated from those assets. + +- [ ] **Step 6: Run tests and type-check** + +Run: `pnpm --filter @ktx/cli run type-check && pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 7: Commit** + +```bash +git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts +git commit -m "feat(cli): add demo context build replay animation" +``` + +--- + +### Task 4: Add transition message and completion summary + +**Files:** +- Modify: `packages/cli/src/setup-demo-tour.ts` +- Modify: `packages/cli/src/setup-demo-tour.test.ts` + +- [ ] **Step 1: Write the failing tests** + +Append to test file: + +```typescript +import { renderDemoAgentTransition, renderDemoCompletionSummary } from './setup-demo-tour.js'; + +describe('renderDemoAgentTransition', () => { + it('includes transition message about connecting agent', () => { + const output = renderDemoAgentTransition(); + expect(output).toContain('Demo project is ready'); + expect(output).toContain('connect your agent'); + }); +}); + +describe('renderDemoCompletionSummary', () => { + it('includes project path and temp warning', () => { + const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', true); + expect(output).toContain('/tmp/ktx-demo-abc123'); + expect(output).toContain('temporary'); + expect(output).toContain('ktx setup'); + }); + + it('shows manual agent instructions when agent not installed', () => { + const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', false); + expect(output).toContain('ktx setup --agents'); + }); + + it('shows success message when agent installed', () => { + const output = renderDemoCompletionSummary('/tmp/ktx-demo-abc123', true); + expect(output).toContain('agent is connected'); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: FAIL — exports not found + +- [ ] **Step 3: Implement transition and completion rendering** + +Add to `setup-demo-tour.ts`: + +```typescript +export function renderDemoAgentTransition(): string { + const lines = [ + '', + `┌ Demo project is ready — let's connect your agent`, + '│', + '│ Your KTX context has been built with demo data.', + '│ Select an agent to start using it.', + '└', + '', + ]; + return lines.join('\n'); +} + +export function renderDemoCompletionSummary(projectDir: string, agentInstalled: boolean): string { + const lines = [ + '', + `${cyan('★')} KTX demo is ready`, + '', + ]; + + if (agentInstalled) { + lines.push(' Your agent is connected to a demo KTX project.'); + } else { + lines.push(' Demo project created. Connect an agent to start using it:'); + lines.push(` $ ktx setup --agents --project-dir ${projectDir}`); + } + + lines.push( + '', + ` ${dim('⚠')} This project is in a temporary directory and will be`, + ` cleaned up by your system. To set up KTX with your own`, + ' data, run: ktx setup', + '', + ` Project: ${projectDir}`, + '', + ); + return lines.join('\n'); +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts +git commit -m "feat(cli): add demo tour transition and completion summary" +``` + +--- + +### Task 5: Implement `runDemoTour` orchestrator + +**Files:** +- Modify: `packages/cli/src/setup-demo-tour.ts` +- Modify: `packages/cli/src/setup-demo-tour.test.ts` + +- [ ] **Step 1: Write the failing test for the orchestrator** + +Append to test file: + +```typescript +import { vi } from 'vitest'; +import type { KtxSetupAgentsResult } from './setup-agents.js'; +import { runDemoTour } from './setup-demo-tour.js'; + +describe('runDemoTour', () => { + function createMockIo() { + const chunks: string[] = []; + return { + io: { + stdout: { isTTY: true, columns: 80, write: (chunk: string) => { chunks.push(chunk); } }, + stderr: { write: () => {} }, + }, + chunks, + }; + } + + it('returns 0 on successful tour with agent installed', async () => { + const { io } = createMockIo(); + const mockAgents = vi.fn<() => Promise>().mockResolvedValue({ + status: 'ready', + projectDir: '/tmp/test', + installs: [{ target: 'claude-code' as const, scope: 'project' as const, mode: 'both' as const }], + }); + + const navigation = vi.fn<() => Promise<'forward' | 'back'>>().mockResolvedValue('forward'); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { agents: mockAgents, waitForNavigation: navigation, skipReplayAnimation: true }, + ); + expect(result).toBe(0); + expect(mockAgents).toHaveBeenCalled(); + }); + + it('handles back navigation from first step', async () => { + const { io } = createMockIo(); + const navigation = vi.fn<() => Promise<'forward' | 'back'>>().mockResolvedValue('back'); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { waitForNavigation: navigation, skipReplayAnimation: true }, + ); + expect(result).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: FAIL — `runDemoTour` not exported or wrong signature + +- [ ] **Step 3: Implement `runDemoTour`** + +Add to `setup-demo-tour.ts`: + +```typescript +import { defaultDemoProjectDir, ensureSeededDemoProject } from './demo-assets.js'; +import type { KtxSetupAgentsResult } from './setup-agents.js'; +import { runKtxSetupAgentsStep } from './setup-agents.js'; + +type DemoStep = 'databases' | 'sources' | 'context' | 'agents'; + +const DEMO_STEPS: DemoStep[] = ['databases', 'sources', 'context', 'agents']; + +export interface DemoTourDeps { + agents?: (args: Parameters[0], io: KtxCliIo) => Promise; + waitForNavigation?: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'>; + ensureProject?: typeof ensureSeededDemoProject; + skipReplayAnimation?: boolean; +} + +export async function runDemoTour( + args: { inputMode: 'auto' | 'disabled' }, + io: KtxCliIo, + deps: DemoTourDeps = {}, +): Promise { + const waitNav = deps.waitForNavigation ?? waitForDemoNavigation; + const ensureProject = deps.ensureProject ?? ensureSeededDemoProject; + + const projectDir = defaultDemoProjectDir(); + await ensureProject({ projectDir }); + + let stepIndex = 0; + + while (stepIndex < DEMO_STEPS.length) { + const step = DEMO_STEPS[stepIndex]!; + let direction: 'forward' | 'back'; + + if (step === 'databases') { + direction = await renderDemoCard('Database connection', ['PostgreSQL (demo warehouse)'], io, undefined, waitNav); + } else if (step === 'sources') { + direction = await renderDemoCard('Context sources', ['dbt', 'Metabase', 'Notion'], io, undefined, waitNav); + } else if (step === 'context') { + io.stdout.write(renderDemoBanner()); + if (deps.skipReplayAnimation) { + direction = await waitNav(); + } else { + direction = await runDemoContextReplay(io); + } + } else { + io.stdout.write(renderDemoAgentTransition()); + const agentsRunner = deps.agents ?? runKtxSetupAgentsStep; + const agentsResult = await agentsRunner( + { + projectDir, + inputMode: args.inputMode, + yes: false, + agents: true, + scope: 'project', + mode: 'both', + skipAgents: false, + }, + io, + ); + const agentInstalled = agentsResult.status === 'ready'; + if (agentsResult.status === 'back') { + direction = 'back'; + } else { + io.stdout.write(renderDemoCompletionSummary(projectDir, agentInstalled)); + return 0; + } + } + + if (direction === 'back') { + if (stepIndex === 0) return 0; + stepIndex -= 1; + } else { + stepIndex += 1; + } + } + + return 0; +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `pnpm --filter @ktx/cli run test -- --testPathPattern setup-demo-tour` +Expected: PASS + +- [ ] **Step 5: Run type-check** + +Run: `pnpm --filter @ktx/cli run type-check` +Expected: PASS — all types align with existing interfaces + +- [ ] **Step 6: Commit** + +```bash +git add packages/cli/src/setup-demo-tour.ts packages/cli/src/setup-demo-tour.test.ts +git commit -m "feat(cli): add runDemoTour orchestrator with step navigation" +``` + +--- + +### Task 6: Wire up in `setup.ts` + +**Files:** +- Modify: `packages/cli/src/setup.ts` + +- [ ] **Step 1: Read the current `runKtxSetupDemoFromEntryMenu` function** + +Read `packages/cli/src/setup.ts` and locate `runKtxSetupDemoFromEntryMenu` (around lines 218-233). + +Current implementation: +```typescript +async function runKtxSetupDemoFromEntryMenu( + args: Extract, + io: KtxCliIo, + deps: KtxSetupDeps, +): Promise { + const runner = deps.demo ?? (await import('./demo.js')).runKtxDemo; + return await runner( + { + command: 'seeded', + projectDir: defaultDemoProjectDir(), + outputMode: 'viz', + inputMode: args.inputMode, + }, + io, + ); +} +``` + +- [ ] **Step 2: Replace with demo tour call** + +Replace the function body to call `runDemoTour`: + +```typescript +async function runKtxSetupDemoFromEntryMenu( + args: Extract, + io: KtxCliIo, + deps: KtxSetupDeps, +): Promise { + const { runDemoTour } = await import('./setup-demo-tour.js'); + return await runDemoTour( + { inputMode: args.inputMode }, + io, + { agents: deps.agents }, + ); +} +``` + +- [ ] **Step 3: Update imports — remove unused `defaultDemoProjectDir` import if no longer needed elsewhere in setup.ts** + +Check if `defaultDemoProjectDir` is used elsewhere in `setup.ts`. If it's only used +in `runKtxSetupDemoFromEntryMenu`, remove the import. If used elsewhere, keep it. + +Also check if the `KtxDemoArgs` import is still needed. If `runKtxSetupDemoFromEntryMenu` +was the only consumer of `deps.demo` with that type, it may now be unused. Keep the +`demo` slot in `KtxSetupDeps` for backwards compatibility but it will no longer be +called from the entry menu path. + +- [ ] **Step 4: Run type-check and tests** + +Run: `pnpm --filter @ktx/cli run type-check && pnpm --filter @ktx/cli run test` +Expected: PASS — existing tests continue to work, demo tour is now wired in + +- [ ] **Step 5: Commit** + +```bash +git add packages/cli/src/setup.ts +git commit -m "feat(cli): wire demo tour into setup entry menu" +``` + +--- + +### Task 7: End-to-end verification + +**Files:** +- None (verification only) + +- [ ] **Step 1: Run full test suite** + +Run: `pnpm --filter @ktx/cli run test 2>&1 | tee /tmp/ktx-demo-tour-test.log` +Expected: All tests pass. Check the output for any regressions. + +- [ ] **Step 2: Run type-check across workspace** + +Run: `pnpm run type-check` +Expected: PASS + +- [ ] **Step 3: Run pre-commit checks if available** + +Run: `pnpm run check` (if configured) +Expected: PASS + +- [ ] **Step 4: Manual smoke test (if TTY available)** + +Run: `pnpm --filter @ktx/cli run build && node packages/cli/dist/cli.js setup` + +1. Select "Try KTX with packaged demo data" +2. Verify demo banner appears with full explanation text +3. Verify "Database connection" card shows with "PostgreSQL (demo warehouse)" +4. Press Enter → verify "Context sources" card shows with dbt, Metabase, Notion +5. Press Escape → verify you go back to database card +6. Press Enter twice → verify context build replay animation runs +7. Verify completion summary appears after replay +8. Press Enter → verify agents step prompt appears (interactive) +9. Press Escape all the way back → verify you return to entry menu + +- [ ] **Step 5: Final commit if any adjustments needed** + +```bash +git add -A +git commit -m "fix(cli): demo tour adjustments from smoke test" +``` + +--- + +## Open Seams for Demo Data + +When the user provides the real pre-packaged demo results, update these locations: + +1. **`renderDemoContextCompletionSummary()`** in `setup-demo-tour.ts` — replace placeholder text with actual counts (business areas, query definitions, knowledge pages) from the demo data +2. **`buildDemoReplayTimeline()`** in `setup-demo-tour.ts` — adjust timing and progress details to match the real ingestion profile +3. **`demo-assets.ts`** — update `REQUIRED_SEEDED_ASSET_PATHS` and `demoConfig()` if the demo dataset changes from SQLite/Orbit to Postgres/dbt/Metabase/Notion +4. **Pre-packaged asset files** in `packages/cli/assets/demo/` — replace with the new demo dataset From 3677193027ab3790e3aca64d285607827eead75b Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 15:52:25 -0700 Subject: [PATCH 03/10] feat(cli): add demo guided tour module with rendering, keypress, and replay Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.test.ts | 151 ++++++++++++ packages/cli/src/setup-demo-tour.ts | 302 +++++++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 packages/cli/src/setup-demo-tour.test.ts create mode 100644 packages/cli/src/setup-demo-tour.ts diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/src/setup-demo-tour.test.ts new file mode 100644 index 00000000..a57aace8 --- /dev/null +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from 'vitest'; +import { + buildDemoReplayTimeline, + DEMO_REPLAY_TARGETS, + renderDemoAgentTransition, + renderDemoBanner, + renderDemoCardContent, + renderDemoCompletionSummary, +} from './setup-demo-tour.js'; + +/** Strip ANSI escape sequences for plain-text assertions. */ +function stripAnsi(text: string): string { + return text.replace(/\x1b\[[0-9;]*m/g, ''); +} + +describe('renderDemoBanner', () => { + it('contains "Demo mode"', () => { + const plain = stripAnsi(renderDemoBanner()); + expect(plain).toContain('Demo mode'); + }); + + it('mentions pre-processed data', () => { + const plain = stripAnsi(renderDemoBanner()); + expect(plain).toContain('pre-processed'); + }); + + it('mentions read-only', () => { + const plain = stripAnsi(renderDemoBanner()); + expect(plain).toContain('read-only'); + }); +}); + +describe('renderDemoCardContent', () => { + it('contains the title', () => { + const plain = stripAnsi(renderDemoCardContent('Database connection', ['Postgres'])); + expect(plain).toContain('Database connection'); + }); + + it('contains each selection', () => { + const plain = stripAnsi(renderDemoCardContent('Sources', ['dbt', 'metabase'])); + expect(plain).toContain('dbt'); + expect(plain).toContain('metabase'); + }); + + it('contains navigation hints', () => { + const plain = stripAnsi(renderDemoCardContent('Title', ['a'])); + expect(plain).toContain('Press Enter to continue'); + expect(plain).toContain('Escape to go back'); + }); + + it('works with multiple selections', () => { + const result = renderDemoCardContent('Pick', ['one', 'two', 'three']); + const plain = stripAnsi(result); + expect(plain).toContain('one'); + expect(plain).toContain('two'); + expect(plain).toContain('three'); + // Each selection gets a ▸ bullet + const bullets = (plain.match(/▸/g) ?? []).length; + expect(bullets).toBe(3); + }); +}); + +describe('renderDemoAgentTransition', () => { + it('contains "Demo project is ready"', () => { + const plain = stripAnsi(renderDemoAgentTransition()); + expect(plain).toContain('Demo project is ready'); + }); + + it('mentions connecting an agent', () => { + const plain = stripAnsi(renderDemoAgentTransition()); + expect(plain).toContain('connect your agent'); + }); +}); + +describe('renderDemoCompletionSummary', () => { + const projectDir = '/tmp/ktx-demo-123'; + + it('includes the project path', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); + expect(plain).toContain(projectDir); + }); + + it('includes a temp directory warning', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); + expect(plain).toContain('temporary demo directory'); + }); + + it('points to ktx setup for real data', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); + expect(plain).toContain('ktx setup'); + }); + + it('shows agent-connected message when installed', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); + expect(plain).toContain('agent is connected'); + }); + + it('shows manual instructions when agent not installed', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, false)); + expect(plain).toContain('agent not installed'); + expect(plain).toContain('--agents'); + expect(plain).toContain(`--project-dir ${projectDir}`); + }); +}); + +describe('buildDemoReplayTimeline', () => { + const timeline = buildDemoReplayTimeline(); + const connectionIds = new Set(timeline.map((e) => e.connectionId)); + + it('produces events for all 4 targets', () => { + expect(connectionIds.size).toBe(4); + expect(connectionIds).toContain('demo-warehouse'); + expect(connectionIds).toContain('dbt'); + expect(connectionIds).toContain('metabase'); + expect(connectionIds).toContain('notion'); + }); + + it('all targets end as done', () => { + for (const id of connectionIds) { + const events = timeline.filter((e) => e.connectionId === id); + const last = events[events.length - 1]; + expect(last.status).toBe('done'); + } + }); + + it('events are sorted by delayMs', () => { + for (let i = 1; i < timeline.length; i++) { + expect(timeline[i].delayMs).toBeGreaterThanOrEqual(timeline[i - 1].delayMs); + } + }); +}); + +describe('DEMO_REPLAY_TARGETS', () => { + it('has 1 primary source', () => { + expect(DEMO_REPLAY_TARGETS.primarySources).toHaveLength(1); + }); + + it('has 3 context sources', () => { + expect(DEMO_REPLAY_TARGETS.contextSources).toHaveLength(3); + }); + + it('primary source is a scan operation', () => { + expect(DEMO_REPLAY_TARGETS.primarySources[0].operation).toBe('scan'); + }); + + it('context sources are source-ingest operations', () => { + for (const source of DEMO_REPLAY_TARGETS.contextSources) { + expect(source.operation).toBe('source-ingest'); + } + }); +}); diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts new file mode 100644 index 00000000..d17d138d --- /dev/null +++ b/packages/cli/src/setup-demo-tour.ts @@ -0,0 +1,302 @@ +import type { KtxCliIo } from './cli-runtime.js'; +import type { + ContextBuildTargetState, + ContextBuildViewState, +} from './context-build-view.js'; +import { createRepainter, renderContextBuildView } from './context-build-view.js'; +import type { KtxPublicIngestPlanTarget } from './public-ingest.js'; +import { KtxSetupExitError } from './setup-interrupt.js'; + +// --------------------------------------------------------------------------- +// ANSI helpers (internal) +// --------------------------------------------------------------------------- + +const ESC = String.fromCharCode(0x1b); + +function cyan(text: string): string { + return `${ESC}[36m${text}${ESC}[39m`; +} + +function dim(text: string): string { + return `${ESC}[2m${text}${ESC}[22m`; +} + +// --------------------------------------------------------------------------- +// Demo target helpers (internal) +// --------------------------------------------------------------------------- + +function createDemoTarget( + connectionId: string, + operation: 'scan' | 'source-ingest', + driver: string, +): KtxPublicIngestPlanTarget { + const adapter = operation === 'source-ingest' ? driver : undefined; + return { + connectionId, + driver, + operation, + ...(adapter ? { adapter } : {}), + debugCommand: `ktx setup context build --target ${connectionId}`, + steps: operation === 'scan' + ? ['scan', 'enrich', 'memory-update'] + : ['source-ingest', 'enrich', 'memory-update'], + }; +} + +function createTargetState(target: KtxPublicIngestPlanTarget): ContextBuildTargetState { + return { + target, + status: 'queued', + detailLine: null, + summaryText: null, + startedAt: null, + elapsedMs: 0, + }; +} + +// --------------------------------------------------------------------------- +// Pure rendering functions +// --------------------------------------------------------------------------- + +export function renderDemoBanner(): string { + const lines = [ + '', + `┌ ${cyan('Demo mode')} — data has been pre-processed and KTX context is already built.`, + '│ This walkthrough illustrates the setup steps. Selections are pre-filled and read-only.', + ]; + return lines.join('\n'); +} + +export function renderDemoCardContent(title: string, selections: string[]): string { + const lines = [ + `┌ ${title}`, + '│', + ...selections.map((s) => `│ ${cyan('▸')} ${s}`), + '│', + `│ ${dim('Press Enter to continue, Escape to go back')}`, + '└', + ]; + return lines.join('\n'); +} + +export function renderDemoAgentTransition(): string { + const lines = [ + '┌ Demo project is ready — let\'s connect your agent', + '│', + '│ Your KTX context has been built with demo data.', + '│ Select an agent to start using it.', + '└', + ]; + return lines.join('\n'); +} + +export function renderDemoCompletionSummary(projectDir: string, agentInstalled: boolean): string { + const lines: string[] = ['']; + + if (agentInstalled) { + lines.push('┌ Your agent is connected to a demo KTX project.'); + } else { + lines.push('┌ Demo project created (agent not installed).'); + lines.push('│'); + lines.push(`│ To connect an agent manually, run:`); + lines.push(`│ ${cyan(`ktx setup --agents --project-dir ${projectDir}`)}`); + } + + lines.push('│'); + lines.push(`│ ${dim('This is a temporary demo directory — data will not persist across sessions.')}`); + lines.push(`│ Run ${cyan('ktx setup')} to connect your own data sources.`); + lines.push('│'); + lines.push(`│ Project: ${projectDir}`); + lines.push('└'); + + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Keypress navigation +// --------------------------------------------------------------------------- + +export async function waitForDemoNavigation( + stdin?: NodeJS.ReadStream, +): Promise<'forward' | 'back'> { + const input = stdin ?? process.stdin; + const hadRawMode = input.isRaw ?? false; + + return new Promise<'forward' | 'back'>((resolve, reject) => { + if (typeof input.setRawMode === 'function') { + input.setRawMode(true); + } + input.resume(); + + const cleanup = () => { + input.off('data', onData); + if (typeof input.setRawMode === 'function') { + input.setRawMode(hadRawMode); + } + }; + + const onData = (data: Buffer) => { + const char = data.toString(); + if (char === '\r' || char === '\n') { + cleanup(); + resolve('forward'); + } else if (char === '\x1b') { + cleanup(); + resolve('back'); + } else if (char === '\x03') { + cleanup(); + reject(new KtxSetupExitError()); + } + }; + + input.on('data', onData); + }); +} + +// --------------------------------------------------------------------------- +// Interactive card +// --------------------------------------------------------------------------- + +export async function renderDemoCard( + title: string, + selections: string[], + io: KtxCliIo, + stdin?: NodeJS.ReadStream, + waitNav: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'> = waitForDemoNavigation, +): Promise<'forward' | 'back'> { + io.stdout.write(renderDemoBanner() + '\n\n'); + io.stdout.write(renderDemoCardContent(title, selections) + '\n'); + return waitNav(stdin); +} + +// --------------------------------------------------------------------------- +// Context build replay +// --------------------------------------------------------------------------- + +export interface DemoReplayEvent { + delayMs: number; + connectionId: string; + status: 'running' | 'done'; + detailLine: string | null; + summaryText: string | null; +} + +export const DEMO_REPLAY_TARGETS = { + primarySources: [ + createDemoTarget('demo-warehouse', 'scan', 'postgres'), + ], + contextSources: [ + createDemoTarget('dbt', 'source-ingest', 'dbt'), + createDemoTarget('metabase', 'source-ingest', 'metabase'), + createDemoTarget('notion', 'source-ingest', 'notion'), + ], +} as const; + +export function buildDemoReplayTimeline(): DemoReplayEvent[] { + return [ + // demo-warehouse: scan + { delayMs: 0, connectionId: 'demo-warehouse', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 600, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] Scanning tables...', summaryText: null }, + { delayMs: 1200, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: '12 tables' }, + // dbt + { delayMs: 1200, connectionId: 'dbt', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 1800, connectionId: 'dbt', status: 'running', detailLine: '[60%] Ingesting models...', summaryText: null }, + { delayMs: 2200, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: '8 models' }, + // metabase + { delayMs: 2200, connectionId: 'metabase', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 2800, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: '5 dashboards' }, + // notion + { delayMs: 2800, connectionId: 'notion', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 3400, connectionId: 'notion', status: 'done', detailLine: null, summaryText: '3 pages' }, + ]; +} + +function renderDemoContextCompletionSummary(): string { + const lines = [ + '', + '┌ Context build complete', + '│', + '│ All sources have been processed.', + '│', + `│ ${dim('Press Enter to continue, Escape to go back')}`, + '└', + ]; + return lines.join('\n'); +} + +export async function runDemoContextReplay( + io: KtxCliIo, + stdin?: NodeJS.ReadStream, +): Promise<'forward' | 'back'> { + const allPrimary = DEMO_REPLAY_TARGETS.primarySources.map(createTargetState); + const allContext = DEMO_REPLAY_TARGETS.contextSources.map(createTargetState); + + const state: ContextBuildViewState = { + primarySources: allPrimary, + contextSources: allContext, + frame: 0, + startedAt: Date.now(), + totalElapsedMs: 0, + }; + + const allTargets = [...allPrimary, ...allContext]; + const timeline = buildDemoReplayTimeline(); + + const repainter = createRepainter(io); + const paint = () => repainter.paint(renderContextBuildView(state, { styled: true })); + + paint(); + + let eventIndex = 0; + const startTime = Date.now(); + + await new Promise((resolve) => { + const frameInterval = setInterval(() => { + const elapsed = Date.now() - startTime; + state.frame++; + state.totalElapsedMs = elapsed; + + // Apply all events up to the current elapsed time + while (eventIndex < timeline.length && timeline[eventIndex].delayMs <= elapsed) { + const event = timeline[eventIndex]; + const target = allTargets.find((t) => t.target.connectionId === event.connectionId); + if (target) { + target.status = event.status; + target.detailLine = event.detailLine; + if (event.summaryText !== null) { + target.summaryText = event.summaryText; + } + if (event.status === 'running' && target.startedAt === null) { + target.startedAt = Date.now(); + } + if (event.status === 'done') { + target.elapsedMs = target.startedAt !== null ? Date.now() - target.startedAt : 0; + } + } + eventIndex++; + } + + // Update running target elapsed times + for (const t of allTargets) { + if (t.status === 'running' && t.startedAt !== null) { + t.elapsedMs = Date.now() - t.startedAt; + } + } + + paint(); + + // Check if all events have been applied + if (eventIndex >= timeline.length) { + clearInterval(frameInterval); + resolve(); + } + }, 120); + }); + + // Final paint with all done + paint(); + + // Show completion summary and wait for navigation + io.stdout.write(renderDemoContextCompletionSummary() + '\n'); + return waitForDemoNavigation(stdin); +} From 8cb6324655e498f8a3aa9c07c9c385f5b51e7160 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 15:56:44 -0700 Subject: [PATCH 04/10] feat(cli): add runDemoTour orchestrator and wire into setup entry menu Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.test.ts | 117 ++++++++++++++++++++++- packages/cli/src/setup-demo-tour.ts | 82 ++++++++++++++++ packages/cli/src/setup.ts | 13 +-- 3 files changed, 202 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/src/setup-demo-tour.test.ts index a57aace8..f45b42f6 100644 --- a/packages/cli/src/setup-demo-tour.test.ts +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import type { KtxSetupAgentsResult } from './setup-agents.js'; import { buildDemoReplayTimeline, DEMO_REPLAY_TARGETS, @@ -6,6 +7,7 @@ import { renderDemoBanner, renderDemoCardContent, renderDemoCompletionSummary, + runDemoTour, } from './setup-demo-tour.js'; /** Strip ANSI escape sequences for plain-text assertions. */ @@ -149,3 +151,116 @@ describe('DEMO_REPLAY_TARGETS', () => { } }); }); + +describe('runDemoTour', () => { + function createMockIo() { + const chunks: string[] = []; + return { + io: { + stdout: { isTTY: true, columns: 80, write: (chunk: string) => { chunks.push(chunk); } }, + stderr: { write: () => {} }, + }, + chunks, + }; + } + + it('returns 0 on successful tour with agent installed', async () => { + const { io, chunks } = createMockIo(); + const mockAgents = vi.fn().mockResolvedValue({ + status: 'ready', + projectDir: '/tmp/test', + installs: [{ target: 'claude-code', scope: 'project', mode: 'both' }], + } satisfies KtxSetupAgentsResult); + + const navigation = vi.fn().mockResolvedValue('forward'); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { + agents: mockAgents, + waitForNavigation: navigation, + skipReplayAnimation: true, + ensureProject: vi.fn().mockResolvedValue({ projectDir: '/tmp/test' }), + }, + ); + expect(result).toBe(0); + expect(mockAgents).toHaveBeenCalled(); + // Should have rendered completion summary + const allOutput = chunks.join(''); + expect(allOutput).toContain('agent is connected'); + }); + + it('handles back navigation from first step by exiting', async () => { + const { io } = createMockIo(); + const navigation = vi.fn().mockResolvedValue('back'); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { + waitForNavigation: navigation, + skipReplayAnimation: true, + ensureProject: vi.fn().mockResolvedValue({ projectDir: '/tmp/test' }), + }, + ); + expect(result).toBe(0); + // Navigation called once for databases step, then exits + expect(navigation).toHaveBeenCalledTimes(1); + }); + + it('goes back from sources to databases', async () => { + const { io } = createMockIo(); + let callCount = 0; + const navigation = vi.fn().mockImplementation(() => { + callCount++; + // First call (databases): forward + // Second call (sources): back + // Third call (databases again): back (exit) + if (callCount === 1) return Promise.resolve('forward'); + return Promise.resolve('back'); + }); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { + waitForNavigation: navigation, + skipReplayAnimation: true, + ensureProject: vi.fn().mockResolvedValue({ projectDir: '/tmp/test' }), + }, + ); + expect(result).toBe(0); + expect(navigation).toHaveBeenCalledTimes(3); + }); + + it('handles agent step returning back', async () => { + const { io } = createMockIo(); + let navCount = 0; + const navigation = vi.fn().mockImplementation(() => { + navCount++; + // Forward through databases, sources, context + // Then back from context (after agents returns back) + // Then back from sources, then back from databases (exit) + if (navCount <= 3) return Promise.resolve('forward'); + return Promise.resolve('back'); + }); + + const mockAgents = vi.fn().mockResolvedValue({ + status: 'back', + projectDir: '/tmp/test', + } satisfies KtxSetupAgentsResult); + + const result = await runDemoTour( + { inputMode: 'auto' }, + io, + { + agents: mockAgents, + waitForNavigation: navigation, + skipReplayAnimation: true, + ensureProject: vi.fn().mockResolvedValue({ projectDir: '/tmp/test' }), + }, + ); + expect(result).toBe(0); + }); +}); diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index d17d138d..75bdf8b9 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -4,7 +4,10 @@ import type { ContextBuildViewState, } from './context-build-view.js'; import { createRepainter, renderContextBuildView } from './context-build-view.js'; +import { defaultDemoProjectDir, ensureSeededDemoProject } from './demo-assets.js'; import type { KtxPublicIngestPlanTarget } from './public-ingest.js'; +import type { KtxSetupAgentsResult } from './setup-agents.js'; +import { runKtxSetupAgentsStep } from './setup-agents.js'; import { KtxSetupExitError } from './setup-interrupt.js'; // --------------------------------------------------------------------------- @@ -300,3 +303,82 @@ export async function runDemoContextReplay( io.stdout.write(renderDemoContextCompletionSummary() + '\n'); return waitForDemoNavigation(stdin); } + +// --------------------------------------------------------------------------- +// Demo tour orchestrator +// --------------------------------------------------------------------------- + +type DemoStep = 'databases' | 'sources' | 'context' | 'agents'; + +const DEMO_STEPS: DemoStep[] = ['databases', 'sources', 'context', 'agents']; + +export interface DemoTourDeps { + agents?: (args: Parameters[0], io: KtxCliIo) => Promise; + waitForNavigation?: (stdin?: NodeJS.ReadStream) => Promise<'forward' | 'back'>; + ensureProject?: typeof ensureSeededDemoProject; + skipReplayAnimation?: boolean; +} + +export async function runDemoTour( + args: { inputMode: 'auto' | 'disabled' }, + io: KtxCliIo, + deps: DemoTourDeps = {}, +): Promise { + const waitNav = deps.waitForNavigation ?? waitForDemoNavigation; + const ensureProject = deps.ensureProject ?? ensureSeededDemoProject; + + const projectDir = defaultDemoProjectDir(); + await ensureProject({ projectDir, force: false }); + + let stepIndex = 0; + + while (stepIndex < DEMO_STEPS.length) { + const step = DEMO_STEPS[stepIndex]!; + let direction: 'forward' | 'back'; + + if (step === 'databases') { + direction = await renderDemoCard('Database connection', ['PostgreSQL (demo warehouse)'], io, undefined, waitNav); + } else if (step === 'sources') { + direction = await renderDemoCard('Context sources', ['dbt', 'Metabase', 'Notion'], io, undefined, waitNav); + } else if (step === 'context') { + io.stdout.write(renderDemoBanner() + '\n\n'); + if (deps.skipReplayAnimation) { + direction = await waitNav(); + } else { + direction = await runDemoContextReplay(io); + } + } else { + // agents step — real interactive + io.stdout.write(renderDemoAgentTransition() + '\n'); + const agentsRunner = deps.agents ?? runKtxSetupAgentsStep; + const agentsResult = await agentsRunner( + { + projectDir, + inputMode: args.inputMode, + yes: false, + agents: true, + scope: 'project', + mode: 'both', + skipAgents: false, + }, + io, + ); + const agentInstalled = agentsResult.status === 'ready'; + if (agentsResult.status === 'back') { + direction = 'back'; + } else { + io.stdout.write(renderDemoCompletionSummary(projectDir, agentInstalled) + '\n'); + return 0; + } + } + + if (direction === 'back') { + if (stepIndex === 0) return 0; + stepIndex -= 1; + } else { + stepIndex += 1; + } + } + + return 0; +} diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts index 5eac2e27..1373517a 100644 --- a/packages/cli/src/setup.ts +++ b/packages/cli/src/setup.ts @@ -4,7 +4,6 @@ import { cancel, isCancel, select } from '@clack/prompts'; import { loadKtxProject } from '@ktx/context/project'; import type { KtxCliIo } from './cli-runtime.js'; import type { KtxDemoArgs } from './demo.js'; -import { defaultDemoProjectDir } from './demo-assets.js'; import { formatSetupNextStepLines } from './next-steps.js'; import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js'; import { @@ -220,15 +219,11 @@ async function runKtxSetupDemoFromEntryMenu( io: KtxCliIo, deps: KtxSetupDeps, ): Promise { - const runner = deps.demo ?? (await import('./demo.js')).runKtxDemo; - return await runner( - { - command: 'seeded', - projectDir: defaultDemoProjectDir(), - outputMode: 'viz', - inputMode: args.inputMode, - }, + const { runDemoTour } = await import('./setup-demo-tour.js'); + return await runDemoTour( + { inputMode: args.inputMode }, io, + { agents: deps.agents }, ); } From 04e9f962af6ff25054644feb2f39c76279982911 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 15:57:45 -0700 Subject: [PATCH 05/10] fix(cli): add star headline to demo completion summary per spec Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.test.ts | 8 +++++-- packages/cli/src/setup-demo-tour.ts | 28 ++++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/src/setup-demo-tour.test.ts index f45b42f6..f484af1e 100644 --- a/packages/cli/src/setup-demo-tour.test.ts +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -84,7 +84,7 @@ describe('renderDemoCompletionSummary', () => { it('includes a temp directory warning', () => { const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); - expect(plain).toContain('temporary demo directory'); + expect(plain).toContain('temporary directory'); }); it('points to ktx setup for real data', () => { @@ -97,9 +97,13 @@ describe('renderDemoCompletionSummary', () => { expect(plain).toContain('agent is connected'); }); + it('includes star headline', () => { + const plain = stripAnsi(renderDemoCompletionSummary(projectDir, true)); + expect(plain).toContain('★ KTX demo is ready'); + }); + it('shows manual instructions when agent not installed', () => { const plain = stripAnsi(renderDemoCompletionSummary(projectDir, false)); - expect(plain).toContain('agent not installed'); expect(plain).toContain('--agents'); expect(plain).toContain(`--project-dir ${projectDir}`); }); diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index 75bdf8b9..37473d34 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -94,23 +94,27 @@ export function renderDemoAgentTransition(): string { } export function renderDemoCompletionSummary(projectDir: string, agentInstalled: boolean): string { - const lines: string[] = ['']; + const lines: string[] = [ + '', + `${cyan('★')} KTX demo is ready`, + '', + ]; if (agentInstalled) { - lines.push('┌ Your agent is connected to a demo KTX project.'); + lines.push(' Your agent is connected to a demo KTX project.'); } else { - lines.push('┌ Demo project created (agent not installed).'); - lines.push('│'); - lines.push(`│ To connect an agent manually, run:`); - lines.push(`│ ${cyan(`ktx setup --agents --project-dir ${projectDir}`)}`); + lines.push(' Demo project created. Connect an agent to start using it:'); + lines.push(` $ ${cyan(`ktx setup --agents --project-dir ${projectDir}`)}`); } - lines.push('│'); - lines.push(`│ ${dim('This is a temporary demo directory — data will not persist across sessions.')}`); - lines.push(`│ Run ${cyan('ktx setup')} to connect your own data sources.`); - lines.push('│'); - lines.push(`│ Project: ${projectDir}`); - lines.push('└'); + lines.push( + '', + ` ${dim('⚠')} This project is in a temporary directory and will be`, + ' cleaned up by your system. To set up KTX with your own', + ' data, run: ktx setup', + '', + ` Project: ${projectDir}`, + ); return lines.join('\n'); } From 6629d69ab4b4a5e6967afff18cbdb6a580dd08e3 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 16:13:30 -0700 Subject: [PATCH 06/10] refactor(cli): rename demo menu option to "Explore a pre-built KTX project" Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup.test.ts | 6 +++--- packages/cli/src/setup.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/setup.test.ts b/packages/cli/src/setup.test.ts index 7cb0d0df..e335591e 100644 --- a/packages/cli/src/setup.test.ts +++ b/packages/cli/src/setup.test.ts @@ -347,10 +347,10 @@ describe('setup status', () => { expect(labels).toEqual([ 'Set up KTX for my data', 'Check setup status', - 'Try KTX with packaged demo data', + 'Explore a pre-built KTX project', 'Exit', ]); - expect(labels.indexOf('Try KTX with packaged demo data')).toBe(labels.length - 2); + expect(labels.indexOf('Explore a pre-built KTX project')).toBe(labels.length - 2); return 'exit'; }); const cancel = vi.fn(); @@ -396,7 +396,7 @@ describe('setup status', () => { 'Create a new KTX project', 'Connect a coding agent to KTX', 'Check setup status', - 'Try KTX with packaged demo data', + 'Explore a pre-built KTX project', 'Exit', ]); return 'exit'; diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts index 1373517a..d72b5bb0 100644 --- a/packages/cli/src/setup.ts +++ b/packages/cli/src/setup.ts @@ -198,13 +198,13 @@ async function runKtxSetupEntryMenu( { value: 'new-project', label: 'Create a new KTX project' }, { value: 'agents', label: 'Connect a coding agent to KTX' }, { value: 'status', label: 'Check setup status' }, - { value: 'demo', label: 'Try KTX with packaged demo data' }, + { value: 'demo', label: 'Explore a pre-built KTX project' }, { value: 'exit', label: 'Exit' }, ] : [ { value: 'setup', label: 'Set up KTX for my data' }, { value: 'status', label: 'Check setup status' }, - { value: 'demo', label: 'Try KTX with packaged demo data' }, + { value: 'demo', label: 'Explore a pre-built KTX project' }, { value: 'exit', label: 'Exit' }, ]; const action = (await prompts.select({ From c75871bd341e2ff2c7f8b2787408b799376207cf Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 16:21:55 -0700 Subject: [PATCH 07/10] fix(cli): slow demo replay animation to half speed (~6.8s total) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index 37473d34..f7b75776 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -203,18 +203,18 @@ export function buildDemoReplayTimeline(): DemoReplayEvent[] { return [ // demo-warehouse: scan { delayMs: 0, connectionId: 'demo-warehouse', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 600, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] Scanning tables...', summaryText: null }, - { delayMs: 1200, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: '12 tables' }, + { delayMs: 1200, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] Scanning tables...', summaryText: null }, + { delayMs: 2400, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: '12 tables' }, // dbt - { delayMs: 1200, connectionId: 'dbt', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 1800, connectionId: 'dbt', status: 'running', detailLine: '[60%] Ingesting models...', summaryText: null }, - { delayMs: 2200, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: '8 models' }, + { delayMs: 2400, connectionId: 'dbt', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 3600, connectionId: 'dbt', status: 'running', detailLine: '[60%] Ingesting models...', summaryText: null }, + { delayMs: 4400, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: '8 models' }, // metabase - { delayMs: 2200, connectionId: 'metabase', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 2800, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: '5 dashboards' }, + { delayMs: 4400, connectionId: 'metabase', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 5600, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: '5 dashboards' }, // notion - { delayMs: 2800, connectionId: 'notion', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 3400, connectionId: 'notion', status: 'done', detailLine: null, summaryText: '3 pages' }, + { delayMs: 5600, connectionId: 'notion', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 6800, connectionId: 'notion', status: 'done', detailLine: null, summaryText: '3 pages' }, ]; } From ebf475e4c171d5332994b5dfdefad6fe4721de28 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 21:51:54 -0700 Subject: [PATCH 08/10] feat(cli): replace fake demo data with real NBC/Orbit project context Replace the placeholder demo assets with real semantic-layer (45 YAML files) and knowledge (28 markdown files) from the fully-built NBC project. Update demo tour cards, replay timeline, and completion summary to reflect real connection names and counts: - PostgreSQL warehouse: 56 tables across 2 schemas - dbt: 34 transformation models - Metabase: 80 dashboard cards - Notion: 9 knowledge pages - 45 semantic layer definitions, 32 knowledge pages created Co-Authored-By: Claude Opus 4.6 (1M context) --- .../demo/orbit/knowledge/global/.gitkeep | 0 .../knowledge/global/activation-policy.md | 18 - .../knowledge/global/arr-contract-first.md | 18 - .../global/customer-communication-policy.md | 44 + .../global/customer-health-scoring.md | 20 - .../knowledge/global/discount-expiration.md | 19 - .../global/internal-test-exclusion.md | 16 - .../global/new-hire-onboarding-policy.md | 47 + .../orbit/knowledge/global/nrr-retention.md | 19 - .../global/orbit-activation-kpi-glossary.md | 65 + ...orbit-activation-policy-change-jan-2026.md | 46 + .../orbit-arr-contract-first-definition.md | 39 + .../global/orbit-company-overview.md | 72 + .../orbit-customer-health-risk-definition.md | 46 + .../orbit-customer-stakeholder-needs.md | 42 + .../global/orbit-customers-source.md | 61 + .../knowledge/global/orbit-dbt-exposures.md | 45 + .../global/orbit-dbt-project-overview.md | 55 + .../knowledge/global/orbit-how-we-work.md | 75 + .../global/orbit-known-product-gaps.md | 47 + .../global/orbit-mart-account-activity.md | 50 + .../global/orbit-mart-account-segments.md | 56 + .../knowledge/global/orbit-mart-arr-daily.md | 47 + .../global/orbit-mart-nrr-quarterly.md | 56 + .../global/orbit-mart-procurement-activity.md | 48 + .../orbit-mart-retention-movement-breakout.md | 56 + .../global/orbit-mart-revenue-daily.md | 57 + .../orbit-metabase-sql-library-patterns.md | 72 + ...orbit-nrr-discount-expiration-treatment.md | 47 + .../orbit-plan-segment-normalization.md | 49 + .../orbit-procurement-qualifying-actions.md | 46 + .../global/orbit-product-design-principles.md | 43 + .../global/orbit-product-review-checklist.md | 44 + ...bit-revenue-gross-to-net-reconciliation.md | 50 + .../knowledge/global/procurement-workflows.md | 17 - .../knowledge/global/revenue-gross-to-net.md | 17 - .../global/sales-ops-cs-handoff-process.md | 58 + .../global/segment-classification.md | 17 - .../knowledge/global/support-escalation.md | 17 - packages/cli/assets/demo/orbit/manifest.json | 6 +- .../assets/demo/orbit/semantic-layer/.gitkeep | 0 .../int_activation_policy_windows.yaml | 27 + .../dbt-main/int_active_contract_arr.yaml | 24 + .../dbt-main/int_customer_health_signals.yaml | 25 + .../int_parent_account_arr_movements.yaml | 49 + .../int_procurement_qualifying_actions.yaml | 27 + .../dbt-main/int_revenue_components.yaml | 37 + .../dbt-main/mart_account_activity.yaml | 23 + .../dbt-main/mart_account_segments.yaml | 27 + .../dbt-main/mart_arr_daily.yaml | 20 + .../dbt-main/mart_customer_health.yaml | 30 + .../dbt-main/mart_nrr_quarterly.yaml | 22 + .../dbt-main/mart_procurement_activity.yaml | 24 + .../mart_retention_movement_breakout.yaml | 31 + .../dbt-main/mart_revenue_daily.yaml | 37 + .../dbt-main/stg_account_hierarchy.yaml | 25 + .../dbt-main/stg_account_owners.yaml | 33 + .../semantic-layer/dbt-main/stg_accounts.yaml | 25 + .../dbt-main/stg_activation_events.yaml | 33 + .../dbt-main/stg_approval_events.yaml | 25 + .../dbt-main/stg_arr_movements.yaml | 29 + .../dbt-main/stg_contract_discount_terms.yaml | 25 + .../dbt-main/stg_contracts.yaml | 29 + .../dbt-main/stg_invoice_line_items.yaml | 25 + .../semantic-layer/dbt-main/stg_invoices.yaml | 33 + .../dbt-main/stg_plan_segment_mapping.yaml | 33 + .../semantic-layer/dbt-main/stg_plans.yaml | 17 + .../dbt-main/stg_purchase_orders.yaml | 29 + .../dbt-main/stg_purchase_requests.yaml | 29 + .../semantic-layer/dbt-main/stg_refunds.yaml | 29 + .../semantic-layer/dbt-main/stg_sessions.yaml | 25 + .../dbt-main/stg_subscriptions.yaml | 25 + .../stg_supplier_onboarding_events.yaml | 29 + .../dbt-main/stg_suppliers.yaml | 21 + .../dbt-main/stg_support_tickets.yaml | 29 + .../semantic-layer/dbt-main/stg_users.yaml | 21 + .../semantic-layer/orbit_demo/accounts.yaml | 44 - .../orbit_demo/arr_movements.yaml | 38 - .../semantic-layer/orbit_demo/contracts.yaml | 39 - .../semantic-layer/orbit_demo/invoices.yaml | 33 - .../orbit_demo/purchase_requests.yaml | 33 - .../orbit_demo/support_tickets.yaml | 37 - .../_schema/orbit_analytics.yaml | 1400 +++++++++++++++++ .../postgres-warehouse/_schema/orbit_raw.yaml | 989 ++++++++++++ .../large_contract_requesters.yaml | 44 + .../mart_account_activity.yaml | 10 + .../mart_account_segments.yaml | 18 + .../postgres-warehouse/mart_arr_daily.yaml | 10 + .../mart_customer_health.yaml | 27 + .../mart_nrr_quarterly.yaml | 19 + .../mart_procurement_activity.yaml | 13 + .../mart_retention_movement_breakout.yaml | 25 + .../mart_revenue_daily.yaml | 20 + packages/cli/src/demo-assets.test.ts | 15 +- packages/cli/src/demo-assets.ts | 11 +- packages/cli/src/demo-seeded-inspect.test.ts | 8 +- packages/cli/src/demo-seeded-inspect.ts | 9 +- packages/cli/src/demo-seeded.test.ts | 16 +- packages/cli/src/setup-demo-tour.test.ts | 8 +- packages/cli/src/setup-demo-tour.ts | 54 +- 100 files changed, 5067 insertions(+), 472 deletions(-) create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/.gitkeep delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/activation-policy.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/arr-contract-first.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/customer-communication-policy.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/customer-health-scoring.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/discount-expiration.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/internal-test-exclusion.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/new-hire-onboarding-policy.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/nrr-retention.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-kpi-glossary.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-policy-change-jan-2026.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-arr-contract-first-definition.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-company-overview.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-health-risk-definition.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-stakeholder-needs.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-customers-source.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-exposures.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-project-overview.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-how-we-work.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-known-product-gaps.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-activity.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-segments.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-arr-daily.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-nrr-quarterly.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-procurement-activity.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-retention-movement-breakout.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-revenue-daily.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-metabase-sql-library-patterns.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-nrr-discount-expiration-treatment.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-plan-segment-normalization.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-procurement-qualifying-actions.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-product-design-principles.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-product-review-checklist.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/orbit-revenue-gross-to-net-reconciliation.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/procurement-workflows.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/revenue-gross-to-net.md create mode 100644 packages/cli/assets/demo/orbit/knowledge/global/sales-ops-cs-handoff-process.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/segment-classification.md delete mode 100644 packages/cli/assets/demo/orbit/knowledge/global/support-escalation.md create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/.gitkeep create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_activation_policy_windows.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_active_contract_arr.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_customer_health_signals.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_parent_account_arr_movements.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_procurement_qualifying_actions.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_revenue_components.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_activity.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_segments.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_arr_daily.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_customer_health.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_nrr_quarterly.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_procurement_activity.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_retention_movement_breakout.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_revenue_daily.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_hierarchy.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_owners.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_accounts.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_activation_events.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_approval_events.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_arr_movements.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contract_discount_terms.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contracts.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoice_line_items.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoices.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plan_segment_mapping.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plans.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_orders.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_requests.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_refunds.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_sessions.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_subscriptions.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_supplier_onboarding_events.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_suppliers.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_support_tickets.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_users.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/accounts.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/arr_movements.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/contracts.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/invoices.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/purchase_requests.yaml delete mode 100644 packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/support_tickets.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_analytics.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_raw.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/large_contract_requesters.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_activity.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_segments.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_arr_daily.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_customer_health.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_nrr_quarterly.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_procurement_activity.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_retention_movement_breakout.yaml create mode 100644 packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_revenue_daily.yaml diff --git a/packages/cli/assets/demo/orbit/knowledge/global/.gitkeep b/packages/cli/assets/demo/orbit/knowledge/global/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/cli/assets/demo/orbit/knowledge/global/activation-policy.md b/packages/cli/assets/demo/orbit/knowledge/global/activation-policy.md deleted file mode 100644 index 186381e5..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/activation-policy.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -summary: Account activation policy changed on January 15, 2026. -tags: - - growth - - activation - - policy -refs: [] -sl_refs: - - orbit_demo.accounts - - orbit_demo.purchase_requests -usage_mode: auto ---- - -Before January 15, 2026, activation meant first requester login. - -On and after January 15, 2026, activation requires an approved purchase request and at least three activated requesters. - -Always separate pre-policy and post-policy cohorts when comparing activation rates. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/arr-contract-first.md b/packages/cli/assets/demo/orbit/knowledge/global/arr-contract-first.md deleted file mode 100644 index 8e67ec65..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/arr-contract-first.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -summary: ARR uses contract-first precedence before subscription-derived revenue. -tags: - - finance - - arr - - revenue -refs: [] -sl_refs: - - orbit_demo.contracts - - orbit_demo.arr_movements -usage_mode: auto ---- - -ARR is calculated from active recurring contract ARR before falling back to subscription-derived revenue. - -Do not double-count subscription MRR when an active contract row covers the same account and period. - -Exclude cancelled contracts ending before the metric date, future-starting contracts, internal accounts, and test accounts. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/customer-communication-policy.md b/packages/cli/assets/demo/orbit/knowledge/global/customer-communication-policy.md new file mode 100644 index 00000000..162ade23 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/customer-communication-policy.md @@ -0,0 +1,44 @@ +--- +summary: "Required elements for valid customer updates: what happened, what is being done, who owns next step, and when customer will hear back. Vague status phrases are not acceptable." +usage_mode: auto +sort_order: 0 +tags: + - policy + - customer-success +refs: + - orbit-how-we-work + - sales-ops-cs-handoff-process +--- + +## Customer Update Communication Standard + +**Source:** Notion — People & Operating Norms, last edited 2026-05-07 + +--- + +## Policy + +Every customer update must contain four elements. An update that omits any of these is incomplete and must not be sent. + +| # | Required Element | Example | +|---|---|---| +| 1 | **What happened** | "The approval routing failed for the renewal PO because the department budget split was not configured." | +| 2 | **What is being done** | "We are reconfiguring the budget split and re-routing the approval to the correct approver." | +| 3 | **Who owns the next step** | "[Name] on our CS team owns this and is working it now." | +| 4 | **When the customer will hear back** | "You will have an update by 3pm today." | + +## Named Anti-Pattern + +- **Do not send:** "We are looking into it." +- This phrase is only acceptable when the actual blocker is genuinely unknown. If the blocker is known, name it. +- Vague status phrases without a named owner and a time commitment are not acceptable customer updates. + +## When This Applies + +- Any written update to a customer during an active issue, escalation, or implementation delay. +- Applies to email, Slack, and any other written channel. +- Verbal updates in calls should follow the same structure; a written summary must follow the call. + +--- + +See also: [[orbit-how-we-work]], [[sales-ops-cs-handoff-process]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/customer-health-scoring.md b/packages/cli/assets/demo/orbit/knowledge/global/customer-health-scoring.md deleted file mode 100644 index 15e84251..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/customer-health-scoring.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -summary: Customer health combines support severity and procurement activity. -tags: - - customer-success - - health - - churn-risk -refs: - - nrr-retention -sl_refs: - - orbit_demo.support_tickets - - orbit_demo.purchase_requests - - orbit_demo.accounts -usage_mode: auto ---- - -High-risk accounts have multiple recent high-severity tickets or no recent procurement activity on growth and enterprise plans. - -Medium risk captures partial support pressure or a material month-over-month decline in procurement activity. - -Internal and test accounts are excluded from customer health scoring. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/discount-expiration.md b/packages/cli/assets/demo/orbit/knowledge/global/discount-expiration.md deleted file mode 100644 index e65039df..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/discount-expiration.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -summary: Discount expirations are tracked separately from organic contraction. -tags: - - finance - - retention -refs: - - arr-contract-first - - nrr-retention -sl_refs: - - orbit_demo.contracts - - orbit_demo.arr_movements -usage_mode: auto ---- - -Discount expiration events identify pricing changes when negotiated discounts end. - -Track these separately from organic contraction so board reporting can split pricing-driven and usage-driven changes. - -Use movement_reason on arr_movements when separating discount expiration from churn or seat-reduction events. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/internal-test-exclusion.md b/packages/cli/assets/demo/orbit/knowledge/global/internal-test-exclusion.md deleted file mode 100644 index 17fce5ea..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/internal-test-exclusion.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -summary: Canonical metrics exclude internal and test accounts and users. -tags: - - data-quality - - governance -refs: [] -sl_refs: - - orbit_demo.accounts -usage_mode: auto ---- - -All canonical customer metrics exclude rows marked as internal or test fixtures. - -This exclusion applies at both account and user grain when joining procurement, support, and revenue activity. - -If a metric unexpectedly increases, check whether new internal or test accounts were created without proper flags. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/new-hire-onboarding-policy.md b/packages/cli/assets/demo/orbit/knowledge/global/new-hire-onboarding-policy.md new file mode 100644 index 00000000..5e827355 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/new-hire-onboarding-policy.md @@ -0,0 +1,47 @@ +--- +summary: "New hire week-one knowledge requirements: four things every new hire must understand by end of week one, with manager as responsible owner." +usage_mode: auto +sort_order: 0 +tags: + - orbit + - policy +refs: + - orbit-company-overview + - orbit-how-we-work +--- + +## New Hire Week-One Onboarding Policy + +**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. + +## 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 | + +## Ownership + +- The **manager** is responsible for delivering this context, not People Ops or a generic onboarding doc. +- If the manager cannot supply item 4 (first useful project) by day one, they should have it ready by end of day two at the latest. +- Items 1–3 can be covered via existing documentation; the manager should point to the right pages rather than re-explaining from scratch. + +## Suggested Reading for Items 1–3 + +- Item 1 & 2: [[orbit-company-overview]] +- Item 3: [[orbit-company-overview]] (Team Lanes section) +- Operating norms and how decisions are made: [[orbit-how-we-work]] + +--- + +See also: [[orbit-company-overview]], [[orbit-how-we-work]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/nrr-retention.md b/packages/cli/assets/demo/orbit/knowledge/global/nrr-retention.md deleted file mode 100644 index b9b0c07b..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/nrr-retention.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -summary: NRR is calculated at parent-account grain by calendar quarter. -tags: - - analytics - - retention - - nrr -refs: - - arr-contract-first -sl_refs: - - orbit_demo.arr_movements - - orbit_demo.accounts -usage_mode: auto ---- - -Net Revenue Retention uses parent-account rollups by calendar quarter. - -The formula is starting ARR plus expansion minus contraction and churn, divided by starting ARR. - -Exclude parent accounts with zero starting ARR, new business, reactivations, and internal/test accounts from the denominator. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-kpi-glossary.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-kpi-glossary.md new file mode 100644 index 00000000..7998c35a --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-kpi-glossary.md @@ -0,0 +1,65 @@ +--- +summary: "Customer activation: email verified + first project + team invite within 14 days of signup. D7/D14 activation rates and Time-to-Activate formulas. Source tables: customer, project, invite." +usage_mode: auto +sort_order: 0 +tags: + - activation + - kpi + - growth + - funnel + - metrics +refs: + - orbit-customers-source + - orbit-activation-policy-change-jan-2026 + - orbit-mart-account-activity +tables: + - orbit_analytics.customer + - orbit_analytics.project + - orbit_analytics.invite +--- + +# Activation KPI Glossary + +**Owner team:** Growth +**Source:** Notion — Orbit Demo Home / Data Team - Onboarding / Activation KPI Glossary, last edited 2026-05-07 + +Use this when a question is about signup-to-habit behavior. Orbit uses activation language across Growth, Product, and CS conversations. + +## Activation Definition + +A customer is **activated** when **all three** of the following happen **within 14 days of signup**: + +1. Email is verified +2. First project is created +3. At least one teammate is invited + +## Funnel Stages + +| Stage | Signal | Data source | +|---|---|---| +| 1. Signup | Customer row created | `orbit_analytics.customer` | +| 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) | — | + +## Conversion-Rate KPIs + +| KPI | Formula | +|---|---| +| **D7 Activation Rate** | `activated_customers_within_7_days / signups_in_cohort` | +| **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. + +## Source Notes + +- Use `orbit_analytics.customer` for `created_at` and `email_verified_at`. +- For project or invite timing, check `orbit_analytics.project` and `orbit_analytics.invite` before changing the activation definition. +- `created_at` is UTC; confirm timezone expectations before cohort filtering. + +## Relationship to Account-Level Activation + +This glossary defines **customer-level** activation (signup-to-habit). The **account-level** activation workflow (requester login → first approved purchase request → account activated) is a separate concept tracked in `mart_account_activity` and governed by the January 2026 policy change. See `orbit-activation-policy-change-jan-2026` for that definition. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-policy-change-jan-2026.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-policy-change-jan-2026.md new file mode 100644 index 00000000..675abb63 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-activation-policy-change-jan-2026.md @@ -0,0 +1,46 @@ +--- +summary: "January 2026 activation policy change: policy_version splits events into pre_2026_01_15 and post_2026_01_15 cohorts. mart_account_activity compares activation counts across the boundary." +usage_mode: auto +sort_order: 0 +tags: + - activation + - growth + - policy + - governed-metric + - procurement +sl_refs: + - mart_account_activity +--- + +# Activation Policy Change — January 2026 + +**Governed metric key:** `activated_accounts` +**Owner team:** growth +**Notion:** `notion://notion_page_activation_policy_decision#policy-change` +**Sources:** `mart_account_activity`, `int_activation_policy_windows`, `stg_activation_events` + +## Policy Boundary + +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 + +## Activation Event Types + +`first_requester_login`, `requester_activated`, `first_approved_purchase_request`, `account_activated` + +## Account Activation Sequence + +1. First requester login → `first_requester_login` +2. Requester activated → `requester_activated` +3. First approved purchase request → `first_approved_purchase_request` +4. Account activated → `account_activated` + +## Exclusions + +Internal and test accounts (lifecycle_status = `internal` or `test` on `stg_accounts`) are excluded from activation counts. Sessions (`stg_sessions`) are used for pre-policy activation and activity exclusions. + +## Dashboard + +Exposed via the **Growth Activation Dashboard** (`https://orbit-demo.example.com/dashboards/activation`), which depends on `mart_account_activity`. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-arr-contract-first-definition.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-arr-contract-first-definition.md new file mode 100644 index 00000000..ac8cd076 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-arr-contract-first-definition.md @@ -0,0 +1,39 @@ +--- +summary: "ARR is calculated contract-first: active contract ARR takes precedence over subscription ARR for any covered period." +usage_mode: auto +sort_order: 0 +tags: + - arr + - governed-metric + - finance + - contracts + - subscriptions +sl_refs: + - mart_arr_daily + - mart_account_segments +--- + +# ARR — Contract-First Definition + +**Governed metric key:** `arr` +**Owner team:** finance +**Notion:** `notion://notion_page_arr_contract_reporting#arr-contract-first` +**Source:** `mart_arr_daily` (grain: `metric_date`) + +## Rule + +ARR is calculated **contract-first**: when an active contract exists for an account and period, `int_active_contract_arr` is used. Subscription ARR (`stg_subscriptions`) is only used when no active contract covers the period. + +## Known Assertion + +The dbt test on `mart_arr_daily.arr_cents` asserts the value equals **1,874,200,000 cents ($18,742,000)** as of `metric_date = 2026-03-31`. + +## Intermediate model + +`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 diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-company-overview.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-company-overview.md new file mode 100644 index 00000000..6cf4afac --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-company-overview.md @@ -0,0 +1,72 @@ +--- +summary: "Orbit: procurement workflow software (requests → approvals → supplier onboarding → POs). Plans: Starter, Growth, Enterprise. Team lanes and open operating questions as of May 2026." +usage_mode: auto +sort_order: 0 +tags: + - company-context + - product + - plans + - team-lanes + - procurement +refs: + - orbit-plan-segment-normalization + - orbit-procurement-qualifying-actions +--- + +# Orbit Company Overview + +**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. + +## Product Workflow + +1. Requester submits a purchase request +2. Approval routing collects the right decision +3. Supplier invite and onboarding happen before work starts +4. Purchase order is created from the approved request +5. Renewal handoff keeps the relationship from drifting + +## Plans + +| Plan | Target customer | +|---|---| +| **Starter** | Teams moving out of spreadsheet tracking | +| **Growth** | Default mid-market plan | +| **Enterprise** | Multiple approval policies, parent/child account structures, heavier renewal coordination | + +**Legacy alias:** `pro_plus` in older notes means Growth. Treat as Growth unless Sales Ops says otherwise. See `orbit-plan-segment-normalization` for the data-layer normalization rule. + +## Team Lanes + +| Team | Responsibilities | +|---|---| +| Product | Requester onboarding, supplier onboarding, approval routing, PO workflow quality | +| Growth | Activation, self-serve conversion | +| Sales Ops | Account segmentation, plan mapping, contracts, handoff hygiene | +| Customer Success | Implementation, support escalations, account health, renewal risk | +| Finance | Billing, close, board prep | +| Data | Cross-functional support for all departments | +| Executive | Company priorities, weekly operating review | + +## Open / Unsettled Questions (as of May 2026) + +- Whether supplier onboarding stays fully inside Product or splits more work with CS for larger accounts. +- Whether Growth is still the right default-plan language in sales materials. +- How renewal handoff works when Sales Ops updates account segment late in-quarter. +- Implementation handoff template decision still pending. +- Renewal risk review agenda should not live only in meeting notes. + +## Common Customer Pain Points (Pre-Sale) + +- "We have too many request paths." +- "Approvals happen, but no one can explain the state of the request." +- "Supplier onboarding is split across three teams." +- "Renewals are visible too late." +- "People keep asking Finance for status because there is nowhere better to look." + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-health-risk-definition.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-health-risk-definition.md new file mode 100644 index 00000000..4457bd21 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-health-risk-definition.md @@ -0,0 +1,46 @@ +--- +summary: "Customer health risk definition: risk_level (low/medium/high) derived from open critical support tickets and recent procurement activity. Mart: mart_customer_health, as of 2026-03-31." +usage_mode: auto +sort_order: 0 +tags: + - customer-health + - risk + - customer-success + - governed-metric + - support +sl_refs: + - mart_customer_health +--- + +# Customer Health Risk Definition + +**Governed metric key:** `active_customers` +**Owner team:** customer_success +**Notion:** `notion://notion_page_customer_health_playbook#risk-definition` +**Sources:** `mart_customer_health`, `int_customer_health_signals` + +## Risk Levels + +`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. + +## Mart + +`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] + +## Support Ticket Severities + +`low`, `medium`, `high`, `critical` + +## Account Ownership Context + +`stg_account_owners` provides effective-dated ownership (owner_team: sales_ops, customer_success, finance) for escalation routing. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-stakeholder-needs.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-stakeholder-needs.md new file mode 100644 index 00000000..f7e57215 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customer-stakeholder-needs.md @@ -0,0 +1,42 @@ +--- +summary: "Recurring customer stakeholder needs by role: Finance, Department leaders, Procurement, Legal, and Customer Success each have distinct priorities that should inform product and positioning decisions." +usage_mode: auto +sort_order: 0 +tags: + - product + - customer-success + - orbit +refs: + - orbit-company-overview + - orbit-product-review-checklist +--- + +## Customer Stakeholder Needs by Role + +**Source:** Notion — Product & Customers, last edited 2026-05-07 + +--- + +## Policy + +These are recurring, role-specific customer needs observed across accounts. Use them to inform product prioritization, positioning, and CS engagement strategies. + +## Stakeholder Map + +| 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 | +| **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 | + +## Usage Notes + +- These needs are recurring patterns, not one-off requests. They should be treated as standing assumptions until explicitly updated. +- When prioritizing roadmap items, map each item to the stakeholder(s) it serves and verify the need is still active. +- When positioning Orbit to a new prospect, use this map to tailor the value proposition to the roles present in the buying committee. + +--- + +See also: [[orbit-company-overview]], [[orbit-product-review-checklist]], [[orbit-known-product-gaps]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-customers-source.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customers-source.md new file mode 100644 index 00000000..3822e31e --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-customers-source.md @@ -0,0 +1,61 @@ +--- +summary: "orbit_analytics.customer: one row per customer. Columns, joins to account/subscription_event, measures (customer_count, paying_customer_count, mrr), and watch-outs." +usage_mode: auto +sort_order: 0 +tags: + - data-source + - customers + - orbit-analytics + - measures +refs: + - orbit-plan-segment-normalization + - orbit-activation-kpi-glossary +tables: + - orbit_analytics.customer + - orbit_analytics.account + - orbit_analytics.subscription_event +--- + +# Orbit Customers Source + +**Table:** `orbit_analytics.customer` +**Grain:** one row per signed-up customer +**Source:** Notion — Orbit Demo Home / Data Team - Onboarding / Orbit Customers Source, last edited 2026-05-07 + +Use this when a question needs customer identity, plan tier, signup timing, recent activity, or the standard customer joins. + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `id` | number | Primary key, surrogate 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` | +| `created_at` | time | UTC signup timestamp | +| `last_seen_at` | time | UTC most recent app activity | +| `email_verified_at` | time | UTC email verification timestamp (used in activation funnel) | + +## Joins + +- **one-to-many** → `orbit_analytics.account` on `customer.id = account.customer_id` +- **one-to-many** → `orbit_analytics.subscription_event` on `customer.id = subscription_event.customer_id` + +Always join through `customer.id`. Do not join on `email`. + +## Standard Measures + +| Measure | Formula | +|---|---| +| `customer_count` | `count(distinct id)` | +| `paying_customer_count` | `count(distinct id) where plan_tier in ('pro', 'enterprise')` | +| `mrr` | `sum(subscription_event.amount) where event_type = 'renewed'` | + +## Watch-outs + +- **Join key:** Always use `customer.id`, never `email`. +- **Timezone:** `created_at` and `last_seen_at` are UTC. Confirm whether a question expects UTC or a local business day before filtering. +- **Paying vs. all:** `free` customers must be excluded from paying-customer follow-ups. Use `paying_customer_count`, not `customer_count`. +- **plan_tier values:** `free`, `pro`, `enterprise`. Note: `pro_plus` is a legacy alias for `growth` in the account/contract layer (see `orbit-plan-segment-normalization`), but `plan_tier` on this table uses `pro` not `pro_plus`. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-exposures.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-exposures.md new file mode 100644 index 00000000..981d494c --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-exposures.md @@ -0,0 +1,45 @@ +--- +summary: "dbt exposures declared in models/exposures.yml: three dashboards (Retention Executive, Executive Revenue, Growth Activation) with their upstream mart dependencies and owners." +usage_mode: auto +sort_order: 0 +tags: + - dbt + - exposures + - dashboards + - orbit +sl_refs: + - mart_nrr_quarterly + - mart_retention_movement_breakout + - mart_arr_daily + - mart_revenue_daily + - mart_account_activity +--- + +# Orbit dbt Exposures + +Declared in `models/exposures.yml`. All exposures are type `dashboard` with maturity `high` or `medium`. + +## Retention Executive Dashboard + +- **URL:** https://orbit-demo.example.com/dashboards/retention +- **Maturity:** high +- **Owner:** Analytics (analytics@orbit-demo.example.com) +- **Depends on:** `mart_nrr_quarterly`, `mart_retention_movement_breakout` +- **Description:** Executive retention view covering NRR and movement breakout. + +## Executive Revenue Dashboard + +- **URL:** https://orbit-demo.example.com/dashboards/revenue +- **Maturity:** high +- **Owner:** Finance (finance@orbit-demo.example.com) +- **Depends on:** `mart_arr_daily`, `mart_revenue_daily` +- **Description:** Board reporting view for ARR and gross-to-net revenue. + +## Growth Activation Dashboard + +- **URL:** https://orbit-demo.example.com/dashboards/activation +- **Maturity:** medium +- **Owner:** Growth (growth@orbit-demo.example.com) +- **Depends on:** `mart_account_activity` +- **Description:** Activation policy comparison around the January 2026 workflow update. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-project-overview.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-project-overview.md new file mode 100644 index 00000000..11aac427 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-dbt-project-overview.md @@ -0,0 +1,55 @@ +--- +summary: "Overview of the kaelio_demo dbt project: connection, schema layout, model layers, and governed metrics." +usage_mode: auto +sort_order: 0 +tags: + - dbt + - orbit + - data-model + - governed-metrics +sl_refs: + - stg_accounts + - stg_contracts + - stg_arr_movements + - mart_arr_daily + - mart_nrr_quarterly + - mart_revenue_daily + - mart_account_activity + - mart_procurement_activity + - mart_customer_health + - mart_account_segments +--- + +# Orbit dbt Project Overview + +**Project name:** `kaelio_demo` +**dbt version:** 1.0.0 +**Profile target:** Postgres (`orbit_analytics` schema, `kaelio_demo` database) +**Raw source schema:** `orbit_raw` +**Analytics schema:** `orbit_analytics` (all models materialised as views by default) + +## Model Layers + +| Layer | Prefix | Purpose | +|---|---|---| +| Staging | `stg_` | 1-to-1 with `orbit_raw` tables; adds type-casting, column tests, enum constraints | +| Intermediate | `int_` | Business-logic joins and rollups; not exposed to BI directly | +| Mart | `mart_` | Board/dashboard-ready aggregates; each has a `governed_metric_key` and `owner_team` | + +## Governed Metrics (mart layer) + +| Mart | `governed_metric_key` | Owner | Notion | +|---|---|---|---| +| `mart_arr_daily` | `arr` | finance | `notion_page_arr_contract_reporting` | +| `mart_nrr_quarterly` | `net_revenue_retention` | analytics | `notion_page_retention_policy_current` | +| `mart_retention_movement_breakout` | `net_revenue_retention` | analytics | `notion_page_retention_policy_current` | +| `mart_revenue_daily` | `net_revenue` | finance | `notion_page_revenue_reporting_policy` | +| `mart_account_activity` | `activated_accounts` | growth | `notion_page_activation_policy_decision` | +| `mart_procurement_activity` | `weekly_active_requesters` | product | `notion_page_procurement_instrumentation` | +| `mart_customer_health` | `active_customers` | customer_success | `notion_page_customer_health_playbook` | +| `mart_account_segments` | `segment` | sales_ops | `notion_page_sales_ops_segmentation` | + +## Raw Source Tables (`orbit_raw` schema) + +accounts, account_hierarchy, plans, contracts, subscriptions, contract_discount_terms, arr_movements, invoices, invoice_line_items, refunds, plan_segment_mapping, users, activation_events, sessions, purchase_requests, approval_events, suppliers, supplier_onboarding_events, purchase_orders, support_tickets, account_owners. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-how-we-work.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-how-we-work.md new file mode 100644 index 00000000..3640a693 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-how-we-work.md @@ -0,0 +1,75 @@ +--- +summary: "Orbit operating model: remote-first, written-first, weekly rhythm, decision process, escalation policy, and standing operating norms." +usage_mode: auto +sort_order: 0 +tags: + - policy + - orbit +refs: + - orbit-company-overview + - customer-communication-policy +--- + +## How We Work + +**Source:** Notion — Orbit Demo Home / How We Work, last edited 2026-05-07 + +--- + +## Operating Model + +- Orbit is a **mostly remote, mostly written** company. +- Meetings must serve a specific purpose: making a decision, unblocking a handoff, or building shared context that writing alone would be slower to achieve. +- If a meeting does not meet one of those three purposes, default to async written communication. + +--- + +## Weekly Rhythm + +| Day(s) | Focus | +|---|---| +| **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 | + +Use this rhythm when scheduling work, meetings, or reviews. Do not schedule decision-making meetings on Fridays; use Friday to record decisions already made. + +--- + +## Decision-Making Process + +1. **The person closest to the work writes the recommendation.** +2. **Stakeholders who will live with the decision get to push back.** +3. **The accountable lead makes the call** when a real tradeoff exists. +4. **The result is written where the work is happening.** Decisions that exist only in Slack or a meeting are not considered durable. + +> A decision that isn't written down didn't happen. + +--- + +## Standing Operating Norms + +These are explicitly codified rules Orbit has identified as recurring failure modes: + +- **Name the accountable person before work begins.** If no one is named, no one is accountable. +- **Never let a quick sync be the only source of truth.** Write it down after. +- **Bring a customer example when proposing product changes.** Abstract proposals without customer grounding are harder to evaluate. +- **Involve affected teams before a plan is finalized.** Surprises in execution are more expensive than slower planning. +- **Prefer a rough written decision today over a perfect recap that never gets written.** Done and documented beats polished and lost. + +--- + +## Escalation Policy + +- **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. + 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. + +--- + +See also: [[orbit-company-overview]], [[orbit-team-lanes-detail]], [[customer-communication-policy]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-known-product-gaps.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-known-product-gaps.md new file mode 100644 index 00000000..ea0c4a9e --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-known-product-gaps.md @@ -0,0 +1,47 @@ +--- +summary: "Known Orbit product friction: approval routing for non-standard cases (weird supplier setups, split budgets, renewal changes) causes teams to fall back to side channels outside Orbit." +usage_mode: auto +sort_order: 0 +tags: + - product + - orbit + - customer-success +refs: + - orbit-customer-stakeholder-needs + - orbit-product-review-checklist + - orbit-company-overview +--- + +## Known Product Gaps and Friction Points + +**Source:** Notion — Product & Customers (Notes from Recent Customer Calls), last edited 2026-05-07 + +--- + +## Primary Friction: Approval Routing for Exceptions + +The primary source of customer friction is **approval routing around non-standard cases**. When a procurement request does not fit the standard routing rules, teams fall back to side channels (email, Slack, spreadsheets) outside Orbit. + +### Specific Triggers + +| Trigger | Why It Causes Fallback | +|---|---| +| **Weird supplier setups** | Non-standard supplier configurations don't fit the default approval chain | +| **Split department budgets** | Requests that span multiple budget owners require manual coordination not supported in the routing UI | +| **Renewal changes** | Mid-term contract changes (scope, price, term) don't map cleanly to the new-request flow | + +## Impact + +- Teams that fall back to side channels for exceptions create a split record: part of the procurement history is in Orbit, part is not. +- This undermines the supplier file completeness that Procurement requires (see [[orbit-customer-stakeholder-needs]]). +- It also creates renewal risk because CS cannot see the full picture of what was agreed. + +## Status + +- This is a known, unresolved gap as of May 2026. +- Treat as a standing assumption in roadmap and analysis decisions until a fix is shipped and validated. +- Do not design analyses or reports that assume all procurement activity flows through Orbit for accounts with known exception patterns. + +--- + +See also: [[orbit-customer-stakeholder-needs]], [[orbit-product-review-checklist]], [[orbit-company-overview]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-activity.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-activity.md new file mode 100644 index 00000000..29ba3392 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-activity.md @@ -0,0 +1,50 @@ +--- +summary: "mart_account_activity: pre/post policy 30-day activation rates per policy_change_date. policy_change_date = 2026-01-15 is the Jan 2026 boundary. Rates are 0–1 ratios." +usage_mode: auto +sort_order: 0 +tags: + - activation + - policy + - mart + - orbit-analytics +sl_refs: + - mart_account_activity +tables: + - orbit_analytics.mart_account_activity +--- + +# mart_account_activity + + + + + + +**Table:** `orbit_analytics.mart_account_activity` +**Grain:** one row per `policy_change_date` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `policy_change_date` | date | The policy boundary date (primary value: `2026-01-15`) | +| `pre_policy_30_day_activation_rate` | decimal | 30-day activation rate before the policy change (0–1 ratio) | +| `post_policy_30_day_activation_rate` | decimal | 30-day activation rate after the policy change (0–1 ratio) | + +## 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)` + +## Common query patterns + +- **Policy comparison:** `WHERE policy_change_date = date '2026-01-15'` +- **As percent:** `round(pre_policy_30_day_activation_rate * 100, 1)` +- **Side-by-side:** UNION of pre and post rows with a `policy_window` label column + +## Business rules + +- The January 2026 activation policy change (`policy_change_date = 2026-01-15`) is the primary boundary. `policy_version` in upstream events splits into `pre_2026_01_15` and `post_2026_01_15` cohorts. +- Rates are ratios (0–1); multiply by 100 for percentage display. +- See [orbit-activation-policy-change-jan-2026](orbit-activation-policy-change-jan-2026) for full policy context. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-segments.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-segments.md new file mode 100644 index 00000000..d23ee684 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-account-segments.md @@ -0,0 +1,56 @@ +--- +summary: "mart_account_segments: account segmentation with contract ARR, plan codes, size_band, segment (self_serve/commercial/enterprise), and contract_status. One row per account_id." +usage_mode: auto +sort_order: 0 +tags: + - arr + - segmentation + - accounts + - mart + - orbit-analytics +sl_refs: + - mart_account_segments +tables: + - orbit_analytics.mart_account_segments +--- + +# mart_account_segments + + + + +**Table:** `orbit_analytics.mart_account_segments` +**Grain:** one row per `account_id` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `account_id` | text | Primary key | +| `parent_account_id` | text | Parent account for hierarchy rollups | +| `current_plan_code` | text | Raw plan code from billing system | +| `normalized_plan_code` | text | Canonical plan code (`pro_plus` → `growth`) | +| `size_band` | text | Company size band | +| `segment` | text | Reporting segment: `self_serve`, `commercial`, `enterprise` | +| `contract_arr_cents` | bigint | Contract ARR in cents | +| `contract_status` | text | `active`, `churned`, etc. | + +## 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 + +## Common query patterns + +- **ARR by segment:** `GROUP BY segment WHERE contract_status = 'active'` +- **Top accounts:** `ORDER BY contract_arr_cents DESC` with `is_internal = false AND is_test = false` (join to `orbit_raw.accounts`) +- **Unmapped segment:** `COALESCE(segment, 'unmapped')` + +## Business rules + +- `normalized_plan_code` maps `pro_plus` → `growth`. Always use `normalized_plan_code` for plan-based reporting. See [orbit-plan-segment-normalization](orbit-plan-segment-normalization). +- `segment` is derived from `canonical_plan_code × size_band` via `stg_plan_segment_mapping`. +- `contract_arr_cents` is the contract-first ARR value. See [orbit-arr-contract-first-definition](orbit-arr-contract-first-definition). + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-arr-daily.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-arr-daily.md new file mode 100644 index 00000000..f1231a30 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-arr-daily.md @@ -0,0 +1,47 @@ +--- +summary: "mart_arr_daily: daily ARR snapshot with contract-first valuation, arr_cents and display columns, used for ARR trend and EoQ reporting." +usage_mode: auto +sort_order: 0 +tags: + - arr + - revenue + - mart + - orbit-analytics +sl_refs: + - mart_arr_daily +tables: + - orbit_analytics.mart_arr_daily +--- + +# mart_arr_daily + + + + +**Table:** `orbit_analytics.mart_arr_daily` +**Grain:** one row per `metric_date` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `metric_date` | date | Snapshot date | +| `arr_cents` | bigint | ARR in cents (contract-first: active contract ARR takes precedence over subscription ARR) | +| `display` | text | Human-readable ARR label (e.g. formatted dollar string) | + +## Key measures (SL: `mart_arr_daily`) + +- `total_arr_cents` — `sum(arr_cents)` +- `arr_millions` — `round(sum(arr_cents) / 100000000.0, 3)` — ARR in $M + +## Common query patterns + +- **Current ARR:** filter `metric_date = current_date` (or latest available date) +- **EoQ ARR:** filter `metric_date = date '2026-03-31'` +- **ARR trend:** group by `metric_date`, plot `arr_cents` + +## Business rules + +- ARR is calculated contract-first: active contract ARR takes precedence over subscription ARR for any covered period. See [orbit-arr-contract-first-definition](orbit-arr-contract-first-definition). +- `display` is a formatted label for UI rendering; use `arr_cents` for all arithmetic. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-nrr-quarterly.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-nrr-quarterly.md new file mode 100644 index 00000000..9b423e6d --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-nrr-quarterly.md @@ -0,0 +1,56 @@ +--- +summary: "mart_nrr_quarterly: quarterly NRR by segment with net_revenue_retention ratio, expansion/contraction/churn ARR cents, and quarter_label. Enterprise is the primary reporting segment." +usage_mode: auto +sort_order: 0 +tags: + - nrr + - retention + - revenue + - mart + - orbit-analytics +sl_refs: + - mart_nrr_quarterly +tables: + - orbit_analytics.mart_nrr_quarterly +--- + +# mart_nrr_quarterly + + + + + +**Table:** `orbit_analytics.mart_nrr_quarterly` +**Grain:** one row per `quarter_label` × `segment` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `quarter_start_date` | date | First day of the quarter | +| `quarter_label` | text | Quarter identifier, e.g. `'2026-Q1'` | +| `segment` | text | Customer segment: `enterprise`, `commercial`, `self_serve` | +| `starting_arr_cents` | bigint | ARR at start of quarter in cents | +| `expansion_arr_cents` | bigint | ARR added from expansions | +| `contraction_arr_cents` | bigint | ARR lost from contractions (includes discount expirations) | +| `churned_arr_cents` | bigint | ARR lost from churn | +| `net_revenue_retention` | decimal | NRR ratio (e.g. `1.12` = 112%) | + +## 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'` +- `total_expansion_arr_cents`, `total_contraction_arr_cents`, `total_churned_arr_cents` + +## Common query patterns + +- **Q1 enterprise NRR:** `WHERE quarter_label = '2026-Q1' AND segment = 'enterprise'` +- **NRR as percent:** `round(net_revenue_retention * 100, 1)` +- **Trend by quarter:** `ORDER BY quarter_start_date` + +## Business rules + +- `net_revenue_retention` is a ratio, not a percentage. Multiply by 100 for display. +- Contraction includes discount expirations (classified as contraction, not churn). See [orbit-nrr-discount-expiration-treatment](orbit-nrr-discount-expiration-treatment). +- Enterprise is the primary executive reporting segment. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-procurement-activity.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-procurement-activity.md new file mode 100644 index 00000000..0b31edae --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-procurement-activity.md @@ -0,0 +1,48 @@ +--- +summary: "mart_procurement_activity: weekly active requester counts by contract_arr_threshold_cents. Standard threshold is 20000000 cents ($200k ARR). Used for golden-week procurement metrics." +usage_mode: auto +sort_order: 0 +tags: + - procurement + - mart + - orbit-analytics + - active-requesters +sl_refs: + - mart_procurement_activity +tables: + - orbit_analytics.mart_procurement_activity +--- + +# mart_procurement_activity + + + + +**Table:** `orbit_analytics.mart_procurement_activity` +**Grain:** one row per `week_start_date` × `contract_arr_threshold_cents` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `week_start_date` | date | Monday of the reporting week | +| `week_end_date` | date | Sunday of the reporting week | +| `contract_arr_threshold_cents` | bigint | ARR threshold filter applied (e.g. `20000000` = $200k) | +| `active_requesters` | bigint | Count of qualifying active requesters for the week | + +## 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` + +## Common query patterns + +- **Golden week (week of 2026-03-23):** `WHERE week_start_date = date '2026-03-23' AND contract_arr_threshold_cents = 20000000` +- **Weekly trend at $200k threshold:** `WHERE contract_arr_threshold_cents = 20000000 ORDER BY week_start_date` + +## Business rules + +- `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. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-retention-movement-breakout.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-retention-movement-breakout.md new file mode 100644 index 00000000..d5021f0a --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-retention-movement-breakout.md @@ -0,0 +1,56 @@ +--- +summary: "mart_retention_movement_breakout: quarterly ARR movement by segment, movement_type, and movement_reason. NRR waterfall source. Contraction includes discount expirations." +usage_mode: auto +sort_order: 0 +tags: + - nrr + - retention + - arr + - mart + - orbit-analytics +sl_refs: + - mart_retention_movement_breakout +tables: + - orbit_analytics.mart_retention_movement_breakout +--- + +# mart_retention_movement_breakout + + + + +**Table:** `orbit_analytics.mart_retention_movement_breakout` +**Grain:** one row per `quarter_label` × `segment` × `movement_type` × `movement_reason` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `quarter_start_date` | date | First day of the quarter | +| `quarter_label` | text | Quarter identifier, e.g. `'2026-Q1'` | +| `segment` | text | Customer segment: `enterprise`, `commercial`, `self_serve` | +| `movement_type` | text | `expansion`, `contraction`, or `churn` | +| `movement_reason` | text | Specific reason (e.g. `discount_expiration`) | +| `parent_account_count` | bigint | Number of parent accounts in this bucket | +| `expansion_arr_cents` | bigint | Expansion ARR in cents | +| `contraction_arr_cents` | bigint | Contraction ARR in cents | +| `churned_arr_cents` | bigint | Churned ARR in cents | + +## Key measures (SL: `mart_retention_movement_breakout`) + +- `total_expansion_arr_cents`, `total_contraction_arr_cents`, `total_churned_arr_cents` +- `expansion_arr_millions`, `contraction_arr_millions`, `churned_arr_millions` +- `parent_account_count` + +## Common query patterns + +- **Q1 enterprise waterfall:** `WHERE quarter_label = '2026-Q1' AND segment = 'enterprise'` +- **Movement summary:** `GROUP BY movement_type ORDER BY movement_type` +- **Discount expiration contraction:** `WHERE movement_reason = 'discount_expiration'` + +## Business rules + +- Contraction includes discount expirations, classified as contraction (not churn), tracked via `movement_reason`. See [orbit-nrr-discount-expiration-treatment](orbit-nrr-discount-expiration-treatment). +- This table is the row-level source for `mart_nrr_quarterly` aggregations. +- Only one of `expansion_arr_cents`, `contraction_arr_cents`, `churned_arr_cents` is non-zero per row. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-revenue-daily.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-revenue-daily.md new file mode 100644 index 00000000..6a088de0 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-mart-revenue-daily.md @@ -0,0 +1,57 @@ +--- +summary: "mart_revenue_daily: daily gross-to-net revenue reconciliation with gross_revenue_cents, credits_cents, refunds_cents, net_revenue_cents, and reconciliation_check." +usage_mode: auto +sort_order: 0 +tags: + - revenue + - reconciliation + - mart + - orbit-analytics +sl_refs: + - mart_revenue_daily +tables: + - orbit_analytics.mart_revenue_daily +--- + +# mart_revenue_daily + + + + + + +**Table:** `orbit_analytics.mart_revenue_daily` +**Grain:** one row per `revenue_date` + +## Columns + +| Column | Type | Notes | +|---|---|---| +| `revenue_date` | date | Revenue recognition date | +| `gross_revenue_cents` | bigint | Gross invoice revenue in cents | +| `credits_cents` | bigint | Credits applied in cents | +| `refunds_cents` | bigint | Refunds issued in cents | +| `net_revenue_cents` | bigint | Net revenue = gross − credits − refunds | +| `reconciliation_check` | boolean | Must be `true` on every row; flags rows where net ≠ gross − credits − refunds | + +## 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)` + +## Common query patterns + +- **Q1 net revenue:** `WHERE revenue_date BETWEEN '2026-01-01' AND '2026-03-31'` +- **February reconciliation:** `WHERE revenue_date BETWEEN '2026-02-01' AND '2026-02-28'` +- **Monthly trend:** `GROUP BY date_trunc('month', revenue_date)` + +## Business rules + +- `reconciliation_check` must be `true` on every row. Any `false` row indicates a data quality issue. +- Gross-to-net reconciliation: gross revenue − credits − refunds = net revenue. See [orbit-revenue-gross-to-net-reconciliation](orbit-revenue-gross-to-net-reconciliation). +- All amounts are in cents; divide by 100 for USD, by 100,000,000 for $M. + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-metabase-sql-library-patterns.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-metabase-sql-library-patterns.md new file mode 100644 index 00000000..d94ba88b --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-metabase-sql-library-patterns.md @@ -0,0 +1,72 @@ +--- +summary: "Metabase SQL Library collection (collection 7): reusable query patterns, the account_join snippet, and field-filter conventions used across Orbit Showcase cards." +usage_mode: auto +sort_order: 0 +tags: + - metabase + - sql-patterns + - orbit-showcase +sl_refs: + - mart_account_segments + - mart_procurement_activity + - mart_customer_health + - mart_retention_movement_breakout + - mart_revenue_daily + - mart_nrr_quarterly +--- + +# 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. + +## Reusable snippet: `account_join` + +Card 55 ("Large contract requesters") references `{{snippet: account_join}}`. The resolved SQL shows the canonical pattern for joining `orbit_analytics.mart_account_segments` to `orbit_raw.accounts`: + +```sql +FROM orbit_analytics.mart_account_segments mart +LEFT JOIN orbit_raw.accounts a + ON a.account_id = mart.account_id + AND a.is_internal = false + AND a.is_test = false +``` + +Key points: +- The `is_internal = false AND is_test = false` guard is applied **in the JOIN condition**, not the WHERE clause, so it does not drop rows from `mart_account_segments` that have no matching account row. +- The alias `mart` is used for `mart_account_segments` throughout the snippet. +- This pattern is equivalent to the filter used in card 48 ("Top accounts by contract ARR"), which applies the same guards in the WHERE clause instead. + +## Field-filter conventions + +Cards in this collection use Metabase dimension field filters (`type: dimension`) for optional narrowing: +- `segment` filter → maps to `mart_account_segments.segment` or `mart_retention_movement_breakout.segment`. +- `date_range` filter → maps to `mart_procurement_activity.week_start_date`. +- `quarter` filter → maps to `mart_nrr_quarterly.quarter_label`. + +These filters are **optional** (`[[ ... ]]` blocks in raw SQL); the resolved SQL drops them, leaving the unfiltered dataset. SL sources derived from these cards should not bake in the filter. + +## Hard-coded date anti-pattern + +Card 54 ("February credits drilldown") is explicitly documented as a **counter-example**: it hard-codes `revenue_date BETWEEN DATE '2026-02-01' AND DATE '2026-02-28'`. This card is not embedded in any dashboard and should not be used as a template. Use `mart_revenue_daily` directly with a runtime date filter instead. + +## Near-duplicate pair: cards 48 and 55 + +Both cards query `mart_account_segments` + `orbit_raw.accounts` and project `account_name`, `contract_arr`, `segment`, `size_band`. They differ only in: +- Card 48: no ARR floor filter, LIMIT 20, on 1 dashboard. +- Card 55: `contract_arr_cents >= 20,000,000` ($200k floor), LIMIT 25, no dashboard. + +Card 48 is the canonical reference; card 55 is a filtered variant for large-contract analysis. + +## Cards and their mart sources + +| Card | Name | Mart | Dashboard count | +|------|------|------|----------------| +| 48 | Top accounts by contract ARR | mart_account_segments | 1 | +| 49 | Procurement actions by week | mart_procurement_activity | 1 | +| 50 | Accounts at risk | mart_customer_health | 1 | +| 51 | ARR movement breakout | mart_retention_movement_breakout | 1 | +| 52 | Revenue refund audit | mart_revenue_daily | 0 | +| 53 | Enterprise NRR quarter breakout | mart_nrr_quarterly | 0 | +| 54 | February credits drilldown | mart_revenue_daily | 0 | +| 55 | Large contract requesters | mart_account_segments | 0 | + diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-nrr-discount-expiration-treatment.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-nrr-discount-expiration-treatment.md new file mode 100644 index 00000000..4af79d78 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-nrr-discount-expiration-treatment.md @@ -0,0 +1,47 @@ +--- +summary: "NRR definition and the Q1 2026 discount-expiration contraction treatment: discount expirations are classified as contraction, not churn, and tracked separately via is_discount_expiration_contraction." +usage_mode: auto +sort_order: 0 +tags: + - nrr + - retention + - governed-metric + - analytics + - discount + - contraction +sl_refs: + - mart_nrr_quarterly +--- + +# NRR — Discount Expiration Treatment + +**Governed metric key:** `net_revenue_retention` +**Owner team:** analytics +**Notion:** `notion://notion_page_retention_policy_current#nrr-definition` and `#discount-expiration-treatment` +**Sources:** `mart_nrr_quarterly`, `mart_retention_movement_breakout` + +## NRR Definition + +Net Revenue Retention (NRR) is calculated quarterly at the **parent-account** grain using `int_parent_account_arr_movements`. The enterprise segment is the primary reporting cut. + +**Known assertions:** +- Enterprise NRR **2026-Q1 = 1.018** (101.8%) +- Enterprise NRR **2025-Q4 = 1.064** (106.4%) + +## Discount Expiration Treatment + +Contraction ARR arising from the expiry of launch/renewal/migration/goodwill discounts is **not classified as churn**. It is tracked via the boolean flag `is_discount_expiration_contraction` on `int_parent_account_arr_movements` and surfaced as `movement_reason = 'discount_expiration'` in `mart_retention_movement_breakout`. + +**Known assertion:** 11 parent accounts had `movement_type = 'contraction'` and `movement_reason = 'discount_expiration'` in Q1 2026. + +## Discount Types (from `stg_contract_discount_terms`) + +`launch`, `renewal`, `migration`, `goodwill` + +## Movement Types + +`new`, `expansion`, `contraction`, `churn`, `reactivation` + +## Why This Matters + +Without the discount-expiration carve-out, Q1 2026 enterprise NRR would appear lower than it is. The Q4 → Q1 drop (1.064 → 1.018) is partly explained by discount expirations, not organic churn. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-plan-segment-normalization.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-plan-segment-normalization.md new file mode 100644 index 00000000..aeab9be3 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-plan-segment-normalization.md @@ -0,0 +1,49 @@ +--- +summary: "Plan code normalization rules: pro_plus maps to growth. Reporting segments (self_serve, commercial, enterprise) are derived from canonical_plan_code × size_band via stg_plan_segment_mapping." +usage_mode: auto +sort_order: 0 +tags: + - segmentation + - plans + - sales-ops + - governed-metric + - normalization +sl_refs: + - mart_account_segments +--- + +# Plan & Segment Normalization + +**Governed metric key:** `segment` +**Owner team:** sales_ops +**Notion:** `notion://notion_page_sales_ops_segmentation#growth-plan-normalization` +**Sources:** `mart_account_segments`, `stg_plan_segment_mapping`, `stg_plans` + +## Canonical Plan Codes + +| Raw / Legacy Code | Canonical Code | +|---|---| +| `starter` | `starter` | +| `growth` | `growth` | +| `pro_plus` | **`growth`** (normalized) | +| `enterprise` | `enterprise` | + +The normalization is applied via `stg_plans.canonical_plan_code`. `mart_account_segments.normalized_plan_code` reflects the post-normalization value. + +## Reporting Segments + +Segments are derived from `canonical_plan_code` × `size_band` using the effective-dated lookup `stg_plan_segment_mapping`: + +| Segment | Typical plan + size band | +|---|---| +| `self_serve` | starter / smb | +| `commercial` | growth / mid_market | +| `enterprise` | enterprise / enterprise | + +## Size Bands + +`smb`, `mid_market`, `enterprise` + +## Effective Dating + +`stg_plan_segment_mapping` has `effective_from` / `effective_to` columns, allowing segment rules to change over time without rewriting history. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-procurement-qualifying-actions.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-procurement-qualifying-actions.md new file mode 100644 index 00000000..87cc64c5 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-procurement-qualifying-actions.md @@ -0,0 +1,46 @@ +--- +summary: "Qualifying procurement actions for weekly active requester counts: non-internal, non-test requesters on large active contracts. Covers golden week metric and supplier onboarding." +usage_mode: auto +sort_order: 0 +tags: + - procurement + - product + - governed-metric + - weekly-active-requesters + - suppliers +sl_refs: + - mart_procurement_activity +--- + +# Procurement — Qualifying Actions & Weekly Active Requesters + +**Governed metric key:** `weekly_active_requesters` +**Owner team:** product +**Notion:** `notion://notion_page_procurement_instrumentation#qualifying-procurement-actions` +**Sources:** `mart_procurement_activity`, `int_procurement_qualifying_actions` + +## Qualifying Action Definition + +A qualifying procurement action is any activity by a **non-internal, non-test** requester on a **large active contract** within the measurement week. Captured in `int_procurement_qualifying_actions`. + +Qualifying action types include: +- Submitting a purchase request (`stg_purchase_requests`, status: submitted/approved) +- Supplier onboarding milestones (`stg_supplier_onboarding_events`, event_type: profile_completed, approved) +- Purchase order creation (`stg_purchase_orders`) + +## Exclusions + +- Accounts with `lifecycle_status IN ('internal', 'test')` on `stg_accounts` +- Requesters without an approved purchase request in the window + +## Supplier Onboarding Milestones + +`invited` → `profile_started` → `profile_completed` → `approved` + +## Approval Decisions (`stg_approval_events`) + +`approved`, `rejected`, `returned` + +## Dashboard + +Exposed via the **Growth Activation Dashboard** (`https://orbit-demo.example.com/dashboards/activation`), which depends on `mart_account_activity`. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-design-principles.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-design-principles.md new file mode 100644 index 00000000..f2b72b43 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-design-principles.md @@ -0,0 +1,43 @@ +--- +summary: "Orbit product design policy: new features must make requester or approver experience clearer; complexity for its own sake is not built." +usage_mode: auto +sort_order: 0 +tags: + - product + - policy + - orbit +refs: + - orbit-product-review-checklist + - orbit-company-overview +--- + +## Orbit Product Design Principles + +**Source:** Notion — Product & Customers, last edited 2026-05-07 + +--- + +## Core Policy + +Orbit does not build complexity for its own sake. + +## Feature Complexity Rule + +- When a new feature adds multiple configuration choices, it **must** be evaluated on whether it makes the requester or approver experience clearer. +- If the added configuration does not make the requester or approver experience clearer, the feature should not be built as designed. +- The test: can a first-time requester or approver use the new feature without needing to understand the configuration choices behind it? + +## Design Heuristics + +- **Default to simpler.** If two designs achieve the same outcome, prefer the one with fewer choices exposed to the end user. +- **Configuration is a last resort.** Expose configuration only when different customers have legitimately incompatible needs that cannot be resolved by a sensible default. +- **Requester and approver clarity are the primary UX metrics.** Speed, completeness, and confidence for those two roles are the measures of a good Orbit feature. + +## What This Is Not + +- This principle does not prohibit powerful or flexible features. +- It prohibits features where the complexity is internal to Orbit's implementation but leaks into the requester or approver experience without benefit. + +--- + +See also: [[orbit-product-review-checklist]], [[orbit-company-overview]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-review-checklist.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-review-checklist.md new file mode 100644 index 00000000..abf8e747 --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-product-review-checklist.md @@ -0,0 +1,44 @@ +--- +summary: "Five-question checklist to evaluate every Orbit product change: requester clarity, approver context, supplier onboarding ownership, PO accuracy, and CS rollout visibility." +usage_mode: auto +sort_order: 0 +tags: + - product + - policy + - orbit +refs: + - orbit-company-overview + - sales-ops-cs-handoff-process +--- + +## Product Review Checklist + +**Source:** Notion — Product & Customers, last edited 2026-05-07 + +--- + +## Policy + +Every product change must be evaluated against all five questions before shipping. A "no" on any question is a blocker unless explicitly waived by the accountable lead with a written rationale. + +## The Five Questions + +| # | Question | What a "no" means | +|---|---|---| +| 1 | **Does a first-time requester know what to do next?** | The requester experience is unclear or requires prior knowledge not surfaced in the UI. | +| 2 | **Can an approver make a decision without missing context?** | The approver is missing information needed to approve or reject confidently. | +| 3 | **Is supplier onboarding assigned to a named person, not a queue?** | Supplier onboarding has no clear owner and will stall. | +| 4 | **Does the PO reflect the approved request?** | There is a mismatch between what was approved and what the PO captures. | +| 5 | **Can Customer Success detect a stuck rollout after week two?** | CS has no signal to identify customers who are not progressing past initial setup. | + +## Usage + +- Use this checklist in product reviews, design critiques, and pre-launch readiness checks. +- Questions 1–2 are requester/approver experience checks. +- Question 3 is a supplier onboarding ownership check. +- Question 4 is a PO accuracy check. +- Question 5 is a post-launch CS visibility check. + +--- + +See also: [[orbit-company-overview]], [[orbit-product-design-principles]], [[sales-ops-cs-handoff-process]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/orbit-revenue-gross-to-net-reconciliation.md b/packages/cli/assets/demo/orbit/knowledge/global/orbit-revenue-gross-to-net-reconciliation.md new file mode 100644 index 00000000..7de4138b --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/orbit-revenue-gross-to-net-reconciliation.md @@ -0,0 +1,50 @@ +--- +summary: "Gross-to-net revenue reconciliation: mart_revenue_daily reconciles gross invoice revenue, credits, and refunds to net revenue daily. reconciliation_check must be true on every row." +usage_mode: auto +sort_order: 0 +tags: + - revenue + - net-revenue + - governed-metric + - finance + - reconciliation +sl_refs: + - mart_revenue_daily +--- + +# Revenue — Gross-to-Net Reconciliation + +**Governed metric key:** `net_revenue` +**Owner team:** finance +**Notion:** `notion://notion_page_revenue_reporting_policy#gross-to-net-reconciliation` +**Source:** `mart_revenue_daily` (grain: `revenue_date`) + +## Formula + +``` +net_revenue = gross_revenue - credits - refunds +``` + +All amounts are in **cents** (USD only — `stg_invoices.currency` is asserted to be `USD`). + +## Components + +| Column | Source | Description | +|---|---|---| +| `gross_revenue_cents` | `stg_invoices` / `stg_invoice_line_items` | Billed amounts before adjustments | +| `credit_cents` | `stg_invoice_line_items` (type=credit) | Credits applied to invoices | +| `refund_cents` | `stg_refunds` | Refunds reduce net revenue in the refund month | +| `net_revenue_cents` | Derived | gross − credits − refunds | + +## Intermediate model + +`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. + +## Line Item Types (`stg_invoice_line_items`) + +`subscription`, `seat`, `usage`, `addon`, `credit` diff --git a/packages/cli/assets/demo/orbit/knowledge/global/procurement-workflows.md b/packages/cli/assets/demo/orbit/knowledge/global/procurement-workflows.md deleted file mode 100644 index 6495065f..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/procurement-workflows.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -summary: Procurement workflow activity measures active requesters and qualifying actions. -tags: - - product - - procurement -refs: - - activation-policy -sl_refs: - - orbit_demo.purchase_requests -usage_mode: auto ---- - -Weekly active requesters counts distinct non-internal requesters with a qualifying procurement action in the calendar week. - -Qualifying actions include purchase request creation, approval decisions, supplier invites, and purchase-order creation. - -Purchase-request comments and short sessions are excluded from the canonical requester activity metric. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/revenue-gross-to-net.md b/packages/cli/assets/demo/orbit/knowledge/global/revenue-gross-to-net.md deleted file mode 100644 index 2c23363d..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/revenue-gross-to-net.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -summary: Gross-to-net revenue reconciles paid invoices, credits, and refunds. -tags: - - finance - - revenue -refs: - - arr-contract-first -sl_refs: - - orbit_demo.invoices -usage_mode: auto ---- - -Gross revenue starts from paid invoice activity. Net revenue subtracts credits and successful refunds in the month they are recorded. - -Exclude unpaid, void, draft, failed, internal, and test-account invoice activity from canonical revenue reporting. - -February 2026 has an elevated refund event captured in the source notes and revenue dashboard. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/sales-ops-cs-handoff-process.md b/packages/cli/assets/demo/orbit/knowledge/global/sales-ops-cs-handoff-process.md new file mode 100644 index 00000000..13eb139f --- /dev/null +++ b/packages/cli/assets/demo/orbit/knowledge/global/sales-ops-cs-handoff-process.md @@ -0,0 +1,58 @@ +--- +summary: "Sales Ops → Customer Success implementation handoff: required fields, ownership, enterprise account risk, and policy that CS must not rediscover sales-stage details." +usage_mode: auto +sort_order: 0 +tags: + - policy + - sales-ops + - customer-success +refs: + - orbit-company-overview + - orbit-how-we-work + - orbit-plan-segment-normalization +--- + +## Sales Ops → Customer Success Implementation Handoff + +**Source:** Notion — People & Operating Norms, last edited 2026-05-07 +**Owner:** Sales Ops (sender), Customer Success (receiver) + +--- + +## Policy + +Sales Ops must complete the handoff **before the first implementation call**. Customer Success should not need to rediscover any of the following details. + +## Required Handoff Fields + +| Field | Notes | +|---|---| +| Current plan | Starter / Growth / Enterprise — use canonical plan name, not legacy aliases | +| 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 | +| Unusual approval requirements | Any non-standard approval routing the customer has configured or requested | +| Unusual supplier requirements | Any supplier onboarding exceptions or pre-approved vendor lists | + +## Ownership + +- **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. + +## Common Failure Mode + +Handoffs that omit contract shape or renewal contact force CS to re-engage Sales Ops mid-implementation, which delays time-to-value and creates duplicate discovery work. This is the primary failure mode this process is designed to prevent. + +--- + +## Enterprise Account Risk: Parent/Child Complexity + +- 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. + +--- + +See also: [[orbit-company-overview]], [[orbit-how-we-work]], [[orbit-plan-segment-normalization]] diff --git a/packages/cli/assets/demo/orbit/knowledge/global/segment-classification.md b/packages/cli/assets/demo/orbit/knowledge/global/segment-classification.md deleted file mode 100644 index 901ea1a8..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/segment-classification.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -summary: Account segments derive from plan normalization and effective-dated mapping. -tags: - - sales-ops - - segmentation -refs: [] -sl_refs: - - orbit_demo.accounts - - orbit_demo.contracts -usage_mode: auto ---- - -Account segment labels combine plan_code, canonical_plan_code, and size_band fields. - -Historical plan code pro_plus maps to growth for current segment analysis. - -Use the mapping active at the metric date when segment definitions change over time. diff --git a/packages/cli/assets/demo/orbit/knowledge/global/support-escalation.md b/packages/cli/assets/demo/orbit/knowledge/global/support-escalation.md deleted file mode 100644 index 569f842a..00000000 --- a/packages/cli/assets/demo/orbit/knowledge/global/support-escalation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -summary: Support escalation tiers map ticket severity to SLA targets. -tags: - - support - - sla -refs: - - customer-health-scoring -sl_refs: - - orbit_demo.support_tickets -usage_mode: auto ---- - -Critical support tickets require immediate response and on-call escalation. - -High severity tickets should receive first response within four business hours. - -Resolution time is measured from created_at to resolved_at and only applies to resolved tickets. diff --git a/packages/cli/assets/demo/orbit/manifest.json b/packages/cli/assets/demo/orbit/manifest.json index 72cad508..1fcb3bef 100644 --- a/packages/cli/assets/demo/orbit/manifest.json +++ b/packages/cli/assets/demo/orbit/manifest.json @@ -43,12 +43,12 @@ }, "generated": { "semanticLayer": { - "path": "semantic-layer/orbit_demo", - "sourceCount": 6 + "path": "semantic-layer", + "sourceCount": 46 }, "knowledge": { "path": "knowledge/global", - "pageCount": 10 + "pageCount": 28 }, "links": { "path": "links", diff --git a/packages/cli/assets/demo/orbit/semantic-layer/.gitkeep b/packages/cli/assets/demo/orbit/semantic-layer/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_activation_policy_windows.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_activation_policy_windows.yaml new file mode 100644 index 00000000..ddceca13 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_activation_policy_windows.yaml @@ -0,0 +1,27 @@ +name: int_activation_policy_windows +table: orbit_analytics.int_activation_policy_windows +grain: + - policy_version +columns: + - name: policy_version + type: string + descriptions: + user: pre_2026_01_15 or post_2026_01_15 + - name: activated_account_count + type: number + descriptions: + ktx: Column activated account count from int_activation_policy_windows. + - name: window_start + type: time + descriptions: + ktx: Column window start from int_activation_policy_windows. + - name: window_end + type: time + descriptions: + ktx: Column window end from int_activation_policy_windows. +joins: [] +measures: + - name: total_activated_accounts + expr: sum(activated_account_count) +descriptions: + user: Activation cohort counts around the January 2026 policy change. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_active_contract_arr.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_active_contract_arr.yaml new file mode 100644 index 00000000..00360734 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_active_contract_arr.yaml @@ -0,0 +1,24 @@ +name: int_active_contract_arr +table: orbit_analytics.int_active_contract_arr +grain: + - contract_id +columns: + - name: contract_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on int_active_contract_arr. + - name: arr_cents + type: number + descriptions: + ktx: Column arr cents from int_active_contract_arr. +joins: [] +measures: + - name: total_arr_cents + expr: sum(arr_cents) + description: Total active contract ARR in cents as of 2026-03-31. +descriptions: + user: Active contract ARR as of 2026-03-31. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_customer_health_signals.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_customer_health_signals.yaml new file mode 100644 index 00000000..5e5af811 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_customer_health_signals.yaml @@ -0,0 +1,25 @@ +name: int_customer_health_signals +table: orbit_analytics.int_customer_health_signals +grain: + - account_id +columns: + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on int_customer_health_signals. + - name: open_critical_ticket_count + type: number + descriptions: + ktx: Column open critical ticket count from int_customer_health_signals. + - name: recent_procurement_action_count + type: number + descriptions: + ktx: Column recent procurement action count from int_customer_health_signals. + - name: risk_level + type: string + descriptions: + user: "Derived risk level: low, medium, high" +joins: [] +measures: [] +descriptions: + user: Support-ticket and recent-procurement signals for customer health risk. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_parent_account_arr_movements.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_parent_account_arr_movements.yaml new file mode 100644 index 00000000..e053ab6e --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_parent_account_arr_movements.yaml @@ -0,0 +1,49 @@ +name: int_parent_account_arr_movements +table: orbit_analytics.int_parent_account_arr_movements +grain: + - arr_movement_id +columns: + - name: arr_movement_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: parent_account_id + type: string + descriptions: + ktx: Identifier for the related parent account on int_parent_account_arr_movements. + - name: movement_type + type: string + descriptions: + user: "dbt: accepted_values [new, expansion, contraction, churn, reactivation]" + - name: is_discount_expiration_contraction + type: boolean + descriptions: + user: Discount expiration contraction flag used to keep discount movement separate from churn. + - name: movement_date + type: time + descriptions: + ktx: Date or time value for movement date on int_parent_account_arr_movements. + - name: arr_cents + type: number + descriptions: + ktx: Column arr cents from int_parent_account_arr_movements. +joins: [] +measures: + - name: expansion_arr_cents + expr: sum(arr_cents) + filter: movement_type = 'expansion' + description: Sum of expansion ARR movements in cents. + - name: contraction_arr_cents + expr: sum(arr_cents) + filter: movement_type = 'contraction' + description: Sum of contraction ARR movements in cents. + - name: churn_arr_cents + expr: sum(arr_cents) + filter: movement_type = 'churn' + description: Sum of churn ARR movements in cents. + - name: discount_expiration_contraction_arr_cents + expr: sum(arr_cents) + filter: is_discount_expiration_contraction = true + description: Contraction ARR from discount expirations — kept separate from churn in NRR calculation. +descriptions: + user: Parent-account movement rollups for retention metrics. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_procurement_qualifying_actions.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_procurement_qualifying_actions.yaml new file mode 100644 index 00000000..03fdc513 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_procurement_qualifying_actions.yaml @@ -0,0 +1,27 @@ +name: int_procurement_qualifying_actions +table: orbit_analytics.int_procurement_qualifying_actions +grain: + - purchase_request_id +columns: + - name: purchase_request_id + type: string + descriptions: + ktx: Identifier for the related purchase request on int_procurement_qualifying_actions. + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on int_procurement_qualifying_actions. + - name: requester_user_id + type: string + descriptions: + ktx: Identifier for the related requester user on int_procurement_qualifying_actions. + - name: action_week + type: time + descriptions: + ktx: Column action week from int_procurement_qualifying_actions. +joins: [] +measures: + - name: qualifying_action_count + expr: count(purchase_request_id) +descriptions: + user: Non-internal, non-test requester activity for large active contracts in the golden week. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_revenue_components.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_revenue_components.yaml new file mode 100644 index 00000000..3bc25415 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/int_revenue_components.yaml @@ -0,0 +1,37 @@ +name: int_revenue_components +table: orbit_analytics.int_revenue_components +grain: + - revenue_date +columns: + - name: revenue_date + type: time + descriptions: + ktx: Date or time value for revenue date on int_revenue_components. + - name: gross_revenue_cents + type: number + descriptions: + ktx: Column gross revenue cents from int_revenue_components. + - name: credit_cents + type: number + descriptions: + ktx: Column credit cents from int_revenue_components. + - name: refund_cents + type: number + descriptions: + ktx: Column refund cents from int_revenue_components. + - name: net_revenue_cents + type: number + descriptions: + ktx: Column net revenue cents from int_revenue_components. +joins: [] +measures: + - name: total_gross_revenue_cents + expr: sum(gross_revenue_cents) + - name: total_credit_cents + expr: sum(credit_cents) + - name: total_refund_cents + expr: sum(refund_cents) + - name: total_net_revenue_cents + expr: sum(net_revenue_cents) +descriptions: + user: Daily gross, credit, refund, and net revenue components. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_activity.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_activity.yaml new file mode 100644 index 00000000..973a5ecb --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_activity.yaml @@ -0,0 +1,23 @@ +name: mart_account_activity +table: orbit_analytics.mart_account_activity +grain: + - policy_version +columns: + - name: policy_version + type: string + descriptions: + user: pre_2026_01_15 or post_2026_01_15 + - name: activated_account_count + type: number + descriptions: + ktx: Column activated account count from mart_account_activity. + - name: window_label + type: string + descriptions: + ktx: Column window label from mart_account_activity. +joins: [] +measures: + - name: total_activated_accounts + expr: sum(activated_account_count) +descriptions: + user: "Activation policy comparison values. Governed metric: activated_accounts. Owner: growth. See notion://notion_page_activation_policy_decision#policy-change." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_segments.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_segments.yaml new file mode 100644 index 00000000..cc08bbf7 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_account_segments.yaml @@ -0,0 +1,27 @@ +name: mart_account_segments +table: orbit_analytics.mart_account_segments +grain: + - account_id +columns: + - name: account_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: normalized_plan_code + type: string + descriptions: + user: pro_plus is normalized to growth through plans.canonical_plan_code. + - name: size_band + type: string + descriptions: + ktx: Column size band from mart_account_segments. + - name: segment + type: string + descriptions: + user: "Reporting segment: self_serve, commercial, enterprise" +joins: [] +measures: + - name: account_count + expr: count(account_id) +descriptions: + user: "Current plan, size band, and reporting segment for accounts. Governed metric: segment. Owner: sales_ops. See notion://notion_page_sales_ops_segmentation#growth-plan-normalization." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_arr_daily.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_arr_daily.yaml new file mode 100644 index 00000000..17a0e0e5 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_arr_daily.yaml @@ -0,0 +1,20 @@ +name: mart_arr_daily +table: orbit_analytics.mart_arr_daily +grain: + - metric_date +columns: + - name: metric_date + type: time + descriptions: + user: "dbt: not_null, unique" + - name: arr_cents + type: number + descriptions: + user: "ARR in cents. dbt assertion: expected value 1874200000 (i.e. $18,742,000) as of 2026-03-31." +joins: [] +measures: + - name: arr_cents + expr: sum(arr_cents) + description: Total ARR in cents across metric dates. +descriptions: + user: "Board-prep ARR as of the metric date. Governed metric: arr. Owner: finance. Contract-first ARR calculation — see notion://notion_page_arr_contract_reporting#arr-contract-first." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_customer_health.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_customer_health.yaml new file mode 100644 index 00000000..b21744c0 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_customer_health.yaml @@ -0,0 +1,30 @@ +name: mart_customer_health +table: orbit_analytics.mart_customer_health +grain: + - account_id +columns: + - name: account_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: risk_level + type: string + descriptions: + user: "dbt: accepted_values [low, medium, high]" + - name: open_critical_ticket_count + type: number + descriptions: + ktx: Column open critical ticket count from mart_customer_health. + - name: recent_procurement_action_count + type: number + descriptions: + ktx: Column recent procurement action count from mart_customer_health. +joins: [] +measures: + - name: account_count + expr: count(account_id) + - name: high_risk_account_count + expr: count(account_id) + filter: risk_level = 'high' +descriptions: + user: "Customer-health risk mart as of 2026-03-31. Governed metric: active_customers. Owner: customer_success. See notion://notion_page_customer_health_playbook#risk-definition." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_nrr_quarterly.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_nrr_quarterly.yaml new file mode 100644 index 00000000..fc61d756 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_nrr_quarterly.yaml @@ -0,0 +1,22 @@ +name: mart_nrr_quarterly +table: orbit_analytics.mart_nrr_quarterly +grain: + - quarter_label + - segment +columns: + - name: quarter_label + type: string + descriptions: + user: "dbt: not_null. Format: YYYY-QN (e.g. 2026-Q1)." + - name: segment + type: string + descriptions: + user: Reporting segment (self_serve, commercial, enterprise). + - name: net_revenue_retention + type: number + descriptions: + user: "NRR ratio. dbt assertions: enterprise 2026-Q1 = 1.018; enterprise 2025-Q4 = 1.064." +joins: [] +measures: [] +descriptions: + user: "Enterprise quarterly net revenue retention. Governed metric: net_revenue_retention. Owner: analytics. See notion://notion_page_retention_policy_current#nrr-definition." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_procurement_activity.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_procurement_activity.yaml new file mode 100644 index 00000000..4579d473 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_procurement_activity.yaml @@ -0,0 +1,24 @@ +name: mart_procurement_activity +table: orbit_analytics.mart_procurement_activity +grain: + - week_start + - account_id +columns: + - name: week_start + type: time + descriptions: + ktx: Date or time value for week start on mart_procurement_activity. + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on mart_procurement_activity. + - name: active_requesters + type: number + descriptions: + user: Weekly active requesters for large active contracts. +joins: [] +measures: + - name: total_active_requesters + expr: sum(active_requesters) +descriptions: + user: "Weekly active requester counts for large active contracts. Governed metric: weekly_active_requesters. Owner: product. See notion://notion_page_procurement_instrumentation#qualifying-procurement-actions." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_retention_movement_breakout.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_retention_movement_breakout.yaml new file mode 100644 index 00000000..7ef31a27 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_retention_movement_breakout.yaml @@ -0,0 +1,31 @@ +name: mart_retention_movement_breakout +table: orbit_analytics.mart_retention_movement_breakout +grain: + - movement_type + - movement_reason +columns: + - name: movement_type + type: string + descriptions: + user: "dbt: accepted_values [expansion, contraction, churn]" + - name: movement_reason + type: string + descriptions: + user: Includes discount_expiration contraction, which is not churn. + - name: parent_account_count + type: number + descriptions: + user: "dbt assertion: 11 parent accounts where movement_type='contraction' and movement_reason='discount_expiration'." + - name: expansion_arr_cents + type: number + descriptions: + user: Expansion ARR cents for Q1 enterprise movement rows. +joins: [] +measures: + - name: total_parent_account_count + expr: sum(parent_account_count) + - name: total_expansion_arr_cents + expr: sum(expansion_arr_cents) + filter: movement_type = 'expansion' +descriptions: + user: "Q1 2026 enterprise retention movement breakout. Governed metric: net_revenue_retention. Owner: analytics. See notion://notion_page_retention_policy_current#discount-expiration-treatment." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_revenue_daily.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_revenue_daily.yaml new file mode 100644 index 00000000..ad876f78 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/mart_revenue_daily.yaml @@ -0,0 +1,37 @@ +name: mart_revenue_daily +table: orbit_analytics.mart_revenue_daily +grain: + - revenue_date +columns: + - name: revenue_date + type: time + descriptions: + user: "dbt: not_null, unique" + - name: reconciliation_check + type: boolean + descriptions: + user: "dbt assertion: must be true on every row." + - name: net_revenue_cents + type: number + descriptions: + user: Daily net revenue in cents. February 2026 total covered by assert_february_2026_net_revenue. + - name: gross_revenue_cents + type: number + descriptions: + ktx: Column gross revenue cents from mart_revenue_daily. + - name: credit_cents + type: number + descriptions: + ktx: Column credit cents from mart_revenue_daily. + - name: refund_cents + type: number + descriptions: + ktx: Column refund cents from mart_revenue_daily. +joins: [] +measures: + - name: total_net_revenue_cents + expr: sum(net_revenue_cents) + - name: total_gross_revenue_cents + expr: sum(gross_revenue_cents) +descriptions: + user: "Daily revenue mart reconciling gross, credits, refunds, and net revenue. Governed metric: net_revenue. Owner: finance. See notion://notion_page_revenue_reporting_policy#gross-to-net-reconciliation." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_hierarchy.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_hierarchy.yaml new file mode 100644 index 00000000..ff2011f0 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_hierarchy.yaml @@ -0,0 +1,25 @@ +name: stg_account_hierarchy +table: orbit_analytics.stg_account_hierarchy +grain: + - account_hierarchy_id +columns: + - name: account_hierarchy_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: parent_account_id + type: string + descriptions: + ktx: Identifier for the related parent account on stg_account_hierarchy. + - name: child_account_id + type: string + descriptions: + ktx: Identifier for the related child account on stg_account_hierarchy. + - name: relationship_type + type: string + descriptions: + user: "dbt: accepted_values [subsidiary, division, billing_group]" +joins: [] +measures: [] +descriptions: + user: Parent-child account relationships used for enterprise retention grain. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_owners.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_owners.yaml new file mode 100644 index 00000000..86e6ca22 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_account_owners.yaml @@ -0,0 +1,33 @@ +name: stg_account_owners +table: orbit_analytics.stg_account_owners +grain: + - account_owner_id +columns: + - name: account_owner_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_account_owners. + - name: owner_user_id + type: string + descriptions: + ktx: Identifier for the related owner user on stg_account_owners. + - name: owner_team + type: string + descriptions: + user: "dbt: accepted_values [sales_ops, customer_success, finance]" + - name: effective_from + type: time + descriptions: + ktx: Column effective from from stg_account_owners. + - name: effective_to + type: time + descriptions: + ktx: Column effective to from stg_account_owners. +joins: [] +measures: [] +descriptions: + user: Effective-dated ownership assignments for account health, renewals, and escalation context. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_accounts.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_accounts.yaml new file mode 100644 index 00000000..24bc3c92 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_accounts.yaml @@ -0,0 +1,25 @@ +name: stg_accounts +table: orbit_analytics.stg_accounts +grain: + - account_id +columns: + - name: account_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: sales_region + type: string + descriptions: + user: "dbt: accepted_values [na, emea, apac]" + - name: size_band + type: string + descriptions: + user: "dbt: accepted_values [smb, mid_market, enterprise]" + - name: lifecycle_status + type: string + descriptions: + user: "dbt: accepted_values [prospect, active, churned, internal, test]" +joins: [] +measures: [] +descriptions: + user: Customer and internal/test account records for Orbit. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_activation_events.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_activation_events.yaml new file mode 100644 index 00000000..10247052 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_activation_events.yaml @@ -0,0 +1,33 @@ +name: stg_activation_events +table: orbit_analytics.stg_activation_events +grain: + - activation_event_id +columns: + - name: activation_event_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_activation_events. + - name: user_id + type: string + descriptions: + ktx: Identifier for the related user on stg_activation_events. + - name: event_type + type: string + descriptions: + user: "dbt: accepted_values [first_requester_login, requester_activated, first_approved_purchase_request, account_activated]" + - name: policy_version + type: string + descriptions: + user: "dbt: accepted_values [pre_2026_01_15, post_2026_01_15]" + - name: event_at + type: time + descriptions: + ktx: Column event at from stg_activation_events. +joins: [] +measures: [] +descriptions: + user: Account and requester activation events across the January policy change. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_approval_events.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_approval_events.yaml new file mode 100644 index 00000000..8e9bd5e8 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_approval_events.yaml @@ -0,0 +1,25 @@ +name: stg_approval_events +table: orbit_analytics.stg_approval_events +grain: + - approval_event_id +columns: + - name: approval_event_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: purchase_request_id + type: string + descriptions: + ktx: Identifier for the related purchase request on stg_approval_events. + - name: decision + type: string + descriptions: + user: "dbt: accepted_values [approved, rejected, returned]" + - name: decided_at + type: time + descriptions: + ktx: Column decided at from stg_approval_events. +joins: [] +measures: [] +descriptions: + user: Approval decisions tied to procurement requests. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_arr_movements.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_arr_movements.yaml new file mode 100644 index 00000000..1e625a9e --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_arr_movements.yaml @@ -0,0 +1,29 @@ +name: stg_arr_movements +table: orbit_analytics.stg_arr_movements +grain: + - arr_movement_id +columns: + - name: arr_movement_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_arr_movements. + - name: movement_type + type: string + descriptions: + user: "dbt: accepted_values [new, expansion, contraction, churn, reactivation]" + - name: movement_date + type: time + descriptions: + ktx: Date or time value for movement date on stg_arr_movements. + - name: arr_cents + type: number + descriptions: + ktx: Column arr cents from stg_arr_movements. +joins: [] +measures: [] +descriptions: + user: ARR movement ledger used by retention and expansion marts. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contract_discount_terms.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contract_discount_terms.yaml new file mode 100644 index 00000000..90332ff8 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contract_discount_terms.yaml @@ -0,0 +1,25 @@ +name: stg_contract_discount_terms +table: orbit_analytics.stg_contract_discount_terms +grain: + - discount_term_id +columns: + - name: discount_term_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: contract_id + type: string + descriptions: + ktx: Identifier for the related contract on stg_contract_discount_terms. + - name: discount_type + type: string + descriptions: + user: "dbt: accepted_values [launch, renewal, migration, goodwill]" + - name: expiry_date + type: time + descriptions: + ktx: Date or time value for expiry date on stg_contract_discount_terms. +joins: [] +measures: [] +descriptions: + user: Contract discount terms that explain Q1 2026 enterprise contraction movement. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contracts.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contracts.yaml new file mode 100644 index 00000000..6ea74539 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_contracts.yaml @@ -0,0 +1,29 @@ +name: stg_contracts +table: orbit_analytics.stg_contracts +grain: + - contract_id +columns: + - name: contract_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_contracts. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [draft, active, cancelled, expired]" + - name: renewal_type + type: string + descriptions: + user: "dbt: accepted_values [new, renewal, expansion, downgrade]" + - name: arr_cents + type: number + descriptions: + ktx: Column arr cents from stg_contracts. +joins: [] +measures: [] +descriptions: + user: Contract records that provide contract-first ARR for active accounts. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoice_line_items.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoice_line_items.yaml new file mode 100644 index 00000000..f67bf4e1 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoice_line_items.yaml @@ -0,0 +1,25 @@ +name: stg_invoice_line_items +table: orbit_analytics.stg_invoice_line_items +grain: + - invoice_line_item_id +columns: + - name: invoice_line_item_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: invoice_id + type: string + descriptions: + ktx: Identifier for the related invoice on stg_invoice_line_items. + - name: line_item_type + type: string + descriptions: + user: "dbt: accepted_values [subscription, seat, usage, addon, credit]" + - name: amount_cents + type: number + descriptions: + ktx: Column amount cents from stg_invoice_line_items. +joins: [] +measures: [] +descriptions: + user: Invoice line items used to split gross revenue, credits, seats, usage, and addons. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoices.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoices.yaml new file mode 100644 index 00000000..7a74c203 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_invoices.yaml @@ -0,0 +1,33 @@ +name: stg_invoices +table: orbit_analytics.stg_invoices +grain: + - invoice_id +columns: + - name: invoice_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_invoices. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [draft, open, paid, void, failed]" + - name: currency + type: string + descriptions: + user: "dbt: accepted_values [USD] — USD only" + - name: invoice_date + type: time + descriptions: + ktx: Date or time value for invoice date on stg_invoices. + - name: gross_amount_cents + type: number + descriptions: + ktx: Column gross amount cents from stg_invoices. +joins: [] +measures: [] +descriptions: + user: Billing invoices that anchor gross revenue recognition dates. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plan_segment_mapping.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plan_segment_mapping.yaml new file mode 100644 index 00000000..c09a06cb --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plan_segment_mapping.yaml @@ -0,0 +1,33 @@ +name: stg_plan_segment_mapping +table: orbit_analytics.stg_plan_segment_mapping +grain: + - plan_segment_mapping_id +columns: + - name: plan_segment_mapping_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: canonical_plan_code + type: string + descriptions: + user: "dbt: accepted_values [starter, growth, enterprise]" + - name: size_band + type: string + descriptions: + user: "dbt: accepted_values [smb, mid_market, enterprise]" + - name: segment + type: string + descriptions: + user: "dbt: accepted_values [self_serve, commercial, enterprise]" + - name: effective_from + type: time + descriptions: + ktx: Column effective from from stg_plan_segment_mapping. + - name: effective_to + type: time + descriptions: + ktx: Column effective to from stg_plan_segment_mapping. +joins: [] +measures: [] +descriptions: + user: Effective-dated mapping from canonical plans and size bands to reporting segments. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plans.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plans.yaml new file mode 100644 index 00000000..225b91f7 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_plans.yaml @@ -0,0 +1,17 @@ +name: stg_plans +table: orbit_analytics.stg_plans +grain: + - plan_id +columns: + - name: plan_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: canonical_plan_code + type: string + descriptions: + user: "dbt: accepted_values [starter, growth, enterprise]. Note: pro_plus is normalized to growth." +joins: [] +measures: [] +descriptions: + user: Canonical and historical Orbit pricing plans. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_orders.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_orders.yaml new file mode 100644 index 00000000..eb037363 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_orders.yaml @@ -0,0 +1,29 @@ +name: stg_purchase_orders +table: orbit_analytics.stg_purchase_orders +grain: + - purchase_order_id +columns: + - name: purchase_order_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: purchase_request_id + type: string + descriptions: + ktx: Identifier for the related purchase request on stg_purchase_orders. + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_purchase_orders. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [created, sent, fulfilled, cancelled]" + - name: created_at + type: time + descriptions: + ktx: Date or time value for created at on stg_purchase_orders. +joins: [] +measures: [] +descriptions: + user: Purchase orders generated from approved procurement requests. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_requests.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_requests.yaml new file mode 100644 index 00000000..36666900 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_purchase_requests.yaml @@ -0,0 +1,29 @@ +name: stg_purchase_requests +table: orbit_analytics.stg_purchase_requests +grain: + - purchase_request_id +columns: + - name: purchase_request_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_purchase_requests. + - name: requester_user_id + type: string + descriptions: + ktx: Identifier for the related requester user on stg_purchase_requests. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [draft, submitted, approved, rejected, cancelled]" + - name: submitted_at + type: time + descriptions: + ktx: Column submitted at from stg_purchase_requests. +joins: [] +measures: [] +descriptions: + user: Procurement request records used for activation, requester activity, and health signals. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_refunds.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_refunds.yaml new file mode 100644 index 00000000..a972f580 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_refunds.yaml @@ -0,0 +1,29 @@ +name: stg_refunds +table: orbit_analytics.stg_refunds +grain: + - refund_id +columns: + - name: refund_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: invoice_id + type: string + descriptions: + ktx: Identifier for the related invoice on stg_refunds. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [pending, succeeded, failed, cancelled]" + - name: refund_date + type: time + descriptions: + ktx: Date or time value for refund date on stg_refunds. + - name: amount_cents + type: number + descriptions: + ktx: Column amount cents from stg_refunds. +joins: [] +measures: [] +descriptions: + user: Refund events that reduce net revenue in the refund month. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_sessions.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_sessions.yaml new file mode 100644 index 00000000..a7595052 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_sessions.yaml @@ -0,0 +1,25 @@ +name: stg_sessions +table: orbit_analytics.stg_sessions +grain: + - session_id +columns: + - name: session_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_sessions. + - name: user_id + type: string + descriptions: + ktx: Identifier for the related user on stg_sessions. + - name: started_at + type: time + descriptions: + ktx: Column started at from stg_sessions. +joins: [] +measures: [] +descriptions: + user: Product sessions used for pre-policy activation and activity exclusions. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_subscriptions.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_subscriptions.yaml new file mode 100644 index 00000000..d2612489 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_subscriptions.yaml @@ -0,0 +1,25 @@ +name: stg_subscriptions +table: orbit_analytics.stg_subscriptions +grain: + - subscription_id +columns: + - name: subscription_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_subscriptions. + - name: status + type: string + descriptions: + user: "dbt: accepted_values [active, cancelled, past_due, trialing]" + - name: arr_cents + type: number + descriptions: + ktx: Column arr cents from stg_subscriptions. +joins: [] +measures: [] +descriptions: + user: Subscription rows used when active contract ARR is not present for a covered period. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_supplier_onboarding_events.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_supplier_onboarding_events.yaml new file mode 100644 index 00000000..853265db --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_supplier_onboarding_events.yaml @@ -0,0 +1,29 @@ +name: stg_supplier_onboarding_events +table: orbit_analytics.stg_supplier_onboarding_events +grain: + - supplier_onboarding_event_id +columns: + - name: supplier_onboarding_event_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: supplier_id + type: string + descriptions: + ktx: Identifier for the related supplier on stg_supplier_onboarding_events. + - name: event_type + type: string + descriptions: + user: "dbt: accepted_values [invited, profile_started, profile_completed, approved]" + - name: status + type: string + descriptions: + user: "dbt: accepted_values [pending, completed, blocked]" + - name: occurred_at + type: time + descriptions: + ktx: Column occurred at from stg_supplier_onboarding_events. +joins: [] +measures: [] +descriptions: + user: Supplier onboarding milestones that qualify as procurement workflow activity. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_suppliers.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_suppliers.yaml new file mode 100644 index 00000000..655787e8 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_suppliers.yaml @@ -0,0 +1,21 @@ +name: stg_suppliers +table: orbit_analytics.stg_suppliers +grain: + - supplier_id +columns: + - name: supplier_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: status + type: string + descriptions: + user: "dbt: accepted_values [invited, onboarding, active, inactive]" + - name: name + type: string + descriptions: + ktx: Column name from stg_suppliers. +joins: [] +measures: [] +descriptions: + user: Supplier directory records associated with procurement workflow events. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_support_tickets.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_support_tickets.yaml new file mode 100644 index 00000000..f5cc4287 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_support_tickets.yaml @@ -0,0 +1,29 @@ +name: stg_support_tickets +table: orbit_analytics.stg_support_tickets +grain: + - support_ticket_id +columns: + - name: support_ticket_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_support_tickets. + - name: severity + type: string + descriptions: + user: "dbt: accepted_values [low, medium, high, critical]" + - name: status + type: string + descriptions: + user: "dbt: accepted_values [open, pending, solved, closed]" + - name: created_at + type: time + descriptions: + ktx: Date or time value for created at on stg_support_tickets. +joins: [] +measures: [] +descriptions: + user: Customer support tickets that inform account health and risk. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_users.yaml b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_users.yaml new file mode 100644 index 00000000..159c6298 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/dbt-main/stg_users.yaml @@ -0,0 +1,21 @@ +name: stg_users +table: orbit_analytics.stg_users +grain: + - user_id +columns: + - name: user_id + type: string + descriptions: + user: "dbt: not_null, unique" + - name: account_id + type: string + descriptions: + ktx: Identifier for the related account on stg_users. + - name: email + type: string + descriptions: + ktx: Column email from stg_users. +joins: [] +measures: [] +descriptions: + user: Orbit user identities shared across warehouse, Slack, Looker, Notion, and Drive artifacts. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/accounts.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/accounts.yaml deleted file mode 100644 index a9dc698f..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/accounts.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: accounts -table: accounts -description: Customer accounts with industry, region, lifecycle, and internal/test flags. -grain: - - account_id -columns: - - name: account_id - type: string - - name: parent_account_id - type: string - - name: account_name - type: string - - name: domain - type: string - - name: industry - type: string - - name: sales_region - type: string - - name: size_band - type: string - - name: lifecycle_status - type: string - - name: is_internal - type: boolean - - name: is_test - type: boolean - - name: created_at - type: time -joins: - - to: contracts - "on": "account_id = contracts.account_id" - relationship: one_to_many - - to: purchase_requests - "on": "account_id = purchase_requests.account_id" - relationship: one_to_many -measures: - - name: account_count - expr: "count(distinct account_id)" - - name: enterprise_count - expr: "count(distinct account_id)" - filter: "size_band = 'enterprise'" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/arr_movements.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/arr_movements.yaml deleted file mode 100644 index cfe4d7fb..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/arr_movements.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: arr_movements -table: arr_movements -description: ARR movement ledger for expansion, contraction, churn, and reactivation analysis. -grain: - - arr_movement_id -columns: - - name: arr_movement_id - type: string - - name: account_id - type: string - - name: parent_account_id - type: string - - name: contract_id - type: string - - name: movement_date - type: time - - name: movement_type - type: string - - name: movement_reason - type: string - - name: arr_delta_cents - type: number - - name: starting_arr_cents - type: number - - name: ending_arr_cents - type: number -joins: - - to: accounts - "on": "account_id = accounts.account_id" - relationship: many_to_one -measures: - - name: movement_count - expr: "count(*)" - - name: net_arr_delta - expr: "sum(arr_delta_cents) / 100.0" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/contracts.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/contracts.yaml deleted file mode 100644 index cf6c4c7c..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/contracts.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: contracts -table: contracts -description: Subscription contracts with ARR, plan, renewal, and status details. -grain: - - contract_id -columns: - - name: contract_id - type: string - - name: account_id - type: string - - name: parent_account_id - type: string - - name: plan_id - type: string - - name: contract_arr_cents - type: number - - name: booked_arr_cents - type: number - - name: start_date - type: time - - name: end_date - type: time - - name: status - type: string - - name: renewal_type - type: string -joins: - - to: accounts - "on": "account_id = accounts.account_id" - relationship: many_to_one -measures: - - name: contract_count - expr: "count(distinct contract_id)" - - name: total_arr - expr: "sum(contract_arr_cents) / 100.0" - filter: "status = 'active'" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/invoices.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/invoices.yaml deleted file mode 100644 index 178c6bad..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/invoices.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: invoices -table: invoices -description: Billing invoices with payment status and revenue-recognition dates. -grain: - - invoice_id -columns: - - name: invoice_id - type: string - - name: account_id - type: string - - name: subscription_id - type: string - - name: invoice_date - type: time - - name: paid_at - type: time - - name: status - type: string - - name: currency - type: string -joins: - - to: accounts - "on": "account_id = accounts.account_id" - relationship: many_to_one -measures: - - name: invoice_count - expr: "count(*)" - - name: paid_invoice_count - expr: "count(*)" - filter: "status = 'paid'" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/purchase_requests.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/purchase_requests.yaml deleted file mode 100644 index db9df059..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/purchase_requests.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: purchase_requests -table: purchase_requests -description: Procurement workflow requests with requester, status, supplier, and spend fields. -grain: - - purchase_request_id -columns: - - name: purchase_request_id - type: string - - name: account_id - type: string - - name: requester_user_id - type: string - - name: created_at - type: time - - name: status - type: string - - name: amount_cents - type: number - - name: supplier_id - type: string -joins: - - to: accounts - "on": "account_id = accounts.account_id" - relationship: many_to_one -measures: - - name: request_count - expr: "count(*)" - - name: approved_spend - expr: "sum(amount_cents) / 100.0" - filter: "status = 'approved'" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/support_tickets.yaml b/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/support_tickets.yaml deleted file mode 100644 index ddbc97e7..00000000 --- a/packages/cli/assets/demo/orbit/semantic-layer/orbit_demo/support_tickets.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: support_tickets -table: support_tickets -description: Customer support tickets with severity, category, status, and resolution tracking. -grain: - - support_ticket_id -columns: - - name: support_ticket_id - type: string - - name: account_id - type: string - - name: requester_user_id - type: string - - name: severity - type: string - - name: category - type: string - - name: status - type: string - - name: created_at - type: time - - name: resolved_at - type: time - - name: owner_user_id - type: string -joins: - - to: accounts - "on": "account_id = accounts.account_id" - relationship: many_to_one -measures: - - name: ticket_count - expr: "count(*)" - - name: open_ticket_count - expr: "count(*)" - filter: "status != 'resolved'" -segments: - - name: external_only - expr: "coalesce(is_internal, 0) = 0 AND coalesce(is_test, 0) = 0" diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_analytics.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_analytics.yaml new file mode 100644 index 00000000..f7787164 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_analytics.yaml @@ -0,0 +1,1400 @@ +tables: + int_activation_policy_windows: + table: orbit_analytics.int_activation_policy_windows + columns: + - name: policy_window + type: string + descriptions: + ai: Categorical indicator distinguishing time periods before and after a policy implementation. + - name: cohort_users + type: number + descriptions: + ai: Total number of users in each cohort being tracked for activation analysis. + - name: activated_users + type: number + descriptions: + ai: Count of users who completed activation within each policy window period. + - name: activation_rate + type: number + descriptions: + ai: Proportion of cohort users who completed activation, expressed as a decimal ratio. + descriptions: + ai: Compares user activation rates before and after a policy change, measuring its impact on converting cohort users into activated users. + int_active_contract_arr: + table: orbit_analytics.int_active_contract_arr + columns: + - name: contract_id + type: string + descriptions: + ai: Unique identifiers for active contracts, linking revenue records to specific customer agreements. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer accounts associated with active contracts and their ARR. + - name: parent_account_id + type: string + descriptions: + ai: Hierarchical identifier linking contracts to their top-level or parent organizational account. + - name: plan_id + type: string + descriptions: + ai: Identifier referencing the subscription or pricing plan associated with an active contract. + - name: contract_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) for active contracts, expressed in cents (USD). + descriptions: + ai: Active contract revenue data tracking Annual Recurring Revenue (ARR) in cents across accounts, parent accounts, and subscription plans. + joins: + - to: accounts + "on": int_active_contract_arr.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: contracts + "on": int_active_contract_arr.contract_id = contracts.contract_id + relationship: many_to_one + source: inferred + - to: plans + "on": int_active_contract_arr.plan_id = plans.plan_id + relationship: many_to_one + source: inferred + int_customer_health_signals: + table: orbit_analytics.int_customer_health_signals + columns: + - name: account_id + type: string + descriptions: + ai: Unique identifier for each customer account, used to track health signals. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a child account to its parent organization in a hierarchical account structure. + - name: account_name + type: string + descriptions: + ai: Display names of customer accounts used to identify organizations in health monitoring. + - name: is_active_customer + type: boolean + descriptions: + ai: Boolean flag indicating whether the account is currently an active paying customer. + - name: has_unresolved_high_ticket + type: boolean + descriptions: + ai: Boolean flag indicating whether the customer has at least one open, high-priority support ticket. + - name: has_recent_procurement_activity + type: boolean + descriptions: + ai: Boolean flag indicating whether the customer has shown recent purchasing or procurement activity. + - name: risk_level + type: string + descriptions: + ai: Categorical assessment of customer churn or account health risk, indicating low, medium, or high exposure. + descriptions: + ai: Customer health monitoring data tracking account activity, support ticket status, procurement behavior, and risk levels to assess overall customer relationship health. + joins: + - to: accounts + "on": int_customer_health_signals.account_id = accounts.account_id + relationship: many_to_one + source: inferred + int_parent_account_arr_movements: + table: orbit_analytics.int_parent_account_arr_movements + columns: + - name: arr_movement_id + type: string + descriptions: + ai: Unique identifier for each ARR movement event, formatted as a sequential alphanumeric code. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the individual customer account associated with each ARR movement record. + - name: parent_account_id + type: string + descriptions: + ai: Unique identifier for the parent/hierarchical account grouping child accounts beneath it. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking ARR movements to specific customer contracts driving revenue changes. + - name: movement_date + type: time + descriptions: + ai: Timestamp of when an ARR movement event occurred for a parent account. + - name: quarter_start_date + type: time + descriptions: + ai: Start date of the fiscal quarter in which the ARR movement occurred. + - name: quarter_label + type: string + descriptions: + ai: Fiscal quarter identifier in "YYYY-QN" format, used for grouping and reporting ARR movements. + - name: segment + type: string + descriptions: + ai: Customer tier or market classification, such as enterprise, mid-market, or SMB. + - name: movement_type + type: string + descriptions: + ai: Categorizes ARR changes by type, such as expansion, contraction, churn, or reactivation. + - name: movement_reason + type: string + descriptions: + ai: Categorizes the underlying cause driving an ARR change, such as seat growth or contraction. + - name: arr_delta_cents + type: number + descriptions: + ai: Monetary change in Annual Recurring Revenue for a movement event, stored in cents. + - name: starting_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) in cents at the beginning of a movement period. + - name: ending_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue balance in cents after applying the recorded ARR movement. + - name: expansion_arr_cents + type: number + descriptions: + ai: Monetary value of ARR growth from expansions, stored in cents at the parent account level. + - name: contraction_arr_cents + type: number + descriptions: + ai: Monetary value (in cents) of ARR decreases due to contraction events; zero indicates no contraction. + - name: churned_arr_cents + type: number + descriptions: + ai: Monetary value in cents of ARR lost due to customer churn; zero indicates no churn. + - name: is_discount_expiration_contraction + type: boolean + descriptions: + ai: Boolean flag indicating whether a contraction in ARR resulted from a discount expiring. + - name: is_reactivation + type: boolean + descriptions: + ai: Boolean flag indicating whether an ARR movement represents a previously churned account returning as a customer. + descriptions: + ai: Tracks ARR (Annual Recurring Revenue) movements aggregated at the parent account level, capturing subscription revenue changes like expansions across billing periods. + joins: + - to: arr_movements + "on": int_parent_account_arr_movements.arr_movement_id = arr_movements.arr_movement_id + relationship: many_to_one + source: inferred + int_procurement_qualifying_actions: + table: orbit_analytics.int_procurement_qualifying_actions + columns: + - name: action_id + type: string + descriptions: + ai: Unique identifiers for procurement approval actions, formatted as sequential approval reference codes. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each qualifying procurement action. + - name: user_id + type: string + descriptions: + ai: Unique identifiers for users who performed qualifying procurement actions. + - name: action_date + type: time + descriptions: + ai: Timestamps recording when qualifying procurement actions occurred, stored in Pacific timezone. + - name: action_type + type: string + descriptions: + ai: Categorizes qualifying procurement actions, such as approval events, within the procurement workflow process. + descriptions: + ai: Records of procurement approval events tied to accounts and users, used to track qualifying actions within a procurement workflow. + joins: + - to: stg_users + "on": int_procurement_qualifying_actions.user_id = stg_users.user_id + relationship: many_to_one + source: inferred + int_revenue_components: + table: orbit_analytics.int_revenue_components + columns: + - name: revenue_date + type: time + descriptions: + ai: Daily timestamps representing when revenue transactions were recorded, used for time-series financial reporting. + - name: gross_revenue_cents + type: number + descriptions: + ai: Total revenue earned before deductions, stored in cents (e.g., 3888000 = $38,880). + - name: credits_cents + type: number + descriptions: + ai: Monetary credits applied against gross revenue, stored in cents; appears consistently zero in sample. + - name: refunds_cents + type: number + descriptions: + ai: Monetary value of customer refunds issued, stored in cents, for daily revenue reconciliation. + - name: net_revenue_cents + type: number + descriptions: + ai: Calculated revenue in cents after deducting credits and refunds from gross revenue. + descriptions: + ai: Daily revenue tracking data capturing gross earnings, credits, and refunds to calculate net revenue for financial reporting and analysis. + mart_account_activity: + table: orbit_analytics.mart_account_activity + columns: + - name: policy_change_date + type: time + descriptions: + ai: Date when a policy change took effect, used to compare pre/post activation rates. + - name: pre_policy_30_day_activation_rate + type: number + descriptions: + ai: Account activation rate within 30 days before a policy change was implemented. + - name: post_policy_30_day_activation_rate + type: number + descriptions: + ai: The 30-day account activation rate measured after a policy change was implemented. + descriptions: + ai: Tracks the impact of policy changes on user activation rates by comparing 30-day engagement metrics before and after implementation. + mart_account_segments: + table: orbit_analytics.mart_account_segments + columns: + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer accounts, formatted with an "acct_" prefix and numeric suffix. + - name: parent_account_id + type: string + descriptions: + ai: Identifiers linking child accounts to their parent accounts in a hierarchical account structure. + - name: current_plan_code + type: string + descriptions: + ai: Subscription plan tier currently assigned to the account (e.g., starter, pro). + - name: normalized_plan_code + type: string + descriptions: + ai: Standardized or simplified version of the subscription plan code for consistent segmentation and reporting. + - name: size_band + type: string + descriptions: + ai: Categorization of accounts by company size, indicating small-to-medium business (SMB) classification. + - name: segment + type: string + descriptions: + ai: Business classification indicating how an account is managed or acquired, e.g., self-serve vs. sales-assisted. + - name: contract_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) for a contract, stored in cents for precision. + - name: contract_status + type: string + descriptions: + ai: Current state of a customer's contract, such as expired or cancelled. + descriptions: + ai: Customer account segmentation data used for classifying accounts by plan type, company size, sales segment, and contract value for business analytics. + joins: + - to: accounts + "on": mart_account_segments.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: plans + "on": mart_account_segments.current_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + - to: plans + "on": mart_account_segments.normalized_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + mart_arr_daily: + table: orbit_analytics.mart_arr_daily + columns: + - name: metric_date + type: time + descriptions: + ai: Daily timestamp marking when ARR metrics were recorded, used for time-series financial tracking. + - name: arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) stored in cents for precise financial calculations, avoiding floating-point errors. + - name: display + type: string + descriptions: + ai: Human-readable, formatted ARR value with currency symbol and abbreviated magnitude (e.g., millions). + descriptions: + ai: Daily snapshot of Annual Recurring Revenue (ARR) metrics, tracking subscription revenue trends over time for financial reporting and business performance monitoring. + mart_customer_health: + table: orbit_analytics.mart_customer_health + columns: + - name: as_of_date + type: time + descriptions: + ai: Snapshot date indicating when the customer health metrics were last calculated or refreshed. + - name: account_id + type: string + descriptions: + ai: Unique identifier for each customer account tracked in the health monitoring mart. + - name: parent_account_id + type: string + descriptions: + ai: Unique identifier linking child accounts to their parent organization in a hierarchical account structure. + - name: account_name + type: string + descriptions: + ai: Descriptive labels identifying customer organizations tracked for health and risk monitoring purposes. + - name: is_active_customer + type: boolean + descriptions: + ai: Boolean flag indicating whether the account is currently an active paying customer. + - name: has_unresolved_high_ticket + type: boolean + descriptions: + ai: Boolean flag indicating whether the customer account has at least one unresolved high-priority support ticket. + - name: has_recent_procurement_activity + type: boolean + descriptions: + ai: Boolean flag indicating whether the account has had recent purchasing or procurement transactions. + - name: risk_level + type: string + descriptions: + ai: Categorical assessment of customer churn or account health risk, segmented into tiers (e.g., low, medium, high). + descriptions: + ai: A snapshot of customer health metrics as of a specific date, used to assess account risk levels and engagement status for customer success management. + joins: + - to: accounts + "on": mart_customer_health.account_id = accounts.account_id + relationship: many_to_one + source: inferred + mart_nrr_quarterly: + table: orbit_analytics.mart_nrr_quarterly + columns: + - name: quarter_start_date + type: time + descriptions: + ai: Start date of each fiscal quarter used to track NRR metrics over time. + - name: quarter_label + type: string + descriptions: + ai: Human-readable fiscal quarter identifier combining year and quarter number (e.g., "2025-Q4"). + - name: segment + type: string + descriptions: + ai: Customer tier or market segment classification, such as enterprise, mid-market, or SMB. + - name: starting_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue at the beginning of each quarter, stored in cents. + - name: expansion_arr_cents + type: number + descriptions: + ai: Revenue growth in cents from existing customers upgrading or expanding their subscriptions quarterly. + - name: contraction_arr_cents + type: number + descriptions: + ai: Reduction in recurring revenue from existing customers who downgraded, measured in cents. + - name: churned_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue lost due to customer cancellations in a quarter, measured in cents. + - name: net_revenue_retention + type: number + descriptions: + ai: Ratio of retained and expanded revenue to starting ARR, typically expressed as a decimal multiplier. + descriptions: + ai: Quarterly Net Revenue Retention metrics tracking ARR movements (expansion, contraction, churn) by customer segment to measure revenue growth and customer health. + mart_procurement_activity: + table: orbit_analytics.mart_procurement_activity + columns: + - name: week_start_date + type: time + descriptions: + ai: Start date of a weekly procurement activity reporting period. + - name: week_end_date + type: time + descriptions: + ai: End date of a weekly procurement activity reporting period. + - name: contract_arr_threshold_cents + type: number + descriptions: + ai: Minimum annual recurring revenue threshold (in cents) for contract procurement eligibility, here $200,000. + - name: active_requesters + type: number + descriptions: + ai: Count of unique users who submitted procurement requests during the given week. + descriptions: + ai: Weekly procurement activity metrics tracking active requesters and contract value thresholds, used for monitoring purchasing behavior and procurement pipeline management. + mart_retention_movement_breakout: + table: orbit_analytics.mart_retention_movement_breakout + columns: + - name: quarter_start_date + type: time + descriptions: + ai: Start date of the fiscal quarter used to group retention movement data. + - name: quarter_label + type: string + descriptions: + ai: Human-readable fiscal quarter identifier combining year and quarter number (e.g., "2026-Q1"). + - name: segment + type: string + descriptions: + ai: Customer tier or market segment classification, such as enterprise, mid-market, or SMB. + - name: movement_type + type: string + descriptions: + ai: Categorizes ARR changes as growth (expansion), reduction (contraction), or full cancellation (churn). + - name: movement_reason + type: string + descriptions: + ai: Categorical reasons driving customer retention movements such as budget loss, discounts, or seat changes. + - name: parent_account_count + type: number + descriptions: + ai: Count of distinct parent accounts experiencing a specific retention movement type within a quarter. + - name: expansion_arr_cents + type: number + descriptions: + ai: Monetary value of ARR growth from account expansions, stored in cents. + - name: contraction_arr_cents + type: number + descriptions: + ai: Monetary value of ARR decreases from existing accounts, stored in cents. + - name: churned_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue lost due to customer churn, expressed in cents, per segment and quarter. + descriptions: + ai: Tracks quarterly ARR movement (churn, contraction, expansion) by customer segment and reason, enabling retention analysis and revenue trend monitoring across enterprise accounts. + mart_revenue_daily: + table: orbit_analytics.mart_revenue_daily + columns: + - name: revenue_date + type: time + descriptions: + ai: Daily timestamp representing the specific calendar date for revenue aggregation and reporting. + - name: gross_revenue_cents + type: number + descriptions: + ai: Total revenue earned before deductions, stored in cents, for a given daily period. + - name: credits_cents + type: number + descriptions: + ai: Monetary credits applied against revenue, stored in cents; appears consistently zero in sample data. + - name: refunds_cents + type: number + descriptions: + ai: Total refund amounts issued on a given day, stored in cents for precision. + - name: net_revenue_cents + type: number + descriptions: + ai: Daily net revenue in cents after deducting credits and refunds from gross revenue. + - name: reconciliation_check + type: boolean + descriptions: + ai: Boolean flag verifying that gross revenue minus credits and refunds equals net revenue. + descriptions: + ai: Daily revenue tracking for financial reporting, capturing gross sales, credits, refunds, and net revenue with reconciliation validation. + stg_account_hierarchy: + table: orbit_analytics.stg_account_hierarchy + columns: + - name: account_hierarchy_id + type: string + descriptions: + ai: Unique identifiers for parent-child account relationship records within the organizational hierarchy. + - name: parent_account_id + type: string + descriptions: + ai: Identifiers for the higher-level accounts in a parent-child account relationship hierarchy. + - name: child_account_id + type: string + descriptions: + ai: Unique identifiers for subordinate accounts within a parent-child account hierarchy relationship. + - name: relationship_type + type: string + descriptions: + ai: Categorizes the structural or organizational relationship between parent and child accounts. + - name: effective_start_date + type: time + descriptions: + ai: Start date when a parent-child account relationship becomes active or valid. + - name: effective_end_date + type: time + descriptions: + ai: End date when the parent-child account relationship expires or becomes inactive. + descriptions: + ai: Staging data capturing parent-child account relationships, including hierarchy types and date-bounded validity periods, used for organizational account structure management. + joins: + - to: account_hierarchy + "on": stg_account_hierarchy.account_hierarchy_id = account_hierarchy.account_hierarchy_id + relationship: many_to_one + source: inferred + stg_account_owners: + table: orbit_analytics.stg_account_owners + columns: + - name: account_owner_id + type: string + descriptions: + ai: Unique identifiers for account ownership records, linking accounts to their assigned owners. + - name: account_id + type: string + descriptions: + ai: Unique identifiers linking ownership records to specific customer or business accounts. + - name: owner_user_id + type: string + descriptions: + ai: Unique identifier for the user assigned as an account owner. + - name: owner_team + type: string + descriptions: + ai: Team responsible for owning or managing the account, such as sales or customer success. + - name: role + type: string + descriptions: + ai: Business function or responsibility of the account owner, such as sales, success, or finance. + - name: effective_start_date + type: time + descriptions: + ai: Start date when an account owner's responsibility or role becomes active. + - name: effective_end_date + type: time + descriptions: + ai: End date marking when an account ownership assignment expires or becomes inactive. + descriptions: + ai: Tracks ownership assignments of accounts to users and teams, including their roles and the time periods during which those assignments are active. + joins: + - to: accounts + "on": stg_account_owners.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: account_owners + "on": stg_account_owners.account_owner_id = account_owners.account_owner_id + relationship: many_to_one + source: inferred + stg_accounts: + table: orbit_analytics.stg_accounts + columns: + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer or business accounts, prefixed with "acct_" followed by a numeric code. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a child account to its parent in a hierarchical account structure. + - name: account_name + type: string + descriptions: + ai: Display name of the customer or business account entity. + - name: domain + type: string + descriptions: + ai: Unique web domain identifiers associated with each customer account, used for account identification. + - name: industry + type: string + descriptions: + ai: Business sector or vertical market classification of the account organization. + - name: sales_region + type: string + descriptions: + ai: Geographic sales territory classification (North America, EMEA, Asia-Pacific) for account-based revenue segmentation. + - name: size_band + type: string + descriptions: + ai: Categorical classification of account size, segmenting customers by organizational scale (e.g., enterprise, SMB). + - name: lifecycle_status + type: string + descriptions: + ai: Current stage of an account in its business relationship lifecycle (e.g., active, churned). + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag identifying whether the account belongs to the company internally, rather than an external customer. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether the account is a test/dummy record to be excluded from analysis. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each account record was first created in the system. + descriptions: + ai: Staged customer account records used for CRM and sales operations, capturing organizational profiles across industries, regions, and lifecycle stages. + joins: + - to: accounts + "on": stg_accounts.account_id = accounts.account_id + relationship: many_to_one + source: inferred + stg_activation_events: + table: orbit_analytics.stg_activation_events + columns: + - name: activation_event_id + type: string + descriptions: + ai: Unique identifiers for individual activation events, formatted with a sequential numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer or business accounts associated with activation events. + - name: user_id + type: string + descriptions: + ai: Unique identifiers for users associated with activation events, formatted as "user\_XXXXXX". + - name: event_type + type: string + descriptions: + ai: Categorical label identifying the specific activation milestone or action completed by a user. + - name: event_at + type: time + descriptions: + ai: Timestamp recording when an activation event occurred, stored in Pacific Standard Time. + - name: policy_version + type: string + descriptions: + ai: Version identifier of the policy in effect at the time of the activation event. + descriptions: + ai: Tracks milestone activation events for user accounts, capturing when users first engage with the platform under specific policy versions. + joins: + - to: activation_events + "on": stg_activation_events.activation_event_id = activation_events.activation_event_id + relationship: many_to_one + source: inferred + stg_approval_events: + table: orbit_analytics.stg_approval_events + columns: + - name: approval_event_id + type: string + descriptions: + ai: Unique identifiers for individual approval events, formatted as sequential alphanumeric strings. + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifiers linking approval events to their associated purchase requests. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each approval event. + - name: approver_user_id + type: string + descriptions: + ai: Unique identifiers for users who reviewed and made approval decisions on purchase requests. + - name: decision + type: string + descriptions: + ai: Outcome of an approver's review on a purchase request, such as approved or rejected. + - name: decided_at + type: time + descriptions: + ai: Timestamp recording when an approver made their decision on a purchase request. + descriptions: + ai: Staging data capturing purchase request approval decisions, tracking which approvers acted on requests and when decisions were made. + joins: + - to: approval_events + "on": stg_approval_events.approval_event_id = approval_events.approval_event_id + relationship: many_to_one + source: inferred + - to: purchase_requests + "on": stg_approval_events.purchase_request_id = purchase_requests.purchase_request_id + relationship: many_to_one + source: inferred + stg_arr_movements: + table: orbit_analytics.stg_arr_movements + columns: + - name: arr_movement_id + type: string + descriptions: + ai: Unique identifiers for individual ARR movement records, prefixed with "arr_move" and sequentially numbered. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the customer account associated with each ARR movement record. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a child account to its parent in a hierarchical account structure. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking ARR movements to specific customer contracts or subscription agreements. + - name: movement_date + type: time + descriptions: + ai: Timestamp recording when an ARR movement event occurred, used for tracking revenue changes over time. + - name: movement_type + type: string + descriptions: + ai: Categorical classification of ARR change events, such as expansion, contraction, churn, or new business. + - name: movement_reason + type: string + descriptions: + ai: Categorizes the specific cause driving an ARR change, such as seat expansion or contraction. + - name: arr_delta_cents + type: number + descriptions: + ai: Monetary change in Annual Recurring Revenue for a given movement, stored in cents. + - name: starting_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue value in cents before the movement was applied. + - name: ending_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) balance in cents after applying the movement transaction. + descriptions: + ai: Tracks changes in Annual Recurring Revenue (ARR) across customer accounts, capturing expansion, contraction, and churn movements for subscription revenue analysis. + joins: + - to: arr_movements + "on": stg_arr_movements.arr_movement_id = arr_movements.arr_movement_id + relationship: many_to_one + source: inferred + stg_contract_discount_terms: + table: orbit_analytics.stg_contract_discount_terms + columns: + - name: discount_term_id + type: string + descriptions: + ai: Unique identifiers for individual discount terms associated with contracts, formatted as sequential codes. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking discount terms to their associated contracts in the system. + - name: discount_type + type: string + descriptions: + ai: Categorizes the reason or occasion for applying a contract discount, such as launch or renewal. + - name: discount_cents + type: number + descriptions: + ai: Fixed discount amount in cents applied to a contract, representing a monetary reduction. + - name: discount_percent + type: number + descriptions: + ai: Percentage-based discount rate applied to a contract, expressed as a decimal (e.g., 0.10 = 10%). + - name: starts_on + type: time + descriptions: + ai: Effective start date when a contract discount term becomes active. + - name: expires_on + type: time + descriptions: + ai: End date when a contract discount term becomes inactive or no longer valid. + - name: reason + type: string + descriptions: + ai: Categorical label indicating the rationale or trigger behind a contract discount term. + descriptions: + ai: Staging data capturing time-bound discount terms applied to contracts, including discount type, value, and validity period for pricing management. + stg_contracts: + table: orbit_analytics.stg_contracts + columns: + - name: contract_id + type: string + descriptions: + ai: Unique identifiers for individual contracts, used to track and reference specific contract records. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the customer account associated with each contract. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a contract to its parent/hierarchical account in a multi-tier account structure. + - name: plan_id + type: string + descriptions: + ai: Identifier referencing the subscription or pricing plan associated with a contract. + - name: contract_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) value of the contract, stored in cents. + - name: booked_arr_cents + type: number + descriptions: + ai: Committed Annual Recurring Revenue at booking time, stored in cents (e.g., $250,000). + - name: start_date + type: time + descriptions: + ai: Date when the contract becomes active and billing or service terms begin. + - name: end_date + type: time + descriptions: + ai: Expiration date marking when a contract term officially concludes, used for renewal tracking. + - name: status + type: string + descriptions: + ai: Current lifecycle state of a contract, such as active, expired, or cancelled. + - name: renewal_type + type: string + descriptions: + ai: Classification of contract motion type, indicating whether it's new business, a renewal, expansion, or downgrade. + descriptions: + ai: Staging data for customer subscription contracts, tracking annual recurring revenue, contract terms, account relationships, and renewal lifecycle stages for revenue management. + joins: + - to: accounts + "on": stg_contracts.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: contracts + "on": stg_contracts.contract_id = contracts.contract_id + relationship: many_to_one + source: inferred + - to: plans + "on": stg_contracts.plan_id = plans.plan_id + relationship: many_to_one + source: inferred + stg_invoice_line_items: + table: orbit_analytics.stg_invoice_line_items + columns: + - name: invoice_line_item_id + type: string + descriptions: + ai: Unique identifiers for individual line items within invoices, formatted as sequential alphanumeric codes. + - name: invoice_id + type: string + descriptions: + ai: Unique identifier linking line items to their parent invoice record. + - name: line_item_type + type: string + descriptions: + ai: Categorizes invoice charges by billing type, such as subscription, usage, seat, or addon fees. + - name: amount_cents + type: number + descriptions: + ai: Monetary value of invoice line items stored in cents (e.g., $20,000.00). + - name: recognized_at + type: time + descriptions: + ai: Timestamp indicating when a invoice line item's revenue was officially recognized. + descriptions: + ai: Staging data capturing individual line items within invoices, detailing billing charges by type (subscription, seat, usage) with associated amounts and recognition dates. + joins: + - to: invoice_line_items + "on": stg_invoice_line_items.invoice_line_item_id = invoice_line_items.invoice_line_item_id + relationship: many_to_one + source: inferred + stg_invoices: + table: orbit_analytics.stg_invoices + columns: + - name: invoice_id + type: string + descriptions: + ai: Unique identifiers for individual invoices, formatted with an "inv_" prefix and sequential numbering. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer accounts associated with each invoice record. + - name: subscription_id + type: string + descriptions: + ai: Unique identifier linking each invoice to its associated subscription plan or service. + - name: invoice_date + type: time + descriptions: + ai: Date when the invoice was issued to the customer or account. + - name: paid_at + type: time + descriptions: + ai: Timestamp recording when an invoice was successfully paid, in Pacific Standard Time. + - name: status + type: string + descriptions: + ai: Current payment state of an invoice, such as paid, pending, or failed. + - name: currency + type: string + descriptions: + ai: Three-letter ISO currency code used for billing and payment processing on invoices. + descriptions: + ai: Staging data for customer invoices tracking billing activity, payment status, and subscription-level charges across accounts in USD currency. + joins: + - to: accounts + "on": stg_invoices.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: invoices + "on": stg_invoices.invoice_id = invoices.invoice_id + relationship: many_to_one + source: inferred + - to: stg_subscriptions + "on": stg_invoices.subscription_id = stg_subscriptions.subscription_id + relationship: many_to_one + source: inferred + stg_plan_segment_mapping: + table: orbit_analytics.stg_plan_segment_mapping + columns: + - name: plan_segment_mapping_id + type: string + descriptions: + ai: Unique identifiers for plan-to-segment mapping records, linking canonical plans with size bands and segments. + - name: canonical_plan_code + type: string + descriptions: + ai: Standardized plan tier labels (e.g., "starter," "growth") used to categorize subscription or pricing plans. + - name: size_band + type: string + descriptions: + ai: "Business size classification of plan segments: small-medium business, mid-market, or enterprise tiers." + - name: segment + type: string + descriptions: + ai: Business tier or market category assigned to a plan, such as self-serve, commercial, or enterprise. + - name: effective_start_date + type: time + descriptions: + ai: Start date when a plan-segment mapping record becomes active and valid for use. + - name: effective_end_date + type: time + descriptions: + ai: End date when a plan-to-segment mapping record expires or becomes inactive. + descriptions: + ai: Maps subscription plans to customer segments (SMB, mid-market, enterprise) with time-bound validity periods, enabling segment-specific plan routing and business rules. + joins: + - to: plans + "on": stg_plan_segment_mapping.canonical_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + - to: plan_segment_mapping + "on": stg_plan_segment_mapping.plan_segment_mapping_id = plan_segment_mapping.plan_segment_mapping_id + relationship: many_to_one + source: inferred + stg_plans: + table: orbit_analytics.stg_plans + columns: + - name: plan_id + type: string + descriptions: + ai: Unique identifiers for individual subscription or service plans within the system. + - name: plan_code + type: string + descriptions: + ai: Short identifier codes representing subscription tier levels (e.g., starter, growth, enterprise). + - name: plan_name + type: string + descriptions: + ai: Human-readable label for a subscription tier offered to customers. + - name: canonical_plan_code + type: string + descriptions: + ai: Standardized plan tier categories used to group or normalize various plan variants. + - name: is_retired + type: boolean + descriptions: + ai: Boolean flag indicating whether a plan has been deactivated or discontinued from active use. + - name: retired_at + type: time + descriptions: + ai: Timestamp indicating when a plan was or will be retired; far-future dates suggest active plans. + descriptions: + ai: Staging data for subscription plan tiers (Starter, Growth, Enterprise), tracking active plan configurations and retirement status for product offerings. + joins: + - to: plans + "on": stg_plans.canonical_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + - to: plans + "on": stg_plans.plan_code = plans.plan_code + relationship: many_to_one + source: inferred + - to: plans + "on": stg_plans.plan_id = plans.plan_id + relationship: many_to_one + source: inferred + stg_purchase_orders: + table: orbit_analytics.stg_purchase_orders + columns: + - name: purchase_order_id + type: string + descriptions: + ai: Unique identifier for each purchase order, formatted with a "po_" prefix and sequential number. + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifier linking a purchase order to its originating purchase request. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each purchase order transaction. + - name: supplier_id + type: string + descriptions: + ai: Unique identifier referencing the supplier fulfilling the purchase order. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each purchase order was created, stored in Pacific Time. + - name: status + type: string + descriptions: + ai: Current state of the purchase order in the procurement workflow (e.g., sent, fulfilled). + - name: amount_cents + type: number + descriptions: + ai: Monetary value of purchase orders stored in cents (e.g., $251.00–$255.00). + descriptions: + ai: Staging data capturing purchase orders issued to suppliers, tracking procurement requests, associated accounts, order amounts, and fulfillment status for purchasing workflow management. + joins: + - to: purchase_orders + "on": stg_purchase_orders.purchase_order_id = purchase_orders.purchase_order_id + relationship: many_to_one + source: inferred + - to: purchase_requests + "on": stg_purchase_orders.purchase_request_id = purchase_requests.purchase_request_id + relationship: many_to_one + source: inferred + - to: stg_suppliers + "on": stg_purchase_orders.supplier_id = stg_suppliers.supplier_id + relationship: many_to_one + source: inferred + stg_purchase_requests: + table: orbit_analytics.stg_purchase_requests + columns: + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifiers for purchase requests, prefixed with "pr_" followed by a sequential number. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each purchase request. + - name: requester_user_id + type: string + descriptions: + ai: Unique identifier of the user who submitted the purchase request. + - name: created_at + type: time + descriptions: + ai: Timestamp indicating when a purchase request was submitted, stored in UTC with timezone offset. + - name: status + type: string + descriptions: + ai: Current approval state of a purchase request in its workflow lifecycle. + - name: amount_cents + type: number + descriptions: + ai: Monetary value of purchase requests stored in cents (e.g., 25100 = $251.00). + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers referencing the supplier associated with each purchase request. + descriptions: + ai: Staging data capturing employee-initiated purchase requests submitted to suppliers for procurement approval workflows, tracking request status and associated monetary amounts. + joins: + - to: purchase_requests + "on": stg_purchase_requests.purchase_request_id = purchase_requests.purchase_request_id + relationship: many_to_one + source: inferred + - to: stg_users + "on": stg_purchase_requests.requester_user_id = stg_users.user_id + relationship: many_to_one + source: inferred + - to: stg_suppliers + "on": stg_purchase_requests.supplier_id = stg_suppliers.supplier_id + relationship: many_to_one + source: inferred + stg_refunds: + table: orbit_analytics.stg_refunds + columns: + - name: refund_id + type: string + descriptions: + ai: Unique identifiers for individual refund transactions, formatted with a sequential numeric suffix. + - name: invoice_id + type: string + descriptions: + ai: Unique identifier linking each refund to its originating invoice record. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking refunds to the associated customer or business account. + - name: amount_cents + type: number + descriptions: + ai: Monetary value of refunds in cents; sample suggests a fixed refund amount of $10,000. + - name: status + type: string + descriptions: + ai: Current state of the refund transaction (e.g., succeeded, pending, failed). + - name: refunded_at + type: time + descriptions: + ai: Timestamp indicating when a refund was processed, recorded in Pacific Standard Time. + - name: reason + type: string + descriptions: + ai: Categorical explanation for why a refund was issued, such as board reconciliation credits. + descriptions: + ai: Staging data capturing processed refund transactions issued to accounts against invoices, primarily for board reconciliation credits and financial auditing purposes. + joins: + - to: accounts + "on": stg_refunds.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: invoices + "on": stg_refunds.invoice_id = invoices.invoice_id + relationship: many_to_one + source: inferred + - to: refunds + "on": stg_refunds.refund_id = refunds.refund_id + relationship: many_to_one + source: inferred + stg_sessions: + table: orbit_analytics.stg_sessions + columns: + - name: session_id + type: string + descriptions: + ai: Unique sequential identifiers for individual user sessions, formatted with a zero-padded numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the customer or business account associated with each session. + - name: user_id + type: string + descriptions: + ai: Unique identifiers for individual users associated with each recorded session. + - name: started_at + type: time + descriptions: + ai: Timestamp marking when a user session began, stored in Pacific Time zone. + - name: duration_seconds + type: number + descriptions: + ai: Length of a user session measured in seconds, indicating engagement duration. + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag indicating whether the session originated from internal company users or staff. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether the session was generated for testing purposes. + descriptions: + ai: Staging data capturing user session activity, tracking engagement duration and filtering out internal or test traffic for analytics purposes. + joins: + - to: sessions + "on": stg_sessions.session_id = sessions.session_id + relationship: many_to_one + source: inferred + stg_subscriptions: + table: orbit_analytics.stg_subscriptions + columns: + - name: subscription_id + type: string + descriptions: + ai: Unique identifiers for individual subscription records, prefixed with "sub_" followed by a sequential number. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking subscriptions to their associated customer accounts. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking subscriptions to their associated contractual agreements or sales contracts. + - name: plan_id + type: string + descriptions: + ai: Identifier referencing the specific subscription plan or pricing tier associated with each subscription record. + - name: mrr_cents + type: number + descriptions: + ai: Monthly Recurring Revenue in cents for each subscription, used for billing and revenue tracking. + - name: status + type: string + descriptions: + ai: Current state of the subscription lifecycle, such as active, cancelled, or expired. + - name: started_at + type: time + descriptions: + ai: Timestamp marking when a subscription became active or the billing period began. + - name: ended_at + type: time + descriptions: + ai: Timestamp marking when a subscription period concludes or expires, indicating the scheduled end date. + - name: cancelled_at + type: time + descriptions: + ai: Timestamp indicating when a subscription was cancelled; far-future dates suggest active, non-cancelled subscriptions. + descriptions: + ai: Staging data tracking customer subscription lifecycle details, including billing amounts, plan assignments, and contract durations for recurring revenue management. + joins: + - to: stg_invoices + "on": stg_subscriptions.subscription_id = stg_invoices.subscription_id + relationship: one_to_many + source: inferred + - to: invoices + "on": stg_subscriptions.subscription_id = invoices.subscription_id + relationship: one_to_many + source: inferred + stg_supplier_onboarding_events: + table: orbit_analytics.stg_supplier_onboarding_events + columns: + - name: supplier_onboarding_event_id + type: string + descriptions: + ai: Unique identifiers for individual supplier onboarding events, formatted with sequential numeric suffixes. + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers for suppliers being tracked through the onboarding process. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for accounts associated with supplier onboarding events, formatted as "acct_XXXX". + - name: event_type + type: string + descriptions: + ai: Stages in the supplier onboarding lifecycle, tracking progression from invitation to approval. + - name: event_at + type: time + descriptions: + ai: Timestamps recording when supplier onboarding events occurred, stored in Pacific Time zone. + - name: status + type: string + descriptions: + ai: Current state of a supplier onboarding event, such as pending, completed, or blocked. + descriptions: + ai: Tracks supplier onboarding lifecycle events, capturing progress milestones and statuses as suppliers move through the onboarding process. + stg_suppliers: + table: orbit_analytics.stg_suppliers + columns: + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers for suppliers, formatted with a zero-padded sequential numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique account identifiers linking suppliers to their associated business accounts in the system. + - name: supplier_name + type: string + descriptions: + ai: Official business name or label assigned to each supplier in the system. + - name: status + type: string + descriptions: + ai: Current lifecycle stage of a supplier's relationship with the organization, from onboarding to active or inactive. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each supplier record was initially created in the system. + descriptions: + ai: Staging data tracking supplier onboarding lifecycle, capturing vendor registration status and progression from invitation through active account activation. + joins: + - to: stg_purchase_orders + "on": stg_suppliers.supplier_id = stg_purchase_orders.supplier_id + relationship: one_to_many + source: inferred + - to: stg_purchase_requests + "on": stg_suppliers.supplier_id = stg_purchase_requests.supplier_id + relationship: one_to_many + source: inferred + - to: purchase_orders + "on": stg_suppliers.supplier_id = purchase_orders.supplier_id + relationship: one_to_many + source: inferred + - to: purchase_requests + "on": stg_suppliers.supplier_id = purchase_requests.supplier_id + relationship: one_to_many + source: inferred + stg_support_tickets: + table: orbit_analytics.stg_support_tickets + columns: + - name: support_ticket_id + type: string + descriptions: + ai: Unique identifiers for individual customer support tickets, formatted as sequential alphanumeric codes. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking support tickets to specific customer or business accounts. + - name: requester_user_id + type: string + descriptions: + ai: Unique identifier of the user who submitted or initiated the support ticket request. + - name: severity + type: string + descriptions: + ai: Priority level of support tickets, indicating urgency (e.g., critical, high). + - name: category + type: string + descriptions: + ai: Classification of the support issue type, such as approval routing workflows or processes. + - name: status + type: string + descriptions: + ai: Current state of a support ticket in its resolution workflow (e.g., open, closed). + - name: created_at + type: time + descriptions: + ai: Timestamp recording when a support ticket was initially submitted by the requester. + - name: resolved_at + type: time + descriptions: + ai: Timestamp indicating when a support ticket was resolved; future date (2099) likely signals unresolved tickets. + - name: owner_user_id + type: string + descriptions: + ai: Unique identifier of the support agent or employee assigned to handle the ticket. + descriptions: + ai: Staging data for customer support tickets tracking issue severity, status, and ownership for resolution management across accounts. + stg_users: + table: orbit_analytics.stg_users + columns: + - name: user_id + type: string + descriptions: + ai: Unique identifiers for individual users, formatted with a zero-padded sequential numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking users to their associated organizational account, formatted as "acct_XXXX". + - name: email + type: string + descriptions: + ai: Email addresses of users, formatted with anonymized user and customer identifiers for staging/testing purposes. + - name: role + type: string + descriptions: + ai: Functional designation of a user within the system, such as requester, approver, admin, or finance. + - name: is_requester + type: boolean + descriptions: + ai: Boolean flag indicating whether the user has submitted or can submit requests. + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag indicating whether a user belongs to the internal organization or team. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether the user account is a test or dummy entry. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when a user account was first created in the system. + - name: slack_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding Slack workspace accounts. + - name: looker_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding Looker analytics platform accounts. + - name: notion_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding Notion workspace accounts. + - name: drive_owner_id + type: string + descriptions: + ai: Unique identifiers linking users to their associated Google Drive ownership accounts. + descriptions: + ai: Staging data for external user accounts, capturing identity, roles, and system integrations (Slack, Looker) to support access management and authentication workflows. + joins: + - to: int_procurement_qualifying_actions + "on": stg_users.user_id = int_procurement_qualifying_actions.user_id + relationship: one_to_many + source: inferred + - to: stg_purchase_requests + "on": stg_users.user_id = stg_purchase_requests.requester_user_id + relationship: one_to_many + source: inferred + - to: purchase_requests + "on": stg_users.user_id = purchase_requests.requester_user_id + relationship: one_to_many + source: inferred + - to: sessions + "on": stg_users.user_id = sessions.user_id + relationship: one_to_many + source: inferred diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_raw.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_raw.yaml new file mode 100644 index 00000000..2011f93d --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/_schema/orbit_raw.yaml @@ -0,0 +1,989 @@ +tables: + account_hierarchy: + table: orbit_raw.account_hierarchy + columns: + - name: account_hierarchy_id + type: string + descriptions: + ai: Unique identifiers for account parent-child relationship records within an organizational hierarchy. + - name: parent_account_id + type: string + descriptions: + ai: Identifiers for the higher-level accounts in a parent-child account relationship hierarchy. + - name: child_account_id + type: string + descriptions: + ai: Unique identifiers for subordinate accounts nested within a parent-child account relationship structure. + - name: relationship_type + type: string + descriptions: + ai: Categorizes the structural or financial relationship between parent and child accounts in a hierarchy. + - name: effective_start_date + type: time + descriptions: + ai: Start date when a parent-child account relationship becomes active and valid. + - name: effective_end_date + type: time + descriptions: + ai: End date when the parent-child account relationship expires or becomes inactive. + descriptions: + ai: Defines parent-child relationships between accounts, supporting organizational structures like subsidiaries, divisions, and billing groups with time-bound validity periods. + joins: + - to: stg_account_hierarchy + "on": account_hierarchy.account_hierarchy_id = stg_account_hierarchy.account_hierarchy_id + relationship: one_to_many + source: inferred + account_owners: + table: orbit_raw.account_owners + columns: + - name: account_owner_id + type: string + descriptions: + ai: Unique identifiers for account ownership records, linking accounts to their designated owners. + - name: account_id + type: string + descriptions: + ai: Unique identifiers linking ownership records to specific customer or business accounts. + - name: owner_user_id + type: string + descriptions: + ai: Unique identifiers for individual users assigned as owners of specific accounts. + - name: owner_team + type: string + descriptions: + ai: Organizational team responsible for managing or owning the account, such as sales or finance. + - name: role + type: string + descriptions: + ai: Business function or responsibility of the owner associated with the account relationship. + - name: effective_start_date + type: time + descriptions: + ai: Start date when an account owner's role or assignment becomes active. + - name: effective_end_date + type: time + descriptions: + ai: End date when an account owner's assigned role or responsibility expires or becomes inactive. + descriptions: + ai: Tracks ownership assignments of accounts to users and teams, defining roles and time-bound responsibilities for managing customer relationships. + joins: + - to: stg_account_owners + "on": account_owners.account_owner_id = stg_account_owners.account_owner_id + relationship: one_to_many + source: inferred + - to: accounts + "on": account_owners.account_id = accounts.account_id + relationship: many_to_one + source: inferred + accounts: + table: orbit_raw.accounts + columns: + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer or business accounts, prefixed with "acct_" followed by a numeric sequence. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a child account to its parent in a hierarchical account structure. + - name: account_name + type: string + descriptions: + ai: Display name or label assigned to each customer or business account. + - name: domain + type: string + descriptions: + ai: Unique web domain associated with each customer account for identification and access purposes. + - name: industry + type: string + descriptions: + ai: Business sector or vertical market classification of the account organization. + - name: sales_region + type: string + descriptions: + ai: Geographic sales territory classification (e.g., North America, EMEA, Asia-Pacific) for account segmentation. + - name: size_band + type: string + descriptions: + ai: Categorical classification of account size, segmenting customers by organizational scale (e.g., enterprise, SMB). + - name: lifecycle_status + type: string + descriptions: + ai: Current stage of an account in the business relationship lifecycle (e.g., active, churned). + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag identifying whether the account belongs to the company internally, rather than an external customer. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether an account is a test/sandbox entry, excluded from production reporting. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each account record was first created in the system. + descriptions: + ai: Customer account records for a SaaS platform, tracking organizational clients across industries, regions, and lifecycle stages for account management purposes. + joins: + - to: int_active_contract_arr + "on": accounts.account_id = int_active_contract_arr.account_id + relationship: one_to_many + source: inferred + - to: int_customer_health_signals + "on": accounts.account_id = int_customer_health_signals.account_id + relationship: one_to_many + source: inferred + - to: mart_account_segments + "on": accounts.account_id = mart_account_segments.account_id + relationship: one_to_many + source: inferred + - to: mart_customer_health + "on": accounts.account_id = mart_customer_health.account_id + relationship: one_to_many + source: inferred + - to: stg_account_owners + "on": accounts.account_id = stg_account_owners.account_id + relationship: one_to_many + source: inferred + - to: stg_accounts + "on": accounts.account_id = stg_accounts.account_id + relationship: one_to_many + source: inferred + - to: stg_contracts + "on": accounts.account_id = stg_contracts.account_id + relationship: one_to_many + source: inferred + - to: stg_invoices + "on": accounts.account_id = stg_invoices.account_id + relationship: one_to_many + source: inferred + - to: stg_refunds + "on": accounts.account_id = stg_refunds.account_id + relationship: one_to_many + source: inferred + - to: account_owners + "on": accounts.account_id = account_owners.account_id + relationship: one_to_many + source: inferred + - to: contracts + "on": accounts.account_id = contracts.account_id + relationship: one_to_many + source: inferred + - to: invoices + "on": accounts.account_id = invoices.account_id + relationship: one_to_many + source: inferred + - to: refunds + "on": accounts.account_id = refunds.account_id + relationship: one_to_many + source: inferred + activation_events: + table: orbit_raw.activation_events + columns: + - name: activation_event_id + type: string + descriptions: + ai: Unique sequential identifiers for individual activation events, formatted with a prefixed numeric string. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer or business accounts associated with activation events. + - name: user_id + type: string + descriptions: + ai: Unique identifiers for users associated with activation events, formatted as sequential user codes. + - name: event_type + type: string + descriptions: + ai: Categorizes key activation milestones, such as a user's first login as a requester. + - name: event_at + type: time + descriptions: + ai: Timestamp recording when an activation event occurred, stored in UTC with timezone offset. + - name: policy_version + type: string + descriptions: + ai: Version identifier of the policy in effect at the time of the activation event. + descriptions: + ai: Tracks milestone activation events when users first access accounts, capturing policy version context for compliance and onboarding analytics. + joins: + - to: stg_activation_events + "on": activation_events.activation_event_id = stg_activation_events.activation_event_id + relationship: one_to_many + source: inferred + approval_events: + table: orbit_raw.approval_events + columns: + - name: approval_event_id + type: string + descriptions: + ai: Unique identifiers for individual approval events, formatted with a sequential numeric suffix. + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifiers linking approval events to their associated purchase requests. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for accounts associated with purchase request approval events. + - name: approver_user_id + type: string + descriptions: + ai: Unique identifiers for users who reviewed and made approval decisions on purchase requests. + - name: decision + type: string + descriptions: + ai: Outcome of an approval action on a purchase request, such as "approved" or "rejected." + - name: decided_at + type: time + descriptions: + ai: Timestamp recording when an approver made their decision on a purchase request. + descriptions: + ai: Records of approval decisions made by designated approvers on purchase requests, tracking who approved what and when for procurement governance. + joins: + - to: stg_approval_events + "on": approval_events.approval_event_id = stg_approval_events.approval_event_id + relationship: one_to_many + source: inferred + - to: purchase_requests + "on": approval_events.purchase_request_id = purchase_requests.purchase_request_id + relationship: many_to_one + source: inferred + arr_movements: + table: orbit_raw.arr_movements + columns: + - name: arr_movement_id + type: string + descriptions: + ai: Unique identifiers for individual ARR movement records, using a sequential prefixed format. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the customer account associated with each ARR movement record. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking a child account to its parent in a hierarchical account structure. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking ARR movements to their associated customer contracts. + - name: movement_date + type: time + descriptions: + ai: Date when an ARR movement or change event occurred, used for revenue tracking. + - name: movement_type + type: string + descriptions: + ai: Categorical classification of ARR change events, such as expansion, contraction, churn, or new business. + - name: movement_reason + type: string + descriptions: + ai: Categorizes the specific cause driving an ARR change, such as seat expansion or contraction. + - name: arr_delta_cents + type: number + descriptions: + ai: Monetary change in Annual Recurring Revenue for a given movement, stored in cents. + - name: starting_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) value in cents before the movement was applied. + - name: ending_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) balance in cents after applying the recorded movement. + descriptions: + ai: Tracks changes in Annual Recurring Revenue (ARR) for customer accounts, capturing expansion, contraction, and churn movements to monitor subscription revenue growth. + joins: + - to: int_parent_account_arr_movements + "on": arr_movements.arr_movement_id = int_parent_account_arr_movements.arr_movement_id + relationship: one_to_many + source: inferred + - to: stg_arr_movements + "on": arr_movements.arr_movement_id = stg_arr_movements.arr_movement_id + relationship: one_to_many + source: inferred + contract_discount_terms: + table: orbit_raw.contract_discount_terms + columns: + - name: discount_term_id + type: string + descriptions: + ai: Unique identifiers for individual discount terms associated with contracts, formatted as sequential codes. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking discount terms to their associated contracts in the system. + - name: discount_type + type: string + descriptions: + ai: Categorizes the business reason or occasion for applying a contract discount. + - name: discount_cents + type: number + descriptions: + ai: Fixed discount amount in cents applied to a contract, representing a flat monetary reduction. + - name: discount_percent + type: number + descriptions: + ai: Percentage-based discount rate applied to a contract, expressed as a decimal (e.g., 0.10 = 10%). + - name: starts_on + type: time + descriptions: + ai: Effective start date for when a contract discount term becomes active. + - name: expires_on + type: time + descriptions: + ai: End date when a contract discount term becomes inactive or no longer valid. + - name: reason + type: string + descriptions: + ai: Categorical label indicating the rationale or trigger for a contract discount, such as expiration events. + descriptions: + ai: Tracks discount terms applied to contracts, capturing promotional pricing details including discount amounts, types, validity periods, and expiration reasons for contract management. + contracts: + table: orbit_raw.contracts + columns: + - name: contract_id + type: string + descriptions: + ai: Unique identifiers for individual contracts, formatted as sequential alphanumeric codes. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking each contract to a specific customer account. + - name: parent_account_id + type: string + descriptions: + ai: Identifier linking contracts to a parent/umbrella account, supporting hierarchical account structures. + - name: plan_id + type: string + descriptions: + ai: Identifier referencing the subscription or pricing plan associated with each contract. + - name: contract_arr_cents + type: number + descriptions: + ai: Annual Recurring Revenue (ARR) value of the contract, stored in cents. + - name: booked_arr_cents + type: number + descriptions: + ai: Committed annual recurring revenue at time of booking, stored in cents. + - name: start_date + type: time + descriptions: + ai: Date when a contract becomes active and billing or service obligations begin. + - name: end_date + type: time + descriptions: + ai: Expiration date marking when a contract term concludes, used for renewal tracking. + - name: status + type: string + descriptions: + ai: Current lifecycle state of a contract, such as active, expired, or cancelled. + - name: renewal_type + type: string + descriptions: + ai: Classification of contract motion type, indicating whether it's new business, a renewal, expansion, or downgrade. + descriptions: + ai: Tracks customer subscription contracts, capturing revenue commitments, contract terms, and lifecycle stages (new, renewal, expansion) for recurring revenue management. + joins: + - to: int_active_contract_arr + "on": contracts.contract_id = int_active_contract_arr.contract_id + relationship: one_to_many + source: inferred + - to: stg_contracts + "on": contracts.contract_id = stg_contracts.contract_id + relationship: one_to_many + source: inferred + - to: accounts + "on": contracts.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: plans + "on": contracts.plan_id = plans.plan_id + relationship: many_to_one + source: inferred + invoice_line_items: + table: orbit_raw.invoice_line_items + columns: + - name: invoice_line_item_id + type: string + descriptions: + ai: Unique identifiers for individual line items within invoices, formatted as sequential alphanumeric codes. + - name: invoice_id + type: string + descriptions: + ai: Foreign key referencing the parent invoice, linking line items to their corresponding invoice record. + - name: line_item_type + type: string + descriptions: + ai: Categorizes invoice charges by billing type, such as subscriptions, seats, usage, or add-ons. + - name: amount_cents + type: number + descriptions: + ai: Monetary value of individual invoice line items, stored in cents (e.g., $20,000). + - name: recognized_at + type: time + descriptions: + ai: Timestamp indicating when a line item's revenue was formally recognized for accounting purposes. + descriptions: + ai: Individual line items within invoices, capturing billing details for subscriptions, seat licenses, and usage-based charges with revenue recognition timestamps. + joins: + - to: stg_invoice_line_items + "on": invoice_line_items.invoice_line_item_id = stg_invoice_line_items.invoice_line_item_id + relationship: one_to_many + source: inferred + invoices: + table: orbit_raw.invoices + columns: + - name: invoice_id + type: string + descriptions: + ai: Unique sequential identifiers for individual invoice records, prefixed with "inv_". + - name: account_id + type: string + descriptions: + ai: Unique identifiers linking invoices to specific customer accounts in the billing system. + - name: subscription_id + type: string + descriptions: + ai: Unique identifiers linking invoices to their associated subscription plans or agreements. + - name: invoice_date + type: time + descriptions: + ai: Timestamp indicating when an invoice was issued to the account or customer. + - name: paid_at + type: time + descriptions: + ai: Timestamp recording when an invoice was successfully paid, stored in UTC. + - name: status + type: string + descriptions: + ai: Current payment state of an invoice, such as paid, pending, or overdue. + - name: currency + type: string + descriptions: + ai: Three-letter ISO currency code used for billing and payment processing on invoices. + descriptions: + ai: Billing records tracking subscription invoices, payment timestamps, and statuses for customer accounts in a subscription-based revenue management system. + joins: + - to: stg_invoices + "on": invoices.invoice_id = stg_invoices.invoice_id + relationship: one_to_many + source: inferred + - to: stg_refunds + "on": invoices.invoice_id = stg_refunds.invoice_id + relationship: one_to_many + source: inferred + - to: accounts + "on": invoices.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: stg_subscriptions + "on": invoices.subscription_id = stg_subscriptions.subscription_id + relationship: many_to_one + source: inferred + - to: refunds + "on": invoices.invoice_id = refunds.invoice_id + relationship: one_to_many + source: inferred + plan_segment_mapping: + table: orbit_raw.plan_segment_mapping + columns: + - name: plan_segment_mapping_id + type: string + descriptions: + ai: Unique identifiers for plan-to-segment mapping records, linking insurance plans to specific customer segments. + - name: canonical_plan_code + type: string + descriptions: + ai: Standardized plan tier identifiers (e.g., "starter," "growth") used to categorize subscription or pricing plans. + - name: size_band + type: string + descriptions: + ai: Business size classification of customers, categorizing them as SMB, mid-market, or enterprise segments. + - name: segment + type: string + descriptions: + ai: Business or market segment classification (e.g., self-serve, commercial, enterprise) for plan targeting. + - name: effective_start_date + type: time + descriptions: + ai: Start date when a plan-segment mapping record becomes active and valid for use. + - name: effective_end_date + type: time + descriptions: + ai: End date when a plan-segment mapping record expires or becomes inactive. + descriptions: + ai: Maps subscription plans to customer segments and size bands with time-bound effective dates, enabling segment-specific plan routing and pricing logic. + joins: + - to: stg_plan_segment_mapping + "on": plan_segment_mapping.plan_segment_mapping_id = stg_plan_segment_mapping.plan_segment_mapping_id + relationship: one_to_many + source: inferred + - to: plans + "on": plan_segment_mapping.canonical_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + plans: + table: orbit_raw.plans + columns: + - name: plan_id + type: string + descriptions: + ai: Unique identifiers for subscription or service plans within the system. + - name: plan_code + type: string + descriptions: + ai: Short identifier codes for subscription tiers, used to programmatically reference available plans. + - name: plan_name + type: string + descriptions: + ai: Human-readable label for a subscription tier offered to customers. + - name: canonical_plan_code + type: string + descriptions: + ai: Standardized plan tier identifiers grouping related plans into core business categories (starter, growth, enterprise). + - name: is_retired + type: boolean + descriptions: + ai: Boolean flag indicating whether a plan has been decommissioned or is no longer active. + - name: retired_at + type: time + descriptions: + ai: Timestamp indicating when a plan was or will be retired/deactivated. + descriptions: + ai: Subscription tier configurations defining available service plans (Starter, Growth, Enterprise) for customer pricing and product packaging decisions. + joins: + - to: int_active_contract_arr + "on": plans.plan_id = int_active_contract_arr.plan_id + relationship: one_to_many + source: inferred + - to: mart_account_segments + "on": plans.plan_code = mart_account_segments.current_plan_code + relationship: one_to_many + source: inferred + - to: mart_account_segments + "on": plans.plan_code = mart_account_segments.normalized_plan_code + relationship: one_to_many + source: inferred + - to: stg_contracts + "on": plans.plan_id = stg_contracts.plan_id + relationship: one_to_many + source: inferred + - to: stg_plan_segment_mapping + "on": plans.plan_code = stg_plan_segment_mapping.canonical_plan_code + relationship: one_to_many + source: inferred + - to: stg_plans + "on": plans.plan_code = stg_plans.canonical_plan_code + relationship: one_to_many + source: inferred + - to: stg_plans + "on": plans.plan_code = stg_plans.plan_code + relationship: one_to_many + source: inferred + - to: stg_plans + "on": plans.plan_id = stg_plans.plan_id + relationship: one_to_many + source: inferred + - to: contracts + "on": plans.plan_id = contracts.plan_id + relationship: one_to_many + source: inferred + - to: plan_segment_mapping + "on": plans.plan_code = plan_segment_mapping.canonical_plan_code + relationship: one_to_many + source: inferred + - to: plans + "on": plans.canonical_plan_code = plans.plan_code + relationship: many_to_one + source: inferred + - to: plans + "on": plans.plan_code = plans.canonical_plan_code + relationship: one_to_many + source: inferred + purchase_orders: + table: orbit_raw.purchase_orders + columns: + - name: purchase_order_id + type: string + descriptions: + ai: Unique identifiers for purchase orders, formatted with a "po_" prefix and sequential numbering. + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifier linking a purchase order to its originating purchase request. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each purchase order transaction. + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers referencing the supplier fulfilling each purchase order. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each purchase order was created or submitted in the system. + - name: status + type: string + descriptions: + ai: Current state of a purchase order in its fulfillment lifecycle (e.g., sent, fulfilled). + - name: amount_cents + type: number + descriptions: + ai: Monetary value of purchase orders stored in cents (e.g., 25100 = $251.00). + descriptions: + ai: Records of formal purchase orders issued to suppliers, tracking procurement transactions from approved purchase requests through fulfillment for financial and supply chain management. + joins: + - to: stg_purchase_orders + "on": purchase_orders.purchase_order_id = stg_purchase_orders.purchase_order_id + relationship: one_to_many + source: inferred + - to: purchase_requests + "on": purchase_orders.purchase_request_id = purchase_requests.purchase_request_id + relationship: many_to_one + source: inferred + - to: stg_suppliers + "on": purchase_orders.supplier_id = stg_suppliers.supplier_id + relationship: many_to_one + source: inferred + purchase_requests: + table: orbit_raw.purchase_requests + columns: + - name: purchase_request_id + type: string + descriptions: + ai: Unique identifiers for purchase requests, formatted with a "pr_" prefix and sequential numbering. + - name: account_id + type: string + descriptions: + ai: Unique identifier for the account associated with each purchase request. + - name: requester_user_id + type: string + descriptions: + ai: Unique identifier of the user who submitted the purchase request. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when each purchase request was submitted, stored in UTC with timezone offset. + - name: status + type: string + descriptions: + ai: Current approval state of a purchase request, such as "submitted" or "approved." + - name: amount_cents + type: number + descriptions: + ai: Monetary value of purchase requests, stored in cents (e.g., $251–$255). + - name: supplier_id + type: string + descriptions: + ai: Unique identifier referencing the vendor or supplier associated with a purchase request. + descriptions: + ai: Tracks employee or user-submitted purchase requests, capturing approval status, requested amounts, and associated suppliers for procurement workflow management. + joins: + - to: stg_approval_events + "on": purchase_requests.purchase_request_id = stg_approval_events.purchase_request_id + relationship: one_to_many + source: inferred + - to: stg_purchase_orders + "on": purchase_requests.purchase_request_id = stg_purchase_orders.purchase_request_id + relationship: one_to_many + source: inferred + - to: stg_purchase_requests + "on": purchase_requests.purchase_request_id = stg_purchase_requests.purchase_request_id + relationship: one_to_many + source: inferred + - to: approval_events + "on": purchase_requests.purchase_request_id = approval_events.purchase_request_id + relationship: one_to_many + source: inferred + - to: purchase_orders + "on": purchase_requests.purchase_request_id = purchase_orders.purchase_request_id + relationship: one_to_many + source: inferred + - to: stg_users + "on": purchase_requests.requester_user_id = stg_users.user_id + relationship: many_to_one + source: inferred + - to: stg_suppliers + "on": purchase_requests.supplier_id = stg_suppliers.supplier_id + relationship: many_to_one + source: inferred + refunds: + table: orbit_raw.refunds + columns: + - name: refund_id + type: string + descriptions: + ai: Unique identifiers for individual refund transactions, formatted with a sequential numeric suffix. + - name: invoice_id + type: string + descriptions: + ai: Unique identifier linking each refund to its originating invoice record. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking refunds to the associated customer or business account. + - name: amount_cents + type: number + descriptions: + ai: Monetary value of refunds in cents; all samples equal $10,000. + - name: status + type: string + descriptions: + ai: Current state of the refund process, indicating whether the refund was successfully completed. + - name: refunded_at + type: time + descriptions: + ai: Timestamp recording when a refund was officially processed and completed. + - name: reason + type: string + descriptions: + ai: Categorical explanation for why a refund was issued, such as board reconciliation credits. + descriptions: + ai: Records of processed financial refunds issued to accounts against invoices, tracking amounts, outcomes, and reasons for reconciliation purposes. + joins: + - to: stg_refunds + "on": refunds.refund_id = stg_refunds.refund_id + relationship: one_to_many + source: inferred + - to: accounts + "on": refunds.account_id = accounts.account_id + relationship: many_to_one + source: inferred + - to: invoices + "on": refunds.invoice_id = invoices.invoice_id + relationship: many_to_one + source: inferred + sessions: + table: orbit_raw.sessions + columns: + - name: session_id + type: string + descriptions: + ai: Unique sequential identifiers for individual user sessions, formatted with zero-padded numeric suffixes. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for customer or business accounts associated with each session. + - name: user_id + type: string + descriptions: + ai: Unique identifiers for individual users associated with each session record. + - name: started_at + type: time + descriptions: + ai: Timestamp marking when a user session began, stored in Pacific Time. + - name: duration_seconds + type: number + descriptions: + ai: Length of a user session measured in seconds, used for engagement analysis. + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag indicating whether the session originated from internal company users or staff. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether the session was generated for testing purposes. + descriptions: + ai: Tracks user session activity across accounts, capturing login events, session duration, and timestamps for monitoring platform engagement and usage patterns. + joins: + - to: stg_sessions + "on": sessions.session_id = stg_sessions.session_id + relationship: one_to_many + source: inferred + - to: stg_users + "on": sessions.user_id = stg_users.user_id + relationship: many_to_one + source: inferred + subscriptions: + table: orbit_raw.subscriptions + columns: + - name: subscription_id + type: string + descriptions: + ai: Unique identifiers for individual subscription records, prefixed with "sub_" followed by a sequential number. + - name: account_id + type: string + descriptions: + ai: Unique identifiers linking subscriptions to their associated customer accounts. + - name: contract_id + type: string + descriptions: + ai: Unique identifier linking subscriptions to their associated contractual agreements with customers. + - name: plan_id + type: string + descriptions: + ai: Identifier referencing the pricing plan associated with a subscription, linking to a plans table. + - name: mrr_cents + type: number + descriptions: + ai: Monthly recurring revenue in cents for each subscription, used for billing and revenue tracking. + - name: status + type: string + descriptions: + ai: Current state of a subscription, such as active, cancelled, or expired. + - name: started_at + type: time + descriptions: + ai: Timestamp marking when a subscription became active or billing period began. + - name: ended_at + type: time + descriptions: + ai: Timestamp marking when a subscription period expires or terminates, often a future contract end date. + - name: cancelled_at + type: time + descriptions: + ai: Timestamp marking when a subscription was cancelled; a far-future date (2099) indicates active, non-cancelled subscriptions. + descriptions: + ai: Tracks customer subscription lifecycle data, linking accounts to contracts and pricing plans, with billing amounts and active/inactive status periods. + supplier_onboarding_events: + table: orbit_raw.supplier_onboarding_events + columns: + - name: supplier_onboarding_event_id + type: string + descriptions: + ai: Unique identifiers for individual supplier onboarding events, formatted with sequential numeric suffixes. + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers for suppliers being tracked through the onboarding process. + - name: account_id + type: string + descriptions: + ai: Unique identifiers for accounts associated with supplier onboarding events, formatted as "acct_XXXX". + - name: event_type + type: string + descriptions: + ai: Stages in the supplier onboarding lifecycle, tracking progression from invitation to approval. + - name: event_at + type: time + descriptions: + ai: Timestamps recording when supplier onboarding events occurred, stored in Pacific Time zone. + - name: status + type: string + descriptions: + ai: Current state of a supplier onboarding event, such as pending, completed, or blocked. + descriptions: + ai: Tracks milestone events during supplier onboarding workflows, capturing progression stages, statuses, and timestamps for managing supplier account activation processes. + suppliers: + table: orbit_raw.suppliers + columns: + - name: supplier_id + type: string + descriptions: + ai: Unique identifiers for suppliers, formatted with a sequential numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique account identifiers linking suppliers to their associated accounts, formatted with an "acct_" prefix. + - name: supplier_name + type: string + descriptions: + ai: Official business name or label assigned to identify each supplier entity. + - name: status + type: string + descriptions: + ai: Current lifecycle stage of a supplier relationship, tracking progression from invitation to active or inactive. + - name: created_at + type: time + descriptions: + ai: Timestamps recording when supplier records were created, stored in Pacific Time zone. + descriptions: + ai: Tracks vendor/supplier onboarding lifecycle, managing their registration status and account associations from initial invitation through active engagement. + support_tickets: + table: orbit_raw.support_tickets + columns: + - name: support_ticket_id + type: string + descriptions: + ai: Unique identifiers for customer support tickets, formatted sequentially with a "ticket_" prefix. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking each support ticket to a specific customer account. + - name: requester_user_id + type: string + descriptions: + ai: Unique identifiers of users who submitted or initiated the support ticket requests. + - name: severity + type: string + descriptions: + ai: Priority level of a support ticket, indicating urgency (e.g., critical, high). + - name: category + type: string + descriptions: + ai: Classification of the support issue type, such as approval routing workflows or processes. + - name: status + type: string + descriptions: + ai: Current state of a support ticket in its resolution workflow (e.g., open, closed). + - name: created_at + type: time + descriptions: + ai: Timestamp recording when a support ticket was initially submitted or opened. + - name: resolved_at + type: time + descriptions: + ai: Timestamp indicating when a ticket was resolved; future sentinel date (2099) likely represents unresolved tickets. + - name: owner_user_id + type: string + descriptions: + ai: Unique identifier of the support agent or staff member assigned to handle the ticket. + descriptions: + ai: Tracks customer support requests, their severity, status, and ownership for managing issue resolution workflows across accounts. + users: + table: orbit_raw.users + columns: + - name: user_id + type: string + descriptions: + ai: Unique identifiers for individual user records, formatted with a sequential numeric suffix. + - name: account_id + type: string + descriptions: + ai: Unique identifier linking users to their associated organizational accounts, formatted with an "acct_" prefix. + - name: email + type: string + descriptions: + ai: Email addresses of users, formatted with user and customer identifiers for multi-tenant accounts. + - name: role + type: string + descriptions: + ai: Functional access level or permission type assigned to each user within the system. + - name: is_requester + type: boolean + descriptions: + ai: Boolean flag indicating whether the user has the role of a requester within the system. + - name: is_internal + type: boolean + descriptions: + ai: Boolean flag indicating whether a user belongs to the internal organization or team. + - name: is_test + type: boolean + descriptions: + ai: Boolean flag identifying whether the user account is a test or dummy entry. + - name: created_at + type: time + descriptions: + ai: Timestamp recording when a user account was first created in the system. + - name: slack_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding Slack workspace accounts for integration purposes. + - name: looker_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding accounts in the Looker analytics platform. + - name: notion_user_id + type: string + descriptions: + ai: Unique identifiers linking users to their corresponding Notion workspace accounts. + - name: drive_owner_id + type: string + descriptions: + ai: Unique identifiers linking users to their associated Google Drive owner accounts. + descriptions: + ai: External customer user accounts with role-based access controls, integrated across Slack and Looker platforms for workflow and analytics management. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/large_contract_requesters.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/large_contract_requesters.yaml new file mode 100644 index 00000000..faf02e14 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/large_contract_requesters.yaml @@ -0,0 +1,44 @@ +name: large_contract_requesters +source_type: sql +sql: |- + select account.account_name, + requester.email as requester_email, + activity.action_type, + activity.action_date, + round(segment.contract_arr_cents / 100.0, 0) as contract_arr_usd + from orbit_analytics.int_procurement_qualifying_actions activity + join orbit_raw.accounts account on account.account_id = activity.account_id + join orbit_raw.users requester on requester.user_id = activity.user_id + left join orbit_analytics.mart_account_segments segment on segment.account_id = activity.account_id + order by activity.action_date desc, segment.contract_arr_cents desc nulls last + limit 25 +grain: + - action_date + - requester_email + - action_type +columns: + - name: account_name + type: string + descriptions: + user: Name of the account + - name: requester_email + type: string + descriptions: + user: Email of the requester + - name: action_type + type: string + descriptions: + user: Type of qualifying procurement action + - name: action_date + type: time + role: time + descriptions: + user: Date the action occurred + - name: contract_arr_usd + type: number + descriptions: + user: Contract ARR in USD +joins: [] +measures: [] +descriptions: + user: Recent procurement actions by requesters on large contracts. Joins qualifying procurement actions to account names, requester emails, and contract ARR. Used to identify active requesters on high-value accounts. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_activity.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_activity.yaml new file mode 100644 index 00000000..6fab01b3 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_activity.yaml @@ -0,0 +1,10 @@ +name: mart_account_activity +measures: + - name: avg_pre_policy_activation_rate + expr: avg(pre_policy_30_day_activation_rate) + description: Average 30-day activation rate for the pre-policy cohort (before 2026-01-15). + - name: avg_post_policy_activation_rate + expr: avg(post_policy_30_day_activation_rate) + description: Average 30-day activation rate for the post-policy cohort (on or after 2026-01-15). +descriptions: + user: "Pre/post-policy 30-day activation rates. Source: dbt mart_account_activity. Compares activation counts across the Jan 2026 policy boundary." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_segments.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_segments.yaml new file mode 100644 index 00000000..d8f98b29 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_account_segments.yaml @@ -0,0 +1,18 @@ +name: mart_account_segments +measures: + - name: total_contract_arr_cents + expr: sum(contract_arr_cents) + description: Total active-contract ARR in cents across all accounts. + - name: active_contract_arr_cents + expr: sum(contract_arr_cents) + filter: contract_status = 'active' + description: Active-contract ARR in cents, filtered to accounts with an active contract status. + - name: count_accounts + expr: count(distinct account_id) + description: Distinct count of accounts in the segment. + - name: count_active_contract_accounts + expr: count(distinct account_id) + filter: contract_status = 'active' + description: Distinct count of accounts with an active contract. +descriptions: + user: "Per-account segment and active-contract ARR. Source: dbt mart_account_segments." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_arr_daily.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_arr_daily.yaml new file mode 100644 index 00000000..543b66c4 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_arr_daily.yaml @@ -0,0 +1,10 @@ +name: mart_arr_daily +measures: + - name: total_arr_cents + expr: sum(arr_cents) + description: Sum of ARR in cents across all snapshot dates (use with a date filter to get point-in-time ARR). + - name: latest_arr_cents + expr: max(arr_cents) + description: Most recent ARR value in cents (max across dates in the filtered window). +descriptions: + user: "Daily ARR snapshot. Source: dbt mart_arr_daily. One row per metric_date with global ARR in cents." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_customer_health.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_customer_health.yaml new file mode 100644 index 00000000..c76b0912 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_customer_health.yaml @@ -0,0 +1,27 @@ +name: mart_customer_health +measures: + - name: count_accounts + expr: count(distinct account_id) + description: Total distinct accounts in the health snapshot. + - name: count_active_customers + expr: count(distinct account_id) + filter: is_active_customer = true + description: Distinct count of accounts flagged as active customers. + - name: count_high_risk_accounts + expr: count(distinct account_id) + filter: risk_level = 'high' + description: Distinct count of accounts with high risk level (open critical tickets and/or recent procurement activity). + - name: count_medium_risk_accounts + expr: count(distinct account_id) + filter: risk_level = 'medium' + description: Distinct count of accounts with medium risk level. + - name: count_accounts_with_high_ticket + expr: count(distinct account_id) + filter: has_unresolved_high_ticket = true + description: Distinct count of accounts with at least one unresolved high-severity support ticket. + - name: count_accounts_with_recent_procurement + expr: count(distinct account_id) + filter: has_recent_procurement_activity = true + description: Distinct count of accounts with recent procurement activity. +descriptions: + user: "Per-account risk signals as of a snapshot date. Source: dbt mart_customer_health." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_nrr_quarterly.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_nrr_quarterly.yaml new file mode 100644 index 00000000..925b4f75 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_nrr_quarterly.yaml @@ -0,0 +1,19 @@ +name: mart_nrr_quarterly +measures: + - name: avg_net_revenue_retention + expr: avg(net_revenue_retention) + description: Average NRR (net revenue retention) across quarters and segments. + - name: total_expansion_arr_cents + expr: sum(expansion_arr_cents) + description: Total expansion ARR in cents across all quarters and segments. + - name: total_contraction_arr_cents + expr: sum(contraction_arr_cents) + description: Total contraction ARR in cents across all quarters and segments. + - name: total_churned_arr_cents + expr: sum(churned_arr_cents) + description: Total churned ARR in cents across all quarters and segments. + - name: total_starting_arr_cents + expr: sum(starting_arr_cents) + description: Total starting ARR in cents at the beginning of each quarter. +descriptions: + user: "Quarterly NRR per segment. Source: dbt mart_nrr_quarterly. Enterprise-focused in current dataset." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_procurement_activity.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_procurement_activity.yaml new file mode 100644 index 00000000..2e9cfcc9 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_procurement_activity.yaml @@ -0,0 +1,13 @@ +name: mart_procurement_activity +measures: + - name: total_active_requesters + expr: sum(active_requesters) + description: Total count of active requesters summed across all weeks. + - name: avg_weekly_active_requesters + expr: avg(active_requesters) + description: Average number of active requesters per week (golden week metric). + - name: avg_contract_arr_threshold_cents + expr: avg(contract_arr_threshold_cents) + description: Average contract ARR threshold in cents used to qualify large active contracts. +descriptions: + user: "Weekly procurement KPI. Source: dbt mart_procurement_activity. Tracks active requesters on large active contracts." diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_retention_movement_breakout.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_retention_movement_breakout.yaml new file mode 100644 index 00000000..ac3756d7 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_retention_movement_breakout.yaml @@ -0,0 +1,25 @@ +name: mart_retention_movement_breakout +measures: + - name: total_expansion_arr_cents + expr: sum(expansion_arr_cents) + description: Total expansion ARR in cents + - name: total_contraction_arr_cents + expr: sum(contraction_arr_cents) + description: Total contraction ARR in cents (includes discount expiration contraction) + - name: total_churned_arr_cents + expr: sum(churned_arr_cents) + description: Total churned ARR in cents + - name: parent_account_count + expr: sum(parent_account_count) + description: Total number of parent accounts affected by the movement + - name: expansion_arr_millions + expr: round(sum(expansion_arr_cents) / 100000000.0, 3) + description: Expansion ARR in millions of dollars + - name: contraction_arr_millions + expr: round(sum(contraction_arr_cents) / 100000000.0, 3) + description: Contraction ARR in millions of dollars + - name: churned_arr_millions + expr: round(sum(churned_arr_cents) / 100000000.0, 3) + description: Churned ARR in millions of dollars +descriptions: + user: Quarterly ARR movement breakout by segment, movement_type (expansion/contraction/churn), and movement_reason. One row per quarter × segment × movement_type × movement_reason. Contraction includes discount expirations (is_discount_expiration_contraction). Used for NRR waterfall analysis. diff --git a/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_revenue_daily.yaml b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_revenue_daily.yaml new file mode 100644 index 00000000..3bbb59d9 --- /dev/null +++ b/packages/cli/assets/demo/orbit/semantic-layer/postgres-warehouse/mart_revenue_daily.yaml @@ -0,0 +1,20 @@ +name: mart_revenue_daily +measures: + - name: total_gross_revenue_cents + expr: sum(gross_revenue_cents) + description: Total gross invoice revenue in cents across all days. + - name: total_credits_cents + expr: sum(credits_cents) + description: Total credits applied in cents across all days. + - name: total_refunds_cents + expr: sum(refunds_cents) + description: Total refunds issued in cents across all days. + - name: total_net_revenue_cents + expr: sum(net_revenue_cents) + description: Total net revenue in cents (gross minus credits and refunds) across all days. + - name: count_reconciled_days + expr: count(*) + filter: reconciliation_check = true + description: Number of days where reconciliation_check passed (gross - credits - refunds = net). +descriptions: + user: "Daily gross-to-net revenue. Source: dbt mart_revenue_daily. Aggregate-only — no account dimension." diff --git a/packages/cli/src/demo-assets.test.ts b/packages/cli/src/demo-assets.test.ts index d8307c66..7ef89296 100644 --- a/packages/cli/src/demo-assets.test.ts +++ b/packages/cli/src/demo-assets.test.ts @@ -91,22 +91,17 @@ describe('demo assets', () => { expect(manifest.sources.bi.explores).toBeGreaterThanOrEqual(2); expect(manifest.sources.bi.dashboards).toBeGreaterThanOrEqual(2); expect(manifest.sources.notion.pages).toBeGreaterThanOrEqual(5); - expect(manifest.generated.semanticLayer.sourceCount).toBeGreaterThanOrEqual(5); - expect(manifest.generated.knowledge.pageCount).toBeGreaterThanOrEqual(10); + expect(manifest.generated.semanticLayer.sourceCount).toBeGreaterThanOrEqual(40); + expect(manifest.generated.knowledge.pageCount).toBeGreaterThanOrEqual(20); expect(manifest.generated.links.linkCount).toBeGreaterThanOrEqual(10); const dbStat = await stat(packagedDemoAssetPath('demo.db')); expect(dbStat.size).toBeGreaterThan(0); expect(dbStat.size).toBeLessThan(10 * 1024 * 1024); - await expect(access(packagedDemoAssetPath('raw-sources/warehouse/accounts.csv'))).resolves.toBeUndefined(); - await expect(access(packagedDemoAssetPath('raw-sources/dbt/schema.yml'))).resolves.toBeUndefined(); - await expect(access(packagedDemoAssetPath('raw-sources/bi/revenue_exec.dashboard.lookml'))).resolves.toBeUndefined(); - await expect(access(packagedDemoAssetPath('raw-sources/notion/revenue-reporting-policy.md'))).resolves.toBeUndefined(); - expect(manifest.generated.semanticLayer.path).toBe('semantic-layer/orbit_demo'); - - await expect(access(packagedDemoAssetPath('semantic-layer/orbit_demo/accounts.yaml'))).resolves.toBeUndefined(); - await expect(access(packagedDemoAssetPath('knowledge/global/arr-contract-first.md'))).resolves.toBeUndefined(); + await expect(access(packagedDemoAssetPath('semantic-layer/dbt-main/mart_arr_daily.yaml'))).resolves.toBeUndefined(); + await expect(access(packagedDemoAssetPath('semantic-layer/postgres-warehouse/mart_account_activity.yaml'))).resolves.toBeUndefined(); + await expect(access(packagedDemoAssetPath('knowledge/global/orbit-company-overview.md'))).resolves.toBeUndefined(); await expect(access(packagedDemoAssetPath('links/provenance.json'))).resolves.toBeUndefined(); await expect(access(packagedDemoAssetPath('reports/seeded-demo-report.json'))).resolves.toBeUndefined(); }); diff --git a/packages/cli/src/demo-assets.ts b/packages/cli/src/demo-assets.ts index c0127d41..6754164a 100644 --- a/packages/cli/src/demo-assets.ts +++ b/packages/cli/src/demo-assets.ts @@ -45,14 +45,9 @@ const REQUIRED_SEEDED_ASSET_PATHS = [ 'demo.db', 'manifest.json', DEMO_REPLAY_FILE, - join('raw-sources', 'warehouse', 'accounts.csv'), - join('raw-sources', 'dbt', 'schema.yml'), - join('raw-sources', 'bi', 'revenue_exec.dashboard.lookml'), - join('raw-sources', 'notion', 'revenue-reporting-policy.md'), - join('semantic-layer', 'orbit_demo', 'accounts.yaml'), - join('knowledge', 'global', 'arr-contract-first.md'), - join('links', 'provenance.json'), - join('reports', 'seeded-demo-report.json'), + join('semantic-layer', 'dbt-main', 'mart_arr_daily.yaml'), + join('semantic-layer', 'postgres-warehouse', 'mart_account_activity.yaml'), + join('knowledge', 'global', 'orbit-company-overview.md'), ] as const; function assetDir(): string { diff --git a/packages/cli/src/demo-seeded-inspect.test.ts b/packages/cli/src/demo-seeded-inspect.test.ts index 35c76861..a45415bd 100644 --- a/packages/cli/src/demo-seeded-inspect.test.ts +++ b/packages/cli/src/demo-seeded-inspect.test.ts @@ -53,8 +53,8 @@ describe('seeded demo inspect contract', () => { notion: { label: 'Notion', path: 'raw-sources/notion', pageCount: 8 }, }, generatedOutputs: { - semanticLayer: { path: 'semantic-layer/orbit_demo', manifestSourceCount: 6, fileCount: 6 }, - knowledge: { path: 'knowledge/global', manifestPageCount: 10, fileCount: 10 }, + semanticLayer: { path: 'semantic-layer', manifestSourceCount: 46, fileCount: 46 }, + knowledge: { path: 'knowledge/global', manifestPageCount: 28, fileCount: 28 }, links: { path: 'links/provenance.json', manifestLinkCount: 23, linkCount: 23 }, reports: { primaryPath: 'reports/seeded-demo-report.json', fileCount: 1 }, replays: { primaryPath: 'replays/replay.memory-flow.v1.json', latestPath: 'replays/latest.memory-flow.v1.json' }, @@ -83,8 +83,8 @@ describe('seeded demo inspect contract', () => { expect(output).toContain('dbt: 3 models, 8 source tables'); expect(output).toContain('BI: 5 explores, 2 dashboards'); expect(output).toContain('Notion: 8 pages'); - expect(output).toContain('Semantic-layer sources: 6 manifest, 6 files'); - expect(output).toContain('Knowledge pages: 10 manifest, 10 files'); + expect(output).toContain('Semantic-layer sources: 46 manifest, 46 files'); + expect(output).toContain('Knowledge pages: 28 manifest, 28 files'); expect(output).toContain('Evidence links: 23 manifest, 23 links'); expect(output).toContain('Report: reports/seeded-demo-report.json'); expect(output).toContain('Replay: replays/replay.memory-flow.v1.json'); diff --git a/packages/cli/src/demo-seeded-inspect.ts b/packages/cli/src/demo-seeded-inspect.ts index 0081e4b8..13320890 100644 --- a/packages/cli/src/demo-seeded-inspect.ts +++ b/packages/cli/src/demo-seeded-inspect.ts @@ -71,12 +71,9 @@ const REQUIRED_SEEDED_PROJECT_PATHS = [ 'state.sqlite', 'manifest.json', join('replays', 'replay.memory-flow.v1.json'), - join('raw-sources', 'warehouse', 'accounts.csv'), - join('raw-sources', 'dbt', 'schema.yml'), - join('raw-sources', 'bi', 'revenue_exec.dashboard.lookml'), - join('raw-sources', 'notion', 'revenue-reporting-policy.md'), - join('semantic-layer', 'orbit_demo', 'accounts.yaml'), - join('knowledge', 'global', 'arr-contract-first.md'), + join('semantic-layer', 'dbt-main', 'mart_arr_daily.yaml'), + join('semantic-layer', 'postgres-warehouse', 'mart_account_activity.yaml'), + join('knowledge', 'global', 'orbit-company-overview.md'), join('links', 'provenance.json'), join('reports', 'seeded-demo-report.json'), ] as const; diff --git a/packages/cli/src/demo-seeded.test.ts b/packages/cli/src/demo-seeded.test.ts index c6065c07..95bf0a5a 100644 --- a/packages/cli/src/demo-seeded.test.ts +++ b/packages/cli/src/demo-seeded.test.ts @@ -19,11 +19,9 @@ describe('demo seeded mode', () => { await expect(access(join(projectDir, 'demo.db'))).resolves.toBeUndefined(); await expect(access(join(projectDir, 'ktx.yaml'))).resolves.toBeUndefined(); await expect(access(join(projectDir, 'manifest.json'))).resolves.toBeUndefined(); - await expect(access(join(projectDir, 'semantic-layer/orbit_demo/accounts.yaml'))).resolves.toBeUndefined(); - await expect(access(join(projectDir, 'knowledge/global/arr-contract-first.md'))).resolves.toBeUndefined(); - await expect(access(join(projectDir, 'raw-sources/dbt/schema.yml'))).resolves.toBeUndefined(); - await expect(access(join(projectDir, 'raw-sources/bi/revenue_exec.dashboard.lookml'))).resolves.toBeUndefined(); - await expect(access(join(projectDir, 'raw-sources/notion/revenue-reporting-policy.md'))).resolves.toBeUndefined(); + await expect(access(join(projectDir, 'semantic-layer/dbt-main/mart_arr_daily.yaml'))).resolves.toBeUndefined(); + await expect(access(join(projectDir, 'semantic-layer/postgres-warehouse/mart_account_activity.yaml'))).resolves.toBeUndefined(); + await expect(access(join(projectDir, 'knowledge/global/orbit-company-overview.md'))).resolves.toBeUndefined(); await expect(access(join(projectDir, 'links/provenance.json'))).resolves.toBeUndefined(); await expect(access(join(projectDir, 'reports/seeded-demo-report.json'))).resolves.toBeUndefined(); }); @@ -88,8 +86,8 @@ describe('demo seeded mode', () => { it('SL YAML validates correctly', async () => { await ensureSeededDemoProject({ projectDir, force: false }); - const slYaml = await readFile(join(projectDir, 'semantic-layer/orbit_demo/accounts.yaml'), 'utf-8'); - expect(slYaml).toContain('name: accounts'); + const slYaml = await readFile(join(projectDir, 'semantic-layer/dbt-main/mart_arr_daily.yaml'), 'utf-8'); + expect(slYaml).toContain('name: mart_arr_daily'); expect(slYaml).toContain('grain:'); expect(slYaml).toContain('columns:'); expect(slYaml).toContain('measures:'); @@ -98,11 +96,11 @@ describe('demo seeded mode', () => { it('wiki pages have valid frontmatter', async () => { await ensureSeededDemoProject({ projectDir, force: false }); - const wiki = await readFile(join(projectDir, 'knowledge/global/arr-contract-first.md'), 'utf-8'); + const wiki = await readFile(join(projectDir, 'knowledge/global/orbit-company-overview.md'), 'utf-8'); expect(wiki).toContain('---'); expect(wiki).toContain('summary:'); expect(wiki).toContain('tags:'); - expect(wiki).toContain('sl_refs:'); + expect(wiki).toContain('refs:'); expect(wiki).toContain('usage_mode: auto'); }); diff --git a/packages/cli/src/setup-demo-tour.test.ts b/packages/cli/src/setup-demo-tour.test.ts index f484af1e..f3c84642 100644 --- a/packages/cli/src/setup-demo-tour.test.ts +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -115,10 +115,10 @@ describe('buildDemoReplayTimeline', () => { it('produces events for all 4 targets', () => { expect(connectionIds.size).toBe(4); - expect(connectionIds).toContain('demo-warehouse'); - expect(connectionIds).toContain('dbt'); - expect(connectionIds).toContain('metabase'); - expect(connectionIds).toContain('notion'); + expect(connectionIds).toContain('postgres-warehouse'); + expect(connectionIds).toContain('dbt-main'); + expect(connectionIds).toContain('metabase-main'); + expect(connectionIds).toContain('notion-main'); }); it('all targets end as done', () => { diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index f7b75776..498dedd2 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -190,43 +190,45 @@ export interface DemoReplayEvent { export const DEMO_REPLAY_TARGETS = { primarySources: [ - createDemoTarget('demo-warehouse', 'scan', 'postgres'), + createDemoTarget('postgres-warehouse', 'scan', 'postgres'), ], contextSources: [ - createDemoTarget('dbt', 'source-ingest', 'dbt'), - createDemoTarget('metabase', 'source-ingest', 'metabase'), - createDemoTarget('notion', 'source-ingest', 'notion'), + createDemoTarget('dbt-main', 'source-ingest', 'dbt'), + createDemoTarget('metabase-main', 'source-ingest', 'metabase'), + createDemoTarget('notion-main', 'source-ingest', 'notion'), ], } as const; export function buildDemoReplayTimeline(): DemoReplayEvent[] { return [ - // demo-warehouse: scan - { delayMs: 0, connectionId: 'demo-warehouse', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 1200, connectionId: 'demo-warehouse', status: 'running', detailLine: '[50%] Scanning tables...', summaryText: null }, - { delayMs: 2400, connectionId: 'demo-warehouse', status: 'done', detailLine: null, summaryText: '12 tables' }, - // dbt - { delayMs: 2400, connectionId: 'dbt', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 3600, connectionId: 'dbt', status: 'running', detailLine: '[60%] Ingesting models...', summaryText: null }, - { delayMs: 4400, connectionId: 'dbt', status: 'done', detailLine: null, summaryText: '8 models' }, - // metabase - { delayMs: 4400, connectionId: 'metabase', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 5600, connectionId: 'metabase', status: 'done', detailLine: null, summaryText: '5 dashboards' }, - // notion - { delayMs: 5600, connectionId: 'notion', status: 'running', detailLine: null, summaryText: null }, - { delayMs: 6800, connectionId: 'notion', status: 'done', detailLine: null, summaryText: '3 pages' }, + // postgres-warehouse: scan + { delayMs: 0, connectionId: 'postgres-warehouse', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 1200, connectionId: 'postgres-warehouse', status: 'running', detailLine: '[50%] scanning tables...', summaryText: null }, + { delayMs: 2400, connectionId: 'postgres-warehouse', status: 'done', detailLine: null, summaryText: '56 tables scanned' }, + // dbt-main + { delayMs: 2400, connectionId: 'dbt-main', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 3600, connectionId: 'dbt-main', status: 'running', detailLine: '[60%] ingesting models...', summaryText: null }, + { delayMs: 4400, connectionId: 'dbt-main', status: 'done', detailLine: null, summaryText: '34 models ingested' }, + // metabase-main + { delayMs: 4400, connectionId: 'metabase-main', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 5600, connectionId: 'metabase-main', status: 'done', detailLine: null, summaryText: '80 cards ingested' }, + // notion-main + { delayMs: 5600, connectionId: 'notion-main', status: 'running', detailLine: null, summaryText: null }, + { delayMs: 6800, connectionId: 'notion-main', status: 'done', detailLine: null, summaryText: '9 pages ingested' }, ]; } function renderDemoContextCompletionSummary(): string { const lines = [ '', - '┌ Context build complete', - '│', - '│ All sources have been processed.', - '│', - `│ ${dim('Press Enter to continue, Escape to go back')}`, - '└', + `${cyan('★')} KTX finished building context`, + '', + ' KTX created:', + ` ${cyan('📊')} 45 semantic layer definitions`, + ` ${cyan('📝')} 32 knowledge pages`, + '', + ` ${dim('Press Enter to continue, Escape to go back')}`, + '', ]; return lines.join('\n'); } @@ -341,9 +343,9 @@ export async function runDemoTour( let direction: 'forward' | 'back'; if (step === 'databases') { - direction = await renderDemoCard('Database connection', ['PostgreSQL (demo warehouse)'], io, undefined, waitNav); + direction = await renderDemoCard('Database connection', ['PostgreSQL — Orbit Analytics (56 tables, 2 schemas)'], io, undefined, waitNav); } else if (step === 'sources') { - direction = await renderDemoCard('Context sources', ['dbt', 'Metabase', 'Notion'], io, undefined, waitNav); + direction = await renderDemoCard('Context sources', ['dbt — 34 transformation models', 'Metabase — 80 dashboard cards', 'Notion — 9 knowledge pages'], io, undefined, waitNav); } else if (step === 'context') { io.stdout.write(renderDemoBanner() + '\n\n'); if (deps.skipReplayAnimation) { From 76f727511b87b24042ff48c949187aeeb6e6e4a9 Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 22:03:12 -0700 Subject: [PATCH 09/10] fix(cli): correct demo completion summary counts to match manifest Hardcoded counts showed 45 semantic layer definitions and 32 knowledge pages, but the manifest and actual asset files have 46 and 28. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup-demo-tour.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/setup-demo-tour.ts b/packages/cli/src/setup-demo-tour.ts index 498dedd2..557a52cb 100644 --- a/packages/cli/src/setup-demo-tour.ts +++ b/packages/cli/src/setup-demo-tour.ts @@ -224,8 +224,8 @@ function renderDemoContextCompletionSummary(): string { `${cyan('★')} KTX finished building context`, '', ' KTX created:', - ` ${cyan('📊')} 45 semantic layer definitions`, - ` ${cyan('📝')} 32 knowledge pages`, + ` ${cyan('📊')} 46 semantic layer definitions`, + ` ${cyan('📝')} 28 knowledge pages`, '', ` ${dim('Press Enter to continue, Escape to go back')}`, '', From 6943f1708db62ba8a674c1c19b6eb09570d635ec Mon Sep 17 00:00:00 2001 From: Luca Martial Date: Mon, 11 May 2026 22:03:20 -0700 Subject: [PATCH 10/10] refactor(cli): remove dead demo dep from KtxSetupDeps The demo entry menu now calls runDemoTour directly, so the injectable demo property and KtxDemoArgs import are unused. Update test to mock the new module import. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/setup.test.ts | 20 ++++++++++---------- packages/cli/src/setup.ts | 2 -- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/setup.test.ts b/packages/cli/src/setup.test.ts index e335591e..da3753af 100644 --- a/packages/cli/src/setup.test.ts +++ b/packages/cli/src/setup.test.ts @@ -4,8 +4,13 @@ import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js'; +import { runDemoTour } from './setup-demo-tour.js'; import { readKtxSetupStatus, runKtxSetup } from './setup.js'; +vi.mock('./setup-demo-tour.js', () => ({ + runDemoTour: vi.fn(async () => 0), +})); + function makeIo() { let stdout = ''; let stderr = ''; @@ -691,9 +696,8 @@ describe('setup status', () => { ); }); - it('runs the seeded demo when the first setup intent menu chooses packaged demo data', async () => { + it('runs the demo tour when the first setup intent menu chooses demo', async () => { const testIo = makeIo(); - const demo = vi.fn(async (_args: { projectDir: string }, _io: unknown) => 0); await expect( runKtxSetup( @@ -714,19 +718,15 @@ describe('setup status', () => { showEntryMenu: true, }, testIo.io, - { entryMenuDeps: { prompts: { select: vi.fn(async () => 'demo'), cancel: vi.fn() } }, demo }, + { entryMenuDeps: { prompts: { select: vi.fn(async () => 'demo'), cancel: vi.fn() } } }, ), ).resolves.toBe(0); - expect(demo).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'seeded', - outputMode: 'viz', - inputMode: 'auto', - }), + expect(runDemoTour).toHaveBeenCalledWith( + { inputMode: 'auto' }, testIo.io, + expect.objectContaining({}), ); - expect(demo.mock.calls[0]?.[0].projectDir).toMatch(/ktx-demo-/); }); it('creates a project through run mode when --new is selected', async () => { diff --git a/packages/cli/src/setup.ts b/packages/cli/src/setup.ts index d72b5bb0..63369f9a 100644 --- a/packages/cli/src/setup.ts +++ b/packages/cli/src/setup.ts @@ -3,7 +3,6 @@ import { join, resolve } from 'node:path'; import { cancel, isCancel, select } from '@clack/prompts'; import { loadKtxProject } from '@ktx/context/project'; import type { KtxCliIo } from './cli-runtime.js'; -import type { KtxDemoArgs } from './demo.js'; import { formatSetupNextStepLines } from './next-steps.js'; import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js'; import { @@ -146,7 +145,6 @@ export interface KtxSetupDeps { removeAgents?: typeof removeKtxAgentInstall; readyMenuDeps?: KtxSetupReadyMenuDeps; entryMenuDeps?: KtxSetupEntryMenuDeps; - demo?: (args: KtxDemoArgs, io: KtxCliIo) => Promise; } const SOURCE_DRIVERS = new Set(['dbt', 'metricflow', 'metabase', 'looker', 'lookml', 'notion']);