Skip to content

clementrog/fresh

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

113 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fresh

Tenant-scoped TypeScript application for:

  • ingesting internal and market signals
  • turning them into editorial opportunities
  • generating drafts
  • tracking what was actually published
  • ingesting LinkedIn performance
  • deriving scorecards, idea families, family guidance, and opportunity priority
  • serving the admin UI
  • running the sales signal pipeline

In plain language:

  • Fresh is no longer just a draft generator
  • it is an editorial operating system with a closed feedback loop
  • it learns from what got published and how it performed

How Fresh Works

Fresh has 3 clear system roles:

  • Fresh DB: the canonical source of truth
    • opportunities
    • drafts
    • PublishedArtifact
    • AnalyticsSnapshot
    • DerivedScore
    • idea-family and advisory layers
  • Notion Content Calendar: the operator workflow surface
    • planning
    • owner
    • status
    • final published text
    • LinkedIn URL
  • LinkedIn: the external platform truth
    • post URL / URN when available
    • analytics numbers

Important rule:

  • ContentOpportunity is not the content calendar
  • workflow stays in Notion
  • publication truth and learning live in Fresh

There are 2 different Notion surfaces:

  • Content Opportunities: a Fresh-managed mirror for operators to inspect
    • not the database of record
    • humans should not use it as the planning surface
  • Content Calendar: the planning / execution surface
    • owner
    • schedule
    • publication status
    • final published text and URL when available

In plain language:

  • the opportunities view and the calendar are not the same Notion database
  • they must not share the same database ID
  • runtime calendar sync only depends on the Content Calendar database

Current Product Model

The editorial loop now works like this:

  1. Fresh ingests raw signals from configured sources.
  2. Fresh turns those signals into Opportunity rows.
  3. Fresh generates Draft rows from selected opportunities.
  4. Operators manage planning in the Notion Content Calendar.
  5. When a post is actually published, Fresh crystallises a PublishedArtifact.
  6. Fresh ingests LinkedIn analytics into append-only AnalyticsSnapshot rows.
  7. Fresh computes DerivedScore rows from canonical analytics.
  8. Fresh derives idea families, family matching, variant guidance, family coverage gaps, and opportunity priority advisories.

In plain language:

  • Fresh now knows what got published
  • how it performed
  • which family it belongs to
  • whether a new opportunity is fresh, too close, promising, or low leverage

Multi-Company Status

The multi-company isolation program is already shipped.

Meaning:

  • tenant context is explicit
  • credentials and settings are company-scoped
  • fail-closed guards are active
  • RLS is in place
  • connectors, Notion, and sales flows are tenant-scoped

If you need the detailed plan and rationale, read:

Key Runtime Rules

  • PostgreSQL is the source of truth.
  • Every content command is tenant-scoped and requires --company <slug>.
  • Notion is a workflow surface, not the database of record.
  • Tone of voice is repo-first.
    • base profiles live in editorial/profiles
    • if NOTION_TONE_OF_VOICE_DB_ID is configured, Notion entries are applied as overrides on top of those repo profiles
    • Notion is not the canonical source unless the architecture is explicitly changed
  • LinkedIn analytics ingestion is append-only.
  • Canonical analytics precedence is:
    • api > csv > manual
  • Historical format analysis must use frozen PublishedArtifact.format, not mutable opportunity fields.

In plain language:

  • engineering owns the base voice profiles in the repo
  • Notion can refine them, but does not replace them
  • this avoids split-brain about which voice definition wins at runtime

Setup

Install dependencies:

pnpm install

Generate Prisma client:

pnpm prisma:generate

Apply migrations:

pnpm prisma:migrate:deploy

Local developer migration flow:

pnpm prisma:migrate:dev

Validation

Typecheck:

pnpm run typecheck

Unit tests:

pnpm test

Integration tests:

pnpm test:integration

Build:

pnpm run build

Core Editorial Commands

All commands below require --company <slug>.

1. Ingest source signals

pnpm run ingest:run -- --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts ingest:run --company <slug> --dry-run

2. Run market research

pnpm run market-research:run -- --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts market-research:run --company <slug> --dry-run

3. Run intelligence

Turns source items into editorial opportunities and enriches existing ones.

pnpm run intelligence:run -- --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts intelligence:run --company <slug> --dry-run

4. Generate one draft

pnpm run draft:generate -- --company <slug> --opportunity-id <opportunity-id>

5. Generate drafts for ready opportunities

pnpm exec tsx src/cli.ts draft:generate-ready --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts draft:generate-ready --company <slug> --dry-run

6. Sync the Notion Content Calendar

Reads the operator workflow surface and crystallises publication truth into Fresh.

pnpm exec tsx src/cli.ts notion:calendar:sync --company <slug>

This command is the bridge from:

  • planned draft in Notion
  • to actual published artifact in Fresh

7. Ingest LinkedIn analytics from CSV

pnpm exec tsx src/cli.ts analytics:snapshot:ingest --company <slug> --csv <path-to-linkedin-export.csv>

Important:

  • CSV is a primary ingestion path, not a temporary hack
  • URNs are never derived by parsing URLs
  • duplicate (artifact, date, source) rows are rejected

8. Recompute derived scores

Recomputes:

  • DerivedScore
  • family variant coverage
  • variant guidance
  • opportunity priority advisories
