locallink

package
v0.158.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: 18 Imported by: 0

Documentation

Overview

Package locallink builds a consumer against a library's *working tree* instead of a published version, so a change is proven across every affected repository before anything is published.

The link is untracked by construction. A Go consumer gains a Git-excluded `go.work`; an npm consumer gains a `node_modules` entry pointing at a built dist cached by the library's content hash. No tracked file changes, no `replace` directive, no pnpm override, alias, or `workspace:` entry — an override is exactly the artefact that survives the stream, reaches CI, and makes a consumer build against something the registry never published.

Every link is recorded in stream state at the moment it is created, with enough detail to reverse it exactly, so `--undo` depends on the record rather than on the library worktree still existing.

Implements: dependency-streams#req:local-link-discovers-what-the-library-publishes, dependency-streams#req:go-consumers-link-through-an-untracked-go-work, dependency-streams#req:npm-consumers-link-through-a-built-dist, dependency-streams#req:links-are-recorded-and-undoable, dependency-streams#req:no-module-graph-mutation-under-a-live-link, dependency-streams#req:the-local-gate-states-what-it-verified-against, dependency-streams#req:verify-runs-single-worker-against-the-linked-copy, dependency-streams#req:verification-prints-its-active-links, dependency-streams#req:npm-link-preserves-a-frozen-lockfile-baseline.

Index

Constants

View Source
const (
	// RefusalNotRecordable fires when no open stream holds the consumer, so
	// the link could not be recorded. An unrecorded link is un-undoable and
	// invisible to the merge guard's state signal.
	RefusalNotRecordable = "link-not-recordable"
	// RefusalLiveLinkSource fires when a worktree slated for removal is still
	// an open stream's local link source: some consumer resolves it instead
	// of a published version, and deleting it would strand that consumer.
	RefusalLiveLinkSource = "live-link-source"
)

Refusal codes are contract: skills and the JSON envelope branch on them.

Variables

This section is empty.

Functions

func GoWorkUseEntries

func GoWorkUseEntries(consumer string) ([]string, error)

GoWorkUseEntries reads the `use` entries of a worktree's go.work.

It delegates to the streams package, which is where `wb worktree end` reads the same signal. Two copies of this parser would drift, and the file-based half of the merge refusal is exactly the check that must not disagree with itself between two verbs.

func LocalGateStatement

func LocalGateStatement(library, hash string, dirty bool) string

LocalGateStatement is the sentence a verification run under a live link must print. Locally, every `go` invocation from the worktree root down discovers the `go.work` and therefore verifies against an unpublished library; that is the point of the link, and it must never be mistaken for a published-dependency result.

func RefusalMessage

func RefusalMessage(worktree string, links []LiveLink) string

RefusalMessage renders the refusal a landing verb prints. It names every offending link and every command that clears one, because a refusal an agent cannot resolve becomes a hand-written workaround.

func RefusalMessageForSources added in v0.117.2

func RefusalMessageForSources(worktree string, sources []LiveLinkSource) string

RefusalMessageForSources renders the refusal a cleanup verb prints when a worktree slated for removal is still a live link source. It names every offending stream and consumer, and every command that clears one, because a refusal an agent cannot resolve becomes a hand-written workaround.

Types

type ConsumerResult

type ConsumerResult struct {
	Consumer string `json:"consumer"`
	// Repository is the consumer's owner/repository when stream state knows
	// it.
	Repository string `json:"repository,omitempty"`
	// Skipped is set when the consumer declares none of the library's
	// published identities. Such a consumer is reported and skipped rather
	// than linked to something it does not use.
	Skipped bool   `json:"skipped,omitempty"`
	Reason  string `json:"reason,omitempty"`
	// Links are the links created (or removed, under --undo).
	Links []streams.Link `json:"links,omitempty"`
	// SkippedChecks name guarantees that could not be evaluated at all, so a
	// silent pass is never mistaken for a proven one.
	SkippedChecks []string `json:"skipped_checks,omitempty"`
	// Verification is the single-worker run against the linked copy.
	Verification *Verification `json:"verification,omitempty"`
	// Notes are informational outcomes that are not failures — for example
	// `--undo` finding a link already superseded by a published package and
	// clearing its record without touching the filesystem.
	Notes []string `json:"notes,omitempty"`
	// Errors are per-consumer failures. One consumer's failure never stops
	// the pass: the point of a stream is to learn about every consumer at
	// once.
	Errors []string `json:"errors,omitempty"`
}

