@karmaniverous/stan-context
v0.6.0
Published
Deterministic dependency graph generator for STAN (LLM context selection).
Downloads
1,036
Readme
“Context compiler” for STAN — scans a repository and produces a deterministic dependency graph so an LLM can select the right files to read.
@karmaniverous/stan-context
STAN is moving from a filtering model (“archive everything, then hide files”) to a selection model (“build a Map, then choose the Territory”).
This package builds the Map:
- Scans a repository (“Universe”) using gitignore + selection config.
- Analyzes TS/JS module relationships via a provider model (default: TypeScript compiler API).
- Produces a deterministic, JSON-serializable dependency graph suitable for LLM context selection.
This package does not:
- create archives or diffs,
- manage
.stan/state, - apply patches,
- implement CLI/TTY behavior.
Those concerns remain in @karmaniverous/stan-core (engine) and @karmaniverous/stan-cli (CLI adapter).
Install
pnpm add @karmaniverous/stan-context
# or npm i @karmaniverous/stan-contextNode: >= 20
TypeScript: required for TS/JS analysis and must be provided explicitly by the host (see example below).
If TypeScript is missing or cannot be loaded from the injected inputs, generateDependencyGraph throws (this is not surfaced via errors).
Error propagation:
- If
typescriptPathis provided but loading fails, the thrown error message includes the underlying loader failure, and the original error is preserved aserror.cause(Node >= 20). - If TypeScript is missing (no injection) or
typescriptis invalid, there is no underlying loader error to propagate.
Quick example
import ts from 'typescript';
import { generateDependencyGraph } from '@karmaniverous/stan-context';
const res = await generateDependencyGraph({
cwd: process.cwd(),
typescript: ts,
config: {
includes: [],
excludes: ['dist/**'],
},
previousGraph: undefined,
});
console.log(res.stats);
// { modules, edges, dirty }Options (high level)
typescript/typescriptPath(required; host-provided)- This package does not attempt to resolve TypeScript implicitly.
- Provide either:
typescript: an already-loaded TypeScript module instance, ortypescriptPath: an absolute path to a TypeScript entry module (for examplerequire.resolve('typescript')from the host environment).
- If both are provided,
typescripttakes precedence. typescriptPathmust be an absolute filesystem path (ESM host example:createRequire(import.meta.url).resolve('typescript')).- If loading from
typescriptPathfails, the thrown error preserves the original loader error aserror.cause(Node >= 20). typescriptPathsemantics (important for hosts):typescriptPathis treated as an entry module file path that exports the TypeScript compiler API (it is not treated as a package root).- A good default is
require.resolve('typescript'), which typically resolves to a CommonJS file like.../typescript/lib/typescript.js. - VS Code’s TypeScript SDK entry (often
.../tsserverlibrary.js) is also suitable as long as it exports the same TypeScript API surface. - Interop: for module injection, stan-context normalizes
mod.default ?? modwhen validating the injected TypeScript module; fortypescriptPath, the file is loaded viarequire(), so the path MUST point to a CommonJS entry module. - Constraint (current): ESM-only entry modules are not supported via
typescriptPath. If your host only has an ESM entrypoint, injecttypescript(module injection) instead. - Future option (tracked): widen
typescriptPathloading to support both CJS and ESM by attemptingimport(pathToFileURL(...))onERR_REQUIRE_ESM, then normalizingmod.default ?? mod.
previousGraph- Pass the previously persisted graph to enable incremental analysis and edge reuse.
nodeDescriptionLimit(default:160)- Produces
GraphNode.descriptionfor TS/JS nodes based on module doc comments. - When truncated, uses a strict prefix of exactly N characters and appends ASCII
.... - Set to
0to omit descriptions.
- Produces
nodeDescriptionTags(default:['@module', '@packageDocumentation'])- Controls which TSDoc tags are considered for TS/JS descriptions.
- Tags must be
@-prefixed and match^@\\w+$.
hashSizeEnforcement(default:'warn')- Controls how to handle the invariant “if
metadata.hashis present for a file node,metadata.sizeshould also be present”. - Values:
'warn' | 'error' | 'ignore'.
- Controls how to handle the invariant “if
maxErrors(default:50)- Caps returned
errorsentries (deterministic truncation). - Set to
0to omit errors.
- Caps returned
What the graph contains (high level)
- Nodes are file-level (module-level) only.
- Node IDs are stable strings:
src/index.ts(repo-relative source)node_modules/.pnpm/[email protected]/node_modules/pkg/index.d.ts(physical external)C:/Users/me/dev/lib/index.d.ts(outside-root absolute, POSIX-normalized)node:fs(builtin)./missing-file(missing/unresolved specifier)
- Edges are outgoing adjacency lists (one entry per source node), including:
- explicit edges to imported modules/files
- implicit (“tunneled”) edges through barrels (
index.ts) for named/default imports
Determinism guarantees (consumer-friendly)
graph.nodeskeys are sorted for stable serialization.graph.edgesis a complete map: every node key exists (empty[]means “analyzed; no outgoing edges”).- Edge lists are de-duplicated and sorted deterministically.
Selection summary helper (context-mode budgeting)
This package also exports a helper for computing dependency selection closure membership and aggregate sizing from a graph plus dependency-state entries:
import {
summarizeDependencySelection,
type DependencyStateEntry,
} from '@karmaniverous/stan-context';
const include: DependencyStateEntry[] = [['src/index.ts', 2, ['runtime']]];
const summary = summarizeDependencySelection({ graph: res.graph, include });
console.log(summary.totalBytes, summary.largest, summary.warnings);Contract (summary):
- Entry forms:
nodeId[nodeId, depth][nodeId, depth, edgeKinds][nodeId, depth, kindMask](compact; runtime=1, type=2, dynamic=4)
- Traversal:
- outgoing edges only
- depth-limited expansion:
- depth
0includes only the seed node - depth
Nincludes nodes reachable withinNoutgoing-edge traversals
- depth
edgeKindsfilters which edges are followed (runtime|type|dynamic)
- Excludes win:
- expands include closure
S - expands exclude closure
Xusing the same semantics - final selection is
S \ X
- expands include closure
- Defaults:
defaultEdgeKinds:['runtime', 'type', 'dynamic']dropNodeKinds: dropsbuiltinandmissingnodes by default (with warnings)- unknown node IDs in state are retained with bytes
0(with warnings)
- Sizing:
totalBytesis the sum ofmetadata.size(bytes) for selected nodes where present- missing sizes are treated as
0(with warnings) - a common deterministic budgeting heuristic is
estimatedTokens ≈ totalBytes / 4
- Determinism:
selectedNodeIdsis sorted lexicographicallylargestis sorted by bytes descending, tie-break by nodeId ascendingwarningsis sorted lexicographically
ESLint plugin
This package ships an optional ESLint plugin subpath export:
import stanContext from '@karmaniverous/stan-context/eslint';
export default [
{
plugins: { 'stan-context': stanContext },
rules: {
...stanContext.configs.recommended.rules,
},
},
];The default config enables stan-context/require-module-description at warn, and ignores test/test-like files by default (common *.test.*, *.spec.*, and test/tests/__tests__ directory patterns across TS/JS-like extensions).
To enforce the rule everywhere (including tests), override it explicitly:
import stanContext from '@karmaniverous/stan-context/eslint';
export default [
{
plugins: { 'stan-context': stanContext },
rules: {
...stanContext.configs.recommended.rules,
'stan-context/require-module-description': [
'warn',
{ ignorePatterns: [] },
],
},
},
];Compact dependency meta encoding (context mode interop)
Hosts that implement STAN “context mode” often need a compact, assistant-facing
dependency.meta.json that:
- preserves NodeId path strings for reasoning,
- embeds outgoing edges in each node (no separate edges map),
- merges multiple edges per target into a single tuple using bitmasks.
This package exports a helper to encode the standard DependencyGraph into a
compact meta form suitable for writing to .stan/context/dependency.meta.json:
import { encodeDependencyMeta, generateDependencyGraph } from '@karmaniverous/stan-context';
const res = await generateDependencyGraph({ cwd, typescript: ts });
const meta = encodeDependencyMeta({ graph: res.graph });
// write meta as minified JSON in the hostCompact meta notes (v2):
meta.v === 2- nodes are keyed by NodeId under
meta.n node.kis a numeric kind index (source/external/builtin/missing)- edges are tuples under
node.e:[targetId, kindMask](explicit-only)[targetId, kindMask, resMask](explicit/implicit/both)
Hash note:
- The assistant-facing compact meta produced by
encodeDependencyMetaomits content hashes to reduce context size. - Hosts that need integrity-sensitive staging verification should maintain a
separate host-private mapping (for example
dependency.map.json) containing canonical nodeId → source locator + size + full sha256.
This helper is pure (no FS) and does not manage .stan/ state; the host is
responsible for persistence, staging, and archiving workflows.
License
BSD-3-Clause
Built for you with ❤️ on Bali! Find more great tools & templates on my GitHub Profile.
