Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
210 changes: 0 additions & 210 deletions packages/agent-core-v2/scripts/dep-graph/analyzer/analyze.ts

Large diffs are not rendered by default.

81 changes: 0 additions & 81 deletions packages/agent-core-v2/scripts/dep-graph/analyzer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,128 +7,47 @@
export type ServiceScope = 'App' | 'Session' | 'Agent';

export type EdgeKind =
/** `constructor(@IToken ...)` — declared DI dependency. */
| 'ctor'
/** `<scope>.accessor.get(IToken)` — runtime lookup. */
| 'accessor'
/** `<eventBus>.publish(...)` — publishes to `IEventService`. */
| 'publish'
/** `<eventBus>.subscribe(...)` — subscribes to `IEventService`. */
| 'subscribe'
/** `<record>.signal(...)` / `<record>.append(...)` — emits on `IAgentRecordService`. */
| 'emit'
/** `<record>.on(...)` — listens on `IAgentRecordService`. */
| 'on';

export interface ServiceNode {
/**
* Stable unique node id. One `registerScopedService` call = one node.
* Format: `${scope}::${token}` — matches the DI registration identity and
* disambiguates the same impl class bound to multiple tokens (e.g.
* `InMemoryStorageService` registered against 4 different tokens) as well
* as the same token bound at multiple scopes (e.g. `ILogService`
* bound at App and Session).
*/
id: string;
/** Token identifier (e.g. `IAgentSystemReminderService`). */
token: string;
/** Impl class name (e.g. `AgentSystemReminderService`). */
impl: string;
scope: ServiceScope;
/** First folder under `src/` (e.g. `systemReminder`). */
domain: string;
/** Repo-relative path of the impl file. */
file: string;
/** 1-indexed line of the `registerScopedService(...)` call. */
line: number;
/**
* Public callable surface of this service — the method/property names
* declared on the interface identified by `token`. Sorted, deduped, with
* the `_serviceBrand` DI marker filtered out. Absent when the analyzer
* couldn't locate an interface declaration for the token (e.g. synthetic
* framework bindings whose token has no interface in `src/`).
*/
publicMembers?: string[];
/**
* True for synthesized interface-only nodes: the token is referenced by at
* least one edge but has no implementation registered at any scope. These
* nodes have no real impl (so `impl` mirrors `token`) and the viewer renders
* them with a distinct border so missing bindings stand out from concrete
* services rather than being dropped as dangling edges.
*/
unresolved?: true;
/**
* True for synthesized scope-mismatch nodes: the token IS registered, but at
* a scope invisible to the edge's source. Rendered distinctly (and placed at
* the token's real registered scope) so a cross-scope reach reads differently
* from a genuinely missing implementation.
*/
scopeMismatch?: true;
}

export interface EdgeRef {
/** Repo-relative path where the reference occurs. */
file: string;
line: number;
/**
* Method on the source impl that contains this reference — the caller.
* `<ctor>` for the constructor, `get <name>` / `set <name>` for accessors,
* `<field <name>>` for a property initializer, or the plain method name.
* Absent for the ctor-param declaration refs and for refs the analyzer
* couldn't attribute to a named scope.
*/
fromMethod?: string;
/**
* Method invoked on the target service at this ref site.
* - `ctor` edge: the method the source calls on the injected field,
* e.g. `this.log.error(...)` → `error`.
* - `accessor` edge: the method chained on `<accessor>.get(IX).<method>()`.
* Absent for the pure declaration ref (the ctor param), for the pure
* lookup ref (a `get()` whose result is stored rather than called), and
* for event-bus edges where the method name is already the edge kind.
*/
toMethod?: string;
}

