Help us shape the future of Go! Take the 2026 Go Developer Survey

plan

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package plan turns a clustering into a reviewable, replayable description of the star lists to create and the repositories to file under them.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StalePlacements

func StalePlacements(existing []gh.List, p *Plan, ours map[string]bool, within map[string]bool) map[string][]string

StalePlacements reports repositories that are in a managed list the plan no longer puts them in. It is separate from Apply's additions because removals are the destructive half and are only performed when reconciling. within, when non-nil, restricts which lists may be stripped. Membership is still judged against the whole plan — a repository leaves a list only when the plan does not put it there — but the lists considered are limited to the ones the run was asked to touch.

Types

type ApplyOptions

type ApplyOptions struct {
	// Delay paces the writes. GitHub's documented ceiling for mutating
	// requests is 500 an hour, and its own advice is at least a second
	// between them; going faster earns escalating secondary rate limits
	// rather than a refusal.
	Delay time.Duration
	// Limit stops after this many repository writes; 0 means no limit. It
	// counts removals as well as additions, and reconciling is refused
	// alongside it — otherwise a "cautious first run" could delete every list
	// the plan has dropped before the limit was ever consulted.
	Limit int
	// Only restricts the apply to the named categories.
	Only []string
	// ContinueOnError keeps going past a failed repository instead of aborting.
	ContinueOnError bool
	// DryRun reports what would change without changing anything.
	DryRun bool
	// Reconcile makes the account match the plan instead of only adding to it:
	// repositories leave categories they no longer belong to, and categories
	// the plan has dropped are deleted.
	//
	// Only lists this tool wrote are touched, recognized by the marker their
	// descriptions carry. A list made by hand is left exactly alone.
	Reconcile bool
	// Private creates and keeps the lists private. Visibility is enforced on
	// every run, not only at creation.
	Private bool
	// Adopt lets apply take over an existing list whose name matches a planned
	// category but which this tool did not create.
	//
	// Taking one over replaces its description with the plan's, and that
	// description carries the marker identifying a list as this tool's — so an
	// adopted list becomes subject to --reconcile and to reset. It is
	// announced when it happens, because neither consequence is recoverable
	// from the flag's name.
	Adopt bool
	// Batch sends this many membership changes in one GraphQL request. One is
	// the conservative default: GraphQL executes batched mutations serially so
	// the effect is identical, but whether GitHub meters a batch as one
	// mutating request or as many is not documented, and that is what decides
	// how long a large apply takes.
	Batch int
	// Force makes room when the plan does not fit inside GitHub's cap by
	// deleting lists this tool created that the plan no longer contains. Lists
	// made by hand are never deleted, and neither is a list the plan still
	// has — so this only removes what a --reconcile run would have removed
	// anyway, and only as many as are needed to fit.
	Force bool
	Out   io.Writer
}

ApplyOptions control how a plan is written back to GitHub.

type ApplyResult

type ApplyResult struct {
	ListsDeleted []string
	ReposRemoved int
	ListsCreated []string
	ReposFiled   int
	ReposSkipped int
	Errors       []error
	StoppedAtCap bool
	// Conflicts are existing lists left untouched because their name matches a
	// planned category but they were not created here.
	Conflicts []string
	// Adopted are lists that were taken over with --adopt: their descriptions
	// were replaced and they are now managed here.
	Adopted []string
	// Placed maps a category to the repositories this run put in it or found
	// already there: the placements the run asserts are on the account, and
	// so the ones a verification can hold it to. A run that stopped at
	// --limit, or skipped a category over a name conflict, asserts nothing
	// about the rest. A dry run records only what was already in place.
	Placed map[string][]string
}

ApplyResult reports what actually happened.

func Apply

func Apply(ctx context.Context, c *gh.ListsClient, p *Plan, opt ApplyOptions) (*ApplyResult, error)

Apply creates the planned lists and files each repository into its category.

The whole account is read once, so every decision below is made against known state rather than by asking GitHub about each repository in turn. A repository is written at most once: its complete set of memberships is computed first — additions from the plan, removals from reconciling — and submitted in a single mutation, because updateUserListsForItem replaces the whole set. Membership the plan knows nothing about is carried across untouched, which is what stops an apply emptying a hand-curated list.

type Category

type Category struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Terms       []string `json:"terms"`
	// Exists is true when a list of this name is already on the account, in
	// which case applying only adds repositories to it.
	Exists   bool    `json:"exists"`
	Cohesion float64 `json:"cohesion"`
	Repos    []Repo  `json:"repos"`
}

Category is one proposed or existing star list.

type Plan

type Plan struct {
	Version     int       `json:"version"`
	GeneratedAt time.Time `json:"generated_at"`
	User        string    `json:"user"`
	Backend     string    `json:"backend"`
	// Settings records the tuning the plan was produced with, so a plan is
	// self-describing: the categories depend on a dozen weights and
	// thresholds, and without them a plan cannot be reproduced or explained
	// months later except by remembering the command line.
	//
	// Keys are the flag names of the run that built the categories. An
	// incremental run does not rebuild them, so it carries these forward
	// unchanged from the plan it extended — whatever clustering flags were on
	// its own command line — and records itself separately:
	// "incremental" is "true", "incremental-from" is the GeneratedAt of the
	// plan extended, in RFC 3339, and the "place-" keys
	// (place-min-similarity, place-outlier-sigmas, place-multi-list,
	// place-multi-list-ratio) are the placement knobs the newcomers were
	// filed with. Only the latest pass is recorded: each one overwrites them.
	Settings   map[string]string `json:"settings,omitempty"`
	TotalStars int               `json:"total_stars"`
	Categories []Category        `json:"categories"`
	Unassigned []Repo            `json:"unassigned"`
}

