@xhulz/nina 0.35.0 → 0.36.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/bin/nina.mjs +4 -1
- package/package.json +1 -1
- package/releases/0.36.0/core/GAPS.md +125 -0
- package/releases/0.36.0/core/answered.json +29 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.36.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.36.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.36.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.36.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.36.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.36.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.36.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.36.0/core/tree/.claude/agents/qa.md +153 -0
- package/releases/0.36.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.36.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.36.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.36.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.36.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.36.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.36.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.36.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.36.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.36.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.36.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.36.0/core/tree/.claude/router.md +198 -0
- package/releases/0.36.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.36.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.36.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.36.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.36.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.36.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.36.0/core/vocabulary.json +13 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.36.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.36.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.36.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.36.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.36.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.36.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.36.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.36.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.36.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.36.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.36.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.36.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.36.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.36.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.36.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.36.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.36.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.36.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.36.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/commands/export.mjs +8 -16
- package/src/commands/gate.mjs +19 -13
- package/src/commands/learn.mjs +5 -15
- package/src/commands/pipeline.mjs +4 -4
- package/src/commands/runs.mjs +14 -8
- package/src/commands/snapshot.mjs +63 -64
- package/src/commands/stats.mjs +46 -28
- package/src/commands/status.mjs +2 -2
- package/src/store.mjs +158 -0
- package/src/transcripts.mjs +15 -4
package/src/commands/gate.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { join, resolve } from 'node:path';
|
|
|
21
21
|
import { HARNESS, legacyHint, slugFor } from '../paths.mjs';
|
|
22
22
|
import { GATE, hookCommand, missingWiring, shippedScripts } from '../wiring.mjs';
|
|
23
23
|
import { LEDGER_TAIL, ledgerPath, loadProject, projectGateDir, readLedger, runGate } from '../gate.mjs';
|
|
24
|
-
import {
|
|
24
|
+
import { projectRecords } from '../store.mjs';
|
|
25
25
|
import { layerRootFor } from './compose.mjs';
|
|
26
26
|
import { handsToModel } from '../detectors.mjs';
|
|
27
27
|
|
|
@@ -102,7 +102,8 @@ const LIVE_SAMPLE = 3;
|
|
|
102
102
|
*
|
|
103
103
|
* @param {string} target - The project directory.
|
|
104
104
|
* @param {{graph: {stages: Set<string>}, tokens: Map<string, Set<string>>}} project - Its composed graph.
|
|
105
|
-
* @returns {string|
|
|
105
|
+
* @returns {{problem: string}|{compared: number}|{unasked: string}} What the comparison found, how many reports it
|
|
106
|
+
* compared, or why there was nothing to compare yet — which the selftest used to report as a comparison that passed.
|
|
106
107
|
*/
|
|
107
108
|
function liveness(target, project) {
|
|
108
109
|
const dir = projectGateDir(target);
|
|
@@ -110,10 +111,10 @@ function liveness(target, project) {
|
|
|
110
111
|
try {
|
|
111
112
|
since = readFileSync(join(dir, SINCE), 'utf8').trim();
|
|
112
113
|
} catch {
|
|
113
|
-
return
|
|
114
|
+
return { unasked: 'no session has been measured since the gate was first asked' };
|
|
114
115
|
}
|
|
115
116
|
const now = Date.now();
|
|
116
|
-
const reports = (
|
|
117
|
+
const reports = (projectRecords(target) ?? []).filter(
|
|
117
118
|
(r) =>
|
|
118
119
|
r.verdict_source === 'handback' &&
|
|
119
120
|
r.session &&
|
|
@@ -128,18 +129,20 @@ function liveness(target, project) {
|
|
|
128
129
|
.filter((rs) => rs.length >= LIVE_SAMPLE)
|
|
129
130
|
.sort((a, b) => String(a.map((r) => r.result_ts).sort().at(-1)).localeCompare(String(b.map((r) => r.result_ts).sort().at(-1))))
|
|
130
131
|
.at(-1);
|
|
131
|
-
if (!last) return
|
|
132
|
+
if (!last) return { unasked: `no session since then holds ${LIVE_SAMPLE} reports with a verdict line to compare` };
|
|
132
133
|
const path = ledgerPath(target, last[0].session);
|
|
133
134
|
const entries = readLedger(path);
|
|
134
135
|
// A ledger too large to read whole is read from its tail; what came before the tail is not asked about.
|
|
135
136
|
const from = entries.length > 0 && statSync(path).size > LEDGER_TAIL ? String(entries[0].at) : '';
|
|
136
137
|
const shown = last.filter((r) => String(r.result_ts) >= from).length;
|
|
137
138
|
const recorded = entries.filter((e) => e.k === 'verdict').length;
|
|
138
|
-
if (shown < LIVE_SAMPLE
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
if (shown < LIVE_SAMPLE) return { unasked: `the newest measured session holds ${shown} report(s) with a verdict line, too few to compare` };
|
|
140
|
+
if (recorded * 2 >= shown) return { compared: shown };
|
|
141
|
+
return {
|
|
142
|
+
problem:
|
|
143
|
+
`in session ${last[0].session.slice(0, 8)}, the transcripts show ${shown} report(s) with a verdict line and the gate recorded ${recorded}: ` +
|
|
144
|
+
`Claude Code may have changed what it sends to the hooks, and a loop the gate does not see is not capped (${path})`,
|
|
145
|
+
};
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
/**
|
|
@@ -299,11 +302,14 @@ export async function gate(argv, ctx) {
|
|
|
299
302
|
// A gate that is not wired sees nothing, and says so above; asked whether it saw the pipeline, it would
|
|
300
303
|
// say the same thing twice.
|
|
301
304
|
const project = composed && unwired.length === 0 ? loadProject(target) : null;
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
305
|
+
const seen = project ? liveness(target, project) : null;
|
|
306
|
+
if (seen?.problem) problems.push(seen.problem);
|
|
304
307
|
|
|
305
308
|
if (problems.length === 0) {
|
|
306
|
-
|
|
309
|
+
const compared = seen?.compared
|
|
310
|
+
? `and it recorded the ${seen.compared} reports the transcripts show`
|
|
311
|
+
: `and whether it records what the transcripts show is not asked yet — ${seen?.unasked ?? 'nothing to compare'}`;
|
|
312
|
+
console.log(` wired, writable, no new failure, a dry run through the hook command sent the round past a cap to the owner, ${compared} — ledgers in ${projectGateDir(target)}`);
|
|
307
313
|
console.log('gate: current');
|
|
308
314
|
return 0;
|
|
309
315
|
}
|
package/src/commands/learn.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
26
26
|
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
27
27
|
import { join, relative, resolve } from 'node:path';
|
|
28
28
|
import { HARNESS, legacyHint, slugFor, snapshotsDir } from '../paths.mjs';
|
|
29
|
+
import { localDay, projectRecords } from '../store.mjs';
|
|
29
30
|
import { isLoopBack, runOf } from '../transcripts.mjs';
|
|
30
31
|
import { REQUIRES, byVersion, layerRootFor } from './compose.mjs';
|
|
31
32
|
import { GRADUATION_AT, frontmatter, graduationTarget, list, pillFiles, roleGates, tidyRetired } from './pills.mjs';
|
|
@@ -52,7 +53,8 @@ const VERIFY_MIN = 5;
|
|
|
52
53
|
export { slugFor };
|
|
53
54
|
|
|
54
55
|
/** The day part of a timestamp or a date. */
|
|
55
|
-
|
|
56
|
+
/** The owner's day a moment fell on: a pill's date is written in it. */
|
|
57
|
+
const day = localDay;
|
|
56
58
|
|
|
57
59
|
/** A verdict the parser actually read, as opposed to one it could not find. */
|
|
58
60
|
const readable = (v) => Boolean(v) && v !== 'UNCLEAR' && v !== 'NONE';
|
|
@@ -64,19 +66,7 @@ const readable = (v) => Boolean(v) && v !== 'UNCLEAR' && v !== 'NONE';
|
|
|
64
66
|
* @returns {Promise<object[]>}
|
|
65
67
|
*/
|
|
66
68
|
async function recorded(target) {
|
|
67
|
-
|
|
68
|
-
const text = await readFile(file, 'utf8').catch(() => '');
|
|
69
|
-
return text
|
|
70
|
-
.split('\n')
|
|
71
|
-
.filter((l) => l.trim())
|
|
72
|
-
.map((l) => {
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(l);
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
.filter(Boolean);
|
|
69
|
+
return projectRecords(target) ?? [];
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
/**
|
|
@@ -486,7 +476,7 @@ async function deepReport(target, records, known, argv) {
|
|
|
486
476
|
*/
|
|
487
477
|
async function fileRequest(target, profile, layerRoot, lesson) {
|
|
488
478
|
const target_ = graduationTarget(lesson.roles, await roleGates(layerRoot));
|
|
489
|
-
const date = new Date().toISOString()
|
|
479
|
+
const date = localDay(new Date().toISOString());
|
|
490
480
|
// The directory separator becomes `__`, so `reviewer/a.b.md` and `reviewer-a/b.md` stay two
|
|
491
481
|
// names — flattening both `/` and `.` to `-` gave them the same one.
|
|
492
482
|
const name = lesson.rel.replace(/^\.claude\/pills\//, '').replace(/\.md$/, '').replace(/\//g, '__');
|
|
@@ -18,7 +18,7 @@ import { TERMINALS, parseGraph } from '../graph.mjs';
|
|
|
18
18
|
import { required } from '../tools.mjs';
|
|
19
19
|
import { isLoopBack, runOf } from '../transcripts.mjs';
|
|
20
20
|
import { frontmatter } from './pills.mjs';
|
|
21
|
-
import {
|
|
21
|
+
import { localDay, projectRecords } from '../store.mjs';
|
|
22
22
|
|
|
23
23
|
/** A stage line under "## Stages": "- `name` — what it does". */
|
|
24
24
|
const STAGE_LINE = /^- `([a-z][a-z-]*)` — (.*)$/;
|
|
@@ -149,7 +149,7 @@ export function historyOf(records, since) {
|
|
|
149
149
|
const byRole = new Map();
|
|
150
150
|
const seen = new Map();
|
|
151
151
|
for (const r of records) {
|
|
152
|
-
if (!r.role || r.status === 'denied' ||
|
|
152
|
+
if (!r.role || r.status === 'denied' || localDay(r.ts) < since) continue;
|
|
153
153
|
const s = byRole.get(r.role) ?? { runs: 0, rounds: 0, read: 0, back: 0 };
|
|
154
154
|
const runs = seen.get(r.role) ?? new Set();
|
|
155
155
|
runs.add(runOf(r));
|
|
@@ -288,8 +288,8 @@ export async function pipeline(argv) {
|
|
|
288
288
|
for (const file of existsSync(agentsDir) ? readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort() : []) {
|
|
289
289
|
specs.set(file.replace(/\.md$/, ''), readFileSync(join(agentsDir, file), 'utf8'));
|
|
290
290
|
}
|
|
291
|
-
const since = arg('--since') || new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString()
|
|
292
|
-
const runs =
|
|
291
|
+
const since = arg('--since') || localDay(new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString());
|
|
292
|
+
const runs = projectRecords(dir);
|
|
293
293
|
const history = runs ? historyOf(runs, since) : null;
|
|
294
294
|
const stages = [...graph.stages].filter((s) => covered.has(s));
|
|
295
295
|
const col = Math.max(...stages.map((s) => s.length), 10) + 2;
|
package/src/commands/runs.mjs
CHANGED
|
@@ -16,8 +16,8 @@ import { HARNESS } from '../paths.mjs';
|
|
|
16
16
|
import { costOf } from '../prices.mjs';
|
|
17
17
|
import { isLoopBack, runOf } from '../transcripts.mjs';
|
|
18
18
|
import { amber, bold, dim, pink } from '../look.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import { cyclesOf, projectName } from './stats.mjs';
|
|
19
|
+
import { localDay, localMinute, projectRecords, zone } from '../store.mjs';
|
|
20
|
+
import { WRITERS, cyclesOf, projectName } from './stats.mjs';
|
|
21
21
|
|
|
22
22
|
/** What ended a cycle, in words. */
|
|
23
23
|
const ENDS = {
|
|
@@ -55,7 +55,8 @@ export function summarize(cycle) {
|
|
|
55
55
|
if (isLoopBack(r.verdict)) s.back += 1;
|
|
56
56
|
stages.set(r.role, s);
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
// The code a cycle wrote, as `stats` counts it: an architect's spec or a tester's report is not a write of code.
|
|
59
|
+
const files = rounds.filter((r) => WRITERS.has(r.role) && typeof r.files_touched === 'number').map((r) => r.files_touched);
|
|
59
60
|
const peaks = rounds.filter((r) => typeof r.context_peak === 'number').map((r) => r.context_peak);
|
|
60
61
|
return {
|
|
61
62
|
start,
|
|
@@ -92,9 +93,14 @@ export async function runs(argv) {
|
|
|
92
93
|
}
|
|
93
94
|
const since = arg('--since');
|
|
94
95
|
const last = Math.max(1, Number(arg('--last') ?? 10) || 10);
|
|
95
|
-
const
|
|
96
|
+
const stored = (projectRecords(dir) ?? []).filter((r) => r.status !== 'denied');
|
|
97
|
+
const records = stored.filter((r) => !since || localDay(r.ts) >= since);
|
|
96
98
|
if (records.length === 0) {
|
|
97
|
-
console.error(
|
|
99
|
+
console.error(
|
|
100
|
+
stored.length === 0
|
|
101
|
+
? ' no measured history for this project yet — `nina snapshot` captures it\n'
|
|
102
|
+
: ` nothing measured since ${since} (${zone()}) — the newest round is from ${localDay(stored.map((r) => String(r.ts)).sort().at(-1))}\n`,
|
|
103
|
+
);
|
|
98
104
|
return 1;
|
|
99
105
|
}
|
|
100
106
|
const cycles = cyclesOf(records)
|
|
@@ -103,10 +109,10 @@ export async function runs(argv) {
|
|
|
103
109
|
const shown = cycles.slice(0, last);
|
|
104
110
|
console.log(
|
|
105
111
|
` ${bold(pink(projectName(records[0].project)))} · ${cycles.length} cycle(s)` +
|
|
106
|
-
dim(` newest first${cycles.length > shown.length ? `, the last ${shown.length} — --last N for more` : ''}; a cycle runs up to the verdict that closes it`),
|
|
112
|
+
dim(` newest first${cycles.length > shown.length ? `, the last ${shown.length} — --last N for more` : ''}; a cycle runs up to the verdict that closes it; times in ${zone()}`),
|
|
107
113
|
);
|
|
108
114
|
for (const c of shown) {
|
|
109
|
-
const when =
|
|
115
|
+
const when = localMinute(c.start);
|
|
110
116
|
console.log(`\n ${pink('▌')} ${bold(when)} · ${span(c.minutes)} · ${bold(money(c.cost))}${c.unpriced ? dim(` + ${c.unpriced} unpriced`) : ''} · ${dim(c.how)}`);
|
|
111
117
|
console.log(` ${c.name.length > 96 ? `${c.name.slice(0, 95)}…` : c.name}`);
|
|
112
118
|
const chain = c.stages.map((s) => {
|
|
@@ -122,7 +128,7 @@ export async function runs(argv) {
|
|
|
122
128
|
const costliest = shown.reduce((a, c) => (c.cost > a.cost ? c : a), shown[0]);
|
|
123
129
|
console.log(
|
|
124
130
|
`\n ${shown.length} cycle(s): ${bold(money(total))} in all` +
|
|
125
|
-
(shown.length > 1 ? `; the costliest, ${money(costliest.cost)}, ${costliest.start
|
|
131
|
+
(shown.length > 1 ? `; the costliest, ${money(costliest.cost)}, ${localDay(costliest.start)} — ${costliest.name.slice(0, 60)}` : '') +
|
|
126
132
|
'\n',
|
|
127
133
|
);
|
|
128
134
|
return 0;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { copyFile, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { HARNESS, snapshotsDir } from '../paths.mjs';
|
|
13
|
+
import { decodeProjectDir } from './stats.mjs';
|
|
14
|
+
import { readStoreFile } from '../store.mjs';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
16
|
import { join, resolve } from 'node:path';
|
|
16
17
|
import { listProjects, scanProject } from '../transcripts.mjs';
|
|
17
18
|
|
|
@@ -21,7 +22,8 @@ const STATE_FILE = '.state.json';
|
|
|
21
22
|
/**
|
|
22
23
|
* Reads a JSON file, or an empty object when it is missing or unreadable. A state file cut short
|
|
23
24
|
* by an interrupted write used to throw on every later snapshot, and observation stopped for every
|
|
24
|
-
* project on the machine with nothing saying so. An unreadable cursor costs one re-read
|
|
25
|
+
* project on the machine with nothing saying so. An unreadable cursor costs one re-read, which keeps what the
|
|
26
|
+
* store already holds (`snapshotProject`).
|
|
25
27
|
*
|
|
26
28
|
* @param {string} path - The file.
|
|
27
29
|
* @returns {Promise<object>}
|
|
@@ -42,35 +44,10 @@ async function readJson(path) {
|
|
|
42
44
|
*/
|
|
43
45
|
async function writeAtomicText(path, text) {
|
|
44
46
|
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
45
|
-
await writeFile(tmp, text);
|
|
47
|
+
await writeFile(tmp, text, { mode: 0o600 });
|
|
46
48
|
await rename(tmp, path);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
50
|
-
* Reads the records already captured for a project.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} file - Path to the snapshot JSONL.
|
|
53
|
-
* @returns {Promise<object[]>} The records, or an empty list.
|
|
54
|
-
*/
|
|
55
|
-
async function readRecords(file) {
|
|
56
|
-
const records = [];
|
|
57
|
-
try {
|
|
58
|
-
await readFile(file, { flag: 'r' });
|
|
59
|
-
} catch {
|
|
60
|
-
return records;
|
|
61
|
-
}
|
|
62
|
-
const rl = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
63
|
-
for await (const line of rl) {
|
|
64
|
-
if (!line.trim()) continue;
|
|
65
|
-
try {
|
|
66
|
-
records.push(JSON.parse(line));
|
|
67
|
-
} catch {
|
|
68
|
-
/* a torn line loses one record, never the file */
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return records;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
51
|
/**
|
|
75
52
|
* Writes the snapshot through a temp file, so an interrupted run cannot leave a
|
|
76
53
|
* truncated one behind.
|
|
@@ -82,41 +59,11 @@ async function writeAtomic(file, records) {
|
|
|
82
59
|
// Unique per writer: two snapshots of one project at once used the same temporary name, and
|
|
83
60
|
// could interleave into it before either renamed it into place.
|
|
84
61
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
85
|
-
|
|
62
|
+
// Its owner's alone: it holds each dispatch's description, which the orchestrator wrote.
|
|
63
|
+
await writeFile(tmp, `${records.map((r) => JSON.stringify(r)).join('\n')}\n`, { mode: 0o600 });
|
|
86
64
|
await rename(tmp, file);
|
|
87
65
|
}
|
|
88
66
|
|
|
89
|
-
/**
|
|
90
|
-
* A project's records in the measurement store, under the name Claude Code gives its directory — as
|
|
91
|
-
* given, or with its links resolved, since either may be the one the sessions ran in.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} dir - The project.
|
|
94
|
-
* @returns {object[]|null} Null when the store has nothing for it.
|
|
95
|
-
*/
|
|
96
|
-
export function storedRecords(dir) {
|
|
97
|
-
const names = new Set([slugFor(dir)]);
|
|
98
|
-
try {
|
|
99
|
-
names.add(slugFor(realpathSync(dir)));
|
|
100
|
-
} catch {
|
|
101
|
-
// A directory that cannot be resolved is looked up as given.
|
|
102
|
-
}
|
|
103
|
-
for (const name of names) {
|
|
104
|
-
const file = join(snapshotsDir(), `${name}.jsonl`);
|
|
105
|
-
if (!existsSync(file)) continue;
|
|
106
|
-
return readFileSync(file, 'utf8')
|
|
107
|
-
.split('\n')
|
|
108
|
-
.filter(Boolean)
|
|
109
|
-
.flatMap((l) => {
|
|
110
|
-
try {
|
|
111
|
-
return [JSON.parse(l)];
|
|
112
|
-
} catch {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
67
|
/**
|
|
121
68
|
* Takes a project's lock, or says it is taken. The records and the cursors are written one after the other,
|
|
122
69
|
* and two snapshots of one project at once — a project's own detector and a global hook, both on every
|
|
@@ -204,6 +151,42 @@ export async function snapshot(argv, ctx) {
|
|
|
204
151
|
return 0;
|
|
205
152
|
}
|
|
206
153
|
|
|
154
|
+
/** Whether a round's verdict came from the report it handed back: the one reading nothing later corrects. */
|
|
155
|
+
const settledByHandback = (r) => r?.verdict_source === 'handback';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A round as a re-read after lost cursors leaves it. What the store held stands, field by field, and the
|
|
159
|
+
* re-read fills only what it lacked — unless the re-read settled the round from the report its stage handed
|
|
160
|
+
* back and the store had not: a verdict read from a notification is the one a handback corrects, and kept
|
|
161
|
+
* over it, a round the store had as REJECTED stayed so after its stage had said APPROVED, and stayed so for
|
|
162
|
+
* good once that stage's transcript was pruned.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} held - The round as the store held it.
|
|
165
|
+
* @param {object} reread - The round as the re-read found it.
|
|
166
|
+
* @returns {object}
|
|
167
|
+
*/
|
|
168
|
+
export function rereadOver(held, reread) {
|
|
169
|
+
const defined = (record) => Object.fromEntries(Object.entries(record).filter(([, v]) => v !== null && v !== undefined));
|
|
170
|
+
return settledByHandback(reread) && !settledByHandback(held) ? { ...held, ...defined(reread) } : { ...reread, ...defined(held) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The release a project pins now, or null when its directory or profile cannot be found.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} slug - Its store name.
|
|
177
|
+
* @returns {string|null}
|
|
178
|
+
*/
|
|
179
|
+
function pinOf(slug) {
|
|
180
|
+
const dir = decodeProjectDir(slug);
|
|
181
|
+
if (!dir) return null;
|
|
182
|
+
try {
|
|
183
|
+
const core = JSON.parse(readFileSync(join(dir, HARNESS, 'profile.json'), 'utf8')).core;
|
|
184
|
+
return typeof core === 'string' ? core : null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
207
190
|
/**
|
|
208
191
|
* Snapshots one project, under its lock.
|
|
209
192
|
*
|
|
@@ -214,12 +197,28 @@ async function snapshotProject(project, { outDir, rebuild, quiet, legacy }) {
|
|
|
214
197
|
// The store is the only copy of history older than the transcripts, and a rebuild re-reads only what is
|
|
215
198
|
// still on disk: the record it replaces is kept beside it first.
|
|
216
199
|
if (rebuild && existsSync(file)) await copyFile(file, `${file}.before-rebuild-${new Date().toISOString().replace(/[:.]/g, '-')}`);
|
|
217
|
-
const
|
|
200
|
+
const stored = readStoreFile(file, { raw: true });
|
|
201
|
+
const prior = rebuild ? [] : stored;
|
|
218
202
|
const own = rebuild ? {} : await readJson(join(outDir, `${project.slug}.state.json`));
|
|
219
203
|
const cursors = rebuild ? {} : (own.cursors ?? legacy[project.slug]?.cursors ?? {});
|
|
220
204
|
|
|
221
205
|
const scanned = await scanProject(project.dir, { cursors, records: prior });
|
|
222
|
-
|
|
206
|
+
// With its cursors gone — a state file deleted, or cut short — the transcripts are read again from the start
|
|
207
|
+
// over records the store already holds, and read that way a notification is taken a second time: one
|
|
208
|
+
// project had 34 verdicts, 639 return times and 79 durations rewritten. What the store held stands, field
|
|
209
|
+
// by field, and the re-read adds only what it lacked; the cursors it leaves hold from then on.
|
|
210
|
+
const reread = !rebuild && prior.length > 0 && Object.keys(cursors).length === 0;
|
|
211
|
+
const held = new Map(prior.map((r) => [r.dispatch_id, r]));
|
|
212
|
+
const kept = (r) => (reread && held.has(r.dispatch_id) ? rereadOver(held.get(r.dispatch_id), r) : r);
|
|
213
|
+
// The release the project pinned when a round was first captured, so a rule is judged only on the rounds
|
|
214
|
+
// that ran under it. A round already on record keeps what it had — a rebuild included, which would
|
|
215
|
+
// otherwise stamp today's pin on weeks of history — and only a new one takes today's.
|
|
216
|
+
const pinned = new Map(stored.map((r) => [r.dispatch_id, r.core ?? null]));
|
|
217
|
+
const pin = pinOf(project.slug);
|
|
218
|
+
const records = scanned.records.map(kept).map((r) => {
|
|
219
|
+
const core = pinned.has(r.dispatch_id) ? pinned.get(r.dispatch_id) : pin;
|
|
220
|
+
return { ...r, project: project.slug, ...(core ? { core } : {}) };
|
|
221
|
+
});
|
|
223
222
|
if (records.length === 0) return 0;
|
|
224
223
|
|
|
225
224
|
const fresh = records.length - prior.length;
|
package/src/commands/stats.mjs
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
import { amber, bar, bold, dim, heading, note, pink, stacked } from '../look.mjs';
|
|
15
15
|
import { readFile, readdir } from 'node:fs/promises';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import { closeSync,
|
|
18
|
-
import { createInterface } from 'node:readline';
|
|
17
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
19
18
|
import { homedir } from 'node:os';
|
|
20
19
|
import { basename, join } from 'node:path';
|
|
21
20
|
import { ROLE_TOKENS, isLoopBack, runOf, transcriptsOf } from '../transcripts.mjs';
|
|
21
|
+
import { localDay, namesFor, readStoreFile } from '../store.mjs';
|
|
22
22
|
import { frontmatter, pillFiles } from './pills.mjs';
|
|
23
23
|
import { HARNESS, slugFor, snapshotsDir } from '../paths.mjs';
|
|
24
24
|
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
25
|
-
import { layerRootFor } from './compose.mjs';
|
|
25
|
+
import { byVersion, layerRootFor } from './compose.mjs';
|
|
26
26
|
import { PRICES_AS_OF, costOf } from '../prices.mjs';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -56,27 +56,16 @@ const PIPELINE_ROLES = new Set(Object.keys(ROLE_TOKENS));
|
|
|
56
56
|
* Loads snapshot records, optionally filtered.
|
|
57
57
|
*
|
|
58
58
|
* @param {string} dir - Snapshot directory.
|
|
59
|
-
* @param {{since: string|null, project: string|null}} opts - Filters.
|
|
59
|
+
* @param {{since: string|null, project: string|null}} opts - Filters; `project` as every report reads it.
|
|
60
60
|
* @returns {Promise<object[]>} The matching records.
|
|
61
61
|
*/
|
|
62
62
|
async function load(dir, opts) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (!line.trim()) continue;
|
|
70
|
-
try {
|
|
71
|
-
const r = JSON.parse(line);
|
|
72
|
-
if (opts.since && String(r.ts).slice(0, 10) < opts.since) continue;
|
|
73
|
-
out.push(r);
|
|
74
|
-
} catch {
|
|
75
|
-
/* skip */
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
63
|
+
const names = opts.project
|
|
64
|
+
? namesFor(opts.project, dir)
|
|
65
|
+
: (await readdir(dir).catch(() => [])).filter((f) => f.endsWith('.jsonl')).map((f) => f.slice(0, -'.jsonl'.length));
|
|
66
|
+
return names
|
|
67
|
+
.flatMap((name) => readStoreFile(join(dir, `${name}.jsonl`)))
|
|
68
|
+
.filter((r) => !opts.since || localDay(r.ts) >= opts.since);
|
|
80
69
|
}
|
|
81
70
|
|
|
82
71
|
|
|
@@ -385,7 +374,7 @@ function modelReport(records) {
|
|
|
385
374
|
.map((rs) => rs.map((r) => (r.tokens ? costOf(r.tokens, r.usage_model) : null)).filter((c) => c !== null))
|
|
386
375
|
.filter((cs) => cs.length > 0)
|
|
387
376
|
.map((cs) => cs.reduce((a, b) => a + b, 0));
|
|
388
|
-
const dates = rounds.map((r) =>
|
|
377
|
+
const dates = rounds.map((r) => localDay(r.ts)).sort();
|
|
389
378
|
lines.push(
|
|
390
379
|
` ${i === 0 ? `${pink(role)}${' '.repeat(Math.max(20 - role.length, 1))}` : ''.padEnd(20)}${model.padEnd(26)}${String(runs.size).padStart(5)} run(s) ${dim(`${dates[0]} → ${dates.at(-1)}`)}` +
|
|
391
380
|
(clear.length ? ` loop-back ${pct(loops, clear.length)} of ${clear.length}` : ' no readable verdict') +
|
|
@@ -427,8 +416,25 @@ function modelReport(records) {
|
|
|
427
416
|
/** The verdicts that close a pipeline cycle: the change passed its tests, reached preview, or cleared the audit. */
|
|
428
417
|
const CYCLE_ENDS = { qa: 'PASS', devops: 'DEPLOYED', secops: 'SECURE' };
|
|
429
418
|
|
|
419
|
+
/**
|
|
420
|
+
* The release each rule this report judges first shipped in. A round is judged on a rule only when the pin it
|
|
421
|
+
* was captured under (`core`) carries it: over every round, "0 of 16 loop-backs named their issues" read
|
|
422
|
+
* reports written before the rule existed as breaking it. Releases are never rewritten, so neither is this.
|
|
423
|
+
*/
|
|
424
|
+
const RULE_SINCE = { issues: '0.22.0', stepFiles: '0.28.8' };
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Whether a round ran under a release that carries a rule. One captured before rounds recorded their pin
|
|
428
|
+
* cannot say, and is left out rather than judged.
|
|
429
|
+
*
|
|
430
|
+
* @param {object} record - A round.
|
|
431
|
+
* @param {keyof RULE_SINCE} rule - The rule.
|
|
432
|
+
* @returns {boolean}
|
|
433
|
+
*/
|
|
434
|
+
export const ranUnder = (record, rule) => typeof record.core === 'string' && (record.core === 'dev' || byVersion(record.core, RULE_SINCE[rule]) >= 0);
|
|
435
|
+
|
|
430
436
|
/** The stages that write code, whose runs' file counts size a cycle. */
|
|
431
|
-
const WRITERS = new Set(['implementer', 'solidity-dev']);
|
|
437
|
+
export const WRITERS = new Set(['implementer', 'solidity-dev']);
|
|
432
438
|
|
|
433
439
|
/** The stages whose presence makes a cycle a designed one. */
|
|
434
440
|
const DESIGNERS = new Set(['planner', 'architect']);
|
|
@@ -539,7 +545,7 @@ function proportionReport(records, stepLimit = () => null) {
|
|
|
539
545
|
// cache read per file held near 1M up to 19 files and was 6.9M in a 52-file run. Asked only of a project
|
|
540
546
|
// whose pinned release states the limit.
|
|
541
547
|
const over = records.filter((r) => {
|
|
542
|
-
if (r.role !== 'implementer' || typeof r.files_touched !== 'number') return false;
|
|
548
|
+
if (r.role !== 'implementer' || typeof r.files_touched !== 'number' || !ranUnder(r, 'stepFiles')) return false;
|
|
543
549
|
const limit = stepLimit(r.project);
|
|
544
550
|
return limit !== null && r.files_touched > limit;
|
|
545
551
|
});
|
|
@@ -672,7 +678,14 @@ export async function stats(argv, ctx) {
|
|
|
672
678
|
// A dispatch a hook denied never ran, so it is neither a run nor a missing verdict.
|
|
673
679
|
const held = records.filter((r) => r.status === 'denied').length;
|
|
674
680
|
const now = Date.now();
|
|
681
|
+
// Agents outside the pipeline — Claude Code's own, a project's own — have no verdict to give. Read as
|
|
682
|
+
// stages they were 100% unreadable and were told to add a verdict token their spec never asked for.
|
|
683
|
+
const outside = new Map();
|
|
675
684
|
for (const r of records.filter((r) => r.status !== 'denied')) {
|
|
685
|
+
if (!PIPELINE_ROLES.has(r.role)) {
|
|
686
|
+
outside.set(r.role, (outside.get(r.role) ?? 0) + 1);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
676
689
|
if (!byRole.has(r.role)) {
|
|
677
690
|
byRole.set(r.role, { n: 0, runs: new Set(), done: 0, lost: 0, clear: 0, loop: 0, unclear: 0, declared: 0, durations: [] });
|
|
678
691
|
}
|
|
@@ -690,7 +703,7 @@ export async function stats(argv, ctx) {
|
|
|
690
703
|
if (typeof r.duration_s === 'number') s.durations.push(r.duration_s);
|
|
691
704
|
}
|
|
692
705
|
|
|
693
|
-
const span = [records[0]?.ts, records.at(-1)?.ts].map(
|
|
706
|
+
const span = [records[0]?.ts, records.at(-1)?.ts].map(localDay);
|
|
694
707
|
const projectCount = new Set(records.map((r) => r.project)).size;
|
|
695
708
|
const run = byRun(records.filter((r) => r.status !== 'denied')).size;
|
|
696
709
|
console.log(
|
|
@@ -738,9 +751,14 @@ export async function stats(argv, ctx) {
|
|
|
738
751
|
const lost = rows.reduce((a, [, s]) => a + s.lost, 0);
|
|
739
752
|
const done = rows.reduce((a, [, s]) => a + s.done, 0);
|
|
740
753
|
console.log('');
|
|
754
|
+
if (outside.size > 0) {
|
|
755
|
+
const named = [...outside.entries()].sort((a, b) => b[1] - a[1]).map(([role, n]) => `${role} ×${n}`).join(', ');
|
|
756
|
+
console.log(note(`${[...outside.values()].reduce((a, b) => a + b, 0)} round(s) by agents outside the pipeline (${named}) — not stages, so no verdict is asked of them.`, 'info'));
|
|
757
|
+
}
|
|
741
758
|
if (unreadable === 0 && lost === 0) console.log(note("every finished round's verdict could be read.", 'ok'));
|
|
759
|
+
// The same share as the table's column: rounds that finished, or should have, with no verdict read.
|
|
742
760
|
if (unreadable > 0) {
|
|
743
|
-
console.log(note(`${pct(unreadable, done + lost)} of finished rounds
|
|
761
|
+
console.log(note(`${pct(unreadable + lost, done + lost)} of finished rounds have no verdict that can be read. Those stages need a verdict token on the report's first line.`, 'warn'));
|
|
744
762
|
}
|
|
745
763
|
if (lost > 0) {
|
|
746
764
|
console.log(
|
|
@@ -754,7 +772,7 @@ export async function stats(argv, ctx) {
|
|
|
754
772
|
|
|
755
773
|
// Whether the stages that send work back say what they send back. Counted only over reports read
|
|
756
774
|
// since the record learned the field, and only over declared loop-backs, which are all it asks of.
|
|
757
|
-
const named = records.filter((r) => typeof r.issues === 'number');
|
|
775
|
+
const named = records.filter((r) => typeof r.issues === 'number' && ranUnder(r, 'issues'));
|
|
758
776
|
if (named.length > 0) {
|
|
759
777
|
const withIds = named.filter((r) => r.issues > 0).length;
|
|
760
778
|
console.log(
|
|
@@ -838,7 +856,7 @@ export async function stats(argv, ctx) {
|
|
|
838
856
|
: note(`${pills.retired} of ${pills.total} pill(s) retired into a rule.`, 'ok'),
|
|
839
857
|
);
|
|
840
858
|
if (newest) {
|
|
841
|
-
const since = records.filter((r) =>
|
|
859
|
+
const since = records.filter((r) => localDay(r.ts) > newest).length;
|
|
842
860
|
if (since > 0) console.log(note(`${since} round(s) since the newest pill (${newest}).`));
|
|
843
861
|
}
|
|
844
862
|
}
|
package/src/commands/status.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { amber, bold, dim, green, pink } from '../look.mjs';
|
|
|
22
22
|
import { byVersion, layerRootFor } from './compose.mjs';
|
|
23
23
|
import { filedRequests } from './learn.mjs';
|
|
24
24
|
import { frontmatter, pillFiles } from './pills.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import { projectRecords } from '../store.mjs';
|
|
26
26
|
|
|
27
27
|
/** How long the registry gets to say which version is newest; a status is not worth waiting on. */
|
|
28
28
|
const REGISTRY_MS = 3000;
|
|
@@ -123,7 +123,7 @@ export async function status(argv, ctx) {
|
|
|
123
123
|
const check = verdictOf(bin, ['check', '--project', target]);
|
|
124
124
|
row('check', check.ok ? green(check.line) : warn(`${check.line} — \`nina check\` lists them`));
|
|
125
125
|
|
|
126
|
-
const records =
|
|
126
|
+
const records = projectRecords(target) ?? [];
|
|
127
127
|
if (records.length === 0) {
|
|
128
128
|
row('measured', dim('nothing yet — `nina snapshot` captures it'));
|
|
129
129
|
} else {
|