fini-proof 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/src/usage.mjs ADDED
@@ -0,0 +1,64 @@
1
+ // Local usage metering: one JSON line per run in $FINI_PROOF_HOME/usage.jsonl. Nothing leaves the machine;
2
+ // `fini-proof usage --export <file>` writes a summary the customer can send for billing / seat true-up.
3
+ // Author identities are stored as sha256 digests only (seat counting without collecting e-mail addresses).
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
+ import path from 'node:path'
6
+ import { homeDir } from './licence.mjs'
7
+ import { sha256 } from './runner.mjs'
8
+
9
+ export const usageFile = () => path.join(homeDir(), 'usage.jsonl')
10
+
11
+ export function recordRun(result, ent) {
12
+ const line = {
13
+ at: result.finishedAt,
14
+ runId: result.runId,
15
+ repo: result.repo.rootCommitDigest,
16
+ commit: result.repo.commit,
17
+ author: result.repo.author ? sha256(result.repo.author.toLowerCase()) : null,
18
+ mode: ent.mode,
19
+ org: ent.org || null,
20
+ verdict: result.verdict,
21
+ files: result.inputs.files,
22
+ findings: result.summary?.findings ?? 0,
23
+ evidenceDigest: result.evidenceDigest,
24
+ ci: !!process.env.CI,
25
+ }
26
+ mkdirSync(homeDir(), { recursive: true })
27
+ appendFileSync(usageFile(), JSON.stringify(line) + '\n')
28
+ return line
29
+ }
30
+
31
+ export function readUsage() {
32
+ if (!existsSync(usageFile())) return []
33
+ return readFileSync(usageFile(), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l))
34
+ }
35
+
36
+ export function summarise(rows, seats = null) {
37
+ const authors = new Set(rows.map((r) => r.author).filter(Boolean))
38
+ const byMonth = {}
39
+ for (const r of rows) {
40
+ const m = (r.at || '').slice(0, 7)
41
+ byMonth[m] ??= { runs: 0, repos: new Set(), authors: new Set(), verdicts: {} }
42
+ byMonth[m].runs++
43
+ byMonth[m].repos.add(r.repo)
44
+ if (r.author) byMonth[m].authors.add(r.author)
45
+ byMonth[m].verdicts[r.verdict] = (byMonth[m].verdicts[r.verdict] || 0) + 1
46
+ }
47
+ return {
48
+ schema: 'fini-proof/usage@1',
49
+ exportedAt: new Date().toISOString(),
50
+ runs: rows.length,
51
+ repos: new Set(rows.map((r) => r.repo)).size,
52
+ distinctAuthors: authors.size,
53
+ seats,
54
+ overSeats: seats ? authors.size > seats : null,
55
+ months: Object.fromEntries(Object.entries(byMonth).map(([m, v]) => [m, { runs: v.runs, repos: v.repos.size, distinctAuthors: v.authors.size, verdicts: v.verdicts }])),
56
+ digest: sha256(rows.map((r) => JSON.stringify(r)).join('\n')),
57
+ }
58
+ }
59
+
60
+ export function exportUsage(out, seats) {
61
+ const s = summarise(readUsage(), seats)
62
+ if (out) writeFileSync(out, JSON.stringify(s, null, 2) + '\n')
63
+ return s
64
+ }