Plan is the full proposal. It is written to disk so that the categorization a human reviewed is exactly the one that later gets applied.

func Build

func Build(user string, repos []gh.Repo, sp *embed.Space, res *cluster.Result, labels []cluster.Label, existing []gh.List) *Plan

Build assembles the plan from the clustering result. Repositories the clusterer refused to place are reported rather than hidden: an honest "these 40 stars fit no category" is more useful than 40 bad placements.

func Load

func Load(path string) (*Plan, error)

Load reads a plan written by Save.

func (*Plan) Categorized

func (p *Plan) Categorized() int

Categorized counts the distinct repositories the plan places in at least one list.

func (*Plan) Markdown

func (p *Plan) Markdown(w io.Writer)

Markdown renders the plan as a document, useful as a starred-repos index even if the lists are never applied.

func (*Plan) NewLists

func (p *Plan) NewLists() int

NewLists counts the categories that do not already exist on the account, which is what applying the plan would have to create.

func (*Plan) Placements

func (p *Plan) Placements() int

Placements counts repository-to-list pairs, which is what applying the plan actually writes. It exceeds Categorized when repositories belong to more than one list.

func (*Plan) Print

func (p *Plan) Print(w io.Writer, verbose bool)

Print renders the plan for a terminal. Print writes the whole plan.

func (*Plan) PrintN

func (p *Plan) PrintN(w io.Writer, verbose bool, limit int)

PrintN writes the plan, showing at most limit categories; 0 shows all. The plan file is unaffected — this only bounds what reaches the terminal, since a thirty-category plan scrolls its own summary off the screen.

func (*Plan) Save

func (p *Plan) Save(path string) error

Save writes the plan as JSON, creating parent directories as needed.

func (*Plan) Stamp

func (p *Plan) Stamp(t time.Time)

Stamp records when the plan was produced. It is separate from Build so that clustering stays a pure function of its inputs.

func (*Plan) Validate added in v0.2.0

func (p *Plan) Validate() error

Validate checks a plan before anything is written from it.

The plan file exists to be reviewed and edited by hand, and an edit can break what Build guarantees: a name GitHub will refuse, a description that lost the marker this tool recognises its own lists by, two categories that differ only in case and so would silently become one list. Every problem is reported in a single error, one per line, so a plan is fixed in one pass rather than one refusal at a time.

A category with no repositories is not an error: Build never writes one, and Apply and Verify skip one deliberately, so emptying a category is a legitimate way of dropping it without deleting the entry.

type Repo

type Repo struct {
	FullName    string    `json:"full_name"`
	Description string    `json:"description,omitempty"`
	Similarity  float64   `json:"similarity"`
	Stars       int       `json:"stars,omitempty"`
	PushedAt    time.Time `json:"pushed_at,omitzero"`
	StarredAt   time.Time `json:"starred_at,omitzero"`
}

Repo is a repository placed in a category.

Stars and the two dates are carried through because the plan is also read by people and by scripts: they are what you sort an index by, and what tells a dormant project from a live one. They cost nothing to record — every one of them was already fetched with the star list.

type ResetOptions

type ResetOptions struct {
	DryRun          bool
	ContinueOnError bool
	Delay           time.Duration
	Out             io.Writer
}

ResetOptions control how Reset tears down the lists this tool created.

type ResetResult

type ResetResult struct {
	Deleted []string
	// Kept are lists left alone because they were not created here.
	Kept   []string
	Errors []error
}

ResetResult reports what Reset did, or would do.

func Reset

func Reset(ctx context.Context, c *gh.ListsClient, user string, opt ResetOptions) (*ResetResult, error)

Reset deletes every star list this tool created, identified by the marker its descriptions carry, and leaves every other list untouched. It is the inverse of apply: a way back to a clean account after experimenting, without deleting anything the user made by hand.

type VerifyResult

type VerifyResult struct {
	// Missing maps a category to the repositories the plan places there that
	// the account does not have.
	Missing map[string][]string
	// MissingLists are planned categories with no list at all.
	MissingLists []string
	Checked      int
}

VerifyResult reports the difference between what the plan asserts and what the account actually holds.

func Verify

func Verify(ctx context.Context, c *gh.ListsClient, p *Plan, only []string, out io.Writer) (*VerifyResult, error)

Verify re-reads the account and checks that every placement the plan asserts is really there.

This exists because a mutation reporting success is not proof the placement is visible. updateUserListsForItem replaces a repository's whole set of lists, so a partial write, or one racing another client, would not surface as an error. Reading the result back is the only real confirmation.

It costs one page read per list, not one per repository, so it is cheap enough to run after every apply.

func VerifyApplied added in v0.2.0

func VerifyApplied(ctx context.Context, c *gh.ListsClient, p *Plan, only []string, res *ApplyResult, out io.Writer) (*VerifyResult, error)

VerifyApplied checks what an apply run left the account asserting, which is not always the whole plan.

A run that completed asserts every placement in the categories it was asked for, less any it skipped because a list of that name was not created here. A run that stopped at --limit asserts only the placements it made or found in place: holding it to the rest would fail by construction, since it never wrote them — which is what made `apply --limit N --verify` always exit 1. A nil res verifies the plan as it stands.

func (*VerifyResult) OK

func (v *VerifyResult) OK() bool

OK reports whether the account matches the plan.

Jump to

Keyboard shortcuts

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