ConsumerResult is one consumer's outcome.

type Engine

type Engine struct {
	Store *streams.Store
	Git   Git
	// Node builds and links npm packages.
	Node Node
	// Verifier runs the consumer's own lint and tests.
	Verifier Verifier
	// CacheRoot is where built library dists are cached by content hash.
	CacheRoot string
	Now       func() time.Time
}

Engine performs local propagation against injected ports.

func (*Engine) Run

func (engine *Engine) Run(ctx context.Context, options Options) (Result, error)

Run performs one invocation.

type ExecGit

type ExecGit struct {
	Timeout time.Duration
}

ExecGit implements Git with the installed Git.

func (ExecGit) ContentHash

func (git ExecGit) ContentHash(ctx context.Context, dir string) (string, bool, error)

ContentHash computes a tree identity over the working tree, including modified and untracked files, using a temporary index so the caller's own index is never touched.

`git write-tree` against that index is the exact bytes Git would record for a commit of this tree, which is what makes the hash a real identity rather than a checksum of a file listing. Ignored paths — `node_modules`, `dist` — stay out, so a rebuild does not change the source identity.

func (ExecGit) ExcludePath

func (git ExecGit) ExcludePath(ctx context.Context, dir, pattern string) error

ExcludePath appends a pattern to the worktree's own exclude file.

The path comes from `git rev-parse --git-path info/exclude`, which is the file Git will actually read for *this* worktree. WB never adds the pattern to a tracked `.gitignore`: that would be a tracked change, which a local link must never make.

func (ExecGit) ExcludedPatterns

func (git ExecGit) ExcludedPatterns(ctx context.Context, dir string) ([]string, error)

ExcludedPatterns reads the worktree's own exclude file.

func (ExecGit) TrackedChanges

func (git ExecGit) TrackedChanges(ctx context.Context, dir string) ([]string, error)

TrackedChanges lists tracked files that differ from HEAD. Untracked paths are deliberately excluded: a link creates untracked artefacts by design.

type ExecNode

type ExecNode struct {
	// CacheRoot holds built dists keyed by the library's content hash.
	CacheRoot string
	// ContentHash identifies the library tree the current build belongs to.
	ContentHash string
	Timeout     time.Duration
}

ExecNode implements Node with the repository's own package manager.

It never runs `pnpm link`. That command writes a `link:` entry into the consumer's `package.json`, which is a tracked file, and `npm-consumers-link-through-a-built-dist` requires that no tracked file changes. What it does instead is exactly what the package manager's link mechanism does to `node_modules` — replace the package directory with a symlink to a built package staged in the consumer's installed peer context — without the manifest edit or provider-side peer resolution.

func (ExecNode) Build

func (node ExecNode) Build(ctx context.Context, libraryDir, packageDir string) (string, error)

Build implements Node, caching the built dist by the library's content hash so an iterative stream never verifies against a stale build.

func (ExecNode) FrozenInstall

func (node ExecNode) FrozenInstall(ctx context.Context, dir string) error

FrozenInstall implements Node.

func (node ExecNode) Link(ctx context.Context, consumerDir, packageName, dist string) (result NodeLinkResult, returnedErr error)

Link implements Node.

Both shapes a package manager leaves in node_modules are preserved. pnpm's default isolated store makes node_modules/<pkg> a SYMLINK into .pnpm/…, and an earlier version simply deleted that symlink with no backup — so `--undo` left the consumer with no package at all until someone re-installed. npm's flat layout leaves a real directory, which is moved aside.

