@praneeth_54/agentdoctor 2.1.0 → 3.0.0
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 +57 -3
- package/README.md +489 -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/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 +10 -0
- package/dist/ai/config.js +1 -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 +19 -8
- package/dist/cli/commands/chat.js +2 -4
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +141 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.js +255 -53
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/intelligence/graph/build.js +36 -8
- 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/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- 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 +24 -0
- package/dist/product/decisions/ledger.js +110 -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 +42 -0
- package/dist/product/discovery/roots.js +215 -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 +193 -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 +69 -0
- package/dist/product/index.js +35 -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 +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -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 +100 -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 +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
4
|
+
import { detectProject } from "../../detectors/project.js";
|
|
5
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
6
|
+
import { resolveSafeRepoPath } from "../../security/paths.js";
|
|
7
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
8
|
+
import { pathExists } from "../../utils/fs.js";
|
|
9
|
+
function ledgerFile(root) {
|
|
10
|
+
return path.join(resolveRepoRoot(root), ".agentdoctor", "decisions", "entries.jsonl");
|
|
11
|
+
}
|
|
12
|
+
function isAdrPath(relativePath) {
|
|
13
|
+
const norm = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
14
|
+
return (norm.startsWith("docs/adr/") ||
|
|
15
|
+
norm.startsWith("adr/") ||
|
|
16
|
+
norm.includes("/adr/") ||
|
|
17
|
+
/^adr-\d+/i.test(path.basename(relativePath)));
|
|
18
|
+
}
|
|
19
|
+
function parseAdr(relativePath, content) {
|
|
20
|
+
const lines = content.split(/\r?\n/);
|
|
21
|
+
let title = "";
|
|
22
|
+
let status;
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (!title && /^#\s+/.test(line)) {
|
|
25
|
+
title = line.replace(/^#\s+/, "").trim();
|
|
26
|
+
}
|
|
27
|
+
const statusMatch = /^\*\*Status\*\*:\s*(.+)$/i.exec(line.trim());
|
|
28
|
+
if (statusMatch)
|
|
29
|
+
status = statusMatch[1].trim();
|
|
30
|
+
}
|
|
31
|
+
if (!title)
|
|
32
|
+
return null;
|
|
33
|
+
const id = relativePath.replace(/[^\w./-]/g, "_");
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
title,
|
|
37
|
+
...(status !== undefined ? { status } : {}),
|
|
38
|
+
source: "adr-file",
|
|
39
|
+
truth: "VERIFIED",
|
|
40
|
+
evidence: [{ path: relativePath, line: 1, excerpt: title.slice(0, 120) }],
|
|
41
|
+
bodyExcerpt: content.slice(0, 400),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function parseAdrDecisions(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
|
|
45
|
+
const root = resolveRepoRoot(rootInput);
|
|
46
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
47
|
+
const decisions = [];
|
|
48
|
+
for (const entry of detection.discovery.files) {
|
|
49
|
+
const rel = entry.relativePath.replace(/\\/g, "/");
|
|
50
|
+
if (!isAdrPath(rel) || !rel.toLowerCase().endsWith(".md"))
|
|
51
|
+
continue;
|
|
52
|
+
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
53
|
+
if (!text)
|
|
54
|
+
continue;
|
|
55
|
+
const parsed = parseAdr(rel, text);
|
|
56
|
+
if (parsed)
|
|
57
|
+
decisions.push(parsed);
|
|
58
|
+
}
|
|
59
|
+
decisions.sort((a, b) => a.id.localeCompare(b.id));
|
|
60
|
+
return decisions;
|
|
61
|
+
}
|
|
62
|
+
export async function appendDecisionLedgerEntry(rootInput, record) {
|
|
63
|
+
const root = resolveRepoRoot(rootInput);
|
|
64
|
+
const dir = path.dirname(ledgerFile(root));
|
|
65
|
+
await fs.mkdir(dir, { recursive: true });
|
|
66
|
+
const full = {
|
|
67
|
+
...record,
|
|
68
|
+
source: record.source ?? "ledger",
|
|
69
|
+
truth: record.truth ?? "VERIFIED",
|
|
70
|
+
recordedAt: record.recordedAt ?? new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
await fs.appendFile(ledgerFile(root), `${JSON.stringify(full)}\n`, "utf8");
|
|
73
|
+
return full;
|
|
74
|
+
}
|
|
75
|
+
export async function loadDecisionLedger(rootInput) {
|
|
76
|
+
const root = resolveRepoRoot(rootInput);
|
|
77
|
+
const limitations = [
|
|
78
|
+
"ADR parsing reads markdown titles and Status lines only.",
|
|
79
|
+
"Ledger JSONL entries are merged with discovered ADR files (deduped by id).",
|
|
80
|
+
];
|
|
81
|
+
const fromAdr = await parseAdrDecisions(root);
|
|
82
|
+
const fromLedger = [];
|
|
83
|
+
const file = ledgerFile(root);
|
|
84
|
+
if (await pathExists(file)) {
|
|
85
|
+
const text = await fs.readFile(file, "utf8");
|
|
86
|
+
for (const line of text.split(/\r?\n/)) {
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
fromLedger.push(JSON.parse(line));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
limitations.push("Skipped malformed decision ledger line.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const byId = new Map();
|
|
98
|
+
for (const d of [...fromAdr, ...fromLedger]) {
|
|
99
|
+
byId.set(d.id, d);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
root,
|
|
103
|
+
decisions: [...byId.values()].sort((a, b) => a.title.localeCompare(b.title)),
|
|
104
|
+
ledgerPath: ledgerFile(root),
|
|
105
|
+
limitations,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function resolveDecisionPath(rootInput, candidate) {
|
|
109
|
+
return resolveSafeRepoPath(rootInput, candidate);
|
|
110
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
import { type LockfilePackageVersion } from "./lockfiles.js";
|
|
3
|
+
export interface DirectDependency {
|
|
4
|
+
name: string;
|
|
5
|
+
versionRange: string;
|
|
6
|
+
packageJsonPath: string;
|
|
7
|
+
truth: TruthLabel;
|
|
8
|
+
evidence: ProductEvidence[];
|
|
9
|
+
}
|
|
10
|
+
export interface DuplicatePackageName {
|
|
11
|
+
name: string;
|
|
12
|
+
paths: string[];
|
|
13
|
+
truth: TruthLabel;
|
|
14
|
+
}
|
|
15
|
+
export interface UpgradeImpactHint {
|
|
16
|
+
dependency: string;
|
|
17
|
+
importers: string[];
|
|
18
|
+
truth: TruthLabel;
|
|
19
|
+
}
|
|
20
|
+
export interface DependencyAnalysisReport {
|
|
21
|
+
root: string;
|
|
22
|
+
monorepo: string;
|
|
23
|
+
directDependencies: DirectDependency[];
|
|
24
|
+
duplicateNames: DuplicatePackageName[];
|
|
25
|
+
lockfilesPresent: string[];
|
|
26
|
+
lockfilesParsed: string[];
|
|
27
|
+
transitiveFromLockfile: LockfilePackageVersion[];
|
|
28
|
+
upgradeImpact: UpgradeImpactHint[];
|
|
29
|
+
limitations: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function analyzeDependencies(rootInput: string, options?: {
|
|
32
|
+
graph?: {
|
|
33
|
+
nodes: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
kind: string;
|
|
36
|
+
label: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
}>;
|
|
39
|
+
edges: Array<{
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
kind: string;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
maxFileSizeBytes?: number;
|
|
46
|
+
}): Promise<DependencyAnalysisReport>;
|
|
@@ -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,42 @@
|
|
|
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
|
+
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
30
|
+
* or unbounded parent trees.
|
|
31
|
+
*/
|
|
32
|
+
export declare function discoverProjectRoots(options?: DiscoverProjectOptions): Promise<ProjectDiscoveryReport>;
|
|
33
|
+
export declare function resolveStartedProjectRoot(pathArg: string | undefined, options?: {
|
|
34
|
+
maxEntries?: number;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
ok: true;
|
|
37
|
+
root: string;
|
|
38
|
+
discovery: ProjectDiscoveryReport;
|
|
39
|
+
} | {
|
|
40
|
+
ok: false;
|
|
41
|
+
discovery: ProjectDiscoveryReport;
|
|
42
|
+
}>;
|