project

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package project is a newsletter's editorial workspace on disk: research, the concept, the backlog of ideas, issue drafts and what each sent issue taught. lfr check reads it, holds every file to its format and the issues to the concept, compares it with the list on the server, and names the one step that comes next, so an agent can run a newsletter for months with nobody watching, or pick up a topic the owner hands in.

Index

Constants

View Source
const (
	PhaseSetup    = "setup"    // lfr.json is broken
	PhaseResearch = "research" // collect voices and peers until the minimums hold
	PhaseConcept  = "concept"  // write or repair the editorial brief
	PhaseLaunch   = "launch"   // welcome issue, list settings and website match the brief
	PhaseLearn    = "learn"    // a sent issue has numbers nobody wrote down
	PhasePlan     = "plan"     // the backlog is too thin to pick a good next issue
	PhaseDraft    = "draft"    // write the next issue from the backlog
	PhaseFix      = "fix"      // a draft breaks a rule
	PhaseTest     = "test"     // push the draft and send a test copy
	PhaseApprove  = "approve"  // the owner approves the tested draft
	PhaseSend     = "send"     // schedule the tested draft for its slot
	PhaseWait     = "wait"     // the next issue is scheduled; nothing to do
)

Phases, in the order a newsletter moves through them. lfr check names exactly one, with the next step and the skill that covers it.

View Source
const (
	MinOpenIdeas   = 4              // fewer open ideas and the next pick is forced, not chosen
	SettleTime     = 48 * time.Hour // numbers of an issue settle after two days
	MaxDraftsAhead = 3              // more unsent drafts than this and the backlog rots in issues/
)

Thresholds of the plan and learn phases.

View Source
const (
	RuleConfig    = "project.config"
	RuleDraft     = "project.draft"
	RuleBacklog   = "project.backlog"
	RuleLearnings = "project.learnings"
	RuleSync      = "project.sync"
	RuleCadence   = "project.cadence"
	RuleGuardrail = "project.guardrail"

	RuleSkills          = "project.skills"
	RuleBacklogFormat   = "backlog.format"
	RuleLearningsFormat = "learnings.format"
	RuleServer          = "project.server"
	RuleImages          = "project.images"
)

Rule IDs of the project checks.

View Source
const (
	ConfigFile    = "lfr.json"
	BacklogFile   = "backlog.md"
	LearningsFile = "learnings.md"
	WelcomeFile   = "welcome.md"
	IssuesDir     = "issues"
	SiteDir       = "site"
)

Files and folders of a project.

View Source
const (
	AutonomyDraft = "draft" // the agent drafts and tests; the owner approves each send
	AutonomySend  = "send"  // the agent sends when lfr check is clean
)

Autonomy settings.

Variables

View Source
var Skills = map[string]string{
	PhaseSetup: "lfr-setup", PhaseResearch: "lfr-research", PhaseConcept: "lfr-concept", PhaseLaunch: "lfr-launch",
	PhaseLearn: "lfr-learn", PhasePlan: "lfr-issue", PhaseDraft: "lfr-issue", PhaseFix: "lfr-issue", PhaseTest: "lfr-issue",
	PhaseApprove: "lfr-issue", PhaseSend: "lfr-issue", PhaseWait: "",
}

Skills names the skill for each phase.

View Source
var Statuses = []string{"focus", "idea", "planned", "drafted", "sent", "dropped"}

Idea statuses, in the order an idea moves through them.

Functions

func Facts

func Facts() map[string]any

Facts are the numbers and names skills render with, so a skill never states a minimum or a format the checks do not enforce.

func Find

func Find(dir string) (string, bool)

Find walks up from dir to the folder holding lfr.json.

func Init

func Init(dir string, c Config, name string) ([]string, error)

Init creates a project in dir for a list. dir must be empty or missing.

func Rules

func Rules() []finding.Rule

Rules describes the project checks.

Types

type Config

type Config struct {
	SchemaVersion int    `json:"schemaVersion"`
	List          string `json:"list"`               // the list's slug on the server
	Language      string `json:"language"`           // en or de
	Owner         string `json:"owner,omitempty"`    // where test copies go
	Autonomy      string `json:"autonomy,omitempty"` // draft (default) or send
}

Config is lfr.json.

func LoadConfig

func LoadConfig(dir string) (Config, error)

LoadConfig reads lfr.json in dir.

func (Config) Mode

func (c Config) Mode() string

Mode returns the autonomy, defaulting to draft.

func (Config) Validate

func (c Config) Validate() error

Validate checks the config.

type Draft

type Draft struct {
	File      string `json:"file"`
	Slug      string `json:"slug"`
	Subject   string `json:"subject"`
	Preheader string `json:"preheader,omitempty"`
	Idea      string `json:"idea,omitempty"`
	Welcome   bool   `json:"welcome,omitempty"`
	Source    string `json:"-"` // the whole file, as the server receives it
	Markdown  string `json:"-"` // the body after the front matter
}

Draft is an issue on disk: issues/<slug>.md, or welcome.md.

---
subject: Your starter is hungry, not dead
preheader: Two feeds a day fix the smell
idea: I007
---
Body in Markdown…

