@chimix/agentscan 0.1.0 → 0.2.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/README.md +27 -17
- package/dist/cli.js +13128 -0
- package/package.json +7 -5
- package/src/analyze.ts +0 -82
- package/src/checks/budgets.ts +0 -189
- package/src/checks/index.ts +0 -738
- package/src/checks/make.ts +0 -37
- package/src/cli.ts +0 -230
- package/src/commands/check.ts +0 -64
- package/src/commands/explain.ts +0 -64
- package/src/commands/init.ts +0 -42
- package/src/commands/rules.ts +0 -36
- package/src/config/load.ts +0 -26
- package/src/config/schema.ts +0 -63
- package/src/discover/index.ts +0 -888
- package/src/facts/extract.ts +0 -273
- package/src/facts/semver.ts +0 -41
- package/src/facts/types.ts +0 -113
- package/src/report/clipboard.ts +0 -90
- package/src/report/exit-code.ts +0 -36
- package/src/report/json.ts +0 -36
- package/src/report/prompt.ts +0 -102
- package/src/report/safe.ts +0 -0
- package/src/report/score.ts +0 -47
- package/src/report/sort.ts +0 -26
- package/src/report/text.ts +0 -142
- package/src/version.ts +0 -1
package/src/report/score.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Finding } from "../facts/types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Points deducted per finding. Published deliberately — see the note below.
|
|
5
|
-
*/
|
|
6
|
-
export const ERROR_COST = 10;
|
|
7
|
-
export const WARNING_COST = 3;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A 0–100 score: start at 100, deduct per finding by severity, floor at zero.
|
|
11
|
-
*
|
|
12
|
-
* score = max(0, 100 - 10 x errors - 3 x warnings)
|
|
13
|
-
*
|
|
14
|
-
* **Deduction, not coverage.** The obvious model — passed checks over total
|
|
15
|
-
* checks — was measured against 17 real projects and rejected: every one landed
|
|
16
|
-
* between 97.7% and 100%, and it *inverted* severity, because the denominator
|
|
17
|
-
* grows with the config while the defects do not. A project with 85 skills and
|
|
18
|
-
* one warning outscored a project with 10 skills and one warning, for the same
|
|
19
|
-
* defect. Deduction has no denominator, so it cannot do that. Verified on the
|
|
20
|
-
* same 17 projects: 40 to 100, and the 85-skill project scores 97 while the
|
|
21
|
-
* 54-skill one with six broken hooks scores 40.
|
|
22
|
-
*
|
|
23
|
-
* **Info costs nothing.** Info findings are budgets and hygiene notes, and they
|
|
24
|
-
* do correlate with project size — charging for them would reintroduce exactly
|
|
25
|
-
* the inversion above. Measured: including them at one point each pulled a
|
|
26
|
-
* healthy 51-skill project from 94 down to 84 purely for having more config.
|
|
27
|
-
*
|
|
28
|
-
* **The formula is published.** Both comparable tools score 0–100 and neither
|
|
29
|
-
* documents how. A number whose derivation cannot be inspected is the false
|
|
30
|
-
* precision this scoring model exists to avoid; anyone can recompute this one
|
|
31
|
-
* by counting two lines of the report.
|
|
32
|
-
*
|
|
33
|
-
* The score is a summary, not a verdict. `--fail-on <severity>` remains the
|
|
34
|
-
* precise gate: it says which *kind* of problem fails a build, where a
|
|
35
|
-
* threshold only says how many points of unspecified trouble is too much.
|
|
36
|
-
*/
|
|
37
|
-
export function score(findings: Finding[]): number {
|
|
38
|
-
let deduction = 0;
|
|
39
|
-
for (const f of findings) {
|
|
40
|
-
if (f.severity === "error") {
|
|
41
|
-
deduction += ERROR_COST;
|
|
42
|
-
} else if (f.severity === "warning") {
|
|
43
|
-
deduction += WARNING_COST;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return Math.max(0, 100 - deduction);
|
|
47
|
-
}
|
package/src/report/sort.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { Action, Finding, Severity } from "../facts/types";
|
|
2
|
-
|
|
3
|
-
const SEVERITY_RANK: Record<Severity, number> = {
|
|
4
|
-
error: 0,
|
|
5
|
-
warning: 1,
|
|
6
|
-
info: 2,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
/** Deterministic: severity error>warning>info, then action, then id */
|
|
10
|
-
export function sortFindings(findings: Finding[]): Finding[] {
|
|
11
|
-
return [...findings].sort((a, b) => {
|
|
12
|
-
const sev = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
13
|
-
if (sev !== 0) {
|
|
14
|
-
return sev;
|
|
15
|
-
}
|
|
16
|
-
const act = compareAction(a.action, b.action);
|
|
17
|
-
if (act !== 0) {
|
|
18
|
-
return act;
|
|
19
|
-
}
|
|
20
|
-
return a.id.localeCompare(b.id);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function compareAction(a: Action, b: Action): number {
|
|
25
|
-
return a.localeCompare(b);
|
|
26
|
-
}
|
package/src/report/text.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import type { Action, Facts, Finding } from "../facts/types";
|
|
3
|
-
import { safe } from "./safe";
|
|
4
|
-
import { score } from "./score";
|
|
5
|
-
import { sortFindings } from "./sort";
|
|
6
|
-
|
|
7
|
-
const ACTION_LABEL: Record<Action, string> = {
|
|
8
|
-
keep: "KEEP",
|
|
9
|
-
delete: "DELETE",
|
|
10
|
-
add: "ADD",
|
|
11
|
-
refresh: "REFRESH",
|
|
12
|
-
warn: "WARN",
|
|
13
|
-
drift: "DRIFT",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/** Left-pad action label to 7 chars for column alignment (DELETE = 6). */
|
|
17
|
-
function actionColumn(action: Action): string {
|
|
18
|
-
return ACTION_LABEL[action].padEnd(7, " ");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function renderText(args: {
|
|
22
|
-
version: string;
|
|
23
|
-
facts: Facts;
|
|
24
|
-
findings: Finding[];
|
|
25
|
-
resolvedFrom?: string;
|
|
26
|
-
verbose: boolean;
|
|
27
|
-
quiet: boolean;
|
|
28
|
-
}): string {
|
|
29
|
-
const { version, facts, verbose, quiet } = args;
|
|
30
|
-
const sorted = sortFindings(args.findings);
|
|
31
|
-
|
|
32
|
-
// Default / quiet: hide KEEP and info severity. Verbose: full listing.
|
|
33
|
-
const visible = verbose
|
|
34
|
-
? sorted
|
|
35
|
-
: sorted.filter((f) => f.action !== "keep" && f.severity !== "info");
|
|
36
|
-
|
|
37
|
-
const summary = formatSummary(sorted, verbose);
|
|
38
|
-
|
|
39
|
-
if (quiet) {
|
|
40
|
-
return `${summary}\n`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const lines: string[] = [];
|
|
44
|
-
const name = projectLabel(facts.root);
|
|
45
|
-
lines.push(`agentscan v${version} — ${safe(name)}`);
|
|
46
|
-
if (args.resolvedFrom !== undefined) {
|
|
47
|
-
// The scan silently targeting an ancestor is worse than it refusing to run.
|
|
48
|
-
lines.push(
|
|
49
|
-
` no package.json in ${safe(args.resolvedFrom)} — scanned ${safe(facts.root)}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
lines.push("");
|
|
53
|
-
lines.push(`Stack: ${formatStack(facts)}`);
|
|
54
|
-
lines.push("");
|
|
55
|
-
|
|
56
|
-
for (const f of visible) {
|
|
57
|
-
lines.push(...formatFinding(f));
|
|
58
|
-
lines.push("");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
lines.push(summary);
|
|
62
|
-
lines.push("");
|
|
63
|
-
return lines.join("\n");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function projectLabel(root: string): string {
|
|
67
|
-
const base = basename(root);
|
|
68
|
-
return base.length > 0 ? base : root;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Orientation only — which project is this and how big. Listing dependencies
|
|
73
|
-
* here told the reader nothing about their agent config, and a real app has
|
|
74
|
-
* 100+ of them.
|
|
75
|
-
*/
|
|
76
|
-
function formatStack(facts: Facts): string {
|
|
77
|
-
const deps =
|
|
78
|
-
Object.keys(facts.dependencies).length +
|
|
79
|
-
Object.keys(facts.devDependencies).length;
|
|
80
|
-
const global = facts.skills.filter((s) => s.source === "global").length;
|
|
81
|
-
const skills =
|
|
82
|
-
global === 0
|
|
83
|
-
? `${facts.skills.length} skills`
|
|
84
|
-
: `${facts.skills.length} skills (${facts.skills.length - global} project + ${global} global)`;
|
|
85
|
-
return [
|
|
86
|
-
`${deps} deps`,
|
|
87
|
-
skills,
|
|
88
|
-
`${facts.mcp.length} mcp`,
|
|
89
|
-
`${facts.agents.length} agents`,
|
|
90
|
-
`packageManager=${facts.packageManager}`,
|
|
91
|
-
].join(" · ");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function formatFinding(f: Finding): string[] {
|
|
95
|
-
const out: string[] = [];
|
|
96
|
-
out.push(`${actionColumn(f.action)} ${safe(f.subject)}`);
|
|
97
|
-
out.push(` rule:${safe(f.ruleId)}`);
|
|
98
|
-
out.push(` ${safe(f.message)}`);
|
|
99
|
-
if (f.evidence.length > 0) {
|
|
100
|
-
const ev = f.evidence
|
|
101
|
-
.map((e) => `${safe(e.kind)} ${safe(e.value)}`)
|
|
102
|
-
.join(" · ");
|
|
103
|
-
out.push(` evidence: ${ev}`);
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function formatSummary(findings: Finding[], verbose: boolean): string {
|
|
109
|
-
const counts: Record<Action, number> = {
|
|
110
|
-
keep: 0,
|
|
111
|
-
delete: 0,
|
|
112
|
-
add: 0,
|
|
113
|
-
refresh: 0,
|
|
114
|
-
warn: 0,
|
|
115
|
-
drift: 0,
|
|
116
|
-
};
|
|
117
|
-
let infoHidden = 0;
|
|
118
|
-
for (const f of findings) {
|
|
119
|
-
if (!verbose && f.severity === "info") {
|
|
120
|
-
infoHidden += 1;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
counts[f.action] += 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Only non-zero actions — most rule sets never emit most actions, and a row of
|
|
127
|
-
// permanent zeros buried the one number that moved.
|
|
128
|
-
const order: Action[] = ["delete", "add", "refresh", "drift", "warn", "keep"];
|
|
129
|
-
const parts = order
|
|
130
|
-
.filter((action) => counts[action] > 0)
|
|
131
|
-
.map((action) => `${counts[action]} ${action}`);
|
|
132
|
-
|
|
133
|
-
if (!verbose && infoHidden > 0) {
|
|
134
|
-
parts.push(`${infoHidden} info hidden (--verbose)`);
|
|
135
|
-
}
|
|
136
|
-
const points = score(findings);
|
|
137
|
-
if (parts.length === 0) {
|
|
138
|
-
return `Summary: no findings · score ${points}/100`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return `Summary: ${parts.join(" · ")} · score ${points}/100`;
|
|
142
|
-
}
|
package/src/version.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const VERSION = "0.1.0";
|