pnpm run feedback:recompute-scores -- --company <slug>

Force recompute:

pnpm run feedback:recompute-scores -- --company <slug> --force

9. Recompute family layers

This is the explicit recompute path for family clustering and related advisory layers.

It requires 3 explicit method flags:

  • --cluster-method
  • --state-method
  • --candidate-rule-method

Example:

pnpm run feedback:recompute-families -- --company <slug> \
  --cluster-method <cluster-method> \
  --state-method <state-method> \
  --candidate-rule-method <candidate-rule-method>

Fresh refuses to run this command without those explicit method values.

That matters because:

  • family logic is derived
  • family logic is versioned
  • reruns must be intentional

Recommended Editorial Loop

The normal operator loop is now:

pnpm run ingest:run -- --company <slug>
pnpm run market-research:run -- --company <slug>
pnpm run intelligence:run -- --company <slug>
pnpm exec tsx src/cli.ts draft:generate-ready --company <slug>
pnpm exec tsx src/cli.ts notion:calendar:sync --company <slug>
pnpm exec tsx src/cli.ts analytics:snapshot:ingest --company <slug> --csv <path.csv>
pnpm run feedback:recompute-scores -- --company <slug>

In plain language:

  • generate opportunities
  • generate drafts
  • sync what actually got published
  • ingest LinkedIn results
  • recompute learning layers

Nightly orchestration

For one-command execution of the daily editorial pass, use pipeline:nightly:

pnpm run pipeline:nightly -- --company <slug>

It chains the same five steps (ingest:run → intelligence:run → draft:generate-ready → notion:calendar:sync → feedback:recompute-scores) under a single parent SyncRun, one company per invocation.

Flags:

  • --skip <step,step> — omit one or more steps (example: --skip notion:calendar:sync,feedback:recompute-scores)
  • --stop-on-failure — abort the run on the first step failure instead of soft-continuing
  • --max-cost-usd <n> — stop between steps once cumulative LLM spend for this run reaches the cap

Error policy:

  • default = soft-continue on ordinary step failures (each failure is recorded on the parent run's warnings)
  • PipelineHardStopError and ForbiddenError always hard-stop, regardless of --stop-on-failure
  • cost-cap breach hard-stops the run but the final status is completed (the cap was respected, not a failure)

No scheduler is wired in yet — invoke the command from any cron, systemd timer, or CI job.

Admin UI

Start the server:

pnpm run server:start

The admin includes, among others:

  • Dashboard
  • Source Items
  • Opportunities
  • Drafts
  • Published
  • Scorecard
  • Idea Families
  • review queues
  • doctrine and source config pages

The admin is now an operator tool for:

  • editorial review
  • publication truth inspection
  • analytics ingestion and verification
  • scorecard reading
  • family and priority guidance

Publication Feedback Model

Fresh stores publication truth in PublishedArtifact.

That row freezes:

  • company
  • draft
  • opportunity
  • actual owner
  • actual publish timestamp
  • LinkedIn URL
  • final published text
  • frozen format

Analytics then land in AnalyticsSnapshot.

Scores and advisory layers are derived from there.

Important consequences:

  • raw analytics are never overwritten
  • derived layers are recomputable
  • historical reads should rely on frozen published fields, not mutable opportunity fields

Current Advisory Layers

Fresh currently supports these post-publication and pre-publication read layers:

  • Scorecard
    • top posts
    • flop posts
    • surprising posts
  • Idea Families
    • cluster related published posts
  • Opportunity Family Candidates
    • likely family match for an open opportunity
  • Opportunity Variant Guidance
    • fresh_variant
    • too_close
    • unclear
  • Family Variant Coverage
    • which owner/format combinations are proven, weak, or credibly untested
  • Opportunity Priority Advisory
    • high_leverage
    • worth_testing
    • hold
    • unclear

These are advisory layers.

Meaning:

  • they help the operator decide
  • they do not auto-publish
  • they do not auto-schedule

Editorial Maintenance

Backfill evidence packs:

pnpm run backfill:evidence -- --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts backfill:evidence --company <slug> --dry-run

Cleanup retention:

pnpm run cleanup:retention -- --company <slug>

Cleanup Claap publishability drift:

pnpm run cleanup:claap-publishability -- --company <slug>

Dry-run:

pnpm exec tsx src/cli.ts cleanup:claap-publishability --company <slug> --dry-run

Inspect tone-of-voice profiles:

pnpm run tone:inspect -- --company <slug>

Run scope migration helper:

pnpm run scope-migration:run

Sales Pipeline

Sales commands use src/sales/cli.ts, not the editorial CLI.

Config and preflight

pnpm run sales:check-config
pnpm run sales:preflight

HubSpot sync

pnpm run sales:sync

Extract activities into facts

pnpm run sales:extract
pnpm run sales:extract -- --reprocess
pnpm run sales:extract -- --drain
pnpm run sales:extract -- --batch-size 100

Additional sales commands

pnpm run sales:detect
pnpm run sales:match
pnpm run sales:cleanup
pnpm run sales:status
pnpm run sales:cleanup-orphans

Read Before Changing Feedback Logic

If you touch the feedback loop, read:

Those docs define the non-negotiable rules around:

  • tenant scoping
  • Notion handoff
  • publication truth
  • append-only analytics
  • frozen format
  • versioned derived layers

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages