@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.
@@ -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
- }
@@ -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
- }
@@ -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";