Merge pull request #26 from Kaelio/luca-martial/demo-data-wizard-ux

feat(cli): add demo guided tour with real Orbit project data
This commit is contained in:
Luca Martial 2026-05-12 01:06:45 -04:00 committed by GitHub
commit 950fa151ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 6779 additions and 468 deletions

View file

@ -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<string, string>();
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<void>((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<KtxSetupAgentsResult>>().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<typeof runKtxSetupAgentsStep>[0], io: KtxCliIo) => Promise<KtxSetupAgentsResult>;
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<number> {
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<KtxSetupArgs, { command: 'run' }>,
io: KtxCliIo,
deps: KtxSetupDeps,
): Promise<number> {
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<KtxSetupArgs, { command: 'run' }>,
io: KtxCliIo,
deps: KtxSetupDeps,
): Promise<number> {
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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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]]

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 13 can be covered via existing documentation; the manager should point to the right pages rather than re-explaining from scratch.
## Suggested Reading for Items 13
- 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]]

View file

@ -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.

View file

@ -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.

View file

@ -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`.

View file

@ -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

View file

@ -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."

View file

@ -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.

View file

@ -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]]

View file

@ -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`.

View file

@ -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.

View file

@ -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.

View file

@ -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]]

View file

@ -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]]

View file

@ -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 01 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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/63.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/101.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/106.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/107.json -->
**Table:** `orbit_analytics.mart_account_activity`
**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 (01 ratio) |
| `post_policy_30_day_activation_rate` | decimal | 30-day activation rate after the policy change (01 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 (01); multiply by 100 for percentage display.
- See [orbit-activation-policy-change-jan-2026](orbit-activation-policy-change-jan-2026) for full policy context.

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/69.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/100.json -->
**Table:** `orbit_analytics.mart_account_segments`
**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).

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/56.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/96.json -->
**Table:** `orbit_analytics.mart_arr_daily`
**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.

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/58.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/98.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/103.json -->
**Table:** `orbit_analytics.mart_nrr_quarterly`
**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.

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/88.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/108.json -->
**Table:** `orbit_analytics.mart_procurement_activity`
**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.

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/105.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/115.json -->
**Table:** `orbit_analytics.mart_retention_movement_breakout`
**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.

View file

@ -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
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/57.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/97.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/102.json -->
<!-- from: raw-sources/postgres-warehouse/metabase/2026-05-12-035303-local-metabase-3-114d957b-f564-4f46-8d4c-2770720a95be/cards/104.json -->
**Table:** `orbit_analytics.mart_revenue_daily`
**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.

View file

@ -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 |

View file

@ -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.

View file

@ -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.

View file

@ -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`.

View file

@ -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]]

View file

@ -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 12 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]]

View file

@ -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`

View file

@ -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.

View file

@ -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.

View file

@ -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]]

View file

@ -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.

View file

@ -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.

View file

@ -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",

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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.

View file

@ -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.

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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."

View file

@ -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.

View file

@ -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."

View file

@ -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();
});

View file

@ -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 {

View file

@ -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');

View file

@ -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;

View file

@ -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');
});

Some files were not shown because too many files have changed in this diff Show more