Files
dsh-recall/lib/focus.js
T
2026-09-26 05:35:29 +01:00

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
}