func (ExecNode) LinkSiblings added in v0.111.2

func (node ExecNode) LinkSiblings(ctx context.Context, consumerDir string, packageNames []string) error

LinkSiblings makes staged packages resolve their declared runtime siblings to the corresponding staged identities. The edges live inside the untracked stage directories, so removing any package stage removes its edges too and leaves the installed pnpm topology available for exact undo.

func (node ExecNode) Unlink(ctx context.Context, consumerDir, packageName string) (string, error)

Unlink implements Node, restoring whichever shape the link displaced.

type Git

type Git interface {
	// ContentHash identifies a working tree, including modified and untracked
	// files. The library is uncommitted by construction, so it has no SHA;
	// dirty reports whether the tree differs from HEAD.
	ContentHash(ctx context.Context, dir string) (hash string, dirty bool, err error)
	// TrackedChanges lists paths of *tracked* files that differ from HEAD.
	// Untracked paths are excluded: a link creates untracked artefacts by
	// design, and only a tracked change is a violation.
	TrackedChanges(ctx context.Context, dir string) ([]string, error)
	// ExcludePath adds one pattern to the worktree's own Git exclude file,
	// resolved through `git rev-parse --git-path info/exclude` so the path is
	// the one Git will actually read for this worktree.
	ExcludePath(ctx context.Context, dir, pattern string) error
	// ExcludedPatterns reads that exclude file.
	ExcludedPatterns(ctx context.Context, dir string) ([]string, error)
}

Git is the local Git surface local propagation needs. Every method is read-only except ExcludePaths, which writes only to the worktree's own exclude file — never to a tracked `.gitignore`.

type LiveLink struct {
	// Source is "stream-state" or "go.work": the two signals are independent
	// on purpose.
	Source string `json:"source"`
	// Detail names the offending link or `use` entry.
	Detail string `json:"detail"`
	// Sanctioned is the exact command that clears it.
	Sanctioned string `json:"sanctioned_command"`
}

LiveLink is one reason a worktree must not be pushed or landed.

func HasLiveLink(store LiveLinkStore, worktree string) ([]LiveLink, error)

HasLiveLink reports every live link recorded against a worktree, from both independent signals.

`wb worktree merge` and `wb pr land` MUST refuse a worktree this reports on, naming the offending link and the command that clears it, and there is no flag that both bypasses the guard and pushes.

The two signals are checked independently and both are reported: stream state alone would miss a hand-written `go.work`, and `go.work` alone would miss an npm link. A worktree with a hand-written workspace and no stream record is still linked, and refusing it is the whole point.

A store that cannot be read is an error, never an empty result: "I could not tell" must not be spelled the same way as "there is no link".

Implements: dependency-streams#req:merge-refuses-a-linked-worktree.

type LiveLinkSource added in v0.117.2

type LiveLinkSource struct {
	// Stream names the open stream recording the link.
	Stream string `json:"stream"`
	// Consumer names the repository still resolving this worktree locally.
	Consumer string `json:"consumer"`
	// Detail names the offending link.
	Detail string `json:"detail"`
	// Sanctioned is the exact command that clears it.
	Sanctioned string `json:"sanctioned_command"`
}

LiveLinkSource is one reason a worktree must not be removed: an open stream's consumer still resolves it locally in place of a published version.

func HasLiveLinkSource added in v0.117.2

func HasLiveLinkSource(store LiveLinkSourceStore, worktree string) ([]LiveLinkSource, error)

HasLiveLinkSource reports every open stream whose consumer still links to this worktree as its unpublished source.

`wb worktree cleanup` — and any verb that removes a managed worktree as part of landing, such as the cleanup step of `wb pr land` and `wb worktree merge` — MUST refuse a worktree this reports on, naming the offending stream, consumer, and link kind, and pointing at the command that repoints or drops the link. There is no flag that both bypasses this guard and removes the worktree.

