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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions docs/service-topology.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,44 @@
# Cross-repository service topology

CodeDecay's service-topology foundation models explicitly configured repositories, packages, services, deployment units, APIs, event topics, schemas, datastores, jobs, environments, and teams. It performs no repository cloning, network discovery, command execution, model calls, or telemetry.
CodeDecay's service-topology model covers repositories, packages, services,
deployment units, APIs, event topics, schemas, datastores, jobs, environments,
and teams. Edges include produces, consumes, calls, publishes, subscribes,
reads, writes, deploys-with, owns, versioned-by, compatibility-requires, and
contains.

Topology manifests use schema version `1` and may be JSON or YAML. Every node and edge has stable IDs, confidence, freshness, trust class, limitations, and at least one source containing a repository ID and revision. Local repository roots are explicit; missing roots remain visible as unavailable partial checkouts.
Every node and edge carries source, repository/revision, confidence, freshness,
trust class, and limitations. Topology is local-first: explicit repository roots
and reviewable manifests only. There is no hidden cloning, network discovery,
install, model call, or telemetry.

Dependency analysis follows declared consumer relationships to changed contracts and reports connected deployment units and owners. Stale or inferred relationships produce verification gaps and never become trusted evidence by themselves. Normalized artifacts are written to `.codedecay/local/service-topology.json` and remain inspectable.
## Adapters

This foundation does not yet expose CLI or MCP commands and does not yet parse OpenAPI or asynchronous contracts. Those adapters should use maintained OSS parsers and feed this model rather than creating a second topology engine.
| Source | Adapter | Parser choice |
|---|---|---|
| Topology manifest | YAML/JSON loader | Maintained `yaml` package for YAML; `JSON.parse` for JSON |
| OpenAPI 3 | Local contract adapter | Maintained `yaml` / JSON parse; remote `$ref` blocked |
| AsyncAPI 2/3 | Local contract adapter | Maintained `yaml` / JSON parse; remote `$ref` blocked |
| Local engineering/impact graph | `contains` linker | Reuses `#676` local graph artifacts |

Rejected alternatives for this slice: hosted service catalogs, automatic git
clone fans-out, and network-resolving OpenAPI/AsyncAPI parsers that fetch remote
refs by default.

## CLI / MCP

```bash
codedecay topology --manifest topology.yml --changed api:billing:v1 --format json
codedecay topology --manifest topology.yml --openapi docs/openapi.yaml --asyncapi docs/asyncapi.yaml --invalidate docs/openapi.yaml
```

MCP tool: `service_topology`.

Normalized artifacts are written to `.codedecay/local/service-topology.json`.
Incremental `--invalidate` rewrites only affected contract-linked nodes/edges.

## Trust rules

- Verified/declared current edges can produce downstream impact tasks.
- Inferred or stale edges emit verification gaps and never raise trusted risk alone.
- Unavailable repositories remain explicit gaps.
- Agent tasks include owners, repositories, and corroboration work.
5 changes: 5 additions & 0 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { runRevalidateCommand as runRevalidateCommandWithDependencies } from "./
import { runRuntimeCommand as runRuntimeCommandWithDependencies } from "./runtime";
import { runSessionCommand as runSessionCommandWithDependencies } from "./session";
import { runSnapshotCommand as runSnapshotCommandWithDependencies } from "./snapshot";
import { runTopologyCommand as runTopologyCommandWithDependencies } from "./topology";
import { createProductTargetReport as createProductTargetReportWithRuntime } from "../product/runtime";
import { renderProductTargetReport } from "../renderers/product-target-report";
import {
Expand Down Expand Up @@ -137,6 +138,10 @@ export function createCommandHandlers(options: CommandRegistryOptions): Record<s
createAnalysisContext: createAnalysisContextForCli,
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
}),
topology: (context) => runTopologyCommandWithDependencies(context, {
resolveRepoRoot: getRepoRootForCli,
writeOutput: writeCliOutput
})
};
}
37 changes: 37 additions & 0 deletions packages/cli/src/commands/topology.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { resolve } from "node:path";
import {
buildServiceTopologyReport,
renderServiceTopologyReportMarkdown
} from "@submuxhq/codedecay-knowledge";
import { parseTopologyArgs } from "../parsers/args";
import type { CliCommandContext, CliRuntime, TopologyOptions } from "../types";

