Skip to content

Arch drift check#115

Merged
lzehrung merged 15 commits into
mainfrom
arch-drift-check
Jun 1, 2026
Merged

Arch drift check#115
lzehrung merged 15 commits into
mainfrom
arch-drift-check

Conversation

@lzehrung
Copy link
Copy Markdown
Owner

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “architecture drift” capability to Codegraph, enabling deterministic snapshotting of architecture signals and comparison across git refs or artifact baselines, with both CLI and library entrypoints.

Changes:

  • Introduces a drift snapshot/compare/report pipeline (cycles, hotspots, unresolved imports, public API, duplicates, graph edges) plus artifact baseline loading.
  • Adds codegraph drift CLI command with JSON/pretty output and --fail-on policy gating, wired into the main CLI dispatcher.
  • Adds comprehensive Vitest coverage and updates README/docs/skill guidance to document drift usage.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/drift.test.ts Unit tests for snapshot determinism, drift findings, policy selection, and pretty report rendering.
tests/drift-git-provider.test.ts Exercises git-ref comparisons and asserts the worktree is not dirtied.
tests/drift-artifact.test.ts Validates artifact baseline loading, head constraints, and signal behavior for artifacts.
tests/cli-regressions.test.ts Adds CLI regression coverage for drift JSON output and fail-on exit behavior.
tests/cli-command-modules.test.ts Ensures drift is exposed via the CLI dispatcher and help output is discoverable.
src/index.ts Exports drift APIs/types from the package entrypoint.
src/drift/types.ts Defines drift snapshot/report types and finding kinds.
src/drift/snapshot.ts Builds deterministic architecture snapshots from a checkout.
src/drift/report.ts Renders a grouped pretty drift report.
src/drift/index.ts Drift module barrel exports.
src/drift/git.ts Implements drift analysis across git refs or artifact baselines.
src/drift/compare.ts Compares snapshots, generates findings, applies thresholds and fail-on policy.
src/drift/artifact.ts Loads drift snapshots from manifest-backed graph-json artifacts.
src/cli/options.ts Registers new CLI value options (--base-artifact, --fail-on, --hotspot-jump-threshold).
src/cli/help.ts Adds drift command to help output and provides drift help text.
src/cli/drift.ts Implements the drift CLI command handler (policy, thresholds, output).
src/cli.ts Routes drift through the main CLI dispatcher and adjusts positional include-root behavior.
README.md Documents drift at a high level and shows CLI/library examples.
docs/library-api.md Documents analyzeArchitectureDrift() usage and output shape.
docs/cli.md Adds drift CLI examples and guidance about policy gating.
docs/agent-workflows.md Adds drift to recommended agent workflows.
codegraph-skill/codegraph/SKILL.md Updates skill guidance to include the drift command and CI gating examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/drift/compare.ts
Comment on lines +99 to +103
const before = baseByFile.get(file)?.score ?? 0;
const after = headByFile.get(file)?.score ?? 0;
const delta = after - before;
if (Math.abs(delta) < threshold) continue;
const kind: ArchitectureDriftFindingKind = delta > 0 ? "hotspot-jump" : "hotspot-drop";
Comment thread src/drift/compare.ts
Comment on lines +262 to +268
const limitedFindings = findings.slice(0, thresholds.maxFindings);
const failOn = [...(options.failOn ?? [])].sort();
const failOnSet = new Set(failOn);
const matchedFailKinds = limitedFindings
.filter((finding) => failOnSet.has(finding.kind))
.map((finding) => finding.kind);
const failedKinds = Array.from(new Set(matchedFailKinds)).sort();
Comment thread src/cli/drift.ts
Comment on lines +50 to +56
const base = context.getOpt("--base");
const baseArtifact = context.getOpt("--base-artifact");
if (!base && !baseArtifact) {
context.writeStderrLine("Usage: codegraph drift [roots...] --base <ref> [--head <ref>] [--json | --pretty]");
context.writeStderrLine("Provide either --base or --base-artifact.");
context.exit(2);
}
Comment thread src/cli/help.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comment thread src/drift/snapshot.ts Outdated
Comment on lines +22 to +43
const DUPLICATE_PROJECT_PATTERNS = [...DEFAULT_PROJECT_PATTERNS, "**/*.{json,jsonc,toml,txt,yaml,yml}"];
const DEFAULT_DUPLICATE_LIMIT = 50;

function normalizeRoot(root: string): string {
return normalizePath(path.resolve(root));
}

