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
- func GoWorkUseEntries(consumer string) ([]string, error)
- func LocalGateStatement(library, hash string, dirty bool) string
- func RefusalMessage(worktree string, links []LiveLink) string
- func RefusalMessageForSources(worktree string, sources []LiveLinkSource) string
- type ConsumerResult
- type Engine
- type ExecGit
- func (git ExecGit) ContentHash(ctx context.Context, dir string) (string, bool, error)
- func (git ExecGit) ExcludePath(ctx context.Context, dir, pattern string) error
- func (git ExecGit) ExcludedPatterns(ctx context.Context, dir string) ([]string, error)
- func (git ExecGit) TrackedChanges(ctx context.Context, dir string) ([]string, error)
- type ExecNode
- func (node ExecNode) Build(ctx context.Context, libraryDir, packageDir string) (string, error)
- func (node ExecNode) FrozenInstall(ctx context.Context, dir string) error
- func (node ExecNode) Link(ctx context.Context, consumerDir, packageName, dist string) (result NodeLinkResult, returnedErr error)
- func (node ExecNode) LinkSiblings(ctx context.Context, consumerDir string, packageNames []string) error
- func (node ExecNode) Unlink(ctx context.Context, consumerDir, packageName string) (string, error)
- type Git
- type LiveLink
- type LiveLinkSource
- type LiveLinkSourceStore
- type LiveLinkStore
- type Node
- type NodeLinkResult
- type Options
- type QualityVerifier
- type Refusal
- type Result
- type SkippedCheck
- type Verification
- type VerificationRun
- type Verifier
Constants ¶
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 ¶
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 ¶
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 ¶
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.
type ExecGit ¶
ExecGit implements Git with the installed Git.
func (ExecGit) ContentHash ¶
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 ¶
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 ¶
ExcludedPatterns reads the worktree's own exclude file.
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 ¶
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 ¶
FrozenInstall implements Node.
func (ExecNode) Link ¶
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.
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 ¶
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 ¶
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
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 ¶
Refusal is a guard that fired, carrying the stable code a caller branches on and the exact commands that satisfy it.
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.
type SkippedCheck ¶
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.