@praneeth_54/agentdoctor 1.1.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -7
- package/README.md +263 -513
- package/dist/agents/aider/adapter.d.ts +1 -0
- package/dist/agents/aider/adapter.js +1 -0
- package/dist/agents/aider/detector.d.ts +14 -0
- package/dist/agents/aider/detector.js +120 -0
- package/dist/agents/copilot/adapter.d.ts +1 -0
- package/dist/agents/copilot/adapter.js +1 -0
- package/dist/agents/copilot/detector.d.ts +13 -0
- package/dist/agents/copilot/detector.js +113 -0
- package/dist/agents/gemini/adapter.d.ts +1 -0
- package/dist/agents/gemini/adapter.js +1 -0
- package/dist/agents/gemini/detector.d.ts +13 -0
- package/dist/agents/gemini/detector.js +151 -0
- package/dist/agents/registry.js +13 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/agents/windsurf/adapter.d.ts +1 -0
- package/dist/agents/windsurf/adapter.js +1 -0
- package/dist/agents/windsurf/detector.d.ts +14 -0
- package/dist/agents/windsurf/detector.js +119 -0
- package/dist/architecture/c4.d.ts +22 -0
- package/dist/architecture/c4.js +62 -0
- package/dist/architecture/contract.d.ts +64 -0
- package/dist/architecture/contract.js +373 -0
- package/dist/assurance/change.d.ts +211 -0
- package/dist/assurance/change.js +624 -0
- package/dist/assurance/proof.d.ts +136 -0
- package/dist/assurance/proof.js +338 -0
- package/dist/auth/index.d.ts +81 -0
- package/dist/auth/index.js +130 -0
- package/dist/auth/rbac.d.ts +9 -0
- package/dist/auth/rbac.js +26 -0
- package/dist/cli/commands/architecture.d.ts +6 -0
- package/dist/cli/commands/architecture.js +109 -0
- package/dist/cli/commands/assurance.d.ts +67 -0
- package/dist/cli/commands/assurance.js +210 -0
- package/dist/cli/commands/brain.d.ts +12 -0
- package/dist/cli/commands/brain.js +162 -0
- package/dist/cli/commands/complete.d.ts +23 -0
- package/dist/cli/commands/complete.js +232 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -2
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/platform.d.ts +17 -0
- package/dist/cli/commands/platform.js +195 -0
- package/dist/cli/commands/policy-graph-run.d.ts +40 -0
- package/dist/cli/commands/policy-graph-run.js +200 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/commands/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +1328 -3
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +5 -1
- package/dist/contracts/adapters.d.ts +7 -0
- package/dist/contracts/adapters.js +60 -0
- package/dist/contracts/index.d.ts +162 -0
- package/dist/contracts/index.js +18 -0
- package/dist/core/baseline/store.d.ts +48 -0
- package/dist/core/baseline/store.js +163 -0
- package/dist/core/brain-cli/service.d.ts +26 -0
- package/dist/core/brain-cli/service.js +175 -0
- package/dist/core/brain-product/init.d.ts +45 -0
- package/dist/core/brain-product/init.js +235 -0
- package/dist/core/changes/analyze.d.ts +41 -0
- package/dist/core/changes/analyze.js +223 -0
- package/dist/core/context-health/analyze.d.ts +15 -0
- package/dist/core/context-health/analyze.js +234 -0
- package/dist/core/fix/apply.d.ts +21 -4
- package/dist/core/fix/apply.js +118 -18
- package/dist/core/fix/backup.d.ts +30 -0
- package/dist/core/fix/backup.js +193 -0
- package/dist/core/fix/plan.d.ts +6 -3
- package/dist/core/fix/plan.js +113 -9
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +36 -21
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/safe-target.d.ts +14 -0
- package/dist/core/fix/safe-target.js +81 -0
- package/dist/core/fix/types.d.ts +12 -1
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/claude-settings.js +3 -1
- package/dist/core/fix/writers/codex-config.d.ts +3 -0
- package/dist/core/fix/writers/codex-config.js +24 -3
- package/dist/core/fix/writers/cursorignore.js +3 -1
- package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
- package/dist/core/fix/writers/simple-ignore.js +68 -0
- package/dist/core/monorepo/detect.d.ts +13 -0
- package/dist/core/monorepo/detect.js +121 -0
- package/dist/core/rules/build-context.js +22 -3
- package/dist/core/rules/context/generated-directory.js +11 -1
- package/dist/core/rules/context/large-instruction-file.js +6 -0
- package/dist/core/rules/context/large-log-file.js +16 -1
- package/dist/core/rules/ignore.d.ts +11 -1
- package/dist/core/rules/ignore.js +22 -1
- package/dist/core/rules/instructions/empty-instructions.js +7 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -2
- package/dist/core/rules/security/private-key-file.js +1 -1
- package/dist/core/scanner/scan.js +1 -1
- package/dist/core/schemas/validate.d.ts +6 -0
- package/dist/core/schemas/validate.js +62 -0
- package/dist/core/scoring/compute-scores.d.ts +1 -1
- package/dist/core/scoring/compute-scores.js +5 -1
- package/dist/core/scoring/placeholder.d.ts +1 -1
- package/dist/core/scoring/placeholder.js +5 -1
- package/dist/core/secrets/scan.d.ts +29 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/coverage/cobertura.d.ts +6 -0
- package/dist/coverage/cobertura.js +52 -0
- package/dist/coverage/istanbul.d.ts +7 -0
- package/dist/coverage/istanbul.js +134 -0
- package/dist/coverage/lcov.d.ts +7 -0
- package/dist/coverage/lcov.js +59 -0
- package/dist/coverage/load.d.ts +9 -0
- package/dist/coverage/load.js +39 -0
- package/dist/coverage/types.d.ts +23 -0
- package/dist/coverage/types.js +5 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +402 -0
- package/dist/enforcement/runner.d.ts +67 -0
- package/dist/enforcement/runner.js +460 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +25 -0
- package/dist/integrations/github/pr-analyze.d.ts +28 -0
- package/dist/integrations/github/pr-analyze.js +98 -0
- package/dist/integrations/local-ai/provider.d.ts +45 -0
- package/dist/integrations/local-ai/provider.js +104 -0
- package/dist/intelligence/git/analyze.d.ts +34 -0
- package/dist/intelligence/git/analyze.js +136 -0
- package/dist/intelligence/graph/build.d.ts +15 -0
- package/dist/intelligence/graph/build.js +259 -0
- package/dist/intelligence/graph/incremental.d.ts +68 -0
- package/dist/intelligence/graph/incremental.js +234 -0
- package/dist/intelligence/resolve/imports.d.ts +36 -0
- package/dist/intelligence/resolve/imports.js +245 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -0
- package/dist/languages/go.d.ts +9 -0
- package/dist/languages/go.js +45 -0
- package/dist/languages/index.d.ts +11 -0
- package/dist/languages/index.js +76 -0
- package/dist/languages/php.d.ts +7 -0
- package/dist/languages/php.js +193 -0
- package/dist/languages/python.d.ts +7 -0
- package/dist/languages/python.js +150 -0
- package/dist/languages/types.d.ts +42 -0
- package/dist/languages/types.js +26 -0
- package/dist/languages/typescript.d.ts +3 -0
- package/dist/languages/typescript.js +95 -0
- package/dist/mcp/agentdoctor/server.d.ts +16 -0
- package/dist/mcp/agentdoctor/server.js +67 -0
- package/dist/mcp/brain/session.js +7 -7
- package/dist/mcp/intelligence/handlers.d.ts +16 -0
- package/dist/mcp/intelligence/handlers.js +364 -0
- package/dist/mcp/intelligence/path-safety.d.ts +9 -0
- package/dist/mcp/intelligence/path-safety.js +63 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +228 -0
- package/dist/ops/health.d.ts +18 -0
- package/dist/ops/health.js +51 -0
- package/dist/platform/ai-quality/analyze.d.ts +9 -0
- package/dist/platform/ai-quality/analyze.js +65 -0
- package/dist/platform/architecture/drift.d.ts +17 -0
- package/dist/platform/architecture/drift.js +74 -0
- package/dist/platform/auth/local.d.ts +15 -0
- package/dist/platform/auth/local.js +58 -0
- package/dist/platform/context-security/analyze.d.ts +6 -0
- package/dist/platform/context-security/analyze.js +97 -0
- package/dist/platform/firewall/evaluate.d.ts +57 -0
- package/dist/platform/firewall/evaluate.js +359 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +189 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +192 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +89 -0
- package/dist/platform/knowledge/analyze.d.ts +20 -0
- package/dist/platform/knowledge/analyze.js +96 -0
- package/dist/platform/provenance/build.d.ts +44 -0
- package/dist/platform/provenance/build.js +65 -0
- package/dist/platform/readiness/scorecard.d.ts +5 -0
- package/dist/platform/readiness/scorecard.js +98 -0
- package/dist/platform/refactor/impact.d.ts +20 -0
- package/dist/platform/refactor/impact.js +60 -0
- package/dist/platform/reports/export.d.ts +12 -0
- package/dist/platform/reports/export.js +92 -0
- package/dist/platform/security/redact.d.ts +15 -0
- package/dist/platform/security/redact.js +87 -0
- package/dist/platform/sessions/store.d.ts +32 -0
- package/dist/platform/sessions/store.js +97 -0
- package/dist/platform/store.d.ts +6 -0
- package/dist/platform/store.js +57 -0
- package/dist/platform/test-impact/analyze.d.ts +50 -0
- package/dist/platform/test-impact/analyze.js +350 -0
- package/dist/platform/time-machine/compare.d.ts +17 -0
- package/dist/platform/time-machine/compare.js +62 -0
- package/dist/platform/tokens/plan.d.ts +28 -0
- package/dist/platform/tokens/plan.js +108 -0
- package/dist/platform/types.d.ts +120 -0
- package/dist/platform/types.js +5 -0
- package/dist/plugins/runtime.d.ts +36 -0
- package/dist/plugins/runtime.js +101 -0
- package/dist/plugins/sdk.d.ts +29 -0
- package/dist/plugins/sdk.js +114 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.d.ts +9 -0
- package/dist/policy/packs.js +91 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/security/paths.d.ts +21 -0
- package/dist/security/paths.js +105 -0
- package/dist/storage/postgres.d.ts +26 -0
- package/dist/storage/postgres.js +90 -0
- package/dist/storage/provider.d.ts +30 -0
- package/dist/storage/provider.js +109 -0
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.d.ts +35 -0
- package/dist/team/auth.js +70 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +15 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4-style views derived from repository graph evidence.
|
|
3
|
+
* Always labels inferred vs observed.
|
|
4
|
+
*/
|
|
5
|
+
export function buildC4Views(graph) {
|
|
6
|
+
const files = graph.nodes.filter((n) => n.kind === "file" || n.kind === "test");
|
|
7
|
+
const deps = graph.edges.filter((e) => e.kind === "imports" || e.kind === "depends_on");
|
|
8
|
+
const containers = [
|
|
9
|
+
{ id: "c_app", label: "Application", kind: "container" },
|
|
10
|
+
{ id: "c_tests", label: "Tests", kind: "container" },
|
|
11
|
+
{ id: "c_docs", label: "Docs", kind: "container" },
|
|
12
|
+
];
|
|
13
|
+
const system = {
|
|
14
|
+
level: "system-context",
|
|
15
|
+
title: "System context (inferred)",
|
|
16
|
+
nodes: [
|
|
17
|
+
{ id: "sys", label: graph.root.split("/").pop() ?? "system", kind: "system" },
|
|
18
|
+
{ id: "dev", label: "Developer / Agent", kind: "person" },
|
|
19
|
+
],
|
|
20
|
+
edges: [{ from: "dev", to: "sys", kind: "uses" }],
|
|
21
|
+
evidenceKind: "inferred",
|
|
22
|
+
limitations: ["System context is scaffolding from repository root name only"],
|
|
23
|
+
};
|
|
24
|
+
const container = {
|
|
25
|
+
level: "container",
|
|
26
|
+
title: "Containers (inferred from paths)",
|
|
27
|
+
nodes: containers,
|
|
28
|
+
edges: [
|
|
29
|
+
{ from: "c_app", to: "c_tests", kind: "tested_by" },
|
|
30
|
+
{ from: "c_docs", to: "c_app", kind: "documents" },
|
|
31
|
+
],
|
|
32
|
+
evidenceKind: "inferred",
|
|
33
|
+
limitations: ["Container boundaries inferred from folder conventions, not runtime topology"],
|
|
34
|
+
};
|
|
35
|
+
const component = {
|
|
36
|
+
level: "component",
|
|
37
|
+
title: "Components (from graph files)",
|
|
38
|
+
nodes: files.slice(0, 40).map((n) => ({
|
|
39
|
+
id: n.id,
|
|
40
|
+
label: n.label,
|
|
41
|
+
kind: n.kind,
|
|
42
|
+
})),
|
|
43
|
+
edges: deps.slice(0, 80).map((e) => ({ from: e.from, to: e.to, kind: e.kind })),
|
|
44
|
+
evidenceKind: deps.some((e) => e.evidence === "verified") ? "observed" : "inferred",
|
|
45
|
+
limitations: ["Component view capped; not a full deployment diagram"],
|
|
46
|
+
};
|
|
47
|
+
const code = {
|
|
48
|
+
level: "code",
|
|
49
|
+
title: "Code-level symbols",
|
|
50
|
+
nodes: graph.nodes
|
|
51
|
+
.filter((n) => n.kind === "function" || n.kind === "class")
|
|
52
|
+
.slice(0, 60)
|
|
53
|
+
.map((n) => ({ id: n.id, label: n.label, kind: n.kind })),
|
|
54
|
+
edges: graph.edges
|
|
55
|
+
.filter((e) => e.kind === "calls" || e.kind === "extends" || e.kind === "implements")
|
|
56
|
+
.slice(0, 80)
|
|
57
|
+
.map((e) => ({ from: e.from, to: e.to, kind: e.kind })),
|
|
58
|
+
evidenceKind: "observed",
|
|
59
|
+
limitations: ["Code view depends on active graph builder (AST or regex)"],
|
|
60
|
+
};
|
|
61
|
+
return [system, container, component, code];
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { RepositoryGraph } from "../platform/types.js";
|
|
2
|
+
export interface ArchitectureLayer {
|
|
3
|
+
id: string;
|
|
4
|
+
paths: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface ForbiddenDependency {
|
|
7
|
+
fromLayer: string;
|
|
8
|
+
toLayer: string;
|
|
9
|
+
id: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AllowedDependency {
|
|
13
|
+
fromLayer: string;
|
|
14
|
+
toLayer: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ArchitectureContract {
|
|
17
|
+
version: "1";
|
|
18
|
+
layers: ArchitectureLayer[];
|
|
19
|
+
forbidden: ForbiddenDependency[];
|
|
20
|
+
allowed: AllowedDependency[];
|
|
21
|
+
}
|
|
22
|
+
export interface ArchitectureViolation {
|
|
23
|
+
ruleId: string;
|
|
24
|
+
kind: "forbidden" | "not-allowed";
|
|
25
|
+
description: string;
|
|
26
|
+
fromPath: string;
|
|
27
|
+
toPath: string;
|
|
28
|
+
fromLayer: string;
|
|
29
|
+
toLayer: string;
|
|
30
|
+
evidence: {
|
|
31
|
+
edgeId: string;
|
|
32
|
+
edgeKind: string;
|
|
33
|
+
evidenceKind: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface ArchitectureCheckResult {
|
|
37
|
+
root: string;
|
|
38
|
+
contractPath: string | null;
|
|
39
|
+
contract: ArchitectureContract | null;
|
|
40
|
+
violations: ArchitectureViolation[];
|
|
41
|
+
importEdgesChecked: number;
|
|
42
|
+
limitations: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_ARCHITECTURE_CONTRACT: ArchitectureContract;
|
|
45
|
+
export declare function layerForPath(filePath: string, layers: ArchitectureLayer[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Minimal YAML subset parser for architecture contracts.
|
|
48
|
+
* Also accepts JSON content inside .yml files.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseArchitectureYamlSubset(text: string): ArchitectureContract;
|
|
51
|
+
export declare function parseArchitectureJson(text: string): ArchitectureContract;
|
|
52
|
+
export declare function loadArchitectureContract(rootInput: string): Promise<{
|
|
53
|
+
contract: ArchitectureContract;
|
|
54
|
+
path: string;
|
|
55
|
+
} | null>;
|
|
56
|
+
export declare function initArchitecture(rootInput: string): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Check import edges against the architecture contract.
|
|
59
|
+
* Forbidden rules always apply. When `allowed` is non-empty, cross-layer
|
|
60
|
+
* imports not listed in allowed (and not same-layer) are reported as not-allowed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function checkArchitecture(rootInput: string, graph: RepositoryGraph, contract?: ArchitectureContract | null, contractPath?: string | null): ArchitectureCheckResult;
|
|
63
|
+
export declare function checkArchitectureAtRoot(rootInput: string, graph: RepositoryGraph): Promise<ArchitectureCheckResult>;
|
|
64
|
+
export declare function explainArchitecture(contract: ArchitectureContract | null, check?: ArchitectureCheckResult | null): string;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
4
|
+
import { atomicWriteTextFile } from "../utils/fs.js";
|
|
5
|
+
const CONTRACT_JSON = ".agentdoctor/architecture.json";
|
|
6
|
+
const CONTRACT_YML = ".agentdoctor/architecture.yml";
|
|
7
|
+
export const DEFAULT_ARCHITECTURE_CONTRACT = {
|
|
8
|
+
version: "1",
|
|
9
|
+
layers: [
|
|
10
|
+
{ id: "cli", paths: ["src/cli/"] },
|
|
11
|
+
{ id: "platform", paths: ["src/platform/"] },
|
|
12
|
+
{ id: "core", paths: ["src/core/"] },
|
|
13
|
+
{ id: "tests", paths: ["tests/"] },
|
|
14
|
+
],
|
|
15
|
+
forbidden: [
|
|
16
|
+
{
|
|
17
|
+
id: "core-must-not-import-cli",
|
|
18
|
+
fromLayer: "core",
|
|
19
|
+
toLayer: "cli",
|
|
20
|
+
description: "Core must not depend on CLI surfaces",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "src-must-not-import-tests",
|
|
24
|
+
fromLayer: "core",
|
|
25
|
+
toLayer: "tests",
|
|
26
|
+
description: "Production core must not import test files",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "platform-must-not-import-tests",
|
|
30
|
+
fromLayer: "platform",
|
|
31
|
+
toLayer: "tests",
|
|
32
|
+
description: "Platform must not import test files",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
allowed: [
|
|
36
|
+
{ fromLayer: "cli", toLayer: "platform" },
|
|
37
|
+
{ fromLayer: "cli", toLayer: "core" },
|
|
38
|
+
{ fromLayer: "platform", toLayer: "core" },
|
|
39
|
+
{ fromLayer: "tests", toLayer: "core" },
|
|
40
|
+
{ fromLayer: "tests", toLayer: "platform" },
|
|
41
|
+
{ fromLayer: "tests", toLayer: "cli" },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
function normalizeRel(p) {
|
|
45
|
+
return p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
46
|
+
}
|
|
47
|
+
export function layerForPath(filePath, layers) {
|
|
48
|
+
const rel = normalizeRel(filePath);
|
|
49
|
+
let best = null;
|
|
50
|
+
for (const layer of layers) {
|
|
51
|
+
for (const prefix of layer.paths) {
|
|
52
|
+
const p = normalizeRel(prefix);
|
|
53
|
+
if (rel === p.replace(/\/$/, "") ||
|
|
54
|
+
rel.startsWith(p) ||
|
|
55
|
+
rel.startsWith(p.replace(/\/$/, "") + "/")) {
|
|
56
|
+
if (!best || p.length > best.len)
|
|
57
|
+
best = { id: layer.id, len: p.length };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return best?.id ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Minimal YAML subset parser for architecture contracts.
|
|
65
|
+
* Also accepts JSON content inside .yml files.
|
|
66
|
+
*/
|
|
67
|
+
export function parseArchitectureYamlSubset(text) {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
if (trimmed.startsWith("{")) {
|
|
70
|
+
return parseArchitectureJson(trimmed);
|
|
71
|
+
}
|
|
72
|
+
// Line-oriented subset: version, layers/forbidden/allowed with indented keys
|
|
73
|
+
const lines = text.split(/\r?\n/);
|
|
74
|
+
let version = "1";
|
|
75
|
+
const layers = [];
|
|
76
|
+
const forbidden = [];
|
|
77
|
+
const allowed = [];
|
|
78
|
+
let section = "none";
|
|
79
|
+
let current = null;
|
|
80
|
+
const pushCurrent = () => {
|
|
81
|
+
if (!current)
|
|
82
|
+
return;
|
|
83
|
+
if (section === "layers" && typeof current.id === "string") {
|
|
84
|
+
const paths = Array.isArray(current.paths)
|
|
85
|
+
? current.paths.filter((p) => typeof p === "string")
|
|
86
|
+
: [];
|
|
87
|
+
layers.push({ id: current.id, paths });
|
|
88
|
+
}
|
|
89
|
+
else if (section === "forbidden" && typeof current.id === "string") {
|
|
90
|
+
forbidden.push({
|
|
91
|
+
id: current.id,
|
|
92
|
+
fromLayer: String(current.fromLayer ?? ""),
|
|
93
|
+
toLayer: String(current.toLayer ?? ""),
|
|
94
|
+
description: String(current.description ?? ""),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (section === "allowed") {
|
|
98
|
+
allowed.push({
|
|
99
|
+
fromLayer: String(current.fromLayer ?? ""),
|
|
100
|
+
toLayer: String(current.toLayer ?? ""),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
current = null;
|
|
104
|
+
};
|
|
105
|
+
for (const raw of lines) {
|
|
106
|
+
if (!raw.trim() || raw.trimStart().startsWith("#"))
|
|
107
|
+
continue;
|
|
108
|
+
const indent = raw.match(/^ */)?.[0].length ?? 0;
|
|
109
|
+
const line = raw.trim();
|
|
110
|
+
if (indent === 0 && line.startsWith("version:")) {
|
|
111
|
+
pushCurrent();
|
|
112
|
+
version = unquote(line.slice("version:".length).trim()) || "1";
|
|
113
|
+
section = "none";
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (indent === 0 && line === "layers:") {
|
|
117
|
+
pushCurrent();
|
|
118
|
+
section = "layers";
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (indent === 0 && line === "forbidden:") {
|
|
122
|
+
pushCurrent();
|
|
123
|
+
section = "forbidden";
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (indent === 0 && line === "allowed:") {
|
|
127
|
+
pushCurrent();
|
|
128
|
+
section = "allowed";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (section !== "none" && line.startsWith("- ")) {
|
|
132
|
+
pushCurrent();
|
|
133
|
+
current = {};
|
|
134
|
+
const rest = line.slice(2).trim();
|
|
135
|
+
if (rest.includes(":")) {
|
|
136
|
+
const [k, ...restParts] = rest.split(":");
|
|
137
|
+
current[k.trim()] = parseYamlValue(restParts.join(":").trim());
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (current && indent >= 2 && line.includes(":")) {
|
|
142
|
+
const [k, ...restParts] = line.split(":");
|
|
143
|
+
const key = k.trim();
|
|
144
|
+
const val = restParts.join(":").trim();
|
|
145
|
+
if (val === "" || val === "|" || val === ">") {
|
|
146
|
+
// multi-line not supported; ignore
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (key === "paths" && val.startsWith("[")) {
|
|
150
|
+
try {
|
|
151
|
+
current.paths = JSON.parse(val.replace(/'/g, '"'));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
current.paths = val
|
|
155
|
+
.replace(/^\[/, "")
|
|
156
|
+
.replace(/\]$/, "")
|
|
157
|
+
.split(",")
|
|
158
|
+
.map((s) => unquote(s.trim()))
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
current[key] = parseYamlValue(val);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
pushCurrent();
|
|
168
|
+
if (version !== "1") {
|
|
169
|
+
throw new Error(`Unsupported architecture contract version: ${version}`);
|
|
170
|
+
}
|
|
171
|
+
return { version: "1", layers, forbidden, allowed };
|
|
172
|
+
}
|
|
173
|
+
function parseYamlValue(val) {
|
|
174
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(val.replace(/'/g, '"'));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return val
|
|
180
|
+
.slice(1, -1)
|
|
181
|
+
.split(",")
|
|
182
|
+
.map((s) => unquote(s.trim()))
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return unquote(val);
|
|
187
|
+
}
|
|
188
|
+
function unquote(s) {
|
|
189
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
190
|
+
return s.slice(1, -1);
|
|
191
|
+
}
|
|
192
|
+
return s;
|
|
193
|
+
}
|
|
194
|
+
export function parseArchitectureJson(text) {
|
|
195
|
+
const parsed = JSON.parse(text);
|
|
196
|
+
if (parsed.version !== "1") {
|
|
197
|
+
throw new Error(`Unsupported architecture contract version: ${String(parsed.version)}`);
|
|
198
|
+
}
|
|
199
|
+
if (!Array.isArray(parsed.layers) || !Array.isArray(parsed.forbidden)) {
|
|
200
|
+
throw new Error("Architecture contract requires layers and forbidden arrays");
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
version: "1",
|
|
204
|
+
layers: parsed.layers,
|
|
205
|
+
forbidden: parsed.forbidden,
|
|
206
|
+
allowed: Array.isArray(parsed.allowed) ? parsed.allowed : [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export async function loadArchitectureContract(rootInput) {
|
|
210
|
+
const root = resolveRepoRoot(rootInput);
|
|
211
|
+
const jsonPath = path.join(root, CONTRACT_JSON);
|
|
212
|
+
const ymlPath = path.join(root, CONTRACT_YML);
|
|
213
|
+
try {
|
|
214
|
+
const text = await fs.readFile(jsonPath, "utf8");
|
|
215
|
+
return { contract: parseArchitectureJson(text), path: jsonPath };
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// try yml
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const text = await fs.readFile(ymlPath, "utf8");
|
|
222
|
+
return { contract: parseArchitectureYamlSubset(text), path: ymlPath };
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export async function initArchitecture(rootInput) {
|
|
229
|
+
const root = resolveRepoRoot(rootInput);
|
|
230
|
+
const out = path.join(root, CONTRACT_JSON);
|
|
231
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
232
|
+
try {
|
|
233
|
+
await fs.access(out);
|
|
234
|
+
return out; // already exists — do not overwrite
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// create
|
|
238
|
+
}
|
|
239
|
+
await atomicWriteTextFile(out, `${JSON.stringify(DEFAULT_ARCHITECTURE_CONTRACT, null, 2)}\n`);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
function resolveNodePath(graph, nodeId) {
|
|
243
|
+
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
244
|
+
if (node?.path)
|
|
245
|
+
return normalizeRel(node.path);
|
|
246
|
+
if (node?.label)
|
|
247
|
+
return normalizeRel(node.label);
|
|
248
|
+
return normalizeRel(nodeId.replace(/^file:/, ""));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check import edges against the architecture contract.
|
|
252
|
+
* Forbidden rules always apply. When `allowed` is non-empty, cross-layer
|
|
253
|
+
* imports not listed in allowed (and not same-layer) are reported as not-allowed.
|
|
254
|
+
*/
|
|
255
|
+
export function checkArchitecture(rootInput, graph, contract, contractPath) {
|
|
256
|
+
const root = resolveRepoRoot(rootInput);
|
|
257
|
+
const limitations = [
|
|
258
|
+
"Architecture check uses static import edges from the repository graph",
|
|
259
|
+
"Layer assignment is path-prefix based — not runtime topology",
|
|
260
|
+
];
|
|
261
|
+
if (!contract) {
|
|
262
|
+
return {
|
|
263
|
+
root,
|
|
264
|
+
contractPath: contractPath ?? null,
|
|
265
|
+
contract: null,
|
|
266
|
+
violations: [],
|
|
267
|
+
importEdgesChecked: 0,
|
|
268
|
+
limitations: [
|
|
269
|
+
...limitations,
|
|
270
|
+
"No architecture contract found — run: agentdoctor architecture init",
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const violations = [];
|
|
275
|
+
const importEdges = graph.edges.filter((e) => e.kind === "imports" || e.kind === "depends_on");
|
|
276
|
+
const allowSet = new Set(contract.allowed.map((a) => `${a.fromLayer}->${a.toLayer}`));
|
|
277
|
+
const enforceAllowList = contract.allowed.length > 0;
|
|
278
|
+
for (const edge of importEdges) {
|
|
279
|
+
const fromPath = resolveNodePath(graph, edge.from);
|
|
280
|
+
const toPath = resolveNodePath(graph, edge.to);
|
|
281
|
+
const fromLayer = layerForPath(fromPath, contract.layers);
|
|
282
|
+
const toLayer = layerForPath(toPath, contract.layers);
|
|
283
|
+
if (!fromLayer || !toLayer)
|
|
284
|
+
continue;
|
|
285
|
+
if (fromLayer === toLayer)
|
|
286
|
+
continue;
|
|
287
|
+
for (const rule of contract.forbidden) {
|
|
288
|
+
if (rule.fromLayer === fromLayer && rule.toLayer === toLayer) {
|
|
289
|
+
violations.push({
|
|
290
|
+
ruleId: rule.id,
|
|
291
|
+
kind: "forbidden",
|
|
292
|
+
description: rule.description,
|
|
293
|
+
fromPath,
|
|
294
|
+
toPath,
|
|
295
|
+
fromLayer,
|
|
296
|
+
toLayer,
|
|
297
|
+
evidence: {
|
|
298
|
+
edgeId: edge.id,
|
|
299
|
+
edgeKind: edge.kind,
|
|
300
|
+
evidenceKind: edge.evidence,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (enforceAllowList && !allowSet.has(`${fromLayer}->${toLayer}`)) {
|
|
306
|
+
const alreadyForbidden = violations.some((v) => v.fromPath === fromPath &&
|
|
307
|
+
v.toPath === toPath &&
|
|
308
|
+
v.fromLayer === fromLayer &&
|
|
309
|
+
v.toLayer === toLayer &&
|
|
310
|
+
v.kind === "forbidden");
|
|
311
|
+
if (!alreadyForbidden) {
|
|
312
|
+
violations.push({
|
|
313
|
+
ruleId: `not-allowed:${fromLayer}->${toLayer}`,
|
|
314
|
+
kind: "not-allowed",
|
|
315
|
+
description: `Cross-layer import ${fromLayer} → ${toLayer} is not in the allowed list`,
|
|
316
|
+
fromPath,
|
|
317
|
+
toPath,
|
|
318
|
+
fromLayer,
|
|
319
|
+
toLayer,
|
|
320
|
+
evidence: {
|
|
321
|
+
edgeId: edge.id,
|
|
322
|
+
edgeKind: edge.kind,
|
|
323
|
+
evidenceKind: edge.evidence,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
violations.sort((a, b) => `${a.ruleId}:${a.fromPath}:${a.toPath}`.localeCompare(`${b.ruleId}:${b.fromPath}:${b.toPath}`));
|
|
330
|
+
return {
|
|
331
|
+
root,
|
|
332
|
+
contractPath: contractPath ?? null,
|
|
333
|
+
contract,
|
|
334
|
+
violations,
|
|
335
|
+
importEdgesChecked: importEdges.length,
|
|
336
|
+
limitations,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export async function checkArchitectureAtRoot(rootInput, graph) {
|
|
340
|
+
const loaded = await loadArchitectureContract(rootInput);
|
|
341
|
+
return checkArchitecture(rootInput, graph, loaded?.contract ?? null, loaded?.path ?? null);
|
|
342
|
+
}
|
|
343
|
+
export function explainArchitecture(contract, check) {
|
|
344
|
+
const lines = ["AgentDoctor architecture contract"];
|
|
345
|
+
if (!contract) {
|
|
346
|
+
lines.push(" No contract loaded. Run: agentdoctor architecture init");
|
|
347
|
+
return `${lines.join("\n")}\n`;
|
|
348
|
+
}
|
|
349
|
+
lines.push(` version: ${contract.version}`);
|
|
350
|
+
lines.push(` layers: ${contract.layers.map((l) => l.id).join(", ") || "(none)"}`);
|
|
351
|
+
for (const layer of contract.layers) {
|
|
352
|
+
lines.push(` - ${layer.id}: ${layer.paths.join(", ")}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push(` forbidden rules: ${contract.forbidden.length}`);
|
|
355
|
+
for (const rule of contract.forbidden) {
|
|
356
|
+
lines.push(` - [${rule.id}] ${rule.fromLayer} → ${rule.toLayer}: ${rule.description}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push(` allowed pairs: ${contract.allowed.length}`);
|
|
359
|
+
for (const a of contract.allowed) {
|
|
360
|
+
lines.push(` - ${a.fromLayer} → ${a.toLayer}`);
|
|
361
|
+
}
|
|
362
|
+
if (check) {
|
|
363
|
+
lines.push(` import edges checked: ${check.importEdgesChecked}`);
|
|
364
|
+
lines.push(` violations: ${check.violations.length}`);
|
|
365
|
+
for (const v of check.violations.slice(0, 20)) {
|
|
366
|
+
lines.push(` - ${v.kind} ${v.ruleId}: ${v.fromPath} → ${v.toPath} (${v.fromLayer}→${v.toLayer})`);
|
|
367
|
+
}
|
|
368
|
+
for (const lim of check.limitations.slice(0, 5)) {
|
|
369
|
+
lines.push(` limitation: ${lim}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return `${lines.join("\n")}\n`;
|
|
373
|
+
}
|