Unlike HasLiveLink, this checks only the recorded stream-state signal. There is no hand-written-`go.work` analogue on the source side: a worktree cannot itself carry evidence that some OTHER checkout's untracked `go.work` names it, so the recorded link is the only signal that exists.

A store that cannot be read is an error, never an empty result: "I could not tell" must not be spelled the same way as "nothing links here".

Implements: dependency-streams#req:merge-refuses-a-linked-worktree (the removal-time half, protecting the link source rather than the consumer).

type LiveLinkSourceStore added in v0.117.2

type LiveLinkSourceStore interface {
	LinkSourcesForWorktree(worktree string) ([]streams.StreamLinkSource, error)
}

LiveLinkSourceStore is the read side of stream state the cleanup guard needs. It is an interface for the same reason LiveLinkStore is: a caller that already holds a store passes it straight through, and a test provides its own without a WB home.

type LiveLinkStore

type LiveLinkStore interface {
	LiveLinksForWorktree(worktree string) ([]streams.StreamLink, error)
}

LiveLinkStore is the read side of stream state the guard needs. It is an interface so a caller that already holds a store passes it straight through, and a test provides its own without a WB home.

type Node

type Node interface {
	// FrozenInstall proves a clean frozen install of the unlinked consumer
	// tree, so a link never masks a lockfile or manifest mismatch.
	FrozenInstall(ctx context.Context, dir string) error
	// Build runs the library repository's own build target and returns the
	// directory holding the built package.
	Build(ctx context.Context, libraryDir, packageDir string) (dist string, err error)
	// Link stages dist beside the consumer's installed package so framework
	// peer dependencies resolve from the consumer, points node_modules at that
	// stage, and reports every generated path. It preserves what was there so
	// --undo restores it exactly and must not modify any manifest.
	Link(ctx context.Context, consumerDir, packageName, dist string) (result NodeLinkResult, err error)
	// Unlink restores the node_modules entry recorded by Link.
	//
	// note is empty for a normal restore. It carries an informational
	// message when the consumer's own package manager already replaced the
	// WB-staged link with a published copy (a governed `pnpm install`, most
	// often) before undo ran: the record is cleared and the filesystem is
	// left exactly as the package manager left it.
	Unlink(ctx context.Context, consumerDir, packageName string) (note string, err error)
	// LinkSiblings wires runtime dependency edges between packages that WB has
	// staged from the same provider. External peers continue to resolve from
	// the consumer's installed dependency context.
	LinkSiblings(ctx context.Context, consumerDir string, packageNames []string) error
}

Node builds and links npm packages through the consumer's and library's own package managers.

type NodeLinkResult added in v0.110.0

type NodeLinkResult struct {
	Previous  string
	Artifacts []string
}

NodeLinkResult reports every generated path relative to the npm workspace.

type Options

type Options struct {
	// Library is the library worktree whose working tree the consumers build
	// against. It may be empty for --undo, where the record is the source of
	// truth.
	Library string
	// Consumers are the consumer worktrees to link or unlink.
	Consumers []string
	// Undo restores published versions and removes the links.
	Undo bool
	// Verify runs each consumer's lint and tests against the linked copy,
	// single-worker, plus the GOWORK=off build and vet the pre-landing gate
	// requires.
	Verify bool
	// Timeout bounds every child process. Zero uses a sensible default.
	Timeout time.Duration
	// Stream names the stream whose state records the links. Empty resolves
	// the stream that holds the first consumer worktree.
	Stream string
}

Options is one `wb deps propagate local` invocation.

type QualityVerifier

type QualityVerifier struct {
	Options quality.RunOptions
}

QualityVerifier runs a consumer's own `wb verify` profiles, constrained to a single worker. It is the production Verifier: reusing the existing profiles is what keeps a local gate executing the same mechanisms CI does, rather than a second, divergent runner.