func Drafts

func Drafts(dir string) ([]Draft, error)

Drafts reads issues/*.md in name order.

func ParseDraft

func ParseDraft(rel, src string) Draft

ParseDraft reads an issue file the way the server will.

func (Draft) BodyLine

func (d Draft) BodyLine() int

BodyLine is the file line where the body after the front matter begins.

func (Draft) Same

func (d Draft) Same(w *RemoteIssue) bool

Same reports whether the server's copy carries this content.

type Idea

type Idea struct {
	Line    int
	ID      int
	Title   string
	Pillar  string
	Added   time.Time
	Status  string
	Angle   string
	Sources []string
}

Idea is one backlog entry.

func ParseBacklog

func ParseBacklog(text string, pillars []string) ([]Idea, []finding.Finding)

ParseBacklog reads backlog.md:

### I001 · <title> · <pillar> · added YYYY-MM-DD · <status>
<the angle, one or more lines>
Sources: V012, E104, https://…, owner

func (Idea) Code

func (i Idea) Code() string

Code is the idea's ID as written: I007.

func (Idea) Open

func (i Idea) Open() bool

Open reports whether the idea still waits for an issue.

type Learning

type Learning struct {
	Line  int
	Slug  string
	Sent  time.Time
	Notes []string
}

Learning is one entry of learnings.md.

func ParseLearnings

func ParseLearnings(text string) ([]Learning, []finding.Finding)

ParseLearnings reads learnings.md:

### <slug> · sent YYYY-MM-DD · recipients N · clicks N · replies N · unsubscribes N · complaints N
- Keep: …
- Change: …

type Numbers

type Numbers struct {
	Recipients   int `json:"recipients"`
	Sent         int `json:"sent"`
	Bounced      int `json:"bounced"`
	Clicked      int `json:"clicked"`
	Replies      int `json:"replies"`
	Unsubscribed int `json:"unsubscribed"`
	Complained   int `json:"complained"`
}

Numbers are the counts of a sent issue the learn phase records.

func (Numbers) Line

func (n Numbers) Line(slug string, sent time.Time) string

Line is the heading of a learnings.md entry for an issue.

func (Numbers) Rate

func (n Numbers) Rate(x int) float64

Rate returns n as a percentage of delivered mail.

type Options

type Options struct {
	Now     time.Time
	Builtin map[string]bool // citable IDs of lfr's built-in research ("lfr:E012")
	Remote  *Remote
}

Options steer a check.

type Remote

type Remote struct {
	Error  string        `json:"error,omitempty"` // why the server could not be asked
	List   *RemoteList   `json:"list,omitempty"`  // nil: the list does not exist
	Issues []RemoteIssue `json:"issues,omitempty"`
}

Remote is what the server knows about the project's list. The CLI fills it; a nil Remote means the files are checked alone.

type RemoteIssue

type RemoteIssue struct {
	ID        int64     `json:"id"`
	Slug      string    `json:"slug"`
	Subject   string    `json:"subject"`
	Preheader string    `json:"preheader,omitempty"`
	Markdown  string    `json:"-"`
	Status    string    `json:"status"`
	Welcome   bool      `json:"welcome,omitempty"`
	Scheduled time.Time `json:"scheduled,omitzero"`
	Started   time.Time `json:"started,omitzero"`
	Tested    time.Time `json:"tested,omitzero"`
	Stats     *Numbers  `json:"stats,omitempty"`
	// Findings are the server's checks of the rendered issue: the legal
	// footer, size, images. Only for issues not sent yet.
	Findings []finding.Finding `json:"findings,omitempty"`
}

RemoteIssue is an issue as the server has it. Markdown is filled for drafts and scheduled issues only.

type RemoteList

type RemoteList struct {
	Slug        string        `json:"slug"`
	Name        string        `json:"name"`
	Description string        `json:"description"`
	Cadence     string        `json:"cadence"`
	Headline    string        `json:"headline"`
	Pitch       string        `json:"pitch"`
	Language    string        `json:"language"`
	Confirmed   int           `json:"confirmed"`
	Paused      string        `json:"paused,omitempty"`
	MinGap      time.Duration `json:"min_gap"`
}

RemoteList is the list as the server has it.

type Report

type Report struct {
	Dir      string            `json:"dir"`
	List     string            `json:"list"`
	Autonomy string            `json:"autonomy"`
	Phase    string            `json:"phase"`
	Next     string            `json:"next"`
	Skill    string            `json:"skill,omitempty"`
	Slot     time.Time         `json:"slot,omitzero"` // when the next issue is due
	Server   string            `json:"server"`        // connected, or why not
	Research research.Summary  `json:"research"`
	Ideas    int               `json:"open_ideas"`
	Drafts   []Draft           `json:"drafts"`
	Findings []finding.Finding `json:"findings"`

	Concept *concept.Concept `json:"-"`
	Backlog []Idea           `json:"-"`
	Config  Config           `json:"-"`
}

Report is the result of lfr check.

func Check

func Check(dir string, opt Options) (*Report, error)

Check reads the project in dir, holds every file to its format and the issues to the brief, compares them with the server when opt.Remote is set, and names the one next step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL