@bigsteele/the-prospect 0.1.1 → 0.3.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 +40 -17
- package/dist/check.js +3 -2
- package/dist/cli.js +116 -28
- package/dist/decisions.d.ts +93 -0
- package/dist/decisions.js +143 -0
- package/dist/detect/costs.js +50 -8
- package/dist/detect/database.d.ts +28 -0
- package/dist/detect/database.js +198 -0
- package/dist/detect/deadweight.js +60 -15
- package/dist/detect/deps.js +56 -0
- package/dist/detect/duplication.js +25 -2
- package/dist/detect/handrolled.js +84 -5
- package/dist/detect/stack.d.ts +8 -0
- package/dist/detect/stack.js +10 -2
- package/dist/detect/types.d.ts +76 -0
- package/dist/detect/vendors.js +108 -8
- package/dist/index.d.ts +15 -2
- package/dist/index.js +88 -2
- package/dist/northstar.js +15 -2
- package/dist/profile.d.ts +55 -0
- package/dist/profile.js +106 -0
- package/dist/report.js +183 -24
- package/dist/score.d.ts +3 -0
- package/dist/score.js +30 -9
- package/dist/verdicts.d.ts +80 -0
- package/dist/verdicts.js +144 -0
- package/dist/walk.d.ts +85 -1
- package/dist/walk.js +189 -5
- package/package.json +1 -1
- package/prompt/THE-PROSPECT.md +162 -118
package/dist/report.js
CHANGED
|
@@ -17,13 +17,17 @@ export function toMarkdown(p) {
|
|
|
17
17
|
const accidental = p.duplicates.filter((d) => !d.deliberate);
|
|
18
18
|
const highHand = p.handrolled.filter((h) => h.confidence === "high");
|
|
19
19
|
const cuts = p.stack.consolidations;
|
|
20
|
+
// A category is not a job: pairs the code shows doing different work are
|
|
21
|
+
// listed under their own heading and never counted as paid twice.
|
|
22
|
+
const paidTwice = p.overlaps.filter((o) => !o.distinct);
|
|
23
|
+
const twoJobs = p.overlaps.filter((o) => o.distinct);
|
|
20
24
|
L.push(`# The Prospect: ${p.app}`, "");
|
|
21
25
|
// The headline is an argument, not a metric.
|
|
22
26
|
const claims = [];
|
|
23
27
|
if (noRef.length)
|
|
24
28
|
claims.push(`**${noRef.length} of ${p.totals.runtime_deps}** dependencies show no reference anywhere`);
|
|
25
|
-
if (
|
|
26
|
-
claims.push(`**${
|
|
29
|
+
if (paidTwice.length)
|
|
30
|
+
claims.push(`**${paidTwice.length}** job${paidTwice.length > 1 ? "s are" : " is"} paid for twice`);
|
|
27
31
|
if (highHand.length)
|
|
28
32
|
claims.push(`**${highHand.length}** subsystem${highHand.length > 1 ? "s" : ""} built by hand where the market sells a rail`);
|
|
29
33
|
if (cuts.length)
|
|
@@ -38,29 +42,123 @@ export function toMarkdown(p) {
|
|
|
38
42
|
else {
|
|
39
43
|
L.push(claims.slice(0, 2).join(", and ") + ".", "");
|
|
40
44
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// NEXT ACTIONS, NOT TODAY/AFTER (0.2). The Big Sean ends every finding with
|
|
46
|
+
// the workflow it protects, who does it, and how long; the Today/After table
|
|
47
|
+
// ended with "Suggestions arrive as opinions". A reader acts on the first
|
|
48
|
+
// shape and skims the second. Only what is NOT on record reaches this list.
|
|
49
|
+
const actions = [];
|
|
50
|
+
for (const d of noRef.filter((x) => !x.on_record).slice(0, 3)) {
|
|
51
|
+
actions.push({
|
|
52
|
+
what: `\`${d.name}\` is declared in \`${d.manifest}\` and nothing references it`,
|
|
53
|
+
touches: d.touches ?? "",
|
|
54
|
+
task: `grep the repository for its name outside \`${d.manifest}\`; if nothing loads it by name at runtime, remove it`,
|
|
55
|
+
retest: "install, build and run the test suite with it gone",
|
|
56
|
+
});
|
|
49
57
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
for (const o of paidTwice.filter((x) => !x.on_record)) {
|
|
59
|
+
actions.push({
|
|
60
|
+
what: `${prose(o.services)} both do ${label(o.category)} work`,
|
|
61
|
+
touches: "one bill and one failure surface per vendor",
|
|
62
|
+
task: `name which one carries this job, or record in DECISIONS.md why both stay`,
|
|
63
|
+
retest: "the next scan lists the pair under On record",
|
|
64
|
+
});
|
|
53
65
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
for (const h of highHand.filter((x) => !x.on_record)) {
|
|
67
|
+
actions.push({
|
|
68
|
+
what: `${h.loc} lines of hand-rolled ${h.rail} in \`${h.signal.file}\``,
|
|
69
|
+
touches: h.touches ?? "",
|
|
70
|
+
task: "decide once: keep it on purpose and record why, or let a rail carry it (Lane 2 names the rails)",
|
|
71
|
+
retest: "the decision appears in the record, or the subsystem is gone",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const bigAccidental = accidental.filter((d) => !d.parallel && !d.on_record).slice(0, 2);
|
|
75
|
+
for (const d of bigAccidental) {
|
|
76
|
+
actions.push({
|
|
77
|
+
what: `${d.lines} lines repeated across ${d.files.length} files, opening \`${d.opens_with.slice(0, 60)}\``,
|
|
78
|
+
touches: d.touches ?? "",
|
|
79
|
+
task: "lift to one place, or mark the copy deliberate in its header",
|
|
80
|
+
retest: "the cluster is gone from the next scan",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (actions.length) {
|
|
84
|
+
L.push(`## Your next actions`, "");
|
|
85
|
+
L.push(`Only what no recorded decision explains. Each names the workflow it touches - by path, a heuristic - and how to know it is done.`, "");
|
|
86
|
+
actions.forEach((a, i) => {
|
|
87
|
+
L.push(`### ${i + 1}. ${a.what}`);
|
|
88
|
+
L.push(`**Touches.** ${a.touches}`);
|
|
89
|
+
L.push(`**Task.** ${a.task}`);
|
|
90
|
+
L.push(`**Retest.** ${a.retest}`, "");
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// ON RECORD. What the scan found and the record already explains. Listed so
|
|
94
|
+
// the reader sees the tool looked, and charged nothing.
|
|
95
|
+
const onRecord = [];
|
|
96
|
+
for (const d of noRef.filter((x) => x.on_record))
|
|
97
|
+
onRecord.push({ what: `\`${d.name}\` shows no reference`, where: `${d.on_record.file}:${d.on_record.line} - ${d.on_record.excerpt}` });
|
|
98
|
+
for (const o of paidTwice.filter((x) => x.on_record))
|
|
99
|
+
onRecord.push({ what: `${prose(o.services)} both do ${label(o.category)} work`, where: `${o.on_record.file}:${o.on_record.line} - ${o.on_record.excerpt}` });
|
|
100
|
+
for (const c of cuts.filter((x) => x.on_record))
|
|
101
|
+
onRecord.push({ what: `${c.candidate} beside ${c.keep}`, where: `${c.on_record.file}:${c.on_record.line} - ${c.on_record.excerpt}` });
|
|
102
|
+
for (const h of highHand.filter((x) => x.on_record))
|
|
103
|
+
onRecord.push({ what: `hand-rolled ${h.rail}`, where: `${h.on_record.file}:${h.on_record.line} - ${h.on_record.excerpt}` });
|
|
104
|
+
for (const d of accidental.filter((x) => x.on_record))
|
|
105
|
+
onRecord.push({ what: `${d.lines} lines shared by ${d.files.map((f) => f.split("/").pop()).join(", ")}`, where: `${d.on_record.file}:${d.on_record.line} - ${d.on_record.excerpt}` });
|
|
106
|
+
const parallels = p.duplicates.filter((d) => d.parallel);
|
|
107
|
+
if (onRecord.length || parallels.length) {
|
|
108
|
+
L.push(`## On record`, "");
|
|
109
|
+
// Say what the record EXPLAINED, not what was read. An empty table under "already
|
|
110
|
+
// explained by CHECKPOINT-QUEUE.md" told the owner their notes covered the findings
|
|
111
|
+
// when they did not - and the gap between what a record holds and what the scan
|
|
112
|
+
// found is itself the useful fact here.
|
|
113
|
+
const explainedBy = [...new Set(onRecord.map((r) => r.where.split(":")[0]))];
|
|
114
|
+
L.push(onRecord.length
|
|
115
|
+
? `Found, and already explained by ${explainedBy.slice(0, 3).map((f) => `\`${f}\``).join(", ")}. Nothing here costs points.`
|
|
116
|
+
: `${p.decisions.files.length} decision file(s) read, ${p.decisions.entries} lines. None explains a finding above: what the scan found is not yet on the record, which is the case for a line in DECISIONS.md rather than a fix.`, "");
|
|
117
|
+
if (onRecord.length) {
|
|
118
|
+
L.push(`| Finding | Where it is decided |`, `| --- | --- |`);
|
|
119
|
+
for (const r of onRecord)
|
|
120
|
+
L.push(`| ${r.what} | ${r.where.replace(/\|/g, "\\|")} |`);
|
|
121
|
+
L.push("");
|
|
122
|
+
}
|
|
123
|
+
if (parallels.length) {
|
|
124
|
+
const total = parallels.reduce((t, d) => t + d.lines * (d.files.length - 1), 0);
|
|
125
|
+
L.push(`${parallels.length} parallel adapter block(s), ${total} lines in total, at the same relative path under sibling adapter directories. A generated file cannot import from the generator, so these duplicate by design; the one question is whether the generator could render them from a single partial.`, "");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// TWO VENDORS, TWO JOBS. A category shared is not a bill doubled; the code
|
|
129
|
+
// showed each doing different work. Listed so the reader sees the pair was
|
|
130
|
+
// weighed, charged nothing, and asked to be written down once.
|
|
131
|
+
if (twoJobs.length) {
|
|
132
|
+
L.push(`## Two vendors, two jobs`, "");
|
|
133
|
+
L.push(`Same category, different work by the look of the code. Not charged. A line in DECISIONS.md naming both keeps the next scan from asking.`, "");
|
|
134
|
+
for (const o of twoJobs)
|
|
135
|
+
L.push(`- **${label(o.category)}**: ${o.services.join(" + ")}. ${o.distinct}${o.on_record ? ` On record: ${o.on_record.file}:${o.on_record.line}.` : ""}`);
|
|
136
|
+
L.push("");
|
|
137
|
+
}
|
|
138
|
+
// UNKNOWN. What the scan could not settle, stated rather than scored.
|
|
139
|
+
const unknowns = [];
|
|
140
|
+
if (p.north_star.confidence !== "high")
|
|
141
|
+
unknowns.push(`the North Star: ${p.north_star.note}`);
|
|
142
|
+
if (!p.decisions.files.length)
|
|
143
|
+
unknowns.push("whether any of the above is on purpose: no decision record was found (DECISIONS.md, ADRs, CLAUDE.md), so nothing could be marked on record");
|
|
144
|
+
for (const n of p.profile.not_applicable)
|
|
145
|
+
unknowns.push(`${n.question}: ${n.why}`);
|
|
146
|
+
if (unknowns.length) {
|
|
147
|
+
L.push(`## Unknown`, "");
|
|
148
|
+
for (const u of unknowns)
|
|
149
|
+
L.push(`- ${u}`);
|
|
150
|
+
L.push("");
|
|
57
151
|
}
|
|
58
|
-
if (p.dead.length)
|
|
59
|
-
L.push(`| ${p.dead.length} files everyone reads, searches and ships but nothing runs | Deleted, with this report as the receipt |`);
|
|
60
|
-
L.push(`| Suggestions arrive as opinions | Every suggestion stands on a fact in your code, a sourced fact from your market, and your North Star |`);
|
|
61
|
-
L.push("");
|
|
62
152
|
L.push(`## Score`, "");
|
|
63
153
|
L.push(`**${p.score.total}/100 (${p.score.grade})** - Level ${p.score.level.n}: **${p.score.level.name}**. ${p.score.level.meaning}`, "");
|
|
154
|
+
// THE NUMBER NEVER TRAVELS ALONE (0.2). 96/100 on a six-file scraper and
|
|
155
|
+
// 96/100 on a four-thousand-file monorepo are not the same claim, and the
|
|
156
|
+
// report used to print them identically.
|
|
157
|
+
L.push(`_${p.profile.depth.why}_`, "");
|
|
158
|
+
if (p.score.not_asked.length) {
|
|
159
|
+
L.push(`**Not asked of this repository:** ${p.score.not_asked.join(", ")}. ` +
|
|
160
|
+
`Neither credited nor penalised - see What was read.`, "");
|
|
161
|
+
}
|
|
64
162
|
for (const f of p.score.floors)
|
|
65
163
|
L.push(`- ${f}`);
|
|
66
164
|
if (p.score.floors.length)
|
|
@@ -72,6 +170,65 @@ export function toMarkdown(p) {
|
|
|
72
170
|
L.push(`| ${d.what} | ${d.points} | ${d.evidence} |`);
|
|
73
171
|
L.push("");
|
|
74
172
|
}
|
|
173
|
+
// THE LEDGER GOES BEFORE THE FINDINGS (0.2). A reader deciding how much to
|
|
174
|
+
// trust a list of findings needs to know what was looked at to produce it, and
|
|
175
|
+
// the honest answer used to be unavailable: the scan read 473 of 2,077 files
|
|
176
|
+
// on the first repository it met and had no way to say so.
|
|
177
|
+
const c = p.coverage;
|
|
178
|
+
L.push(`## What was read`, "");
|
|
179
|
+
const pr = p.profile;
|
|
180
|
+
L.push(`A ${pr.languages[0] ?? "mixed"} repository` +
|
|
181
|
+
(pr.languages.length > 1 ? ` (also ${pr.languages.slice(1, 3).join(", ")})` : "") +
|
|
182
|
+
(pr.manifests.length ? `, declaring dependencies in ${pr.manifests.join(" and ")}` : `, with no dependency manifest found`) +
|
|
183
|
+
(pr.traits.length ? `. Shapes recognised: ${pr.traits.join(", ")}.` : "."), "");
|
|
184
|
+
if (pr.not_applicable.length) {
|
|
185
|
+
L.push(`Questions with no ground to stand on here:`, "");
|
|
186
|
+
for (const n of pr.not_applicable)
|
|
187
|
+
L.push(`- **${n.question}** - ${n.why}`);
|
|
188
|
+
L.push("");
|
|
189
|
+
}
|
|
190
|
+
L.push(`**${c.analysed} of ${c.walked} files analysed.** ` +
|
|
191
|
+
`${c.unreadable} unreadable (binaries and files over the size cap), ` +
|
|
192
|
+
`${c.walked - c.analysed - c.unreadable} excluded by a named rule, ` +
|
|
193
|
+
`${c.unaccounted} unaccounted for.`, "");
|
|
194
|
+
if (c.excluded.length) {
|
|
195
|
+
L.push(`| Excluded | Files | Why |`);
|
|
196
|
+
L.push(`| --- | --- | --- |`);
|
|
197
|
+
for (const e of c.excluded)
|
|
198
|
+
L.push(`| \`${e.rule}\` | ${e.files} | ${e.why} |`);
|
|
199
|
+
L.push("");
|
|
200
|
+
}
|
|
201
|
+
if (c.unclaimed.length) {
|
|
202
|
+
L.push(`Readable file classes no question claims. Some of these are right to ignore; ` +
|
|
203
|
+
`the list is here so the choice is visible rather than assumed.`, "");
|
|
204
|
+
for (const u of c.unclaimed) {
|
|
205
|
+
L.push(`- \`${u.ext}\` - ${u.files} files, none analysed (e.g. \`${u.examples[0] ?? ""}\`)`);
|
|
206
|
+
}
|
|
207
|
+
L.push("");
|
|
208
|
+
}
|
|
209
|
+
const db = p.database;
|
|
210
|
+
if (db.files > 0) {
|
|
211
|
+
L.push(`### The database`, "");
|
|
212
|
+
L.push(`${db.files} migration file(s): ${db.tables} table(s), ${db.policies} policy/policies, ` +
|
|
213
|
+
`${db.definer_functions} function(s) running as definer` +
|
|
214
|
+
(db.definer_execute_revoked ? `, ${db.definer_execute_revoked} of them with EXECUTE revoked from public, anon or authenticated in the migrations` : "") +
|
|
215
|
+
`. ` +
|
|
216
|
+
(db.guard ? `The repository carries \`${db.guard}\`, which checks the live grants; this scan reads only what the migrations state. ` : "") +
|
|
217
|
+
(db.findings.length === 0
|
|
218
|
+
? `Nothing below stood out.`
|
|
219
|
+
: `${db.findings.length} shape(s) worth a minute.`), "");
|
|
220
|
+
if (db.findings.length) {
|
|
221
|
+
L.push(`| Shape | Subject | Where |`);
|
|
222
|
+
L.push(`| --- | --- | --- |`);
|
|
223
|
+
for (const x of db.findings.slice(0, 15)) {
|
|
224
|
+
L.push(`| ${x.kind.replace(/_/g, " ")} | \`${x.subject}\` | \`${x.file}\` |`);
|
|
225
|
+
}
|
|
226
|
+
if (db.findings.length > 15)
|
|
227
|
+
L.push(`| ...and ${db.findings.length - 15} more | | in the JSON |`);
|
|
228
|
+
L.push("");
|
|
229
|
+
L.push(db.findings[0].note, "");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
75
232
|
L.push(`## North Star`, "");
|
|
76
233
|
if (p.north_star.sentence) {
|
|
77
234
|
L.push(`> ${p.north_star.sentence}`, "");
|
|
@@ -93,9 +250,9 @@ export function toMarkdown(p) {
|
|
|
93
250
|
L.push("");
|
|
94
251
|
L.push(`A package can earn its keep without an import (a CLI run from a script, a plugin loaded by name at runtime). This table is the list to answer for, not a kill list.`, "");
|
|
95
252
|
}
|
|
96
|
-
if (
|
|
253
|
+
if (paidTwice.length) {
|
|
97
254
|
L.push(`### Jobs paid for twice`, "");
|
|
98
|
-
for (const o of
|
|
255
|
+
for (const o of paidTwice) {
|
|
99
256
|
L.push(`- **${label(o.category)}**: ${o.services.join(" + ")} (${o.call_sites} call sites). One category of work, ${o.services.length} vendors, ${o.services.length} invoices, ${o.services.length} places a key can leak.`);
|
|
100
257
|
}
|
|
101
258
|
L.push("");
|
|
@@ -138,9 +295,11 @@ export function toMarkdown(p) {
|
|
|
138
295
|
}
|
|
139
296
|
if (p.dead.length) {
|
|
140
297
|
L.push(`### Files no entrypoint reaches`, "");
|
|
141
|
-
|
|
298
|
+
const scaffold = p.dead.filter((d) => d.scaffold).length;
|
|
299
|
+
L.push(`From ${p.totals.entrypoints} entrypoints, the import walk never arrived at these, and no reached file names them by path. A file loaded by a path built at runtime can still be alive - this is the list to answer for.` +
|
|
300
|
+
(scaffold ? ` ${scaffold} are UI-kit scaffold components (a components.json sits beside them) that no file imports: never bundled, only read and searched, and weighed a quarter.` : ""), "");
|
|
142
301
|
for (const d of p.dead.slice(0, 20))
|
|
143
|
-
L.push(`- \`${d.file}\` (${d.loc} lines)`);
|
|
302
|
+
L.push(`- \`${d.file}\` (${d.loc} lines${d.scaffold ? ", scaffold" : ""})`);
|
|
144
303
|
if (p.dead.length > 20)
|
|
145
304
|
L.push(`- …and ${p.dead.length - 20} more in the JSON`);
|
|
146
305
|
L.push("");
|
package/dist/score.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface ProspectScore {
|
|
|
28
28
|
evidence: string;
|
|
29
29
|
}>;
|
|
30
30
|
floors: string[];
|
|
31
|
+
not_asked: string[];
|
|
31
32
|
}
|
|
32
33
|
export declare function grade(total: number): string;
|
|
33
34
|
export interface ScoreInput {
|
|
@@ -41,6 +42,8 @@ export interface ScoreInput {
|
|
|
41
42
|
consolidations?: ConsolidationFact[];
|
|
42
43
|
/** Total runtime files, for shares. */
|
|
43
44
|
runtime_files: number;
|
|
45
|
+
/** Questions the repository could not answer; carried through, never scored. */
|
|
46
|
+
not_asked?: string[];
|
|
44
47
|
/** True when a vendor SDK dependency itself has no reference found - paying for a thing never called. */
|
|
45
48
|
unused_paid_service: string | null;
|
|
46
49
|
}
|
package/dist/score.js
CHANGED
|
@@ -29,30 +29,49 @@ const LEVELS = [
|
|
|
29
29
|
{ n: 4, name: "Sharp", meaning: "Nothing unaccounted for: every dependency referenced, every file reached, one vendor per job, and every hand-rolled subsystem is a recorded decision." },
|
|
30
30
|
];
|
|
31
31
|
export function scoreProspect(input) {
|
|
32
|
+
// WHAT THE SCORE IS NOT (0.2). A number out of 100 invites the reading "how
|
|
33
|
+
// healthy is this repository", and across five repositories of different
|
|
34
|
+
// shapes it was measuring something closer to "how much is here". A six-file
|
|
35
|
+
// scraper with no manifest, no migrations and nothing duplicated scored 96,
|
|
36
|
+
// because every question it could not answer returned nothing found, and
|
|
37
|
+
// nothing found scored as clean.
|
|
38
|
+
//
|
|
39
|
+
// The score still measures only what was ASKED. What changes is that the
|
|
40
|
+
// report now carries the profile beside it, so 96 out of 100 on a thin
|
|
41
|
+
// reading cannot be mistaken for 96 on a thorough one - and `reading` says
|
|
42
|
+
// which it is, in the same breath as the number.
|
|
32
43
|
const deductions = [];
|
|
33
44
|
const ding = (what, points, evidence) => {
|
|
34
45
|
if (points > 0)
|
|
35
46
|
deductions.push({ what, points: Math.round(points * 10) / 10, evidence });
|
|
36
47
|
};
|
|
37
48
|
const runtimeDeps = input.deps.filter((d) => !d.dev);
|
|
38
|
-
|
|
49
|
+
// ON RECORD IS NOT A DEDUCTION (0.2). What the decision record explains was
|
|
50
|
+
// chosen and written down; charging for it is charging for the choice.
|
|
51
|
+
const noRef = runtimeDeps.filter((d) => d.no_reference_found && !d.on_record);
|
|
39
52
|
// Unreferenced runtime dependencies: up to 20.
|
|
40
53
|
ding("dependencies with no reference found", Math.min(20, noRef.length * 2), noRef.length ? `${noRef.length} of ${runtimeDeps.length} runtime dependencies` : "");
|
|
41
|
-
// Files no entrypoint reaches: up to 20, by share of runtime files.
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
// Files no entrypoint reaches: up to 20, by share of runtime files. A UI-kit
|
|
55
|
+
// scaffold component nothing imports is never bundled, so it weighs a quarter.
|
|
56
|
+
const dead = input.dead.filter((d) => !d.on_record);
|
|
57
|
+
const scaffold = dead.filter((d) => d.scaffold).length;
|
|
58
|
+
const deadWeight = dead.length - scaffold + scaffold / 4;
|
|
59
|
+
const deadShare = input.runtime_files > 0 ? deadWeight / input.runtime_files : 0;
|
|
60
|
+
ding("files no entrypoint reaches", Math.min(20, Math.round(deadShare * 100)), dead.length ? `${dead.length} files (${Math.round(deadShare * 100)} in 100${scaffold ? `; ${scaffold} are scaffold components, weighed a quarter` : ""})` : "");
|
|
44
61
|
// Accidental duplicate clusters: up to 20. Deliberate copies cost nothing.
|
|
45
|
-
const accidental = input.duplicates.filter((d) => !d.deliberate);
|
|
62
|
+
const accidental = input.duplicates.filter((d) => !d.deliberate && !d.parallel && !d.on_record);
|
|
46
63
|
const dupLines = accidental.reduce((t, d) => t + d.lines * (d.files.length - 1), 0);
|
|
47
64
|
ding("duplicated blocks not marked deliberate", Math.min(20, Math.round(dupLines / 40)), accidental.length ? `${accidental.length} clusters, ${dupLines} repeated lines` : "");
|
|
48
|
-
// Vendor overlap: up to 20. Two vendors
|
|
49
|
-
|
|
65
|
+
// Vendor overlap: up to 20. Two vendors doing ONE JOB is a doubled bill; two
|
|
66
|
+
// vendors the code shows doing different jobs inside a category is not (0.2.1).
|
|
67
|
+
const overlaps = input.overlaps.filter((o) => !o.on_record && !o.distinct);
|
|
68
|
+
ding("two or more vendors doing one job", Math.min(20, overlaps.length * 7), overlaps.length ? overlaps.map((o) => `${o.category}: ${o.services.join(" + ")}`).join("; ") : "");
|
|
50
69
|
// Hand-rolled where a rail exists: up to 12, high-confidence only.
|
|
51
|
-
const high = input.handrolled.filter((h) => h.confidence === "high");
|
|
70
|
+
const high = input.handrolled.filter((h) => h.confidence === "high" && !h.on_record);
|
|
52
71
|
ding("hand-rolled subsystems with rails available", Math.min(12, high.length * 4), high.length ? high.map((h) => h.rail).join(", ") : "");
|
|
53
72
|
// The stack cut: up to 12. A platform another platform already covers is
|
|
54
73
|
// a bill and a failure surface, and nobody decided to have both.
|
|
55
|
-
const consolidations = input.consolidations ?? [];
|
|
74
|
+
const consolidations = (input.consolidations ?? []).filter((c) => !c.on_record);
|
|
56
75
|
ding("platforms another platform already covers", Math.min(12, consolidations.length * 4), consolidations.length ? consolidations.map((c) => `${c.candidate} (covered by ${c.keep})`).join("; ") : "");
|
|
57
76
|
// Multiplying cost shapes: up to 8; per-row is the loud one.
|
|
58
77
|
const perRow = input.costs.filter((c) => c.shape === "per-row").length;
|
|
@@ -89,5 +108,7 @@ export function scoreProspect(input) {
|
|
|
89
108
|
level: LEVELS[n],
|
|
90
109
|
deductions: deductions.sort((a, b) => b.points - a.points),
|
|
91
110
|
floors,
|
|
111
|
+
/** Questions with no ground in this repository, so no points either way. */
|
|
112
|
+
not_asked: input.not_asked ?? [],
|
|
92
113
|
};
|
|
93
114
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verdicts: what the agent decided about every Step 0 finding, and the
|
|
3
|
+
* score recomputed from those decisions.
|
|
4
|
+
*
|
|
5
|
+
* Step 0 is a deterministic scan, and a deterministic scan is a hypothesis
|
|
6
|
+
* machine: it reads shapes and prints them. The protocol's first job is to
|
|
7
|
+
* take every finding to the code and rule on it, one of four ways -
|
|
8
|
+
* CONFIRMED with evidence, REFUTED with the reason the shape lied, ON_RECORD
|
|
9
|
+
* with the line that decided it, or UNKNOWN with what would settle it. A
|
|
10
|
+
* finding no verdict covers is a finding nobody checked, and the gate says so.
|
|
11
|
+
*
|
|
12
|
+
* The score then follows the verdicts, not the scan: a refuted finding costs
|
|
13
|
+
* nothing, a recorded one costs nothing, and the math is printed so the number
|
|
14
|
+
* can be argued with rather than trusted. Same rule as The Big Sean's
|
|
15
|
+
* validator: if this and the agent disagree, believe this and fix the file.
|
|
16
|
+
*
|
|
17
|
+
* PURE MODULE: no filesystem, no process. The CLI feeds it parsed JSON.
|
|
18
|
+
*/
|
|
19
|
+
import type { Prospect } from "./index.js";
|
|
20
|
+
import { type ProspectScore } from "./score.js";
|
|
21
|
+
export type Verdict = "CONFIRMED" | "REFUTED" | "ON_RECORD" | "UNKNOWN";
|
|
22
|
+
export interface VerdictRecord {
|
|
23
|
+
/** The finding's id from the scan JSON: dep:zod, overlap:ai, hand:search, dead:src/x.ts, cost:file:line, db:kind:subject, cut:Vercel, dup:1:file */
|
|
24
|
+
id: string;
|
|
25
|
+
verdict: Verdict;
|
|
26
|
+
/** Evidence ids (EV-nn) from the register. Required for CONFIRMED and REFUTED. */
|
|
27
|
+
evidence?: string[];
|
|
28
|
+
/** Why - the shape that lied, the record that decided, or what would settle it. */
|
|
29
|
+
reason?: string;
|
|
30
|
+
/** For ON_RECORD: the decision line, file:line. */
|
|
31
|
+
record?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface EvidenceEntry {
|
|
34
|
+
id: string;
|
|
35
|
+
/** What was done: the file read, the command run, the page loaded. */
|
|
36
|
+
what: string;
|
|
37
|
+
/** Where: file:line, a sanitised command, or a URL. */
|
|
38
|
+
where: string;
|
|
39
|
+
/** What was observed, in one line. Secrets and personal data never. */
|
|
40
|
+
observed: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Verdicts {
|
|
43
|
+
format: "bigsteele-prospect-verdicts/1";
|
|
44
|
+
/** The scan these verdicts rule on. */
|
|
45
|
+
scan?: string;
|
|
46
|
+
commit?: string;
|
|
47
|
+
findings: VerdictRecord[];
|
|
48
|
+
evidence: EvidenceEntry[];
|
|
49
|
+
}
|
|
50
|
+
/** Every finding id the scan carries, in the order the report prints them. */
|
|
51
|
+
export declare function findingIds(p: Prospect): string[];
|
|
52
|
+
export interface VerdictCheck {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
problems: string[];
|
|
55
|
+
counts: Record<Verdict, number> & {
|
|
56
|
+
total: number;
|
|
57
|
+
unruled: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate the verdicts against the scan they claim to rule on. Every scan
|
|
62
|
+
* finding needs a verdict; every CONFIRMED and REFUTED needs evidence that
|
|
63
|
+
* exists in the register; every UNKNOWN needs a reason; every ON_RECORD needs
|
|
64
|
+
* the line. A report text may be passed so that cited evidence is also
|
|
65
|
+
* required to appear in the report.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkVerdicts(p: Prospect, v: Verdicts, reportMd?: string): VerdictCheck;
|
|
68
|
+
export interface Rescore {
|
|
69
|
+
before: ProspectScore;
|
|
70
|
+
after: ProspectScore;
|
|
71
|
+
dropped: {
|
|
72
|
+
refuted: string[];
|
|
73
|
+
on_record: string[];
|
|
74
|
+
};
|
|
75
|
+
unknown: string[];
|
|
76
|
+
}
|
|
77
|
+
/** The score with refuted and recorded findings removed. UNKNOWN still counts: unproven is not innocent. */
|
|
78
|
+
export declare function rescore(p: Prospect, v: Verdicts): Rescore;
|
|
79
|
+
/** The math, printed. Nothing on the page is typed by hand. */
|
|
80
|
+
export declare function showMath(p: Prospect, v: Verdicts, r: Rescore, c: VerdictCheck): string;
|
package/dist/verdicts.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { scoreProspect } from "./score.js";
|
|
2
|
+
const VERDICTS = new Set(["CONFIRMED", "REFUTED", "ON_RECORD", "UNKNOWN"]);
|
|
3
|
+
/** Every finding id the scan carries, in the order the report prints them. */
|
|
4
|
+
export function findingIds(p) {
|
|
5
|
+
const ids = [];
|
|
6
|
+
for (const d of p.deps)
|
|
7
|
+
if (d.id)
|
|
8
|
+
ids.push(d.id);
|
|
9
|
+
for (const o of p.overlaps)
|
|
10
|
+
if (o.id)
|
|
11
|
+
ids.push(o.id);
|
|
12
|
+
for (const h of p.handrolled)
|
|
13
|
+
if (h.id)
|
|
14
|
+
ids.push(h.id);
|
|
15
|
+
for (const c of p.stack.consolidations)
|
|
16
|
+
if (c.id)
|
|
17
|
+
ids.push(c.id);
|
|
18
|
+
for (const d of p.duplicates)
|
|
19
|
+
if (d.id)
|
|
20
|
+
ids.push(d.id);
|
|
21
|
+
for (const d of p.dead)
|
|
22
|
+
if (d.id)
|
|
23
|
+
ids.push(d.id);
|
|
24
|
+
for (const c of p.cost_surfaces)
|
|
25
|
+
if (c.id)
|
|
26
|
+
ids.push(c.id);
|
|
27
|
+
for (const f of p.database.findings)
|
|
28
|
+
if (f.id)
|
|
29
|
+
ids.push(f.id);
|
|
30
|
+
return ids;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate the verdicts against the scan they claim to rule on. Every scan
|
|
34
|
+
* finding needs a verdict; every CONFIRMED and REFUTED needs evidence that
|
|
35
|
+
* exists in the register; every UNKNOWN needs a reason; every ON_RECORD needs
|
|
36
|
+
* the line. A report text may be passed so that cited evidence is also
|
|
37
|
+
* required to appear in the report.
|
|
38
|
+
*/
|
|
39
|
+
export function checkVerdicts(p, v, reportMd) {
|
|
40
|
+
const problems = [];
|
|
41
|
+
const counts = { CONFIRMED: 0, REFUTED: 0, ON_RECORD: 0, UNKNOWN: 0, total: 0, unruled: 0 };
|
|
42
|
+
if (v.format !== "bigsteele-prospect-verdicts/1")
|
|
43
|
+
problems.push(`format is "${String(v.format)}", expected "bigsteele-prospect-verdicts/1"`);
|
|
44
|
+
const evidence = new Map((v.evidence ?? []).map((e) => [e.id, e]));
|
|
45
|
+
for (const e of v.evidence ?? []) {
|
|
46
|
+
if (!/^EV-\d+$/.test(e.id))
|
|
47
|
+
problems.push(`evidence "${e.id}" is not named EV-<n>`);
|
|
48
|
+
if (!e.where || !e.observed)
|
|
49
|
+
problems.push(`evidence ${e.id} lacks where or observed`);
|
|
50
|
+
}
|
|
51
|
+
const byId = new Map();
|
|
52
|
+
for (const r of v.findings ?? []) {
|
|
53
|
+
if (byId.has(r.id))
|
|
54
|
+
problems.push(`finding ${r.id} has two verdicts`);
|
|
55
|
+
byId.set(r.id, r);
|
|
56
|
+
}
|
|
57
|
+
const ids = findingIds(p);
|
|
58
|
+
const known = new Set(ids);
|
|
59
|
+
for (const r of v.findings ?? []) {
|
|
60
|
+
if (!known.has(r.id))
|
|
61
|
+
problems.push(`verdict on "${r.id}", which the scan did not report`);
|
|
62
|
+
}
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
const r = byId.get(id);
|
|
65
|
+
counts.total++;
|
|
66
|
+
if (!r) {
|
|
67
|
+
counts.unruled++;
|
|
68
|
+
problems.push(`no verdict on ${id}: a finding nobody checked`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!VERDICTS.has(r.verdict)) {
|
|
72
|
+
problems.push(`${id}: verdict "${String(r.verdict)}" is not CONFIRMED, REFUTED, ON_RECORD or UNKNOWN`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
counts[r.verdict]++;
|
|
76
|
+
if (r.verdict === "CONFIRMED" || r.verdict === "REFUTED") {
|
|
77
|
+
if (!r.evidence?.length)
|
|
78
|
+
problems.push(`${id}: ${r.verdict} with no evidence entry`);
|
|
79
|
+
for (const ev of r.evidence ?? []) {
|
|
80
|
+
if (!evidence.has(ev))
|
|
81
|
+
problems.push(`${id}: cites ${ev}, which is not in the register`);
|
|
82
|
+
else if (reportMd && !reportMd.includes(ev))
|
|
83
|
+
problems.push(`${id}: cites ${ev}, which the report never shows`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (r.verdict === "REFUTED" && !r.reason)
|
|
87
|
+
problems.push(`${id}: REFUTED without saying what shape lied`);
|
|
88
|
+
if (r.verdict === "UNKNOWN" && !r.reason)
|
|
89
|
+
problems.push(`${id}: UNKNOWN without saying what would settle it`);
|
|
90
|
+
if (r.verdict === "ON_RECORD" && !r.record)
|
|
91
|
+
problems.push(`${id}: ON_RECORD without the deciding line`);
|
|
92
|
+
}
|
|
93
|
+
return { ok: problems.length === 0, problems, counts };
|
|
94
|
+
}
|
|
95
|
+
/** The score with refuted and recorded findings removed. UNKNOWN still counts: unproven is not innocent. */
|
|
96
|
+
export function rescore(p, v) {
|
|
97
|
+
const verdictOf = new Map((v.findings ?? []).map((r) => [r.id, r.verdict]));
|
|
98
|
+
const keep = (id) => !id || !(verdictOf.get(id) === "REFUTED" || verdictOf.get(id) === "ON_RECORD");
|
|
99
|
+
const refuted = [...verdictOf].filter(([, x]) => x === "REFUTED").map(([id]) => id);
|
|
100
|
+
const onRecord = [...verdictOf].filter(([, x]) => x === "ON_RECORD").map(([id]) => id);
|
|
101
|
+
const unknown = [...verdictOf].filter(([, x]) => x === "UNKNOWN").map(([id]) => id);
|
|
102
|
+
const deps = p.deps.map((d) => (keep(d.id) ? d : { ...d, no_reference_found: false }));
|
|
103
|
+
const unusedPaid = deps.find((d) => !d.dev && d.no_reference_found && /^(openai|stripe|twilio|resend|@sendgrid\/|@anthropic-ai\/|@clerk\/|algoliasearch|cloudinary)/.test(d.name))?.name ?? null;
|
|
104
|
+
const after = scoreProspect({
|
|
105
|
+
not_asked: p.score.not_asked,
|
|
106
|
+
deps,
|
|
107
|
+
overlaps: p.overlaps.filter((o) => keep(o.id)),
|
|
108
|
+
consolidations: p.stack.consolidations.filter((c) => keep(c.id)),
|
|
109
|
+
handrolled: p.handrolled.filter((h) => keep(h.id)),
|
|
110
|
+
duplicates: p.duplicates.filter((d) => keep(d.id)),
|
|
111
|
+
dead: p.dead.filter((d) => keep(d.id)),
|
|
112
|
+
costs: p.cost_surfaces.filter((c) => keep(c.id)),
|
|
113
|
+
runtime_files: p.totals.runtime_files,
|
|
114
|
+
unused_paid_service: unusedPaid,
|
|
115
|
+
});
|
|
116
|
+
return { before: p.score, after, dropped: { refuted, on_record: onRecord }, unknown };
|
|
117
|
+
}
|
|
118
|
+
/** The math, printed. Nothing on the page is typed by hand. */
|
|
119
|
+
export function showMath(p, v, r, c) {
|
|
120
|
+
const L = [];
|
|
121
|
+
L.push(`THE PROSPECT - recomputed from ${v.scan ?? "the scan"}${v.commit ? ` at ${v.commit}` : ""}`);
|
|
122
|
+
L.push(`findings: ${c.counts.total} CONFIRMED ${c.counts.CONFIRMED} REFUTED ${c.counts.REFUTED} ON_RECORD ${c.counts.ON_RECORD} UNKNOWN ${c.counts.UNKNOWN} unruled ${c.counts.unruled}`);
|
|
123
|
+
L.push(`evidence entries: ${(v.evidence ?? []).length}`);
|
|
124
|
+
L.push("");
|
|
125
|
+
L.push(`Step 0 score ${String(r.before.total).padStart(3)}/100 (${r.before.grade}) Level ${r.before.level.n} ${r.before.level.name}`);
|
|
126
|
+
L.push(`After verdicts ${String(r.after.total).padStart(3)}/100 (${r.after.grade}) Level ${r.after.level.n} ${r.after.level.name}`);
|
|
127
|
+
L.push("");
|
|
128
|
+
L.push("Deductions after verdicts:");
|
|
129
|
+
if (!r.after.deductions.length)
|
|
130
|
+
L.push(" none");
|
|
131
|
+
for (const d of r.after.deductions)
|
|
132
|
+
L.push(` ${String(d.points).padStart(4)} ${d.what} (${d.evidence})`);
|
|
133
|
+
for (const f of r.after.floors)
|
|
134
|
+
L.push(` floor: ${f}`);
|
|
135
|
+
if (r.dropped.refuted.length)
|
|
136
|
+
L.push("", `Refuted, cost nothing: ${r.dropped.refuted.join(", ")}`);
|
|
137
|
+
if (r.dropped.on_record.length)
|
|
138
|
+
L.push(`On record, cost nothing: ${r.dropped.on_record.join(", ")}`);
|
|
139
|
+
if (r.unknown.length)
|
|
140
|
+
L.push(`Still unknown, still charged: ${r.unknown.join(", ")}`);
|
|
141
|
+
L.push("");
|
|
142
|
+
L.push(c.ok ? "verdicts: complete" : `verdicts: ${c.problems.length} problem(s) - the number above is not final until they are fixed`);
|
|
143
|
+
return L.join("\n");
|
|
144
|
+
}
|