@praneeth_54/agentdoctor 2.1.0 → 3.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 +86 -3
- package/README.md +490 -290
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/service.js +8 -14
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +31 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +21 -5
- package/dist/agent/runtime.js +141 -28
- package/dist/agent/tools/execute.d.ts +2 -0
- package/dist/agent/tools/execute.js +49 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/config.js +1 -0
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/ai/redact.d.ts +1 -0
- package/dist/ai/redact.js +47 -7
- package/dist/ai/types.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.js +32 -11
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +15 -7
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +157 -0
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +86 -0
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/program.js +141 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +117 -140
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/intelligence/graph/build.js +46 -38
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +84 -24
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +1 -1
- package/dist/mcp/agent/registry.js +109 -23
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +36 -0
- package/dist/product/decisions/ledger.js +141 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +52 -0
- package/dist/product/discovery/roots.js +231 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +168 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +72 -0
- package/dist/product/index.js +36 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +96 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +92 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +105 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +78 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +91 -0
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
3
|
+
import { detectProject } from "../../detectors/project.js";
|
|
4
|
+
import { detectMonorepo } from "../../detectors/monorepo.js";
|
|
5
|
+
import { readJsonFile } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
import { parseAllLockfiles } from "./lockfiles.js";
|
|
8
|
+
function basename(rel) {
|
|
9
|
+
const parts = rel.split("/");
|
|
10
|
+
return parts[parts.length - 1] ?? rel;
|
|
11
|
+
}
|
|
12
|
+
function lockfilesFromPaths(relativePaths) {
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (const p of relativePaths) {
|
|
15
|
+
const base = basename(p).toLowerCase();
|
|
16
|
+
if (base === "package-lock.json" ||
|
|
17
|
+
base === "yarn.lock" ||
|
|
18
|
+
base === "pnpm-lock.yaml" ||
|
|
19
|
+
base === "bun.lockb") {
|
|
20
|
+
names.add(p.replace(/\\/g, "/"));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...names].sort();
|
|
24
|
+
}
|
|
25
|
+
function importersForPackage(depName, graph) {
|
|
26
|
+
if (!graph)
|
|
27
|
+
return [];
|
|
28
|
+
const importers = new Set();
|
|
29
|
+
for (const edge of graph.edges) {
|
|
30
|
+
if (edge.kind !== "imports" && edge.kind !== "depends-on")
|
|
31
|
+
continue;
|
|
32
|
+
const target = graph.nodes.find((n) => n.id === edge.to);
|
|
33
|
+
const source = graph.nodes.find((n) => n.id === edge.from);
|
|
34
|
+
if (!target || !source)
|
|
35
|
+
continue;
|
|
36
|
+
const label = target.label ?? target.id;
|
|
37
|
+
if (label === depName || label.endsWith(`/${depName}`) || label.includes(depName)) {
|
|
38
|
+
if (source.path)
|
|
39
|
+
importers.add(source.path.replace(/\\/g, "/"));
|
|
40
|
+
else if (source.label)
|
|
41
|
+
importers.add(source.label);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...importers].sort();
|
|
45
|
+
}
|
|
46
|
+
export async function analyzeDependencies(rootInput, options) {
|
|
47
|
+
const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
48
|
+
const root = resolveRepoRoot(rootInput);
|
|
49
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
50
|
+
const relativePaths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
|
|
51
|
+
let rootWorkspaces;
|
|
52
|
+
const rootPkg = await readJsonFile(path.join(root, "package.json"), maxFileSizeBytes);
|
|
53
|
+
if (rootPkg.ok)
|
|
54
|
+
rootWorkspaces = rootPkg.data.workspaces;
|
|
55
|
+
const monorepo = detectMonorepo({
|
|
56
|
+
relativePaths,
|
|
57
|
+
packageJsonWorkspaces: rootWorkspaces,
|
|
58
|
+
}).monorepo;
|
|
59
|
+
const lockfilesPresent = lockfilesFromPaths(relativePaths);
|
|
60
|
+
const limitations = [
|
|
61
|
+
"Direct dependencies are read from package.json files only.",
|
|
62
|
+
"Transitive versions are VERIFIED only when a lockfile was parsed successfully.",
|
|
63
|
+
];
|
|
64
|
+
if (!lockfilesPresent.length) {
|
|
65
|
+
limitations.push("No lockfile detected; transitive upgrade impact is UNKNOWN.");
|
|
66
|
+
}
|
|
67
|
+
const lockParse = await parseAllLockfiles(root, lockfilesPresent, maxFileSizeBytes);
|
|
68
|
+
limitations.push(...lockParse.limitations);
|
|
69
|
+
const packageJsonPaths = relativePaths.filter((p) => basename(p).toLowerCase() === "package.json");
|
|
70
|
+
const directDependencies = [];
|
|
71
|
+
const nameToPaths = new Map();
|
|
72
|
+
for (const rel of packageJsonPaths) {
|
|
73
|
+
const parsed = await readJsonFile(path.join(root, rel), maxFileSizeBytes);
|
|
74
|
+
if (!parsed.ok)
|
|
75
|
+
continue;
|
|
76
|
+
const pkgName = parsed.data.name?.trim();
|
|
77
|
+
if (pkgName) {
|
|
78
|
+
const list = nameToPaths.get(pkgName) ?? [];
|
|
79
|
+
list.push(rel);
|
|
80
|
+
nameToPaths.set(pkgName, list);
|
|
81
|
+
}
|
|
82
|
+
const sections = [
|
|
83
|
+
["dependencies", parsed.data.dependencies],
|
|
84
|
+
["devDependencies", parsed.data.devDependencies],
|
|
85
|
+
["peerDependencies", parsed.data.peerDependencies],
|
|
86
|
+
];
|
|
87
|
+
for (const [section, deps] of sections) {
|
|
88
|
+
if (!deps)
|
|
89
|
+
continue;
|
|
90
|
+
for (const [name, versionRange] of Object.entries(deps)) {
|
|
91
|
+
directDependencies.push({
|
|
92
|
+
name,
|
|
93
|
+
versionRange,
|
|
94
|
+
packageJsonPath: rel,
|
|
95
|
+
truth: "VERIFIED",
|
|
96
|
+
evidence: [{ path: rel, excerpt: `${section}.${name}` }],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
directDependencies.sort((a, b) => a.name === b.name
|
|
102
|
+
? a.packageJsonPath.localeCompare(b.packageJsonPath)
|
|
103
|
+
: a.name.localeCompare(b.name));
|
|
104
|
+
const duplicateNames = [];
|
|
105
|
+
for (const [name, paths] of nameToPaths) {
|
|
106
|
+
if (paths.length > 1) {
|
|
107
|
+
duplicateNames.push({
|
|
108
|
+
name,
|
|
109
|
+
paths: [...paths].sort(),
|
|
110
|
+
truth: "VERIFIED",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const upgradeImpact = [];
|
|
115
|
+
const uniqueDeps = [...new Set(directDependencies.map((d) => d.name))];
|
|
116
|
+
for (const dep of uniqueDeps) {
|
|
117
|
+
const importers = importersForPackage(dep, options?.graph);
|
|
118
|
+
if (importers.length) {
|
|
119
|
+
upgradeImpact.push({
|
|
120
|
+
dependency: dep,
|
|
121
|
+
importers,
|
|
122
|
+
truth: options?.graph ? "INFERRED" : "UNKNOWN",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
root,
|
|
128
|
+
monorepo,
|
|
129
|
+
directDependencies,
|
|
130
|
+
duplicateNames,
|
|
131
|
+
lockfilesPresent,
|
|
132
|
+
lockfilesParsed: lockParse.parsedLockfiles,
|
|
133
|
+
transitiveFromLockfile: lockParse.packages,
|
|
134
|
+
upgradeImpact,
|
|
135
|
+
limitations,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export interface LockfilePackageVersion {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
lockfile: string;
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
evidence: ProductEvidence[];
|
|
8
|
+
}
|
|
9
|
+
export interface ParsedLockfile {
|
|
10
|
+
lockfilePath: string;
|
|
11
|
+
format: "npm" | "yarn" | "pnpm" | "unknown";
|
|
12
|
+
packages: LockfilePackageVersion[];
|
|
13
|
+
limitations: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function parsePackageLockJson(content: string, lockfilePath: string): ParsedLockfile;
|
|
16
|
+
export declare function parseYarnLock(content: string, lockfilePath: string): ParsedLockfile;
|
|
17
|
+
export declare function parsePnpmLockYaml(content: string, lockfilePath: string): ParsedLockfile;
|
|
18
|
+
export declare function parseLockfileAtPath(root: string, relativePath: string, maxBytes: number): Promise<ParsedLockfile | null>;
|
|
19
|
+
export declare function parseAllLockfiles(root: string, lockfilePaths: string[], maxBytes: number): Promise<{
|
|
20
|
+
packages: LockfilePackageVersion[];
|
|
21
|
+
parsedLockfiles: string[];
|
|
22
|
+
limitations: string[];
|
|
23
|
+
}>;
|
|
24
|
+
/** Test helper: read lockfile from disk */
|
|
25
|
+
export declare function readLockfileFile(filePath: string): Promise<string>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
4
|
+
function npmPackageNameFromKey(key) {
|
|
5
|
+
if (!key || key === "")
|
|
6
|
+
return null;
|
|
7
|
+
const idx = key.lastIndexOf("node_modules/");
|
|
8
|
+
const tail = idx >= 0 ? key.slice(idx + "node_modules/".length) : key;
|
|
9
|
+
if (!tail || (tail.startsWith("@") === false && tail.includes("@"))) {
|
|
10
|
+
const at = tail.indexOf("@");
|
|
11
|
+
if (at > 0)
|
|
12
|
+
return tail.slice(0, at);
|
|
13
|
+
}
|
|
14
|
+
return tail.split("/").filter(Boolean).pop() ?? null;
|
|
15
|
+
}
|
|
16
|
+
export function parsePackageLockJson(content, lockfilePath) {
|
|
17
|
+
const limitations = [];
|
|
18
|
+
let data;
|
|
19
|
+
try {
|
|
20
|
+
data = JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {
|
|
24
|
+
lockfilePath,
|
|
25
|
+
format: "npm",
|
|
26
|
+
packages: [],
|
|
27
|
+
limitations: ["package-lock.json is not valid JSON"],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const obj = data;
|
|
31
|
+
const packages = [];
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
if (obj.packages && typeof obj.packages === "object") {
|
|
34
|
+
for (const [key, entry] of Object.entries(obj.packages)) {
|
|
35
|
+
const version = entry?.version?.trim();
|
|
36
|
+
if (!version)
|
|
37
|
+
continue;
|
|
38
|
+
const name = entry.name?.trim() ||
|
|
39
|
+
npmPackageNameFromKey(key) ||
|
|
40
|
+
(key === "" ? null : key.replace(/^node_modules\//, ""));
|
|
41
|
+
if (!name)
|
|
42
|
+
continue;
|
|
43
|
+
const dedupe = `${name}@${version}`;
|
|
44
|
+
if (seen.has(dedupe))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(dedupe);
|
|
47
|
+
packages.push({
|
|
48
|
+
name,
|
|
49
|
+
version,
|
|
50
|
+
lockfile: lockfilePath,
|
|
51
|
+
truth: "VERIFIED",
|
|
52
|
+
evidence: [{ path: lockfilePath, excerpt: `packages["${key}"].version=${version}` }],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (obj.dependencies && typeof obj.dependencies === "object") {
|
|
57
|
+
limitations.push("npm lockfile v1 dependencies tree — shallow direct entries only");
|
|
58
|
+
for (const [name, entry] of Object.entries(obj.dependencies)) {
|
|
59
|
+
const version = entry?.version?.trim();
|
|
60
|
+
if (!version)
|
|
61
|
+
continue;
|
|
62
|
+
packages.push({
|
|
63
|
+
name,
|
|
64
|
+
version,
|
|
65
|
+
lockfile: lockfilePath,
|
|
66
|
+
truth: "VERIFIED",
|
|
67
|
+
evidence: [{ path: lockfilePath, excerpt: `dependencies.${name}=${version}` }],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
limitations.push("Unrecognized package-lock.json shape");
|
|
73
|
+
}
|
|
74
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
75
|
+
return { lockfilePath, format: "npm", packages, limitations };
|
|
76
|
+
}
|
|
77
|
+
export function parseYarnLock(content, lockfilePath) {
|
|
78
|
+
const packages = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
let currentKey = null;
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
const header = /^([^\s].+):$/.exec(line);
|
|
85
|
+
if (header) {
|
|
86
|
+
currentKey = header[1].trim();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (currentKey && /^\s+version\s+/.test(line)) {
|
|
90
|
+
const verMatch = /version\s+"([^"]+)"/.exec(line);
|
|
91
|
+
if (!verMatch)
|
|
92
|
+
continue;
|
|
93
|
+
const version = verMatch[1];
|
|
94
|
+
const resolvedName = (() => {
|
|
95
|
+
const first = currentKey.split(",")[0].trim();
|
|
96
|
+
const at = first.lastIndexOf("@");
|
|
97
|
+
if (at <= 0)
|
|
98
|
+
return first;
|
|
99
|
+
return first.slice(0, at);
|
|
100
|
+
})();
|
|
101
|
+
const dedupe = `${resolvedName}@${version}`;
|
|
102
|
+
if (seen.has(dedupe))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(dedupe);
|
|
105
|
+
packages.push({
|
|
106
|
+
name: resolvedName,
|
|
107
|
+
version,
|
|
108
|
+
lockfile: lockfilePath,
|
|
109
|
+
truth: "VERIFIED",
|
|
110
|
+
evidence: [{ path: lockfilePath, line: i + 1, excerpt: line.trim().slice(0, 120) }],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (line.trim() === "" && currentKey) {
|
|
114
|
+
currentKey = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
118
|
+
return {
|
|
119
|
+
lockfilePath,
|
|
120
|
+
format: "yarn",
|
|
121
|
+
packages,
|
|
122
|
+
limitations: ["yarn.lock parsing uses version blocks — workspace aliases may be incomplete"],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function parsePnpmLockYaml(content, lockfilePath) {
|
|
126
|
+
const packages = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const limitations = [
|
|
129
|
+
"pnpm-lock.yaml parsing is heuristic (packages: keys) — not a full YAML AST",
|
|
130
|
+
];
|
|
131
|
+
const pkgSection = /(?:^|\n)packages:\s*\n([\s\S]*?)(?:\n\S|\n*$)/.exec(content);
|
|
132
|
+
const body = pkgSection?.[1] ?? content;
|
|
133
|
+
for (const line of body.split(/\r?\n/)) {
|
|
134
|
+
const keyMatch = /^\s{2}(['"]?)(\/[^'":]+@[^'":]+)\1:\s*$/.exec(line);
|
|
135
|
+
if (!keyMatch)
|
|
136
|
+
continue;
|
|
137
|
+
const key = keyMatch[2];
|
|
138
|
+
const at = key.lastIndexOf("@");
|
|
139
|
+
if (at <= 1)
|
|
140
|
+
continue;
|
|
141
|
+
const name = key.slice(1, at);
|
|
142
|
+
const version = key.slice(at + 1);
|
|
143
|
+
const dedupe = `${name}@${version}`;
|
|
144
|
+
if (seen.has(dedupe))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(dedupe);
|
|
147
|
+
packages.push({
|
|
148
|
+
name,
|
|
149
|
+
version,
|
|
150
|
+
lockfile: lockfilePath,
|
|
151
|
+
truth: "VERIFIED",
|
|
152
|
+
evidence: [{ path: lockfilePath, excerpt: key.slice(0, 120) }],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
packages.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
156
|
+
return { lockfilePath, format: "pnpm", packages, limitations };
|
|
157
|
+
}
|
|
158
|
+
export async function parseLockfileAtPath(root, relativePath, maxBytes) {
|
|
159
|
+
const abs = path.join(root, relativePath);
|
|
160
|
+
const base = path.basename(relativePath).toLowerCase();
|
|
161
|
+
const text = await readTextFile(abs, maxBytes);
|
|
162
|
+
if (text === null)
|
|
163
|
+
return null;
|
|
164
|
+
if (base === "package-lock.json" || base === "npm-shrinkwrap.json") {
|
|
165
|
+
return parsePackageLockJson(text, relativePath);
|
|
166
|
+
}
|
|
167
|
+
if (base === "yarn.lock") {
|
|
168
|
+
return parseYarnLock(text, relativePath);
|
|
169
|
+
}
|
|
170
|
+
if (base === "pnpm-lock.yaml") {
|
|
171
|
+
return parsePnpmLockYaml(text, relativePath);
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
export async function parseAllLockfiles(root, lockfilePaths, maxBytes) {
|
|
176
|
+
const all = [];
|
|
177
|
+
const parsedLockfiles = [];
|
|
178
|
+
const limitations = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const rel of lockfilePaths) {
|
|
181
|
+
const parsed = await parseLockfileAtPath(root, rel, maxBytes);
|
|
182
|
+
if (!parsed || parsed.packages.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
parsedLockfiles.push(rel);
|
|
185
|
+
limitations.push(...parsed.limitations);
|
|
186
|
+
for (const p of parsed.packages) {
|
|
187
|
+
const key = `${p.name}@${p.version}::${p.lockfile}`;
|
|
188
|
+
if (seen.has(key))
|
|
189
|
+
continue;
|
|
190
|
+
seen.add(key);
|
|
191
|
+
all.push(p);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
all.sort((a, b) => a.name.localeCompare(b.name) || a.version.localeCompare(b.version));
|
|
195
|
+
return { packages: all, parsedLockfiles, limitations };
|
|
196
|
+
}
|
|
197
|
+
/** Test helper: read lockfile from disk */
|
|
198
|
+
export async function readLockfileFile(filePath) {
|
|
199
|
+
return fs.readFile(filePath, "utf8");
|
|
200
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TruthLabel } from "../truth.js";
|
|
2
|
+
export interface ProjectCandidate {
|
|
3
|
+
root: string;
|
|
4
|
+
score: number;
|
|
5
|
+
markers: string[];
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectDiscoveryReport {
|
|
10
|
+
cwd: string;
|
|
11
|
+
home: string;
|
|
12
|
+
candidates: ProjectCandidate[];
|
|
13
|
+
selected: ProjectCandidate | null;
|
|
14
|
+
blocked: boolean;
|
|
15
|
+
blockReason: string | null;
|
|
16
|
+
estimatedEntries: number;
|
|
17
|
+
limitations: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface DiscoverProjectOptions {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/** Maximum directory entries to count before aborting deep scans. */
|
|
22
|
+
maxEntries?: number;
|
|
23
|
+
/** Prefer this path if it is a valid project. */
|
|
24
|
+
prefer?: string;
|
|
25
|
+
/** Auto-select highest score when exactly one strong candidate. */
|
|
26
|
+
autoSelect?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when `cwd` is the user home directory or a similarly unsafe broad folder
|
|
30
|
+
* (Desktop / Downloads / Documents). Used to refuse silent whole-home scans.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyBroadUserScanRoot(cwdInput: string, homeInput?: string): {
|
|
33
|
+
blocked: true;
|
|
34
|
+
reason: string;
|
|
35
|
+
} | {
|
|
36
|
+
blocked: false;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
40
|
+
* or unbounded parent trees.
|
|
41
|
+
*/
|
|
42
|
+
export declare function discoverProjectRoots(options?: DiscoverProjectOptions): Promise<ProjectDiscoveryReport>;
|
|
43
|
+
export declare function resolveStartedProjectRoot(pathArg: string | undefined, options?: {
|
|
44
|
+
maxEntries?: number;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
ok: true;
|
|
47
|
+
root: string;
|
|
48
|
+
discovery: ProjectDiscoveryReport;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
discovery: ProjectDiscoveryReport;
|
|
52
|
+
}>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_IGNORE_DIRECTORIES } from "../../constants.js";
|
|
5
|
+
import { isDirectory, pathExists } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
/** Markers that strongly suggest a software project root. */
|
|
8
|
+
const PROJECT_MARKERS = [
|
|
9
|
+
"package.json",
|
|
10
|
+
"composer.json",
|
|
11
|
+
"pyproject.toml",
|
|
12
|
+
"Cargo.toml",
|
|
13
|
+
"go.mod",
|
|
14
|
+
"pom.xml",
|
|
15
|
+
"build.gradle",
|
|
16
|
+
"build.gradle.kts",
|
|
17
|
+
"Gemfile",
|
|
18
|
+
"mix.exs",
|
|
19
|
+
"pubspec.yaml",
|
|
20
|
+
".git",
|
|
21
|
+
];
|
|
22
|
+
const SUSPICIOUS_DIR_NAMES = new Set([
|
|
23
|
+
"desktop",
|
|
24
|
+
"downloads",
|
|
25
|
+
"documents",
|
|
26
|
+
"pictures",
|
|
27
|
+
"movies",
|
|
28
|
+
"music",
|
|
29
|
+
"library",
|
|
30
|
+
"applications",
|
|
31
|
+
]);
|
|
32
|
+
function normalizeHome(home) {
|
|
33
|
+
return path.resolve(home);
|
|
34
|
+
}
|
|
35
|
+
function isUnderHomeSubtree(abs, home, leaf) {
|
|
36
|
+
const target = path.resolve(home, leaf);
|
|
37
|
+
const resolved = path.resolve(abs);
|
|
38
|
+
return resolved === target || resolved.startsWith(target + path.sep);
|
|
39
|
+
}
|
|
40
|
+
async function countEntriesShallow(dir, budget) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
const queue = [dir];
|
|
43
|
+
const skip = DEFAULT_IGNORE_DIRECTORIES;
|
|
44
|
+
while (queue.length > 0 && count < budget) {
|
|
45
|
+
const current = queue.pop();
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
count += 1;
|
|
55
|
+
if (count >= budget)
|
|
56
|
+
return count;
|
|
57
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
58
|
+
continue;
|
|
59
|
+
if (skip.has(entry.name) || entry.name.startsWith("."))
|
|
60
|
+
continue;
|
|
61
|
+
queue.push(path.join(current, entry.name));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
async function markersAt(dir) {
|
|
67
|
+
const found = [];
|
|
68
|
+
for (const marker of PROJECT_MARKERS) {
|
|
69
|
+
if (await pathExists(path.join(dir, marker))) {
|
|
70
|
+
found.push(marker);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
function scoreMarkers(markers) {
|
|
76
|
+
let score = 0;
|
|
77
|
+
for (const m of markers) {
|
|
78
|
+
if (m === ".git")
|
|
79
|
+
score += 3;
|
|
80
|
+
else if (m === "package.json" || m === "composer.json" || m === "pyproject.toml")
|
|
81
|
+
score += 5;
|
|
82
|
+
else
|
|
83
|
+
score += 4;
|
|
84
|
+
}
|
|
85
|
+
return score;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* True when `cwd` is the user home directory or a similarly unsafe broad folder
|
|
89
|
+
* (Desktop / Downloads / Documents). Used to refuse silent whole-home scans.
|
|
90
|
+
*/
|
|
91
|
+
export function classifyBroadUserScanRoot(cwdInput, homeInput) {
|
|
92
|
+
const cwd = path.resolve(cwdInput);
|
|
93
|
+
const home = normalizeHome(homeInput ?? os.homedir());
|
|
94
|
+
const baselower = path.basename(cwd).toLowerCase();
|
|
95
|
+
if (cwd === home ||
|
|
96
|
+
SUSPICIOUS_DIR_NAMES.has(baselower) ||
|
|
97
|
+
isUnderHomeSubtree(cwd, home, "Desktop") ||
|
|
98
|
+
isUnderHomeSubtree(cwd, home, "Downloads") ||
|
|
99
|
+
isUnderHomeSubtree(cwd, home, "Documents")) {
|
|
100
|
+
return {
|
|
101
|
+
blocked: true,
|
|
102
|
+
reason: "Refusing to scan home / Desktop / Downloads / Documents (or similarly broad user folders) as a project root. Navigate into a project directory or pass an explicit project path.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { blocked: false };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
109
|
+
* or unbounded parent trees.
|
|
110
|
+
*/
|
|
111
|
+
export async function discoverProjectRoots(options = {}) {
|
|
112
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
113
|
+
const home = normalizeHome(os.homedir());
|
|
114
|
+
const maxEntries = options.maxEntries ?? 25_000;
|
|
115
|
+
const limitations = [
|
|
116
|
+
"Discovery uses shallow marker probes and bounded entry counts — not a full repository index.",
|
|
117
|
+
"Candidates are VERIFIED only when marker files/directories exist on disk.",
|
|
118
|
+
];
|
|
119
|
+
const broad = classifyBroadUserScanRoot(cwd, home);
|
|
120
|
+
if (broad.blocked) {
|
|
121
|
+
const estimatedEntries = await countEntriesShallow(cwd, Math.min(maxEntries, 5_000));
|
|
122
|
+
return {
|
|
123
|
+
cwd,
|
|
124
|
+
home,
|
|
125
|
+
candidates: [],
|
|
126
|
+
selected: null,
|
|
127
|
+
blocked: true,
|
|
128
|
+
blockReason: broad.reason,
|
|
129
|
+
estimatedEntries,
|
|
130
|
+
limitations,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const estimatedEntries = await countEntriesShallow(cwd, maxEntries + 1);
|
|
134
|
+
if (estimatedEntries > maxEntries) {
|
|
135
|
+
return {
|
|
136
|
+
cwd,
|
|
137
|
+
home,
|
|
138
|
+
candidates: [],
|
|
139
|
+
selected: null,
|
|
140
|
+
blocked: true,
|
|
141
|
+
blockReason: `Directory tree exceeds safe scan budget (${maxEntries} entries). Narrow to a project subdirectory or raise --max-entries deliberately.`,
|
|
142
|
+
estimatedEntries,
|
|
143
|
+
limitations,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const candidates = [];
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
async function consider(dir, reason) {
|
|
149
|
+
const resolved = path.resolve(dir);
|
|
150
|
+
if (seen.has(resolved))
|
|
151
|
+
return;
|
|
152
|
+
if (!(await isDirectory(resolved)))
|
|
153
|
+
return;
|
|
154
|
+
if (resolved === home)
|
|
155
|
+
return;
|
|
156
|
+
const markers = await markersAt(resolved);
|
|
157
|
+
if (markers.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
seen.add(resolved);
|
|
160
|
+
candidates.push({
|
|
161
|
+
root: resolved,
|
|
162
|
+
score: scoreMarkers(markers),
|
|
163
|
+
markers,
|
|
164
|
+
truth: "VERIFIED",
|
|
165
|
+
reason,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (options.prefer) {
|
|
169
|
+
await consider(options.prefer, "explicit prefer path");
|
|
170
|
+
}
|
|
171
|
+
await consider(cwd, "current working directory");
|
|
172
|
+
// Walk up a few parents looking for markers (bounded).
|
|
173
|
+
let parent = path.dirname(cwd);
|
|
174
|
+
for (let i = 0; i < 4; i++) {
|
|
175
|
+
if (parent === home || parent === path.dirname(parent))
|
|
176
|
+
break;
|
|
177
|
+
await consider(parent, "parent directory with project markers");
|
|
178
|
+
parent = path.dirname(parent);
|
|
179
|
+
}
|
|
180
|
+
// Immediate children that look like projects (monorepo packages / sibling apps).
|
|
181
|
+
try {
|
|
182
|
+
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
185
|
+
continue;
|
|
186
|
+
if (DEFAULT_IGNORE_DIRECTORIES.has(entry.name) || entry.name.startsWith("."))
|
|
187
|
+
continue;
|
|
188
|
+
await consider(path.join(cwd, entry.name), "child directory with project markers");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
limitations.push("Could not list child directories for candidate discovery.");
|
|
193
|
+
}
|
|
194
|
+
candidates.sort((a, b) => b.score - a.score || a.root.localeCompare(b.root));
|
|
195
|
+
let selected = null;
|
|
196
|
+
if (options.prefer) {
|
|
197
|
+
selected = candidates.find((c) => c.root === path.resolve(options.prefer)) ?? null;
|
|
198
|
+
}
|
|
199
|
+
else if (options.autoSelect !== false) {
|
|
200
|
+
const strong = candidates.filter((c) => c.score >= 5);
|
|
201
|
+
if (strong.length === 1) {
|
|
202
|
+
selected = strong[0];
|
|
203
|
+
}
|
|
204
|
+
else if (candidates.length === 1) {
|
|
205
|
+
selected = candidates[0];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
cwd,
|
|
210
|
+
home,
|
|
211
|
+
candidates,
|
|
212
|
+
selected,
|
|
213
|
+
blocked: false,
|
|
214
|
+
blockReason: null,
|
|
215
|
+
estimatedEntries,
|
|
216
|
+
limitations,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export async function resolveStartedProjectRoot(pathArg, options) {
|
|
220
|
+
const prefer = pathArg ? resolveRepoRoot(pathArg) : undefined;
|
|
221
|
+
const discovery = await discoverProjectRoots({
|
|
222
|
+
cwd: prefer ?? process.cwd(),
|
|
223
|
+
...(prefer !== undefined ? { prefer } : {}),
|
|
224
|
+
...(options?.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
|
|
225
|
+
autoSelect: true,
|
|
226
|
+
});
|
|
227
|
+
if (discovery.blocked || !discovery.selected) {
|
|
228
|
+
return { ok: false, discovery };
|
|
229
|
+
}
|
|
230
|
+
return { ok: true, root: discovery.selected.root, discovery };
|
|
231
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TruthLabel } from "../truth.js";
|
|
2
|
+
export interface ProjectDna {
|
|
3
|
+
fingerprint: string;
|
|
4
|
+
name: string;
|
|
5
|
+
root: string;
|
|
6
|
+
projectType: string;
|
|
7
|
+
languages: string[];
|
|
8
|
+
frameworks: string[];
|
|
9
|
+
frontend: string[];
|
|
10
|
+
backend: string[];
|
|
11
|
+
database: string[];
|
|
12
|
+
packageManagers: string[];
|
|
13
|
+
buildSystems: string[];
|
|
14
|
+
testFrameworks: string[];
|
|
15
|
+
infrastructure: string[];
|
|
16
|
+
cloudIndicators: string[];
|
|
17
|
+
git: {
|
|
18
|
+
present: boolean;
|
|
19
|
+
truth: TruthLabel;
|
|
20
|
+
};
|
|
21
|
+
monorepo: {
|
|
22
|
+
isMonorepo: boolean;
|
|
23
|
+
tool: string;
|
|
24
|
+
packages: Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}>;
|
|
28
|
+
truth: TruthLabel;
|
|
29
|
+
};
|
|
30
|
+
services: string[];
|
|
31
|
+
packages: string[];
|
|
32
|
+
applications: string[];
|
|
33
|
+
externalIntegrations: string[];
|
|
34
|
+
aiCodingTools: string[];
|
|
35
|
+
counts: {
|
|
36
|
+
files: number;
|
|
37
|
+
sourceFiles: number;
|
|
38
|
+
testFiles: number;
|
|
39
|
+
configFiles: number;
|
|
40
|
+
};
|
|
41
|
+
configurationStructure: string[];
|
|
42
|
+
truth: TruthLabel;
|
|
43
|
+
limitations: string[];
|
|
44
|
+
generatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic Project DNA from repository detectors + file inventory.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildProjectDna(rootInput: string): Promise<ProjectDna>;
|
|
50
|
+
export declare function persistProjectDna(rootInput: string, dna?: ProjectDna): Promise<ProjectDna>;
|