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 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. 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 new file mode 100644 index 00000000..f3c84642 --- /dev/null +++ b/packages/cli/src/setup-demo-tour.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { KtxSetupAgentsResult } from './setup-agents.js'; +import { + buildDemoReplayTimeline, + DEMO_REPLAY_TARGETS, + renderDemoAgentTransition, + renderDemoBanner, + renderDemoCardContent, + renderDemoCompletionSummary, + runDemoTour, +} 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 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('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('--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('postgres-warehouse'); + expect(connectionIds).toContain('dbt-main'); + expect(connectionIds).toContain('metabase-main'); + expect(connectionIds).toContain('notion-main'); + }); + + 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'); + } + }); +}); + +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 new file mode 100644 index 00000000..557a52cb --- /dev/null +++ b/packages/cli/src/setup-demo-tour.ts @@ -0,0 +1,390 @@ +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 { 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'; + +// --------------------------------------------------------------------------- +// 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[] = [ + '', + `${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(` $ ${cyan(`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'); +} + +// --------------------------------------------------------------------------- +// 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('postgres-warehouse', 'scan', 'postgres'), + ], + contextSources: [ + 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 [ + // 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 = [ + '', + `${cyan('★')} KTX finished building context`, + '', + ' KTX created:', + ` ${cyan('📊')} 46 semantic layer definitions`, + ` ${cyan('📝')} 28 knowledge pages`, + '', + ` ${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); +} + +// --------------------------------------------------------------------------- +// 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 — Orbit Analytics (56 tables, 2 schemas)'], io, undefined, waitNav); + } else if (step === 'sources') { + 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) { + 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.test.ts b/packages/cli/src/setup.test.ts index c8961e2a..eaf6d2c5 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 = ''; @@ -347,10 +352,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 +401,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'; @@ -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 89c5dcdc..ee6fbdc6 100644 --- a/packages/cli/src/setup.ts +++ b/packages/cli/src/setup.ts @@ -3,8 +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 { defaultDemoProjectDir } from './demo-assets.js'; import { formatSetupNextStepLines } from './next-steps.js'; import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js'; import { @@ -148,7 +146,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']); @@ -200,13 +197,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({ @@ -221,15 +218,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 }, ); }