`

About Revenue Reimagined

Revenue Reimagined (RR) is a GTM strategy and revenue operations firm that helps B2B SaaS and services companies hit their revenue targets. They partner with leadership teams to diagnose gaps, design the fix, implement it, and optimize until results are achieved.

RR is a Papr design partner — building a fully automated audit-to-implementation platform powered by Papr's vector memory and knowledge graph.

RR's 4-Phase Engagement Model

Using their proprietary GTM Gap framework, crafted from working with dozens of SaaS and service companies, RR follows a structured engagement model:

1

Audit

Systematic GTM evaluation across 5 domains. Stakeholder interviews, rubric scoring (0-2 scale), evidence collection, conflict resolution. Produces a quantified maturity assessment.

2

Discover

Deep dive into the company, customers, and the "why" behind the gaps. Understanding context before prescribing solutions.

3

Implement

GTM Sprints built from audit findings. Prioritized by impact and effort, with clear owners and timelines. RR works alongside the team to execute.

4

Optimize

Continuous feedback and data-driven iteration. Track sprint progress, measure results, adjust. RR stays engaged until revenue targets are hit.

The Rubric: 5 Domains, 178 Items

Proprietary scoring framework. Each item scored 0-2: 0 = Doesn't Exist, 1 = Partially There, 2 = Fully Deployed.

DomainSubcategoriesItems
Strategy & OperationsStrategy & GTM, Business Ops, Revenue Ops43
MarketingDemand Gen, Content, Events/Brand/Comms31
SalesRoles & Comp, SDR, Enablement, Management, Motivation, Partners, Cadence46
Customer SuccessCS Ops & Strategy, Onboarding, Retention, Health22
People OperationsCulture, Hiring, Key Hires & Quality36

Where RR Is Today

Built (RR's Paprwork Instance)

Admin PortalNext.js app on Digital Ocean with Supabase backend
Customer PortalClient-facing views for reports and sprint tracking
Auth LayerSupabase Auth with role-based access control
DatabaseSupabase PostgreSQL with RLS for multi-tenant isolation

Not Yet Built

Agent JobsAutomated pipeline: transcript processing, scoring, conflict detection, recommendations, reports
Vector MemoryPapr memory for semantic search across audit evidence and findings
Knowledge GraphPapr KG for structured relationships: clients, domains, scores, patterns

What RR Wants to Achieve

Automate the audit pipelineUpload transcripts, get scored rubric + findings + recommendations + sprint plan automatically
Cross-client intelligencePattern detection across audits — "companies with weak RevOps also score low on forecasting"
Sprint tracking with feedbackTrack implementation, measure progress, re-score after sprints complete
Scalable deliveryServe more clients with the same team by automating the heavy parts
``; }; const p0Tasks: Task[] = [ { name:'Architecture Design', status:'done', detail:'3-tier architecture: Supabase → Papr Vector → Papr KG. Data flow defined.' }, { name:'KG Schema & Node Policies', status:'done', detail:'14 node types with uniqueness policies (lookup/upsert/per-audit). Relationship map complete.' }, { name:'SQL Schema (Supabase)', status:'done', detail:'All tables defined: lookup tables, auth, stakeholders, conflicts, recommendations, sprints.' }, { name:'Papr SDK Integration Guide', status:'done', detail:'3-tier integration: seed, structured sync, narrative. Correct SDK syntax verified.' }, { name:'Agent Job Specs', status:'done', detail:'Job creation patterns, pipeline DAG, autoTrigger dependencies documented.' }, { name:'Namespace & Embedding Strategy', status:'done', detail:'Per-client namespace_id, transform_embedding with mode:auto, domain_id:general.' }, { name:'Milestone Tracker', status:'done', detail:'This tracker — 6 phases, 38 tasks, critical path defined.' }, ]; const p1Tasks: Task[] = [ { name:'Supabase project setup', status:'done', detail:'PostgreSQL database provisioned. Connection pooling enabled.' }, { name:'Auth configuration', status:'done', detail:'Supabase Auth with email/password + invite-only for clients. RLS policies.' }, { name:'Lookup tables seeded', status:'done', detail:'5 domains, 20 subcategories inserted. display_order and icons set.' }, { name:'Core tables created', status:'done', detail:'clients, audits, rubric_items, rubric_scoring, evidence_sources, findings.' }, { name:'RLS policies applied', status:'done', detail:'Row-level security: clients see only their data, RR team sees all.' }, { name:'Profiles + roles', status:'done', detail:'user_profiles table with role enum (admin/consultant/client). Linked to Supabase Auth.' }, ]; const renderPhase = (title: string, icon: string, status: Status, tasks: Task[]) => { const done = tasks.filter(t => t.status === 'done').length; const pct = Math.round((done / tasks.length) * 100); const rows = tasks.map(t => `` ).join(''); return `