export interface Edge {
/** Source `ServiceNode.id` (impl-side, not token). */
from: string;
/**
* Resolved target `ServiceNode.id` — the concrete registration that the
* DI container would actually pick when the source is instantiated. For
* `unresolved: true` edges this is the token that couldn't be resolved,
* prefixed with `unresolved::`; for `scopeMismatch: true` edges it is
* prefixed with `scopeMismatch::`.
*/
to: string;
/** The interface/decorator name that appears at the source site. */
token: string;
kind: EdgeKind;
/**
* True when there is no impl registered for `token` at ANY scope — the
* token is simply unknown to the container. A `ctor` edge in this state
* would crash the container at instantiation time.
*/
unresolved?: true;
/**
* True when the token IS registered, but only at a scope that is not
* visible from the source (e.g. an App-scope service reaching for an
* Agent-scope token through an accessor whose scope the analyzer couldn't
* pin down). Distinct from `unresolved`: an implementation exists, the
* edge just can't be satisfied from where it is requested.
*/
scopeMismatch?: true;
/** When `scopeMismatch`, the innermost scope where `token` is registered. */
actualScope?: ServiceScope;
/** One or more locations that produced this edge (deduped). */
refs: EdgeRef[];
}

export interface Graph {
/** Wall-clock, but injected from the analyzer caller so the file is deterministic. */
generatedAt: string;
services: ServiceNode[];
edges: Edge[];
/** Tokens referenced by edges but not registered — usually external / test-only. */
unknownTokens: string[];
}
6 changes: 1 addition & 5 deletions packages/agent-core-v2/scripts/dep-graph/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ interface Violation {

function loadGraph(): Graph {
if (existsSync(SNAPSHOT_PATH)) {
// Only trust the snapshot if it's newer than the most recently touched
// source file — otherwise a stale JSON would silently mask violations
// introduced since the last analyze.
const snapMtime = statSync(SNAPSHOT_PATH).mtimeMs;
const srcMtime = latestMtime(SRC_ROOT);
if (snapMtime >= srcMtime) {
Expand Down Expand Up @@ -79,7 +76,7 @@ function lint(graph: Graph): Violation[] {
for (const edge of graph.edges) {
if (!edge.unresolved) continue;
const from = byId.get(edge.from);
if (!from) continue; // shouldn't happen — edge from unregistered source
if (!from) continue;
if (edge.kind === 'ctor') {
violations.push({ severity: 'error', edge, from });
} else if (edge.kind === 'accessor') {
Expand All @@ -101,7 +98,6 @@ function main(): number {
console.log(
` [${v.severity.toUpperCase()} ${v.from.scope}→?] ${v.from.impl} (${v.from.token}) --${v.edge.kind}--> ${v.edge.token} (no binding visible from ${v.from.scope})`,
);
// Refs are stored repo-relative in the graph, so print verbatim.
for (const ref of v.edge.refs) {
console.log(` ${ref.file}:${ref.line}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import type { Graph } from '../analyzer/types';
const VIRTUAL_ID = 'virtual:dep-graph';
const RESOLVED_ID = `\0${VIRTUAL_ID}`;

/** Coalesce watcher bursts (single save often fires add+change+rename). */
const DEBOUNCE_MS = 200;

function tag(): string {
Expand All @@ -41,17 +40,9 @@ function isSrcFile(file: string): boolean {
}

interface PluginOptions {
/** If false, don't mirror the graph to disk (in-memory only). Default true. */
writeSnapshotFile?: boolean;
}

/**
* Structural fingerprint of a graph: services + edges + unknownTokens only,
* with `generatedAt` deliberately excluded. The analyzer already sorts each
* of these arrays deterministically, so a stable `JSON.stringify` is enough
* to detect real content changes and ignore metadata-only churn (e.g. the
* HEAD sha bumping without any DI edit).
*/
function fingerprint(g: Graph): string {
return JSON.stringify({
services: g.services,
Expand All @@ -68,11 +59,6 @@ export function depGraphPlugin(options: PluginOptions = {}): Plugin {
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let watcher: FSWatcher | undefined;

/**
* Re-run the analyzer and swap `cached` only when the structural
* fingerprint changed. Returns whether the graph actually changed so the
* caller can decide whether to invalidate the virtual module.
*/
function analyzeNow(reason: string): boolean {
const started = Date.now();
const next = analyze({ generatedAt: tag() });
Expand Down Expand Up @@ -113,21 +99,10 @@ export function depGraphPlugin(options: PluginOptions = {}): Plugin {
return {
name: 'agent-core-v2:dep-graph',
buildStart() {
// Run once eagerly so the snapshot file exists as soon as the dev
// server prints its "ready" banner — external tools (and the first
// browser load) don't have to wait for the first save.
if (!cached) analyzeNow('startup');
},
configureServer(dev) {
server = dev;
// Vite's own watcher is scoped to the project `root` (the `web/`
// directory) and doesn't observe files under `src/`, so we spin up a
// dedicated chokidar watcher pointed at the source tree. Debounced
// above so a single save that fires multiple chokidar events only
// triggers one re-analysis.
//
// We watch the directory (not a glob) because chokidar v4 dropped
// built-in glob support — filtering to `.ts` happens in `isSrcFile`.
watcher = chokidar.watch(SRC_ROOT, {
ignoreInitial: true,
ignored: (path, stats) => {
Expand Down
8 changes: 0 additions & 8 deletions packages/agent-core-v2/scripts/dep-graph/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ import { depGraphPlugin } from './plugin/virtual-dep-graph';

const here = dirname(fileURLToPath(import.meta.url));

/**
* Dev-only Vite config for the `dep-graph` viewer. Rooted inside
* `scripts/dep-graph/web/` so it never touches `src/` or `dist/`; the
* frontend imports the analyzer output through the `virtual:dep-graph`
* plugin below.
*/
export default defineConfig({
root: resolve(here, 'web'),
cacheDir: resolve(here, '.vite'),
Expand All @@ -25,8 +19,6 @@ export default defineConfig({
},
plugins: [react(), depGraphPlugin()],
build: {
// Not shipped anywhere — never invoked, but guard against accidental
// `vite build` producing output inside src/.
outDir: resolve(here, '.local', 'web-dist'),
emptyOutDir: true,
},
Expand Down
4 changes: 0 additions & 4 deletions packages/agent-core-v2/scripts/dep-graph/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { collectTagCounts, loadTags, saveTags, tagsEqual, type TagMap } from './
const ALL_SCOPES: ServiceScope[] = ['App', 'Session', 'Agent'];

export function App(): JSX.Element {
// Read once at mount — deep-link params seed the initial filters; later
// interaction is purely client-side and does not write back to the URL.
const queryParams = useMemo(() => readQueryParams(window.location.search), []);

const domains = useMemo(
Expand Down Expand Up @@ -45,8 +43,6 @@ export function App(): JSX.Element {
: undefined,
);

// User-authored node tags, keyed by `ServiceNode.id`. Loaded once from
// localStorage and re-persisted on every change.
const [tags, setTags] = useState<TagMap>(() => loadTags());
useEffect(() => {
saveTags(tags);
Expand Down
11 changes: 0 additions & 11 deletions packages/agent-core-v2/scripts/dep-graph/web/src/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@ export interface FilterState {
hiddenDomains: Set<string>;
search: string;
hideOrphans: boolean;
/** When true, dagre runs once per scope and the bands are stacked vertically. */
groupByScope: boolean;
/**
* Tags the user is focusing. When non-empty, nodes carrying any of these
* tags (and their neighbours) stay bright and everything else dims — the
* "group by tag" view. Empty set means tag focus is off.
*/
activeTags: Set<string>;
}

Expand All @@ -28,11 +22,6 @@ interface FiltersProps {

const SCOPES: ServiceScope[] = ['App', 'Session', 'Agent'];

/**
* Left sidebar. All controls mutate `state` via `onChange` — the graph view
* re-derives its nodes/edges from the current filter set. Rendered as a
* fixed-width column so the graph takes the rest of the viewport.
*/
export function Filters({
graph,
domains,
Expand Down
Loading
Loading