216 lines
8.3 KiB
JavaScript
216 lines
8.3 KiB
JavaScript
/**
|
|
* Focus resolution: which slice of the workspace an agent is confined to.
|
|
*
|
|
* ## Why this exists
|
|
*
|
|
* A workspace-wide index has to rank across every part of a repository, and in
|
|
* a codebase built from many near-identical modules that is exactly where
|
|
* embedding similarity fails. In the repository this was written against there
|
|
* are eighteen controllers — each a state machine with the same idioms, the same
|
|
* naming and the same shape — spread across 228k lines. A query about one of
|
|
* them returns plausible fragments of all eighteen.
|
|
*
|
|
* Narrowing the index to the one being worked on removes the confusability
|
|
* rather than trying to rank through it, and it does three things at once:
|
|
*
|
|
* 1. **precision** — there are no near-duplicates to outrank the right answer;
|
|
* 2. **cold start** — ten files embed in seconds where six hundred took minutes;
|
|
* 3. **context** — code the agent never sees is code that is never re-sent on
|
|
* every later step, which is where the money actually is.
|
|
*
|
|
* ## Resolution order
|
|
*
|
|
* Most explicit wins, and the last fallback is the workspace root — which is the
|
|
* behaviour that existed before focus did, so adopting this cannot regress a
|
|
* deployment that never configures it.
|
|
*
|
|
* The marker step is the interesting one: a folder that contains its own
|
|
* instructions *is* a scope. A controller directory holding a `CONTROLLER.md`
|
|
* becomes the world the agent sees, and the document that describes the module
|
|
* also defines its boundary. Nothing needed to be invented for that — the
|
|
* convention already means "this folder is self-describing".
|
|
*/
|
|
|
|
import { existsSync, statSync } from 'node:fs'
|
|
import { readdir } from 'node:fs/promises'
|
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
|
|
/**
|
|
* Files whose presence marks a directory as a scope root.
|
|
*
|
|
* Ordered by how specific they are: a `CONTROLLER.md` is a stronger claim about
|
|
* a directory than the `AGENTS.md` that sits at every repository root.
|
|
*/
|
|
export const DEFAULT_FOCUS_MARKERS = Object.freeze(['CONTROLLER.md', 'DEBUG_UI.md', 'AGENTS.md'])
|
|
|
|
/** True when `path` is `root` itself or sits underneath it. */
|
|
export function isInside(root, path) {
|
|
const prefix = root.endsWith(sep) ? root : root + sep
|
|
return path === root || path.startsWith(prefix)
|
|
}
|
|
|
|
/** True when `path` exists and is a directory. */
|
|
function statIsDirectory(path) {
|
|
try {
|
|
return statSync(path).isDirectory()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** Every ancestor of `from` up to and including `stop`, nearest first. */
|
|
export function ancestorsTo(from, stop) {
|
|
const out = []
|
|
let current = resolve(from)
|
|
const limit = resolve(stop)
|
|
for (;;) {
|
|
out.push(current)
|
|
if (current === limit) break
|
|
const parent = dirname(current)
|
|
// A path that is not under `stop`, or a filesystem root, ends the walk
|
|
// rather than looping.
|
|
if (parent === current || !isInside(limit, parent)) break
|
|
current = parent
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Resolve the focus directory for one session.
|
|
*
|
|
* @param {object} input
|
|
* @param {string} input.workspace absolute workspace root
|
|
* @param {string} [input.cwd] directory the session is working from
|
|
* @param {string} [input.explicit] a configured or caller-supplied focus
|
|
* @param {(path: string) => boolean} [input.isDirectory] injection point for tests
|
|
* @param {readonly string[]} [input.markers] scope-marker filenames
|
|
* @param {(path: string) => boolean} [input.exists] injection point for tests
|
|
* @param {{path: string, marker: string}[]} [input.scopes] scopes found by {@link discoverScopes}
|
|
* @returns {{root: string, source: 'explicit'|'scope'|'marker'|'workspace', marker?: string, problem?: string}}
|
|
*/
|
|
export function resolveFocus(input) {
|
|
const {
|
|
workspace,
|
|
cwd = workspace,
|
|
explicit = '',
|
|
markers = DEFAULT_FOCUS_MARKERS,
|
|
scopes = [],
|
|
exists = existsSync,
|
|
isDirectory = statIsDirectory,
|
|
} = input
|
|
|
|
// 1. An explicit focus wins, but a bad one is reported rather than silently
|
|
// ignored: a typo that quietly fell back to the whole workspace would look
|
|
// like the feature doing nothing.
|
|
const requested = String(explicit ?? '').trim()
|
|
if (requested !== '') {
|
|
const absolute = isAbsolute(requested) ? resolve(requested) : resolve(workspace, requested)
|
|
if (!isInside(workspace, absolute)) {
|
|
return {
|
|
root: resolve(workspace),
|
|
source: 'workspace',
|
|
problem: `focus "${requested}" is outside the workspace`,
|
|
}
|
|
}
|
|
if (!isDirectory(absolute)) {
|
|
return {
|
|
root: resolve(workspace),
|
|
source: 'workspace',
|
|
problem: `focus "${requested}" is not a directory`,
|
|
}
|
|
}
|
|
return { root: absolute, source: 'explicit' }
|
|
}
|
|
|
|
// 2. Nothing here adopts a discovered scope automatically, and that is
|
|
// deliberate. A session sitting at the repository root is not thereby
|
|
// *about* any one controller, and silently confining it to whichever scope
|
|
// sorted first would hide code the session legitimately needs. Discovery
|
|
// exists so a caller can be shown the scopes and name one; choosing is an
|
|
// explicit act, which is what `focus` is for.
|
|
|
|
// 3. The nearest ancestor carrying a marker file. `cwd` is normally the
|
|
// workspace root, at which point the root's own AGENTS.md matches and the
|
|
// result is the workspace — the same answer the old code gave.
|
|
const start = isInside(workspace, resolve(cwd)) ? resolve(cwd) : resolve(workspace)
|
|
for (const directory of ancestorsTo(start, workspace)) {
|
|
for (const marker of markers) {
|
|
if (marker !== '' && exists(join(directory, marker))) {
|
|
return { root: directory, source: 'marker', marker }
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. The workspace.
|
|
return { root: resolve(workspace), source: 'workspace' }
|
|
}
|
|
|
|
/** A stable key for one focus directory, for index filenames. */
|
|
export function focusKey(workspace, root) {
|
|
const rel = relative(resolve(workspace), resolve(root)).split(sep).join('/')
|
|
return rel === '' ? '.' : rel
|
|
}
|
|
|
|
/**
|
|
* Directories skipped while looking for scopes.
|
|
*
|
|
* A marker inside a dependency or a build directory describes somebody else's
|
|
* project, so discovering it would offer the user a scope they do not own.
|
|
*/
|
|
const DISCOVERY_SKIP_DIRS = new Set([
|
|
'node_modules', '.git', 'dist', 'build', 'out', 'coverage', 'target',
|
|
'.next', '.nuxt', '.venv', 'venv', '__pycache__', 'vendor', '.cache',
|
|
])
|
|
|
|
/**
|
|
* Find every directory that declares itself a scope.
|
|
*
|
|
* This exists because walking up from the working directory cannot find a
|
|
* marker in a subdirectory, and in the repository this was written for that is
|
|
* the whole point: the session sits at the workspace root while the eighteen
|
|
* controllers each carry their own instructions further down. Inheritance and
|
|
* discovery are different questions — "what contains me?" versus "what is
|
|
* available?" — and a scope needs both.
|
|
*
|
|
* Results are workspace-relative and sorted, so a list shown to a user or a
|
|
* model is stable between calls.
|
|
*
|
|
* @param {object} input
|
|
* @param {string} input.workspace absolute workspace root
|
|
* @param {readonly string[]} [input.markers] marker filenames
|
|
* @param {number} [input.maxDepth] deepest directory level to scan
|
|
* @returns {Promise<{path: string, marker: string}[]>} discovered scopes
|
|
*/
|
|
export async function discoverScopes(input) {
|
|
const { workspace, markers = DEFAULT_FOCUS_MARKERS, maxDepth = 8 } = input
|
|
const root = resolve(workspace)
|
|
const found = []
|
|
|
|
const walk = async (directory, depth) => {
|
|
if (depth > maxDepth) return
|
|
let entries
|
|
try {
|
|
entries = await readdir(directory, { withFileTypes: true })
|
|
} catch {
|
|
return
|
|
}
|
|
const names = new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name))
|
|
for (const marker of markers) {
|
|
if (names.has(marker)) {
|
|
const rel = relative(root, directory).split(sep).join('/')
|
|
found.push({ path: rel === '' ? '.' : rel, marker })
|
|
break
|
|
}
|
|
}
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory() || entry.isSymbolicLink()) continue
|
|
if (DISCOVERY_SKIP_DIRS.has(entry.name)) continue
|
|
await walk(join(directory, entry.name), depth + 1)
|
|
}
|
|
}
|
|
|
|
await walk(root, 0)
|
|
found.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
return found
|
|
}
|