func (QualityVerifier) BuildAndVet

func (verifier QualityVerifier) BuildAndVet(ctx context.Context, dir string) (VerificationRun, error)

BuildAndVet implements Verifier. GOWORK=off is set by the verb itself rather than left to the caller, because a workspace the toolchain discovers is exactly what this check exists to exclude.

func (QualityVerifier) Verify

func (verifier QualityVerifier) Verify(ctx context.Context, dir string, env []string) (VerificationRun, error)

Verify implements Verifier.

type Refusal

type Refusal struct {
	Code       string
	Message    string
	Sanctioned []string
}

Refusal is a guard that fired, carrying the stable code a caller branches on and the exact commands that satisfy it.

func Refused

func Refused(err error) (*Refusal, bool)

Refused reports whether err is a guard refusal rather than a failure.

func (*Refusal) Error

func (refusal *Refusal) Error() string

type Result

type Result struct {
	Library string `json:"library,omitempty"`
	// LibraryRepository is the library's owner/repository, which survives the
	// worktree being removed.
	LibraryRepository string `json:"library_repository,omitempty"`
	// ContentHash identifies the library working tree the links exposed,
	// including modified and untracked files. The library is uncommitted by
	// construction, so it has no SHA.
	ContentHash string `json:"content_hash,omitempty"`
	// Dirty reports whether the library working tree differs from its HEAD.
	Dirty bool `json:"dirty,omitempty"`
	// Identities are the published identities discovered from the library
	// worktree itself.
	Identities []streams.Identity `json:"identities,omitempty"`
	Stream     string             `json:"stream,omitempty"`
	Consumers  []ConsumerResult   `json:"consumers"`
	// Plan states the checks this invocation will run, before it runs them.
	Plan []string `json:"plan,omitempty"`
}

Result is the whole invocation's report.

func (Result) Failed

func (result Result) Failed() bool

Failed reports whether any consumer failed to link or verify.

type SkippedCheck

type SkippedCheck struct {
	Check  string
	Reason string
}

SkippedCheck reports a guarantee that could not be evaluated, as distinct from one that passed.

A check that silently returns nil is indistinguishable from a check that succeeded, which is how the frozen-install baseline came to look proven on consumers that have no lockfile at all.

func Skipped

func Skipped(err error) (*SkippedCheck, bool)

Skipped reports whether err is a check that could not run.

func (*SkippedCheck) Error

func (skipped *SkippedCheck) Error() string

type Verification

type Verification struct {
	// Statement is the sentence every run under a live link must print, so a
	// local result is never mistaken for a published-dependency result.
	Statement string `json:"statement"`
	// ActiveLinks names the links in effect, the published version each
	// replaced, and the content hash verified against, so a result can be tied
	// to an exact library tree after the fact.
	ActiveLinks []string        `json:"active_links"`
	Linked      VerificationRun `json:"linked"`
	// PublishedBaseline is the GOWORK=off build and vet: the pre-landing check
	// proving the consumer still resolves its published dependency.
	PublishedBaseline VerificationRun `json:"published_baseline"`
	Passed            bool            `json:"passed"`
}

Verification is one consumer's verification against the linked copy.

type VerificationRun

type VerificationRun struct {
	Passed  bool     `json:"passed"`
	Command string   `json:"command,omitempty"`
	Details []string `json:"details,omitempty"`
}

VerificationRun is one verification pass over a consumer.

type Verifier

type Verifier interface {
	// Verify runs the consumer's lint and tests, constrained to a single
	// worker, with env applied on top of the process environment.
	Verify(ctx context.Context, dir string, env []string) (VerificationRun, error)
	// BuildAndVet runs a build and vet with GOWORK=off, which is the
	// pre-landing check proving the consumer still resolves its *published*
	// dependency.
	BuildAndVet(ctx context.Context, dir string) (VerificationRun, error)
}

Verifier runs a consumer's own lint and tests.

Jump to

Keyboard shortcuts

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