export interface RunTopologyCommandDependencies {
resolveRepoRoot(cwd: string, options: TopologyOptions): string;
writeOutput(input: { cwd: string; output?: string | undefined; rendered: string; runtime: CliRuntime }): void;
}

export function runTopologyCommand(context: CliCommandContext, dependencies: RunTopologyCommandDependencies): void {
const options = parseTopologyArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, options);
const report = buildServiceTopologyReport({
rootDir,
manifest: options.manifest,
openapi: options.openapi,
asyncapi: options.asyncapi,
localGraph: options.localGraph,
changedNodeIds: options.changed,
invalidatePaths: options.invalidate,
repositoryId: options.repositoryId,
revision: options.revision,
producerServiceId: options.producerServiceId,
publisherServiceId: options.publisherServiceId,
subscriberServiceId: options.subscriberServiceId
});
const rendered =
options.format === "json"
? `${JSON.stringify(report, null, 2)}\n`
: renderServiceTopologyReportMarkdown(report);
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
}
33 changes: 33 additions & 0 deletions packages/cli/src/docs/command-docs/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,39 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
],
notes: ["Inputs must resolve inside the repository. The command performs no network calls or project command execution."]
},
topology: {
name: "topology",
summary: "Model cross-repository services and deployment dependencies.",
usage: ["codedecay topology [options]"],
description: [
"Load a reviewable topology manifest plus local OpenAPI/AsyncAPI contracts, merge repository-local graph evidence, and report downstream consumers, owners, deployments, and verification gaps.",
"Local-only: no repository cloning, remote $ref fetch, network discovery, model calls, installs, or telemetry."
],
options: [
{ flag: "--manifest <path>", description: "Repo-local topology YAML/JSON manifest" },
{ flag: "--openapi <path>", description: "Repo-local OpenAPI 3 contract; repeatable" },
{ flag: "--asyncapi <path>", description: "Repo-local AsyncAPI 2/3 contract; repeatable" },
{ flag: "--local-graph <path>", description: "Optional engineering/impact graph JSON to link as contains edges" },
{ flag: "--changed <node-id>", description: "Changed topology node id; repeatable" },
{ flag: "--invalidate <path>", description: "Contract/manifest path to incrementally rebuild; repeatable" },
{ flag: "--repository-id <id>", description: "Repository id stamped onto contract-derived nodes" },
{ flag: "--revision <rev>", description: "Source revision stamped onto contract-derived nodes" },
{ flag: "--producer-service <id>", description: "Optional service id that produces OpenAPI operations" },
{ flag: "--publisher-service <id>", description: "Optional service id that publishes AsyncAPI channels" },
{ flag: "--subscriber-service <id>", description: "Optional service id that subscribes to AsyncAPI channels" },
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
{ flag: "--format <format>", description: "json or markdown (default: markdown)" },
{ flag: "--output <path>", description: "Write the topology report to a file instead of stdout" }
],
examples: [
"codedecay topology --manifest topology.yml --changed api:billing:v1 --format json",
"codedecay topology --manifest topology.yml --openapi docs/openapi.yaml --asyncapi docs/asyncapi.yaml --invalidate docs/openapi.yaml"
],
notes: [
"Stale and inferred dependencies remain untrusted and emit corroboration tasks instead of merge-safe proof.",
"Normalized artifacts are written to `.codedecay/local/service-topology.json`."
]
},
analyze: {
name: "analyze",
summary: "Deterministic PR risk, impact, and decay report.",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/docs/command-docs/order.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const COMMAND_ORDER = ["ai", "session", "context", "analyze", "runtime", "migration", "benchmark", "snapshot", "redteam", "revalidate", "llm-review", "agent", "loop", "doctor", "config", "memory", "memory-import", "memory-learn", "execute", "differential", "product", "dashboard", "mcp"] as const;
export const COMMAND_ORDER = ["ai", "session", "context", "analyze", "runtime", "migration", "topology", "benchmark", "snapshot", "redteam", "revalidate", "llm-review", "agent", "loop", "doctor", "config", "memory", "memory-import", "memory-learn", "execute", "differential", "product", "dashboard", "mcp"] as const;
export const UTILITY_COMMAND_ORDER = ["help", "man", "update", "uninstall", "version"] as const;
export const ROOT_FLAG_ALIASES = ["--help", "-h", "--version", "-V"] as const;
1 change: 1 addition & 0 deletions packages/cli/src/parsers/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export { parseProductArgs } from "./product";
export { parseRedteamArgs } from "./redteam";
export { parseSessionArgs } from "./session";
export { parseSnapshotArgs } from "./snapshot";
export { parseTopologyArgs } from "./topology";
export { parseUninstallArgs, parseUpdateArgs } from "./maintenance";
export { HelpRequested } from "./shared";
52 changes: 52 additions & 0 deletions packages/cli/src/parsers/topology.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { TopologyOptions } from "../types";
import { requireValue } from "./primitives";
import { HelpRequested, throwUnknownOption } from "./shared";

export function parseTopologyArgs(args: string[]): TopologyOptions {
const options: TopologyOptions = {
format: "markdown",
openapi: [],
asyncapi: [],
changed: [],
invalidate: []
};

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) continue;
if (arg === "--help" || arg === "-h") throw new HelpRequested();
const [flag, inline] = splitArg(arg);
const value = () => inline ?? requireValue(args, index, flag);
if (flag === "--cwd") options.cwd = value();
else if (flag === "--output") options.output = value();
else if (flag === "--format") options.format = parseFormat(value());
else if (flag === "--manifest") options.manifest = value();
else if (flag === "--openapi") options.openapi.push(value());
else if (flag === "--asyncapi") options.asyncapi.push(value());
else if (flag === "--local-graph") options.localGraph = value();
else if (flag === "--changed") options.changed.push(value());
else if (flag === "--invalidate") options.invalidate.push(value());
else if (flag === "--repository-id") options.repositoryId = value();
else if (flag === "--revision") options.revision = value();
else if (flag === "--producer-service") options.producerServiceId = value();
else if (flag === "--publisher-service") options.publisherServiceId = value();
else if (flag === "--subscriber-service") options.subscriberServiceId = value();
else {
throwUnknownOption(arg, "topology");
continue;
}
if (inline === undefined) index += 1;
}

