workspace

package
v0.0.0-...-95517ba Latest Latest
Warning

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

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

Documentation

Overview

Package workspace is the file-access layer behind the read, ls, glob, and grep tools: one way to see the project, whether or not it is a git repository.

It deliberately does not ask git which files exist. Shelling out to `git ls-files` would give a different answer with and without a repository — and no answer at all in a plain directory, which is a case Strument supports (editing live configuration, or a project under another SCM). So the tree is walked directly and .gitignore rules are applied in process, through the vendored matcher in internal/gitignore. A new file the model just wrote is visible immediately, which `git ls-files` would not show until it was staged.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Entry

type Entry struct {
	// Path is root-relative and forward-slashed.
	Path  string
	IsDir bool
	Size  int64
	// Link is a symlink's target as written, empty for everything else. A
	// listing that doesn't say so shows one file under two names and gives the
	// reader no way to tell it from a duplicate.
	Link string
}

Entry is one directory entry, as List reports it.

type FileText

type FileText struct {
	Path string
	// Text is the requested window, one line per line, with no numbering
	// applied — the tool layer decides how to present it.
	Lines []string
	// Start is the 1-based line number of Lines[0].
	Start int
	// Total is the file's full line count.
	Total int
	// Truncated reports that the window stops short of the end of the file.
	Truncated bool
	// Link is the target when Path is a symlink, so a reader given both the
	// link and the target can tell it is holding one file rather than two.
	Link string
}

FileText is one file read, with enough context for the caller to tell the model what it is looking at and how to see the rest.

type GrepFileResult

type GrepFileResult struct {
	Path  string
	Count int
	// Lines is populated in GrepContent mode only.
	Lines []GrepLine
}

GrepFileResult is one file's matches.

type GrepLine

type GrepLine struct {
	Number int // 1-based
	Text   string
}

GrepLine is one matching line.

type GrepMode

type GrepMode int

GrepMode selects how much a search returns.

const (
	// GrepFiles returns only the paths that contain a match. It is the
	// default because it is the cheapest useful answer: a model orienting
	// itself wants to know where to look before it wants the lines.
	GrepFiles GrepMode = iota
	// GrepContent returns the matching lines with their line numbers.
	GrepContent
	// GrepCount returns a per-file match count.
	GrepCount
)

type GrepQuery

type GrepQuery struct {
	// Pattern is a Go regular expression.
	Pattern string
	// Glob, when set, restricts the search to matching paths (** supported).
	Glob string
	// Dir, when set, restricts the search to a subtree.
	Dir string
	// IgnoreCase folds case.
	IgnoreCase bool
	Mode       GrepMode
}

GrepQuery is one content search.

type GrepResult

type GrepResult struct {
	Files     []GrepFileResult
	Total     int // matching lines across all files
	Truncated Truncated
	// InScope counts the files Glob and Dir admitted, and Scanned the subset
	// actually searched — the rest were binary or over the size cap.
	//
	// They exist so a caller can tell three different nothings apart: a scope
	// that admitted no files (the pattern was never tested), files that could
	// not be read, and a pattern that genuinely is not there. Reporting all
	// three as "no matches" tells a reader the identifier does not exist in the
	// project, which is a different claim and often a false one.
	InScope int
	Scanned int
	// Shortened counts returned lines that were clipped to MaxMatchBytes, so
	// the caller can say so rather than let a "…" pass for the file's content.
	Shortened int
}

GrepResult is a whole search.

type Limits

type Limits struct {
	// MaxEntries caps how many filesystem entries a walk visits.
	MaxEntries int
	// MaxResults caps how many paths a call returns: ls, glob, and grep in its
	// files and count modes.
	MaxResults int
	// MaxMatches caps how many matching lines a content search returns.
	MaxMatches int
	// MaxMatchBytes caps the length of one returned matching line.
	MaxMatchBytes int
	// MaxFileBytes caps the size of a file that will be read or searched.
	MaxFileBytes int64
}

Limits bound what a single call may traverse or return, so one tool call cannot stall the turn or flood the context. Zero means the default. A path and a matching line are different currencies, which is why they have separate caps. Measured on this repo: a path averages 37 bytes, so even a thousand of them is about 9k tokens and the ceiling is predictable. A *matching line* is whatever happened to be on that line — the median for one unscoped search here was 1383 bytes and the longest was 157 KB, a single line in a recorded fixture. Capping the two with one number meant the cheap case was throttled and the expensive one was not bounded at all.

type Truncated

type Truncated struct {
	Entries bool // the walk hit MaxEntries
	Results bool // the result hit MaxResults
}

Truncated reports that a result was cut short by a limit. Callers surface this to the model, because a silently short answer reads as "nothing more exists" and sends it down the wrong path.

func (Truncated) Any

func (t Truncated) Any() bool

type Workspace

type Workspace struct {
	Root   string
	Limits Limits
	// Pinned reports whether an absolute path is one the user explicitly
	// pinned with /add or /read-only. Those are sanctioned wherever they live,
	// the same exemption unsafePath makes for edits: containment guards against
	// a model inventing a path out of the project, not against what the user
	// deliberately reached for.
	//
	// A predicate rather than a list, because a list would go stale the first
	// time /drop ran. nil means nothing is pinned, which is what `strument
	// tool` passes: the command line is contained with no exception at all.
	Pinned func(abs string) bool
}

Workspace reads one project tree rooted at Root.

func New

func New(root string) *Workspace

New builds a Workspace over root with the default limits.

func (*Workspace) Files

func (w *Workspace) Files() ([]string, Truncated, error)

Files lists every non-ignored file in the tree, root-relative, sorted.

func (*Workspace) Glob

func (w *Workspace) Glob(pattern string) ([]string, Truncated, error)

Glob returns the non-ignored files matching pattern, root-relative and sorted. The pattern is slash-separated and supports ** for "zero or more path segments", which is what models reach for and what path.Match alone does not provide.

func (*Workspace) Grep

func (w *Workspace) Grep(q GrepQuery) (GrepResult, error)

Grep searches file contents across the workspace.

This is a structured search rather than a shell-out to grep or rg. Doing it in process is what lets the result be capped, keeps the behavior identical on Windows, and means the search obeys the same ignore rules as everything else here. A model left to run `rg -n foo | head -50` through the shell would take the cap out of the harness's hands, and would need the shell gate for what is a pure observation.

func (*Workspace) List

func (w *Workspace) List(dir string) ([]Entry, error)

List reports the immediate contents of one directory, root-relative and sorted, directories included. dir is root-relative; "" or "." is the root.

It is not redundant with Glob: models use ls to orient themselves in an unfamiliar tree, and answer "what is in here" badly when they have to guess a pattern first.

func (*Workspace) Read

func (w *Workspace) Read(rel string, offset, limit int) (FileText, error)

Read returns a window of a text file. offset is 1-based; 0 means the start. limit is a line count; 0 means defaultReadLines.

Reading is deliberately line-windowed rather than whole-file. A read tool that can only return everything makes a large file unusable, and a model that receives a silently truncated file will edit against text that is not there. Truncated says so, and the tool layer turns it into a paging hint.

Jump to

Keyboard shortcuts

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