@erclx/aitk 0.109.0 → 0.110.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.
@@ -0,0 +1,342 @@
1
+ import { execa } from 'execa'
2
+ import type { Command } from 'commander'
3
+ import {
4
+ BASELINE_REL,
5
+ type Baseline,
6
+ baselineFrom,
7
+ compareBaseline,
8
+ type Delta,
9
+ readBaseline,
10
+ writeBaseline,
11
+ } from '@/audits/baseline'
12
+ import { AUDITS, type AuditResult } from '@/audits/catalog'
13
+ import {
14
+ exitCodeFor,
15
+ runAudits,
16
+ spawnAudit,
17
+ type Summary,
18
+ summarize,
19
+ } from '@/audits/run'
20
+ import { gitEnv } from '@/git-env'
21
+ import { intro, logError, logInfo, logStep, logWarn, outro, plural } from '@/ui'
22
+ import { currentWorktreeRoot } from '@/worktree'
23
+
24
+ interface RunCommandOptions {
25
+ readonly json?: boolean
26
+ readonly record?: boolean
27
+ readonly root?: string
28
+ }
29
+
30
+ interface ListCommandOptions {
31
+ readonly json?: boolean
32
+ }
33
+
34
+ export function register(program: Command): void {
35
+ const audits = program
36
+ .command('audits')
37
+ .description('Run every health check this repository owns as one set')
38
+ .helpOption('-h, --help', 'Show this help message')
39
+
40
+ audits
41
+ .command('run')
42
+ .description(
43
+ 'Run every audit, report per check under one verdict, and compare each count to the recorded baseline',
44
+ )
45
+ .helpOption('-h, --help', 'Show this help message')
46
+ .option('--json', 'Add a machine-readable record on stdout')
47
+ .option(
48
+ '--root <path>',
49
+ 'Tree to measure, defaulting to the current worktree',
50
+ )
51
+ .option(
52
+ '--record',
53
+ `Write this run's tracked counts to ${BASELINE_REL} as the new baseline`,
54
+ )
55
+ .addHelpText(
56
+ 'after',
57
+ [
58
+ '',
59
+ 'Exit codes:',
60
+ ' 0 every audit reported and none carried a finding that is a fact',
61
+ ' 1 refused, with the reason on stderr',
62
+ ' 2 an audit carries a finding that is a fact',
63
+ ' 3 an audit did not report, so the run measured less than the set',
64
+ '',
65
+ 'It gates on exactly what already gates a push and on nothing new:',
66
+ 'an unresolved context citation, a banned character, word, or spelling,',
67
+ 'and a skill folder carrying no REQUIREMENT.md. Every other measure is a',
68
+ 'judgment a reader settles, and failing a push on one teaches',
69
+ 'contributors to route around the stage.',
70
+ '',
71
+ 'Exit 3 is a defect in the run rather than in the tree. An aggregate that',
72
+ 'reports a pass over a set it did not finish measuring is the failure this',
73
+ 'command exists against, so a verb that did not report takes its own code.',
74
+ '',
75
+ `The baseline at ${BASELINE_REL} holds the counts from the last recorded`,
76
+ 'run, so a measure that reports rather than gates still costs something',
77
+ 'when it grows. Only a tracked corpus is retained: a gitignored record',
78
+ "folder holds one machine's scratch, and its counts answer nobody else.",
79
+ '',
80
+ 'Examples:',
81
+ ' aitk audits run',
82
+ ' aitk audits run --json',
83
+ ' aitk audits run --record',
84
+ '',
85
+ ].join('\n'),
86
+ )
87
+ .action(async (opts: RunCommandOptions) => {
88
+ process.exitCode = await runAll(opts)
89
+ })
90
+
91
+ audits
92
+ .command('list')
93
+ .description(
94
+ 'List every audit this command runs and what each one gates on',
95
+ )
96
+ .helpOption('-h, --help', 'Show this help message')
97
+ .option('--json', 'Emit JSON with the id, invocation, corpus, and gate')
98
+ .addHelpText(
99
+ 'after',
100
+ [
101
+ '',
102
+ 'Exit codes:',
103
+ ' 0 the catalog was listed',
104
+ '',
105
+ 'Examples:',
106
+ ' aitk audits list',
107
+ ' aitk audits list --json',
108
+ '',
109
+ ].join('\n'),
110
+ )
111
+ .action((opts: ListCommandOptions) => {
112
+ process.exitCode = runList(opts)
113
+ })
114
+ }
115
+
116
+ function runList(opts: ListCommandOptions): number {
117
+ if (opts.json) {
118
+ process.stdout.write(
119
+ `${JSON.stringify({
120
+ audits: AUDITS.map((audit) => ({
121
+ id: audit.id,
122
+ label: audit.label,
123
+ command: `aitk ${audit.argv.join(' ')}`,
124
+ corpus: audit.corpus,
125
+ gates: audit.gatingExits.length > 0,
126
+ })),
127
+ })}\n`,
128
+ )
129
+ return 0
130
+ }
131
+
132
+ intro('aitk audits list')
133
+ for (const audit of AUDITS) {
134
+ logStep(audit.label)
135
+ logInfo(`aitk ${audit.argv.join(' ')}`)
136
+ logInfo(
137
+ `${audit.corpus} corpus, ${audit.gatingExits.length > 0 ? 'gates on a fact' : 'reports only'}`,
138
+ )
139
+ }
140
+ outro()
141
+ return 0
142
+ }
143
+
144
+ /**
145
+ * The day a record is stamped with, as `YYYY-MM-DD`.
146
+ *
147
+ * Local rather than UTC, because the stamp is read beside a commit date in a
148
+ * context entry and a run taken in the evening should not record tomorrow.
149
+ */
150
+ function today(): string {
151
+ const now = new Date()
152
+ return [
153
+ now.getFullYear(),
154
+ String(now.getMonth() + 1).padStart(2, '0'),
155
+ String(now.getDate()).padStart(2, '0'),
156
+ ].join('-')
157
+ }
158
+
159
+ /**
160
+ * The commit the counts were read at, or `unknown` outside a repository.
161
+ *
162
+ * A target project can install this CLI without a git history behind it, and
163
+ * refusing there would withhold the whole report over a field that only makes
164
+ * the record reproducible.
165
+ */
166
+ async function headCommit(root: string): Promise<string> {
167
+ const result = await execa('git', ['-C', root, 'rev-parse', 'HEAD'], {
168
+ reject: false,
169
+ env: gitEnv(),
170
+ extendEnv: false,
171
+ })
172
+ return result.exitCode === 0 ? result.stdout.trim() : 'unknown'
173
+ }
174
+
175
+ async function runAll(opts: RunCommandOptions): Promise<number> {
176
+ const emitJson = opts.json ?? false
177
+ const root = opts.root ?? (await currentWorktreeRoot())
178
+
179
+ let baseline: Baseline | undefined
180
+ try {
181
+ baseline = await readBaseline(root)
182
+ } catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error)
184
+ if (emitJson) {
185
+ process.stderr.write(`${message}\n`)
186
+ process.stdout.write(
187
+ `${JSON.stringify({ ok: false, reason: 'bad-baseline', message })}\n`,
188
+ )
189
+ return 1
190
+ }
191
+ intro('aitk audits run')
192
+ logStep('Refused')
193
+ logError(message)
194
+ outro()
195
+ return 1
196
+ }
197
+
198
+ const results = await runAudits(AUDITS, spawnAudit(root))
199
+ const deltas = compareBaseline(baseline, results)
200
+ const summary = summarize(results, deltas)
201
+
202
+ let recorded: string | undefined
203
+ if (opts.record) {
204
+ const next = baselineFrom(results, {
205
+ recordedAt: today(),
206
+ commit: await headCommit(root),
207
+ })
208
+ recorded = await writeBaseline(root, next)
209
+ }
210
+
211
+ if (emitJson) {
212
+ process.stdout.write(
213
+ `${JSON.stringify({
214
+ ok: true,
215
+ root,
216
+ // Flat scalars, so a shell stage greps one out without a JSON parser.
217
+ // The nested arrays below carry the detail behind each number.
218
+ summary,
219
+ baseline: baseline
220
+ ? { recordedAt: baseline.recordedAt, commit: baseline.commit }
221
+ : undefined,
222
+ recorded,
223
+ audits: results,
224
+ deltas,
225
+ })}\n`,
226
+ )
227
+ } else {
228
+ report(results, deltas, baseline, recorded, summary)
229
+ }
230
+
231
+ return exitCodeFor(results)
232
+ }
233
+
234
+ /** The counts of one result, rendered as `key n` pairs a reader can scan. */
235
+ function countLine(counts: Record<string, number>): string {
236
+ return Object.entries(counts)
237
+ .map(([key, value]) => `${key} ${value}`)
238
+ .join(', ')
239
+ }
240
+
241
+ /**
242
+ * The delta line for one audit, or nothing when there is no comparison to
243
+ * report. A first run says so rather than showing a delta of zero, since zero
244
+ * against an absent baseline says the same as a corpus that did not move.
245
+ */
246
+ function deltaLine(delta: Delta): string | undefined {
247
+ if (delta.kind === 'per-machine' || delta.kind === 'unmeasured') {
248
+ return undefined
249
+ }
250
+ if (delta.kind === 'unrecorded') return 'No recorded baseline to compare'
251
+
252
+ const parts = [
253
+ ...delta.moved.map(
254
+ ({ key, from, to, delta: moved }) =>
255
+ `${key} ${from} to ${to} (${moved > 0 ? '+' : ''}${moved})`,
256
+ ),
257
+ ...delta.added.map(({ key, to }) => `${key} ${to}, newly measured`),
258
+ ...delta.dropped.map(({ key, from }) => `${key} was ${from}, not measured`),
259
+ ]
260
+
261
+ return parts.length === 0 ? undefined : parts.join(', ')
262
+ }
263
+
264
+ function report(
265
+ results: readonly AuditResult[],
266
+ deltas: readonly Delta[],
267
+ baseline: Baseline | undefined,
268
+ recorded: string | undefined,
269
+ summary: Summary,
270
+ ): void {
271
+ const byId = new Map(deltas.map((delta) => [delta.id, delta]))
272
+
273
+ intro('aitk audits run')
274
+
275
+ logStep('Baseline')
276
+ if (baseline === undefined) {
277
+ logWarn(`None recorded. Take one with aitk audits run --record.`)
278
+ } else {
279
+ logInfo(`${baseline.recordedAt} at ${baseline.commit.slice(0, 8)}`)
280
+ }
281
+
282
+ for (const result of results) {
283
+ logStep(result.label)
284
+
285
+ if (result.status === 'absent') {
286
+ logInfo(`No corpus on this machine: ${result.reason ?? 'not found'}`)
287
+ continue
288
+ }
289
+
290
+ if (result.status === 'unmeasured') {
291
+ logWarn(`Did not report: ${result.reason ?? 'no reason given'}`)
292
+ continue
293
+ }
294
+
295
+ const counts = result.counts ?? {}
296
+ const line = countLine(counts)
297
+ if (result.status === 'finding') {
298
+ logError(line === '' ? 'A finding that is a fact' : line)
299
+ } else {
300
+ logInfo(line === '' ? 'Reported' : line)
301
+ }
302
+
303
+ if (!result.tracked) {
304
+ logInfo('Per-machine corpus, so no baseline is kept')
305
+ continue
306
+ }
307
+
308
+ const moved = deltaLine(
309
+ byId.get(result.id) ?? { id: result.id, kind: 'unrecorded' },
310
+ )
311
+ if (moved !== undefined) logInfo(moved)
312
+ }
313
+
314
+ if (recorded !== undefined) {
315
+ logStep('Recorded')
316
+ logInfo(recorded)
317
+ }
318
+
319
+ logStep('Verdict')
320
+
321
+ if (summary.verdict === 'findings') {
322
+ logError(
323
+ `${plural(summary.facts, 'audit')} carrying a finding that is a fact. Fix what each names.`,
324
+ )
325
+ } else if (summary.verdict === 'incomplete') {
326
+ logError(
327
+ `${plural(summary.unmeasured, 'audit')} did not report, so this run measured less than the set.`,
328
+ )
329
+ } else if (summary.verdict === 'reported') {
330
+ logInfo('No fact. Every other finding is a judgment a reader settles.')
331
+ } else {
332
+ logInfo('Every audit reported and every count is zero.')
333
+ }
334
+
335
+ // Stated on every run, including a clean one. A count of what passed reads as
336
+ // a verdict on the whole set unless the run also says what it never reached.
337
+ logInfo(
338
+ `${summary.audited} of ${results.length} corpora measured, ${summary.absent} absent on this machine`,
339
+ )
340
+
341
+ outro()
342
+ }