return options;
}

function splitArg(arg: string): [string, string | undefined] {
const index = arg.indexOf("=");
return index < 0 ? [arg, undefined] : [arg.slice(0, index), arg.slice(index + 1)];
}

function parseFormat(value: string): TopologyOptions["format"] {
if (value === "json" || value === "markdown") return value;
throw new Error(`Invalid topology format "${value}". Expected json or markdown.`);
}
1 change: 1 addition & 0 deletions packages/cli/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from "./revalidate";
export * from "./runtime";
export * from "./session";
export * from "./snapshot";
export * from "./topology";
18 changes: 18 additions & 0 deletions packages/cli/src/types/topology.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ConfigFormat } from "./common";

export interface TopologyOptions {
cwd?: string | undefined;
format: ConfigFormat;
output?: string | undefined;
manifest?: string | undefined;
openapi: string[];
asyncapi: string[];
localGraph?: string | undefined;
changed: string[];
invalidate: string[];
repositoryId?: string | undefined;
revision?: string | undefined;
producerServiceId?: string | undefined;
publisherServiceId?: string | undefined;
subscriberServiceId?: string | undefined;
}
11 changes: 11 additions & 0 deletions packages/knowledge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ export {
topologyEvidenceId
} from "./topology/manifest";
export { analyzeServiceTopologyImpact, renderServiceTopologyImpactMarkdown } from "./topology/impact";
export {
buildServiceTopologyReport,
createTopologyAgentTasks,
mergeTopologyGraphs,
renderServiceTopologyReportMarkdown
} from "./topology/compose";
export { parseOpenApiTopology, topologyContractId } from "./topology/contracts/openapi";
export { parseAsyncApiTopology } from "./topology/contracts/asyncapi";
export type { BuildServiceTopologyOptions, ServiceTopologyAgentTask, ServiceTopologyReport } from "./topology/compose";
export type { ParseOpenApiTopologyOptions } from "./topology/contracts/openapi";
export type { ParseAsyncApiTopologyOptions } from "./topology/contracts/asyncapi";
export {
SERVICE_TOPOLOGY_EDGE_KINDS,
SERVICE_TOPOLOGY_NODE_KINDS,
Expand Down
Loading
Loading