function normalizeIncludeRoot(root: string, includeRoot: string): string {
return normalizePath(resolveFilePathFromRoot(root, includeRoot));
}

function isUnderIncludeRoots(filePath: string, roots: readonly string[]): boolean {
if (!roots.length) return true;
const normalizedFile = normalizePath(filePath);
return roots.some((root) => normalizedFile === root || normalizedFile.startsWith(`${root}/`));
}

async function listFilesForSnapshot(root: string, options: ArchitectureSnapshotOptions): Promise<string[] | undefined> {
if (!options.includeRoots?.length) return undefined;
const roots = options.includeRoots.map((entry) => normalizeIncludeRoot(root, entry));
const files = await listProjectFiles(root, DUPLICATE_PROJECT_PATTERNS, options.discovery);
return files.filter((file) => isUnderIncludeRoots(file, roots)).sort();
Comment thread src/drift/artifact.ts Outdated
Comment on lines +153 to +156
cycles: sortDetailedCycles(findDetailedCycles(graph), "priority").map((cycle) => {
const files = cycle.files.map(normalizePath).sort();
return { key: files.join("->"), files, priorityScore: cycle.priorityScore, size: cycle.fileCount };
}),
Comment thread src/drift/report.ts
Comment on lines +49 to +66
const limit = options.limit ?? report.findings.length;
const findings = report.findings.slice(0, limit);
const lines = ["Architecture drift", ""];
if (!findings.length) {
lines.push("No architecture drift findings.");
} else {
for (const severity of ["error", "warning", "info"] as const) {
pushSeveritySection(
lines,
severityHeading(severity),
findings.filter((finding) => finding.severity === severity),
);
}
}
const omitted = report.omittedCounts.findings + Math.max(0, report.findings.length - findings.length);
if (omitted) {
lines.push("", `Omitted ${omitted} finding(s).`);
}
Comment thread src/drift/git.ts Outdated
throw new Error("Architecture drift requires --base or --base-artifact.");
}
const resolvedRoot = path.resolve(root);
const base = await materializeGitRef(resolvedRoot, options.base, "cg-drift-base-");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Temp directory leak if head materialization fails

base is created outside the try block. If materializeGitRef for head throws, base.cleanup is never removed because control never reaches the finally block. Move both materialization calls inside the try (or add a catch around head that cleans up base).