StatusTaskDetail
`; }; export function renderMilestones(): string { const total = 38; const done = 13; const pct = Math.round((done/total)*100); return `

Implementation Milestones

Overall Progress / tasks (%)

Phase 0 & 1 done. Next: Phase 2 (Agent Jobs). Critical path: P2 → (P3 ∥ P4) → P5

``; }; interface Task { name: string; status: Status; detail: string; } interface Milestone { id: string; title: string; status: Status; icon: string; tasks: Task[]; } const phases: Milestone[] = [ { id:'p2', title:'Phase 2 — Build Agent Jobs', status:'todo', icon:'ic-bolt', tasks:[ { name:'Transcript Processor', status:'todo', detail:'Agent job: parse uploaded transcripts, extract stakeholder quotes, map to rubric items, chunk & store to Papr vector memory with per-client namespace' }, { name:'Rubric Scorer', status:'todo', detail:'Agent job: score each rubric item per stakeholder based on transcript evidence. Write perspective_scores to Supabase with stakeholder_id FK' }, { name:'Conflict Detector', status:'todo', detail:'Agent job: compare perspective_scores per rubric item, flag disagreements >1pt variance, create conflict rows with resolution_status=pending' }, { name:'Audit Analyzer', status:'todo', detail:'Agent job: synthesize scores + resolved conflicts into domain-level findings. Write audit_synthesis and computed domain scores to Supabase' }, { name:'Recommendation Planner', status:'todo', detail:'Agent job: gap analysis (score vs 3.5 benchmark), impact/effort prioritization, cross-domain dependencies. Outputs max 8 recommendations with 2+ sprints each' }, { name:'Report Writer', status:'todo', detail:'Agent job: generate full narrative audit report incorporating findings, evidence, and recommendations. Write to Supabase audit_reports' }, { name:'Executive Summary Writer', status:'todo', detail:'Agent job: generate concise 2-page executive summary for client leadership. Write to Supabase exec_summaries' }, { name:'Memory Sync', status:'todo', detail:'Agent job: push all structured data to Papr KG (Tier 2) + report narrative sections to vector memory (Tier 3). Handles both graph and vector in one job' }, ]}, { id:'p3', title:'Phase 3 — Vector Memory Integration', status:'todo', icon:'ic-brain', tasks:[ { name:'Namespace strategy', status:'todo', detail:'Configure per-client namespace_id pattern. Set up transform_embedding with mode:auto, domain_id:general' }, { name:'Seed rubric reference data', status:'todo', detail:'Tier 1: seed Domain, Subcategory, RubricItem nodes to Papr so lookup policies work for future memory.add() calls' }, { name:'Verify transcript vector search', status:'todo', detail:'Test semantic search across transcript chunks — "what did the CRO say about pricing?" returns correct evidence' }, { name:'Verify cross-client search', status:'todo', detail:'Test namespace-scoped search. Queries in client A namespace never return client B data' }, ]}, { id:'p4', title:'Phase 4 — Knowledge Graph Integration', status:'todo', icon:'ic-graph', tasks:[ { name:'Register KG schema', status:'todo', detail:'Call papr.schemas.create() with all 14 node types, relationship definitions, and lookup/upsert policies' }, { name:'Build Memory Sync job logic', status:'todo', detail:'Implement the Tier 2 structured sync: Client, Audit, Stakeholder, PerspectiveScore, Conflict, Recommendation, Sprint nodes' }, { name:'Build Tier 3 narrative sync', status:'todo', detail:'Implement auto-extract mode for report sections — let Papr discover entities (tools, competitors, metrics) and link to known anchors' }, { name:'Verify graph queries', status:'todo', detail:'Test GraphQL queries: "all audits for Client X", "which domains score below 2.5 across all clients"' }, { name:'Cross-client pattern detection', status:'todo', detail:'Verify Pattern and Theme nodes merge correctly across audits — "weak pricing governance" seen in 5 clients = 1 node, 5 edges' }, ]}, { id:'p5', title:'Phase 5 — Portals & Deployment', status:'todo', icon:'ic-rocket', tasks:[ { name:'Admin portal — audit management', status:'todo', detail:'Client list, create/manage audits, upload transcripts, trigger pipeline, monitor job status' }, { name:'Admin portal — conflict resolution UI', status:'todo', detail:'View flagged conflicts, override scores, mark resolved. Only admin/RR team roles can resolve' }, { name:'Admin portal — report editing', status:'todo', detail:'Edit AI-generated report sections before publishing. Track edit history per section' }, { name:'Admin portal — benchmarking dashboard', status:'todo', detail:'Cross-client comparison (admin-only). Domain heatmaps, trend lines, anonymized percentiles' }, { name:'Customer portal — audit view', status:'todo', detail:'Read-only: executive summary, interactive scorecard with evidence drill-down, full report download' }, { name:'Customer portal — recommendation tracker', status:'todo', detail:'View recommendations + sprints, update sprint progress, mark deliverables complete' }, { name:'Publish & access controls', status:'todo', detail:'RR team clicks "Publish" → client gets portal access. Supabase RLS policies enforce data isolation' }, { name:'Deploy to Digital Ocean', status:'todo', detail:'Containerize admin + customer portals. CI/CD pipeline. SSL + custom domain setup' }, ]}, ]; export function renderMilestones2(): string { return phases.map(p => { const done = p.tasks.filter(t => t.status === 'done').length; const pct = Math.round((done / p.tasks.length) * 100); const rows = p.tasks.map(t => `
`).join(''); return `

%">
` : ''}
`

Architecture Overview

The platform has three layers: user-facing portals, a shared database with access controls, and Papr's AI backend for intelligence.

Admin Portal
RR Team
Client Mgmt · Pipeline Triggers · Report Editing · Benchmarks
Customer Portal
Client Stakeholders
Exec Summary · Scorecard · Full Report · Sprint Tracking
Supabase (PostgreSQL)
Shared Multi-Tenant DB + Auth
RLS per client · Lookup tables (rubric) · Scores · Reports · Sprints
Papr Vector Memory
Semantic Search
Evidence · Findings · Reports · Namespace per client
Papr Knowledge Graph
Structured Relationships
Clients · Domains · Scores · Patterns · Recommendations
Agent Jobs
Automation Pipeline
8 per-audit jobs · Triggered from Admin · Auto-chained

Data Flow

How data moves through the system for each client audit.

Transcripts Uploaded
Admin Portal → Supabase storage
Agent Pipeline Triggered
8 jobs run in sequence with auto-chaining
Scores + Findings Written
Jobs write to Supabase (structured) + Papr Memory (semantic)
KG Synced
Memory Sync job pushes nodes and relationships to Papr KG
Report Published
RR team reviews, edits, publishes → Customer Portal

Auth & Multi-Tenancy

LayerMechanism
Auth ProviderSupabase Auth (email/password + magic link)
Admin AccessRole = admin in profiles table — sees all clients, can trigger pipelines
Client AccessRole = client — RLS filters to assigned_clients only
Data IsolationPostgreSQL RLS + Papr namespace_id per client
DeploymentDigital Ocean (app) + Supabase (DB/auth) + Papr (AI backend)

Build Phases

PhaseWhatStatus
0Architecture & Implementation Guide (this app)DONE
1Supabase + Auth + Admin/Customer PortalsDONE
2Agent Jobs — 8-job automation pipelineTODO
3Vector Memory Integration (Papr SDK)TODO
4Knowledge Graph Integration (schema + sync)TODO
5Cross-client intelligence + re-audit flowTODO
`

Per-Audit Pipeline — 8 Agent Jobs

Each audit triggers this pipeline. All jobs are LLM-powered agents with access to Supabase + Papr SDK. DB Setup & Rubric Seeding are one-time tasks (Phase 1, done) — not per-audit jobs.

1. Transcript
Processor
2. Rubric
Scorer
3. Conflict
Detector
4. Audit
Analyzer
5. Recommendation
Planner
6. Report
Writer
7. Executive Summary
Writer
8. Memory
Sync

What Each Job Does

#JobReadsWrites
1Transcript Processor
Parse & chunk transcripts, extract stakeholder quotes, map to rubric items, store chunks to Papr vector
Uploaded transcript filesSupabase: evidence_sources
Papr: vector chunks (Tier 1)
2Rubric Scorer
Score each rubric item per stakeholder from transcript evidence
evidence_sources, rubric_itemsSupabase: perspective_scores
3Conflict Detector
Compare stakeholder scores per rubric item, flag disagreements >1pt
perspective_scoresSupabase: conflicts (status=pending)
4Audit Analyzer
Synthesize scores + resolved conflicts into domain-level findings
perspective_scores, conflictsSupabase: audit_synthesis, domain scores
5Recommendation Planner
Gap analysis, impact/effort prioritization, sprint planning
synthesis, conflicts, evidenceSupabase: recommendations, sprints
6Report Writer
Generate full narrative audit report with findings + recommendations
synthesis, recommendations, evidenceSupabase: audit_reports
7Executive Summary Writer
Concise 2-page summary for client leadership
audit_reports, recommendationsSupabase: exec_summaries
8Memory Sync
Push structured data to Papr KG + report narrative to vector memory
All Supabase tables for this auditPapr: KG nodes (Tier 2) + vectors (Tier 3)

Creating Agent Jobs

Use create_job with type: "agent". The command is the prompt the agent follows.

create_job({
  name: "Rubric Scorer",
  type: "agent",
  command: "Score each rubric item...\\n\\n## Steps\\n1. Read evidence...\\n2. For each item...",
  dependsOn: [{
    jobId: "transcript-processor-id",
    onStatus: "completed",
    autoTrigger: true
  }],
  retries: { maxAttempts: 2, backoffMs: 5000 }
})
autoTrigger: true chains the pipeline. When Transcript Processor completes → Rubric Scorer starts → and so on through all 8 jobs.

Papr SDK in Agent Jobs

Graph nodes are created automatically via policy.graph in memory.add() — no separate step. The schema's lookup/upsert policies control matching.

Recommendation Planner — Deep Dive

Sits between Audit Analyzer and Report Writer. The analyzer says what's wrong. The planner says what to do about it.

InputProcessOutput
Synthesis findings
Resolved conflicts
Domain scores
Evidence sources
Gap analysis (score vs 3.5)
Impact/effort matrix
Cross-domain dependencies
Stakeholder alignment
Max 8 prioritized recommendations
2+ sprints each
Success criteria
Quarterly timeline
`

Knowledge Graph Schema

Schema: Revenue Reimagined Audit Intelligence

Register your own schema using the code in the Integration tab. You will receive a unique schema ID back from the API.

Register via the Papr SDK using node(), prop(), edge() builders. Policies (lookup() / upsert()) control how nodes are created/matched when you call papr.memory.add(). See Integration tab for code.

Node Types & Uniqueness Policies

NodeUnique ByPolicyRationale
Clientnameupsert()One node per company. Re-audit links to same.
Stakeholderclient + name + roleupsert()Same person across audits = same node.
Domainnamelookup()5 rubric domains. Never duplicated.
Subcategorydomain + namelookup()20 subcats. Shared across all audits.
RubricItemdomain + subcat + namelookup()178 criteria. Same across audits.
⚠ Seeding required for lookup() nodes: Domain, Subcategory, RubricItem use @lookup — they only MATCH existing nodes, never create. Seed them first via memory_policy override with create: "upsert". See Integration tab, Tier 1.
NodeUnique ByPolicyRationale
Auditclient + dateupsert()Each engagement is distinct. Keyed by client+date.
PerspectiveScoreaudit + stakeholder + itemupsert()Each stakeholder's score per item.
Conflictaudit + rubric_itemupsert()Disagreement in that audit context.
Findingaudit + subcategoryupsert()Different findings per audit.
Recommendationaudit + titleupsert()Actionable advice per engagement.
Sprintrecommendation + numberupsert()Implementation chunks.
SprintUpdatesprint + dateupsert()Progress tracking. Always appended.
Patternnameupsert()Cross-client. 1 node, many edges.
Themenameupsert()High-level grouping across patterns.

Key Relationships

Client ──HAS_AUDIT──▶ Audit
Audit ──SCORED_BY──▶ Stakeholder
Stakeholder ──GAVE_SCORE──▶ PerspectiveScore
PerspectiveScore ──FOR_ITEM──▶ RubricItem
RubricItem ──IN_SUBCATEGORY──▶ Subcategory
Subcategory ──IN_DOMAIN──▶ Domain
Audit ──HAS_CONFLICT──▶ Conflict
Conflict ──RESOLVED_TO──▶ FinalScore
Audit ──HAS_FINDING──▶ Finding
Audit ──HAS_RECOMMENDATION──▶ Recommendation
Recommendation ──HAS_SPRINT──▶ Sprint
Sprint ──HAS_UPDATE──▶ SprintUpdate
Pattern ──OBSERVED_IN──▶ Audit
Theme ──GROUPS──▶ Pattern
`

SQL Schema — Full Specification

Supabase (PostgreSQL) DB shared by both portals. Domain/Subcategory are lookup tables (not enums) — FK-enforced but extensible without migrations. Auth handled by Supabase Auth.

Lookup Tables (Rubric Framework)

CREATE TABLE domains (
  id SERIAL PRIMARY KEY,
  name TEXT UNIQUE NOT NULL,
  display_order INTEGER, icon TEXT,
  active BOOLEAN DEFAULT true
);

CREATE TABLE subcategories (
  id SERIAL PRIMARY KEY,
  domain_id INTEGER REFERENCES domains(id),
  name TEXT NOT NULL, display_order INTEGER,
  active BOOLEAN DEFAULT true,
  UNIQUE(domain_id, name)
);

CREATE TABLE rubric_items (
  id SERIAL PRIMARY KEY,
  domain_id INTEGER REFERENCES domains(id),
  subcategory_id INTEGER REFERENCES subcategories(id),
  item_name TEXT NOT NULL, max_score INTEGER DEFAULT 2,
  UNIQUE(domain_id, subcategory_id, item_name)
);

Auth & Access Control

Auth handled by Supabase Auth. Profiles table links user to role/client:

CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id),
  role TEXT CHECK(role IN ('admin','team','client')) NOT NULL,
  client_id INTEGER REFERENCES clients(id),
  display_name TEXT,
  active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE assigned_clients (
  team_member_id UUID REFERENCES profiles(id),
  client_id INTEGER REFERENCES clients(id),
  PRIMARY KEY(team_member_id, client_id)
);
Supabase RLS: auth.uid() = profiles.id + role checks. Client users auto-filtered by client_id. RR team scoped via assigned_clients.

Stakeholders & Conflict Resolution

CREATE TABLE stakeholders (
  id SERIAL PRIMARY KEY,
  client_id INTEGER REFERENCES clients(id),
  name TEXT NOT NULL, role TEXT, email TEXT,
  UNIQUE(client_id, name, role)
);

CREATE TABLE perspective_scores (
  id SERIAL PRIMARY KEY,
  audit_id INTEGER REFERENCES audits(id),
  stakeholder_id INTEGER REFERENCES stakeholders(id),
  rubric_item_id INTEGER REFERENCES rubric_items(id),
  score INTEGER NOT NULL, citation TEXT,
  evidence_source_id INTEGER REFERENCES evidence_sources(id),
  UNIQUE(audit_id, stakeholder_id, rubric_item_id)
);

CREATE TABLE audit_conflicts (
  audit_id INTEGER REFERENCES audits(id),
  rubric_item_id INTEGER REFERENCES rubric_items(id),
  score_spread INTEGER, resolution_notes TEXT,
  final_score INTEGER,
  resolved_by UUID REFERENCES profiles(id),
  resolved_at TIMESTAMPTZ
);
`

Recommendations → Sprints → Updates

CREATE TABLE recommendations (
  id SERIAL PRIMARY KEY,
  audit_id INTEGER REFERENCES audits(id),
  title TEXT NOT NULL, description TEXT,
  priority TEXT CHECK(priority IN ('critical','high','medium','low')),
  domain_id INTEGER REFERENCES domains(id),
  status TEXT DEFAULT 'proposed',
  created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE sprints (
  id SERIAL PRIMARY KEY,
  recommendation_id INTEGER REFERENCES recommendations(id),
  sprint_number INTEGER NOT NULL,
  title TEXT NOT NULL, target_quarter TEXT,
  status TEXT DEFAULT 'planned',
  started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ,
  UNIQUE(recommendation_id, sprint_number)
);

CREATE TABLE sprint_updates (
  id SERIAL PRIMARY KEY,
  sprint_id INTEGER REFERENCES sprints(id),
  update_date DATE NOT NULL,
  status TEXT NOT NULL, notes TEXT,
  updated_by UUID REFERENCES profiles(id),
  created_at TIMESTAMPTZ DEFAULT now()
);

Report Publishing & Editing

CREATE TABLE report_edits (
  id SERIAL PRIMARY KEY,
  audit_id INTEGER REFERENCES audits(id),
  section_key TEXT NOT NULL,
  original_content TEXT, edited_content TEXT,
  edited_by UUID REFERENCES profiles(id),
  edited_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE publish_status (
  id SERIAL PRIMARY KEY,
  audit_id INTEGER REFERENCES audits(id) UNIQUE,
  status TEXT DEFAULT 'draft',
  published_at TIMESTAMPTZ,
  published_by UUID REFERENCES profiles(id)
);
RR team edits via report_edits — originals preserved. publish_status gates visibility: only published audits appear in customer portal.
`

Papr Memory SDK — v2.27+

Install: pip install papr-memory (Python) or npm install @papr/memory (TS)

# Python
from papr_memory import Papr
client = Papr()  # reads PAPR_MEMORY_API_KEY env var

// TypeScript
import Papr from "@papr/memory";
const client = new Papr();

Three Data Tiers — What Goes Where

TierSourceGraph ApproachWhy
1. SeedRubric defs memory_policy override w/ upsert Lookup nodes must EXIST before schema can find them
2. StructuredSupabase link_to exact matches Known graph — scores, conflicts. Tell Papr exactly.
3. NarrativeReports graph.mode: auto + link_to Rich text has extra entities Papr should discover
Key: Supabase = operational DB. Papr KG = intelligence layer. Structured data → explicit graph. Report narrative → auto-extract + link.

Tier 1 — Seed Lookup Nodes (Run Once)

@lookup nodes ONLY match, never create. Seed them first with a memory_policy override.

from papr_memory.lib import build_memory_policy, serialize_set_values

domains = ["Revenue Operations", "Sales", "Marketing",
           "Customer Success", "Pricing and Packaging"]

for name in domains:
  client.memory.add(
    content=f"RR Rubric Domain: {name}",
    namespace_id="rr_rubric",
    metadata={"customMetadata": {"node_type": "Domain"}},
    memory_policy=build_memory_policy(
      schema_id="rr_audit_intel",
      node_constraints=[{
        "node_type": "Domain",
        "create": "upsert",  # Override: force create
        "search": {"properties": [
          {"name": "name", "mode": "exact"}
        ]},
        "set": serialize_set_values({"name": name})
      }]
    )
  )
Why override? Schema says @lookup for Domain (never create). Override with create: "upsert" per-memory for seeding. Once done, future memory.add() calls use schema's lookup policy.
`

Tier 2 — Structured Data (Supabase → KG)

You know the graph. Use link_to with exact matches.

# Sync a score from Supabase → Papr KG
client.memory.add(
  content=f"Score: {item_name} = {score}/5 by {stakeholder}. "
          f"Evidence: {evidence_text}",
  namespace_id=f"client_{client_slug}",
  external_user_id=f"stakeholder_{stakeholder_id}",
  metadata={
    "customMetadata": {
      "audit_id": audit_id,
      "domain": domain_name,
      "score_value": score,
      "source": "supabase_sync"
    }
  },
  policy={
    "transform_embedding": {"mode": "auto", "domain_id": "general"},
    "graph": {
      "mode": "auto",
      "link_to": [
        f"Client:name={client_name}",        # exact
        f"Audit:date={audit_date}",           # exact
        f"Domain:name={domain_name}",         # exact lookup
        f"Stakeholder:name={stakeholder}"     # exact
      ]
    }
  }
)
Exact matches use = (not ~). Domain uses @lookup — matches seeded node, never creates. Client/Audit use @upsert — creates if new.

Tier 3 — Narrative Content (Auto-Extract + Link)

Report text has entities Supabase doesn't capture. Let Papr discover them.

# Sync a finding — auto-extract + link to known nodes
client.memory.add(
  content=finding_narrative,  # Rich text w/ tools, metrics
  namespace_id=f"client_{client_slug}",
  metadata={
    "customMetadata": {
      "content_type": "finding",
      "domain": domain_name,
      "audit_id": audit_id
    }
  },
  policy={
    "transform_embedding": {"mode": "auto", "domain_id": "general"},
    "graph": {
      "mode": "auto",  # LLM extracts additional entities
      "link_to": [
        f"Client:name={client_name}",
        f"Domain:name={domain_name}"
      ]
    }
  }
)
# Papr auto-extracts: tool names, competitor mentions,
# metrics — builds additional graph relationships
The mix: link_to anchors to known nodes. mode: "auto" discovers new entities from narrative that aren't in Supabase.
`

Schema Registration

Register once. Defines node policies that memory.add() auto-applies.

from papr_memory.lib import (
  schema, node, lookup, upsert, constraint,
  prop, edge, exact, semantic, Auto,
  build_schema_params
)

@schema("rr_audit_intel")
class RRAuditSchema:
  @node
  @upsert
  class Client:
    name: str = prop(required=True, search=exact())

  @node
  @lookup   # Never create — must be seeded first
  class Domain:
    name: str = prop(required=True, search=exact())

  @node
  @upsert   # Merge same person across audits
  class Stakeholder:
    name: str = prop(required=True, search=semantic(0.9))
    role: str = prop(search=exact())

  @node
  @upsert
  @constraint(set={"overall_score": Auto()})
  class Audit:
    date: str = prop(required=True, search=exact())
    overall_score: float = prop()

  has_audit = edge(Client, Audit, create="upsert")
  scored_by = edge(Audit, Stakeholder, create="upsert")
  covers = edge(Audit, Domain, create="lookup")

params = build_schema_params(RRAuditSchema)
client.schemas.create(**params)

Namespace Strategy

NamespaceDataExample
client_{slug}Per-client audit dataclient_acme_corp
rr_rubricShared rubric definitionsDomains, subcategories, items
rr_globalCross-client patternsBenchmarks, themes
Data segregation: Each client's data in its own namespace. Rubric reference data shared. Cross-client intelligence in rr_global — admin-only.

Search — Querying the KG

# Vector + graph search within a client namespace
results = client.memory.search(
  query="What are the main pricing governance gaps?",
  namespace_id="client_acme_corp",
  max_memories=10,
  rank_results=True
)

# Cross-client benchmarking (admin only)
results = client.memory.search(
  query="Common patterns in customer success scoring",
  namespace_id="rr_global",
  max_memories=20
)