Comment thread src/drift/git.ts Outdated
await execFileAsync("git", ["checkout", "--quiet", checkoutRef], { cwd: tempRoot, env: process.env });
return { root: tempRoot, cleanup: tempRoot };
} catch (error) {
await fsp.rm(tempRoot, { recursive: true, force: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Cleanup failure masks the original error

If fsp.rm throws during cleanup, the original git clone/git checkout error is lost.

Suggested change
await fsp.rm(tempRoot, { recursive: true, force: true });
try { await fsp.rm(tempRoot, { recursive: true, force: true }); } catch {}

Comment thread src/drift/snapshot.ts Outdated
}

function cycleKey(files: readonly string[]): string {
return [...files].sort().join("->");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Cycle key collision with filenames containing ->

Using -> as a separator can produce colliding keys (e.g. ["a.ts", "b.ts", "c.ts"] and ["a.ts", "b.ts->c.ts"] both become "a.ts->b.ts->c.ts"). Other keys in this codebase use \0 for safety.

Suggested change
return [...files].sort().join("->");
return [...files].sort().join("\0");

Comment thread src/drift/artifact.ts Outdated
cycles: sortDetailedCycles(findDetailedCycles(graph), "priority")
.map((cycle) => {
const files = cycle.files.map(normalizePath).sort();
return { key: files.join("->"), files, priorityScore: cycle.priorityScore, size: cycle.fileCount };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Cycle key collision with filenames containing ->

Joining with -> can produce colliding keys when filenames contain ->. Other keys in this module use \0 as a separator.

Suggested change
return { key: files.join("->"), files, priorityScore: cycle.priorityScore, size: cycle.fileCount };
return { key: files.join("\0"), files, priorityScore: cycle.priorityScore, size: cycle.fileCount };

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented May 31, 2026

Code Review Summary

Status: 12 Issues Found | Recommendation: Address before merge

Resolved in this update: The previous option-injection vulnerability in materializeGitRef() has been fixed by resolving git refs to commit hashes via resolveGitCommit() before checkout.

Overview

Severity Count
CRITICAL 0
WARNING 12
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/drift/compare.ts 103 compareHotspots will emit a finding even when there is no actual hotspot change
src/drift/compare.ts 266 --fail-on policy is currently evaluated only against limit-truncated findings
src/cli/drift.ts 60 The command currently allows passing both --base and --base-artifact at the CLI level before the library validation catches it
src/cli/help.ts 255 DRIFT_HELP_TEXT doesn't mention drift-specific flags that are actually supported
src/drift/snapshot.ts N/A When includeRoots is provided, listFilesForSnapshot() uses the caller's root instead of resolving relative to the snapshot root
src/drift/artifact.ts N/A sortDetailedCycles(..., "priority") does not provide a deterministic tie-breaker
src/drift/report.ts 70 When findings is empty due to a limit (e.g. maxFindings), the summary still reports "0 issues found" without indicating findings were truncated
src/drift/snapshot.ts N/A Cycle key separator -> can collide with filenames containing ->
src/drift/artifact.ts N/A Cycle key separator -> can collide with filenames containing ->
src/drift/git.ts 77 When using an artifact baseline, passing both baseArtifact and base is validated at the library level but the error message could be clearer
src/cli/drift.ts N/A The drift command currently defaults to pretty output when no format flag is passed, which may be unexpected for programmatic consumers
src/drift/artifact.ts N/A loadArchitectureSnapshotFromArtifact() currently leaves highLevelCycles and detailedCycles unsorted
Other Observations (not in diff)

No new issues found in unchanged code.

Files Reviewed (22 files)
  • src/drift/git.ts - option-injection vulnerability resolved
  • src/drift/snapshot.ts
  • src/drift/artifact.ts
  • src/drift/compare.ts
  • src/drift/report.ts
  • src/drift/index.ts
  • src/cli/drift.ts
  • src/cli/help.ts
  • src/cli.ts
  • src/cli/options.ts
  • src/index.ts
  • README.md
  • docs/cli.md
  • docs/library-api.md
  • docs/agent-workflows.md
  • codegraph-skill/codegraph/SKILL.md
  • tests/drift.test.ts
  • tests/drift-git-provider.test.ts
  • tests/drift-artifact.test.ts
  • tests/cli-command-modules.test.ts
  • tests/cli-regressions.test.ts

Fix these issues in Kilo Cloud


Reviewed by kimi-k2.6-20260420 · 228,783 tokens

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Comment thread src/drift/git.ts
Comment on lines +60 to +63
if (options.baseArtifact) {
if (options.head && !isCurrentCheckoutRef(options.head)) {
throw new Error("Architecture drift with --base-artifact only supports the current checkout as --head.");
}
Comment thread src/cli/drift.ts Outdated
Comment on lines +78 to +84
if (context.hasFlag("--json") && !context.hasFlag("--pretty")) {
context.writeJSONLine(report);
} else {
for (const line of renderArchitectureDriftReport(report, { limit: maxFindings }).trimEnd().split("\n")) {
context.writeStdoutLine(line);
}
}
Comment thread src/drift/artifact.ts Outdated
schemaVersion: 1,
root: outDir,
files: { total: graphJson.graph.files.length, byLanguage: languageCounts(graphJson.graph.files) },
hotspots: getHotspots(graph).map((entry) => ({ file: entry.file, fanIn: entry.fanIn, fanOut: entry.fanOut, score: entry.score })),
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Comment thread src/drift/git.ts Outdated
Comment on lines +47 to +48
await execFileAsync("git", ["clone", "--quiet", "--no-checkout", root, tempRoot], { env: process.env });
await execFileAsync("git", ["checkout", "--quiet", checkoutRef], { cwd: tempRoot, env: process.env });
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Comment thread src/cli/drift.ts Outdated
Comment on lines +62 to +76
const head = context.getOpt("--head");
const report = await analyzeArchitectureDrift(context.projectRootFs, {
...(base ? { provider: "git" as const, base } : {}),
...(head ? { head } : {}),
...(baseArtifact ? { baseArtifact } : {}),
includeRoots: context.positionals,
failOn,
thresholds: {
...(hotspotJump !== undefined ? { hotspotJump } : {}),
maxFindings,
},
...(context.graphOptions ? { graph: context.graphOptions } : {}),
...(context.indexOptions ? { index: context.indexOptions } : {}),
...(context.nativeMode !== "auto" ? { native: context.nativeMode } : {}),
});
Comment thread src/cli/help.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

@lzehrung lzehrung merged commit 847d9ab into main Jun 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants